OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / maps / shapes / param / param_shape.gd

97 lines · gd ref:
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)