hovered edge highlight
This commit is contained in:
parent
c05f0436e3
commit
4d09a47118
|
|
@ -1,4 +1,5 @@
|
||||||
import { useSceneHelper } from "../helpers/hooks/useSceneHelper";
|
import { useSceneHelper } from "../helpers/hooks/useSceneHelper";
|
||||||
|
import { model } from "../model/model";
|
||||||
import { state } from "../state/root";
|
import { state } from "../state/root";
|
||||||
import { ThreeView, type ThreeViewEventArgs, type ThreeViewMouseEventArgs } from "./ThreeView";
|
import { ThreeView, type ThreeViewEventArgs, type ThreeViewMouseEventArgs } from "./ThreeView";
|
||||||
|
|
||||||
|
|
@ -9,10 +10,13 @@ export const Viewport = function () {
|
||||||
function handleMouseMove(e: ThreeViewMouseEventArgs) {
|
function handleMouseMove(e: ThreeViewMouseEventArgs) {
|
||||||
state.setHitTest(e.screenPosition, e.hitResults);
|
state.setHitTest(e.screenPosition, e.hitResults);
|
||||||
|
|
||||||
sceneHelper.clearHints();
|
sceneHelper.hints.clear();
|
||||||
if (e.hitResults.hits.length) {
|
if (e.hitResults.hits.length) {
|
||||||
e.hitResults.hits.forEach((hit) => {
|
e.hitResults.hits.forEach((hit) => {
|
||||||
sceneHelper.showPointHint(hit.uuid, hit.point, 5, hit.kind === 'vertex' ? 'yellow' : (hit.kind === 'edge' ? 'lime' : 'white'));
|
sceneHelper.hints.showPoint(hit.uuid + '-point', hit.point, 5, hit.kind === 'vertex' ? 'yellow' : (hit.kind === 'edge' ? 'lime' : 'white'));
|
||||||
|
if (hit.kind === 'edge') {
|
||||||
|
sceneHelper.hints.showEdge(hit.uuid + '-edge', hit.id);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
// console.log(e.position);
|
// console.log(e.position);
|
||||||
// console.log(e.hitTest.objects.map((o) => o));
|
// console.log(e.hitTest.objects.map((o) => o));
|
||||||
|
|
@ -26,7 +30,7 @@ export const Viewport = function () {
|
||||||
|
|
||||||
sceneHelper.setSelection(hoveredFaceIds);
|
sceneHelper.setSelection(hoveredFaceIds);
|
||||||
|
|
||||||
sceneHelper.showMouseFrustumHint();
|
sceneHelper.updateMouseFrustumHint();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDispose(e: ThreeViewEventArgs): void {
|
function handleDispose(e: ThreeViewEventArgs): void {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
import type { CircularFrustum } from "./circularFrustum";
|
||||||
|
import { model } from "../model/model";
|
||||||
|
import type { Id } from "../types";
|
||||||
|
import { Line2 } from 'three/addons/lines/Line2.js';
|
||||||
|
import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
|
||||||
|
import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
|
||||||
|
|
||||||
export type ThreeHint = {
|
export type ThreeHint = {
|
||||||
mesh: THREE.Mesh,
|
object: THREE.Object3D,
|
||||||
position: THREE.Vector3Like,
|
position: THREE.Vector3Like,
|
||||||
options: ThreeHintOptions,
|
options: ThreeHintOptions,
|
||||||
}
|
}
|
||||||
|
|
@ -10,7 +16,6 @@ export type ThreeBaseHintOptions = {
|
||||||
color: THREE.ColorRepresentation,
|
color: THREE.ColorRepresentation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type ThreePointHintOptions = ThreeBaseHintOptions & {
|
export type ThreePointHintOptions = ThreeBaseHintOptions & {
|
||||||
kind: 'point',
|
kind: 'point',
|
||||||
size: number,
|
size: number,
|
||||||
|
|
@ -22,15 +27,23 @@ export type ThreeCircleHintOptions = ThreeBaseHintOptions & {
|
||||||
thickness: number,
|
thickness: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ThreeHintOptions = ThreePointHintOptions | ThreeCircleHintOptions;
|
export type ThreeLineHintOptions = ThreeBaseHintOptions & {
|
||||||
|
kind: 'line',
|
||||||
|
end: THREE.Vector3Like,
|
||||||
|
extendStart: boolean,
|
||||||
|
extendEnd: boolean,
|
||||||
|
thickness: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThreeHintOptions = ThreePointHintOptions | ThreeCircleHintOptions | ThreeLineHintOptions;
|
||||||
|
|
||||||
export class ThreeHintDisplay {
|
export class ThreeHintDisplay {
|
||||||
|
|
||||||
private scene: THREE.Scene;
|
private scene: THREE.Scene;
|
||||||
private camera: THREE.PerspectiveCamera | THREE.OrthographicCamera;
|
private camera: THREE.PerspectiveCamera | THREE.OrthographicCamera;
|
||||||
private renderer: THREE.WebGLRenderer;
|
private renderer: THREE.WebGLRenderer;
|
||||||
|
|
||||||
private readonly baseMaterial = new THREE.MeshBasicMaterial({ color: 'red' });
|
private readonly baseMeshMaterial = new THREE.MeshBasicMaterial({ color: 'red' });
|
||||||
|
private readonly baseLineMaterial = new LineMaterial({ color: 'red' });
|
||||||
|
|
||||||
private readonly hints: Record<string, ThreeHint> = {};
|
private readonly hints: Record<string, ThreeHint> = {};
|
||||||
|
|
||||||
|
|
@ -44,10 +57,12 @@ export class ThreeHintDisplay {
|
||||||
this.renderer = renderer;
|
this.renderer = renderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private createGeometry(options: ThreeHintOptions): THREE.BufferGeometry {
|
private createGeometry(options: ThreeHintOptions): THREE.BufferGeometry | LineGeometry {
|
||||||
switch (options.kind) {
|
switch (options.kind) {
|
||||||
case 'point':
|
case 'point':
|
||||||
return new THREE.SphereGeometry(1);
|
return new THREE.SphereGeometry(1);
|
||||||
|
case 'line':
|
||||||
|
return new LineGeometry().setPositions([0, 0, 0, 0, 0, 0]);
|
||||||
case 'circle':
|
case 'circle':
|
||||||
return new THREE.TorusGeometry(options.radius, options.thickness * options.radius);
|
return new THREE.TorusGeometry(options.radius, options.thickness * options.radius);
|
||||||
default:
|
default:
|
||||||
|
|
@ -57,42 +72,85 @@ export class ThreeHintDisplay {
|
||||||
|
|
||||||
private ensure(id: string, options: ThreeHintOptions): ThreeHint {
|
private ensure(id: string, options: ThreeHintOptions): ThreeHint {
|
||||||
if (!this.hints[id]) {
|
if (!this.hints[id]) {
|
||||||
const material = this.baseMaterial.clone();
|
const material = options.kind === 'line'
|
||||||
|
? this.baseLineMaterial.clone()
|
||||||
|
: this.baseMeshMaterial.clone();
|
||||||
material.color.set(options.color);
|
material.color.set(options.color);
|
||||||
|
|
||||||
this.hints[id] = {
|
this.hints[id] = {
|
||||||
mesh: new THREE.Mesh(
|
object: options.kind === 'line'
|
||||||
this.createGeometry(options),
|
? new Line2(this.createGeometry(options) as LineGeometry, material as LineMaterial)
|
||||||
material,
|
: new THREE.Mesh(this.createGeometry(options), material),
|
||||||
),
|
|
||||||
position: { x: 0, y: 0, z: 0 },
|
position: { x: 0, y: 0, z: 0 },
|
||||||
options,
|
options,
|
||||||
};
|
};
|
||||||
this.scene.add(this.hints[id].mesh);
|
this.scene.add(this.hints[id].object);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.hints[id];
|
return this.hints[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
private disposeHint(id: string) {
|
private hide(id: string) {
|
||||||
const point = this.hints[id];
|
const hint = this.hints[id];
|
||||||
if (point) {
|
if (hint) {
|
||||||
this.scene.remove(point.mesh);
|
this.scene.remove(hint.object);
|
||||||
point.mesh.geometry.dispose();
|
(hint.object as (THREE.Line | THREE.Mesh)).geometry.dispose();
|
||||||
delete (this.hints[id]);
|
delete (this.hints[id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public clear() {
|
||||||
for (const id in this.hints)
|
for (const id in this.hints)
|
||||||
this.disposeHint(id);
|
this.hide(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public set(id: string, position: THREE.Vector3Like, options: ThreeHintOptions) {
|
public show(id: string, position: THREE.Vector3Like, options: ThreeHintOptions) {
|
||||||
const object = this.ensure(id, options);
|
const object = this.ensure(id, options);
|
||||||
object.position = position;
|
object.position = position;
|
||||||
this.applyCameraToHint(object);
|
this.applyCameraToHint(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public showPoint(id: string, position: THREE.Vector3Like, size: number = 5, color: THREE.ColorRepresentation = 0xffffff) {
|
||||||
|
this.show(id, position, { kind: "point", size, color });
|
||||||
|
}
|
||||||
|
|
||||||
|
public showCircle(id: string, position: THREE.Vector3Like, radius: number, thickness: number = 0.1, color: THREE.ColorRepresentation = 0xffffff) {
|
||||||
|
this.show(id, position, { kind: "circle", radius, thickness, color });
|
||||||
|
}
|
||||||
|
|
||||||
|
public showLine(id: string, start: THREE.Vector3Like, end: THREE.Vector3Like, thickness: number = 1, color: THREE.ColorRepresentation = 0xffffff) {
|
||||||
|
this.show(id, start, { kind: "line", end, color, thickness, extendStart: true, extendEnd: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
public showEdge(id: string, edgeId: Id) {
|
||||||
|
const edge = model.edgeById(edgeId);
|
||||||
|
if (!edge?.a || !edge?.b)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const heA = model.halfEdgeById(edge.a);
|
||||||
|
const heB = model.halfEdgeById(edge.b);
|
||||||
|
if (!heA || !heB)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const vA = model.vertexById(heA.origin);
|
||||||
|
const vB = model.vertexById(heB.origin);
|
||||||
|
if (!vA || !vB)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.showLine(id, vA, vB, 1, 'red');
|
||||||
|
}
|
||||||
|
|
||||||
|
public showFrustum(id: string, frustum: CircularFrustum) {
|
||||||
|
const cameraDepth = this.camera.far - this.camera.near;
|
||||||
|
const nearDepth = this.camera.near + cameraDepth * 0.01;
|
||||||
|
const farDepth = this.camera.far - cameraDepth * 0.01;
|
||||||
|
const near = frustum.getCircleAtDepth(nearDepth);
|
||||||
|
const far = frustum.getCircleAtDepth(farDepth);
|
||||||
|
|
||||||
|
this.showCircle(id + '_near', near.center, near.radius, 0.05);
|
||||||
|
this.showCircle(id + '_far', far.center, far.radius, 0.1, 'red');
|
||||||
|
}
|
||||||
|
|
||||||
private applyCameraToHint(hint: ThreeHint) {
|
private applyCameraToHint(hint: ThreeHint) {
|
||||||
|
|
||||||
const rendererSize = new THREE.Vector2();
|
const rendererSize = new THREE.Vector2();
|
||||||
|
|
@ -110,14 +168,31 @@ export class ThreeHintDisplay {
|
||||||
else {
|
else {
|
||||||
scale = (hint.options.size * (this.camera.top - this.camera.bottom)) / rendererSize.height;
|
scale = (hint.options.size * (this.camera.top - this.camera.bottom)) / rendererSize.height;
|
||||||
}
|
}
|
||||||
hint.mesh.scale.setScalar(scale);
|
hint.object.scale.setScalar(scale);
|
||||||
break;
|
break;
|
||||||
case 'circle':
|
case 'circle':
|
||||||
hint.mesh.lookAt(this.camera.position);
|
hint.object.lookAt(this.camera.position);
|
||||||
|
break;
|
||||||
|
case 'line':
|
||||||
|
const line = hint.object as Line2;
|
||||||
|
const dir = new THREE.Vector3().subVectors(hint.position, hint.options.end);
|
||||||
|
|
||||||
|
const BIG = 1e3; // large enough to leave the frustum
|
||||||
|
|
||||||
|
const start = new THREE.Vector3().copy(hint.position);
|
||||||
|
if (hint.options.extendStart)
|
||||||
|
start.addScaledVector(dir, -BIG)
|
||||||
|
const end = new THREE.Vector3().copy(hint.options.end);
|
||||||
|
if (hint.options.extendEnd)
|
||||||
|
end.addScaledVector(dir, BIG);
|
||||||
|
|
||||||
|
line.geometry.setFromPoints([start.clone().sub(hint.position), new THREE.Vector3().copy(end).sub(hint.position)]);
|
||||||
|
(line.material as LineMaterial).linewidth = hint.options.thickness;
|
||||||
|
line.computeLineDistances(); // required for dashed material to work
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
hint.mesh.position.copy(hint.position);
|
hint.object.position.copy(hint.position);
|
||||||
}
|
}
|
||||||
|
|
||||||
public applyCamera() {
|
public applyCamera() {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
import type { ColorRepresentation, Object3D, Object3DEventMap, OrthographicCamera, PerspectiveCamera, Scene, Vector2Like, Vector3Like, WebGLRenderer } from "three";
|
import type { Object3D, OrthographicCamera, PerspectiveCamera, Scene, Vector2Like, WebGLRenderer } from "three";
|
||||||
import { SceneSync } from "../layers/sceneSync";
|
import { MeshCache } from "../layers/meshCache";
|
||||||
import { GeometryCache } from "../layers/geometryCache";
|
import { GeometryCache } from "../layers/geometryCache";
|
||||||
import type { Id } from "../types";
|
import type { Id } from "../types";
|
||||||
import { CircularFrustumIntersection, type HitResults, type HitResult } from "./circularFrustumIntersect";
|
import { CircularFrustumIntersection, type HitResults } from "./circularFrustumIntersect";
|
||||||
import { CircularFrustum } from "./circularFrustum";
|
import { CircularFrustum } from "./circularFrustum";
|
||||||
import './bvh';
|
import './bvh';
|
||||||
import { ThreeHintDisplay, type ThreeHintOptions } from "./ThreeHintDisplay";
|
import { ThreeHintDisplay } from "./ThreeHintDisplay";
|
||||||
|
import { model } from "../model/model";
|
||||||
|
import { Geometry } from "../backend/geometry/geometry";
|
||||||
|
import { meshToDto } from "../backend/dto";
|
||||||
|
|
||||||
export class SceneHelper {
|
export class SceneHelper {
|
||||||
|
|
||||||
private sync: SceneSync | undefined;
|
private meshes: MeshCache | undefined;
|
||||||
|
|
||||||
private hints: ThreeHintDisplay | undefined;
|
public _hints: ThreeHintDisplay | undefined;
|
||||||
private camera: PerspectiveCamera | OrthographicCamera | undefined;
|
private camera: PerspectiveCamera | OrthographicCamera | undefined;
|
||||||
|
|
||||||
private mouseFrustum = new CircularFrustumIntersection(new CircularFrustum());
|
private mouseFrustum = new CircularFrustumIntersection(new CircularFrustum());
|
||||||
|
|
@ -21,11 +24,26 @@ export class SceneHelper {
|
||||||
camera: PerspectiveCamera | OrthographicCamera,
|
camera: PerspectiveCamera | OrthographicCamera,
|
||||||
renderer: WebGLRenderer,
|
renderer: WebGLRenderer,
|
||||||
) {
|
) {
|
||||||
this.hints = new ThreeHintDisplay(scene, camera, renderer);
|
this._hints = new ThreeHintDisplay(scene, camera, renderer);
|
||||||
this.camera = camera;
|
this.camera = camera;
|
||||||
|
|
||||||
this.sync = new SceneSync(scene, new GeometryCache());
|
this.meshes = new MeshCache(scene, new GeometryCache());
|
||||||
this.sync.addWholeModel();
|
for (const id of Object.keys(model.solids)) {
|
||||||
|
const meshes = Geometry
|
||||||
|
.tessellateSolid(id)
|
||||||
|
.map(meshToDto);
|
||||||
|
// this.sync.addSolid(meshes[0]); // bottom
|
||||||
|
// this.sync.addSolid(meshes[3]); // front
|
||||||
|
for (const mesh of meshes)
|
||||||
|
this.meshes.addMesh(mesh.faceId, mesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hints(): ThreeHintDisplay {
|
||||||
|
if (!this._hints)
|
||||||
|
throw new Error('SceneHelper is not initiallized');
|
||||||
|
|
||||||
|
return this._hints;
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildMouseFrustum(
|
public buildMouseFrustum(
|
||||||
|
|
@ -34,7 +52,7 @@ export class SceneHelper {
|
||||||
radius: number = 5,
|
radius: number = 5,
|
||||||
): void {
|
): void {
|
||||||
if (!this.camera)
|
if (!this.camera)
|
||||||
throw new Error('Camera is not initialized');
|
throw new Error('SceneHelper is not initiallized');
|
||||||
|
|
||||||
this.mouseFrustum.frustum.setFromScreenPoint(
|
this.mouseFrustum.frustum.setFromScreenPoint(
|
||||||
mouseNormalized,
|
mouseNormalized,
|
||||||
|
|
@ -56,52 +74,32 @@ export class SceneHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public setSelection(faceIds: Id[]) {
|
public setSelection(faceIds: Id[]) {
|
||||||
this.sync?.setSelected(faceIds);
|
if (!this.meshes)
|
||||||
|
throw new Error('SceneHelper is not initiallized');
|
||||||
|
|
||||||
|
this.meshes.selection = faceIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get objects(): Object3D<Object3DEventMap>[] {
|
public get objects(): Object3D[] {
|
||||||
|
if (!this.meshes)
|
||||||
|
throw new Error('SceneHelper is not initiallized');
|
||||||
|
|
||||||
return this.sync?.meshes ?? [];
|
return this.meshes.meshes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public showHint(id: string, position: Vector3Like, options: ThreeHintOptions) {
|
public updateMouseFrustumHint() {
|
||||||
this.hints?.set(id, position, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public showPointHint(id: string, position: Vector3Like, size: number = 5, color: ColorRepresentation = 0xffffff) {
|
|
||||||
this.hints?.set(id, position, { kind: "point", size, color });
|
|
||||||
}
|
|
||||||
|
|
||||||
public showCircleHint(id: string, position: Vector3Like, radius: number, thickness: number = 0.1, color: ColorRepresentation = 0xffffff) {
|
|
||||||
this.hints?.set(id, position, { kind: "circle", radius, thickness, color });
|
|
||||||
}
|
|
||||||
|
|
||||||
public showMouseFrustumHint() {
|
|
||||||
if (!this.camera)
|
if (!this.camera)
|
||||||
throw new Error('Camera is not initialized');
|
throw new Error('SceneHelper is not initiallized');
|
||||||
|
|
||||||
const frustum = this.mouseFrustum.frustum;
|
this.hints.showFrustum('mouse', this.mouseFrustum.frustum);
|
||||||
const cameraDepth = this.camera.far - this.camera.near;
|
|
||||||
const nearDepth = this.camera.near + cameraDepth * 0.01;
|
|
||||||
const farDepth = this.camera.far - cameraDepth * 0.01;
|
|
||||||
const near = frustum.getCircleAtDepth(nearDepth);
|
|
||||||
const far = frustum.getCircleAtDepth(farDepth);
|
|
||||||
|
|
||||||
this.showCircleHint('hittest_near', near.center, near.radius, 0.05);
|
|
||||||
this.showCircleHint('hittest_far', far.center, far.radius, 0.1, 'red');
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearHints() {
|
|
||||||
this.hints?.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public dispose() {
|
||||||
this.sync?.dispose();
|
this.meshes?.dispose();
|
||||||
|
this.hints.clear();
|
||||||
this.clearHints();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public applyCamera() {
|
public applyCamera() {
|
||||||
this.hints?.applyCamera();
|
this._hints?.applyCamera();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,47 @@
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import type { MeshDto } from '../backend/dto';
|
import type { MeshDto } from '../backend/dto';
|
||||||
import type { Id } from '../types';
|
|
||||||
|
|
||||||
export class GeometryCache {
|
export class GeometryCache {
|
||||||
private readonly _cache = new Map<string, THREE.BufferGeometry>();
|
private readonly items = new Map<string, THREE.BufferGeometry>();
|
||||||
|
|
||||||
private key(solidId: Id, lod: number) {
|
private key(id: string, lod: number) {
|
||||||
return `${solidId}:${lod}`;
|
return `${id}:${lod}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public has(solidId: Id, lod: number): boolean {
|
public has(id: string, lod: number): boolean {
|
||||||
return this._cache.has(this.key(solidId, lod));
|
return this.items.has(this.key(id, lod));
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(solidId: Id, lod: number): THREE.BufferGeometry | undefined {
|
public get(id: string, lod: number): THREE.BufferGeometry | undefined {
|
||||||
return this._cache.get(this.key(solidId, lod));
|
return this.items.get(this.key(id, lod));
|
||||||
}
|
}
|
||||||
|
|
||||||
public set(solidId: Id, lod: number, payload: THREE.BufferGeometry) {
|
public set(id: string, lod: number, payload: THREE.BufferGeometry) {
|
||||||
this._cache.set(this.key(solidId, lod), payload);
|
this.items.set(this.key(id, lod), payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getOrCreate(solidId: Id, lod: number, dto: MeshDto): THREE.BufferGeometry {
|
public getOrCreate(id: string, lod: number, dto: MeshDto): THREE.BufferGeometry {
|
||||||
let geometry = this.get(solidId, lod);
|
let geometry = this.get(id, lod);
|
||||||
if (geometry)
|
if (geometry)
|
||||||
return geometry;
|
return geometry;
|
||||||
|
|
||||||
geometry = new THREE.BufferGeometry();
|
geometry = new THREE.BufferGeometry();
|
||||||
geometry.userData.solidId = solidId;
|
geometry.userData.id = id;
|
||||||
// geometry.userData.faceIds = dto.faceIds;
|
|
||||||
|
|
||||||
geometry.setAttribute('position', new THREE.BufferAttribute(dto.vertices, 3));
|
geometry.setAttribute('position', new THREE.BufferAttribute(dto.vertices, 3));
|
||||||
geometry.setAttribute('normal', new THREE.BufferAttribute(dto.normals, 3));
|
geometry.setAttribute('normal', new THREE.BufferAttribute(dto.normals, 3));
|
||||||
geometry.setIndex(new THREE.BufferAttribute(dto.indices, 1));
|
geometry.setIndex(new THREE.BufferAttribute(dto.indices, 1));
|
||||||
|
|
||||||
this.set(solidId, lod, geometry);
|
this.set(id, lod, geometry);
|
||||||
|
|
||||||
return geometry;
|
return geometry;
|
||||||
}
|
}
|
||||||
|
|
||||||
unset(solidId: Id, lod: number) {
|
unset(id: string, lod: number) {
|
||||||
const geometry = this.get(solidId, lod);
|
const geometry = this.get(id, lod);
|
||||||
if (geometry) {
|
if (geometry) {
|
||||||
geometry.dispose();
|
geometry.dispose();
|
||||||
this._cache.delete(this.key(solidId, lod));
|
this.items.delete(this.key(id, lod));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,29 +1,28 @@
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import type { GeometryCache } from './geometryCache';
|
import type { GeometryCache } from './geometryCache';
|
||||||
import { meshToDto, type MeshDto } from '../backend/dto/mesh';
|
import { type MeshDto } from '../backend/dto/mesh';
|
||||||
import { model } from '../model/model';
|
|
||||||
import { Geometry } from '../backend/geometry/geometry';
|
|
||||||
import type { Id } from '../types';
|
import type { Id } from '../types';
|
||||||
|
|
||||||
export class SceneSync {
|
export class MeshCache {
|
||||||
private scene: THREE.Scene;
|
private scene: THREE.Scene;
|
||||||
private meshByFace: Record<Id, THREE.Mesh> = {}; // faceId → THREE.Mesh
|
|
||||||
private cache: GeometryCache;
|
private cache: GeometryCache;
|
||||||
|
|
||||||
private _selectedFaceIds: Id[] = [];
|
private selectedIds: Id[] = [];
|
||||||
|
|
||||||
|
public readonly items: Record<string, THREE.Mesh> = {}; // faceId → THREE.Mesh
|
||||||
|
|
||||||
private readonly baseMaterial = new THREE.ShaderMaterial({
|
private readonly baseMaterial = new THREE.ShaderMaterial({
|
||||||
side: THREE.DoubleSide,
|
side: THREE.DoubleSide,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
wireframe: true,
|
wireframe: false,
|
||||||
uniforms: {
|
uniforms: {
|
||||||
frontColor: { value: new THREE.Color(0x88ccff) },
|
frontColor: { value: new THREE.Color(0x88ccff) },
|
||||||
frontOpacity: { value: 0.6 },
|
frontOpacity: { value: 0.9 },
|
||||||
backColor: { value: new THREE.Color(0xcc88ff) },
|
backColor: { value: new THREE.Color(0xcc88ff) },
|
||||||
backOpacity: { value: 0.2 },
|
backOpacity: { value: 0.2 },
|
||||||
selected: { value: 0 },
|
selected: { value: 0 },
|
||||||
selectedOpacity: { value: 0.8 },
|
selectedOpacity: { value: 1 },
|
||||||
},
|
},
|
||||||
vertexShader: `
|
vertexShader: `
|
||||||
varying vec3 vNormal;
|
varying vec3 vNormal;
|
||||||
|
|
@ -57,59 +56,47 @@ export class SceneSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
public get selectedFaceIds() {
|
public get selectedFaceIds() {
|
||||||
return this._selectedFaceIds;
|
return this.selectedIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get ids(): string[] {
|
||||||
|
return Object.keys(this.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get meshes(): THREE.Mesh[] {
|
public get meshes(): THREE.Mesh[] {
|
||||||
return Object.values(this.meshByFace);
|
return Object.values(this.items);
|
||||||
}
|
|
||||||
|
|
||||||
public get items(): Record<Id, THREE.Mesh> {
|
|
||||||
return this.meshByFace;
|
|
||||||
}
|
|
||||||
|
|
||||||
addWholeModel() {
|
|
||||||
for (const id of Object.keys(model.solids)) {
|
|
||||||
const meshes = Geometry
|
|
||||||
.tessellateSolid(id)
|
|
||||||
.map(meshToDto);
|
|
||||||
this.addSolid(meshes[0]); // bottom
|
|
||||||
this.addSolid(meshes[3]); // front
|
|
||||||
// for (const mesh of meshes)
|
|
||||||
// this.addSolid(mesh);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when FE scene graph syncs from BE
|
// Called when FE scene graph syncs from BE
|
||||||
addSolid(dto: MeshDto) {
|
addMesh(id: string, dto: MeshDto) {
|
||||||
const faceId = dto.faceId;
|
if (this.items[id])
|
||||||
|
|
||||||
if (this.meshByFace[faceId])
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const geo = this.cache.getOrCreate(faceId, 0, dto);
|
const geo = this.cache.getOrCreate(id, 0, dto);
|
||||||
|
|
||||||
const geoVCount = geo.attributes.position.count;
|
const geoVCount = geo.attributes.position.count;
|
||||||
geo.setAttribute('selected', new THREE.BufferAttribute(new Float32Array(geoVCount), 1));
|
geo.setAttribute('selected', new THREE.BufferAttribute(new Float32Array(geoVCount), 1));
|
||||||
|
|
||||||
const mesh = new THREE.Mesh(geo, this.baseMaterial);
|
const mesh = new THREE.Mesh(geo, this.baseMaterial);
|
||||||
mesh.userData = dto
|
mesh.userData = dto
|
||||||
mesh.userData.vertexIds = dto.loop.map((v) => v.vertex);
|
mesh.userData.vertexIds = dto.loop.map((v) => v.vertex);
|
||||||
|
|
||||||
this.scene.add(mesh);
|
this.scene.add(mesh);
|
||||||
this.meshByFace[faceId] = mesh;
|
this.items[id] = mesh;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelected(faceIds: Id[]) {
|
public set selection(ids: string[]) {
|
||||||
this._selectedFaceIds = faceIds;
|
this.selectedIds = ids;
|
||||||
|
|
||||||
for (const [sid, mesh] of Object.entries(this.meshByFace)) {
|
for (const [id, mesh] of Object.entries(this.items)) {
|
||||||
const attr = mesh.geometry.attributes.selected;
|
const attr = mesh.geometry.attributes.selected;
|
||||||
attr.array.fill(faceIds.includes(sid) ? 1.0 : 0.0);
|
attr.array.fill(ids.includes(id) ? 1.0 : 0.0);
|
||||||
attr.needsUpdate = true; // required!
|
attr.needsUpdate = true; // required!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public disposeMesh(faceId: Id) {
|
public disposeItem(id: string) {
|
||||||
const mesh = this.meshByFace[faceId];
|
const mesh = this.items[id];
|
||||||
if (!mesh)
|
if (!mesh)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -124,13 +111,13 @@ export class SceneSync {
|
||||||
else
|
else
|
||||||
mesh.material.dispose();
|
mesh.material.dispose();
|
||||||
|
|
||||||
this.cache.unset(faceId, 0);
|
this.cache.unset((mesh.userData as MeshDto).faceId, 0);
|
||||||
delete (this.meshByFace[faceId]);
|
delete (this.items[id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public dispose() {
|
||||||
for (const faceId of Object.keys(this.meshByFace))
|
for (const id of this.ids)
|
||||||
this.disposeMesh(faceId);
|
this.disposeItem(id);
|
||||||
|
|
||||||
this.baseMaterial.dispose();
|
this.baseMaterial.dispose();
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue