@tool extends "res://addons/modular_builder/modular_builder_editor_ui.gd" ## ScopedVR-Bibliothek-Export (Karte t_82d2a8dd): dritter Export-Modus ## „In ScopedVR-Bibliothek" — gebackenes Mesh + Trimesh-Kollision durch die ## Objekt-Pipeline (tools/objekt im Match-Server-Repo), Name-Dialog inklusive. const SCOPEDVR_EXPORT_UI := preload("res://addons/modular_builder/scopedvr_export_ui.gd") var _scopedvr_export: RefCounted = null func _add_piece() -> void: var root := get_editor_interface().get_edited_scene_root() if root == null or not root is Node3D: status_label.text = "Open a 3D scene before adding a piece." return var piece := Node3D.new() piece.name = "ModularBuilderPiece" piece.set_script(PIECE_SCRIPT) piece.set("piece_type", ModularBuilderPiece.PieceType.BUILDING) var module_size_units := _meters_to_godot_units(size_spin.value) piece.set("unit_size", module_size_units) var foundation := Node3D.new() foundation.name = "Foundation" foundation.set_script(PIECE_SCRIPT) var chosen_type := _foundation_type_selected if chosen_type == 1: foundation.set("piece_type", ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE) else: foundation.set("piece_type", ModularBuilderPiece.PieceType.FOUNDATION_SQUARE) foundation.set("unit_size", module_size_units) foundation.set("centered_origin", true) foundation.position.y = foundation.thickness * 0.5 piece.add_child(foundation, true) foundation.owner = root var undo := get_undo_redo() undo.create_action("Add ModularBuilderPiece") undo.add_do_method(root, "add_child", piece, true) undo.add_do_property(piece, "owner", root) undo.add_do_property(foundation, "owner", root) undo.add_undo_method(root, "remove_child", piece) undo.add_do_method(get_editor_interface().get_selection(), "clear") undo.add_do_method(get_editor_interface().get_selection(), "add_node", piece) undo.commit_action() _last_created_piece = foundation _active_button_key = TOOL_FOUNDATION_TRIANGLE if _foundation_type_selected == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE else TOOL_FOUNDATION_SQUARE _set_tool(TOOL_FOUNDATION) func _meters_to_godot_units(meters: float) -> float: return meters * METERS_TO_GODOT_UNITS func _show_save_module_dialog() -> void: var building := _get_selected_piece() if building == null: status_label.text = "Select a ModularBuilderPiece structure to save as a module." return _save_module_dialog.current_file = "%s.tscn" % building.name _save_module_dialog.popup_centered_ratio(0.7) func _show_load_module_dialog() -> void: _load_module_dialog.popup_centered_ratio(0.7) func _on_export_pressed() -> void: var building := _get_selected_piece() if building == null: status_label.text = "Select a ModularBuilderPiece to export." return var baked_mesh := _bake_building_mesh(building) if baked_mesh == null or baked_mesh.get_surface_count() == 0: status_label.text = "No visible meshes were found to export." return if _export_mode_option.get_selected_id() == EXPORT_LIBRARY: _export_to_library(building, baked_mesh) return if _export_mode_option.get_selected_id() == EXPORT_SCENE_COPY: _add_baked_copy(building, baked_mesh) return _pending_export_mesh = baked_mesh _pending_export_name = "%s_Baked" % building.name _pending_export_basis = building.global_transform.basis _export_scene_dialog.current_file = "%s.tscn" % _pending_export_name _export_scene_dialog.popup_centered_ratio(0.7) func _bake_building_mesh(building: ModularBuilderPiece) -> ArrayMesh: return MESH_BAKER.new().bake(building) ## Export-Modus EXPORT_LIBRARY (Karte t_82d2a8dd): Name-Dialog öffnen, danach ## läuft die Objekt-Pipeline (scopedvr_export_ui.gd); der Status landet im ## Dock-Statusfeld. Die Instanz bleibt gespeichert, damit der Dialog nicht ## vom Garbage Collector gefressen wird, während er offen ist. func _export_to_library(building: ModularBuilderPiece, baked_mesh: ArrayMesh) -> void: _scopedvr_export = SCOPEDVR_EXPORT_UI.new() _scopedvr_export.starten(self, building, baked_mesh, func(text: String) -> void: status_label.text = str(text)) func _add_baked_copy(building: ModularBuilderPiece, mesh: ArrayMesh) -> void: var edited_scene_root := get_editor_interface().get_edited_scene_root() if edited_scene_root == null or building.get_parent() == null: status_label.text = "Open a scene before adding the exported copy." return var sibling_parent := building.get_parent() var baked_root := _make_baked_scene_root(building.name + "_Baked", mesh) var undo := get_undo_redo() undo.create_action("Export structure as a mesh copy") undo.add_do_method(self, "_attach_baked_copy", sibling_parent, edited_scene_root, baked_root, building.global_transform) undo.add_undo_method(self, "_detach_baked_copy", sibling_parent, baked_root) undo.commit_action() status_label.text = "Copy exported as one mesh with %d material surface(s). Ctrl+Z removes the copy." % mesh.get_surface_count() func _attach_baked_copy(sibling_parent: Node, edited_scene_root: Node, baked_root: Node3D, target_transform: Transform3D) -> void: if baked_root.get_parent() != sibling_parent: if baked_root.get_parent() != null: baked_root.get_parent().remove_child(baked_root) sibling_parent.add_child(baked_root, true) baked_root.global_transform = target_transform baked_root.owner = edited_scene_root _set_export_owner_recursive(baked_root, edited_scene_root) var editor_selection := get_editor_interface().get_selection() editor_selection.clear() editor_selection.add_node(baked_root) func _detach_baked_copy(sibling_parent: Node, baked_root: Node3D) -> void: if baked_root.get_parent() == sibling_parent: sibling_parent.remove_child(baked_root) func _make_baked_scene_root(root_name: String, mesh: ArrayMesh) -> Node3D: var baked_root := Node3D.new() baked_root.name = root_name var mesh_instance := MeshInstance3D.new() mesh_instance.name = "BakedMesh" mesh_instance.mesh = mesh baked_root.add_child(mesh_instance) return baked_root func _export_baked_scene_to_path(path: String) -> void: if _pending_export_mesh == null: status_label.text = "The exported mesh is no longer available. Bake it again." return var baked_root := _make_baked_scene_root(_pending_export_name, _pending_export_mesh) baked_root.transform = Transform3D(_pending_export_basis, Vector3.ZERO) _set_export_owner_recursive(baked_root, baked_root) var packed := PackedScene.new() var pack_error := packed.pack(baked_root) baked_root.free() if pack_error != OK: status_label.text = "Could not pack the exported mesh (%s)." % error_string(pack_error) return var save_error := ResourceSaver.save(packed, path) if save_error != OK: status_label.text = "Could not save the exported scene (%s)." % error_string(save_error) return get_editor_interface().get_resource_filesystem().scan() status_label.text = "Exported scene: %s (%d material surface(s))." % [path, _pending_export_mesh.get_surface_count()] _pending_export_mesh = null func _set_export_owner_recursive(node: Node, scene_owner: Node) -> void: for child in node.get_children(): child.owner = scene_owner _set_export_owner_recursive(child, scene_owner) func _save_module_to_path(path: String) -> void: var building := _get_selected_piece() if building == null: status_label.text = "Select a ModularBuilderPiece structure to save as a module." return var descendants: Array[Node] = [] _collect_module_nodes(building, descendants) var original_owners: Dictionary = {} for node in descendants: original_owners[node] = node.owner node.owner = building var packed := PackedScene.new() var pack_error := packed.pack(building) for node in descendants: node.owner = original_owners[node] if pack_error != OK: status_label.text = "Could not pack the structure as a scene (%s)." % error_string(pack_error) return var save_error := ResourceSaver.save(packed, path) if save_error != OK: status_label.text = "Could not save the module (%s)." % error_string(save_error) return status_label.text = "Module saved: %s" % path func _collect_module_nodes(node: Node, result: Array[Node]) -> void: for child in node.get_children(): if child.name == "ModularBuilderMarkers" or child.has_meta("modular_builder_preview"): continue result.append(child) _collect_module_nodes(child, result) func _load_module_from_path(path: String) -> void: var packed := ResourceLoader.load(path) as PackedScene if packed == null: status_label.text = "The selected file is not a valid Godot scene." return var instance := packed.instantiate() if not instance is Node3D: instance.free() status_label.text = "The module scene must have a Node3D root." return var scene_root := get_editor_interface().get_edited_scene_root() if scene_root == null: instance.free() status_label.text = "Open a scene before inserting a module." return var anchor := _get_selected_piece() if anchor != null: var target_world := anchor.global_position + anchor.global_transform.basis.x * maxf(anchor.unit_size, 1.0) instance.position = scene_root.to_local(target_world) var undo := get_undo_redo() undo.create_action("Insert module %s" % instance.name) undo.add_do_method(self, "_attach_module", scene_root, instance) undo.add_undo_method(self, "_detach_module", scene_root, instance) undo.commit_action() status_label.text = "Module inserted. Move the piece to the desired location." func _attach_module(scene_root: Node, instance: Node) -> void: if instance.get_parent() != scene_root: scene_root.add_child(instance, true) instance.owner = scene_root var editor_selection := get_editor_interface().get_selection() editor_selection.clear() editor_selection.add_node(instance) func _detach_module(scene_root: Node, instance: Node) -> void: if instance.get_parent() == scene_root: scene_root.remove_child(instance) func _rotate_selected_piece() -> void: var selected_piece := _raw_selected_piece() var target := selected_piece if selected_piece == null: target = _last_created_piece if is_instance_valid(_last_created_piece) and _last_created_piece.is_inside_tree() else null elif selected_piece.piece_type == ModularBuilderPiece.PieceType.BUILDING: target = null if is_instance_valid(_last_created_piece) and _last_created_piece.is_inside_tree() and _is_descendant_of(_last_created_piece, selected_piece): target = _last_created_piece if target == null: status_label.text = "Select or create a piece to rotate." return var old_rotation := target.rotation_degrees var undo := get_undo_redo() undo.create_action("Rotate piece 90 degrees") undo.add_do_property(target, "rotation_degrees", old_rotation + Vector3(0.0, 90.0, 0.0)) undo.add_undo_property(target, "rotation_degrees", old_rotation) undo.commit_action() _last_created_piece = target status_label.text = "%s rotated by 90°." % target.name update_overlays() func _is_descendant_of(node: Node, ancestor: Node) -> bool: var current := node.get_parent() while current != null: if current == ancestor: return true current = current.get_parent() return false func _raw_selected_piece() -> ModularBuilderPiece: var selected := get_editor_interface().get_selection().get_selected_nodes() if selected.size() == 1 and selected[0] is ModularBuilderPiece: return selected[0] as ModularBuilderPiece return null func _add_general_collision() -> void: var piece := _get_selected_piece() if piece == null: status_label.text = "Select a structure to generate general collision." return var scene_root := get_editor_interface().get_edited_scene_root() var faces := PackedVector3Array() var structure_pieces: Array[ModularBuilderPiece] = [] _collect_structure_pieces(piece, structure_pieces) _collect_scene_pieces(scene_root, structure_pieces) _collect_collision_faces(piece, piece, faces, {}, structure_pieces) if faces.is_empty(): status_label.text = "The selected structure has no geometry for collision." return var concave := ConcavePolygonShape3D.new() concave.set_faces(faces) var body := piece.find_child("GeneralCollision", true, false) as StaticBody3D var undo := get_undo_redo() if body != null: var shape_node := body.get_node_or_null("Shape") as CollisionShape3D if shape_node == null: shape_node = CollisionShape3D.new() shape_node.name = "Shape" shape_node.shape = concave undo.create_action("Add shape to general collision") undo.add_do_method(body, "add_child", shape_node, true) undo.add_do_property(shape_node, "owner", scene_root) undo.add_undo_method(body, "remove_child", shape_node) else: var old_shape: Shape3D = shape_node.shape undo.create_action("Update general collision") undo.add_do_property(shape_node, "shape", concave) undo.add_undo_property(shape_node, "shape", old_shape) else: body = StaticBody3D.new() body.name = "GeneralCollision" var shape_node := CollisionShape3D.new() shape_node.name = "Shape" shape_node.shape = concave body.add_child(shape_node) undo.create_action("Add general collision") undo.add_do_method(piece, "add_child", body, true) undo.add_do_property(body, "owner", scene_root) undo.add_do_property(shape_node, "owner", scene_root) undo.add_undo_method(piece, "remove_child", body) undo.commit_action() status_label.text = "General collision added or updated. Snap markers are excluded." func _collect_structure_pieces(node: Node, pieces: Array[ModularBuilderPiece]) -> void: if node is ModularBuilderPiece: pieces.append(node) for child in node.get_children(): if child.name == "ModularBuilderMarkers" or child.name == "GeneralCollision": continue _collect_structure_pieces(child, pieces) func _collect_scene_pieces(node: Node, pieces: Array[ModularBuilderPiece]) -> void: if node is ModularBuilderPiece and not pieces.has(node): pieces.append(node) for child in node.get_children(): if child.name == "ModularBuilderMarkers" or child.name == "GeneralCollision": continue _collect_scene_pieces(child, pieces) func _collect_collision_faces(node: Node, piece_root: ModularBuilderPiece, faces: PackedVector3Array, seen_faces: Dictionary, structure_pieces: Array[ModularBuilderPiece]) -> void: if node is MeshInstance3D and node.name == "Geometry" and node.mesh != null: var relative_transform: Transform3D = piece_root.global_transform.affine_inverse() * node.global_transform var mesh_faces: PackedVector3Array = node.mesh.get_faces() var owner_piece := node.get_parent() as ModularBuilderPiece for i in range(0, mesh_faces.size(), 3): if i + 2 >= mesh_faces.size(): break var local_a: Vector3 = mesh_faces[i] var local_b: Vector3 = mesh_faces[i + 1] var local_c: Vector3 = mesh_faces[i + 2] if _is_connection_face(owner_piece, local_a, local_b, local_c, structure_pieces): continue # Procedural meshes have both sides. Deduplication may retain # either orientation; normalize top faces beforehand to # keep custom foundations navigable. if _is_piece_top_face(owner_piece, local_a, local_b, local_c): if (local_b - local_a).cross(local_c - local_a).y < 0.0: var swap := local_b local_b = local_c local_c = swap var a: Vector3 = relative_transform * local_a var b: Vector3 = relative_transform * local_b var c: Vector3 = relative_transform * local_c var key_parts := [_collision_vertex_key(a), _collision_vertex_key(b), _collision_vertex_key(c)] key_parts.sort() var face_key := "%s|%s|%s" % key_parts if seen_faces.has(face_key): continue seen_faces[face_key] = true faces.append_array(PackedVector3Array([a, b, c])) for child in node.get_children(): if child.name == "ModularBuilderMarkers" or child.name == "GeneralCollision": continue _collect_collision_faces(child, piece_root, faces, seen_faces, structure_pieces) func _is_piece_top_face(piece: ModularBuilderPiece, a: Vector3, b: Vector3, c: Vector3) -> bool: if piece == null: return false if piece.piece_type != ModularBuilderPiece.PieceType.FLOOR and piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_SQUARE and piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE: return false if absf(a.y - b.y) > 0.002 or absf(a.y - c.y) > 0.002: return false var top_y := piece.thickness * 0.5 if piece.centered_origin else piece.thickness if piece.piece_type == ModularBuilderPiece.PieceType.FLOOR and piece.custom_points.size() >= 3: top_y = piece.custom_points[0].y return absf(a.y - top_y) < 0.002 and absf(b.y - top_y) < 0.002 and absf(c.y - top_y) < 0.002 func _is_connection_face(piece: ModularBuilderPiece, a: Vector3, b: Vector3, c: Vector3, structure_pieces: Array[ModularBuilderPiece]) -> bool: if piece == null: return false if (piece.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_SQUARE or piece.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE) and _foundation_edge_is_shared(piece, a, b, c, structure_pieces): return true if piece.piece_type == ModularBuilderPiece.PieceType.FLOOR and piece.custom_points.size() >= 3: var bottom_point := piece.custom_points[0] - Vector3.UP * piece.thickness return _triangle_on_plane(a, b, c, bottom_point, Vector3.UP) if piece.piece_type != ModularBuilderPiece.PieceType.WALL or piece.custom_points.size() < 2: return false var start: Vector3 = piece.custom_points[0] var finish: Vector3 = piece.custom_points[1] var direction := (finish - start).normalized() var side := Vector3(-direction.z, 0.0, direction.x).normalized() * piece.thickness * 0.5 var base_normal := (finish - start).cross(side).normalized() if _triangle_on_plane(a, b, c, start, base_normal): return true if _wall_has_floor_contact(piece, structure_pieces): if _triangle_on_plane(a, b, c, start + Vector3.UP * piece.unit_size, base_normal): return true for other in structure_pieces: if other == piece or other.piece_type != ModularBuilderPiece.PieceType.WALL or other.custom_points.size() < 2: continue for endpoint in piece.custom_points: var endpoint_world := piece.to_global(endpoint) for other_endpoint in other.custom_points: if endpoint_world.distance_to(other.to_global(other_endpoint)) < 0.02: var local_endpoint := piece.to_local(endpoint_world) if _triangle_on_plane(a, b, c, local_endpoint, direction): return true return false func _wall_has_floor_contact(wall: ModularBuilderPiece, structure_pieces: Array[ModularBuilderPiece]) -> bool: var endpoints: PackedVector3Array = wall.custom_points var direction := (endpoints[1] - endpoints[0]).normalized() var side := Vector3(-direction.z, 0.0, direction.x).normalized() * wall.thickness * 0.5 for endpoint in endpoints: var top := endpoint + Vector3.UP * wall.unit_size for wall_corner in [wall.to_global(top + side), wall.to_global(top - side)]: for other in structure_pieces: if other.piece_type != ModularBuilderPiece.PieceType.FLOOR or other.custom_points.size() < 3: continue for floor_point in other.custom_points: if wall_corner.distance_to(other.to_global(floor_point)) < 0.02: return true return false func _foundation_edge_is_shared(piece: ModularBuilderPiece, a: Vector3, b: Vector3, c: Vector3, structure_pieces: Array[ModularBuilderPiece]) -> bool: var polygon := _foundation_polygon(piece) for other in structure_pieces: if other == piece or (other.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_SQUARE and other.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE): continue var other_polygon := _foundation_polygon(other) for i in range(polygon.size()): var p0: Vector2 = polygon[i] var p1: Vector2 = polygon[(i + 1) % polygon.size()] for j in range(other_polygon.size()): var q0: Vector2 = other_polygon[j] var q1: Vector2 = other_polygon[(j + 1) % other_polygon.size()] var other_base_y := -other.thickness * 0.5 if other.centered_origin else 0.0 var q0_local := piece.to_local(other.to_global(Vector3(q0.x, other_base_y, q0.y))) var q1_local := piece.to_local(other.to_global(Vector3(q1.x, other_base_y, q1.y))) var shared := _segments_overlap(p0, p1, Vector2(q0_local.x, q0_local.z), Vector2(q1_local.x, q1_local.z)) var piece_base_y := -piece.thickness * 0.5 if piece.centered_origin else 0.0 if shared and absf(q0_local.y - piece_base_y) < 0.02 and absf(q1_local.y - piece_base_y) < 0.02 and _triangle_on_edge(a, b, c, p0, p1): return true return false func _triangle_on_edge(a: Vector3, b: Vector3, c: Vector3, edge_a: Vector2, edge_b: Vector2) -> bool: return _point_on_edge(Vector2(a.x, a.z), edge_a, edge_b) and _point_on_edge(Vector2(b.x, b.z), edge_a, edge_b) and _point_on_edge(Vector2(c.x, c.z), edge_a, edge_b) func _point_on_edge(point: Vector2, edge_a: Vector2, edge_b: Vector2) -> bool: var edge := edge_b - edge_a var length_squared := edge.length_squared() if length_squared < 0.000001: return false var t := (point - edge_a).dot(edge) / length_squared var closest := edge_a + edge * clampf(t, 0.0, 1.0) return t >= -0.002 and t <= 1.002 and point.distance_to(closest) < 0.002 func _segments_overlap(a: Vector2, b: Vector2, c: Vector2, d: Vector2) -> bool: var direction := b - a var length := direction.length() if length < 0.0001: return false var unit := direction / length if absf(unit.x * (c - a).y - unit.y * (c - a).x) > 0.02 or absf(unit.x * (d - a).y - unit.y * (d - a).x) > 0.02: return false var c_proj := (c - a).dot(unit) var d_proj := (d - a).dot(unit) var overlap := minf(length, maxf(c_proj, d_proj)) - maxf(0.0, minf(c_proj, d_proj)) return overlap > 0.02 func _triangle_on_plane(a: Vector3, b: Vector3, c: Vector3, plane_point: Vector3, plane_normal: Vector3) -> bool: if plane_normal.length_squared() < 0.000001: return false var normal := plane_normal.normalized() return absf((a - plane_point).dot(normal)) < 0.002 and absf((b - plane_point).dot(normal)) < 0.002 and absf((c - plane_point).dot(normal)) < 0.002 func _collision_vertex_key(point: Vector3) -> String: return "%.4f,%.4f,%.4f" % [point.x, point.y, point.z] func _set_tool(tool: String) -> void: active_tool = tool _detail_options_row.visible = _is_detail_tool(tool) _rise_options_row.visible = tool == TOOL_WALL or tool == TOOL_GABLE_WALL or tool == TOOL_GABLE_WALL_RIGHT or tool == TOOL_ROOF_GABLE or tool == TOOL_ROOF_PYRAMID _linear_options_row.visible = tool == TOOL_STAIRS or tool == TOOL_FENCE _stair_count_label.visible = tool == TOOL_STAIRS stair_count_spin.visible = tool == TOOL_STAIRS _stair_width_label.visible = tool == TOOL_STAIRS stair_width_spin.visible = tool == TOOL_STAIRS _fence_divisions_label.visible = tool == TOOL_FENCE fence_divisions_spin.visible = tool == TOOL_FENCE _gable_centralize_check.visible = tool == TOOL_WALL or tool == TOOL_GABLE_WALL or tool == TOOL_GABLE_WALL_RIGHT _rise_label.text = "Peak rise:" if not _is_detail_tool(tool): call("_clear_detail_preview") if tool == TOOL_NONE: _active_button_key = "" pending_points.clear() for button_tool in _tool_buttons: _tool_buttons[button_tool].set_pressed_no_signal(button_tool == _active_button_key) if tool == TOOL_NONE: status_label.text = "Tool deactivated." elif _get_selected_piece() == null: status_label.text = "Select the corresponding piece in the Scene tree." elif tool == TOOL_FOUNDATION: status_label.text = "Click a + button beside any foundation. Click the tool again to deactivate it." elif tool == TOOL_WINDOW or tool == TOOL_DOOR: status_label.text = "Click a wall face to insert the detail. Edit its mode and dimensions in the Inspector." elif tool == TOOL_HATCH: status_label.text = "Click a foundation or floor face to insert the hatch. Edit it in the Inspector." elif tool == TOOL_GABLE_WALL: status_label.text = "Click the two base vertices of the gable. Adjust peak height below." elif tool == TOOL_GABLE_WALL_RIGHT: status_label.text = "Click the base vertices; the second point sets the high side of the right triangular gable." elif tool == TOOL_WALL: status_label.text = "Click two vertices on foundations, floors, or wall tops. Enable Center walls to align the endpoints with supporting walls." elif tool == TOOL_ROOF_GABLE: status_label.text = "Click the peaks of two gables or diagonal corners atop opposite walls." elif tool == TOOL_ROOF_SHED: status_label.text = "Click a low point, then a diagonally opposite high point. The roof finds the other corners; adjust width in the Inspector." elif tool == TOOL_ROOF_PYRAMID: status_label.text = "Click two diagonal vertices at the same height. Adjust pyramid peak height below or in the Inspector." elif tool == TOOL_STAIRS: status_label.text = "Click the low point, then the high point to create stairs. Adjust steps and width below." elif tool == TOOL_FENCE or tool == TOOL_PARAPET: status_label.text = "Click two vertices to set the path. Adjust height and intermediate posts below." elif tool == TOOL_MERGE_VERTICES: status_label.text = "Click two vertices to merge their markers. Ctrl+Z undoes the merge." elif tool == TOOL_CREATE_VERTEX: status_label.text = "Click a surface to create a connection point linked to the selected piece or detail." elif tool == TOOL_MOVE_VERTEX: status_label.text = "Click a blue manual point, then the surface where you want to move it." elif tool == TOOL_REMOVE_VERTEX: status_label.text = "Click a blue manual point to remove it." elif tool == TOOL_PILLAR: status_label.text = "Click two vertices to connect them. Alt+click one vertex to create a vertical pillar with the panel length." else: status_label.text = "Click the blue structure vertices. Click the tool again to deactivate it." call("_sync_vertex_markers") update_overlays() func _get_selected_piece() -> ModularBuilderPiece: var selected := get_editor_interface().get_selection().get_selected_nodes() if selected.is_empty(): return null var common_root: ModularBuilderPiece for selected_node in selected: var selected_piece := selected_node as ModularBuilderPiece if selected_piece == null and selected_node is ModularBuilderDetail: var parent := selected_node.get_parent() while parent != null and not parent is ModularBuilderPiece: parent = parent.get_parent() selected_piece = parent as ModularBuilderPiece if selected_piece == null: return null var piece_root := selected_piece var ancestor := selected_piece.get_parent() while ancestor != null: if ancestor is ModularBuilderPiece and ancestor.piece_type == ModularBuilderPiece.PieceType.BUILDING: piece_root = ancestor break ancestor = ancestor.get_parent() if common_root == null: common_root = piece_root elif common_root != piece_root: return null return common_root func _on_editor_selection_changed() -> void: if _redirecting_selection: return var selected := get_editor_interface().get_selection().get_selected_nodes() var redirected_selection: Array[Node] = [] var needs_redirect := false for selected_node in selected: var target_node: Node = selected_node if selected_node is MeshInstance3D: var ancestor := selected_node.get_parent() while ancestor != null and not (ancestor is ModularBuilderPiece or ancestor is ModularBuilderDetail): ancestor = ancestor.get_parent() if ancestor != null: target_node = ancestor needs_redirect = true if not redirected_selection.has(target_node): redirected_selection.append(target_node) if needs_redirect: _redirecting_selection = true var editor_selection := get_editor_interface().get_selection() editor_selection.clear() for target_node in redirected_selection: editor_selection.add_node(target_node) _redirecting_selection = false if active_tool != TOOL_NONE: pending_points.clear() _sync_pillar_centralize_control() call("_sync_vertex_markers") update_overlays() func _on_marker_scope_toggled(_enabled: bool) -> void: call("_sync_vertex_markers") update_overlays() func _on_simplify_vertices_toggled(_enabled: bool) -> void: pending_points.clear() call("_sync_vertex_markers") update_overlays() func _foundation_polygon(root: ModularBuilderPiece) -> Array[Vector2]: if root.custom_points.size() >= 3: var custom: Array[Vector2] = [] for point in root.custom_points: custom.append(Vector2(point.x, point.z)) return custom var half := root.unit_size * 0.5 if root.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE: return [Vector2(-half, -half), Vector2(half, -half), Vector2(-half, half)] return [Vector2(-half, -half), Vector2(half, -half), Vector2(half, half), Vector2(-half, half)]