@tool extends "res://addons/modular_builder/modular_builder_editor_actions.gd" const PILLAR_SUPPORT = preload("res://addons/modular_builder/modular_pillar_support.gd") const VERTEX_SIMPLIFIER = preload("res://addons/modular_builder/modular_vertex_simplifier.gd") func _create_detail_from_click(camera: Camera3D, screen_position: Vector2) -> bool: var placement := _find_detail_placement(camera, screen_position) if placement.is_empty(): return false var detail := _new_detail(placement, false) var best_piece := placement.host as ModularBuilderPiece if best_piece == null: return false var undo := get_undo_redo() undo.create_action("Add %s" % detail.name) undo.add_do_method(self, "_attach_detail", best_piece, detail, get_editor_interface().get_edited_scene_root()) undo.add_undo_method(self, "_detach_detail", best_piece, detail) undo.commit_action() get_editor_interface().get_selection().clear() get_editor_interface().get_selection().add_node(detail) _set_tool(TOOL_NONE) status_label.text = "%s created. Edit style, materials, and dimensions in the Inspector." % detail.name return true func _create_pillar_from_vertices(root: ModularBuilderPiece, first: Vector3, second: Vector3) -> void: var placement := _find_pillar_placement(root, first, second) if placement.is_empty(): pending_points.clear() status_label.text = "Choose two different structure vertices to connect with the pillar." return _commit_pillar_placement(placement, "Plain pillar created between vertices. Edit its section and material in the Inspector.") func _create_vertical_pillar_from_vertex(root: ModularBuilderPiece, anchor: Vector3) -> void: pending_points.clear() var length := maxf(0.2, detail_height_spin.value * METERS_TO_GODOT_UNITS) var direction := _vertical_pillar_direction(root, anchor) var placement := _find_pillar_placement(root, anchor, anchor + direction * length) if placement.is_empty(): status_label.text = "Could not place a vertical pillar at this vertex." return _commit_pillar_placement(placement, "Vertical pillar created from the vertex. Edit its length in the Inspector.") func _commit_pillar_placement(placement: Dictionary, message: String) -> void: var detail := _new_detail(placement, false) var host := placement.host as ModularBuilderPiece detail.set("height", float(placement.length)) var undo := get_undo_redo() undo.create_action("Add plain pillar") undo.add_do_method(self, "_attach_detail", host, detail, get_editor_interface().get_edited_scene_root()) undo.add_undo_method(self, "_detach_detail", host, detail) undo.commit_action() get_editor_interface().get_selection().clear() get_editor_interface().get_selection().add_node(detail) _set_tool(TOOL_NONE) status_label.text = message func _vertical_pillar_direction(root: ModularBuilderPiece, anchor: Vector3) -> Vector3: var selected_pillar := _get_selected_pillar_detail() if selected_pillar != null: var pillar_local := selected_pillar.to_local(anchor) if absf(pillar_local.x) <= selected_pillar.width * 0.5 + 0.05 and absf(pillar_local.z) <= selected_pillar.width * 0.5 + 0.05 and absf(absf(pillar_local.y) - selected_pillar.height * 0.5) <= 0.05: return PILLAR_SUPPORT.vertical_direction_for_pillar(selected_pillar, anchor) var source := _get_pillar_vertex_source(root, anchor) if source != null: return PILLAR_SUPPORT.vertical_direction_for_piece(source, anchor) var pillar := _find_pillar_at_vertex(root, anchor) return PILLAR_SUPPORT.vertical_direction_for_pillar(pillar, anchor) if pillar != null else Vector3.UP func _find_pillar_at_vertex(node: Node, anchor: Vector3) -> ModularBuilderDetail: if node is ModularBuilderDetail and node.detail_type == ModularBuilderDetail.DetailType.PILLAR: var detail := node as ModularBuilderDetail var local := detail.to_local(anchor) var half := detail.width * 0.5 var at_corner := absf(absf(local.x) - half) <= 0.05 and absf(absf(local.z) - half) <= 0.05 var at_center := _simplify_vertices_check != null and _simplify_vertices_check.button_pressed and absf(local.x) <= 0.05 and absf(local.z) <= 0.05 if (at_corner or at_center) and absf(absf(local.y) - detail.height * 0.5) <= 0.05: return detail for child in node.get_children(): var pillar := _find_pillar_at_vertex(child, anchor) if pillar != null: return pillar return null func _create_fence_decoration(root: ModularBuilderPiece, first: Vector3, second: Vector3) -> void: if first.distance_to(second) < 0.05: pending_points.clear() status_label.text = "Choose two vertices far enough apart." return var is_parapet := active_tool == TOOL_PARAPET var detail := Node3D.new() detail.set_script(DETAIL_SCRIPT) detail.name = "Parapet" if is_parapet else "Fence" detail.set("detail_type", ModularBuilderDetail.DetailType.PARAPET if is_parapet else ModularBuilderDetail.DetailType.FENCE) detail.set("detail_mode", ModularBuilderDetail.DetailMode.SEPARATE_OBJECT) detail.set("path_points", PackedVector3Array([root.to_local(first), root.to_local(second)])) detail.set("fence_height", fence_height_spin.value * METERS_TO_GODOT_UNITS) detail.set("fence_divisions", int(fence_divisions_spin.value)) var undo := get_undo_redo() undo.create_action("Add parapet" if is_parapet else "Add decorative fence") undo.add_do_method(self, "_attach_detail", root, detail, get_editor_interface().get_edited_scene_root()) undo.add_undo_method(self, "_detach_detail", root, detail) undo.commit_action() get_editor_interface().get_selection().clear() get_editor_interface().get_selection().add_node(detail) _set_tool(TOOL_NONE) pending_points.clear() status_label.text = "%s created with %d intermediate post(s)." % [detail.name, detail.fence_divisions] func _find_pillar_placement(root: ModularBuilderPiece, first: Vector3, second: Vector3) -> Dictionary: if first.distance_to(second) < 0.02: return {} var local_first := root.to_local(first) var local_second := root.to_local(second) var segment := local_second - local_first var segment_length := segment.length() if segment_length < 0.02: return {} var y_axis := segment / segment_length var reference_axis := Vector3.FORWARD if absf(y_axis.dot(Vector3.UP)) > 0.99 else Vector3.UP var x_axis := reference_axis.cross(y_axis).normalized() var z_axis := x_axis.cross(y_axis).normalized() var basis := Basis(x_axis, y_axis, z_axis).orthonormalized() var source := _get_pillar_vertex_source(root, first) var alignment_basis := Basis.IDENTITY if source != null: alignment_basis = (root.global_transform.basis.inverse() * source.global_transform.basis).orthonormalized() return { "host": root, "position": (local_first + local_second) * 0.5, "rotation": Vector3.ZERO, "basis": basis, "direction": segment, "alignment_basis": alignment_basis, "length": segment_length } func _get_pillar_vertex_source(root: ModularBuilderPiece, point: Vector3) -> ModularBuilderPiece: var best_piece: ModularBuilderPiece var best_distance := 0.05 var pieces: Array[ModularBuilderPiece] = [] call("_collect_all_modular_pieces", root, pieces) for piece in pieces: if piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_SQUARE and piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE and piece.piece_type != ModularBuilderPiece.PieceType.FLOOR and piece.piece_type != ModularBuilderPiece.PieceType.WALL and piece.piece_type != ModularBuilderPiece.PieceType.GABLE_WALL and piece.piece_type != ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT: continue var vertices: Array[Vector3] = [] call("_collect_piece_geometry_vertices", piece, vertices) for vertex in vertices: var distance := vertex.distance_to(point) if _simplify_vertices_check != null and _simplify_vertices_check.button_pressed: distance = minf(distance, VERTEX_SIMPLIFIER.piece_vertex(piece, vertex).distance_to(point)) if distance < best_distance: best_distance = distance best_piece = piece return best_piece func _point_inside_surface_or_edge(piece: ModularBuilderPiece, point: Vector2, edge_tolerance: float) -> bool: if _point_inside_surface(piece, point): return true var polygon := _foundation_polygon(piece) if piece.piece_type != ModularBuilderPiece.PieceType.FLOOR else _piece_polygon_xz(piece) for i in range(polygon.size()): var a: Vector2 = polygon[i] var b: Vector2 = polygon[(i + 1) % polygon.size()] var edge := b - a var t := clampf((point - a).dot(edge) / maxf(edge.length_squared(), 0.000001), 0.0, 1.0) if point.distance_to(a + edge * t) <= edge_tolerance + 0.01: return true return false func _find_detail_placement(camera: Camera3D, screen_position: Vector2) -> Dictionary: var root := _get_selected_piece() if root == null or not _is_detail_tool(active_tool): return {} var ray_origin := camera.project_ray_origin(screen_position) var ray_direction := camera.project_ray_normal(screen_position) var candidates: Array[ModularBuilderPiece] = [] if active_tool == TOOL_HATCH or active_tool == TOOL_PILLAR: _collect_surface_pieces(root, candidates) else: _collect_wall_pieces(root, candidates) var best_distance := INF var best_piece: ModularBuilderPiece var best_local := Vector3.ZERO var best_surface_y := 0.0 for candidate in candidates: if not is_instance_valid(candidate) or candidate.piece_type == ModularBuilderPiece.PieceType.BUILDING: continue var local_origin := candidate.to_local(ray_origin) var local_direction := candidate.to_local(ray_origin + ray_direction) - local_origin if active_tool != TOOL_HATCH and candidate.custom_points.size() < 2: continue var uses_surface := active_tool == TOOL_HATCH or active_tool == TOOL_PILLAR var plane_normal := Vector3.UP if uses_surface else Vector3(-candidate.custom_points[1].z + candidate.custom_points[0].z, 0.0, candidate.custom_points[1].x - candidate.custom_points[0].x).normalized() if uses_surface and candidate.custom_points.size() >= 3: plane_normal = Vector3.UP if absf(local_direction.dot(plane_normal)) < 0.0001: continue var plane_y := 0.0 var plane_point := Vector3.ZERO if uses_surface: plane_y = candidate.thickness * 0.5 if candidate.centered_origin else candidate.thickness plane_point.y = plane_y var distance := (plane_point - local_origin).dot(plane_normal) / local_direction.dot(plane_normal) if distance < 0.0 or distance >= best_distance: continue var hit := local_origin + local_direction * distance if uses_surface: if candidate.piece_type != ModularBuilderPiece.PieceType.FLOOR and candidate.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_SQUARE and candidate.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE: continue if not _point_inside_surface(candidate, Vector2(hit.x, hit.z)): continue else: if candidate.piece_type != ModularBuilderPiece.PieceType.WALL or candidate.custom_points.size() < 2: continue var a: Vector3 = candidate.custom_points[0] var b: Vector3 = candidate.custom_points[1] var wall_dir := (b - a).normalized() var along := (hit - a).dot(wall_dir) var vertical := hit.y - a.y if along < 0.0 or along > a.distance_to(b) or vertical < 0.0 or vertical > candidate.unit_size: continue best_distance = distance best_piece = candidate best_local = hit best_surface_y = plane_y if best_piece == null: return {} # best_local uses the selected piece’s local coordinates. The child detail # must use the same space, especially for attached foundations. var host_local := best_local var detail_position: Vector3 var detail_rotation := Vector3.ZERO if active_tool == TOOL_HATCH: detail_position = Vector3(host_local.x, 0.0, host_local.z) elif active_tool == TOOL_PILLAR: detail_position = Vector3(host_local.x, best_surface_y, host_local.z) else: var wall_a: Vector3 = best_piece.custom_points[0] var wall_b: Vector3 = best_piece.custom_points[1] var direction := (wall_b - wall_a).normalized() var n := Vector3(-direction.z, 0.0, direction.x) detail_rotation.y = atan2(n.x, n.z) var opening_center_y := wall_a.y + detail_height_spin.value * 0.5 + detail_height_offset_spin.value detail_position = Vector3((wall_a.x + wall_b.x) * 0.5, opening_center_y, (wall_a.z + wall_b.z) * 0.5) return {"host": best_piece, "position": detail_position, "rotation": detail_rotation} func _new_detail(placement: Dictionary, preview: bool) -> ModularBuilderDetail: var detail := Node3D.new() detail.set_script(DETAIL_SCRIPT) if active_tool == TOOL_WINDOW: detail.name = "Window" elif active_tool == TOOL_DOOR: detail.name = "Door" elif active_tool == TOOL_HATCH: detail.name = "Hatch" elif active_tool == TOOL_PILLAR: detail.name = "Pillar" elif active_tool == TOOL_FENCE: detail.name = "Fence" else: detail.name = "Parapet" if active_tool == TOOL_WINDOW: detail.set("detail_type", ModularBuilderDetail.DetailType.WINDOW) elif active_tool == TOOL_DOOR: detail.set("detail_type", ModularBuilderDetail.DetailType.DOOR) elif active_tool == TOOL_HATCH: detail.set("detail_type", ModularBuilderDetail.DetailType.HATCH) elif active_tool == TOOL_FENCE: detail.set("detail_type", ModularBuilderDetail.DetailType.FENCE) elif active_tool == TOOL_PARAPET: detail.set("detail_type", ModularBuilderDetail.DetailType.PARAPET) else: detail.set("detail_type", ModularBuilderDetail.DetailType.PILLAR) detail.set("width", detail_width_spin.value * METERS_TO_GODOT_UNITS) detail.set("height", float(placement.length) if active_tool == TOOL_PILLAR and placement.has("length") else detail_height_spin.value * METERS_TO_GODOT_UNITS) detail.set("detail_mode", ModularBuilderDetail.DetailMode.SEPARATE_OBJECT if preview or active_tool == TOOL_PILLAR else detail_mode_option.get_selected_id()) detail.set("surface_material", _preview_material if preview else null) if preview: var host := placement.host as ModularBuilderPiece var preview_depth := host.thickness if host != null else 0.08 if active_tool == TOOL_WINDOW or active_tool == TOOL_DOOR: preview_depth *= 1.25 detail.set("follow_host_thickness", false) detail.set("frame_depth", preview_depth) if active_tool == TOOL_WINDOW: detail.set("window_style", _selected_window_style) elif active_tool == TOOL_DOOR: detail.set("door_style", _selected_door_style) if preview: detail.name = "ModularBuilderPreview" detail.set_meta("modular_builder_preview", true) var detail_basis: Basis = placement.basis if placement.has("basis") else Basis.from_euler(placement.rotation) detail.transform = Transform3D(detail_basis, placement.position) if active_tool == TOOL_PILLAR: detail.set("pillar_direction", placement.direction) detail.set("pillar_alignment_basis", placement.alignment_basis) detail.set("center_align", _pillar_centralize_check.button_pressed) if active_tool == TOOL_FENCE or active_tool == TOOL_PARAPET: detail.set("fence_height", fence_height_spin.value * METERS_TO_GODOT_UNITS) detail.set("fence_divisions", int(fence_divisions_spin.value)) return detail as ModularBuilderDetail func _update_detail_preview() -> void: if not _is_detail_tool(active_tool) or active_tool == TOOL_FENCE or active_tool == TOOL_PARAPET or _preview_camera == null: return var placement: Dictionary if active_tool == TOOL_PILLAR: var root := _get_selected_piece() if root == null or pending_points.is_empty(): if is_instance_valid(_detail_preview): _detail_preview.visible = false return var second_vertex := _nearest_vertex(_preview_camera, _preview_screen_position, call("_get_tool_vertices", root)) if second_vertex == null: if is_instance_valid(_detail_preview): _detail_preview.visible = false return placement = _find_pillar_placement(root, pending_points[0], second_vertex) else: placement = _find_detail_placement(_preview_camera, _preview_screen_position) if placement.is_empty(): if is_instance_valid(_detail_preview): _detail_preview.visible = false return if not is_instance_valid(_detail_preview) or _detail_preview.detail_type != _detail_type_for_tool(active_tool): _clear_detail_preview() _detail_preview = _new_detail(placement, true) var scene_root := get_editor_interface().get_edited_scene_root() if scene_root == null: _detail_preview = null return var preview_host := placement.host as ModularBuilderPiece if active_tool == TOOL_PILLAR and preview_host != null: preview_host.add_child(_detail_preview, true) else: scene_root.add_child(_detail_preview, true) _detail_preview.owner = scene_root _detail_preview.visible = true if not is_equal_approx(_detail_preview.width, detail_width_spin.value): _detail_preview.width = detail_width_spin.value var preview_height := float(placement.length) if active_tool == TOOL_PILLAR and placement.has("length") else detail_height_spin.value if not is_equal_approx(_detail_preview.height, preview_height): _detail_preview.height = preview_height var host := placement.host as ModularBuilderPiece if host != null: var preview_basis: Basis = placement.basis if placement.has("basis") else Basis.from_euler(placement.rotation) if active_tool == TOOL_PILLAR: _detail_preview.transform = Transform3D(preview_basis, placement.position) _detail_preview.pillar_direction = placement.direction _detail_preview.pillar_alignment_basis = placement.alignment_basis _detail_preview.center_align = _pillar_centralize_check.button_pressed else: _detail_preview.global_transform = host.global_transform * Transform3D(preview_basis, placement.position) func _detail_type_for_tool(tool: String) -> int: if tool == TOOL_DOOR: return ModularBuilderDetail.DetailType.DOOR if tool == TOOL_HATCH: return ModularBuilderDetail.DetailType.HATCH if tool == TOOL_PILLAR: return ModularBuilderDetail.DetailType.PILLAR if tool == TOOL_FENCE: return ModularBuilderDetail.DetailType.FENCE if tool == TOOL_PARAPET: return ModularBuilderDetail.DetailType.PARAPET return ModularBuilderDetail.DetailType.WINDOW func _refresh_detail_preview(_value: float = 0.0) -> void: if _preview_camera != null: _update_detail_preview() func _clear_detail_preview() -> void: if is_instance_valid(_detail_preview): _detail_preview.queue_free() _detail_preview = null func _attach_detail(host: ModularBuilderPiece, detail: Node, scene_root: Node) -> void: if detail.get_parent() != host: host.add_child(detail, true) detail.owner = scene_root host._rebuild() func _detach_detail(host: ModularBuilderPiece, detail: Node) -> void: if detail.get_parent() == host: host.remove_child(detail) host._rebuild() func _collect_surface_pieces(node: Node, pieces: Array[ModularBuilderPiece]) -> void: if node is ModularBuilderPiece and (node.piece_type == ModularBuilderPiece.PieceType.FLOOR or node.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_SQUARE or node.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE): pieces.append(node) for child in node.get_children(): _collect_surface_pieces(child, pieces) func _collect_wall_pieces(node: Node, pieces: Array[ModularBuilderPiece]) -> void: if node is ModularBuilderPiece and node.piece_type == ModularBuilderPiece.PieceType.WALL: pieces.append(node) for child in node.get_children(): _collect_wall_pieces(child, pieces) func _point_inside_surface(piece: ModularBuilderPiece, point: Vector2) -> bool: var polygon := _foundation_polygon(piece) if piece.piece_type != ModularBuilderPiece.PieceType.FLOOR else _piece_polygon_xz(piece) if polygon.size() < 3: return false var inside := false var j := polygon.size() - 1 for i in range(polygon.size()): var a: Vector2 = polygon[i] var b: Vector2 = polygon[j] if (a.y > point.y) != (b.y > point.y) and point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x: inside = not inside j = i return inside func _piece_polygon_xz(piece: ModularBuilderPiece) -> Array[Vector2]: var polygon: Array[Vector2] = [] for point in piece.custom_points: polygon.append(Vector2(point.x, point.z)) return polygon func _nearest_vertex(camera: Camera3D, screen_position: Vector2, vertices: Array[Vector3]) -> Variant: var nearest: Variant = null var nearest_distance := MARKER_HIT_RADIUS for world_point in vertices: if camera.is_position_behind(world_point): continue var distance := camera.unproject_position(world_point).distance_to(screen_position) if distance < nearest_distance: nearest_distance = distance nearest = world_point return nearest