259 lines
11 KiB
Python
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")
|