OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / maps / shapes / shape_kit.gd

140 lines · gd ref:
extends RefCounted
class_name ShapeKit
## PLANE BUILDERS every brush shape shares (editor redesign E4, docs/editor-redesign.md §8b).
##
## Moved out of CreateBoxOperator when its three shapes became a registry: a shape says
## which solids fill the bounds, and these are the solids. Static, so a shape plugin and a
## gate call them the same way.
##
## A piece is {planes: PackedFloat32Array (n.x n.y n.z d per plane, n · x <= d), edges:
## PackedVector3Array (line pairs, for the preview)}.


## An axis-aligned piece. `n · x <= d` per plane, so the min side is stated with a
## negative normal — the same convention _box_planes and the importer use.
static func box_piece(lo: Vector3, hi: Vector3) -> Dictionary:
	var p := [
		Vector3(lo.x, lo.y, lo.z), Vector3(hi.x, lo.y, lo.z),
		Vector3(hi.x, lo.y, hi.z), Vector3(lo.x, lo.y, hi.z),
		Vector3(lo.x, hi.y, lo.z), Vector3(hi.x, hi.y, lo.z),
		Vector3(hi.x, hi.y, hi.z), Vector3(lo.x, hi.y, hi.z),
	]
	var edges := PackedVector3Array()
	for pair in [[0,1],[1,2],[2,3],[3,0],[4,5],[5,6],[6,7],[7,4],[0,4],[1,5],[2,6],[3,7]]:
		edges.append(p[pair[0]])
		edges.append(p[pair[1]])
	return {
		"planes": PackedFloat32Array([
			-1, 0, 0, -lo.x,   1, 0, 0, hi.x,
			0, -1, 0, -lo.y,   0, 1, 0, hi.y,
			0, 0, -1, -lo.z,   0, 0, 1, hi.z,
		]),
		"edges": edges,
	}


## A convex 2D profile in the (span, vert) plane, extruded between two positions
## on `tun`. One plane per profile edge plus the two caps.
##
## THE SIGN COMES FROM THE CENTROID, for the reason hex_piece spells out: a
## plane written the other way round bounds nothing, and every cheap invariant is
## unchanged by the flip, so the mistake is invisible until the store returns a
## brush with no volume. Here it also has to survive `up` being negative, which
## reverses the profile's winding — an interior point does not care.
static func prism_piece(profile: Array[Vector2], span: int, vert: int, tun: int,
		w0: float, w1: float) -> Dictionary:
	var n: int = profile.size()
	if n < 3:
		return {}
	var c2 := Vector2.ZERO
	for p: Vector2 in profile:
		c2 += p
	c2 /= float(n)

	var planes := PackedFloat32Array()
	for i in n:
		var a: Vector2 = profile[i]
		var b: Vector2 = profile[(i + 1) % n]
		var e: Vector2 = b - a
		if e.length() < 1.0e-9:
			continue                       # a repeated point contributes no plane
		var nn := Vector2(e.y, -e.x).normalized()
		if nn.dot(c2 - a) > 0.0:
			nn = -nn                       # outward: the centroid is behind it
		var q := Vector3.ZERO
		q[span] = nn.x
		q[vert] = nn.y
		planes.append_array(PackedFloat32Array([q.x, q.y, q.z, nn.dot(a)]))
	var cap := Vector3.ZERO
	cap[tun] = -1.0
	planes.append_array(PackedFloat32Array([cap.x, cap.y, cap.z, -w0]))
	cap[tun] = 1.0
	planes.append_array(PackedFloat32Array([cap.x, cap.y, cap.z, w1]))

	# Edges: both rings plus the rungs between them.
	var r0: Array = _ring(profile, span, vert, tun, w0)
	var r1: Array = _ring(profile, span, vert, tun, w1)
	var edges := PackedVector3Array()
	for i in n:
		edges.append(r0[i]); edges.append(r0[(i + 1) % n])
		edges.append(r1[i]); edges.append(r1[(i + 1) % n])
		edges.append(r0[i]); edges.append(r1[i])
	return {"planes": planes, "edges": edges}


static func _ring(profile: Array[Vector2], span: int, vert: int, tun: int, w: float) -> Array:
	var out: Array = []
	for p: Vector2 in profile:
		var v := Vector3.ZERO
		v[span] = p.x
		v[vert] = p.y
		v[tun] = w
		out.append(v)
	return out


## Six planes from the eight corners of a wedge.
##
## THE SIGN COMES FROM THE CENTROID, NOT FROM THE VERTEX ORDER, and that is
## deliberate. A plane written the other way round bounds nothing, and every
## invariant that would catch it — a face's area, whether its loop closes — is
## unchanged by the flip, so a winding mistake here is invisible until the store
## returns a brush with no volume. An interior point cannot be got backwards.
static func hex_piece(v: Array) -> Dictionary:
	var centroid := Vector3.ZERO
	for p: Vector3 in v:
		centroid += p
	centroid /= float(v.size())

	var planes := PackedFloat32Array()
	# Each triple names one of the six faces: outer, inner, the two radial ends, and
	# the two tunnel caps.
	for tri: Array in [[0, 1, 3], [4, 5, 7], [0, 1, 5], [2, 3, 7], [0, 2, 6], [1, 3, 7]]:
		var a: Vector3 = v[tri[0]]
		var b: Vector3 = v[tri[1]]
		var c: Vector3 = v[tri[2]]
		var n: Vector3 = (b - a).cross(c - a)
		if n.length() < 1.0e-9:
			return {}          # degenerate mid-drag; the caller drops the piece
		n = n.normalized()
		var d: float = n.dot(a)
		if n.dot(centroid) > d:
			n = -n
			d = -d
		planes.append_array(PackedFloat32Array([n.x, n.y, n.z, d]))

	var edges := PackedVector3Array()
	for pair in [[0,1],[1,3],[3,2],[2,0],[4,5],[5,7],[7,6],[6,4],[0,4],[1,5],[2,6],[3,7]]:
		edges.append(v[pair[0]])
		edges.append(v[pair[1]])
	return {"planes": planes, "edges": edges}


## A distance in metres, rounded UP to a whole number of grid steps. Never zero:
## a margin that quantized to nothing would leave the shape with no clearance at
## all and look like the feature was not wired.
static func lattice(metres: float, step: float) -> float:
	if step <= 1.0e-6:
		return metres
	return maxf(1.0, ceilf(metres / step)) * step