extends RefCounted
## A PARAMETRIC SHAPE TYPE (docs/parametric-shapes-design.md §2): params + a box size
## -> convex pieces, in the shape's LOCAL frame. The shape's frame (a durable record)
## places them in the world; the fold does that (shape_fold.gd).
##
## LOCAL FRAME: x = the run (forward), y = up, z = across (width). The shape fills the
## box 0..size. Generators are PURE and DETERMINISTIC — every peer folds the same
## records into the same brushes — and VERSIONED by `gv`: a change that alters a
## generator's output is a new gv, and the old one stays, so a room's stairs never
## move because a release changed the maths.
##
## A piece is ShapeKit's {planes: PackedFloat32Array (n.x n.y n.z d, n·x <= d), edges}.
## Generator version. Bump when output changes; keep the old path (see above).
const GV := 1
func id() -> String:
return ""
func label() -> String:
return id().capitalize()
## Parameter descriptors, the same shape BrushShape.params() uses:
## {id, label, type: "int"|"float"|"bool"|"enum", default, min, max, step, unit,
## options (enum), tip}. One list drives the fold, the Shape panel and validation.
func params() -> Array:
return []
func local_pieces(_size: Vector3, _p: Dictionary) -> Array:
return []
func local_clearance(_size: Vector3, _p: Dictionary) -> Array:
return []
## WHAT EACH PIECE IS, in the fold's piece order (solids, then clearance) — the
## Outliner's child names (§13.4: "children should also be named appropriately,
## passage is fine a name for the actual subtractive brush"). A generator tags a piece
## with "role"; untagged solids are the type itself, the clearance is "passage". The
## fold ignores the key, so it changes no brush.
func roles(size: Vector3, p: Dictionary) -> Array:
var out: Array = []
for pc: Dictionary in local_pieces(size, p):
out.append(String(pc.get("role", id())))
for _pc: Dictionary in local_clearance(size, p):
out.append("passage")
return out
## A piece tagged with its role (see roles()).
static func tagged(piece: Dictionary, role: String) -> Dictionary:
piece["role"] = role
return piece
## Defaults <- given, each value coerced to its type and clamped to its range.
## Unknown ids are dropped: a record from a newer build cannot smuggle in a param
## this generator would misread.
func resolve(given: Dictionary) -> Dictionary:
var out := {}
for d: Dictionary in params():
var pid := String(d["id"])
var v: Variant = given.get(pid, d["default"])
match String(d.get("type", "float")):
"int":
var i := int(v)
if d.has("min"):
i = maxi(i, int(d["min"]))
if d.has("max"):
i = mini(i, int(d["max"]))
out[pid] = i
"float":
var f := float(v)
if d.has("min"):
f = maxf(f, float(d["min"]))
if d.has("max"):
f = minf(f, float(d["max"]))
out[pid] = f
"bool":
out[pid] = bool(v)
"enum":
var opts: Array = d.get("options", [])
out[pid] = String(v) if opts.has(String(v)) else String(d["default"])
_:
out[pid] = v
return out
## Every type's headroom / margin clearance is an axis-aligned box or a prism; shared.
static func box(lo: Vector3, hi: Vector3) -> Dictionary:
return ShapeKit.box_piece(lo, hi)