| … | … | @@ -53,6 +53,84 @@ var _segments := 24 # circle |
| 53 | 53 | var _points: Array = [] # custom: the P points, on the plane |
| 54 | 54 | var _surface := "face" # which plane the footprint is drawn on |
| 55 | 55 | |
| 56 | # ---- step 5: modifiers while drawing (§13.4, BoxCutter's keys) ---- |
| 57 | ## The queue, written as mod.create records on the cutter at commit and then emptied: |
| 58 | ## B chamfer · V array (V V radial) · 1 / 2 / 3 mirror x / y / z · Shift+G grid. The same |
| 59 | ## key again takes it off. Mirror and radial work about the TARGET's centre, as |
| 60 | ## BoxCutter's do about the object. |
| 61 | var _q_chamfer := false |
| 62 | var _q_array := "" # "" / "array" / "radial" |
| 63 | var _q_mirror := [false, false, false] |
| 64 | var _q_grid := false |
| 65 | var _live := true # L: false = the cut is written OFF |
| 66 | var _taper := 1.0 # the prism's (D menu) |
| 67 | var _shear_x := 0.0 |
| 68 | var _shear_z := 0.0 |
| 69 | |
| 70 | |
| 71 | func _queue_text() -> String: |
| 72 | var q: Array = [] |
| 73 | for i in 3: |
| 74 | if _q_mirror[i]: |
| 75 | q.append("mirror " + "xyz"[i]) |
| 76 | if _q_array != "": |
| 77 | q.append(_q_array) |
| 78 | if _q_grid: |
| 79 | q.append("grid") |
| 80 | if _q_chamfer: |
| 81 | q.append("chamfer") |
| 82 | return ", ".join(q) if not q.is_empty() else "none" |
| 83 | |
| 84 | |
| 85 | func _clear_queue() -> void: |
| 86 | _q_chamfer = false |
| 87 | _q_array = "" |
| 88 | _q_mirror = [false, false, false] |
| 89 | _q_grid = false |
| 90 | _live = true |
| 91 | |
| 92 | |
| 93 | ## The target's centre: the middle of its solid pieces' corners. |
| 94 | func _target_centre() -> Vector3: |
| 95 | var lo := Vector3(INF, INF, INF) |
| 96 | var hi := -lo |
| 97 | for b: int in (level.get_group_brush_ids(_target) as PackedInt32Array): |
| 98 | var bd: Dictionary = level.get_brush(b) |
| 99 | if bool(bd.get("clearance", false)) or bool(bd.get("cutter", false)): |
| 100 | continue |
| 101 | for c: Vector3 in FaceKit.corners(bd["planes"]): |
| 102 | lo = lo.min(c) |
| 103 | hi = hi.max(c) |
| 104 | return _o if lo.x > hi.x else (lo + hi) * 0.5 |
| 105 | |
| 106 | |
| 107 | ## The queued modifiers as [kind, params] for the cutter framed `f` (the prism's frame): |
| 108 | ## mirror's offset puts its plane through the target's centre along that frame axis; |
| 109 | ## radial pivots there. The rest take the map's defaults. |
| 110 | func _mod_params(f: Transform3D) -> Array: |
| 111 | var out: Array = [] |
| 112 | var c := _target_centre() |
| 113 | var b := f.basis.orthonormalized() |
| 114 | # 1 / 2 / 3 are the WORLD's X / Y / Z (BoxCutter's are the object's); ModFold mirrors along |
| 115 | # a FRAME axis, so each world axis goes to the frame axis most nearly parallel to it. |
| 116 | for k in 3: |
| 117 | if not _q_mirror[k]: |
| 118 | continue |
| 119 | var best := 0 |
| 120 | for i in 3: |
| 121 | if absf(b[i][k]) > absf(b[best][k]): |
| 122 | best = i |
| 123 | out.append(["mirror", {"axis": "xyz"[best], "offset": b[best].dot(c - f.origin)}]) |
| 124 | if _q_array == "array": |
| 125 | out.append(["array", {}]) |
| 126 | elif _q_array == "radial": |
| 127 | out.append(["radial", {"pivot": [c.x, c.y, c.z]}]) |
| 128 | if _q_grid: |
| 129 | out.append(["grid", {}]) |
| 130 | if _q_chamfer: |
| 131 | out.append(["chamfer", {}]) |
| 132 | return out |
| 133 | |
| 56 | 134 | |
| 57 | 135 | ## Place a CUSTOM footprint directly (gates): the plane, its points, the target. |
| 58 | 136 | func place_points(o: Vector3, n: Vector3, pts: Array, target: String) -> void: |
| … | … | @@ -91,9 +169,21 @@ func _open_menu() -> void: |
| 91 | 169 | for s: String in SURFACES: |
| 92 | 170 | rows.append(["Surface: %s%s" % [s.capitalize(), " ✓" if s == _surface else ""], |
| 93 | 171 | set_tool_setting.bind("surface", s)]) |
| 172 | # Taper and wedge: the prism's values (BoxCutter binds no key; its T is solidify). |
| 173 | for t: float in [1.0, 0.75, 0.5]: |
| 174 | rows.append(["Taper: %s%s" % [str(t), " ✓" if is_equal_approx(t, _taper) else ""], |
| 175 | set_tool_setting.bind("taper", t)]) |
| 176 | for w: Array in [["none", 0.0, 0.0], ["X", 0.5, 0.0], ["Z", 0.0, 0.5]]: |
| 177 | rows.append(["Wedge: %s" % w[0], _set_wedge.bind(float(w[1]), float(w[2]))]) |
| 94 | 178 | q.open_choices("BoxCutter", rows, map.get_viewport().get_mouse_position()) |
| 95 | 179 | |
| 96 | 180 | |
| 181 | func _set_wedge(x: float, z: float) -> void: |
| 182 | _shear_x = x |
| 183 | _shear_z = z |
| 184 | update_preview() |
| 185 | |
| 186 | |
| 97 | 187 | ## The footprint in the plane's (u, v): {lo: Vector3, w, l, poly: unit [[u, v], …]}, or {} |
| 98 | 188 | ## when it has no area yet. |
| 99 | 189 | func _footprint() -> Dictionary: |
| … | … | @@ -150,6 +240,8 @@ func setup_modal(m: ModalControls) -> void: |
| 150 | 240 | m.table([["P", "first corner", func() -> String: return "set — aim for the other" if _phase == Phase.DRAW else "aim, then P"], |
| 151 | 241 | ["X / I", "mode", func() -> String: return mode()], |
| 152 | 242 | ["D", "shape / surface", func() -> String: return "%s on %s" % [_shape, _surface]], |
| 243 | ["B V 1-3 ⇧G", "modifiers", func() -> String: return _queue_text()], |
| 244 | ["L", "live", func() -> String: return "on" if _live else "OFF (written off)"], |
| 153 | 245 | ["E / Space", "depth", func() -> String: return _depth_text()], |
| 154 | 246 | ["", "aim", func() -> String: return "" if _valid else "aim at a surface"]]) |
| 155 | 247 | m.bind("depth", "depth", func() -> float: return float(_depth_steps), _set_depth, |
| … | … | @@ -215,6 +307,25 @@ func handle_key(event: InputEvent) -> bool: |
| 215 | 307 | KEY_D: |
| 216 | 308 | _open_menu() |
| 217 | 309 | return true |
| 310 | # Modifiers while drawing (BoxCutter's): the same key again takes it off. |
| 311 | KEY_B: |
| 312 | _q_chamfer = not _q_chamfer |
| 313 | return true |
| 314 | KEY_V: |
| 315 | _q_array = {"": "array", "array": "radial", "radial": ""}[_q_array] |
| 316 | return true |
| 317 | KEY_1, KEY_2, KEY_3: |
| 318 | var i := int(KeyRules.key_of(k) - KEY_1) |
| 319 | _q_mirror[i] = not bool(_q_mirror[i]) |
| 320 | return true |
| 321 | KEY_G: |
| 322 | if not k.shift_pressed: |
| 323 | return false # bare G is grab, not ours |
| 324 | _q_grid = not _q_grid |
| 325 | return true |
| 326 | KEY_L: |
| 327 | _live = not _live |
| 328 | return true |
| 218 | 329 | KEY_X: |
| 219 | 330 | _mode = (_mode + 1) % MODES.size() |
| 220 | 331 | return true |
| … | … | @@ -391,6 +502,12 @@ func tool_settings() -> Array: |
| 391 | 502 | "tip": "The footprint (D menu): box, circle, ngon, custom polygon."}, |
| 392 | 503 | {"id": "sides", "label": "Sides", "type": "int", "value": _sides, "min": 3, "max": 64, "step": 1, |
| 393 | 504 | "tip": "An ngon's sides."}, |
| 505 | {"id": "taper", "label": "Taper", "type": "float", "value": _taper, "min": 0.05, "max": 4.0, "step": 0.05, |
| 506 | "tip": "The far end's size against the footprint (1 = straight)."}, |
| 507 | {"id": "shear_x", "label": "Wedge X", "type": "float", "value": _shear_x, "min": -4.0, "max": 4.0, "step": 0.05, |
| 508 | "tip": "The far end's offset along the footprint's x, a fraction of its width."}, |
| 509 | {"id": "shear_z", "label": "Wedge Z", "type": "float", "value": _shear_z, "min": -4.0, "max": 4.0, "step": 0.05, |
| 510 | "tip": "The far end's offset along the footprint's z, a fraction of its length."}, |
| 394 | 511 | {"id": "surface", "label": "Surface", "type": "enum", "value": SURFACES.find(_surface), "choices": SURFACES, |
| 395 | 512 | "tip": "Where it is drawn (D menu): the aimed face, the view, the 3D cursor, the grid."}, |
| 396 | 513 | {"id": "cut", "label": "Cut", "type": "action", "tip": "Commit (LMB / Enter / Space)."}, |
| … | … | @@ -407,6 +524,12 @@ func set_tool_setting(sid: String, value: Variant) -> bool: |
| 407 | 524 | _shape = String(value) if value is String else String(SHAPES[posmod(int(value), SHAPES.size())]) |
| 408 | 525 | "sides": |
| 409 | 526 | _sides = clampi(int(value), 3, 64) |
| 527 | "taper": |
| 528 | _taper = clampf(float(value), 0.05, 4.0) |
| 529 | "shear_x": |
| 530 | _shear_x = clampf(float(value), -4.0, 4.0) |
| 531 | "shear_z": |
| 532 | _shear_z = clampf(float(value), -4.0, 4.0) |
| 410 | 533 | "surface": |
| 411 | 534 | _surface = String(value) if value is String else String(SURFACES[posmod(int(value), SURFACES.size())]) |
| 412 | 535 | "cut": |
| … | … | @@ -435,12 +558,22 @@ func confirm() -> bool: |
| 435 | 558 | map._ensure_persistence() |
| 436 | 559 | map._persist.begin_gesture(gesture_label()) |
| 437 | 560 | var dot: String = map._persist.record("shape.create", {"type": "prism", "gv": prism.GV, |
| 438 | | "params": prism.resolve({"poly": fs[2]}), "frame": SceneObjects.xform_to(fs[0]), |
| 561 | "params": prism.resolve({"poly": fs[2], "taper": _taper, "shear_x": _shear_x, "shear_z": _shear_z}), |
| 562 | "frame": SceneObjects.xform_to(fs[0]), |
| 439 | 563 | "size": [size.x, size.y, size.z], "material_id": _material, "level_id": _level_id}) |
| 440 | 564 | var ok := dot != "" |
| 441 | 565 | if ok: |
| 442 | 566 | var rec := {"op": mode(), "cutter": dot, "targets": [_target]} |
| 443 | | ok = map._persist.record("op.create", rec) != "" |
| 567 | var odot: String = map._persist.record("op.create", rec) |
| 568 | ok = odot != "" |
| 569 | if ok and not _live: |
| 570 | map._persist.record("op.set", {"target": odot, "on": false}) # L: written off |
| 571 | # The queued modifiers, on the CUTTER (ModFold copies its pieces as cutters). |
| 572 | var defaults: Dictionary = map.get("MOD_DEFAULTS") if map.get("MOD_DEFAULTS") is Dictionary else {} |
| 573 | for kp: Array in _mod_params(fs[0]): |
| 574 | var p: Dictionary = (defaults.get(kp[0], {}) as Dictionary).duplicate(true) |
| 575 | p.merge(kp[1] as Dictionary, true) |
| 576 | map._persist.record("mod.create", {"target": dot, "mod": kp[0], "params": p}) |
| 444 | 577 | map._persist.end_gesture() |
| 445 | 578 | if not ok: |
| 446 | 579 | print("[boxcutter] the journal refused the cut") |
| … | … | @@ -453,4 +586,5 @@ func confirm() -> bool: |
| 453 | 586 | _phase = Phase.AIM |
| 454 | 587 | _frozen = false |
| 455 | 588 | _points.clear() |
| 589 | _clear_queue() |
| 456 | 590 | return true |