extends LevelOperator
## A SHAPE'S FACE HANDLE (#120 §17.7 Q3a, docs/node-tree-and-stacks.md §17.8 step 3;
## TrenchBroom's face drag, Blender's gizmo arrow): pressed on one of the six dots a
## selected shape shows, it follows the pointer along that face's axis; the opposite face
## stays where it is. The size snaps to the grid. Release writes ONE gesture (shape.size +
## shape.xform, through map.set_shape_extent); Esc writes nothing.
##
## The map sets it up (setup) and activates it from its click-to-select, before the pick.
## The smallest extent a face can be dragged to (a shape with no thickness folds to no brush).
const MIN_EXTENT := 0.05
var _dot := ""
var _axis := 0 # the frame axis: 0 x, 1 y, 2 z
var _sign := 1 # +1 the far face, -1 the near (origin) face
var _frame := Transform3D.IDENTITY # at the press
var _size := Vector3.ONE
var _t0 := 0.0 # the pointer's place along the axis at the press
var _d := 0.0 # the drag, snapped
func setup(dot: String, axis: int, sign: int) -> void:
_dot = dot
_axis = axis
_sign = sign
func op_name() -> String:
return "face handle"
func preview_color() -> Color:
return Color(0.4, 0.85, 1.0, 0.9)
## NOT a continuous grab: a face drag is short (TrenchBroom's does not wrap either), and the
## pointer is read every frame regardless.
func follows_aim() -> bool:
return false
## Pressed to start, released to write (a gizmo drag), not click-click like the tools.
func confirms_on_release() -> bool:
return true
func setup_modal(m: ModalControls) -> void:
m.confirm_label = "resize"
m.table([["", "face", func() -> String: return "%s%s" % ["+" if _sign > 0 else "-", "xyz"[_axis]]],
["", "by", func() -> String: return "%+.2f m" % _d]])
func status() -> String:
return modal().status_text()
## The face's centre and outward unit axis for the state at the press.
func _face() -> Array:
var u: Vector3 = _frame.basis[_axis].normalized()
var c := _size * 0.5
c[_axis] = _size[_axis] if _sign > 0 else 0.0
return [_frame * c, u * float(_sign)]
## Where the pointer's ray passes the face's axis line, as a distance along the axis.
func _along() -> float:
var ray: Dictionary = map._crosshair_ray()
if ray.is_empty():
return _t0
var f := _face()
var a: Vector3 = f[0]
var u: Vector3 = f[1]
var from: Vector3 = ray["from"]
var cp: PackedVector3Array = Geometry3D.get_closest_points_between_segments(a - u * 10000.0, a + u * 10000.0,
from, from + (ray["dir"] as Vector3) * 10000.0)
return (cp[0] - a).dot(u)
func on_activate() -> bool:
var st: Dictionary = map._persist.shape_state(_dot) if _dot != "" else {}
if st.is_empty():
return false
_frame = st["frame"]
_size = st["size"]
_d = 0.0
_t0 = _along()
update_preview()
return true
func on_deactivate() -> void:
clear_preview()
## The frame and size the drag gives: the dragged face moves by _d (outward positive),
## the opposite face stays.
func result() -> Array:
var size := _size
var ext := maxf(MIN_EXTENT, _size[_axis] + _d)
var grow := ext - _size[_axis]
size[_axis] = ext
var frame := _frame
if _sign < 0:
frame.origin -= _frame.basis[_axis].normalized() * grow
return [frame, size]
func update_preview() -> void:
clear_preview()
var step: float = float(map.grid_units()) * float(map.world_scale()) if map.has_method("grid_units") else 0.25
var raw := _along() - _t0
_d = roundf(raw / step) * step if step > 0.0 else raw
var r := result()
var fr: Transform3D = r[0]
var sz: Vector3 = r[1]
var c := PackedVector3Array()
for i in 8:
c.append(fr * Vector3(sz.x if i & 1 else 0.0, sz.y if i & 2 else 0.0, sz.z if i & 4 else 0.0))
var e := PackedVector3Array()
for pr: Array in [[0, 1], [2, 3], [4, 5], [6, 7], [0, 2], [1, 3], [4, 6], [5, 7], [0, 4], [1, 5], [2, 6], [3, 7]]:
e.append(c[pr[0]])
e.append(c[pr[1]])
draw_lines(e)
func confirm() -> bool:
if is_zero_approx(_d):
return false
var r := result()
if not map.set_shape_extent(_dot, r[0], r[1]):
print("[face handle] the journal refused that size")
return false
ModalControls.summary("Resized %s%s by %+.2f m" % ["+" if _sign > 0 else "-", "xyz"[_axis], _d])
return true