From e42a07423e4887129633c7209b5c93db67b84077 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sun, 31 Aug 2025 10:42:59 +0200 Subject: [PATCH] ui/machineLabels: use troika for label rendering --- pkgs/clan-app/ui/src/routes/Clan/Clan.tsx | 3 ++ pkgs/clan-app/ui/src/scene/MachineRepr.ts | 52 ++++++++++------------- pkgs/clan-app/ui/src/scene/RenderLoop.ts | 5 +++ pkgs/clan-app/ui/src/scene/cubes.tsx | 13 +++--- 4 files changed, 35 insertions(+), 38 deletions(-) diff --git a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx index f6ca76a52..a1a9fbf77 100644 --- a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx +++ b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx @@ -12,6 +12,7 @@ import { useContext, } from "solid-js"; import { + buildClanPath, buildMachinePath, maybeUseMachineName, useClanURI, @@ -197,6 +198,8 @@ const ClanSceneController = (props: RouteSectionProps) => { const selected = ids.values().next().value; if (selected) { navigate(buildMachinePath(ctx.clanURI, selected)); + } else { + navigate(buildClanPath(ctx.clanURI)); } }; diff --git a/pkgs/clan-app/ui/src/scene/MachineRepr.ts b/pkgs/clan-app/ui/src/scene/MachineRepr.ts index 47d256f02..48bdfa034 100644 --- a/pkgs/clan-app/ui/src/scene/MachineRepr.ts +++ b/pkgs/clan-app/ui/src/scene/MachineRepr.ts @@ -3,9 +3,9 @@ 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"; -import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js"; -import { FontLoader, FontData } from "three/examples/jsm/Addons"; -import font from "../../.fonts/CommitMono_Regular.json"; +// @ts-expect-error: No types for troika-three-text +import { Text } from "troika-three-text"; +import ttf from "../../.fonts/CommitMonoV143-VF.ttf"; // Constants const BASE_SIZE = 0.9; @@ -68,7 +68,6 @@ export class MachineRepr { this.baseMesh.name = "base"; const label = this.createLabel(id); - // this.cubeMesh.add(label); const shadowPlaneMaterial = new THREE.MeshStandardMaterial({ color: BASE_COLOR, // any color you like @@ -168,34 +167,27 @@ export class MachineRepr { } private createLabel(id: string) { - const loader = new FontLoader(); - const final = loader.parse(font as unknown as FontData); + const text = new Text(); + text.text = id; + text.font = ttf; + // text.font = ".fonts/CommitMonoV143-VF.woff2"; // <-- normal web font, not JSON + text.fontSize = 0.15; // relative to your cube size + text.color = 0x000000; // any THREE.Color + text.anchorX = "center"; // horizontal centering + text.anchorY = "bottom"; // baseline aligns to cube top + text.position.set(0, CUBE_SIZE + 0.05, 0); - const geometry = new TextGeometry(id, { - font: final, - size: 0.1, - depth: 0.01, - curveSegments: 12, - bevelEnabled: false, - bevelThickness: 0.01, - bevelSize: 0.01, - bevelOffset: 0, - bevelSegments: 5, + // If you want it to always face camera: + text.userData.isLabel = true; + text.outlineWidth = 0.005; + text.outlineColor = 0x333333; + text.quaternion.copy(this.camera.quaternion); + + // Re-render on text changes + text.sync(() => { + renderLoop.requestRender(); }); - - const textMaterial = new THREE.MeshPhongMaterial({ color: 0x000000 }); - const textMesh = new THREE.Mesh(geometry, textMaterial); - - geometry.computeBoundingBox(); - if (geometry.boundingBox) { - const xMid = - -0.5 * (geometry.boundingBox.max.x - geometry.boundingBox.min.x); - geometry.translate(xMid, 0, 0); // shift so it's centered on X - } - textMesh.position.set(0, CUBE_SIZE + 0.15, 0); // above the cube - textMesh.quaternion.copy(this.camera.quaternion); - textMesh.userData.isLabel = true; - return textMesh; + return text; } dispose(scene: THREE.Scene) { diff --git a/pkgs/clan-app/ui/src/scene/RenderLoop.ts b/pkgs/clan-app/ui/src/scene/RenderLoop.ts index a65466bd4..42e318a3f 100644 --- a/pkgs/clan-app/ui/src/scene/RenderLoop.ts +++ b/pkgs/clan-app/ui/src/scene/RenderLoop.ts @@ -98,6 +98,11 @@ class RenderLoop { if (obj.userData.isLabel) { (obj as THREE.Mesh).quaternion.copy(this.camera.quaternion); } + // if (obj.userData.isLabel) { + // const camPos = new THREE.Vector3(); + // this.camera.getWorldPosition(camPos); + // obj.lookAt(new THREE.Vector3(camPos.x, obj.position.y, camPos.z)); + // } }); this.labelRenderer.render(this.scene, this.camera); diff --git a/pkgs/clan-app/ui/src/scene/cubes.tsx b/pkgs/clan-app/ui/src/scene/cubes.tsx index 3dc3319e0..ba242a210 100644 --- a/pkgs/clan-app/ui/src/scene/cubes.tsx +++ b/pkgs/clan-app/ui/src/scene/cubes.tsx @@ -25,6 +25,7 @@ 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"; function intersectMachines( event: MouseEvent, @@ -177,12 +178,6 @@ export function CubeScene(props: { return base; } - function toggleSelection(id: string) { - const next = new Set(); - next.add(id); - props.onSelect(next); - } - const initialCameraPosition = { x: 20, y: 20, z: 20 }; const initialSphericalCameraPosition = new THREE.Spherical(); initialSphericalCameraPosition.setFromVector3( @@ -469,6 +464,7 @@ export function CubeScene(props: { props.setMachinePos(currId, pos); setWorldMode("select"); + clearHighlight("move"); } const rect = renderer.domElement.getBoundingClientRect(); @@ -486,13 +482,13 @@ export function CubeScene(props: { console.log("Clicked on cube:", intersects); const id = intersects[0].object.userData.id; - if (worldMode() === "select") toggleSelection(id); + if (worldMode() === "select") props.onSelect(new Set([id])); emitMachineClick(id); // notify subscribers } else { emitMachineClick(null); - props.onSelect(new Set()); // Clear selection if clicked outside cubes + if (worldMode() === "select") props.onSelect(new Set()); } }; @@ -646,6 +642,7 @@ export function CubeScene(props: { }; const handleMenuSelect = (mode: "move") => { setWorldMode(mode); + setHighlightGroups({ move: new Set(menuIntersection()) }); console.log("Menu selected, new World mode", worldMode()); };