extends "res://scenes/maps/shapes/param/param_shape.gd"
## PRISM — THE BOXCUTTER'S CUTTER (#120 L2, docs/node-tree-and-stacks.md §13.1).
##
## A footprint polygon on the bottom of the shape's box, extruded up its height: ONE convex
## piece, the hull of the bottom and top sections (FaceKit.hull_sections, the same hull rule
## as cables and merges). The footprint is stored in the box's UNIT xz (0..1), so the
## shape's size stretches it, as hull's planes are.
##
## poly [[u, v], …] the footprint (hidden: data drawn by the tool, not a knob). A
## concave one is used as its CONVEX HULL: a cutter is convex.
## taper the top section's scale about the footprint's centre (1 = straight sides,
## 0.5 = a frustum; 0.05 is the floor, a near-apex)
## shear_x the top section's offset, as a fraction of size.x / size.z (a wedge, a lean)
## shear_z
##
## Taper and shear are PARAMS, not modifiers: a tapered piece is not a rigid transform of
## the untapered one, and ModFold applies only those (§13.1). A hull of two convex
## sections is convex, so the result always is.
##
## Not in the create tool's Tab list (ParamShapes.ids): BoxCutter draws it.
## ⛔ VERSIONING as every generator: a change to the output is a new GV (1 here).
func id() -> String:
return "prism"
func label() -> String:
return "Prism"
func params() -> Array:
return [
{"id": "poly", "type": "list", "default": [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
"hidden": true, "tip": "the footprint, [u, v] in the box's unit xz"},
{"id": "taper", "label": "Taper", "type": "float", "default": 1.0, "min": 0.05, "max": 4.0,
"step": 0.05, "tip": "Top size against the footprint (1 = straight sides)"},
{"id": "shear_x", "label": "Shear X", "type": "float", "default": 0.0, "min": -4.0, "max": 4.0,
"step": 0.05, "tip": "Top offset along x, a fraction of the width (a wedge)"},
{"id": "shear_z", "label": "Shear Z", "type": "float", "default": 0.0, "min": -4.0, "max": 4.0,
"step": 0.05, "tip": "Top offset along z, a fraction of the depth (a wedge)"},
]
func local_pieces(size: Vector3, p: Dictionary) -> Array:
var pts := PackedVector2Array()
for q: Variant in p.get("poly", []):
if q is Array and (q as Array).size() >= 2:
pts.append(Vector2(float(q[0]), float(q[1])))
if pts.size() < 3:
return []
var hull := Geometry2D.convex_hull(pts) # closed: the last point repeats the first
if hull.size() < 4:
return []
hull.resize(hull.size() - 1)
var lo := hull[0]
var hi := hull[0]
for v: Vector2 in hull:
lo = lo.min(v)
hi = hi.max(v)
var c := (lo + hi) * 0.5
var taper := float(p.get("taper", 1.0))
var sh := Vector2(float(p.get("shear_x", 0.0)), float(p.get("shear_z", 0.0)))
var bottom := PackedVector3Array()
var top := PackedVector3Array()
for v: Vector2 in hull:
bottom.append(Vector3(v.x * size.x, 0.0, v.y * size.z))
var t: Vector2 = c + (v - c) * taper + sh
top.append(Vector3(t.x * size.x, size.y, t.y * size.z))
var planes: PackedFloat32Array = FaceKit.hull_sections(bottom, top)
if planes.is_empty():
return []
return [{"planes": planes, "edges": PackedVector3Array()}]
## A regular n-gon inscribed in the unit box (centre 0.5, 0.5, radius 0.5), the first point
## at -z: the footprint of BoxCutter's ngon, and of its circle at n = segments.
static func ngon_poly(sides: int) -> Array:
var n := maxi(3, sides)
var out: Array = []
for k in n:
var a := TAU * float(k) / float(n) - PI * 0.5
out.append([0.5 + 0.5 * cos(a), 0.5 + 0.5 * sin(a)])
return out