from __future__ import annotations from pathlib import Path from PIL import Image, ImageDraw, ImageFont ROOT = Path("/home/nikola/codex-cli/projects/potkrovlje-dizajn-2026") OUT = ROOT / "renders" OUT.mkdir(exist_ok=True) def load_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: paths = [ "/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf", ] for path in paths: try: return ImageFont.truetype(path, size) except OSError: continue return ImageFont.load_default() TITLE = load_font(54, bold=True) SUB = load_font(28, bold=True) TEXT = load_font(24) SMALL = load_font(18) def lerp(a: tuple[int, int], b: tuple[int, int], t: float) -> tuple[int, int]: return (int(a[0] + (b[0] - a[0]) * t), int(a[1] + (b[1] - a[1]) * t)) def draw_planks(draw: ImageDraw.ImageDraw, p1, p2, p3, p4, count: int, color: tuple[int, int, int], width: int = 2) -> None: for i in range(1, count): a = lerp(p1, p2, i / count) b = lerp(p4, p3, i / count) draw.line([a, b], fill=color, width=width) def draw_brick_wall(draw: ImageDraw.ImageDraw, x1: int, y1: int, x2: int, y2: int, base: tuple[int, int, int], mortar: tuple[int, int, int]) -> None: draw.rectangle([x1, y1, x2, y2], fill=base) brick_h = 28 brick_w = 74 row = 0 for y in range(y1, y2, brick_h): offset = 0 if row % 2 == 0 else brick_w // 2 for x in range(x1 - offset, x2, brick_w): draw.rectangle([x, y, min(x + brick_w - 6, x2), min(y + brick_h - 4, y2)], outline=mortar, width=2) row += 1 def draw_window(draw: ImageDraw.ImageDraw, box, frame, glass, muntin) -> None: x1, y1, x2, y2 = box draw.rounded_rectangle(box, radius=6, fill=frame, outline=(80, 58, 38), width=3) inset = 10 draw.rectangle([x1 + inset, y1 + inset, x2 - inset, y2 - inset], fill=glass) mid = (x1 + x2) // 2 draw.line([(mid, y1 + inset), (mid, y2 - inset)], fill=muntin, width=4) draw.line([(x1 + inset, (y1 + y2) // 2), (x2 - inset, (y1 + y2) // 2)], fill=(255, 255, 255, 85), width=1) def draw_perspective_room(name: str, palette: dict[str, tuple[int, int, int]], furniture: str) -> None: img = Image.new("RGB", (1600, 1000), palette["bg"]) draw = ImageDraw.Draw(img) draw.rectangle([0, 0, 1600, 1000], fill=palette["bg"]) draw.rectangle([0, 0, 1600, 300], fill=palette["sky"]) left_roof = [(120, 300), (470, 160), (530, 540), (250, 780)] right_roof = [(1130, 160), (1480, 300), (1350, 780), (1070, 540)] back_wall = [(470, 160), (1130, 160), (1300, 300), (1100, 540), (500, 540), (300, 300)] floor = [(250, 780), (1350, 780), (1100, 540), (500, 540)] left_knee = [(110, 680), (250, 780), (500, 540), (365, 460)] right_knee = [(1235, 460), (1100, 540), (1350, 780), (1490, 680)] draw.polygon(floor, fill=palette["floor"]) draw_planks(draw, floor[0], floor[1], floor[2], floor[3], 11, palette["floor_lines"], 3) draw.polygon(back_wall, fill=palette["brick"]) draw_brick_wall(draw, 350, 210, 1240, 540, palette["brick"], palette["mortar"]) draw.polygon(left_knee, fill=palette["knee"]) draw.polygon(right_knee, fill=palette["knee"]) draw.polygon(left_roof, fill=palette["ceiling"]) draw.polygon(right_roof, fill=palette["ceiling"]) draw_planks(draw, left_roof[0], left_roof[1], left_roof[2], left_roof[3], 12, palette["ceiling_lines"]) draw_planks(draw, right_roof[1], right_roof[0], right_roof[3], right_roof[2], 12, palette["ceiling_lines"]) draw.line([(470, 160), (1130, 160)], fill=palette["beam"], width=18) for x in [250, 420, 590, 760, 930, 1100, 1270]: draw.line([(x, 250), (x + 80, 540)], fill=palette["beam_soft"], width=18) draw_window(draw, (620, 240, 760, 465), palette["frame"], palette["glass"], palette["muntin"]) draw_window(draw, (850, 240, 1010, 465), palette["frame"], palette["glass"], palette["muntin"]) draw.rounded_rectangle((420, 230, 570, 510), radius=8, fill=palette["frame"], outline=(70, 45, 25), width=3) draw.rectangle((435, 245, 555, 495), fill=palette["glass"]) draw.line([(490, 245), (490, 495)], fill=palette["muntin"], width=3) draw.rectangle((315, 575, 495, 635), fill=palette["storage"]) draw.rectangle((1110, 575, 1285, 635), fill=palette["storage"]) if furniture == "lounge": draw.rounded_rectangle((540, 650, 980, 760), radius=20, fill=palette["sofa"]) draw.rectangle((660, 720, 860, 760), fill=palette["sofa_shadow"]) draw.ellipse((720, 705, 900, 785), fill=palette["table"]) draw.rectangle((1180, 595, 1290, 720), fill=palette["desk"]) draw.rectangle((1215, 655, 1260, 765), fill=palette["chair"]) label = "Koncept 1 Warm Nordic Loft" note = "Otvoreni lounge + desk zona uz pogled i terasu." elif furniture == "bedroom": draw.rounded_rectangle((545, 635, 1045, 760), radius=20, fill=palette["bed"]) draw.rectangle((565, 615, 1025, 650), fill=palette["headboard"]) draw.rectangle((630, 660, 780, 735), fill=palette["linen_1"]) draw.rectangle((800, 660, 950, 735), fill=palette["linen_2"]) draw.rectangle((340, 610, 450, 705), fill=palette["bench"]) draw.rectangle((1145, 600, 1265, 710), fill=palette["bench"]) label = "Koncept 2 Soft Japandi Suite" note = "Mirna spavaca zona sa niskim plakarima pod kosinama." else: draw.rounded_rectangle((560, 660, 930, 760), radius=20, fill=palette["sofa"]) draw.rectangle((960, 615, 1120, 760), fill=palette["shelf"]) draw.rectangle((1030, 560, 1185, 720), fill=palette["shelf"]) draw.ellipse((620, 705, 810, 785), fill=palette["table"]) draw.rectangle((345, 600, 520, 740), fill=palette["desk"]) draw.rectangle((390, 700, 455, 790), fill=palette["chair"]) label = "Koncept 3 Rustic Studio Loft" note = "Studio i biblioteka sa jacim kontrastom drveta i opeke." draw.rectangle((0, 0, 1600, 88), fill=(15, 18, 24)) draw.text((48, 18), label, font=SUB, fill=(245, 240, 232)) draw.text((48, 915), note, font=TEXT, fill=(30, 30, 32)) chips = [palette["ceiling"], palette["brick"], palette["floor"], palette["storage"], palette["accent"]] x = 1050 for chip in chips: draw.rounded_rectangle((x, 905, x + 56, 961), radius=12, fill=chip) x += 72 draw.text((48, 952), "Koncept render: baziran na dostavljenim fotografijama i osnovi, bez laserskog snimka.", font=SMALL, fill=(72, 72, 76)) img.save(OUT / f"{name}.png", quality=95) def draw_zoning_plan() -> None: img = Image.new("RGB", (1400, 1000), (246, 241, 235)) draw = ImageDraw.Draw(img) draw.rectangle((120, 140, 1280, 860), outline=(45, 45, 45), width=5, fill=(252, 250, 246)) draw.rectangle((180, 620, 430, 840), outline=(150, 40, 40), width=6, fill=(242, 238, 234)) draw.text((215, 705), "Stepenice", font=SUB, fill=(120, 25, 25)) draw.rectangle((430, 160, 1180, 420), fill=(232, 214, 196), outline=(140, 122, 101), width=4) draw.text((620, 255), "Lounge + terasa", font=TITLE, fill=(76, 58, 46)) draw.rectangle((540, 470, 1180, 780), fill=(223, 232, 226), outline=(93, 118, 106), width=4) draw.text((690, 585), "Spavanje", font=TITLE, fill=(52, 78, 68)) draw.rectangle((180, 160, 420, 500), fill=(219, 225, 236), outline=(85, 100, 124), width=4) draw.text((208, 300), "Garderoba /\nodlaganje", font=SUB, fill=(60, 76, 96)) draw.rectangle((1190, 160, 1235, 830), fill=(212, 201, 186)) draw.rectangle((165, 160, 210, 830), fill=(212, 201, 186)) draw.text((80, 90), "Predlog zoniranja potkrovlja", font=TITLE, fill=(28, 28, 28)) draw.text((120, 900), "Niske ivice pod kosinama: plakar 45-60 cm dubine celom duzinom.", font=TEXT, fill=(40, 40, 40)) img.save(OUT / "zoning-plan.png", quality=95) PALETTES = { "render-1-warm-nordic": { "bg": (245, 240, 233), "sky": (227, 234, 239), "floor": (142, 112, 84), "floor_lines": (95, 71, 49), "brick": (178, 132, 98), "mortar": (208, 188, 169), "knee": (171, 157, 140), "ceiling": (214, 180, 138), "ceiling_lines": (162, 123, 84), "beam": (104, 64, 44), "beam_soft": (120, 76, 54), "frame": (126, 88, 52), "glass": (176, 205, 219), "muntin": (230, 236, 240), "storage": (181, 164, 146), "sofa": (208, 191, 170), "sofa_shadow": (183, 164, 144), "table": (208, 178, 137), "desk": (121, 96, 72), "chair": (94, 79, 64), "accent": (162, 121, 84), "bed": (222, 212, 198), "headboard": (130, 103, 82), "linen_1": (236, 229, 220), "linen_2": (198, 180, 159), "bench": (168, 136, 104), "shelf": (116, 88, 67), }, "render-2-soft-japandi": { "bg": (243, 241, 236), "sky": (226, 231, 228), "floor": (156, 140, 114), "floor_lines": (117, 99, 74), "brick": (194, 184, 171), "mortar": (228, 223, 214), "knee": (188, 184, 170), "ceiling": (224, 210, 183), "ceiling_lines": (176, 161, 133), "beam": (124, 96, 70), "beam_soft": (144, 114, 86), "frame": (133, 105, 74), "glass": (190, 205, 202), "muntin": (235, 239, 237), "storage": (203, 198, 188), "sofa": (214, 208, 198), "sofa_shadow": (184, 177, 167), "table": (189, 171, 146), "desk": (156, 142, 122), "chair": (121, 117, 101), "accent": (125, 141, 122), "bed": (219, 213, 203), "headboard": (150, 138, 118), "linen_1": (241, 236, 228), "linen_2": (193, 204, 193), "bench": (174, 166, 148), "shelf": (126, 120, 106), }, "render-3-rustic-studio": { "bg": (236, 232, 227), "sky": (219, 226, 234), "floor": (116, 98, 86), "floor_lines": (80, 63, 52), "brick": (153, 108, 82), "mortar": (196, 176, 164), "knee": (111, 110, 108), "ceiling": (170, 143, 109), "ceiling_lines": (116, 92, 65), "beam": (69, 44, 32), "beam_soft": (86, 58, 40), "frame": (112, 72, 43), "glass": (175, 197, 212), "muntin": (232, 239, 242), "storage": (96, 96, 94), "sofa": (138, 121, 105), "sofa_shadow": (98, 84, 73), "table": (177, 138, 95), "desk": (83, 71, 64), "chair": (56, 57, 58), "accent": (122, 100, 78), "bed": (192, 175, 159), "headboard": (103, 84, 71), "linen_1": (218, 206, 196), "linen_2": (143, 132, 118), "bench": (134, 103, 80), "shelf": (67, 63, 61), }, } if __name__ == "__main__": draw_zoning_plan() draw_perspective_room("render-1-warm-nordic", PALETTES["render-1-warm-nordic"], "lounge") draw_perspective_room("render-2-soft-japandi", PALETTES["render-2-soft-japandi"], "bedroom") draw_perspective_room("render-3-rustic-studio", PALETTES["render-3-rustic-studio"], "studio")