233 lines
7.2 KiB
Python
233 lines
7.2 KiB
Python
"""Layout rule generator.
|
|
|
|
Emits the four canonical layout strategies the Creative Brain requires:
|
|
1. road_buffer - keep a no-build buffer band beside roads;
|
|
2. layer - near/mid/far depth layering for depth perception;
|
|
3. scatter - asymmetric, jittered, non-grid placement;
|
|
4. corner - weighted decoration at corners / junctions.
|
|
|
|
Each rule is a :class:`brain.schema.LayoutRule`. For scatter we also compute
|
|
concrete cell coordinates so the batch driver can place assets without
|
|
re-deriving geometry.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
from typing import List, Tuple
|
|
|
|
from brain import llm
|
|
from brain.schema import (
|
|
AssetRef,
|
|
GenerationPlan,
|
|
GenerationStep,
|
|
LayoutRule,
|
|
SceneConfig,
|
|
)
|
|
|
|
LAYER_NAMES = ("foreground", "midground", "background")
|
|
|
|
|
|
def _road_band(config: SceneConfig) -> List[Tuple[int, int]]:
|
|
"""Cells belonging to roads + their no-build buffer band (simple grid model)."""
|
|
w, h = config.width, config.height
|
|
buffer = max(0, config.roads.noBuildBuffer)
|
|
road_cells = set()
|
|
if config.roads.network == "grid":
|
|
step = max(2, min(w, h) // 4)
|
|
for x in range(0, w, step):
|
|
for y in range(h):
|
|
road_cells.add((x, y))
|
|
for y in range(0, h, step):
|
|
for x in range(w):
|
|
road_cells.add((x, y))
|
|
elif config.roads.network == "radial":
|
|
cx, cy = w // 2, h // 2
|
|
for x in range(w):
|
|
for y in range(h):
|
|
if (x - cx) % 4 == 0 and (y - cy) % 4 == 0:
|
|
road_cells.add((x, y))
|
|
else: # organic
|
|
for x in range(w):
|
|
for y in range(h):
|
|
if (x + y) % 5 == 0:
|
|
road_cells.add((x, y))
|
|
|
|
band = set(road_cells)
|
|
for (x, y) in list(road_cells):
|
|
for dx in range(-buffer, buffer + 1):
|
|
for dy in range(-buffer, buffer + 1):
|
|
if 0 <= x + dx < w and 0 <= y + dy < h:
|
|
band.add((x + dx, y + dy))
|
|
return sorted(band)
|
|
|
|
|
|
def _usable_cells(config: SceneConfig) -> List[Tuple[int, int]]:
|
|
band = set(_road_band(config))
|
|
all_cells = [
|
|
(x, y)
|
|
for x in range(config.width)
|
|
for y in range(config.height)
|
|
if (x, y) not in band
|
|
]
|
|
return all_cells
|
|
|
|
|
|
def generate_layout(config: SceneConfig, whitelist: List[AssetRef]) -> List[LayoutRule]:
|
|
"""Generate layout rules from a scene config + asset whitelist."""
|
|
rules: List[LayoutRule] = []
|
|
w, h = config.width, config.height
|
|
buffer = config.roads.noBuildBuffer
|
|
|
|
rules.append(
|
|
LayoutRule(
|
|
kind="road_buffer",
|
|
description=(
|
|
f"Keep a {buffer}-cell no-build band on both sides of the "
|
|
f"'{config.roads.network}' road network; vegetation and props avoid it."
|
|
),
|
|
params={"buffer": buffer, "network": config.roads.network},
|
|
)
|
|
)
|
|
|
|
depth_layers = 3 if max(w, h) >= 32 else 2
|
|
rules.append(
|
|
LayoutRule(
|
|
kind="layer",
|
|
description=(
|
|
f"Depth layers = {depth_layers}: "
|
|
+ " > ".join(LAYER_NAMES[:depth_layers])
|
|
+ " with decreasing density toward the back."
|
|
),
|
|
params={"layers": list(LAYER_NAMES[:depth_layers])},
|
|
)
|
|
)
|
|
|
|
rules.append(
|
|
LayoutRule(
|
|
kind="scatter",
|
|
description=(
|
|
"Asymmetric, jittered scatter with uneven spacing (never uniform "
|
|
"grid) for a natural, hand-placed feel."
|
|
),
|
|
params={"jitter": 1.0, "asymmetry": True},
|
|
)
|
|
)
|
|
|
|
rules.append(
|
|
LayoutRule(
|
|
kind="corner",
|
|
description=(
|
|
"Weighted decoration at map corners and road junctions so focal "
|
|
"points feel deliberate rather than random."
|
|
),
|
|
params={"corner_weight": 2.0},
|
|
)
|
|
)
|
|
return rules
|
|
|
|
|
|
def build_placement_steps(
|
|
config: SceneConfig, whitelist: List[AssetRef], seed: int = 1
|
|
) -> List[GenerationStep]:
|
|
"""Compute concrete scatter + corner placements and return batch steps.
|
|
|
|
Deterministic given ``seed``. Respects the road no-build band. Returns
|
|
``place`` steps (asset in a layer) plus a terrain + lighting + material step.
|
|
"""
|
|
rng = random.Random(seed)
|
|
cells = _usable_cells(config)
|
|
if not cells:
|
|
return []
|
|
|
|
band = set(_road_band(config))
|
|
corner_pts = [
|
|
(1, 1),
|
|
(config.width - 2, 1),
|
|
(1, config.height - 2),
|
|
(config.width - 2, config.height - 2),
|
|
(config.width // 2, config.height // 2),
|
|
]
|
|
steps: List[GenerationStep] = []
|
|
|
|
steps.append(
|
|
GenerationStep(
|
|
action="terrain",
|
|
target=config.terrain.algorithm,
|
|
params={
|
|
"seed": config.terrain.seed,
|
|
"width": config.width,
|
|
"height": config.height,
|
|
"material": config.terrain.material,
|
|
},
|
|
)
|
|
)
|
|
steps.append(
|
|
GenerationStep(
|
|
action="set_lighting",
|
|
target=config.terrain.material,
|
|
params={
|
|
"atmosphere": config.lighting.atmosphere,
|
|
"timeOfDay": config.lighting.timeOfDay,
|
|
"intensity": config.lighting.intensity,
|
|
},
|
|
)
|
|
)
|
|
|
|
target_cells = max(1, int(config.vegetation.density * len(cells)))
|
|
rng.shuffle(cells)
|
|
|
|
decor_assets = [a for a in whitelist if a.category in ("decoration", "plant", "prop")]
|
|
for i, (x, y) in enumerate(cells[:target_cells]):
|
|
layer = LAYER_NAMES[0 if i < target_cells // 3 else (1 if i < 2 * target_cells // 3 else 2)]
|
|
asset = decor_assets[i % len(decor_assets)] if decor_assets else None
|
|
if asset is None:
|
|
continue
|
|
steps.append(
|
|
GenerationStep(
|
|
action="place",
|
|
target=asset.id,
|
|
params={"x": x, "y": y, "layer": layer, "seed": rng.randint(0, 100000)},
|
|
)
|
|
)
|
|
|
|
for idx, (x, y) in enumerate(corner_pts):
|
|
if (x, y) in band or not decor_assets:
|
|
continue
|
|
steps.append(
|
|
GenerationStep(
|
|
action="place",
|
|
target=decor_assets[idx % len(decor_assets)].id,
|
|
params={
|
|
"x": x,
|
|
"y": y,
|
|
"layer": "foreground",
|
|
"corner": True,
|
|
"seed": rng.randint(0, 100000),
|
|
},
|
|
)
|
|
)
|
|
return steps
|
|
|
|
|
|
def apply_layout(
|
|
config: SceneConfig, whitelist: List[AssetRef], missing_assets: List[str], seed: int = 1
|
|
) -> GenerationPlan:
|
|
"""Assemble a complete GenerationPlan from config + whitelist."""
|
|
rules = generate_layout(config, whitelist)
|
|
steps = build_placement_steps(config, whitelist, seed=seed)
|
|
if llm.llm_enabled() and rules:
|
|
llm.pick_with_llm(
|
|
f"Which layout rule best matches style '{config.style}'?",
|
|
[r.kind for r in rules],
|
|
"Choose one layout strategy",
|
|
fallback_index=0,
|
|
)
|
|
return GenerationPlan(
|
|
config=config,
|
|
whitelist=whitelist,
|
|
missing_assets=missing_assets,
|
|
layout_rules=rules,
|
|
steps=steps,
|
|
)
|