ui/machines: move on long press

This commit is contained in:
Johannes Kirschbauer
2025-09-01 19:14:32 +02:00
parent fe58de0997
commit 95c781bf4d
2 changed files with 147 additions and 55 deletions

View File

@@ -23,6 +23,53 @@ const BASE_EMISSIVE = 0x0c0c0c;
const BASE_SELECTED_COLOR = 0x69b0e3;
const BASE_SELECTED_EMISSIVE = 0x666666; // Emissive color for selected bases
export function createMachineMesh() {
const geometry = new THREE.BoxGeometry(CUBE_SIZE, CUBE_SIZE, CUBE_SIZE);
const material = new THREE.MeshPhongMaterial({
color: CUBE_COLOR,
emissive: CUBE_EMISSIVE,
shininess: 100,
transparent: true,
});
const cubeMesh = new THREE.Mesh(geometry, material);
cubeMesh.castShadow = true;
cubeMesh.receiveShadow = true;
cubeMesh.name = "cube";
cubeMesh.position.set(0, CUBE_HEIGHT / 2 + BASE_HEIGHT, 0);
const { baseMesh, baseMaterial } = createCubeBase(
BASE_COLOR,
BASE_EMISSIVE,
new THREE.BoxGeometry(BASE_SIZE, BASE_HEIGHT, BASE_SIZE),
);
return {
cubeMesh,
baseMesh,
baseMaterial,
geometry,
material,
};
}
export function createCubeBase(
color: THREE.ColorRepresentation,
emissive: THREE.ColorRepresentation,
geometry: THREE.BoxGeometry,
) {
const baseMaterial = new THREE.MeshPhongMaterial({
color,
emissive,
transparent: true,
opacity: 1,
});
const baseMesh = new THREE.Mesh(geometry, baseMaterial);
baseMesh.position.set(0, BASE_HEIGHT / 2, 0);
baseMesh.receiveShadow = false;
return { baseMesh, baseMaterial };
}
export class MachineRepr {
public id: string;
public group: THREE.Group;
@@ -46,27 +93,17 @@ export class MachineRepr {
) {
this.id = id;
this.camera = camera;
this.geometry = new THREE.BoxGeometry(CUBE_SIZE, CUBE_SIZE, CUBE_SIZE);
this.material = new THREE.MeshPhongMaterial({
color: CUBE_COLOR,
emissive: CUBE_EMISSIVE,
shininess: 100,
});
this.cubeMesh = new THREE.Mesh(this.geometry, this.material);
this.cubeMesh.castShadow = true;
this.cubeMesh.receiveShadow = true;
const { baseMesh, cubeMesh, geometry, material } = createMachineMesh();
this.cubeMesh = cubeMesh;
this.cubeMesh.userData = { id };
this.cubeMesh.name = "cube";
this.cubeMesh.position.set(0, CUBE_HEIGHT / 2 + BASE_HEIGHT, 0);
this.baseMesh = this.createCubeBase(
BASE_COLOR,
BASE_EMISSIVE,
new THREE.BoxGeometry(BASE_SIZE, BASE_HEIGHT, BASE_SIZE),
);
this.baseMesh = baseMesh;
this.baseMesh.name = "base";
this.geometry = geometry;
this.material = material;
const label = this.createLabel(id);
const shadowPlaneMaterial = new THREE.MeshStandardMaterial({
@@ -149,23 +186,6 @@ export class MachineRepr {
renderLoop.requestRender();
}
private createCubeBase(
color: THREE.ColorRepresentation,
emissive: THREE.ColorRepresentation,
geometry: THREE.BoxGeometry,
) {
const baseMaterial = new THREE.MeshPhongMaterial({
color,
emissive,
transparent: true,
opacity: 1,
});
const base = new THREE.Mesh(geometry, baseMaterial);
base.position.set(0, BASE_HEIGHT / 2, 0);
base.receiveShadow = false;
return base;
}
private createLabel(id: string) {
const text = new Text();
text.text = id;

View File

@@ -25,7 +25,12 @@ import { MachineManager } from "./MachineManager";
import cx from "classnames";
import { Portal } from "solid-js/web";
import { Menu } from "../components/ContextMenu/ContextMenu";
import { clearHighlight, setHighlightGroups } from "./highlightStore";
import {
clearHighlight,
highlightGroups,
setHighlightGroups,
} from "./highlightStore";
import { createMachineMesh } from "./MachineRepr";
function intersectMachines(
event: MouseEvent,
@@ -113,6 +118,7 @@ export function CubeScene(props: {
// Raycaster for clicking
const raycaster = new THREE.Raycaster();
let actionBase: THREE.Mesh | undefined;
let actionMachine: THREE.Group | undefined;
// Create background scene
const bgScene = new THREE.Scene();
@@ -129,6 +135,8 @@ export function CubeScene(props: {
// Managed by controls
const [isDragging, setIsDragging] = createSignal(false);
const [cancelMove, setCancelMove] = createSignal<NodeJS.Timeout>();
const [cursorPosition, setCursorPosition] = createSignal<[number, number]>();
const [cameraInfo, setCameraInfo] = createSignal({
@@ -300,12 +308,12 @@ export function CubeScene(props: {
bgCamera,
);
controls.addEventListener("start", (e) => {
setIsDragging(true);
});
controls.addEventListener("end", (e) => {
setIsDragging(false);
});
// controls.addEventListener("start", (e) => {
// setIsDragging(true);
// });
// controls.addEventListener("end", (e) => {
// setIsDragging(false);
// });
// Lighting
const ambientLight = new THREE.AmbientLight(0xd9f2f7, 0.72);
@@ -384,6 +392,23 @@ export function CubeScene(props: {
scene.add(actionBase);
function createActionMachine() {
const { baseMesh, cubeMesh, material, baseMaterial } =
createMachineMesh();
const group = new THREE.Group();
group.add(baseMesh);
group.add(cubeMesh);
// group.scale.set(0.75, 0.75, 0.75);
material.opacity = 0.6;
baseMaterial.opacity = 0.3;
baseMaterial.emissive.set(MOVE_BASE_EMISSIVE);
// Hide until needed
group.visible = false;
return group;
}
actionMachine = createActionMachine();
scene.add(actionMachine);
// const spherical = new THREE.Spherical();
// spherical.setFromVector3(camera.position);
@@ -520,24 +545,69 @@ export function CubeScene(props: {
};
const handleMouseDown = (e: MouseEvent) => {
const intersection = intersectMachines(
e,
renderer,
camera,
machineManager,
raycaster,
);
if (e.button === 0) {
// Left button
if (worldMode() === "select" && intersection.length) {
// Disable controls to avoid conflict
controls.enabled = false;
// Change cursor to grabbing
const cancelMove = setTimeout(() => {
setIsDragging(true);
// Set machine as flying
setHighlightGroups({ move: new Set(intersection) });
setWorldMode("move");
renderLoop.requestRender();
}, 500);
setCancelMove(cancelMove);
}
}
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);
}
};
const handleMouseUp = (e: MouseEvent) => {
if (e.button === 0) {
console.log("Left mouse up");
setIsDragging(false);
if (cancelMove()) {
clearTimeout(cancelMove()!);
setCancelMove(undefined);
}
if (worldMode() === "move") {
// Cancel long-press if it wasn't triggered yet
// Re-enable controls
controls.enabled = true;
// Set machine as not flying
props.setMachinePos(
highlightGroups["move"].values().next().value!,
cursorPosition() || null,
);
clearHighlight("move");
setWorldMode("select");
renderLoop.requestRender();
}
}
};
renderer.domElement.addEventListener("mousedown", handleMouseDown);
renderer.domElement.addEventListener("mouseup", handleMouseUp);
renderer.domElement.addEventListener("mousemove", onMouseMove);
window.addEventListener("resize", handleResize);
@@ -589,14 +659,16 @@ export function CubeScene(props: {
};
const onMouseMove = (event: MouseEvent) => {
if (!(worldMode() === "create" || worldMode() === "move")) return;
if (!actionBase) return;
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,
);
const actionRepr = worldMode() === "create" ? actionBase : actionMachine;
if (!actionRepr) return;
actionRepr.visible = true;
// (actionRepr.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
@@ -630,11 +702,11 @@ export function CubeScene(props: {
}
if (
Math.abs(actionBase.position.x - snapped.x) > 0.01 ||
Math.abs(actionBase.position.z - snapped.z) > 0.01
Math.abs(actionRepr.position.x - snapped.x) > 0.01 ||
Math.abs(actionRepr.position.z - snapped.z) > 0.01
) {
// Only request render if the position actually changed
actionBase.position.set(snapped.x, 0, snapped.z);
actionRepr.position.set(snapped.x, 0, snapped.z);
setCursorPosition([snapped.x, snapped.z]); // Update next position for cube creation
renderLoop.requestRender();
}