scopedvr-map-editor/addons/modular_builder_en/modular_piece_geometry.gd

748 lines
33 KiB
GDScript

@tool
class_name ModularBuilderPieceGeometry
extends Node3D
const DETAIL_SCRIPT_PATH := "res://addons/modular_builder_en/modular_building_detail.gd"
const DETAIL_TYPE_HATCH := 2
const DETAIL_TYPE_PILLAR := 3
enum PieceType { FOUNDATION_SQUARE, FOUNDATION_TRIANGLE, FLOOR, WALL, BUILDING, GABLE_WALL, ROOF_GABLE, ROOF_SHED, GABLE_WALL_RIGHT, ROOF_PYRAMID, STAIRS, FENCE }
@export var piece_type: PieceType = PieceType.FOUNDATION_SQUARE:
set(value):
piece_type = value
notify_property_list_changed()
_rebuild()
@export_range(0.5, 8.0, 0.001, "suffix:m") var unit_size: float = 2.0:
set(value):
unit_size = value
_rebuild()
@export_range(0.01, 1.0, 0.001, "suffix:m") var thickness: float = 0.2:
set(value):
thickness = value
_rebuild()
for child in get_children():
if child.has_method("_on_host_thickness_changed"):
child.call("_on_host_thickness_changed")
@export_range(0.1, 8.0, 0.001, "suffix:m") var rise: float = 1.0:
set(value):
rise = value
_rebuild()
@export_range(0.01, 32.0, 0.001, "suffix:m") var roof_width: float = 2.0:
set(value):
roof_width = value
_rebuild()
@export_range(1, 100, 1, "suffix: steps") var stair_count: int = 10:
set(value):
stair_count = value
_rebuild()
@export_range(0.01, 8.0, 0.001, "suffix:m") var stair_width: float = 1.0:
set(value):
stair_width = value
_rebuild()
@export_range(0, 100, 1, "suffix: posts") var fence_divisions: int = 1:
set(value):
fence_divisions = value
_rebuild()
@export_range(0.1, 8.0, 0.001, "suffix:m") var fence_height: float = 1.0:
set(value):
fence_height = value
_rebuild()
@export var symmetric_dimensions: bool = true:
set(value):
if symmetric_dimensions == value:
return
if not value:
_capture_mesh_dimensions()
symmetric_dimensions = value
notify_property_list_changed()
if value:
scale = Vector3.ONE
else:
_apply_dimension_scale()
@export_range(0.01, 100.0, 0.001, "suffix:m") var dimension_width: float = 2.0:
set(value):
dimension_width = value
_apply_dimension_scale()
@export_range(0.01, 100.0, 0.001, "suffix:m") var dimension_height: float = 2.0:
set(value):
dimension_height = value
_apply_dimension_scale()
@export_range(0.01, 100.0, 0.001, "suffix:m") var dimension_depth: float = 2.0:
set(value):
dimension_depth = value
_apply_dimension_scale()
@export_storage var _dimension_reference: Vector3 = Vector3.ZERO
@export var center_align: bool = false:
set(value):
center_align = value
_rebuild()
@export var surface_material: Material:
set(value):
surface_material = value
_rebuild()
@export var custom_points: PackedVector3Array = PackedVector3Array():
set(value):
custom_points = value
_rebuild()
@export var centered_origin: bool = false:
set(value):
centered_origin = value
_rebuild()
func _rebuild() -> void:
pass
func _capture_mesh_dimensions() -> void:
var geometry := get_node_or_null("Geometry") as MeshInstance3D
if geometry == null or geometry.mesh == null:
return
var dimensions := geometry.mesh.get_aabb().size
if dimensions.x > 0.001:
dimension_width = dimensions.x
if dimensions.y > 0.001:
dimension_height = dimensions.y
if dimensions.z > 0.001:
dimension_depth = dimensions.z
_dimension_reference = dimensions
func _apply_dimension_scale() -> void:
if symmetric_dimensions or _dimension_reference.x <= 0.001 or _dimension_reference.y <= 0.001 or _dimension_reference.z <= 0.001:
return
var target := Vector3(dimension_width, dimension_height, dimension_depth)
if piece_type == PieceType.FOUNDATION_SQUARE or piece_type == PieceType.FOUNDATION_TRIANGLE or piece_type == PieceType.FLOOR:
target.y = _dimension_reference.y
if piece_type == PieceType.WALL:
target.z = _dimension_reference.z
scale = Vector3(target.x / _dimension_reference.x, target.y / _dimension_reference.y, target.z / _dimension_reference.z)
func _make_mesh() -> ArrayMesh:
if piece_type == PieceType.GABLE_WALL and custom_points.size() >= 2:
return _make_gable_wall_mesh()
if piece_type == PieceType.GABLE_WALL_RIGHT and custom_points.size() >= 2:
return _make_right_gable_wall_mesh()
if (piece_type == PieceType.ROOF_GABLE or piece_type == PieceType.ROOF_SHED or piece_type == PieceType.ROOF_PYRAMID) and custom_points.size() >= 2:
return _make_roof_mesh()
if piece_type == PieceType.STAIRS and custom_points.size() >= 2:
return _make_stair_mesh()
if piece_type == PieceType.FENCE and custom_points.size() >= 2:
return _make_fence_mesh()
if custom_points.size() >= 2 and piece_type == PieceType.WALL:
return _make_custom_wall_mesh()
if custom_points.size() >= 3 and piece_type == PieceType.FLOOR:
return _make_custom_floor_mesh()
if custom_points.size() >= 3 and (piece_type == PieceType.FOUNDATION_SQUARE or piece_type == PieceType.FOUNDATION_TRIANGLE):
return _make_custom_foundation_mesh()
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var s := unit_size
var polygon: Array[Vector2]
match piece_type:
PieceType.FOUNDATION_TRIANGLE:
polygon = [Vector2(-s * 0.5, -s * 0.5), Vector2(s * 0.5, -s * 0.5), Vector2(-s * 0.5, s * 0.5)]
_:
polygon = [Vector2(-s * 0.5, -s * 0.5), Vector2(s * 0.5, -s * 0.5), Vector2(s * 0.5, s * 0.5), Vector2(-s * 0.5, s * 0.5)]
var y0 := -thickness * 0.5 if centered_origin else 0.0
var y1 := thickness * 0.5 if centered_origin else thickness
if piece_type == PieceType.WALL:
polygon = [Vector2(-s * 0.5, -thickness * 0.5), Vector2(s * 0.5, -thickness * 0.5), Vector2(s * 0.5, thickness * 0.5), Vector2(-s * 0.5, thickness * 0.5)]
y0 = -s * 0.5 if centered_origin else 0.0
y1 = s * 0.5 if centered_origin else s
if piece_type == PieceType.FOUNDATION_SQUARE or piece_type == PieceType.FOUNDATION_TRIANGLE:
var hatch_mesh := _make_hatch_surface_mesh(polygon, y1, y0)
if hatch_mesh != null:
return hatch_mesh
for i in range(1, polygon.size() - 1):
_add_tri(vertices, normals, Vector3(polygon[0].x, y1, polygon[0].y), Vector3(polygon[i + 1].x, y1, polygon[i + 1].y), Vector3(polygon[i].x, y1, polygon[i].y))
_add_tri(vertices, normals, Vector3(polygon[0].x, y0, polygon[0].y), Vector3(polygon[i].x, y0, polygon[i].y), Vector3(polygon[i + 1].x, y0, polygon[i + 1].y))
for i in range(polygon.size()):
var j := (i + 1) % polygon.size()
var a := polygon[i]
var b := polygon[j]
_add_tri(vertices, normals, Vector3(a.x, y0, a.y), Vector3(b.x, y0, b.y), Vector3(b.x, y1, b.y))
_add_tri(vertices, normals, Vector3(a.x, y0, a.y), Vector3(b.x, y1, b.y), Vector3(a.x, y1, a.y))
var arrays: Array = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = normals
var result := ArrayMesh.new()
result.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return result
func _make_gable_wall_mesh() -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var base_points := _get_centered_gable_base() if center_align else PackedVector3Array([custom_points[0], custom_points[1]])
var a: Vector3 = base_points[0]
var b: Vector3 = base_points[1]
var direction := (b - a).normalized()
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * thickness * 0.5
var peak := (a + b) * 0.5 + Vector3.UP * rise
var front_a := a + side
var front_b := b + side
var front_peak := peak + side
var back_a := a - side
var back_b := b - side
var back_peak := peak - side
_add_tri(vertices, normals, front_a, front_b, front_peak)
_add_tri(vertices, normals, back_b, back_a, back_peak)
_add_quad(vertices, normals, front_a, back_a, back_b, front_b)
_add_quad(vertices, normals, front_b, back_b, back_peak, front_peak)
_add_quad(vertices, normals, front_peak, back_peak, back_a, front_a)
return _array_mesh(vertices, normals)
func _make_right_gable_wall_mesh() -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var base_points := _get_centered_gable_base() if center_align else PackedVector3Array([custom_points[0], custom_points[1]])
var a: Vector3 = base_points[0]
var b: Vector3 = base_points[1]
var direction := (b - a).normalized()
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * thickness * 0.5
var peak := b + Vector3.UP * rise
var front_a := a + side
var front_b := b + side
var front_peak := peak + side
var back_a := a - side
var back_b := b - side
var back_peak := peak - side
_add_tri(vertices, normals, front_a, front_b, front_peak)
_add_tri(vertices, normals, back_peak, back_b, back_a)
_add_quad(vertices, normals, front_a, back_a, back_b, front_b)
_add_quad(vertices, normals, front_b, back_b, back_peak, front_peak)
_add_quad(vertices, normals, front_peak, back_peak, back_a, front_a)
return _array_mesh(vertices, normals)
func _make_roof_mesh() -> ArrayMesh:
if piece_type == PieceType.ROOF_SHED and custom_points.size() == 2:
return _make_shed_roof_mesh()
if custom_points.size() >= 4:
return _make_oriented_roof_mesh()
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var first: Vector3 = custom_points[0]
var second: Vector3 = custom_points[1]
var min_x := minf(first.x, second.x)
var max_x := maxf(first.x, second.x)
var min_z := minf(first.z, second.z)
var max_z := maxf(first.z, second.z)
var span_x := max_x - min_x
var span_z := max_z - min_z
if span_x < 0.02 or span_z < 0.02:
return _array_mesh(vertices, normals)
var panels: Array = []
if piece_type == PieceType.ROOF_GABLE:
if span_x <= span_z:
var ridge_x := (min_x + max_x) * 0.5
panels.append([Vector3(min_x, 0.0, min_z), Vector3(min_x, 0.0, max_z), Vector3(ridge_x, rise, max_z), Vector3(ridge_x, rise, min_z)])
panels.append([Vector3(ridge_x, rise, min_z), Vector3(ridge_x, rise, max_z), Vector3(max_x, 0.0, max_z), Vector3(max_x, 0.0, min_z)])
else:
var ridge_z := (min_z + max_z) * 0.5
panels.append([Vector3(min_x, 0.0, min_z), Vector3(max_x, 0.0, min_z), Vector3(max_x, rise, ridge_z), Vector3(min_x, rise, ridge_z)])
panels.append([Vector3(min_x, rise, ridge_z), Vector3(max_x, rise, ridge_z), Vector3(max_x, 0.0, max_z), Vector3(min_x, 0.0, max_z)])
else:
if span_x <= span_z:
var low_x := first.x
var high_x := second.x
panels.append([Vector3(low_x, 0.0, min_z), Vector3(low_x, 0.0, max_z), Vector3(high_x, rise, max_z), Vector3(high_x, rise, min_z)])
else:
var low_z := first.z
var high_z := second.z
panels.append([Vector3(min_x, 0.0, low_z), Vector3(max_x, 0.0, low_z), Vector3(max_x, rise, high_z), Vector3(min_x, rise, high_z)])
for panel in panels:
_append_solid_panel(vertices, normals, panel)
return _array_mesh(vertices, normals)
func _make_shed_roof_mesh() -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var low: Vector3 = custom_points[0]
var high: Vector3 = custom_points[1]
var slope := high - low
var horizontal := Vector3(slope.x, 0.0, slope.z)
if horizontal.length_squared() < 0.0001 or roof_width <= 0.0:
return _array_mesh(vertices, normals)
var across := Vector3(horizontal.z, 0.0, -horizontal.x).normalized() * roof_width * 0.5
var panel := [low - across, low + across, high + across, high - across]
_append_solid_panel(vertices, normals, panel)
return _array_mesh(vertices, normals)
func _make_oriented_roof_mesh() -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var p0: Vector3 = custom_points[0]
var p1: Vector3 = custom_points[1]
var p2: Vector3 = custom_points[2]
var p3: Vector3 = custom_points[3]
var edge_01 := Vector2(p1.x - p0.x, p1.z - p0.z).length()
var edge_12 := Vector2(p2.x - p1.x, p2.z - p1.z).length()
var panels: Array = []
if piece_type == PieceType.ROOF_GABLE:
if edge_01 <= edge_12:
var ridge_01 := (p0 + p1) * 0.5 + Vector3.UP * rise
var ridge_23 := (p2 + p3) * 0.5 + Vector3.UP * rise
panels.append([p0, p3, ridge_23, ridge_01])
panels.append([ridge_01, ridge_23, p2, p1])
else:
var ridge_12 := (p1 + p2) * 0.5 + Vector3.UP * rise
var ridge_30 := (p3 + p0) * 0.5 + Vector3.UP * rise
panels.append([p0, p1, ridge_12, ridge_30])
panels.append([ridge_30, ridge_12, p2, p3])
elif piece_type == PieceType.ROOF_PYRAMID:
var peak := (p0 + p1 + p2 + p3) * 0.25 + Vector3.UP * rise
panels.append([p0, p1, peak])
panels.append([p1, p2, peak])
panels.append([p2, p3, peak])
panels.append([p3, p0, peak])
else:
# The four points define the low and high edges. Keep their centers
# fixed when adjusting both edge widths in the Inspector.
var base_width := Vector2(p1.x - p0.x, p1.z - p0.z).length()
if base_width > 0.001 and roof_width > 0.0:
var direction := Vector3(p1.x - p0.x, 0.0, p1.z - p0.z).normalized()
var low_center := (p0 + p1) * 0.5
var high_center := (p2 + p3) * 0.5
var half_width := roof_width * 0.5
panels.append([
low_center - direction * half_width,
low_center + direction * half_width,
high_center + direction * half_width,
high_center - direction * half_width,
])
else:
panels.append([p0, p1, p2, p3])
for panel in panels:
_append_solid_panel(vertices, normals, panel)
return _array_mesh(vertices, normals)
func _make_stair_mesh() -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var start: Vector3 = custom_points[0]
var finish: Vector3 = custom_points[1]
var horizontal := Vector3(finish.x - start.x, 0.0, finish.z - start.z)
var run := horizontal.length()
var rise_amount := finish.y - start.y
if run < 0.05 or rise_amount <= 0.05 or stair_count < 1 or stair_width <= 0.0:
return _array_mesh(vertices, normals)
var forward := horizontal / run
var width_axis := Vector3(-forward.z, 0.0, forward.x) * stair_width * 0.5
var profile := PackedVector2Array([Vector2(0.0, 0.0), Vector2(run, 0.0), Vector2(run, rise_amount)])
for step_index in range(stair_count - 1, -1, -1):
var tread_start := run * float(step_index) / stair_count
var tread_end := run * float(step_index + 1) / stair_count
var tread_height := rise_amount * float(step_index + 1) / stair_count
profile.append(Vector2(tread_start, tread_height))
if step_index > 0:
profile.append(Vector2(tread_start, rise_amount * float(step_index) / stair_count))
var triangulation := Geometry2D.triangulate_polygon(profile)
for side_sign in [-1.0, 1.0]:
for triangle_index in range(0, triangulation.size(), 3):
var a := profile[triangulation[triangle_index]]
var b := profile[triangulation[triangle_index + 1]]
var c := profile[triangulation[triangle_index + 2]]
var point_a: Vector3 = start + forward * a.x + Vector3.UP * a.y + width_axis * side_sign
var point_b: Vector3 = start + forward * b.x + Vector3.UP * b.y + width_axis * side_sign
var point_c: Vector3 = start + forward * c.x + Vector3.UP * c.y + width_axis * side_sign
if side_sign > 0.0:
_add_tri(vertices, normals, point_a, point_b, point_c)
else:
_add_tri(vertices, normals, point_c, point_b, point_a)
for profile_index in range(profile.size()):
var next_index := (profile_index + 1) % profile.size()
var a := profile[profile_index]
var b := profile[next_index]
var point_a_left := start + forward * a.x + Vector3.UP * a.y - width_axis
var point_b_left := start + forward * b.x + Vector3.UP * b.y - width_axis
var point_b_right := start + forward * b.x + Vector3.UP * b.y + width_axis
var point_a_right := start + forward * a.x + Vector3.UP * a.y + width_axis
_add_quad(vertices, normals, point_a_left, point_b_left, point_b_right, point_a_right)
return _array_mesh(vertices, normals)
func _make_fence_mesh() -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var start: Vector3 = custom_points[0]
var finish: Vector3 = custom_points[1]
var path := finish - start
var path_length := path.length()
if path_length < 0.05 or fence_height <= 0.0:
return _array_mesh(vertices, normals)
var path_axis := path / path_length
var post_count := fence_divisions + 2
var post_positions: Array[Vector3] = []
for post_index in range(post_count):
var base_position := start.lerp(finish, float(post_index) / float(post_count - 1))
post_positions.append(base_position)
var side_axis := path_axis.cross(Vector3.UP).normalized()
if side_axis.length_squared() < 0.001:
side_axis = Vector3.FORWARD
_append_oriented_box(vertices, normals, base_position + Vector3.UP * fence_height * 0.5, path_axis, Vector3.UP, side_axis, Vector3(thickness, fence_height, thickness))
for section_index in range(post_positions.size() - 1):
var section_start := post_positions[section_index]
var section_end := post_positions[section_index + 1]
var section_axis := (section_end - section_start).normalized()
var rail_up := Vector3.UP - section_axis * section_axis.dot(Vector3.UP)
if rail_up.length_squared() < 0.001:
rail_up = Vector3.FORWARD - section_axis * section_axis.dot(Vector3.FORWARD)
rail_up = rail_up.normalized()
var rail_side := section_axis.cross(rail_up).normalized()
var section_length := section_start.distance_to(section_end)
for height_ratio in [0.28, 0.78]:
var rail_center: Vector3 = (section_start + section_end) * 0.5 + Vector3.UP * fence_height * height_ratio
_append_oriented_box(vertices, normals, rail_center, section_axis, rail_up, rail_side, Vector3(section_length, thickness, thickness))
return _array_mesh(vertices, normals)
func _append_oriented_box(vertices: PackedVector3Array, normals: PackedVector3Array, center: Vector3, axis_x: Vector3, axis_y: Vector3, axis_z: Vector3, dimensions: Vector3) -> void:
var x := axis_x.normalized() * dimensions.x * 0.5
var y := axis_y.normalized() * dimensions.y * 0.5
var z := axis_z.normalized() * dimensions.z * 0.5
var nnn := center - x - y - z
var pnn := center + x - y - z
var ppn := center + x + y - z
var npn := center - x + y - z
var nnp := center - x - y + z
var pnp := center + x - y + z
var ppp := center + x + y + z
var npp := center - x + y + z
_add_quad(vertices, normals, nnp, pnp, ppp, npp)
_add_quad(vertices, normals, pnn, nnn, npn, ppn)
_add_quad(vertices, normals, npp, ppp, ppn, npn)
_add_quad(vertices, normals, nnn, pnn, pnp, nnp)
_add_quad(vertices, normals, nnn, nnp, npp, npn)
_add_quad(vertices, normals, pnp, pnn, ppn, ppp)
func _get_centered_gable_base() -> PackedVector3Array:
var selected_a := to_global(custom_points[0])
var selected_b := to_global(custom_points[1])
var search_root: Node = get_parent()
while search_root != null and not search_root is ModularBuilderPieceGeometry:
search_root = search_root.get_parent()
if search_root == null:
return PackedVector3Array([custom_points[0], custom_points[1]])
var walls: Array[ModularBuilderPieceGeometry] = []
_collect_centering_walls(search_root, walls)
var centered_a := selected_a
var centered_b := selected_b
var nearest_a := INF
var nearest_b := INF
for wall in walls:
if wall.custom_points.size() < 2:
continue
var wall_a := wall.to_global(wall.custom_points[0] + Vector3.UP * wall.unit_size)
var wall_b := wall.to_global(wall.custom_points[1] + Vector3.UP * wall.unit_size)
var tolerance: float = wall.thickness + 0.04
for endpoint in [wall_a, wall_b]:
var distance_a := selected_a.distance_to(endpoint)
if distance_a < nearest_a and distance_a <= tolerance:
nearest_a = distance_a
centered_a = endpoint
var distance_b := selected_b.distance_to(endpoint)
if distance_b < nearest_b and distance_b <= tolerance:
nearest_b = distance_b
centered_b = endpoint
if nearest_a < INF and nearest_b < INF and centered_a.distance_to(centered_b) >= 0.02:
return PackedVector3Array([to_local(centered_a), to_local(centered_b)])
return PackedVector3Array([custom_points[0], custom_points[1]])
func _collect_centering_walls(node: Node, walls: Array[ModularBuilderPieceGeometry]) -> void:
if node is ModularBuilderPieceGeometry and node.piece_type == PieceType.WALL:
walls.append(node)
for child in node.get_children():
_collect_centering_walls(child, walls)
func _append_solid_panel(vertices: PackedVector3Array, normals: PackedVector3Array, panel: Array) -> void:
if panel.size() < 3:
return
var p0: Vector3 = panel[0]
var p1: Vector3 = panel[1]
var p2: Vector3 = panel[2]
var normal: Vector3 = (p1 - p0).cross(p2 - p0).normalized()
if normal.is_zero_approx():
return
var half_depth: Vector3 = normal * thickness * 0.5
var front: Array[Vector3] = []
var back: Array[Vector3] = []
for point in panel:
front.append(point + half_depth)
back.append(point - half_depth)
for i in range(1, panel.size() - 1):
_add_tri(vertices, normals, front[0], front[i], front[i + 1])
_add_tri(vertices, normals, back[i + 1], back[i], back[0])
for i in range(panel.size()):
var next := (i + 1) % panel.size()
_add_quad(vertices, normals, front[i], back[i], back[next], front[next])
func _make_custom_wall_mesh() -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var wall_points: PackedVector3Array = _get_centered_wall_points() if center_align else custom_points
var a: Vector3 = wall_points[0]
var b: Vector3 = wall_points[1]
var direction := (b - a).normalized()
var length := a.distance_to(b)
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * thickness * 0.5
var height := Vector3.UP * unit_size
var openings: Array[PackedVector2Array] = []
for child in get_children():
if _is_detail_node(child) and int(child.get("detail_type")) != DETAIL_TYPE_HATCH and int(child.get("detail_type")) != DETAIL_TYPE_PILLAR:
var detail := child as Node3D
var center := Vector2((detail.position - a).dot(direction), detail.position.y - a.y)
var axis_x := Vector2(detail.transform.basis.x.dot(direction), detail.transform.basis.x.y).normalized()
var axis_y := Vector2(detail.transform.basis.y.dot(direction), detail.transform.basis.y.y).normalized()
var opening := PackedVector2Array([
center - axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
center + axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
center + axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5,
center - axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5
])
var wall_rect := PackedVector2Array([Vector2(0.0, 0.0), Vector2(length, 0.0), Vector2(length, unit_size), Vector2(0.0, unit_size)])
for clipped_opening in Geometry2D.intersect_polygons(opening, wall_rect):
if clipped_opening.size() >= 3:
openings.append(clipped_opening)
var xs: Array[float] = [0.0, length]
var ys: Array[float] = [0.0, unit_size]
for opening in openings:
for point in opening:
xs.append(clampf(point.x, 0.0, length))
ys.append(clampf(point.y, 0.0, unit_size))
xs.sort()
ys.sort()
for xi in range(xs.size() - 1):
for yi in range(ys.size() - 1):
var u0: float = xs[xi]
var u1: float = xs[xi + 1]
var v0: float = ys[yi]
var v1: float = ys[yi + 1]
var mid := Vector2((u0 + u1) * 0.5, (v0 + v1) * 0.5)
var cell := PackedVector2Array([Vector2(u0, v0), Vector2(u1, v0), Vector2(u1, v1), Vector2(u0, v1)])
var remaining: Array[PackedVector2Array] = [cell]
for opening in openings:
var next_remaining: Array[PackedVector2Array] = []
for polygon in remaining:
for clipped in Geometry2D.clip_polygons(polygon, opening):
if clipped.size() >= 3:
next_remaining.append(clipped)
remaining = next_remaining
for polygon in remaining:
var indices := Geometry2D.triangulate_polygon(polygon)
for ti in range(0, indices.size(), 3):
var p0: Vector2 = polygon[indices[ti]]
var p1: Vector2 = polygon[indices[ti + 1]]
var p2: Vector2 = polygon[indices[ti + 2]]
var f0 := a + direction * p0.x + Vector3.UP * p0.y
var f1 := a + direction * p1.x + Vector3.UP * p1.y
var f2 := a + direction * p2.x + Vector3.UP * p2.y
_add_tri(vertices, normals, f0 + side, f1 + side, f2 + side)
_add_tri(vertices, normals, f2 - side, f1 - side, f0 - side)
# Close the wall body at its ends, top, and base.
_add_quad(vertices, normals, a - side, a + side, a + side + height, a - side + height)
_add_quad(vertices, normals, b + side, b - side, b - side + height, b + side + height)
_add_quad(vertices, normals, a + side + height, b + side + height, b - side + height, a - side + height)
_add_quad(vertices, normals, a - side, b - side, b + side, a + side)
for opening in openings:
for edge_index in range(opening.size()):
var p0 := opening[edge_index]
var p1 := opening[(edge_index + 1) % opening.size()]
var a0 := a + direction * p0.x + Vector3.UP * p0.y
var a1 := a + direction * p1.x + Vector3.UP * p1.y
_add_quad(vertices, normals, a0 + side, a0 - side, a1 - side, a1 + side)
return _array_mesh(vertices, normals)
func _get_centered_wall_points() -> PackedVector3Array:
if custom_points.size() < 2:
return custom_points
var selected_a := to_global(custom_points[0])
var selected_b := to_global(custom_points[1])
var search_root: Node = get_parent()
while search_root != null and not search_root is ModularBuilderPieceGeometry:
search_root = search_root.get_parent()
if search_root == null:
return custom_points
var walls: Array[ModularBuilderPieceGeometry] = []
_collect_centering_walls(search_root, walls)
var centered_a := selected_a
var centered_b := selected_b
var nearest_a := INF
var nearest_b := INF
for wall in walls:
if wall == self or wall.custom_points.size() < 2:
continue
var direction := (wall.custom_points[1] - wall.custom_points[0]).normalized()
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * wall.thickness * 0.5
for endpoint in wall.custom_points:
var center := wall.to_global(endpoint + Vector3.UP * wall.unit_size)
for surface_corner in [center + side, center - side]:
var distance_a := selected_a.distance_to(surface_corner)
if distance_a < nearest_a and distance_a <= wall.thickness + 0.04:
nearest_a = distance_a
centered_a = center
var distance_b := selected_b.distance_to(surface_corner)
if distance_b < nearest_b and distance_b <= wall.thickness + 0.04:
nearest_b = distance_b
centered_b = center
if nearest_a < INF and nearest_b < INF and centered_a.distance_to(centered_b) >= 0.02:
return PackedVector3Array([to_local(centered_a), to_local(centered_b)])
return custom_points
func _make_custom_floor_mesh() -> ArrayMesh:
var polygon: Array[Vector2] = []
for point in custom_points:
polygon.append(Vector2(point.x, point.z))
var hatch_mesh := _make_hatch_surface_mesh(polygon, custom_points[0].y, custom_points[0].y - thickness)
if hatch_mesh != null:
return hatch_mesh
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var points := custom_points
var bottom: Array[Vector3] = []
for point in points:
bottom.append(point - Vector3.UP * thickness)
for i in range(1, points.size() - 1):
_add_tri(vertices, normals, points[0], points[i + 1], points[i])
_add_tri(vertices, normals, bottom[0], bottom[i], bottom[i + 1])
for i in range(points.size()):
var j := (i + 1) % points.size()
_add_quad(vertices, normals, bottom[i], bottom[j], points[j], points[i])
return _array_mesh(vertices, normals)
func _make_hatch_surface_mesh(polygon: Array[Vector2], top_y: float, bottom_y: float) -> ArrayMesh:
var openings: Array[PackedVector2Array] = []
for child in get_children():
if _is_detail_node(child) and int(child.get("detail_type")) == DETAIL_TYPE_HATCH:
var detail := child as Node3D
var center := Vector2(detail.position.x, detail.position.z)
var axis_x := Vector2(detail.transform.basis.x.x, detail.transform.basis.x.z).normalized()
var axis_y := Vector2(detail.transform.basis.z.x, detail.transform.basis.z.z).normalized()
var opening := PackedVector2Array([
center - axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
center + axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
center + axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5,
center - axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5
])
openings.append(opening)
if openings.is_empty() or polygon.size() < 3:
return null
var min_x := polygon[0].x
var max_x := polygon[0].x
var min_z := polygon[0].y
var max_z := polygon[0].y
var xs: Array[float] = []
var zs: Array[float] = []
for point in polygon:
min_x = minf(min_x, point.x)
max_x = maxf(max_x, point.x)
min_z = minf(min_z, point.y)
max_z = maxf(max_z, point.y)
xs.append(point.x)
zs.append(point.y)
for opening in openings:
for point in opening:
xs.append(clampf(point.x, min_x, max_x))
zs.append(clampf(point.y, min_z, max_z))
xs.sort()
zs.sort()
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var source_polygon := PackedVector2Array()
for point in polygon:
source_polygon.append(point)
for xi in range(xs.size() - 1):
for zi in range(zs.size() - 1):
var x0: float = xs[xi]
var x1: float = xs[xi + 1]
var z0: float = zs[zi]
var z1: float = zs[zi + 1]
if x1 - x0 < 0.0001 or z1 - z0 < 0.0001:
continue
var cell := PackedVector2Array([Vector2(x0, z0), Vector2(x1, z0), Vector2(x1, z1), Vector2(x0, z1)])
var remaining: Array[PackedVector2Array] = []
for clipped_surface in Geometry2D.intersect_polygons(source_polygon, cell):
remaining.append(clipped_surface)
for opening in openings:
var next_remaining: Array[PackedVector2Array] = []
for surface_polygon in remaining:
for clipped_surface in Geometry2D.clip_polygons(surface_polygon, opening):
if clipped_surface.size() >= 3:
next_remaining.append(clipped_surface)
remaining = next_remaining
for clipped_surface in remaining:
var triangulated := Geometry2D.triangulate_polygon(clipped_surface)
for ti in range(0, triangulated.size(), 3):
var a: Vector2 = clipped_surface[triangulated[ti]]
var b: Vector2 = clipped_surface[triangulated[ti + 1]]
var c: Vector2 = clipped_surface[triangulated[ti + 2]]
_add_tri(vertices, normals, Vector3(a.x, top_y, a.y), Vector3(c.x, top_y, c.y), Vector3(b.x, top_y, b.y))
_add_tri(vertices, normals, Vector3(a.x, bottom_y, a.y), Vector3(b.x, bottom_y, b.y), Vector3(c.x, bottom_y, c.y))
for i in range(polygon.size()):
var j := (i + 1) % polygon.size()
var a := polygon[i]
var b := polygon[j]
_add_quad(vertices, normals, Vector3(a.x, bottom_y, a.y), Vector3(b.x, bottom_y, b.y), Vector3(b.x, top_y, b.y), Vector3(a.x, top_y, a.y))
for opening in openings:
var visible_edges := Geometry2D.intersect_polygons(opening, source_polygon)
for visible_opening in visible_edges:
for edge_index in range(visible_opening.size()):
var p0 := visible_opening[edge_index]
var p1 := visible_opening[(edge_index + 1) % visible_opening.size()]
_add_quad(vertices, normals, Vector3(p0.x, bottom_y, p0.y), Vector3(p0.x, top_y, p0.y), Vector3(p1.x, top_y, p1.y), Vector3(p1.x, bottom_y, p1.y))
return _array_mesh(vertices, normals)
func _is_detail_node(node: Node) -> bool:
if not node is Node3D or node.get_script() == null:
return false
return node.get_script().resource_path == DETAIL_SCRIPT_PATH
func _make_custom_foundation_mesh() -> ArrayMesh:
var polygon: Array[Vector2] = []
for point in custom_points:
polygon.append(Vector2(point.x, point.z))
var bottom_y := -thickness * 0.5 if centered_origin else 0.0
var top_y := thickness * 0.5 if centered_origin else thickness
var hatch_mesh := _make_hatch_surface_mesh(polygon, top_y, bottom_y)
if hatch_mesh != null:
return hatch_mesh
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
for i in range(1, custom_points.size() - 1):
var a := custom_points[0]
var b := custom_points[i]
var c := custom_points[i + 1]
_add_tri(vertices, normals, Vector3(a.x, top_y, a.z), Vector3(c.x, top_y, c.z), Vector3(b.x, top_y, b.z))
_add_tri(vertices, normals, Vector3(a.x, bottom_y, a.z), Vector3(b.x, bottom_y, b.z), Vector3(c.x, bottom_y, c.z))
for i in range(custom_points.size()):
var j := (i + 1) % custom_points.size()
var a := custom_points[i]
var b := custom_points[j]
_add_quad(vertices, normals, Vector3(a.x, bottom_y, a.z), Vector3(b.x, bottom_y, b.z), Vector3(b.x, top_y, b.z), Vector3(a.x, top_y, a.z))
return _array_mesh(vertices, normals)
func _add_quad(v: PackedVector3Array, n: PackedVector3Array, a: Vector3, b: Vector3, c: Vector3, d: Vector3) -> void:
_add_tri(v, n, a, b, c)
_add_tri(v, n, a, c, d)
func _array_mesh(v: PackedVector3Array, n: PackedVector3Array) -> ArrayMesh:
var arrays: Array = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = v
arrays[Mesh.ARRAY_NORMAL] = n
var result := ArrayMesh.new()
result.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return result
func _add_tri(v: PackedVector3Array, n: PackedVector3Array, a: Vector3, b: Vector3, c: Vector3) -> void:
var normal := (b - a).cross(c - a).normalized()
v.append(a)
v.append(b)
v.append(c)
n.append(normal)
n.append(normal)
n.append(normal)
# Also generate reversed faces to show both sides.
# This keeps floors and foundations visible from below with any material.
v.append(c)
v.append(b)
v.append(a)
n.append(-normal)
n.append(-normal)
n.append(-normal)