extends LevelOperator
class_name BoxCutterOperator
## BOXCUTTER — draw a cutter on the surface you aim at, and it cuts (#120 L2,
## docs/node-tree-and-stacks.md §13.2–13.4).
##
## The mouse is the camera in every builder tool, so drawing is AIMING, as in the create
## and curve tools: aim at a face (it picks the plane and the target), P sets the first
## corner, the aim drags the other across that plane, snapped to the lattice.
##
## KEYS ARE BOXCUTTER'S (its manual; user 2026-10-18), and only while the tool is up:
## P the first corner X mode: slice → intersect → inset → outset → cut
## I inset E extrude: a set depth ([ ] steps it)
## Space THROUGH (lazorcut), and commit
## LMB / Enter commit · Esc / RMB cancel · H help
##
## THE CUTTER is a `prism` shape (§13.1): its frame's y points INTO the surface, its
## bottom sits one grid step ABOVE the surface (so the face is opened cleanly, not left
## coplanar), and it reaches the set depth below — or THROUGH the target (CutDepth,
## measured now and stored). ONE gesture writes the shape and the live op, so one Ctrl+Z
## takes the whole cut back.
const ParamShapes := preload("res://scenes/maps/shapes/param/param_shapes.gd")
const CutDepth := preload("res://scenes/maps/cut_depth.gd")
## In X's cycle order, BoxCutter's: cut (the default), slice, intersect, inset; outset
## (no BoxCutter key) after inset.
const MODES := ["subtract", "slice", "intersect", "inset", "outset"]
const _UNIT := [Vector3.RIGHT, Vector3.UP, Vector3.BACK]
enum Phase { AIM, DRAW }
var _phase := Phase.AIM
var _mode := 0
var _o := Vector3.ZERO # the drawn plane: a point on it, its normal, two axes in it
var _n := Vector3.UP
var _u := Vector3.BACK
var _v := Vector3.RIGHT
var _a := Vector3.ZERO # the first corner, on the plane
var _b := Vector3.ZERO # the other corner
var _target := "" # the group (or brush key) the cut goes into
var _material := 0
var _level_id := 0
var _depth_steps := 0 # 0 = THROUGH
var _valid := false
var _frozen := false # placed by place() (gates, Adjust Last): the aim does not move it
# ---- step 4: footprints and surfaces (§13.2, BoxCutter's D pie) ----
const SHAPES := ["box", "circle", "ngon", "custom"]
const SURFACES := ["face", "view", "cursor", "grid"]
var _shape := "box" # box: corner to corner · circle / ngon: centre, then radius
var _sides := 6 # ngon
var _segments := 24 # circle
var _points: Array = [] # custom: the P points, on the plane
var _surface := "face" # which plane the footprint is drawn on
# ---- step 5: modifiers while drawing (§13.4, BoxCutter's keys) ----
## The queue, written as mod.create records on the cutter at commit and then emptied:
## B chamfer · V array (V V radial) · 1 / 2 / 3 mirror x / y / z · Shift+G grid. The same
## key again takes it off. Mirror and radial work about the TARGET's centre, as
## BoxCutter's do about the object.
var _q_chamfer := false
var _q_array := "" # "" / "array" / "radial"
var _q_mirror := [false, false, false]
var _q_grid := false
var _live := true # L: false = the cut is written OFF
var _taper := 1.0 # the prism's (D menu)
var _shear_x := 0.0
var _shear_z := 0.0
func _queue_text() -> String:
var q: Array = []
for i in 3:
if _q_mirror[i]:
q.append("mirror " + "xyz"[i])
if _q_array != "":
q.append(_q_array)
if _q_grid:
q.append("grid")
if _q_chamfer:
q.append("chamfer")
return ", ".join(q) if not q.is_empty() else "none"
func _clear_queue() -> void:
_q_chamfer = false
_q_array = ""
_q_mirror = [false, false, false]
_q_grid = false
_live = true
# ---- step 6: housekeeping (§13.5) ----
## The last commit's queue, for Shift+R (restore_last_queue).
var _last_queue: Dictionary = {}
## Shift+R: the last cut's modifiers come back (the rest — mode, shape, depth — is sticky).
func restore_last_queue() -> void:
if _last_queue.is_empty():
return
_q_chamfer = bool(_last_queue["chamfer"])
_q_array = String(_last_queue["array"])
_q_mirror = (_last_queue["mirror"] as Array).duplicate()
_q_grid = bool(_last_queue["grid"])
func _remember_queue() -> void:
_last_queue = {"chamfer": _q_chamfer, "array": _q_array, "mirror": _q_mirror.duplicate(), "grid": _q_grid}
## What the cut goes into: the aimed object, and every SELECTED object with it (each
## brush's group, or its key when it has none), never a cutter.
func _targets() -> Array:
var out: Array = []
if _target != "":
out.append(_target)
var sel: Variant = map.get("_sel_brushes")
for b: Variant in (sel as Array if sel is Array else []):
var bd: Dictionary = level.get_brush(int(b))
if bd.is_empty() or bool(bd.get("cutter", false)) or bool(bd.get("clearance", false)):
continue
var g := String(level.get_brush_group(int(b)))
var t := g if g != "" else String(level.get_brush_key(int(b)))
if t != "" and not out.has(t):
out.append(t)
return out
## The room's "Cutters" collection, created (inside the caller's gesture) the first time.
func _cutters_collection() -> String:
var t: Dictionary = map._persist.collection_tree()
var cols: Dictionary = t.get("cols", {})
var keys: Array = cols.keys()
keys.sort()
for cd: String in keys:
if String(cols[cd].get("name", "")) == "Cutters":
return cd
return String(map._persist.record("col.create", {"name": "Cutters", "parent": ""}))
## The target's centre: the middle of its solid pieces' corners.
func _target_centre() -> Vector3:
var lo := Vector3(INF, INF, INF)
var hi := -lo
for b: int in (level.get_group_brush_ids(_target) as PackedInt32Array):
var bd: Dictionary = level.get_brush(b)
if bool(bd.get("clearance", false)) or bool(bd.get("cutter", false)):
continue
for c: Vector3 in FaceKit.corners(bd["planes"]):
lo = lo.min(c)
hi = hi.max(c)
return _o if lo.x > hi.x else (lo + hi) * 0.5
## The queued modifiers as [kind, params] for the cutter framed `f` (the prism's frame):
## mirror's offset puts its plane through the target's centre along that frame axis;
## radial pivots there. The rest take the map's defaults.
func _mod_params(f: Transform3D) -> Array:
var out: Array = []
var c := _target_centre()
var b := f.basis.orthonormalized()
# 1 / 2 / 3 are the WORLD's X / Y / Z (BoxCutter's are the object's); ModFold mirrors along
# a FRAME axis, so each world axis goes to the frame axis most nearly parallel to it.
for k in 3:
if not _q_mirror[k]:
continue
var best := 0
for i in 3:
if absf(b[i][k]) > absf(b[best][k]):
best = i
out.append(["mirror", {"axis": "xyz"[best], "offset": b[best].dot(c - f.origin)}])
if _q_array == "array":
out.append(["array", {}])
elif _q_array == "radial":
out.append(["radial", {"pivot": [c.x, c.y, c.z]}])
if _q_grid:
out.append(["grid", {}])
if _q_chamfer:
out.append(["chamfer", {}])
return out
## Place a CUSTOM footprint directly (gates): the plane, its points, the target.
func place_points(o: Vector3, n: Vector3, pts: Array, target: String) -> void:
place(o, n, o, o, target)
_points = pts.duplicate()
## The plane the current SURFACE draws on, as [point, normal], from the aim `hit` ({} when
## there is none). face = the aimed face · view = facing you through the 3D cursor ·
## cursor = the cursor's own plane (its up) · grid = the world's level plane at the
## cursor's height. A surface with no plane yet (face, no aim) returns [].
func surface_plane(hit: Dictionary = {}) -> Array:
var cur: Node3D = map.get("_cursor3d") as Node3D
var cp: Vector3 = cur.global_position if cur != null else Vector3.ZERO
match _surface:
"view":
var ray: Dictionary = map._crosshair_ray()
var d: Vector3 = ray.get("dir", Vector3.FORWARD)
return [cp, -d.normalized()]
"cursor":
return [cp, (cur.global_basis.y if cur != null else Vector3.UP).normalized()]
"grid":
return [cp, Vector3.UP]
return [] if hit.is_empty() else [hit["position"], hit.get("normal", Vector3.UP)]
## D: the tool's menu (BoxCutter's pie) — the shape and the surface.
func _open_menu() -> void:
var q: Node = map.get("_q_menu")
if q == null:
return
var rows: Array = []
for s: String in SHAPES:
rows.append(["Shape: %s%s" % [s.capitalize(), " ✓" if s == _shape else ""],
set_tool_setting.bind("shape", s)])
for s: String in SURFACES:
rows.append(["Surface: %s%s" % [s.capitalize(), " ✓" if s == _surface else ""],
set_tool_setting.bind("surface", s)])
# Taper and wedge: the prism's values (BoxCutter binds no key; its T is solidify).
for t: float in [1.0, 0.75, 0.5]:
rows.append(["Taper: %s%s" % [str(t), " ✓" if is_equal_approx(t, _taper) else ""],
set_tool_setting.bind("taper", t)])
for w: Array in [["none", 0.0, 0.0], ["X", 0.5, 0.0], ["Z", 0.0, 0.5]]:
rows.append(["Wedge: %s" % w[0], _set_wedge.bind(float(w[1]), float(w[2]))])
q.open_choices("BoxCutter", rows, map.get_viewport().get_mouse_position())
## Y (BoxCutter's extract): the drawn shape ∩ the targets becomes new hull shapes (an
## "Extract" collection), and NOTHING is cut — no cutter, no op. One gesture.
func _extract_now() -> void:
var fs := _frame()
if fs.is_empty() or _targets().is_empty():
print("[boxcutter] Y: draw a footprint on a target first")
return
var prism: Object = ParamShapes.get_type("prism")
var params: Dictionary = prism.resolve({"poly": fs[2], "taper": _taper, "shear_x": _shear_x, "shear_z": _shear_z})
var cutters: Array = []
for pc: Dictionary in (prism.local_pieces(fs[1], params) as Array):
cutters.append(ShapeFold.transform_planes(pc["planes"], fs[0]))
map._ensure_persistence()
map._persist.begin_gesture("BoxCutter extract")
var made: Array[String] = StackOps.extract_planes(map, _targets(), cutters)
map._persist.end_gesture()
map._persist.refold()
print("[boxcutter] extracted %d shape(s)" % made.size())
ModalControls.summary("BoxCutter extract · %d shape(s)" % made.size())
_phase = Phase.AIM
_frozen = false
_points.clear()
const ShapeFold := preload("res://scenes/maps/shapes/param/shape_fold.gd")
const StackOps := preload("res://scenes/maps/stack_ops.gd")
func _set_wedge(x: float, z: float) -> void:
_shear_x = x
_shear_z = z
update_preview()
## The footprint in the plane's (u, v): {lo: Vector3, w, l, poly: unit [[u, v], …]}, or {}
## when it has no area yet.
func _footprint() -> Dictionary:
match _shape:
"circle", "ngon":
var dv := _b - _a
var r := (dv - _n * dv.dot(_n)).length()
if r < 1.0e-3:
return {}
var n := _segments if _shape == "circle" else _sides
return {"lo": _a - (_u + _v) * r, "w": 2.0 * r, "l": 2.0 * r,
"poly": ParamShapes.get_type("prism").ngon_poly(n)}
"custom":
if _points.size() < 3:
return {}
var u0 := INF
var u1 := -INF
var v0 := INF
var v1 := -INF
for p: Vector3 in _points:
u0 = minf(u0, (p - _o).dot(_u))
u1 = maxf(u1, (p - _o).dot(_u))
v0 = minf(v0, (p - _o).dot(_v))
v1 = maxf(v1, (p - _o).dot(_v))
if u1 - u0 < 1.0e-3 or v1 - v0 < 1.0e-3:
return {}
var poly: Array = []
for p2: Vector3 in _points:
poly.append([((p2 - _o).dot(_u) - u0) / (u1 - u0), ((p2 - _o).dot(_v) - v0) / (v1 - v0)])
return {"lo": _o + _u * u0 + _v * v0, "w": u1 - u0, "l": v1 - v0, "poly": poly}
var r4 := _rect()
var lo: Vector3 = r4[0]
var w := (r4[1] as Vector3 - lo).length()
var l := (r4[3] as Vector3 - lo).length()
if w < 1.0e-3 or l < 1.0e-3:
return {}
return {"lo": lo, "w": w, "l": l, "poly": [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]}
func op_name() -> String:
return "boxcutter"
func preview_color() -> Color:
return Color(1.0, 0.25, 0.2, 0.45) # red: about to cut
func mode() -> String:
return String(MODES[_mode])
func setup_modal(m: ModalControls) -> void:
m.confirm_label = "cut"
m.table([["P", "first corner", func() -> String: return "set — aim for the other" if _phase == Phase.DRAW else "aim, then P"],
["X / I", "mode", func() -> String: return mode()],
["D", "shape / surface", func() -> String: return "%s on %s" % [_shape, _surface]],
["B V 1-3 ⇧G", "modifiers", func() -> String: return _queue_text()],
["L", "live", func() -> String: return "on" if _live else "OFF (written off)"],
["E / Space", "depth", func() -> String: return _depth_text()],
["", "aim", func() -> String: return "" if _valid else "aim at a surface"]])
m.bind("depth", "depth", func() -> float: return float(_depth_steps), _set_depth,
1.0, func() -> String: return " (%s)" % _depth_text(), 1.0)
# THE DEPTH DOT (§17.9): at the footprint's centre, at the depth; dragged into the
# surface. THROUGH becomes its step count at the grab (as E does).
m.handle("depth_dot", "depth", _depth_dot, func() -> Vector3: return -_n,
func(start: float, metres: float) -> float: return start + metres / grid_step(), _depth_begin)
## The depth dot's place, or null before the first corner.
func _depth_dot() -> Variant:
if not _valid or (_phase != Phase.DRAW and not _frozen):
return null
var fp := _footprint()
if fp.is_empty():
return null
var lo: Vector3 = fp["lo"]
return lo + _u * (float(fp["w"]) * 0.5) + _v * (float(fp["l"]) * 0.5) - _n * _depth()
func _depth_begin() -> void:
if _depth_steps == 0:
_depth_steps = maxi(1, roundi(_through() / grid_step()))
func _set_depth(v: float) -> void:
_depth_steps = maxi(0, int(roundf(v)))
update_preview()
func _depth_text() -> String:
return "through" if _depth_steps == 0 else fmt_units(_depth_steps * grid_units())
func status() -> String:
return modal().status_text()
func readout() -> String:
return "%s · %s" % [mode(), _depth_text()] if _valid else ""
func on_activate() -> bool:
_phase = Phase.AIM
_frozen = false
update_preview()
if not _valid:
print("[boxcutter] aim at a surface to draw a cutter on")
return false
return true
func on_deactivate() -> void:
_phase = Phase.AIM
_frozen = false
clear_preview()
func handle_key(event: InputEvent) -> bool:
if not (event is InputEventKey) or not (event as InputEventKey).pressed:
return false
var k := event as InputEventKey
if k.ctrl_pressed or k.alt_pressed:
return false
match KeyRules.key_of(k):
KEY_P:
if _valid:
if _shape == "custom":
if _phase != Phase.DRAW:
_points.clear()
_points.append(_b if _phase == Phase.DRAW else _a)
else:
_a = _b if _phase == Phase.DRAW else _a
_phase = Phase.DRAW
update_preview()
return true
KEY_BACKSPACE:
if _shape == "custom" and not _points.is_empty():
_points.pop_back()
update_preview()
return true
KEY_D:
_open_menu()
return true
# Modifiers while drawing (BoxCutter's): the same key again takes it off.
KEY_B:
_q_chamfer = not _q_chamfer
return true
KEY_V:
_q_array = {"": "array", "array": "radial", "radial": ""}[_q_array]
return true
KEY_1, KEY_2, KEY_3:
var i := int(KeyRules.key_of(k) - KEY_1)
_q_mirror[i] = not bool(_q_mirror[i])
return true
KEY_G:
if not k.shift_pressed:
return false # bare G is grab, not ours
_q_grid = not _q_grid
return true
KEY_L:
_live = not _live
return true
KEY_Y:
_extract_now()
return true
KEY_X:
_mode = (_mode + 1) % MODES.size()
return true
KEY_I:
_mode = MODES.find("inset")
return true
KEY_E:
# A set depth, starting from what THROUGH would be, so [ ] steps from there.
if _depth_steps == 0:
_depth_steps = maxi(1, roundi(_through() / grid_step()))
update_preview()
return true
KEY_SPACE:
_depth_steps = 0
confirm_via_stack()
return true
return modal().handle_key(event)
## Place the drawn rect directly: the plane `o` / `n`, corners `a` and `b` on it, the cut
## going into `target` — what Adjust Last and the gates use instead of aiming.
func place(o: Vector3, n: Vector3, a: Vector3, b: Vector3, target: String) -> void:
_set_plane(o, n)
_a = a
_b = b
_target = target
_phase = Phase.DRAW
_valid = true
_frozen = true
func _set_plane(o: Vector3, n: Vector3) -> void:
_o = o
_n = n.normalized()
var ax := 0
if absf(_n.y) >= absf(_n.x) and absf(_n.y) >= absf(_n.z):
ax = 1
elif absf(_n.z) > absf(_n.x):
ax = 2
var axial := absf(_n[ax]) > 0.999
if axial:
_u = _UNIT[(ax + 1) % 3]
else:
_u = (_n.cross(Vector3.UP) if absf(_n.y) < 0.99 else _n.cross(Vector3.RIGHT)).normalized()
_v = _n.cross(_u)
## A point on the plane, its two in-plane coordinates on the map lattice (an axial plane;
## a slanted one keeps the lattice of its own u / v from the plane's point).
func _snap(p: Vector3) -> Vector3:
var out := p
for i in 3:
var e: Vector3 = _UNIT[i]
if absf(_n.dot(e)) > 0.999:
continue # along the normal: stay on the surface
if absf(_u.dot(e)) > 0.999 or absf(_v.dot(e)) > 0.999:
out[i] = map.snap_to_map_grid(p[i], e, grid_units(), map._map_center, map._aligned_offset)
return out
func _aim_on_plane() -> Variant:
var ray: Dictionary = map._crosshair_ray()
if ray.is_empty():
return null
var dir: Vector3 = ray["dir"]
var den := _n.dot(dir)
if absf(den) < 1.0e-4:
return null
var t := _n.dot(_o - (ray["from"] as Vector3)) / den
return null if t < 0.0 else (ray["from"] as Vector3) + dir * t
func update_preview() -> void:
clear_preview()
# While the depth dot is held the footprint stays where it is (the pointer drives the
# depth, not the corner).
if not _frozen and not modal().grabbing():
if _phase == Phase.AIM:
_valid = false
var hit: Dictionary = map._crosshair_hit()
if hit.is_empty():
return
var brush: int = map._brush_from_hit(hit)
if brush < 0:
return
var b: Dictionary = level.get_brush(brush)
_material = int(b.get("material_id", 0))
_level_id = int(b.get("level_id", 0))
var g := String(level.get_brush_group(brush))
_target = g if g != "" else String(level.get_brush_key(brush))
var pl := surface_plane(hit)
_set_plane(pl[0], pl[1])
# Off the aimed face, the first point is where the aim crosses the chosen plane.
var on: Variant = _aim_on_plane() if _surface != "face" else null
_a = _snap(on if on != null else _o)
_b = _a + (_u + _v) * grid_step()
_valid = true
else:
var p: Variant = _aim_on_plane()
if p != null:
_b = _snap(p)
if _valid:
draw_lines(_edges())
## The rect's corners on the plane, ordered round it: [lo-lo, hi-lo, hi-hi, lo-hi] in (u, v).
func _rect() -> Array:
var ua := (_a - _o).dot(_u)
var ub := (_b - _o).dot(_u)
var va := (_a - _o).dot(_v)
var vb := (_b - _o).dot(_v)
var u0 := minf(ua, ub)
var u1 := maxf(ua, ub)
var v0 := minf(va, vb)
var v1 := maxf(va, vb)
return [_o + _u * u0 + _v * v0, _o + _u * u1 + _v * v0, _o + _u * u1 + _v * v1, _o + _u * u0 + _v * v1]
## How far below the surface the cut reaches: the set depth, or THROUGH the target now.
func _depth() -> float:
if _depth_steps > 0:
return float(_depth_steps) * grid_step()
var d := _through()
return d if d > 0.0 else grid_step()
func _through() -> float:
var pieces: Array = []
for t: String in _targets(): # through EVERY target
var before := pieces.size()
for b: int in (level.get_group_brush_ids(t) as PackedInt32Array):
var bd: Dictionary = level.get_brush(b)
if not bool(bd.get("clearance", false)) and not bool(bd.get("cutter", false)):
pieces.append(bd["planes"])
if pieces.size() == before:
var id: int = level.brush_by_key(t) if level.has_method("brush_by_key") else -1
if id >= 0:
pieces.append(level.get_brush(id)["planes"])
return CutDepth.through(pieces, _o, _n, grid_step())
## [frame, size] of the prism: x = u, y = INTO the surface, z = v (right-handed: u × −n = v);
## the bottom one grid step above the surface, the size reaching the depth below it.
func _frame() -> Array:
var fp := _footprint()
if fp.is_empty():
return []
var m := grid_step()
var f := Transform3D(Basis(_u, -_n, _v), (fp["lo"] as Vector3) + _n * m)
return [f, Vector3(float(fp["w"]), _depth() + m, float(fp["l"])), fp["poly"]]
## The footprint's outline on the surface and at the depth, joined: the cut you will get.
func _edges() -> PackedVector3Array:
var out := PackedVector3Array()
var fp := _footprint()
if fp.is_empty():
return out
var down := -_n * _depth()
var lo: Vector3 = fp["lo"]
var pts: Array = []
for q: Array in (fp["poly"] as Array):
pts.append(lo + _u * (float(q[0]) * float(fp["w"])) + _v * (float(q[1]) * float(fp["l"])))
for i in pts.size():
var p: Vector3 = pts[i]
var q2: Vector3 = pts[(i + 1) % pts.size()]
out.append_array(PackedVector3Array([p, q2, p + down, q2 + down, p, p + down]))
return out
func tool_settings() -> Array:
return [
{"id": "mode", "label": "Mode", "type": "enum", "value": _mode, "choices": MODES,
"tip": "X cycles, I = inset (BoxCutter's keys)."},
{"id": "depth", "label": "Depth", "type": "int", "value": _depth_steps, "min": 0, "step": 1,
"unit": "grid", "tip": "Below the surface, in grid steps; 0 = through (Space)."},
{"id": "shape", "label": "Shape", "type": "enum", "value": SHAPES.find(_shape), "choices": SHAPES,
"tip": "The footprint (D menu): box, circle, ngon, custom polygon."},
{"id": "sides", "label": "Sides", "type": "int", "value": _sides, "min": 3, "max": 64, "step": 1,
"tip": "An ngon's sides."},
{"id": "taper", "label": "Taper", "type": "float", "value": _taper, "min": 0.05, "max": 4.0, "step": 0.05,
"tip": "The far end's size against the footprint (1 = straight)."},
{"id": "shear_x", "label": "Wedge X", "type": "float", "value": _shear_x, "min": -4.0, "max": 4.0, "step": 0.05,
"tip": "The far end's offset along the footprint's x, a fraction of its width."},
{"id": "shear_z", "label": "Wedge Z", "type": "float", "value": _shear_z, "min": -4.0, "max": 4.0, "step": 0.05,
"tip": "The far end's offset along the footprint's z, a fraction of its length."},
{"id": "surface", "label": "Surface", "type": "enum", "value": SURFACES.find(_surface), "choices": SURFACES,
"tip": "Where it is drawn (D menu): the aimed face, the view, the 3D cursor, the grid."},
{"id": "cut", "label": "Cut", "type": "action", "tip": "Commit (LMB / Enter / Space)."},
]
func set_tool_setting(sid: String, value: Variant) -> bool:
match sid:
"mode":
_mode = posmod(int(value), MODES.size())
"depth":
_depth_steps = maxi(0, int(value))
"shape":
_shape = String(value) if value is String else String(SHAPES[posmod(int(value), SHAPES.size())])
"sides":
_sides = clampi(int(value), 3, 64)
"taper":
_taper = clampf(float(value), 0.05, 4.0)
"shear_x":
_shear_x = clampf(float(value), -4.0, 4.0)
"shear_z":
_shear_z = clampf(float(value), -4.0, 4.0)
"surface":
_surface = String(value) if value is String else String(SURFACES[posmod(int(value), SURFACES.size())])
"cut":
confirm_via_stack()
return true
_:
return false
update_preview()
return true
func gesture_label() -> String:
return "BoxCutter %s" % mode()
func confirm() -> bool:
if not _valid or _phase != Phase.DRAW or _target == "":
print("[boxcutter] draw a rect first: aim, P, aim")
return false
var fs := _frame()
if fs.is_empty():
print("[boxcutter] the footprint has no area yet: aim away from the first point (custom: 3 points)")
return false
var size: Vector3 = fs[1]
var prism: Object = ParamShapes.get_type("prism")
map._ensure_persistence()
map._persist.begin_gesture(gesture_label())
var dot: String = map._persist.record("shape.create", {"type": "prism", "gv": prism.GV,
"params": prism.resolve({"poly": fs[2], "taper": _taper, "shear_x": _shear_x, "shear_z": _shear_z}),
"frame": SceneObjects.xform_to(fs[0]),
"size": [size.x, size.y, size.z], "material_id": _material, "level_id": _level_id})
var ok := dot != ""
if ok:
var rec := {"op": mode(), "cutter": dot, "targets": _targets()}
var odot: String = map._persist.record("op.create", rec)
ok = odot != ""
if ok and not _live:
map._persist.record("op.set", {"target": odot, "on": false}) # L: written off
# The queued modifiers, on the CUTTER (ModFold copies its pieces as cutters).
var defaults: Dictionary = map.get("MOD_DEFAULTS") if map.get("MOD_DEFAULTS") is Dictionary else {}
for kp: Array in _mod_params(fs[0]):
var p: Dictionary = (defaults.get(kp[0], {}) as Dictionary).duplicate(true)
p.merge(kp[1] as Dictionary, true)
map._persist.record("mod.create", {"target": dot, "mod": kp[0], "params": p})
# Every cutter in ONE "Cutters" collection (organisation; the wires are the
# cutters overlay's, Shift+Alt+C).
var col := _cutters_collection()
if col != "":
map._persist.record("col.link", {"target": col, "members": ["g:" + dot]})
map._persist.end_gesture()
if not ok:
print("[boxcutter] the journal refused the cut")
return false
map._persist.refold()
if map.has_method("_refresh_brush_view"):
map._refresh_brush_view()
print("[boxcutter] %s %s into %s, %s" % [mode(), dot, _target, _depth_text()])
ModalControls.summary("BoxCutter %s · %s" % [mode(), _depth_text()])
_phase = Phase.AIM
_frozen = false
_points.clear()
_remember_queue()
_clear_queue()
return true