Godot equivalent to the Roblox function 'Model:MoveTo()'

Godot Version

4.3

Question

I am attempting to recreate the old drag / move tool from Roblox in Godot, however one of the main problems is that Nodes dragged against 2 surfaces will intersect those 2 surfaces. I tried to solve this by adding the nodes AABB to the position calculation, but it is still (albeit to a lesser extent) a problem:

var aabb = _calculate_spatial_bounds(saved_part, false)
var targ_pos = get_collision_point()
var norm = get_collision_normal()
var rounded_pos = snappedvec3(targ_pos, 0.25)

saved_part.position = (rounded_pos - offset) + norm * (aabb.size / 2)

I have learned that most attempts at recreating the drag tool in Roblox often use a function called “Model:MoveTo(vector3)”, which tries to perform a ‘safe move’ on the Model (collection of objects), which is where it tries to move the Model to the given position, and if there is an intersection, it moves it upward to avoid intersecting any other Parts. Is there an equivalent function or simple method of performing a ‘safe move’ in Godot?

MoveTo documentation: https://create.roblox.com/docs/reference/engine/classes/Model/MoveTo

Here’s the full program in case it helps or you wanna use it (most of the “welding” portions don’t work)

extends RayCast3D

var text
var offset = Vector3(0, 0, 0)

var collider

var saved_part
var dragged_to_part

var can_grab = false

var drag_count = 0 # if print(get_collider()) zero, then change can_grab but like otherwise you are already grabbing a part

var rot_count_x = 0
var rot_count_y = 0
var rot_count_z = 0
var weld_count = 0 

var part_rotation = Vector3(0, 0, 0)

var pos_diff = Vector3.ZERO

var saved_part_name = ""

func _physics_process(_delta):
   #if button_toggle.button_toggle: # debug
   var par = get_parent()
   var play = par.get_parent()
   
   collider = get_collider()
   #print(saved_part)
   if collider:
   	if not collider.name == "Floor": # exclude floor
   		saved_part_name = collider.name
   		#play.get_child(4).text = saved_part_name # debug
   		#print(get_collision_normal().inverse()) # debug
   		
   		if drag_count == 0:
   			saved_part = collider

   			add_exception(collider)
   			can_grab = true
   			drag_count += 1
   		
   			
   	
   #if not collider or collider and collider.name == "Floor": # debug
   	#play.get_child(4).text = saved_part_name # debug
   	
   if can_grab and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
   	weld_count = 0
   	#if button_toggle.button_toggle: # debug
   	if saved_part.find_child("CollisionShape3D"):
   		saved_part.find_child("CollisionShape3D").disabled = true
   	
   	var targ_pos = get_collision_point()
   	var norm = get_collision_normal()
   	
   	if saved_part is RigidBody3D:
   		saved_part.freeze = true
   	
   	var aabb = _calculate_spatial_bounds(saved_part, false)
   	
   	var rounded_pos = snappedvec3(targ_pos, 0.25)
   	if can_grab:
   		force_raycast_update()
   		if get_collider():
   			dragged_to_part = get_collider()
   			
   		#print("Ground: ", dragged_to_part) # debug
   		#print("dragged: ", saved_part)saved_part # debug
   	
   	saved_part.position = (rounded_pos - offset) + norm * (aabb.size / 2)

   		
   	if Input.is_key_pressed(KEY_R) and rot_count_x == 0:
   		saved_part.rotate(Vector3(0, 1, 0), PI / 2)
   		rot_count_x += 1
   		
   	if Input.is_key_pressed(KEY_T) and rot_count_z == 0:
   		saved_part.rotate(Vector3(1, 0, 0), PI / 2)
   		rot_count_z += 1

   	if not Input.is_key_pressed(KEY_R):
   		rot_count_x = 0
   	if not Input.is_key_pressed(KEY_T):
   		rot_count_z = 0
   	
   	if saved_part.get_node("Area3D"): # welding related stuff
   			for body in saved_part.get_node("Area3D").get_overlapping_bodies():
   				if body is HingeJoint3D:
   					print(body.node_a)
   					body.queue_free()
   
   if can_grab and Input.is_action_just_pressed("click"):
   	var offset = get_collision_point() - saved_part.position
   	
   if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
   	if can_grab:
   		can_grab = false
   		dragged_to_part = null
   		
   		if saved_part is RigidBody3D:
   			saved_part.freeze = false
   			
   	if saved_part:
   		if saved_part.find_child("CollisionShape3D"):
   			saved_part.find_child("CollisionShape3D").disabled = false
   		drag_count = 0
   		remove_exception(saved_part)
   		if dragged_to_part:
   			if dragged_to_part.get_node("HingeJoint3D"):
   				print("has weld") # debug
   		
   		if saved_part.get_node("Area3D") and weld_count == 0: # welding stuff
   			for body in saved_part.get_node("Area3D").get_overlapping_bodies():
   				if not body == saved_part:
   					var weld = HingeJoint3D.new()
   					
   					weld.set_flag(HingeJoint3D.FLAG_USE_LIMIT, true)
   					weld.set_param(HingeJoint3D.PARAM_BIAS, 0.99)
   					weld.set_param(HingeJoint3D.PARAM_LIMIT_LOWER, 0)
   					weld.set_param(HingeJoint3D.PARAM_LIMIT_UPPER, 0)
   					
   					weld.set_node_a(saved_part.get_path())
   					weld.set_node_b(body.get_path())
   					
   					saved_part.add_child(weld)
   					
   					weld_count = 1
   					print("New weld created between the touching objects " + saved_part.name + " and " + body.name)
   
   
func _calculate_spatial_bounds(parent : Node3D, exclude_top_level_transform: bool) -> AABB: # taken from reddit
   var bounds : AABB = AABB()
   if parent is VisualInstance3D:
   	bounds = parent.get_aabb();

   for i in range(parent.get_child_count()):
   	var child : Node3D = parent.get_child(i)
   	if child:
   		var child_bounds : AABB = _calculate_spatial_bounds(child, false)
   		if bounds.size == Vector3.ZERO && parent:
   			bounds = child_bounds
   		else:
   			bounds = bounds.merge(child_bounds)
   if bounds.size == Vector3.ZERO && !parent:
   	bounds = AABB(Vector3(-0.2, -0.2, -0.2), Vector3(0.4, 0.4, 0.4))
   if !exclude_top_level_transform:
   	bounds = parent.transform * bounds
   return bounds


func snappedvec3(vec3: Vector3, snap_rate: float):
   return Vector3(snapped(vec3.x, snap_rate), snapped(vec3.y, snap_rate), snapped(vec3.z, snap_rate))

In Roblox, you use models and primary parts to move a myriad of descendant baseparts around while keeping relative positions.

In Godot, the position property is relative to the parent node, not the entire world coordinates. That being said, you just need to modify the position of the top-level node (that you would consider your model)

As said, editing the position is relative to the parent’s one, meaning if you want your model to be in 0, 0, 0, it wouldn’t be centered in world coordinates, it would be centered to the parent instead (which may lead to 0,0,0 in particular scenarios but that’s something else)
Fortunately, you can manipulate the global_position and use that to express your world coords !

Basically, nodes work like attachments in Roblox

Position <=> position
WorldPosition <=> global_position

And

Model:MoveTo(Vec3)
<=>
parent_node.global_position = vec3

There’s no generalized built in functionality like that in Godot. You could implement is relatively simply but for specific suggestions you’ll need to describe the constraints of the system, or post a video that shows how it works in Roblox.

Sorry for the lack of info, this is the current behavior in Godot:

And this is the wanted behavior in Roblox (old Roblox client is being used (with Novetus) because they deprecated HopperBins quite a while ago):

Unity also has a function that does something similar: Unity - Scripting API: Collider.ClosestPoint

I think I could probably do something to mimic this behavior using either variable size ShapeCasts, or by just having the object be moved with a high force, as I plan to use RigidBody3D nodes for this, as opposed to directly setting the position. A way to directly set the position to a known area that won’t intersect would be ideal, as it would avoid issues with Area3D nodes not detecting the object, despite the object being hovered over them.

Movement looks snapped to a grid. You can simply check if the new position would intersect and in case it would just keep the block where it is.

You can also instantiate a character body stand in for the moving block and use its move_and_collide() or even move_and_slide() methods.

I think both of these options would work well, especially move_and_collide() as it emits data when it collides, meaning I can store the last position and keep it there until it isn’t colliding anymore, and this seems compatible with my idea of making a prototype vehicle builder.

One possible problem is that I plan to have the ability to attach objects onto other off grid objects (for example, a vehicle), and Roblox seems to handle that by adding the position and rotation of the object being dragged against to the grid, as shown in this video

One other problem is that I don’t know how to detect if a part is going to intersect with another part without already knowing the position of all of the other parts, which I’m pretty sure you also need to do if you use move_and_collide() as you need to know when the part isn’t intersecting, so that way the object can be moved, and then check again for intersecting parts.

You can use raycasts to find adjacent candidates to snap/align to.