scopedvr-map-editor/addons/modular_builder/modular_building_detail.gd

284 lines
12 KiB
GDScript

@tool
class_name ModularBuilderDetail
extends Node3D
enum DetailType { WINDOW, DOOR, HATCH, PILLAR, FENCE, PARAPET }
enum DetailMode { OPENING, SEPARATE_OBJECT }
enum WindowStyle { SINGLE_PANE, CROSS_PANE }
enum DoorStyle { FLAT, PANELED }
@export var detail_type: DetailType = DetailType.WINDOW:
set(value):
detail_type = value
notify_property_list_changed()
_rebuild()
@export var detail_mode: DetailMode = DetailMode.OPENING:
set(value):
detail_mode = value
notify_property_list_changed()
_rebuild()
@export var center_align: bool = false:
set(value):
center_align = value
_apply_pillar_alignment()
notify_property_list_changed()
@export_storage var pillar_direction: Vector3 = Vector3.UP
@export_storage var pillar_alignment_basis: Basis = Basis.IDENTITY
@export var window_style: WindowStyle = WindowStyle.SINGLE_PANE:
set(value):
window_style = value
_rebuild()
@export var door_style: DoorStyle = DoorStyle.FLAT:
set(value):
door_style = value
_rebuild()
@export_range(0.01, 5.0, 0.001, "suffix:m") var width: float = 1.0:
set(value):
width = value
_rebuild()
@export_range(0.2, 5.0, 0.001, "suffix:m") var height: float = 1.0:
set(value):
height = value
_rebuild()
@export_range(0.02, 0.5, 0.001, "suffix:m") var frame_depth: float = 0.08:
set(value):
frame_depth = value
_rebuild()
@export_range(0.1, 8.0, 0.001, "suffix:m") var fence_height: float = 0.7:
set(value):
fence_height = value
_rebuild()
@export_range(0, 100, 1, "suffix: posts") var fence_divisions: int = 1:
set(value):
fence_divisions = value
_rebuild()
@export_storage var path_points: PackedVector3Array = PackedVector3Array()
@export_range(0.01, 0.5, 0.001, "suffix:m") var frame_width: float = 0.08:
set(value):
frame_width = value
_rebuild()
@export var follow_host_thickness: bool = true:
set(value):
follow_host_thickness = value
_rebuild()
@export var surface_material: Material:
set(value):
surface_material = value
_rebuild()
@export var frame_material: Material:
set(value):
frame_material = value
_rebuild()
@export var panel_material: Material:
set(value):
panel_material = value
_rebuild()
func _validate_property(property: Dictionary) -> void:
var separate_object := detail_mode == DetailMode.SEPARATE_OBJECT
var is_window := detail_type == DetailType.WINDOW
var is_door := detail_type == DetailType.DOOR
var has_frame_and_panel := separate_object and detail_type != DetailType.PILLAR and detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
var show_property := true
match StringName(property.name):
&"detail_mode":
show_property = detail_type != DetailType.PILLAR and detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
&"center_align":
show_property = detail_type == DetailType.PILLAR
&"fence_height", &"fence_divisions":
show_property = detail_type == DetailType.FENCE or detail_type == DetailType.PARAPET
&"path_points":
show_property = false
&"width":
show_property = detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
if detail_type == DetailType.PILLAR:
property.hint = PROPERTY_HINT_RANGE
property.hint_string = "0.01,5.0,0.001,suffix:m"
&"height":
show_property = detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
&"window_style":
show_property = is_window and separate_object
&"door_style":
show_property = is_door and separate_object
&"frame_width", &"frame_material", &"panel_material":
show_property = has_frame_and_panel
&"follow_host_thickness":
show_property = has_frame_and_panel
&"frame_depth":
show_property = has_frame_and_panel and not follow_host_thickness
&"surface_material":
show_property = separate_object
if not show_property:
property.usage = PROPERTY_USAGE_NO_EDITOR
func _ready() -> void:
set_notify_transform(true)
_rebuild()
func _notification(what: int) -> void:
if what == NOTIFICATION_TRANSFORM_CHANGED and is_inside_tree():
var host := get_parent()
if host != null and host.has_method("_rebuild"):
host.call("_rebuild")
func _on_host_thickness_changed() -> void:
_rebuild(false)
func _apply_pillar_alignment() -> void:
if detail_type != DetailType.PILLAR:
return
var direction := pillar_direction.normalized()
if direction.is_zero_approx():
direction = Vector3.UP
if center_align:
var object_axes := pillar_alignment_basis.orthonormalized()
var best_axis := direction
var best_dot := -1.0
for axis in [object_axes.x, object_axes.y, object_axes.z]:
var normalized_axis: Vector3 = axis.normalized()
var alignment := absf(direction.dot(normalized_axis))
if alignment > best_dot:
best_dot = alignment
best_axis = normalized_axis if direction.dot(normalized_axis) >= 0.0 else -normalized_axis
direction = best_axis
var reference_axis := Vector3.FORWARD if absf(direction.dot(Vector3.UP)) > 0.99 else Vector3.UP
var x_axis := reference_axis.cross(direction).normalized()
var z_axis := x_axis.cross(direction).normalized()
transform.basis = Basis(x_axis, direction, z_axis).orthonormalized()
if center_align and pillar_direction.length() > 0.0:
height = pillar_direction.length()
func _rebuild(notify_host: bool = true) -> void:
if not is_inside_tree():
return
for child in get_children():
if child.name == "DetailGeometry":
remove_child(child)
child.queue_free()
var host := get_parent()
if notify_host and host != null and host.has_method("_rebuild"):
host.call("_rebuild")
if detail_mode == DetailMode.OPENING and detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET:
return
var holder := Node3D.new()
holder.name = "DetailGeometry"
add_child(holder)
var scene_root: Node = null
if Engine.is_editor_hint() and not has_meta("modular_builder_preview"):
scene_root = get_tree().edited_scene_root
if scene_root:
holder.owner = scene_root
var effective_depth := _effective_frame_depth()
var frame := minf(maxf(frame_width, 0.01), minf(width, height) * 0.45)
if detail_type == DetailType.FENCE or detail_type == DetailType.PARAPET:
_build_fence_geometry(holder)
return
if detail_type == DetailType.PILLAR:
_add_box(holder, "PillarBody", Vector3(width, height, width), Vector3.ZERO, surface_material)
return
if detail_type == DetailType.HATCH:
_add_box(holder, "LeftFrame", Vector3(frame, effective_depth, height), Vector3(-width * 0.5 + frame * 0.5, 0.0, 0.0), _frame_material())
_add_box(holder, "RightFrame", Vector3(frame, effective_depth, height), Vector3(width * 0.5 - frame * 0.5, 0.0, 0.0), _frame_material())
_add_box(holder, "NearFrame", Vector3(width, effective_depth, frame), Vector3(0.0, 0.0, -height * 0.5 + frame * 0.5), _frame_material())
_add_box(holder, "FarFrame", Vector3(width, effective_depth, frame), Vector3(0.0, 0.0, height * 0.5 - frame * 0.5), _frame_material())
_add_box(holder, "HatchPanel", Vector3(width - frame * 2.0, effective_depth * 0.35, height - frame * 2.0), Vector3(0.0, effective_depth * 0.4, 0.0), _panel_material())
return
_add_box(holder, "LeftFrame", Vector3(frame, height, effective_depth), Vector3(-width * 0.5 + frame * 0.5, 0.0, 0.0), _frame_material())
_add_box(holder, "RightFrame", Vector3(frame, height, effective_depth), Vector3(width * 0.5 - frame * 0.5, 0.0, 0.0), _frame_material())
_add_box(holder, "TopFrame", Vector3(width, frame, effective_depth), Vector3(0.0, height * 0.5 - frame * 0.5, 0.0), _frame_material())
if detail_type != DetailType.DOOR:
_add_box(holder, "BottomFrame", Vector3(width, frame, effective_depth), Vector3(0.0, -height * 0.5 + frame * 0.5, 0.0), _frame_material())
if detail_type == DetailType.WINDOW:
_add_box(holder, "WindowPanel", Vector3(maxf(width - frame * 2.0, 0.001), maxf(height - frame * 2.0, 0.001), effective_depth * 0.35), Vector3.ZERO, _panel_material())
if window_style == WindowStyle.CROSS_PANE:
_add_box(holder, "VerticalMullion", Vector3(frame * 0.55, height - frame * 2.0, effective_depth * 0.4), Vector3.ZERO, _frame_material())
_add_box(holder, "HorizontalMullion", Vector3(width - frame * 2.0, frame * 0.55, effective_depth * 0.4), Vector3.ZERO, _frame_material())
elif detail_type == DetailType.DOOR:
_add_box(holder, "DoorPanel", Vector3(width - frame * 2.0, height - frame, effective_depth * 0.45), Vector3(0.0, -frame * 0.5, 0.0), _panel_material())
if door_style == DoorStyle.PANELED:
var inset_depth := maxf(effective_depth * 0.04, 0.005)
var panel_width := (width - frame * 2.0) * 0.82
var panel_height := maxf((height - frame) * 0.28, 0.05)
for panel_index in range(2):
var panel_y := -frame * 0.5 + (panel_index - 0.5) * (panel_height + frame * 0.25)
_add_box(holder, "DoorInset_%d" % panel_index, Vector3(panel_width, panel_height, inset_depth), Vector3(0.0, panel_y, effective_depth * 0.24), _frame_material())
func _effective_frame_depth() -> float:
if follow_host_thickness:
var host := get_parent()
if host != null:
var host_thickness = host.get("thickness")
if host_thickness is float or host_thickness is int:
var depth := float(host_thickness)
if detail_type == DetailType.WINDOW or detail_type == DetailType.DOOR:
depth *= 1.25
return maxf(depth, 0.02)
return frame_depth
func _build_fence_geometry(holder: Node3D) -> void:
if path_points.size() < 2 or fence_height <= 0.0:
return
var start := path_points[0]
var finish := path_points[1]
var path := finish - start
var path_length := path.length()
if path_length < 0.05:
return
var path_axis := path / path_length
var up_axis := Vector3.UP - path_axis * path_axis.dot(Vector3.UP)
if up_axis.length_squared() < 0.001:
up_axis = Vector3.FORWARD - path_axis * path_axis.dot(Vector3.FORWARD)
up_axis = up_axis.normalized()
var side_axis := path_axis.cross(up_axis).normalized()
var post_width := 0.16 if detail_type == DetailType.PARAPET else 0.08
var post_count := fence_divisions + 2
var posts: Array[Vector3] = []
for i in range(post_count):
var base := start.lerp(finish, float(i) / float(post_count - 1))
posts.append(base)
_add_oriented_box(holder, "Post_%d" % i, base + up_axis * fence_height * 0.5, path_axis, up_axis, side_axis, Vector3(post_width, fence_height, post_width))
for i in range(posts.size() - 1):
var a := posts[i]
var b := posts[i + 1]
var segment := b - a
var segment_length := segment.length()
if detail_type == DetailType.PARAPET:
_add_oriented_box(holder, "Rail_%d" % i, (a + b) * 0.5 + up_axis * fence_height * 0.78, segment.normalized(), up_axis, side_axis, Vector3(segment_length, post_width * 0.8, post_width * 0.8))
else:
for height_ratio in [0.28, 0.78]:
_add_oriented_box(holder, "Rail_%d_%d" % [i, int(height_ratio * 100.0)], (a + b) * 0.5 + up_axis * fence_height * height_ratio, segment.normalized(), up_axis, side_axis, Vector3(segment_length, post_width, post_width))
func _add_oriented_box(holder: Node3D, node_name: String, center: Vector3, axis_x: Vector3, axis_y: Vector3, axis_z: Vector3, dimensions: Vector3) -> void:
var mesh_instance := MeshInstance3D.new()
mesh_instance.name = node_name
var box := BoxMesh.new()
box.size = dimensions
mesh_instance.mesh = box
var basis := Basis(axis_x.normalized(), axis_y.normalized(), axis_z.normalized()).orthonormalized()
mesh_instance.transform = Transform3D(basis, center)
mesh_instance.material_override = surface_material
holder.add_child(mesh_instance)
if Engine.is_editor_hint() and not has_meta("modular_builder_preview"):
var scene_root := get_tree().edited_scene_root
if scene_root:
mesh_instance.owner = scene_root
func _frame_material() -> Material:
return frame_material if frame_material != null else surface_material
func _panel_material() -> Material:
return panel_material if panel_material != null else surface_material
func _add_box(holder: Node3D, node_name: String, box_size: Vector3, box_position: Vector3, material: Material = null) -> void:
var mesh_instance := MeshInstance3D.new()
mesh_instance.name = node_name
var box := BoxMesh.new()
box.size = box_size
mesh_instance.mesh = box
mesh_instance.position = box_position
mesh_instance.material_override = material if material != null else surface_material
holder.add_child(mesh_instance)
if Engine.is_editor_hint() and not has_meta("modular_builder_preview"):
var scene_root := get_tree().edited_scene_root
if scene_root:
mesh_instance.owner = scene_root