I'm building a 3D configurator for sofa modules and had a question about my snapping function.
In the scene, I drag models around to place them using use-gesture's useDrag hook, and when they approach they snap onto eachother.
However: They seem to love eachother a bit to much, so it's getting hard to get them off while dragging. Any idea how I can fix this?
I already tried delaying the snap function on "first" so you have a second to pull it off, but that glitched.
import { useAppStore } from "@/stores/appStore";
import { useDrag } from "@use-gesture/react";
import { Box3, Vector3 } from "three";
const useModelDrag = (id) => {
//all variable declarations
/************************ Drag function ************************/
const bind = useDrag(({ last, first, event }) => {
if (first) {
update({ dragging: true });
}
if (last) {
update({ dragging: false });
}
event.ray.intersectPlane(floorPlane, planeIntersectPoint);
draggingModule(id, planeIntersectPoint, last);
});
/************************ Actions while dragging (snapping) ************************/
const draggingModule = (id, dragPosition, last) => {
if (dragging === false) return;
const currentModule = active[id];
currentBox.setFromObject(currentModule.ref);
currentBox.getCenter(currentCenter);
currentBox.getSize(currentSize);
if (
dragPosition.x < room.width / 2 - currentSize.x / 2 + 10 &&
dragPosition.x > -room.width / 2 + currentSize.x / 2 - 10 &&
dragPosition.z < room.depth / 2 - currentSize.z / 2 + 10 &&
dragPosition.z > -room.depth / 2 + currentSize.z / 2 - 10
)
currentModule.ref.position.set(dragPosition.x, dragPosition.y, dragPosition.z);
// start comparison when more than two models
if (active.length >= 2) {
const distances = [];
for (let i = 0; i < active.length; i++) {
if (i === id) continue;
const module = active[i];
moduleBox.setFromObject(module.ref);
const modelDistance = moduleBox.distanceToPoint(currentCenter);
distances.push({ modelDistance, id: i });
}
distances.sort((a, b) => {
return a.modelDistance - b.modelDistance;
});
// calculating the closest module
const smallestDistance = distances[0];
const closestModule = active[smallestDistance.id];
if (closestModule === undefined) return;
if (smallestDistance.modelDistance > 80) return;
closestBox.setFromObject(closestModule.ref);
closestBox.getCenter(closestCenter);
closestBox.getSize(closestSize);
if (!closestBox) return;
if (currentBottom.z !== currentBox.max.z) { //extra check to not execute unnecessary code
closestRight.x = closestBox.max.x;
closestLeft.x = closestBox.min.x;
closestTop.z = closestBox.min.z;
closestBottom.z = closestBox.max.z;
currentRight.x = currentBox.max.x;
currentLeft.x = currentBox.min.x;
currentTop.z = currentBox.min.z;
currentBottom.z = currentBox.max.z;
}
sideDistances = [
{ name: "right", dist: closestRight.distanceTo(currentLeft) },
{ name: "left", dist: closestLeft.distanceTo(currentRight) },
{ name: "top", dist: closestTop.distanceTo(currentBottom) },
{ name: "bottom", dist: closestBottom.distanceTo(currentTop) },
];
sideDistances.sort((a, b) => {
return a.dist - b.dist;
});
const closestSide = sideDistances[0];
// snapping conditions based on model sides
switch (closestSide.name) {
case "left":
currentModule.ref.position.x = closestBox.min.x - currentSize.x / 2 + 11;
break;
case "right":
currentModule.ref.position.x = closestBox.max.x + currentSize.x / 2 - 11;
break;
case "top":
currentModule.ref.position.z = closestBox.min.z - currentSize.z / 2 + 11;
break;
case "bottom":
currentModule.ref.position.z = closestBox.max.z + currentSize.z / 2 - 11;
break;
default:
break;
}
// extra check to snap to corner after dragging
if (last) {
const closestModuleSnapPoints = closestModule.ref.children[0].children;
const currentModuleSnapPoints = currentModule.ref.children[0].children;
for (let i = 0; i < currentModuleSnapPoints.length; i++) {
for (let j = 0; j < closestModuleSnapPoints.length; j++) {
currentModuleSnapPoints[i].getWorldPosition(positionA);
closestModuleSnapPoints[j].getWorldPosition(positionB);
const pDist = positionA.distanceTo(positionB);
pointDistances.push({ pDist, pointAIndex: i, pointBIndex: j });
}
}
pointDistances.sort((a, b) => {
return a.pDist - b.pDist;
});
const closestPair = pointDistances[0];
// conditions for snapping to model corners
if (closestPair.pDist > 15) return;
switch (true) {
case (closestPair.pointBIndex === 0 && closestPair.pointAIndex === 3) ||
(closestPair.pointBIndex === 3 && closestPair.pointAIndex === 0):
currentModule.ref.position.z = closestBox.min.z + currentSize.z / 2;
break;
case (closestPair.pointBIndex === 1 && closestPair.pointAIndex === 2) ||
(closestPair.pointBIndex === 2 && closestPair.pointAIndex === 1):
currentModule.ref.position.z = closestBox.max.z - currentSize.z / 2;
break;
case (closestPair.pointBIndex === 3 && closestPair.pointAIndex === 2) ||
(closestPair.pointBIndex === 2 && closestPair.pointAIndex === 3):
currentModule.ref.position.x = closestBox.min.x + currentSize.x / 2;
break;
case (closestPair.pointBIndex === 1 && closestPair.pointAIndex === 0) ||
(closestPair.pointBIndex === 0 && closestPair.pointAIndex === 1):
currentModule.ref.position.x = closestBox.max.x - currentSize.x / 2;
break;
default:
break;
}
}
}
};
return [bind];
};
export default useModelDrag;
The snappoints are configured like this:
const useSnapPoints = (id, ref, position = [0, 0, 0]) => {
let [snapPoints, setSnappoints] = useState([]);
const initiateModule = useAppStore((state) => state.initiateModule);
const activeModules = useAppStore((state) => state.activeModules);
const initialRun = useRef();
useEffect(() => {
if (ref.current) {
initiateModule(id, ref.current);
const modelBox = new Box3().setFromObject(ref.current.children[1]);
const wp = new Vector3();
ref.current.getWorldPosition(wp);
snapPoints = [
{
position: [modelBox.max.x - wp.x, 10, modelBox.min.z - wp.z],
class: "back",
// color: "orange",
},
{
position: [modelBox.max.x - wp.x, 10, modelBox.max.z - wp.z],
class: "front",
// color: "red",
},
{
position: [modelBox.min.x - wp.x, 10, modelBox.max.z - wp.z],
class: "front",
// color: "purple",
},
{
position: [modelBox.min.x - wp.x, 10, modelBox.min.z - wp.z],
class: "back",
// color: "green",
},
];
if (!initialRun?.current) {
ref.current.position.set(position[0], position[1], position[2]);
initialRun.current = { ran: true };
}
setSnappoints(snapPoints);
}
}, [ref.current, activeModules[id].rotationIndex]);
return [snapPoints];
};
export default useSnapPoints;