ui/machineLabels: use troika for label rendering
This commit is contained in:
@@ -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));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user