From 36f73d40b34f21632c04fb9e3bb9b5d27f3b60ef Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Tue, 2 Sep 2025 14:09:16 +0200 Subject: [PATCH] ui/scene: fix double click on move --- pkgs/clan-app/ui/src/routes/Clan/Clan.tsx | 17 +--- pkgs/clan-app/ui/src/scene/cubes.tsx | 108 ++++++++++++++-------- 2 files changed, 71 insertions(+), 54 deletions(-) diff --git a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx index 3c7a08f00..7128f95f0 100644 --- a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx +++ b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx @@ -132,8 +132,6 @@ const ClanSceneController = (props: RouteSectionProps) => { const navigate = useNavigate(); - const [showService, setShowService] = createSignal(false); - const [currentPromise, setCurrentPromise] = createSignal<{ resolve: ({ id }: { id: string }) => void; reject: (err: unknown) => void; @@ -219,21 +217,9 @@ const ClanSceneController = (props: RouteSectionProps) => { console.error("Error creating service instance", result.errors); } toast.success("Created"); - setShowService(false); setWorldMode("select"); }; - createEffect( - on(worldMode, (mode) => { - if (mode === "service") { - setShowService(true); - } else { - // TODO: request soft close instead of forced close - setShowService(false); - } - }), - ); - return ( <> @@ -268,11 +254,10 @@ const ClanSceneController = (props: RouteSectionProps) => { isLoading={ctx.isLoading()} cubesQuery={ctx.machinesQuery} toolbarPopup={ - + { - setShowService(false); setWorldMode("select"); currentPromise()?.resolve({ id: "0" }); }} diff --git a/pkgs/clan-app/ui/src/scene/cubes.tsx b/pkgs/clan-app/ui/src/scene/cubes.tsx index 6d4a428aa..5053a3099 100644 --- a/pkgs/clan-app/ui/src/scene/cubes.tsx +++ b/pkgs/clan-app/ui/src/scene/cubes.tsx @@ -38,7 +38,7 @@ function intersectMachines( camera: THREE.Camera, machineManager: MachineManager, raycaster: THREE.Raycaster, -): string[] { +) { const rect = renderer.domElement.getBoundingClientRect(); const mouse = new THREE.Vector2( ((event.clientX - rect.left) / rect.width) * 2 - 1, @@ -49,7 +49,10 @@ function intersectMachines( Array.from(machineManager.machines.values().map((m) => m.group)), ); - return intersects.map((i) => i.object.userData.id); + return { + machines: intersects.map((i) => i.object.userData.id), + intersection: intersects, + }; } function garbageCollectGroup(group: THREE.Group) { @@ -129,6 +132,8 @@ export function CubeScene(props: { let sharedCubeGeometry: THREE.BoxGeometry; let sharedBaseGeometry: THREE.BoxGeometry; + let machineManager: MachineManager; + const [positionMode, setPositionMode] = createSignal<"grid" | "circle">( "grid", ); @@ -137,6 +142,7 @@ export function CubeScene(props: { const [cancelMove, setCancelMove] = createSignal(); + // TODO: Unify this with actionRepr position const [cursorPosition, setCursorPosition] = createSignal<[number, number]>(); const [cameraInfo, setCameraInfo] = createSignal({ @@ -446,7 +452,7 @@ export function CubeScene(props: { const registry = new ObjectRegistry(); - const machineManager = new MachineManager( + machineManager = new MachineManager( scene, registry, props.sceneStore, @@ -545,7 +551,7 @@ export function CubeScene(props: { }; const handleMouseDown = (e: MouseEvent) => { - const intersection = intersectMachines( + const { machines, intersection } = intersectMachines( e, renderer, camera, @@ -555,7 +561,7 @@ export function CubeScene(props: { if (e.button === 0) { // Left button - if (worldMode() === "select" && intersection.length) { + if (worldMode() === "select" && machines.length) { // Disable controls to avoid conflict controls.enabled = false; @@ -563,7 +569,8 @@ export function CubeScene(props: { const cancelMove = setTimeout(() => { setIsDragging(true); // Set machine as flying - setHighlightGroups({ move: new Set(intersection) }); + setHighlightGroups({ move: new Set(machines) }); + setWorldMode("move"); renderLoop.requestRender(); }, 500); @@ -575,7 +582,7 @@ export function CubeScene(props: { e.preventDefault(); e.stopPropagation(); if (!intersection.length) return; - setMenuIntersection(intersection); + setMenuIntersection(machines); setMenuPos({ x: e.clientX, y: e.clientY }); setContextOpen(true); } @@ -649,6 +656,39 @@ export function CubeScene(props: { }); }); + const snapToGrid = (point: THREE.Vector3) => { + if (!props.sceneStore) return; + // Snap to grid + const snapped = new THREE.Vector3( + Math.round(point.x / GRID_SIZE) * GRID_SIZE, + 0, + Math.round(point.z / GRID_SIZE) * GRID_SIZE, + ); + + // Skip snapping if there's already a cube at this position + const positions = Object.entries(props.sceneStore()); + const intersects = positions.some( + ([_id, p]) => p.position[0] === snapped.x && p.position[1] === snapped.z, + ); + const movingMachine = Array.from(highlightGroups["move"] || [])[0]; + const startingPos = positions.find(([_id, p]) => _id === movingMachine); + if (startingPos) { + const isStartingPos = + snapped.x === startingPos[1].position[0] && + snapped.z === startingPos[1].position[1]; + // If Intersect any other machine and not the one being moved + if (!isStartingPos && intersects) { + return; + } + } else { + if (intersects) { + return; + } + } + + return snapped; + }; + const onAddClick = (event: MouseEvent) => { setPositionMode("grid"); setWorldMode("create"); @@ -678,37 +718,8 @@ export function CubeScene(props: { if (intersects.length > 0) { const point = intersects[0].point; - // Snap to grid - const snapped = new THREE.Vector3( - Math.round(point.x / GRID_SIZE) * GRID_SIZE, - 0, - Math.round(point.z / GRID_SIZE) * GRID_SIZE, - ); - - // Skip snapping if there's already a cube at this position - if (props.sceneStore()) { - const positions = Object.entries(props.sceneStore()); - const intersects = positions.some( - ([_id, p]) => - p.position[0] === snapped.x && p.position[1] === snapped.z, - ); - const movingMachine = Array.from(highlightGroups["move"] || [])[0]; - const startingPos = positions.find(([_id, p]) => _id === movingMachine); - if (startingPos) { - const isStartingPos = - snapped.x === startingPos[1].position[0] && - snapped.z === startingPos[1].position[1]; - // If Intersect any other machine and not the one being moved - if (!isStartingPos && intersects) { - return; - } - } else { - if (intersects) { - return; - } - } - } - + const snapped = snapToGrid(point); + if (!snapped) return; if ( Math.abs(actionRepr.position.x - snapped.x) > 0.01 || Math.abs(actionRepr.position.z - snapped.z) > 0.01 @@ -723,8 +734,29 @@ export function CubeScene(props: { const handleMenuSelect = (mode: "move") => { setWorldMode(mode); setHighlightGroups({ move: new Set(menuIntersection()) }); + + // Find the position of the first selected machine + // Set the actionMachine position to that + const firstId = menuIntersection()[0]; + if (firstId) { + const machine = machineManager.machines.get(firstId); + if (machine && actionMachine) { + actionMachine.position.set( + machine.group.position.x, + 0, + machine.group.position.z, + ); + setCursorPosition([machine.group.position.x, machine.group.position.z]); + } + } }; + createEffect( + on(worldMode, (mode) => { + console.log("World mode changed to", mode); + }), + ); + const machinesQuery = useMachinesQuery(props.clanURI); return (