Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-ng/display/src/lib/services/three-clipping-internals.spec.ts is written in an unsupported language. File is not indexed.

0001 /**
0002  * Canary tests pinning the three.js clipping internals that
0003  * `ThreeService.dropClippingShaderState()` compensates for.
0004  *
0005  * Built shader states bind clipping planes to the ARRAY INSTANCE their
0006  * ClippingContext held at build time, and the state cache is keyed by plane
0007  * COUNTS. ClippingContext.update() REPLACES its arrays whenever the parent
0008  * group chain changes (an outer clipping group toggled on or off), so a
0009  * toggle that returns to a previously-seen count shape would reuse a shader
0010  * bound to an orphaned array whose view-space values are never re-projected
0011  * again — the cut freezes to the camera. Firebird's answer is to dispose the
0012  * clipped geometry's materials on every clipping STRUCTURE change, which
0013  * releases their render objects and, through three's reference counting,
0014  * the built node states (see dropClippingShaderState in three.service.ts).
0015  *
0016  * These tests assert that three still behaves that way. If a three upgrade
0017  * makes the "replaces arrays" test fail, the upstream defect is likely fixed
0018  * — re-evaluate whether dropClippingShaderState() still needs to evict the
0019  * node states.
0020  *
0021  * The `three/src/...` import below is TEST-ONLY and deliberate: the class is
0022  * not exported from `three/webgpu`, and this spec never runs in the browser
0023  * bundle (where a second copy of three's node system would corrupt TSL
0024  * state). Do not copy this import into app code.
0025  */
0026 import ClippingContext from 'three/src/renderers/common/ClippingContext.js';
0027 import { Matrix4, Plane, Vector3 } from 'three';
0028 
0029 type AnyCtx = {
0030   cacheKey: string;
0031   intersectionPlanes: { x: number; y: number; z: number; w: number }[];
0032   unionPlanes: { x: number; y: number; z: number; w: number }[];
0033   updateGlobal(scene: unknown, camera: unknown): void;
0034   getGroupContext(group: unknown): AnyCtx;
0035 };
0036 
0037 function makeGroup(planes: Plane[], clipIntersection: boolean) {
0038   return { isClippingGroup: true, enabled: true, clipShadows: false, clipIntersection, clippingPlanes: planes };
0039 }
0040 
0041 function makeCamera() {
0042   return { matrixWorldInverse: new Matrix4() };
0043 }
0044 
0045 const SCENE = { overrideMaterial: null };
0046 
0047 describe('three ClippingContext internals (pinned)', () => {
0048   it('REPLACES plane arrays when the parent group chain changes (why the node cache must be evicted)', () => {
0049     const root = new ClippingContext() as AnyCtx;
0050     root.updateGlobal(SCENE, makeCamera());
0051 
0052     const zGroup = makeGroup([new Plane(new Vector3(0, 0, 1), 0)], false);
0053     const wedgeGroup = makeGroup(
0054       [new Plane(new Vector3(0, -1, 0), 0), new Plane(new Vector3(1, 0, 0), 0)],
0055       true,
0056     );
0057 
0058     // Angular only: wedge context parented to the root.
0059     const wedgeCtx = root.getGroupContext(wedgeGroup);
0060     const intersection = wedgeCtx.intersectionPlanes; // what a built shader captures
0061     expect(intersection.length).toBe(2);
0062 
0063     // Z group enabled: the wedge context is reparented under the Z context.
0064     // three resyncs by REPLACING both arrays — the captured `intersection`
0065     // instance is orphaned. A cached shader state keeps reading it.
0066     const zCtx = root.getGroupContext(zGroup);
0067     const wedgeCtxUnderZ = zCtx.getGroupContext(wedgeGroup);
0068     expect(wedgeCtxUnderZ).toBe(wedgeCtx); // one context per group, whatever the parent
0069     expect(wedgeCtx.intersectionPlanes).not.toBe(intersection);
0070     // If this starts failing (arrays kept stable), three fixed the defect:
0071     // re-evaluate dropClippingShaderState()'s eviction.
0072   });
0073 
0074   it('re-projects plane VALUES into the current arrays on every update call (per-view value swaps)', () => {
0075     // RenderView.clipPlane relies on this: sequential per-view renders write
0076     // different values into ONE shared plane, and every render call
0077     // re-projects them with the current camera.
0078     const root = new ClippingContext() as AnyCtx;
0079     const camera = makeCamera();
0080     root.updateGlobal(SCENE, camera);
0081 
0082     const group = makeGroup([new Plane(new Vector3(0, -1, 0), 0)], true);
0083     const ctx = root.getGroupContext(group);
0084     const held = ctx.intersectionPlanes;
0085     const before = held[0].w;
0086 
0087     camera.matrixWorldInverse.makeTranslation(0, 100, 0);
0088     root.updateGlobal(SCENE, camera);
0089     root.getGroupContext(group); // per-render update path
0090 
0091     expect(ctx.intersectionPlanes).toBe(held); // no structure change, no replacement
0092     expect(held[0].w).not.toBe(before);
0093   });
0094 
0095   it('cache key carries the context id — sibling groups with equal shapes no longer collide', () => {
0096     // Through three r185 the key was plane COUNTS only ("0:1"), so two
0097     // sibling groups with the same shape shared one built shader state bound
0098     // to ONE group's plane array, and the other group clipped with the wrong
0099     // planes. three r186 prefixes the context id ("<id>:0:1"), which gives
0100     // every group its own state. ClippedGeometrySlice keeps its distinct
0101     // (1 intersection : 0 union) shape as a guard: if the two keys below
0102     // become equal again, that distinct shape is load-bearing once more.
0103     const root = new ClippingContext() as AnyCtx;
0104     root.updateGlobal(SCENE, makeCamera());
0105 
0106     const groupA = makeGroup([new Plane(new Vector3(0, 0, 1), 0)], false);
0107     const groupB = makeGroup([new Plane(new Vector3(1, 0, 0), 500)], false);
0108     const ctxA = root.getGroupContext(groupA);
0109     const ctxB = root.getGroupContext(groupB);
0110     const countShape = (key: string) => key.split(':').slice(-2).join(':');
0111 
0112     expect(ctxA).not.toBe(ctxB);
0113     expect(countShape(ctxA.cacheKey)).toBe(countShape(ctxB.cacheKey)); // same shape "0:1"
0114     expect(ctxA.cacheKey).not.toBe(ctxB.cacheKey); // distinct keys since r186
0115 
0116     // The slice's intersection-mode single plane still has a distinct shape:
0117     const sliceLike = makeGroup([new Plane(new Vector3(0, -1, 0), 0)], true);
0118     expect(countShape(root.getGroupContext(sliceLike).cacheKey)).not.toBe(countShape(ctxA.cacheKey));
0119   });
0120 });