abstract-3d 4.0.0-beta.2 → 4.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +2 -2
  2. package/lib/abstract-3d.d.ts +22 -1
  3. package/lib/abstract-3d.d.ts.map +1 -1
  4. package/lib/abstract-3d.js +4 -2
  5. package/lib/abstract-3d.js.map +1 -1
  6. package/lib/renderers/react/react.js +3 -3
  7. package/lib/renderers/react/react.js.map +1 -1
  8. package/lib/renderers/three/appearance/appearance.d.ts +5 -0
  9. package/lib/renderers/three/appearance/appearance.d.ts.map +1 -1
  10. package/lib/renderers/three/appearance/appearance.js +15 -5
  11. package/lib/renderers/three/appearance/appearance.js.map +1 -1
  12. package/lib/renderers/three/build/build.js +4 -1
  13. package/lib/renderers/three/build/build.js.map +1 -1
  14. package/lib/renderers/three/build/materials.d.ts.map +1 -1
  15. package/lib/renderers/three/build/materials.js +74 -2
  16. package/lib/renderers/three/build/materials.js.map +1 -1
  17. package/lib/renderers/three/camera/gizmo.d.ts.map +1 -1
  18. package/lib/renderers/three/camera/gizmo.js +2 -1
  19. package/lib/renderers/three/camera/gizmo.js.map +1 -1
  20. package/lib/renderers/three/environment/studio.d.ts +8 -0
  21. package/lib/renderers/three/environment/studio.d.ts.map +1 -0
  22. package/lib/renderers/three/environment/studio.js +14 -0
  23. package/lib/renderers/three/environment/studio.js.map +1 -0
  24. package/lib/renderers/three/geometry/geometry.d.ts.map +1 -1
  25. package/lib/renderers/three/geometry/geometry.js +5 -2
  26. package/lib/renderers/three/geometry/geometry.js.map +1 -1
  27. package/lib/renderers/three/quality/quality.d.ts +3 -2
  28. package/lib/renderers/three/quality/quality.d.ts.map +1 -1
  29. package/lib/renderers/three/quality/quality.js +4 -3
  30. package/lib/renderers/three/quality/quality.js.map +1 -1
  31. package/lib/renderers/three/shadow/key-light.d.ts +6 -0
  32. package/lib/renderers/three/shadow/key-light.d.ts.map +1 -0
  33. package/lib/renderers/three/shadow/key-light.js +37 -0
  34. package/lib/renderers/three/shadow/key-light.js.map +1 -0
  35. package/lib/renderers/three/shadow/shadow.d.ts +7 -1
  36. package/lib/renderers/three/shadow/shadow.d.ts.map +1 -1
  37. package/lib/renderers/three/shadow/shadow.js +63 -24
  38. package/lib/renderers/three/shadow/shadow.js.map +1 -1
  39. package/lib/renderers/three/viewer/options.d.ts +7 -6
  40. package/lib/renderers/three/viewer/options.d.ts.map +1 -1
  41. package/lib/renderers/three/viewer/options.js +1 -1
  42. package/lib/renderers/three/viewer/options.js.map +1 -1
  43. package/lib/renderers/three/viewer/viewer.d.ts +7 -1
  44. package/lib/renderers/three/viewer/viewer.d.ts.map +1 -1
  45. package/lib/renderers/three/viewer/viewer.js +69 -65
  46. package/lib/renderers/three/viewer/viewer.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/abstract-3d.ts +32 -2
  49. package/src/renderers/ifc/ifc.test.ts +7 -5
  50. package/src/renderers/react/react.tsx +3 -3
  51. package/src/renderers/three/appearance/appearance.test.ts +36 -4
  52. package/src/renderers/three/appearance/appearance.ts +21 -6
  53. package/src/renderers/three/build/build.test.ts +18 -10
  54. package/src/renderers/three/build/build.ts +4 -1
  55. package/src/renderers/three/build/materials.ts +93 -2
  56. package/src/renderers/three/camera/gizmo.ts +2 -1
  57. package/src/renderers/three/compile/compile.test.ts +32 -19
  58. package/src/renderers/three/environment/studio.ts +17 -0
  59. package/src/renderers/three/geometry/geometry.test.ts +15 -1
  60. package/src/renderers/three/geometry/geometry.ts +5 -2
  61. package/src/renderers/three/quality/quality.test.ts +2 -2
  62. package/src/renderers/three/quality/quality.ts +4 -3
  63. package/src/renderers/three/shadow/key-light.ts +41 -0
  64. package/src/renderers/three/shadow/shadow.ts +79 -25
  65. package/src/renderers/three/viewer/options.ts +8 -7
  66. package/src/renderers/three/viewer/viewer.ts +75 -74
@@ -1,5 +1,5 @@
1
1
  import { BatchedMesh, InstancedMesh, Matrix4, PerspectiveCamera, Raycaster, Vector2, Vector3, Vector4 } from "three";
2
- import { describe, expect, it } from "vitest";
2
+ import { assert, describe, expect, it } from "vitest";
3
3
  import type { Group, Scene } from "../../../abstract-3d.js";
4
4
  import { alignedDimension, box, culledLine, group, sphere, text, vec3, vec3Zero } from "../../../abstract-3d.js";
5
5
  import type { PaintState, StructureState } from "../appearance/appearance.js";
@@ -28,11 +28,18 @@ const rasterize = (): GlyphBitmap => {
28
28
  const idle: StructureState = { relationsActive: false };
29
29
  const scene = (groups: ReadonlyArray<Group>, rest: Partial<Scene> = {}): Scene => ({ size_deprecated: vec3(1, 1, 1), groups, ...rest });
30
30
  const part = (id: string, x: number, material = red): Group => group([box(vec3(10, 10, 10), material)], vec3(x, 0, 0), vec3Zero, [], { id });
31
+ // Destructured, as the build's and lint's compiler options disagree on reading an index signature by dot or bracket
32
+ const groupId = (g: Group): string => {
33
+ const { id = "" }: Record<string, string | undefined> = g.data ?? {};
34
+ return id;
35
+ };
31
36
  const build = (s: Scene, structure = idle, activeRelations?: ReadonlyArray<string>): BuiltScene =>
32
- buildScene(
33
- compileScene(s, (g) => g.data?.id ?? ""),
34
- { structure, activeRelations, geometries: new Map(), atlas: createGlyphAtlas(rasterize, 256) }
35
- );
37
+ buildScene(compileScene(s, groupId), { structure, activeRelations, geometries: new Map(), atlas: createGlyphAtlas(rasterize, 256) });
38
+ const firstBatch = (built: BuiltScene): BatchedMesh => {
39
+ const [batch] = batches(built);
40
+ assert(batch);
41
+ return batch;
42
+ };
36
43
  const relation = (id: string): NonNullable<Scene["relations"]>[number] => ({ id, pos: vec3Zero, groups: [group([box(vec3(1, 1, 1), red)])] });
37
44
  const batches = (built: BuiltScene): ReadonlyArray<BatchedMesh> => built.root.children.filter((o): o is BatchedMesh => o instanceof BatchedMesh);
38
45
 
@@ -45,9 +52,10 @@ function pickId(built: BuiltScene, x: number): string | undefined {
45
52
  const raycaster = new Raycaster();
46
53
  raycaster.setFromCamera(new Vector2(0, 0), camera);
47
54
  const hits = raycaster.intersectObjects(getPickables(built), false);
48
- const item = hits.length > 0 ? getPickedItem(built, hits[0]) : undefined;
55
+ const hit = hits.at(0);
56
+ const item = hit && getPickedItem(built, hit);
49
57
  const { compiled }: { readonly compiled: CompiledScene } = built;
50
- const target = item === undefined ? undefined : compiled.targets[compiled.items[item].target];
58
+ const target = item === undefined ? undefined : compiled.targets[compiled.items.at(item)?.target ?? -1];
51
59
  return target?.kind === "group" || target?.kind === "dimension" ? target.id : undefined;
52
60
  }
53
61
 
@@ -71,7 +79,7 @@ describe("buildScene", () => {
71
79
 
72
80
  it("repaints the hovered and selected targets", () => {
73
81
  const built = build(scene([part("a", 0), part("b", 20)]));
74
- const [batch] = batches(built);
82
+ const batch = firstBatch(built);
75
83
  const color = (instance: number): ReadonlyArray<number> =>
76
84
  batch
77
85
  .getColorAt(instance, new Vector4())
@@ -117,7 +125,7 @@ describe("buildScene", () => {
117
125
  const built = build(scene([group([box(vec3(1, 1, 1), red)], vec3(5, 0, 0), vec3Zero, [], {}, spin)]));
118
126
  expect(built.animatedItems).toHaveLength(1);
119
127
  animateScene(built, 0.5);
120
- const position = new Vector3(1, 0, 0).applyMatrix4(batches(built)[0].getMatrixAt(0, new Matrix4()));
128
+ const position = new Vector3(1, 0, 0).applyMatrix4(firstBatch(built).getMatrixAt(0, new Matrix4()));
121
129
  expect(position.toArray().map((v) => Math.round(v * 1000) / 1000 + 0)).toEqual([5, 1, 0]);
122
130
  });
123
131
 
@@ -134,7 +142,7 @@ describe("buildScene", () => {
134
142
  it("disposes its batches and materials", () => {
135
143
  const built = build(scene([part("a", 0)]));
136
144
  const disposed: Array<string> = [];
137
- batches(built)[0].material.addEventListener("dispose", () => disposed.push("material"));
145
+ firstBatch(built).material.addEventListener("dispose", () => disposed.push("material"));
138
146
  disposeScene(built);
139
147
  expect(disposed).toEqual(["material"]);
140
148
  });
@@ -232,7 +232,10 @@ function createPart(
232
232
  return { geometry, local: shape.local };
233
233
  });
234
234
  if (first.role === "solid") {
235
- return createSolidPart(entries, createClassMaterial(getMaterialClass(first, targetStates[first.target]!)));
235
+ const c = getMaterialClass(first, targetStates[first.target]!);
236
+ const part = createSolidPart(entries, createClassMaterial(c));
237
+ Object.assign(part.object, { castShadow: c.castShadow, receiveShadow: c.lit });
238
+ return part;
236
239
  }
237
240
  // Images are hidden until their texture loads
238
241
  const part = createImagePart(
@@ -8,12 +8,14 @@ import {
8
8
  LinearMipmapLinearFilter,
9
9
  type Material,
10
10
  MeshBasicMaterial,
11
+ MeshPhysicalMaterial,
12
+ type MeshPhysicalMaterialParameters,
11
13
  MeshStandardMaterial,
12
14
  SRGBColorSpace,
13
15
  type Texture,
14
16
  TextureLoader,
15
17
  } from "three";
16
- import type { Image } from "../../../abstract-3d.js";
18
+ import type { Image, MaterialType } from "../../../abstract-3d.js";
17
19
  import type { MaterialClass, MaterialState } from "../appearance/appearance.js";
18
20
  import { ERROR_IMG_KEY } from "../appearance/appearance.js";
19
21
 
@@ -22,7 +24,96 @@ import { ERROR_IMG_KEY } from "../appearance/appearance.js";
22
24
  export function createClassMaterial(c: MaterialClass): Material {
23
25
  const side = c.side === "front" ? FrontSide : c.side === "back" ? BackSide : DoubleSide;
24
26
  const common = { side, transparent: c.transparent, depthWrite: !c.transparent };
25
- return c.lit ? new MeshStandardMaterial({ ...common, metalness: c.metalness, roughness: c.roughness }) : new MeshBasicMaterial(common);
27
+ if (!c.lit) {
28
+ return new MeshBasicMaterial(common);
29
+ }
30
+ const material = c.type
31
+ ? new MeshPhysicalMaterial({ ...common, ...finishes[c.type] })
32
+ : new MeshStandardMaterial({ ...common, metalness: c.metalness, roughness: c.roughness });
33
+ return Object.assign(material, { onBeforeCompile: roundEdges });
34
+ }
35
+
36
+ /**
37
+ * The physical finish of each type of surface, in the material's color: metals reflect their color, brighter than physical, as product renders light
38
+ * metals, since the studio around them is dark; the others are dielectrics with a white reflection; painted sheet has a light clearcoat, fabric a
39
+ * soft sheen. Glass is a thin, smooth, mostly see-through surface, not refracting, which would render the scene once more each frame
40
+ */
41
+ const finishes: Record<MaterialType, MeshPhysicalMaterialParameters> = {
42
+ painted: { metalness: 0, roughness: 0.45, clearcoat: 0.3, clearcoatRoughness: 0.25 },
43
+ galvanized: { metalness: 0.85, roughness: 0.55, envMapIntensity: 1.6 },
44
+ aluminum: { metalness: 0.9, roughness: 0.4, envMapIntensity: 1.6 },
45
+ stainless: { metalness: 1, roughness: 0.25, envMapIntensity: 1.5 },
46
+ copper: { metalness: 1, roughness: 0.3, envMapIntensity: 1.3 },
47
+ plastic: { metalness: 0, roughness: 0.55 },
48
+ rubber: { metalness: 0, roughness: 0.9 },
49
+ fabric: { metalness: 0, roughness: 1, sheen: 1, sheenRoughness: 0.8, sheenColor: 0x888888 },
50
+ glass: { metalness: 0, roughness: 0.05, specularIntensity: 1 },
51
+ };
52
+
53
+ /**
54
+ * Shades the edges of boxes with a radius as rounded by it, so they catch light and reflections: the normal of a rounded box of the same size, from
55
+ * the signed distance function of a rounded box (Inigo Quilez's sdRoundBox), is used at each point of the sharp one. No extra geometry, and the
56
+ * radius stays the same along every edge, as boxes are one unit cube scaled to their size. Only the outline stays sharp
57
+ */
58
+ function roundEdges(shader: { vertexShader: string; fragmentShader: string }): void {
59
+ shader.vertexShader = shader.vertexShader
60
+ .replace(
61
+ "#include <common>",
62
+ `#include <common>
63
+ attribute float bevel;
64
+ varying float vBevel;
65
+ varying vec3 vBoxPoint;
66
+ varying vec3 vBoxHalf;
67
+ varying mat3 vBoxRotation;`
68
+ )
69
+ .replace(
70
+ "#include <batching_vertex>",
71
+ `#include <batching_vertex>
72
+ #ifdef USE_BATCHING
73
+ vec3 boxSize = vec3( length( batchingMatrix[ 0 ].xyz ), length( batchingMatrix[ 1 ].xyz ), length( batchingMatrix[ 2 ].xyz ) );
74
+ mat3 boxRotation = mat3( batchingMatrix[ 0 ].xyz / boxSize.x, batchingMatrix[ 1 ].xyz / boxSize.y, batchingMatrix[ 2 ].xyz / boxSize.z );
75
+ #else
76
+ vec3 boxSize = vec3( 1.0 );
77
+ mat3 boxRotation = mat3( 1.0 );
78
+ #endif
79
+ // From the box's own axes to view space, where the fragment shader's normal is
80
+ vBoxRotation = normalMatrix * boxRotation;
81
+ vBevel = bevel;
82
+ vBoxHalf = boxSize * 0.5;
83
+ vBoxPoint = position * boxSize;`
84
+ );
85
+ shader.fragmentShader = shader.fragmentShader
86
+ .replace(
87
+ "#include <common>",
88
+ `#include <common>
89
+ varying float vBevel;
90
+ varying vec3 vBoxPoint;
91
+ varying vec3 vBoxHalf;
92
+ varying mat3 vBoxRotation;`
93
+ )
94
+ .replace(
95
+ "#include <normal_fragment_maps>",
96
+ `#include <normal_fragment_maps>
97
+ if ( vBevel > 0.0 ) {
98
+ // At least 2 pixels wide, so a rounded edge far away is a soft line instead of a jagged sub-pixel highlight, and at most half the box
99
+ vec3 pixel = fwidth( vBoxPoint );
100
+ float radius = min( max( vBevel, 2.0 * max( pixel.x, max( pixel.y, pixel.z ) ) ), min( vBoxHalf.x, min( vBoxHalf.y, vBoxHalf.z ) ) );
101
+ vec3 rounded = sign( vBoxPoint ) * max( abs( vBoxPoint ) - ( vBoxHalf - radius ), 0.0 );
102
+ if ( dot( rounded, rounded ) > 0.0 ) {
103
+ normal = normalize( vBoxRotation * normalize( rounded ) ) * faceDirection;
104
+ }
105
+ }`
106
+ )
107
+ .replace(
108
+ "#include <clearcoat_normal_fragment_maps>",
109
+ `#include <clearcoat_normal_fragment_maps>
110
+ #ifdef USE_CLEARCOAT
111
+ // A painted finish's clearcoat follows the rounded edge too
112
+ if ( vBevel > 0.0 ) {
113
+ clearcoatNormal = normal;
114
+ }
115
+ #endif`
116
+ );
26
117
  }
27
118
 
28
119
  export function createImageMaterial(backfaceCulling: boolean | undefined, useAlphaTest: boolean): MeshBasicMaterial {
@@ -59,7 +59,8 @@ export function createGizmo(
59
59
  size,
60
60
  placement: helper.props?.alignment ?? "bottom-right",
61
61
  offset: { left: mx - size / 2, right: mx - size / 2, top: my - size / 2, bottom: my - size / 2 },
62
- font: { family, weight },
62
+ // A family without a generic fallback, e.g. a web font, would otherwise fall back to the browser's serif
63
+ font: { family: `${family}, sans-serif`, weight },
63
64
  ...(helper.type === "Viewcube" ? getViewcubeOptions(helper.viewcubeProps ?? {}) : getAxesOptions(helper.viewportProps ?? {})),
64
65
  });
65
66
  gizmo.enabled = !(helper.type === "Viewport" && helper.viewportProps?.disabled);
@@ -1,43 +1,56 @@
1
1
  import { Matrix4, Vector3 } from "three";
2
- import { describe, expect, it } from "vitest";
2
+ import { assert, describe, expect, it } from "vitest";
3
3
  import type { Group, Scene } from "../../../abstract-3d.js";
4
4
  import { alignedDimension, box, group, line, sphere, text, vec3, vec3Zero } from "../../../abstract-3d.js";
5
+ import type { CompiledScene, Item } from "./compile.js";
5
6
  import { compileScene, getItemMatrix } from "./compile.js";
6
7
 
7
8
  const red = { normal: "rgb(255, 0, 0)" };
8
9
  const scene = (groups: ReadonlyArray<Group>, rest: Partial<Scene> = {}): Scene => ({ size_deprecated: vec3(1, 1, 1), groups, ...rest });
9
- const apply = (m: Matrix4, x: number, y: number, z: number): ReadonlyArray<number> =>
10
- new Vector3(x, y, z)
10
+ const apply = (m: Matrix4 | undefined, x: number, y: number, z: number): ReadonlyArray<number> => {
11
+ assert(m);
12
+ return new Vector3(x, y, z)
11
13
  .applyMatrix4(m)
12
14
  .toArray()
13
15
  .map((v) => Math.round(v * 1000) / 1000 + 0);
16
+ };
17
+ const getItem = (compiled: CompiledScene, index: number): Item => {
18
+ const item = compiled.items.at(index);
19
+ assert(item);
20
+ return item;
21
+ };
22
+ // Destructured, as the build's and lint's compiler options disagree on reading an index signature by dot or bracket
23
+ const groupId = (g: Group): string => {
24
+ const { id = "" }: Record<string, string | undefined> = g.data ?? {};
25
+ return id;
26
+ };
14
27
 
15
28
  describe("compileScene", () => {
16
29
  it("flattens nested groups into items with world matrices", () => {
17
30
  const inner = group([sphere(1, red)], vec3(0, 10, 0));
18
31
  const compiled = compileScene(scene([group([box(vec3(1, 1, 1), red)], vec3(100, 0, 0), vec3Zero, [inner])]));
19
32
  expect(compiled.items.map((i) => i.role)).toEqual(["solid", "solid"]);
20
- expect(apply(compiled.items[0].matrix, 0, 0, 0)).toEqual([100, 0, 0]);
21
- expect(apply(compiled.items[1].matrix, 0, 0, 0)).toEqual([100, 10, 0]);
33
+ expect(apply(compiled.items.at(0)?.matrix, 0, 0, 0)).toEqual([100, 0, 0]);
34
+ expect(apply(compiled.items.at(1)?.matrix, 0, 0, 0)).toEqual([100, 10, 0]);
22
35
  });
23
36
 
24
37
  it("applies group rotation before the group's position", () => {
25
38
  const compiled = compileScene(scene([group([box(vec3(1, 1, 1), red)], vec3(5, 0, 0), vec3(0, 0, Math.PI / 2))]));
26
- expect(apply(compiled.items[0].matrix, 1, 0, 0)).toEqual([5, 1, 0]);
39
+ expect(apply(compiled.items.at(0)?.matrix, 1, 0, 0)).toEqual([5, 1, 0]);
27
40
  });
28
41
 
29
42
  it("rotates the scene by its deprecated rotation, then moves it by minus its deprecated center", () => {
30
43
  const g = group([box(vec3(1, 1, 1), red)], vec3(10, 0, 0));
31
44
  const centered = compileScene(scene([g], { center_deprecated: vec3(10, 0, 0) }));
32
- expect(apply(centered.items[0].matrix, 0, 0, 0)).toEqual([0, 0, 0]);
45
+ expect(apply(centered.items.at(0)?.matrix, 0, 0, 0)).toEqual([0, 0, 0]);
33
46
  const rotated = compileScene(scene([g], { center_deprecated: vec3(-10, 0, 0), rotation_deprecated: vec3(0, Math.PI, 0) }));
34
- expect(apply(rotated.items[0].matrix, 1, 0, 0)).toEqual([-1, 0, 0]);
47
+ expect(apply(rotated.items.at(0)?.matrix, 1, 0, 0)).toEqual([-1, 0, 0]);
35
48
  });
36
49
 
37
50
  it("gives every item of a top-level group that group's target, and the data of the group it's in", () => {
38
51
  const inner = group([sphere(1, red)], vec3Zero, vec3Zero, [], { part: "motor" });
39
52
  const fan = group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [inner], { id: "fan" });
40
- const compiled = compileScene(scene([fan, group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [], { id: "coil" })]), (g) => g.data?.id ?? "");
53
+ const compiled = compileScene(scene([fan, group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [], { id: "coil" })]), groupId);
41
54
  expect(compiled.targets).toEqual([
42
55
  { kind: "group", id: "fan", root: 0, rootData: { id: "fan" } },
43
56
  { kind: "group", id: "coil", root: 1, rootData: { id: "coil" } },
@@ -70,9 +83,9 @@ describe("compileScene", () => {
70
83
  );
71
84
  const compiled = compileScene(scene([group([], vec3(10, 0, 0), vec3Zero, [fan])]));
72
85
  expect(compiled.animations).toHaveLength(1);
73
- expect(compiled.animations[0].speed).toEqual(vec3(0, 0, Math.PI));
74
- expect(compiled.items[0].animation).toBe(0);
75
- const world = (seconds: number): ReadonlyArray<number> => apply(getItemMatrix(compiled, compiled.items[0], seconds, new Matrix4()), 1, 0, 0);
86
+ expect(compiled.animations.at(0)?.speed).toEqual(vec3(0, 0, Math.PI));
87
+ expect(compiled.items.at(0)?.animation).toBe(0);
88
+ const world = (seconds: number): ReadonlyArray<number> => apply(getItemMatrix(compiled, getItem(compiled, 0), seconds, new Matrix4()), 1, 0, 0);
76
89
  expect(world(0)).toEqual([11, 5, 0]);
77
90
  expect(world(0.5)).toEqual([10, 6, 0]);
78
91
  });
@@ -82,8 +95,8 @@ describe("compileScene", () => {
82
95
  const inner = group([box(vec3(1, 1, 1), red)], vec3(1, 0, 0), vec3Zero, [], {}, spin);
83
96
  const compiled = compileScene(scene([group([], vec3Zero, vec3Zero, [inner], {}, spin)]));
84
97
  expect(compiled.animations.map((a) => a.parent)).toEqual([undefined, 0]);
85
- expect(apply(getItemMatrix(compiled, compiled.items[0], 0.5, new Matrix4()), 0, 0, 0)).toEqual([0, 1, 0]);
86
- expect(apply(getItemMatrix(compiled, compiled.items[0], 0.5, new Matrix4()), 1, 0, 0)).toEqual([-1, 1, 0]);
98
+ expect(apply(getItemMatrix(compiled, getItem(compiled, 0), 0.5, new Matrix4()), 0, 0, 0)).toEqual([0, 1, 0]);
99
+ expect(apply(getItemMatrix(compiled, getItem(compiled, 0), 0.5, new Matrix4()), 1, 0, 0)).toEqual([-1, 1, 0]);
87
100
  });
88
101
 
89
102
  it("converts aligned dimensions to dimension items with a target per dimension", () => {
@@ -92,22 +105,22 @@ describe("compileScene", () => {
92
105
  const items = compiled.items.filter((i) => i.style === "dimension");
93
106
  expect(items.length).toBeGreaterThan(0);
94
107
  expect(new Set(items.map((i) => i.role))).toEqual(new Set(["line", "text", "solid"]));
95
- expect(compiled.targets[items[0].target]).toEqual({ kind: "dimension", id: "fan" });
108
+ expect(compiled.targets[items.at(0)?.target ?? -1]).toEqual({ kind: "dimension", id: "fan" });
96
109
  });
97
110
 
98
111
  it("adds relation shapes, with an outline around their boxes", () => {
99
112
  const relation = { id: "r1", pos: vec3Zero, groups: [group([box(vec3(10, 10, 10), red), sphere(1, red)], vec3(5, 0, 0))] };
100
113
  const compiled = compileScene(scene([], { relations: [relation] }));
101
114
  expect(compiled.items.map((i) => i.style)).toEqual(["relation", "outline", "relation"]);
102
- expect(compiled.targets[compiled.items[0].target]).toEqual({ kind: "relation", relation });
103
- const outline = compiled.items[1].mesh.geometry;
115
+ expect(compiled.targets[getItem(compiled, 0).target]).toEqual({ kind: "relation", relation });
116
+ const outline = getItem(compiled, 1).mesh.geometry;
104
117
  expect(outline.type === "Box" && outline.size).toEqual(vec3(10.125, 10.125, 10.125));
105
- expect(apply(compiled.items[0].matrix, 0, 0, 0)).toEqual([5, 0, 0]);
118
+ expect(apply(compiled.items.at(0)?.matrix, 0, 0, 0)).toEqual([5, 0, 0]);
106
119
  });
107
120
 
108
121
  it("keeps the world matrix of each top-level group for its tooltips", () => {
109
122
  const compiled = compileScene(scene([group([], vec3(3, 0, 0))], { center_deprecated: vec3(1, 0, 0) }));
110
- expect(apply(compiled.roots[0], 0, 0, 0)).toEqual([2, 0, 0]);
123
+ expect(apply(compiled.roots.at(0), 0, 0, 0)).toEqual([2, 0, 0]);
111
124
  expect(apply(compiled.matrix, 0, 0, 0)).toEqual([-1, 0, 0]);
112
125
  });
113
126
  });