From 9e32be4e48a4902f2e7bff425bf80c150915f5ba Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Tue, 29 Jul 2025 10:00:36 +0200 Subject: [PATCH] ui: add machineRepr to handle machine visual representation --- pkgs/clan-app/ui/src/scene/MachineRepr.ts | 141 ++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 pkgs/clan-app/ui/src/scene/MachineRepr.ts diff --git a/pkgs/clan-app/ui/src/scene/MachineRepr.ts b/pkgs/clan-app/ui/src/scene/MachineRepr.ts new file mode 100644 index 000000000..cd29239be --- /dev/null +++ b/pkgs/clan-app/ui/src/scene/MachineRepr.ts @@ -0,0 +1,141 @@ +import * as THREE from "three"; +import { ObjectRegistry } from "./ObjectRegistry"; +import { CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer.js"; +import { Accessor, createEffect, createRoot, on } from "solid-js"; +import { renderLoop } from "./RenderLoop"; + +// Constants +const BASE_SIZE = 0.9; +const CUBE_SIZE = BASE_SIZE / 1.5; +const CUBE_HEIGHT = CUBE_SIZE; +const BASE_HEIGHT = 0.05; +const CUBE_COLOR = 0xd7e0e1; +const CUBE_EMISSIVE = 0x303030; + +const CUBE_SELECTED_COLOR = 0x4b6767; + +const BASE_COLOR = 0xecfdff; +const BASE_EMISSIVE = 0x0c0c0c; +const BASE_SELECTED_COLOR = 0x69b0e3; +const BASE_SELECTED_EMISSIVE = 0x666666; // Emissive color for selected bases + +export class MachineRepr { + public id: string; + public group: THREE.Group; + + private cubeMesh: THREE.Mesh; + private baseMesh: THREE.Mesh; + private geometry: THREE.BoxGeometry; + private material: THREE.MeshPhongMaterial; + + private disposeRoot: () => void; + + constructor( + scene: THREE.Scene, + registry: ObjectRegistry, + position: THREE.Vector2, + id: string, + selectedSignal: Accessor>, + ) { + this.id = id; + 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; + this.cubeMesh.userData = { id }; + this.cubeMesh.name = "cube"; + this.cubeMesh.position.set(0, CUBE_HEIGHT / 2, 0); + + this.baseMesh = this.createCubeBase( + BASE_COLOR, + BASE_EMISSIVE, + new THREE.BoxGeometry(BASE_SIZE, BASE_HEIGHT, BASE_SIZE), + ); + this.baseMesh.name = "base"; + + const label = this.createLabel(id); + this.cubeMesh.add(label); + + this.group = new THREE.Group(); + this.group.add(this.cubeMesh); + this.group.add(this.baseMesh); + + this.group.position.set(position.x, 0, position.y); + this.group.userData.id = id; + + this.disposeRoot = createRoot((disposeEffects) => { + createEffect( + on(selectedSignal, (selectedIds) => { + const isSelected = selectedIds.has(this.id); + // Update cube + (this.cubeMesh.material as THREE.MeshPhongMaterial).color.set( + isSelected ? CUBE_SELECTED_COLOR : CUBE_COLOR, + ); + + // Update base + (this.baseMesh.material as THREE.MeshPhongMaterial).color.set( + isSelected ? BASE_SELECTED_COLOR : BASE_COLOR, + ); + (this.baseMesh.material as THREE.MeshPhongMaterial).emissive.set( + isSelected ? BASE_SELECTED_EMISSIVE : BASE_EMISSIVE, + ); + + renderLoop.requestRender(); + }), + ); + + return disposeEffects; + }); + + scene.add(this.group); + + registry.add({ + object: this.group, + id, + type: "machine", + dispose: () => this.dispose(scene), + }); + } + + 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 = true; + return base; + } + + private createLabel(id: string) { + const div = document.createElement("div"); + div.className = "machine-label"; + div.textContent = id; + const label = new CSS2DObject(div); + label.position.set(0, CUBE_SIZE + 0.1, 0); + return label; + } + + dispose(scene: THREE.Scene) { + this.disposeRoot?.(); // Stop SolidJS effects + + scene.remove(this.group); + + this.geometry.dispose(); + this.material.dispose(); + (this.baseMesh.material as THREE.Material).dispose(); + } +}