diff --git a/pkgs/clan-app/ui/src/scene/cubes.tsx b/pkgs/clan-app/ui/src/scene/cubes.tsx index 8004c4a65..62e403f1e 100644 --- a/pkgs/clan-app/ui/src/scene/cubes.tsx +++ b/pkgs/clan-app/ui/src/scene/cubes.tsx @@ -5,6 +5,7 @@ import { onMount, on, JSX, + Show, } from "solid-js"; import "./cubes.css"; @@ -22,6 +23,28 @@ import { renderLoop } from "./RenderLoop"; import { ObjectRegistry } from "./ObjectRegistry"; import { MachineManager } from "./MachineManager"; import cx from "classnames"; +import { Portal } from "solid-js/web"; +import { Menu } from "../components/ContextMenu/ContextMenu"; + +function intersectMachines( + event: MouseEvent, + renderer: THREE.WebGLRenderer, + 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, + -((event.clientY - rect.top) / rect.height) * 2 + 1, + ); + raycaster.setFromCamera(mouse, camera); + const intersects = raycaster.intersectObjects( + Array.from(machineManager.machines.values().map((m) => m.group)), + ); + + return intersects.map((i) => i.object.userData.id); +} function garbageCollectGroup(group: THREE.Group) { for (const child of group.children) { @@ -64,7 +87,7 @@ export function useMachineClick() { /*Gloabl signal*/ const [worldMode, setWorldMode] = createSignal< - "default" | "select" | "service" | "create" + "default" | "select" | "service" | "create" | "move" >("select"); export { worldMode, setWorldMode }; @@ -88,7 +111,7 @@ export function CubeScene(props: { let controls: MapControls; // Raycaster for clicking const raycaster = new THREE.Raycaster(); - let initBase: THREE.Mesh | undefined; + let actionBase: THREE.Mesh | undefined; // Create background scene const bgScene = new THREE.Scene(); @@ -111,6 +134,10 @@ export function CubeScene(props: { position: { x: 0, y: 0, z: 0 }, spherical: { radius: 0, theta: 0, phi: 0 }, }); + // Context menu state + const [contextOpen, setContextOpen] = createSignal(false); + const [menuPos, setMenuPos] = createSignal<{ x: number; y: number }>(); + const [menuIntersection, setMenuIntersection] = createSignal([]); // Grid configuration const GRID_SIZE = 1; @@ -126,8 +153,10 @@ export function CubeScene(props: { const BASE_COLOR = 0xecfdff; const BASE_EMISSIVE = 0x0c0c0c; - const CREATE_BASE_COLOR = 0x636363; + const ACTION_BASE_COLOR = 0x636363; + const CREATE_BASE_EMISSIVE = 0xc5fad7; + const MOVE_BASE_EMISSIVE = 0xb2d7ff; function createCubeBase( cube_pos: [number, number, number], @@ -350,15 +379,15 @@ export function CubeScene(props: { ); // Important create CubeBase depends on sharedBaseGeometry - initBase = createCubeBase( + actionBase = createCubeBase( [1, BASE_HEIGHT / 2, 1], 1, - CREATE_BASE_COLOR, + ACTION_BASE_COLOR, CREATE_BASE_EMISSIVE, ); - initBase.visible = false; + actionBase.visible = false; - scene.add(initBase); + scene.add(actionBase); // const spherical = new THREE.Spherical(); // spherical.setFromVector3(camera.position); @@ -387,9 +416,9 @@ export function CubeScene(props: { createEffect( on(worldMode, (mode) => { if (mode === "create") { - initBase!.visible = true; + actionBase!.visible = true; } else { - initBase!.visible = false; + actionBase!.visible = false; } renderLoop.requestRender(); }), @@ -426,11 +455,20 @@ export function CubeScene(props: { console.error("Error creating cube:", error); }) .finally(() => { - if (initBase) initBase.visible = false; + if (actionBase) actionBase.visible = false; setWorldMode("default"); }); } + if (worldMode() === "move") { + console.log("sanpped"); + const currId = menuIntersection().at(0); + const pos = cursorPosition(); + if (!currId || !pos) return; + + props.setMachinePos(currId, pos); + setWorldMode("select"); + } const rect = renderer.domElement.getBoundingClientRect(); const mouse = new THREE.Vector2( @@ -484,18 +522,28 @@ export function CubeScene(props: { renderLoop.requestRender(); }; + const handleMouseDown = (e: MouseEvent) => { + if (e.button === 2) { + e.preventDefault(); + e.stopPropagation(); + const intersection = intersectMachines( + e, + renderer, + camera, + machineManager, + raycaster, + ); + if (!intersection.length) return; + setMenuIntersection(intersection); + setMenuPos({ x: e.clientX, y: e.clientY }); + setContextOpen(true); + } + }; + + renderer.domElement.addEventListener("mousedown", handleMouseDown); renderer.domElement.addEventListener("mousemove", onMouseMove); window.addEventListener("resize", handleResize); - // For debugging, - // TODO: Remove in production - window.addEventListener( - "contextmenu", - (e) => { - e.stopPropagation(); - }, - { capture: true }, - ); // Initial render renderLoop.requestRender(); @@ -522,12 +570,12 @@ export function CubeScene(props: { renderer.domElement.removeEventListener("mousemove", onMouseMove); window.removeEventListener("resize", handleResize); - if (initBase) { - initBase.geometry.dispose(); - if (Array.isArray(initBase.material)) { - initBase.material.forEach((material) => material.dispose()); + if (actionBase) { + actionBase.geometry.dispose(); + if (Array.isArray(actionBase.material)) { + actionBase.material.forEach((material) => material.dispose()); } else { - initBase.material.dispose(); + actionBase.material.dispose(); } } @@ -543,10 +591,18 @@ export function CubeScene(props: { renderLoop.requestRender(); }; const onMouseMove = (event: MouseEvent) => { - if (worldMode() !== "create") return; - if (!initBase) return; + if (!(worldMode() === "create" || worldMode() === "move")) return; + if (!actionBase) return; - initBase.visible = true; + console.log("Mouse move in create/move mode"); + + actionBase.visible = true; + (actionBase.material as THREE.MeshPhongMaterial).emissive.set( + worldMode() === "create" ? CREATE_BASE_EMISSIVE : MOVE_BASE_EMISSIVE, + ); + + // Calculate mouse position in normalized device coordinates + // (-1 to +1) for both components const rect = renderer.domElement.getBoundingClientRect(); const mouse = new THREE.Vector2( @@ -577,21 +633,48 @@ export function CubeScene(props: { } if ( - Math.abs(initBase.position.x - snapped.x) > 0.01 || - Math.abs(initBase.position.z - snapped.z) > 0.01 + Math.abs(actionBase.position.x - snapped.x) > 0.01 || + Math.abs(actionBase.position.z - snapped.z) > 0.01 ) { // Only request render if the position actually changed - initBase.position.set(snapped.x, 0, snapped.z); + actionBase.position.set(snapped.x, 0, snapped.z); setCursorPosition([snapped.x, snapped.z]); // Update next position for cube creation renderLoop.requestRender(); } } }; + createEffect(() => { + if (contextOpen()) { + // Disable canvas pointer events so menu can receive events + renderer.domElement.style.pointerEvents = "none"; + labelRenderer.domElement.style.pointerEvents = "none"; + } else { + // Re-enable canvas interactions + renderer.domElement.style.pointerEvents = "auto"; + labelRenderer.domElement.style.pointerEvents = "none"; // keep labels non-interactive + } + }); + const handleMenuSelect = (mode: "move") => { + setWorldMode(mode); + console.log("Menu selected, new World mode", worldMode()); + }; + const machinesQuery = useMachinesQuery(props.clanURI); return ( <> + + + setContextOpen(false)} + /> + +