Files
2026-05-19 14:53:38 +02:00

259 lines
11 KiB
Python

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")