OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps commits / 2fe5066

#120 L2 step 1: the prism cutter shape (§13.1)
param_prism: a unit-xz footprint polygon extruded up the box, ONE convex piece (FaceKit.hull_sections of bottom + top). taper scales the top about the footprint's centre, shear_x / shear_z offset it (a wedge); a concave footprint is used as its convex hull; ngon_poly(n) for ngons and circles. Registered, not in the create tool's Tab list. gate_param_prism (10); create_param and param_builtins still green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2fe5066e460257082f3817a2c3f1eac700fc07e3
by plankatron <fleeky@gmail.com> · 2026-10-19T00:33:32.000Z

Changed files (3) +186 -2

scenes/maps/shapes/param/param_prism.gd +84 -0
……@@ -0,0 +1,84 @@
1extends "res://scenes/maps/shapes/param/param_shape.gd"
2## PRISM — THE BOXCUTTER'S CUTTER (#120 L2, docs/node-tree-and-stacks.md §13.1).
3##
4## A footprint polygon on the bottom of the shape's box, extruded up its height: ONE convex
5## piece, the hull of the bottom and top sections (FaceKit.hull_sections, the same hull rule
6## as cables and merges). The footprint is stored in the box's UNIT xz (0..1), so the
7## shape's size stretches it, as hull's planes are.
8##
9## poly [[u, v], …] the footprint (hidden: data drawn by the tool, not a knob). A
10## concave one is used as its CONVEX HULL: a cutter is convex.
11## taper the top section's scale about the footprint's centre (1 = straight sides,
12## 0.5 = a frustum; 0.05 is the floor, a near-apex)
13## shear_x the top section's offset, as a fraction of size.x / size.z (a wedge, a lean)
14## shear_z
15##
16## Taper and shear are PARAMS, not modifiers: a tapered piece is not a rigid transform of
17## the untapered one, and ModFold applies only those (§13.1). A hull of two convex
18## sections is convex, so the result always is.
19##
20## Not in the create tool's Tab list (ParamShapes.ids): BoxCutter draws it.
21## ⛔ VERSIONING as every generator: a change to the output is a new GV (1 here).
22
23
24func id() -> String:
25 return "prism"
26
27
28func label() -> String:
29 return "Prism"
30
31
32func params() -> Array:
33 return [
34 {"id": "poly", "type": "list", "default": [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
35 "hidden": true, "tip": "the footprint, [u, v] in the box's unit xz"},
36 {"id": "taper", "label": "Taper", "type": "float", "default": 1.0, "min": 0.05, "max": 4.0,
37 "step": 0.05, "tip": "Top size against the footprint (1 = straight sides)"},
38 {"id": "shear_x", "label": "Shear X", "type": "float", "default": 0.0, "min": -4.0, "max": 4.0,
39 "step": 0.05, "tip": "Top offset along x, a fraction of the width (a wedge)"},
40 {"id": "shear_z", "label": "Shear Z", "type": "float", "default": 0.0, "min": -4.0, "max": 4.0,
41 "step": 0.05, "tip": "Top offset along z, a fraction of the depth (a wedge)"},
42 ]
43
44
45func local_pieces(size: Vector3, p: Dictionary) -> Array:
46 var pts := PackedVector2Array()
47 for q: Variant in p.get("poly", []):
48 if q is Array and (q as Array).size() >= 2:
49 pts.append(Vector2(float(q[0]), float(q[1])))
50 if pts.size() < 3:
51 return []
52 var hull := Geometry2D.convex_hull(pts) # closed: the last point repeats the first
53 if hull.size() < 4:
54 return []
55 hull.resize(hull.size() - 1)
56 var lo := hull[0]
57 var hi := hull[0]
58 for v: Vector2 in hull:
59 lo = lo.min(v)
60 hi = hi.max(v)
61 var c := (lo + hi) * 0.5
62 var taper := float(p.get("taper", 1.0))
63 var sh := Vector2(float(p.get("shear_x", 0.0)), float(p.get("shear_z", 0.0)))
64 var bottom := PackedVector3Array()
65 var top := PackedVector3Array()
66 for v: Vector2 in hull:
67 bottom.append(Vector3(v.x * size.x, 0.0, v.y * size.z))
68 var t: Vector2 = c + (v - c) * taper + sh
69 top.append(Vector3(t.x * size.x, size.y, t.y * size.z))
70 var planes: PackedFloat32Array = FaceKit.hull_sections(bottom, top)
71 if planes.is_empty():
72 return []
73 return [{"planes": planes, "edges": PackedVector3Array()}]
74
75
76## A regular n-gon inscribed in the unit box (centre 0.5, 0.5, radius 0.5), the first point
77## at -z: the footprint of BoxCutter's ngon, and of its circle at n = segments.
78static func ngon_poly(sides: int) -> Array:
79 var n := maxi(3, sides)
80 var out: Array = []
81 for k in n:
82 var a := TAU * float(k) / float(n) - PI * 0.5
83 out.append([0.5 + 0.5 * cos(a), 0.5 + 0.5 * sin(a)])
84 return out
scenes/maps/shapes/param/param_shapes.gd +4 -2
……@@ -26,6 +26,8 @@ static func _ensure() -> void:
2626 _types["hull"] = preload("res://scenes/maps/shapes/param/param_hull.gd").new()
2727 # Generated ON a curve (Add → Cable, #120 L3), not drawn as a box: also not in ids().
2828 _types["cable"] = preload("res://scenes/maps/shapes/param/param_cable.gd").new()
29 # Drawn by BoxCutter (#120 L2 §13.1), not by the create tool: also not in ids().
30 _types["prism"] = preload("res://scenes/maps/shapes/param/param_prism.gd").new()
2931
3032
3133static func get_type(type_id: String) -> Object:
……@@ -34,10 +36,10 @@ static func get_type(type_id: String) -> Object:
3436
3537
3638## The create tool's types, in tab order — every registered type but those that only
37## come from a brush (hull).
39## come from a brush (hull), a curve (cable) or the BoxCutter (prism).
3840static func ids() -> Array:
3941 _ensure()
40 return _types.keys().filter(func(k: String) -> bool: return k != "hull" and k != "cable")
42 return _types.keys().filter(func(k: String) -> bool: return k != "hull" and k != "cable" and k != "prism")
4143
4244
4345## A type's params as ToolSettingsBar rows ("p.<id>"), valued from `values` (resolved).
scenes/test/gate_param_prism.gd +98 -0
……@@ -0,0 +1,98 @@
1extends SceneTree
2## GATE — THE PRISM CUTTER SHAPE (#120 L2 §13.1, param_prism.gd).
3## P1 the unit square, size (2, 1, 3): ONE piece, exactly the box 0..size (8 corners)
4## P2 taper 0.5: the top is the footprint halved about its centre; the bottom unchanged
5## P3 shear [0.5, 0]: the top moves half the size along x (a wedge / lean)
6## P4 ngon_poly(6): six points on the unit circle's box; its prism has 6 + 2 faces
7## P5 a concave footprint (an L) becomes its CONVEX HULL (cutters are convex)
8## P6 registered, and NOT in the create tool's Tab list (BoxCutter draws it)
9## P7 the same params give the same planes (deterministic)
10##
11## RUN: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$GODOT" --headless --path . --script res://scenes/test/gate_param_prism.gd
12
13const ParamShapes := preload("res://scenes/maps/shapes/param/param_shapes.gd")
14
15var fails := 0
16var checks := 0
17
18
19func _p(c: bool, m: String, got: Variant = "") -> void:
20 checks += 1
21 if not c:
22 fails += 1
23 print(" %-62s %s %s" % [m, "OK" if c else "**FAIL**", str(got)])
24
25
26func _corners(t: Object, size: Vector3, params: Dictionary) -> Array:
27 var pcs: Array = t.local_pieces(size, t.resolve(params))
28 if pcs.size() != 1:
29 return []
30 var out: Array = []
31 for v: Vector3 in FaceKit.corners(pcs[0]["planes"]):
32 out.append(Vector3(snappedf(v.x, 0.001), snappedf(v.y, 0.001), snappedf(v.z, 0.001)))
33 out.sort()
34 return out
35
36
37func _at_y(cs: Array, y: float) -> Array:
38 return cs.filter(func(v: Vector3) -> bool: return absf(v.y - y) < 1.0e-3)
39
40
41func _initialize() -> void:
42 var t: Object = ParamShapes.get_type("prism")
43 # ---- P6 ----
44 _p(t != null, "P6 prism is a registered shape type")
45 _p(not ParamShapes.ids().has("prism"), "P6 ... and not in the create tool's Tab list", ParamShapes.ids())
46 if t == null:
47 _end()
48 return
49 var sq := [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]
50 var size := Vector3(2, 1, 3)
51 # ---- P1 ----
52 var c1 := _corners(t, size, {"poly": sq})
53 var box: Array = []
54 for x: float in [0.0, 2.0]:
55 for y: float in [0.0, 1.0]:
56 for z: float in [0.0, 3.0]:
57 box.append(Vector3(x, y, z))
58 box.sort()
59 _p(c1 == box, "P1 the unit square: one piece, the box 0..size", c1)
60 # ---- P2 ----
61 var c2 := _corners(t, size, {"poly": sq, "taper": 0.5})
62 var top2: Array = _at_y(c2, 1.0)
63 var want2: Array = [Vector3(0.5, 1, 0.75), Vector3(0.5, 1, 2.25), Vector3(1.5, 1, 0.75), Vector3(1.5, 1, 2.25)]
64 _p(top2 == want2, "P2 taper 0.5: the top is the footprint halved about its centre", top2)
65 _p(_at_y(c2, 0.0).size() == 4 and (_at_y(c2, 0.0)[3] as Vector3) == Vector3(2, 0, 3), "P2 ... the bottom unchanged", _at_y(c2, 0.0))
66 # ---- P3 ----
67 var c3 := _corners(t, size, {"poly": sq, "shear_x": 0.5})
68 var top3: Array = _at_y(c3, 1.0)
69 _p(top3.size() == 4 and (top3[0] as Vector3).x == 1.0 and (top3[3] as Vector3).x == 3.0,
70 "P3 shear 0.5 x: the top moves one (half of 2) along x", top3)
71 # ---- P4 ----
72 var hexp: Array = t.ngon_poly(6)
73 var pc4: Array = t.local_pieces(size, t.resolve({"poly": hexp}))
74 var faces := 0
75 if pc4.size() == 1:
76 var pl: PackedFloat32Array = pc4[0]["planes"]
77 for i in pl.size() / 4:
78 if FaceKit.face_polygon(pl, i).size() >= 3:
79 faces += 1
80 _p(hexp.size() == 6, "P4 ngon_poly(6): six points", hexp.size())
81 _p(faces == 8, "P4 ... its prism has 6 sides + top + bottom", faces)
82 # ---- P5 ----
83 var ell := [[0.0, 0.0], [1.0, 0.0], [1.0, 0.4], [0.4, 0.4], [0.4, 1.0], [0.0, 1.0]]
84 var c5 := _corners(t, Vector3(1, 1, 1), {"poly": ell})
85 _p(c5.size() == 10 and not c5.has(Vector3(0.4, 0, 0.4)), "P5 an L footprint becomes its convex hull (no inner corner)", c5.size())
86 # ---- P7 ----
87 var a: Array = t.local_pieces(size, t.resolve({"poly": hexp, "taper": 0.7}))
88 var b: Array = t.local_pieces(size, t.resolve({"poly": hexp, "taper": 0.7}))
89 _p(a.size() == 1 and b.size() == 1 and a[0]["planes"] == b[0]["planes"], "P7 the same params give the same planes")
90 _end()
91
92
93func _end() -> void:
94 if checks < 9:
95 print(" **FAIL** only %d checks ran" % checks)
96 fails += 1
97 print("\nGATE param_prism: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
98 quit(0 if fails == 0 else 1)