OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / maps / operators / shape_handle_operator.gd

135 lines · gd ref:
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