ui/machineLabels: use troika for label rendering

This commit is contained in:
Johannes Kirschbauer
2025-08-31 10:42:59 +02:00
parent c5178ac16a
commit e42a07423e
4 changed files with 35 additions and 38 deletions

View File

@@ -12,6 +12,7 @@ import {
useContext, useContext,
} from "solid-js"; } from "solid-js";
import { import {
buildClanPath,
buildMachinePath, buildMachinePath,
maybeUseMachineName, maybeUseMachineName,
useClanURI, useClanURI,
@@ -197,6 +198,8 @@ const ClanSceneController = (props: RouteSectionProps) => {
const selected = ids.values().next().value; const selected = ids.values().next().value;
if (selected) { if (selected) {
navigate(buildMachinePath(ctx.clanURI, selected)); navigate(buildMachinePath(ctx.clanURI, selected));
} else {
navigate(buildClanPath(ctx.clanURI));
} }
}; };

View File

@@ -3,9 +3,9 @@ import { ObjectRegistry } from "./ObjectRegistry";
import { CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer.js"; import { CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer.js";
import { Accessor, createEffect, createRoot, on } from "solid-js"; import { Accessor, createEffect, createRoot, on } from "solid-js";
import { renderLoop } from "./RenderLoop"; import { renderLoop } from "./RenderLoop";
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js"; // @ts-expect-error: No types for troika-three-text
import { FontLoader, FontData } from "three/examples/jsm/Addons"; import { Text } from "troika-three-text";
import font from "../../.fonts/CommitMono_Regular.json"; import ttf from "../../.fonts/CommitMonoV143-VF.ttf";
// Constants // Constants
const BASE_SIZE = 0.9; const BASE_SIZE = 0.9;
@@ -68,7 +68,6 @@ export class MachineRepr {
this.baseMesh.name = "base"; this.baseMesh.name = "base";
const label = this.createLabel(id); const label = this.createLabel(id);
// this.cubeMesh.add(label);
const shadowPlaneMaterial = new THREE.MeshStandardMaterial({ const shadowPlaneMaterial = new THREE.MeshStandardMaterial({
color: BASE_COLOR, // any color you like color: BASE_COLOR, // any color you like
@@ -168,34 +167,27 @@ export class MachineRepr {
} }
private createLabel(id: string) { private createLabel(id: string) {
const loader = new FontLoader(); const text = new Text();
const final = loader.parse(font as unknown as FontData); 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, { // If you want it to always face camera:
font: final, text.userData.isLabel = true;
size: 0.1, text.outlineWidth = 0.005;
depth: 0.01, text.outlineColor = 0x333333;
curveSegments: 12, text.quaternion.copy(this.camera.quaternion);
bevelEnabled: false,
bevelThickness: 0.01, // Re-render on text changes
bevelSize: 0.01, text.sync(() => {
bevelOffset: 0, renderLoop.requestRender();
bevelSegments: 5,
}); });
return text;
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;
} }
dispose(scene: THREE.Scene) { dispose(scene: THREE.Scene) {

View File

@@ -98,6 +98,11 @@ class RenderLoop {
if (obj.userData.isLabel) { if (obj.userData.isLabel) {
(obj as THREE.Mesh).quaternion.copy(this.camera.quaternion); (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); this.labelRenderer.render(this.scene, this.camera);

View File

@@ -25,6 +25,7 @@ import { MachineManager } from "./MachineManager";
import cx from "classnames"; import cx from "classnames";
import { Portal } from "solid-js/web"; import { Portal } from "solid-js/web";
import { Menu } from "../components/ContextMenu/ContextMenu"; import { Menu } from "../components/ContextMenu/ContextMenu";
import { clearHighlight, setHighlightGroups } from "./highlightStore";
function intersectMachines( function intersectMachines(
event: MouseEvent, event: MouseEvent,
@@ -177,12 +178,6 @@ export function CubeScene(props: {
return base; return base;
} }
function toggleSelection(id: string) {
const next = new Set<string>();
next.add(id);
props.onSelect(next);
}
const initialCameraPosition = { x: 20, y: 20, z: 20 }; const initialCameraPosition = { x: 20, y: 20, z: 20 };
const initialSphericalCameraPosition = new THREE.Spherical(); const initialSphericalCameraPosition = new THREE.Spherical();
initialSphericalCameraPosition.setFromVector3( initialSphericalCameraPosition.setFromVector3(
@@ -469,6 +464,7 @@ export function CubeScene(props: {
props.setMachinePos(currId, pos); props.setMachinePos(currId, pos);
setWorldMode("select"); setWorldMode("select");
clearHighlight("move");
} }
const rect = renderer.domElement.getBoundingClientRect(); const rect = renderer.domElement.getBoundingClientRect();
@@ -486,13 +482,13 @@ export function CubeScene(props: {
console.log("Clicked on cube:", intersects); console.log("Clicked on cube:", intersects);
const id = intersects[0].object.userData.id; const id = intersects[0].object.userData.id;
if (worldMode() === "select") toggleSelection(id); if (worldMode() === "select") props.onSelect(new Set<string>([id]));
emitMachineClick(id); // notify subscribers emitMachineClick(id); // notify subscribers
} else { } else {
emitMachineClick(null); emitMachineClick(null);
props.onSelect(new Set<string>()); // Clear selection if clicked outside cubes if (worldMode() === "select") props.onSelect(new Set<string>());
} }
}; };
@@ -646,6 +642,7 @@ export function CubeScene(props: {
}; };
const handleMenuSelect = (mode: "move") => { const handleMenuSelect = (mode: "move") => {
setWorldMode(mode); setWorldMode(mode);
setHighlightGroups({ move: new Set(menuIntersection()) });
console.log("Menu selected, new World mode", worldMode()); console.log("Menu selected, new World mode", worldMode());
}; };