Spaces:
Running on Zero
Running on Zero
| """Deterministically snap a catalog part onto a creature's bounding box. | |
| Pure functions, no model. The selector chooses {part, anchor, scale, color}; | |
| this module computes the geometry. See the design spec for the anchor rules. | |
| """ | |
| from __future__ import annotations | |
| from copy import deepcopy | |
| from .parts import ANCHORS, PARTS | |
| from .boxes import repair_boxes | |
| def bbox(boxes: list[dict]) -> tuple[int, int, int, int, int, int]: | |
| """(minx, maxx, miny, maxy, minz, maxz). x,z = center; y = bottom.""" | |
| minx = min(b["x"] - b["w"] / 2 for b in boxes) | |
| maxx = max(b["x"] + b["w"] / 2 for b in boxes) | |
| miny = min(b["y"] for b in boxes) | |
| maxy = max(b["y"] + b["h"] for b in boxes) | |
| minz = min(b["z"] - b["d"] / 2 for b in boxes) | |
| maxz = max(b["z"] + b["d"] / 2 for b in boxes) | |
| return (int(minx), int(maxx), int(miny), int(maxy), int(minz), int(maxz)) | |
| def _mirror_x(boxes: list[dict]) -> list[dict]: | |
| return [{**b, "x": -b["x"]} for b in boxes] | |
| def _scaled(boxes: list[dict], scale: float) -> list[dict]: | |
| """Scale by a float multiplier, rounding to the integer voxel grid. The cube | |
| size stays 1 — a bigger part is simply more voxels (wider/taller block), per | |
| the design: "voxel stays the same size, the whole block just gets bigger".""" | |
| if abs(float(scale) - 1.0) < 1e-9: | |
| return deepcopy(boxes) | |
| s = float(scale) | |
| out = [] | |
| for b in boxes: | |
| nb = dict(b) | |
| for k in ("x", "y", "z"): | |
| nb[k] = round(b[k] * s) | |
| for k in ("w", "h", "d"): | |
| nb[k] = max(1, round(b[k] * s)) | |
| out.append(nb) | |
| return out | |
| def _cells(boxes: list[dict]) -> list[tuple]: | |
| """Explode boxes into (x, y, z, color) unit cells, matching voxelsFromBoxes | |
| (x,z = center, y = bottom).""" | |
| out = [] | |
| for b in boxes: | |
| cx, cy, cz = b["x"], b["y"], b["z"] | |
| w, h, d = b["w"], b["h"], b["d"] | |
| color = b.get("color", "#caa06f") | |
| for ix in range(w): | |
| for iy in range(h): | |
| for iz in range(d): | |
| out.append((round(cx + ix - (w - 1) / 2), cy + iy, | |
| round(cz + iz - (d - 1) / 2), color)) | |
| return out | |
| def _rotate(boxes: list[dict], axis: str, quarters: int) -> list[dict]: | |
| """Rotate the part by 90° increments about its own center, on the integer | |
| grid (exact, per-voxel). Returns 1x1x1 boxes — small parts, renders identically | |
| through voxelsFromBoxes.""" | |
| q = quarters % 4 | |
| if q == 0: | |
| return deepcopy(boxes) | |
| cells = _cells(boxes) | |
| if not cells: | |
| return deepcopy(boxes) | |
| cx = sum(c[0] for c in cells) / len(cells) | |
| cy = sum(c[1] for c in cells) / len(cells) | |
| cz = sum(c[2] for c in cells) / len(cells) | |
| out = [] | |
| for x, y, z, color in cells: | |
| dx, dy, dz = x - cx, y - cy, z - cz | |
| for _ in range(q): | |
| if axis == "x": | |
| dy, dz = -dz, dy | |
| elif axis == "z": | |
| dx, dy = -dy, dx | |
| else: # "y" | |
| dx, dz = -dz, dx | |
| out.append({"x": round(cx + dx), "y": round(cy + dy), | |
| "z": round(cz + dz), "w": 1, "h": 1, "d": 1, "color": color}) | |
| return out | |
| def _translate(boxes: list[dict], dx: int, dy: int, dz: int) -> list[dict]: | |
| return [{**b, "x": b["x"] + dx, "y": b["y"] + dy, "z": b["z"] + dz} for b in boxes] | |
| def _offset_for_anchor(anchor: str, tgt, part) -> tuple[int, int, int]: | |
| tminx, tmaxx, tminy, tmaxy, tminz, tmaxz = tgt | |
| pminx, pmaxx, pminy, pmaxy, pminz, pmaxz = part | |
| tcx = (tminx + tmaxx) // 2 | |
| tcz = (tminz + tmaxz) // 2 | |
| tcy = (tminy + tmaxy) // 2 | |
| if anchor == "bottom": | |
| return (tcx, tminy - pmaxy, tcz) | |
| if anchor == "rear": | |
| return (tcx, -pminy, tminz - pmaxz) | |
| if anchor == "front": | |
| return (tcx, -pminy, tmaxz - pminz) | |
| if anchor == "left": | |
| return (tminx - pmaxx, tcy - (pminy + pmaxy) // 2, tcz) | |
| if anchor == "right": | |
| return (tmaxx - pminx, tcy - (pminy + pmaxy) // 2, tcz) | |
| return (tcx, tmaxy - pminy, tcz) # "top" (and default) | |
| def _mirror_about_x(boxes: list[dict], cx: float) -> list[dict]: | |
| """Mirror placed boxes across the body's x-center (so a symmetric part STRADDLES | |
| the body — left half hugging the left face, its mirror hugging the right).""" | |
| return [{**b, "x": int(round(2 * cx - b["x"]))} for b in boxes] | |
| def _even(n: int, extent: int) -> list[int]: | |
| """n offsets spread evenly across, and centered within, `extent` — so multiple | |
| instances sit ON the body face (within its footprint), not drifting off it.""" | |
| if n <= 1: | |
| return [0] | |
| return [int(round(((i + 0.5) / n - 0.5) * extent)) for i in range(n)] | |
| def _slot_offsets(anchor: str, count: int, tgt) -> list[tuple]: | |
| """Tangent (dx,dy,dz) shifts for `count` instances, spread evenly across the | |
| anchor face and clamped to the body's extent so they hug it. Side + top/bottom | |
| anchors fan front-back (z); front/rear anchors fan left-right (x).""" | |
| if count <= 1: | |
| return [(0, 0, 0)] | |
| tminx, tmaxx, tminy, tmaxy, tminz, tmaxz = tgt | |
| if anchor in ("front", "rear"): | |
| return [(o, 0, 0) for o in _even(count, max(1, tmaxx - tminx))] | |
| return [(0, 0, o) for o in _even(count, max(1, tmaxz - tminz))] | |
| def assemble_part(boxes: list[dict], part_id: str, anchor: str, | |
| scale: float = 1, color: str | None = None, | |
| count: int = 1, rotation: tuple | None = None) -> list[dict]: | |
| """Return boxes + the placed part(s). Unknown part_id (or empty boxes) → unchanged. | |
| Parts FUSE rather than stack: each placed box is tagged `_part`/`_pc`, so adding | |
| the same part again accumulates its instance count and RE-distributes the whole | |
| set evenly across the anchor face (hugging the body), instead of piling new | |
| copies on the previous ones. Symmetric parts straddle the body (left half on the | |
| left face, mirrored across the body center to the right). scale is a float | |
| (rounded to grid); rotation is (axis, quarter_turns) for an exact 90° turn.""" | |
| part = PARTS.get(part_id) | |
| if part is None or not boxes: | |
| return boxes | |
| if anchor not in ANCHORS: | |
| anchor = part["default_anchor"] | |
| # Accumulate: peel off any earlier instances of THIS part; the rest is the body | |
| # (and other parts) we anchor against and keep. | |
| existing = [b for b in boxes if b.get("_part") == part_id] | |
| base = [b for b in boxes if b.get("_part") != part_id] or list(boxes) | |
| prev = int(existing[0].get("_pc", 0)) if existing else 0 | |
| total = max(1, prev + max(1, int(count))) | |
| # The authored part is the LEFT half for symmetric parts; place that, then mirror | |
| # across the body center so it straddles (never pre-mirror into one blob). | |
| local = _scaled(part["boxes"], scale or part.get("default_scale", 1)) | |
| if rotation: | |
| local = _rotate(local, rotation[0], int(rotation[1])) | |
| if color: | |
| local = [{**b, "color": color} for b in local] | |
| tgt = bbox(base) | |
| tcx = (tgt[0] + tgt[1]) / 2 | |
| local_bb = bbox(local) | |
| dx, dy, dz = _offset_for_anchor(anchor, tgt, local_bb) | |
| placed: list[dict] = [] | |
| for sx, sy, sz in _slot_offsets(anchor, total, tgt): | |
| inst = _translate(local, dx + sx, dy + sy, dz + sz) | |
| if part.get("symmetric"): | |
| inst = inst + _mirror_about_x(inst, tcx) | |
| placed += inst | |
| placed = repair_boxes(placed) | |
| placed = [{**b, "_part": part_id, "_pc": total} for b in placed] | |
| return base + placed | |