Warning, /firebird/packages/firebird-ng/display/src/lib/services/geometry-slice.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * ClippedGeometrySlice — a second, independently clipped copy of the detector
0003 * geometry, used by projection views (top / side / front) that need a
0004 * different cut than the main 3D view.
0005 *
0006 * Why a copy: the renderer caches an object's shader state by its clipping
0007 * plane COUNTS, and all views of one canvas share render objects. If the same
0008 * mesh had 2 wedge planes in the 3D view but 1 axis plane in a projection
0009 * view, its cached state would flip every view render — a full
0010 * dispose/rebuild of every object, every frame. Giving each clipping
0011 * STRUCTURE its own Object3D copy keeps every cached state valid forever.
0012 * Only the Object3D spine is copied; BufferGeometry, materials and textures
0013 * are shared references, so GPU memory does not duplicate.
0014 *
0015 * Why ONE slice serves all projection views: plane VALUES are re-projected
0016 * on every render call, so views that share a structure (one axis plane
0017 * each) share this slice and just write their own plane value before their
0018 * render (see RenderView.clipPlane). Per-view plane values on a shared
0019 * group are free; per-view plane counts are not.
0020 *
0021 * Layer routing (constants in @dexvis/firebird-ng/api): the original geometry
0022 * moves to GEOMETRY_MAIN_LAYER (seen by the main/3D camera), the slice spine
0023 * lives on GEOMETRY_SLICE_LAYER (seen by projection cameras). Event data
0024 * (EVENT_DATA_LAYER), helpers and lights are shared, never copied.
0025 */
0026
0027 import { Object3D, Plane, Vector3 } from 'three';
0028 import { ClippingGroup } from 'three/webgpu';
0029 import { GEOMETRY_MAIN_LAYER, GEOMETRY_SLICE_LAYER, type GeometrySlice } from '@dexvis/firebird-ng/api';
0030
0031 export class ClippedGeometrySlice implements GeometrySlice {
0032 /** The clipping group holding the spine copy. Added to the scene by ThreeService. */
0033 readonly group: ClippingGroup;
0034
0035 /**
0036 * The one clip plane, in world space. Views sharing this slice write their
0037 * own plane value here right before their render call (plane values are
0038 * re-projected per render, so sequential views each get their own cut).
0039 */
0040 readonly plane = new Plane(new Vector3(0, -1, 0), 0);
0041
0042 /** The geometry root the spine was built from (for layer restore on dispose). */
0043 private source: Object3D | null = null;
0044
0045 constructor() {
0046 this.group = new ClippingGroup();
0047 this.group.name = 'GeometrySlice';
0048 // Intersection mode with a single plane clips exactly like union mode,
0049 // but gives this group the (1 intersection : 0 union) plane-count shape —
0050 // one the main clipping chain can never produce (the wedge is always 2
0051 // planes, Z clipping is always union). Through three r185 the renderer
0052 // cached built shader states by plane counts only, so two groups with the
0053 // same shape shared one shader bound to ONE group's plane array, and the
0054 // other group silently clipped with the wrong planes. Since r186 the
0055 // cache key also carries the clipping context id; the distinct shape
0056 // stays as a guard (pinned by three-clipping-internals.spec.ts).
0057 this.group.clipIntersection = true;
0058 this.group.clippingPlanes = [this.plane];
0059 this.group.enabled = true;
0060 }
0061
0062 /**
0063 * (Re)builds the spine from the current content of `sourceGeometry` and
0064 * routes layers: originals to GEOMETRY_MAIN_LAYER, spine copies to
0065 * GEOMETRY_SLICE_LAYER. Call again after a geometry load replaces the
0066 * source content. Copies share geometry/material with the originals, so a
0067 * later visibility or material edit on originals does NOT propagate to the
0068 * spine — rebuild to resync.
0069 */
0070 rebuild(sourceGeometry: Object3D): void {
0071 this.source = sourceGeometry;
0072 this.group.clear();
0073
0074 // Mirror the source root transform (geometry loading applies a cm→mm
0075 // scale on the geometry root; the spine must sit in the same frame).
0076 this.group.position.copy(sourceGeometry.position);
0077 this.group.quaternion.copy(sourceGeometry.quaternion);
0078 this.group.scale.copy(sourceGeometry.scale);
0079
0080 for (const child of sourceGeometry.children) {
0081 this.group.add(child.clone(true));
0082 }
0083
0084 this.group.traverse(node => {
0085 if (node !== this.group) {
0086 node.layers.set(GEOMETRY_SLICE_LAYER);
0087 // Spine transforms are frozen copies; skip per-frame matrix math.
0088 node.matrixAutoUpdate = false;
0089 }
0090 });
0091 sourceGeometry.traverse(node => {
0092 if (node !== sourceGeometry) node.layers.set(GEOMETRY_MAIN_LAYER);
0093 });
0094
0095 this.group.updateMatrixWorld(true);
0096 }
0097
0098 /**
0099 * Returns the original geometry to layer 0 (single-view pages render it
0100 * with any camera again) and drops the spine. The caller removes
0101 * `group` from the scene.
0102 */
0103 dispose(): void {
0104 this.source?.traverse(node => {
0105 if (node !== this.source) node.layers.set(0);
0106 });
0107 this.source = null;
0108 this.group.clear();
0109 }
0110 }