From 03b16e226e0a718e1b21d7ef89a679e01f25a595 Mon Sep 17 00:00:00 2001 From: yvmou_ Date: Sun, 4 Oct 2026 16:38:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4surface=20snap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/surface_snap/LICENSE | 21 -- addons/surface_snap/README.md | 52 ---- addons/surface_snap/plugin.cfg | 7 - addons/surface_snap/plugin.gd | 60 ---- addons/surface_snap/plugin.gd.uid | 1 - addons/surface_snap/surface_snap_tool.gd | 280 ------------------- addons/surface_snap/surface_snap_tool.gd.uid | 1 - project.godot | 2 +- 8 files changed, 1 insertion(+), 423 deletions(-) delete mode 100644 addons/surface_snap/LICENSE delete mode 100644 addons/surface_snap/README.md delete mode 100644 addons/surface_snap/plugin.cfg delete mode 100644 addons/surface_snap/plugin.gd delete mode 100644 addons/surface_snap/plugin.gd.uid delete mode 100644 addons/surface_snap/surface_snap_tool.gd delete mode 100644 addons/surface_snap/surface_snap_tool.gd.uid diff --git a/addons/surface_snap/LICENSE b/addons/surface_snap/LICENSE deleted file mode 100644 index 3aaca81..0000000 --- a/addons/surface_snap/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Surface Snap Contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/addons/surface_snap/README.md b/addons/surface_snap/README.md deleted file mode 100644 index e6624b4..0000000 --- a/addons/surface_snap/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Surface Snap - -An editor plugin for Godot 4.7+ that lets you drag a `Node3D` directly onto -the **surface** of another object — unlike the built-in vertex snap, which -only snaps to specific vertices. - -## Usage - -1. Enable the plugin in **Project → Project Settings → Plugins**. -2. Select the object you want to - position. -3. Hold **Alt** and drag with the left mouse button over the target surface. -4. Release the button to confirm (the action is added to the editor's - Undo/Redo, `Ctrl+Z` works normally). - -The object's Y axis automatically aligns to the surface normal, and the -object's **edge** (computed from its AABB) touches the contact point — not -its origin/pivot. - -## Requirements - -- The **target (surface)** object needs real physics collision: - `CollisionShape3D` (under a `StaticBody3D`, `Area3D`, etc.) or a - `CSGShape3D` with `use_collision` enabled in the inspector. -- The **dragged** object does not need a collider. -- Only one selected `Node3D` at a time (multi-selection is not supported in - this version). - -## Configuration - -In **Project Settings → Surface Snap → Collision Mask**, choose which -physics layers count as a valid snap surface (all layers by default). - -## Known limitations - -- The "object edge" offset is based on the AABB (bounding box), not the - actual mesh — for very irregular or non-convex shapes, the contact point - may not be the true closest point on the surface. -- Dragging multiple selected objects at once is not supported. - -## AI usage disclosure - -The GDScript code for this plugin was written with the assistance of an AI -language model (Claude, by Anthropic), based on requirements, testing, and -bug reports provided by the developer throughout an iterative development -process. The developer directed the design decisions, tested every -iteration in the Godot editor, and reported issues that were then fixed -collaboratively with the AI. - -## License - -MIT — see `LICENSE`. diff --git a/addons/surface_snap/plugin.cfg b/addons/surface_snap/plugin.cfg deleted file mode 100644 index 3e7b53b..0000000 --- a/addons/surface_snap/plugin.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[plugin] - -name="Surface Snap" -description="Snap objects directly onto the surface of another object" -author="Adrm00" -version="1.0.1" -script="plugin.gd" diff --git a/addons/surface_snap/plugin.gd b/addons/surface_snap/plugin.gd deleted file mode 100644 index 56d5ba5..0000000 --- a/addons/surface_snap/plugin.gd +++ /dev/null @@ -1,60 +0,0 @@ -@tool -extends EditorPlugin - -## Snaps a 3D object against the surface of another collidable object. -## -## Usage: Select a single Node3D, hold Alt -## and drag with the left mouse button over a collidable surface. Releasing -## the button confirms the move (registered in the editor's Undo/Redo). -## -## Configuration: Project Settings -> "surface_snap/collision_mask" controls -## which physics layers count as a valid snap surface. - -var _snap_tool: SurfaceSnapTool = SurfaceSnapTool.new() - - -func _enter_tree() -> void: - _register_project_settings() - - -func _exit_tree() -> void: - if _snap_tool.is_dragging(): - _snap_tool.cancel_drag() - - -func _handles(object: Object) -> bool: - return object is Node3D - - -func _edit(object: Object) -> void: - pass - - -func _forward_3d_gui_input(camera: Camera3D, event: InputEvent) -> int: - var selected_nodes: Array[Node] = EditorInterface.get_selection().get_selected_nodes() - var target: Node3D = null - - if selected_nodes.size() == 1 and selected_nodes[0] is Node3D: - target = selected_nodes[0] as Node3D - - var consumed: bool = _snap_tool.handle_input(camera, event, target, get_undo_redo()) - - if consumed: - return EditorPlugin.AFTER_GUI_INPUT_STOP - - return EditorPlugin.AFTER_GUI_INPUT_PASS - - -func _register_project_settings() -> void: - var setting_name: String = SurfaceSnapTool.SETTING_COLLISION_MASK - var default_value: int = SurfaceSnapTool.DEFAULT_COLLISION_MASK - - if not ProjectSettings.has_setting(setting_name): - ProjectSettings.set_setting(setting_name, default_value) - - ProjectSettings.set_initial_value(setting_name, default_value) - ProjectSettings.add_property_info({ - "name": setting_name, - "type": TYPE_INT, - "hint": PROPERTY_HINT_LAYERS_3D_PHYSICS, - }) diff --git a/addons/surface_snap/plugin.gd.uid b/addons/surface_snap/plugin.gd.uid deleted file mode 100644 index 97f58a1..0000000 --- a/addons/surface_snap/plugin.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c41uif1g5stoa diff --git a/addons/surface_snap/surface_snap_tool.gd b/addons/surface_snap/surface_snap_tool.gd deleted file mode 100644 index 5450146..0000000 --- a/addons/surface_snap/surface_snap_tool.gd +++ /dev/null @@ -1,280 +0,0 @@ -@tool -class_name SurfaceSnapTool -extends RefCounted - -## Drag logic for Surface Snap. -## -## Flow: Hold Alt and drag (left mouse -## button) a selected Node3D. The object is repositioned via a physics raycast -## against colliders (CollisionShape3D / CSGShape3D with "use_collision" -## enabled), with: -## - The object's Y axis aligned to the surface normal (original yaw preserved). -## - The object pushed along the normal just far enough that its AABB -## doesn't clip into the surface, while its origin tracks the mouse -## laterally (in the surface's tangent plane). -## Releasing the button confirms the move and registers it in the editor's Undo/Redo. -## -## Target (surface) requirements: it needs real physics collision -## (an own or inherited CollisionShape3D, or a CSGShape3D with use_collision=true). -## The dragged object itself does not need a collider — and if it has one, its -## collision_layer is temporarily zeroed out during the drag so it can't snap -## against itself. - -const SETTING_COLLISION_MASK: String = "surface_snap/collision_mask" -const DEFAULT_COLLISION_MASK: int = 0xFFFFFFFF - -const RAY_LENGTH: float = 10000.0 - -var _dragging: bool = false -var _target: Node3D = null -var _original_transform: Transform3D = Transform3D.IDENTITY -var _original_basis: Basis = Basis.IDENTITY -var _original_scale: Vector3 = Vector3.ONE -var _local_aabb_corners: Array[Vector3] = [] -var _disabled_collision_layers: Dictionary = {} - - -func is_dragging() -> bool: - return _dragging - - -## Called by the EditorPlugin for every 3D viewport input event. -## Returns true when the event was consumed by this tool. -func handle_input(camera: Camera3D, event: InputEvent, target: Node3D, undo_redo: EditorUndoRedoManager) -> bool: - if event is InputEventMouseButton: - return _handle_mouse_button(camera, event as InputEventMouseButton, target, undo_redo) - elif event is InputEventMouseMotion: - return _handle_mouse_motion(camera, event as InputEventMouseMotion) - return false - - -## Called externally (e.g. plugin being disabled) to abort an ongoing drag. -func cancel_drag() -> void: - if _is_target_valid(): - _target.global_transform = _original_transform - _reset_state() - - -# --------------------------------------------------------------------------- -# Input handling -# --------------------------------------------------------------------------- - -func _handle_mouse_button(camera: Camera3D, event: InputEventMouseButton, target: Node3D, undo_redo: EditorUndoRedoManager) -> bool: - if event.button_index != MOUSE_BUTTON_LEFT: - return false - - if event.pressed and event.alt_pressed and target != null: - _start_drag(camera, event.position, target) - return true - - if not event.pressed and _dragging: - _finish_drag(undo_redo) - return true - - return false - - -func _handle_mouse_motion(camera: Camera3D, event: InputEventMouseMotion) -> bool: - if not _dragging: - return false - _update_drag(camera, event.position) - return true - - -# --------------------------------------------------------------------------- -# Drag lifecycle -# --------------------------------------------------------------------------- - -func _start_drag(camera: Camera3D, mouse_pos: Vector2, target: Node3D) -> void: - _target = target - _original_transform = target.global_transform - _original_scale = target.scale - _original_basis = Basis(_original_transform.basis).orthonormalized() - _local_aabb_corners = _compute_local_aabb_corners(target) - _dragging = true - - _disable_target_collision(target) - - var hit: Dictionary = _raycast_surface(camera, mouse_pos) - if not hit.is_empty(): - _apply_hit(hit) - - -func _update_drag(camera: Camera3D, mouse_pos: Vector2) -> void: - if not _is_target_valid(): - _reset_state() - return - - var hit: Dictionary = _raycast_surface(camera, mouse_pos) - if hit.is_empty(): - return - - _apply_hit(hit) - - -func _apply_hit(hit: Dictionary) -> void: - var hit_position: Vector3 = hit["position"] - var hit_normal: Vector3 = hit["normal"] - var normal_n: Vector3 = hit_normal.normalized() - - var new_basis: Basis = _align_up_to_normal(_original_basis, hit_normal) - - # Only push the object along the normal, by exactly how far its AABB - # extends past the origin in that direction — this keeps the origin's - # lateral (tangent-plane) position matching the mouse ray, instead of - # pinning a specific AABB corner to the hit point. - var min_projection: float = _find_min_normal_projection(_local_aabb_corners, new_basis, normal_n) - var new_origin: Vector3 = hit_position - min_projection * normal_n - - # Re-apply the original scale, which is stripped out by orthonormalization. - var scaled_basis: Basis = Basis( - new_basis.x * _original_scale.x, - new_basis.y * _original_scale.y, - new_basis.z * _original_scale.z - ) - - _target.global_transform = Transform3D(scaled_basis, new_origin) - - -func _finish_drag(undo_redo: EditorUndoRedoManager) -> void: - if not _is_target_valid(): - _reset_state() - return - - var affected_target: Node3D = _target - var final_transform: Transform3D = affected_target.global_transform - var previous_transform: Transform3D = _original_transform - - undo_redo.create_action("Surface Snap") - undo_redo.add_do_property(affected_target, "global_transform", final_transform) - undo_redo.add_undo_property(affected_target, "global_transform", previous_transform) - undo_redo.commit_action(false) - - _reset_state() - - -func _reset_state() -> void: - _restore_target_collision() - _dragging = false - _target = null - _original_scale = Vector3.ONE - _local_aabb_corners = [] - - -func _is_target_valid() -> bool: - return _target != null and is_instance_valid(_target) and _target.is_inside_tree() - - -# --------------------------------------------------------------------------- -# Temporarily disable the dragged object's own collision, so it can't be hit -# by its own raycast (e.g. a CSGShape3D with use_collision enabled). -# --------------------------------------------------------------------------- - -func _disable_target_collision(node: Node) -> void: - if node is CollisionObject3D: - var collider := node as CollisionObject3D - _disabled_collision_layers[collider] = collider.collision_layer - collider.collision_layer = 0 - elif node is CSGShape3D: - var csg := node as CSGShape3D - _disabled_collision_layers[csg] = csg.collision_layer - csg.collision_layer = 0 - - for child in node.get_children(): - _disable_target_collision(child as Node) - - -func _restore_target_collision() -> void: - for node: Object in _disabled_collision_layers.keys(): - if is_instance_valid(node): - var original_layer: int = _disabled_collision_layers[node] - if node is CollisionObject3D: - (node as CollisionObject3D).collision_layer = original_layer - elif node is CSGShape3D: - (node as CSGShape3D).collision_layer = original_layer - - _disabled_collision_layers.clear() - - -# --------------------------------------------------------------------------- -# Physics raycast -# --------------------------------------------------------------------------- - -func _raycast_surface(camera: Camera3D, mouse_pos: Vector2) -> Dictionary: - var world_3d: World3D = camera.get_world_3d() - var space_state: PhysicsDirectSpaceState3D = world_3d.direct_space_state - - var from: Vector3 = camera.project_ray_origin(mouse_pos) - var dir: Vector3 = camera.project_ray_normal(mouse_pos) - var to: Vector3 = from + dir * RAY_LENGTH - - var collision_mask: int = ProjectSettings.get_setting(SETTING_COLLISION_MASK, DEFAULT_COLLISION_MASK) - var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(from, to, collision_mask) - - return space_state.intersect_ray(query) - - -# --------------------------------------------------------------------------- -# Rotation alignment (Y axis -> surface normal) -# --------------------------------------------------------------------------- - -func _align_up_to_normal(reference_basis: Basis, normal: Vector3) -> Basis: - var normal_n: Vector3 = normal.normalized() - var forward: Vector3 = -reference_basis.z - - # Project the reference forward vector onto the surface's tangent plane - # to preserve the object's original yaw as much as possible. - var forward_proj: Vector3 = forward - normal_n * forward.dot(normal_n) - - if forward_proj.length() < 0.001: - forward_proj = reference_basis.x - normal_n * reference_basis.x.dot(normal_n) - - if forward_proj.length() < 0.001: - forward_proj = Vector3.FORWARD - normal_n * Vector3.FORWARD.dot(normal_n) - - forward_proj = forward_proj.normalized() - - var right: Vector3 = forward_proj.cross(normal_n).normalized() - var true_forward: Vector3 = normal_n.cross(right).normalized() - - var new_basis: Basis = Basis(right, normal_n, -true_forward) - return new_basis.orthonormalized() - - -# --------------------------------------------------------------------------- -# Grounding offset via local AABB (object edge, not origin, along the normal) -# --------------------------------------------------------------------------- - -func _compute_local_aabb_corners(node: Node3D) -> Array[Vector3]: - var aabb: AABB = _get_node_aabb(node) - var corners: Array[Vector3] = [] - - for i in range(8): - corners.append(aabb.position + Vector3( - aabb.size.x * float(i & 1), - aabb.size.y * float((i >> 1) & 1), - aabb.size.z * float((i >> 2) & 1) - )) - - return corners - - -func _get_node_aabb(node: Node3D) -> AABB: - if node is VisualInstance3D: - return (node as VisualInstance3D).get_aabb() - return AABB(Vector3.ZERO, Vector3.ZERO) - - -func _find_min_normal_projection(corners_local: Array[Vector3], rotation_basis: Basis, normal: Vector3) -> float: - if corners_local.is_empty(): - return 0.0 - - var min_dot: float = INF - - for corner_local in corners_local: - var corner_world: Vector3 = rotation_basis * corner_local - var d: float = corner_world.dot(normal) - if d < min_dot: - min_dot = d - - return min_dot diff --git a/addons/surface_snap/surface_snap_tool.gd.uid b/addons/surface_snap/surface_snap_tool.gd.uid deleted file mode 100644 index 2931da3..0000000 --- a/addons/surface_snap/surface_snap_tool.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bq3nx5ukyhb1e diff --git a/project.godot b/project.godot index 6e5ab4d..f1d8a1d 100644 --- a/project.godot +++ b/project.godot @@ -36,7 +36,7 @@ project/assembly_name="agame" [editor_plugins] -enabled=PackedStringArray("res://addons/csg_blockout/plugin.cfg", "res://addons/godot_ai/plugin.cfg", "res://addons/godotsteam/plugin.cfg", "res://addons/surface_snap/plugin.cfg") +enabled=PackedStringArray("res://addons/csg_blockout/plugin.cfg", "res://addons/godot_ai/plugin.cfg", "res://addons/godotsteam/plugin.cfg", "res://addons/model_localizer/plugin.cfg") [input]