abstract-3d 3.1.1 → 4.0.0-beta.0

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 (229) hide show
  1. package/README.md +21 -1
  2. package/lib/renderers/dxf/dxf-encoding/dxf-build.d.ts.map +1 -1
  3. package/lib/renderers/dxf/dxf-encoding/dxf-build.js +1 -2
  4. package/lib/renderers/dxf/dxf-encoding/dxf-build.js.map +1 -1
  5. package/lib/renderers/ifc/ifc.test.d.ts +2 -0
  6. package/lib/renderers/ifc/ifc.test.d.ts.map +1 -0
  7. package/lib/renderers/ifc/ifc.test.js +380 -0
  8. package/lib/renderers/ifc/ifc.test.js.map +1 -0
  9. package/lib/renderers/index.d.ts +1 -0
  10. package/lib/renderers/index.d.ts.map +1 -1
  11. package/lib/renderers/index.js +1 -0
  12. package/lib/renderers/index.js.map +1 -1
  13. package/lib/renderers/react/index.d.ts +2 -5
  14. package/lib/renderers/react/index.d.ts.map +1 -1
  15. package/lib/renderers/react/index.js +1 -2
  16. package/lib/renderers/react/index.js.map +1 -1
  17. package/lib/renderers/react/react.d.ts +16 -42
  18. package/lib/renderers/react/react.d.ts.map +1 -1
  19. package/lib/renderers/react/react.js +49 -32
  20. package/lib/renderers/react/react.js.map +1 -1
  21. package/lib/renderers/three/appearance/appearance.d.ts +41 -0
  22. package/lib/renderers/three/appearance/appearance.d.ts.map +1 -0
  23. package/lib/renderers/three/appearance/appearance.js +89 -0
  24. package/lib/renderers/three/appearance/appearance.js.map +1 -0
  25. package/lib/renderers/three/appearance/appearance.test.d.ts +2 -0
  26. package/lib/renderers/three/appearance/appearance.test.d.ts.map +1 -0
  27. package/lib/renderers/three/appearance/appearance.test.js +97 -0
  28. package/lib/renderers/three/appearance/appearance.test.js.map +1 -0
  29. package/lib/renderers/three/appearance.d.ts +41 -0
  30. package/lib/renderers/three/appearance.d.ts.map +1 -0
  31. package/lib/renderers/three/appearance.js +100 -0
  32. package/lib/renderers/three/appearance.js.map +1 -0
  33. package/lib/renderers/three/build/build.d.ts +51 -0
  34. package/lib/renderers/three/build/build.d.ts.map +1 -0
  35. package/lib/renderers/three/build/build.js +174 -0
  36. package/lib/renderers/three/build/build.js.map +1 -0
  37. package/lib/renderers/three/build/build.test.d.ts +2 -0
  38. package/lib/renderers/three/build/build.test.d.ts.map +1 -0
  39. package/lib/renderers/three/build/build.test.js +120 -0
  40. package/lib/renderers/three/build/build.test.js.map +1 -0
  41. package/lib/renderers/three/build/materials.d.ts +12 -0
  42. package/lib/renderers/three/build/materials.d.ts.map +1 -0
  43. package/lib/renderers/three/build/materials.js +55 -0
  44. package/lib/renderers/three/build/materials.js.map +1 -0
  45. package/lib/renderers/three/build/parts.d.ts +65 -0
  46. package/lib/renderers/three/build/parts.d.ts.map +1 -0
  47. package/lib/renderers/three/build/parts.js +203 -0
  48. package/lib/renderers/three/build/parts.js.map +1 -0
  49. package/lib/renderers/three/build.d.ts +50 -0
  50. package/lib/renderers/three/build.d.ts.map +1 -0
  51. package/lib/renderers/three/build.js +210 -0
  52. package/lib/renderers/three/build.js.map +1 -0
  53. package/lib/renderers/three/camera/camera.d.ts +40 -0
  54. package/lib/renderers/three/camera/camera.d.ts.map +1 -0
  55. package/lib/renderers/three/camera/camera.js +51 -0
  56. package/lib/renderers/three/camera/camera.js.map +1 -0
  57. package/lib/renderers/three/camera/camera.test.d.ts +2 -0
  58. package/lib/renderers/three/camera/camera.test.d.ts.map +1 -0
  59. package/lib/renderers/three/camera/camera.test.js +46 -0
  60. package/lib/renderers/three/camera/camera.test.js.map +1 -0
  61. package/lib/renderers/three/camera/controls.d.ts +24 -0
  62. package/lib/renderers/three/camera/controls.d.ts.map +1 -0
  63. package/lib/renderers/three/camera/controls.js +39 -0
  64. package/lib/renderers/three/camera/controls.js.map +1 -0
  65. package/lib/renderers/three/camera/gizmo.d.ts +44 -0
  66. package/lib/renderers/three/camera/gizmo.d.ts.map +1 -0
  67. package/lib/renderers/three/camera/gizmo.js +42 -0
  68. package/lib/renderers/three/camera/gizmo.js.map +1 -0
  69. package/lib/renderers/three/camera.d.ts +63 -0
  70. package/lib/renderers/three/camera.d.ts.map +1 -0
  71. package/lib/renderers/three/camera.js +85 -0
  72. package/lib/renderers/three/camera.js.map +1 -0
  73. package/lib/renderers/three/compile/compile.d.ts +59 -0
  74. package/lib/renderers/three/compile/compile.d.ts.map +1 -0
  75. package/lib/renderers/three/compile/compile.js +88 -0
  76. package/lib/renderers/three/compile/compile.js.map +1 -0
  77. package/lib/renderers/three/compile/compile.test.d.ts +2 -0
  78. package/lib/renderers/three/compile/compile.test.d.ts.map +1 -0
  79. package/lib/renderers/three/compile/compile.test.js +93 -0
  80. package/lib/renderers/three/compile/compile.test.js.map +1 -0
  81. package/lib/renderers/three/compile.d.ts +58 -0
  82. package/lib/renderers/three/compile.d.ts.map +1 -0
  83. package/lib/renderers/three/compile.js +89 -0
  84. package/lib/renderers/three/compile.js.map +1 -0
  85. package/lib/renderers/three/controls.d.ts +27 -0
  86. package/lib/renderers/three/controls.d.ts.map +1 -0
  87. package/lib/renderers/three/controls.js +115 -0
  88. package/lib/renderers/three/controls.js.map +1 -0
  89. package/lib/renderers/three/geometry/geometry.d.ts +21 -0
  90. package/lib/renderers/three/geometry/geometry.d.ts.map +1 -0
  91. package/lib/renderers/three/geometry/geometry.js +132 -0
  92. package/lib/renderers/three/geometry/geometry.js.map +1 -0
  93. package/lib/renderers/three/geometry/geometry.test.d.ts +2 -0
  94. package/lib/renderers/three/geometry/geometry.test.d.ts.map +1 -0
  95. package/lib/renderers/three/geometry/geometry.test.js +80 -0
  96. package/lib/renderers/three/geometry/geometry.test.js.map +1 -0
  97. package/lib/renderers/three/geometry.d.ts +20 -0
  98. package/lib/renderers/three/geometry.d.ts.map +1 -0
  99. package/lib/renderers/three/geometry.js +164 -0
  100. package/lib/renderers/three/geometry.js.map +1 -0
  101. package/lib/renderers/three/gizmo.d.ts +49 -0
  102. package/lib/renderers/three/gizmo.d.ts.map +1 -0
  103. package/lib/renderers/three/gizmo.js +172 -0
  104. package/lib/renderers/three/gizmo.js.map +1 -0
  105. package/lib/renderers/three/index.d.ts +16 -0
  106. package/lib/renderers/three/index.d.ts.map +1 -0
  107. package/lib/renderers/three/index.js +6 -0
  108. package/lib/renderers/three/index.js.map +1 -0
  109. package/lib/renderers/three/input.d.ts +25 -0
  110. package/lib/renderers/three/input.d.ts.map +1 -0
  111. package/lib/renderers/three/input.js +71 -0
  112. package/lib/renderers/three/input.js.map +1 -0
  113. package/lib/renderers/three/lines.d.ts +16 -0
  114. package/lib/renderers/three/lines.d.ts.map +1 -0
  115. package/lib/renderers/three/lines.js +63 -0
  116. package/lib/renderers/three/lines.js.map +1 -0
  117. package/lib/renderers/three/materials.d.ts +15 -0
  118. package/lib/renderers/three/materials.d.ts.map +1 -0
  119. package/lib/renderers/three/materials.js +70 -0
  120. package/lib/renderers/three/materials.js.map +1 -0
  121. package/lib/renderers/three/options.d.ts +68 -0
  122. package/lib/renderers/three/options.d.ts.map +1 -0
  123. package/lib/renderers/three/options.js +64 -0
  124. package/lib/renderers/three/options.js.map +1 -0
  125. package/lib/renderers/three/overlays.d.ts +25 -0
  126. package/lib/renderers/three/overlays.d.ts.map +1 -0
  127. package/lib/renderers/three/overlays.js +57 -0
  128. package/lib/renderers/three/overlays.js.map +1 -0
  129. package/lib/renderers/three/quality/quality.d.ts +37 -0
  130. package/lib/renderers/three/quality/quality.d.ts.map +1 -0
  131. package/lib/renderers/three/quality/quality.js +64 -0
  132. package/lib/renderers/three/quality/quality.js.map +1 -0
  133. package/lib/renderers/three/quality/quality.test.d.ts +2 -0
  134. package/lib/renderers/three/quality/quality.test.d.ts.map +1 -0
  135. package/lib/renderers/three/quality/quality.test.js +23 -0
  136. package/lib/renderers/three/quality/quality.test.js.map +1 -0
  137. package/lib/renderers/three/quality.d.ts +24 -0
  138. package/lib/renderers/three/quality.d.ts.map +1 -0
  139. package/lib/renderers/three/quality.js +67 -0
  140. package/lib/renderers/three/quality.js.map +1 -0
  141. package/lib/renderers/three/text/text.d.ts +55 -0
  142. package/lib/renderers/three/text/text.d.ts.map +1 -0
  143. package/lib/renderers/three/text/text.js +90 -0
  144. package/lib/renderers/three/text/text.js.map +1 -0
  145. package/lib/renderers/three/text/text.test.d.ts +2 -0
  146. package/lib/renderers/three/text/text.test.d.ts.map +1 -0
  147. package/lib/renderers/three/text/text.test.js +70 -0
  148. package/lib/renderers/three/text/text.test.js.map +1 -0
  149. package/lib/renderers/three/text.d.ts +53 -0
  150. package/lib/renderers/three/text.d.ts.map +1 -0
  151. package/lib/renderers/three/text.js +151 -0
  152. package/lib/renderers/three/text.js.map +1 -0
  153. package/lib/renderers/three/viewer/options.d.ts +71 -0
  154. package/lib/renderers/three/viewer/options.d.ts.map +1 -0
  155. package/lib/renderers/three/viewer/options.js +31 -0
  156. package/lib/renderers/three/viewer/options.js.map +1 -0
  157. package/lib/renderers/three/viewer/options.test.d.ts +2 -0
  158. package/lib/renderers/three/viewer/options.test.d.ts.map +1 -0
  159. package/lib/renderers/three/viewer/options.test.js +38 -0
  160. package/lib/renderers/three/viewer/options.test.js.map +1 -0
  161. package/lib/renderers/three/viewer/overlays.d.ts +31 -0
  162. package/lib/renderers/three/viewer/overlays.d.ts.map +1 -0
  163. package/lib/renderers/three/viewer/overlays.js +43 -0
  164. package/lib/renderers/three/viewer/overlays.js.map +1 -0
  165. package/lib/renderers/three/viewer/viewer.d.ts +90 -0
  166. package/lib/renderers/three/viewer/viewer.d.ts.map +1 -0
  167. package/lib/renderers/three/viewer/viewer.js +432 -0
  168. package/lib/renderers/three/viewer/viewer.js.map +1 -0
  169. package/lib/renderers/three/viewer.d.ts +38 -0
  170. package/lib/renderers/three/viewer.d.ts.map +1 -0
  171. package/lib/renderers/three/viewer.js +435 -0
  172. package/lib/renderers/three/viewer.js.map +1 -0
  173. package/package.json +26 -9
  174. package/src/abstract-3d.ts +1 -1
  175. package/src/custom_typings/n8ao.d.ts +11 -0
  176. package/src/renderers/dxf/dxf-encoding/dxf-build.ts +1 -2
  177. package/src/renderers/index.ts +1 -0
  178. package/src/renderers/react/index.ts +2 -5
  179. package/src/renderers/react/react.tsx +96 -181
  180. package/src/renderers/three/appearance/appearance.test.ts +116 -0
  181. package/src/renderers/three/appearance/appearance.ts +132 -0
  182. package/src/renderers/three/build/build.test.ts +141 -0
  183. package/src/renderers/three/build/build.ts +270 -0
  184. package/src/renderers/three/build/materials.ts +85 -0
  185. package/src/renderers/three/build/parts.ts +277 -0
  186. package/src/renderers/three/camera/camera.test.ts +53 -0
  187. package/src/renderers/three/camera/camera.ts +82 -0
  188. package/src/renderers/three/camera/controls.ts +65 -0
  189. package/src/renderers/three/camera/gizmo.ts +93 -0
  190. package/src/renderers/three/compile/compile.test.ts +113 -0
  191. package/src/renderers/three/compile/compile.ts +151 -0
  192. package/src/renderers/three/geometry/geometry.test.ts +95 -0
  193. package/src/renderers/three/geometry/geometry.ts +179 -0
  194. package/src/renderers/three/index.ts +15 -0
  195. package/src/renderers/three/quality/quality.test.ts +30 -0
  196. package/src/renderers/three/quality/quality.ts +95 -0
  197. package/src/renderers/three/text/text.test.ts +79 -0
  198. package/src/renderers/three/text/text.ts +128 -0
  199. package/src/renderers/three/viewer/options.ts +98 -0
  200. package/src/renderers/three/viewer/overlays.ts +71 -0
  201. package/src/renderers/three/viewer/viewer.ts +540 -0
  202. package/lib/renderers/dxf/color.d.ts +0 -7
  203. package/lib/renderers/dxf/color.d.ts.map +0 -1
  204. package/lib/renderers/dxf/color.js +0 -311
  205. package/lib/renderers/dxf/color.js.map +0 -1
  206. package/lib/renderers/dxf/dxf-encoding.d.ts +0 -17
  207. package/lib/renderers/dxf/dxf-encoding.d.ts.map +0 -1
  208. package/lib/renderers/dxf/dxf-encoding.js +0 -18467
  209. package/lib/renderers/dxf/dxf-encoding.js.map +0 -1
  210. package/lib/renderers/react/react-hotspot.d.ts +0 -27
  211. package/lib/renderers/react/react-hotspot.d.ts.map +0 -1
  212. package/lib/renderers/react/react-hotspot.js +0 -37
  213. package/lib/renderers/react/react-hotspot.js.map +0 -1
  214. package/lib/renderers/shared.d.ts +0 -30
  215. package/lib/renderers/shared.d.ts.map +0 -1
  216. package/lib/renderers/shared.js +0 -164
  217. package/lib/renderers/shared.js.map +0 -1
  218. package/src/custom_typings/react-three-fiber.d.ts +0 -9
  219. package/src/custom_typings/react-three-postprocessing.d.ts +0 -1
  220. package/src/renderers/react/react-camera.tsx +0 -391
  221. package/src/renderers/react/react-dimension.tsx +0 -122
  222. package/src/renderers/react/react-group.test.ts +0 -56
  223. package/src/renderers/react/react-group.tsx +0 -202
  224. package/src/renderers/react/react-image-material.tsx +0 -139
  225. package/src/renderers/react/react-material.tsx +0 -91
  226. package/src/renderers/react/react-mesh.tsx +0 -512
  227. package/src/renderers/react/react-relation.tsx +0 -86
  228. package/src/renderers/react/react-scene.tsx +0 -111
  229. package/src/renderers/react/react-types.ts +0 -9
@@ -0,0 +1,141 @@
1
+ import { BatchedMesh, InstancedMesh, Matrix4, PerspectiveCamera, Raycaster, Vector2, Vector3, Vector4 } from "three";
2
+ import { describe, expect, it } from "vitest";
3
+ import type { Group, Scene } from "../../../abstract-3d.js";
4
+ import { alignedDimension, box, culledLine, group, sphere, text, vec3, vec3Zero } from "../../../abstract-3d.js";
5
+ import type { PaintState, StructureState } from "../appearance/appearance.js";
6
+ import type { CompiledScene } from "../compile/compile.js";
7
+ import { compileScene } from "../compile/compile.js";
8
+ import type { GlyphBitmap } from "../text/text.js";
9
+ import { createGlyphAtlas, SDF_BUFFER } from "../text/text.js";
10
+ import type { BuiltScene } from "./build.js";
11
+ import { animateScene, buildScene, cullScene, disposeScene, getPickables, getPickedItem, paintScene } from "./build.js";
12
+
13
+ const red = { normal: "rgb(255, 0, 0)" };
14
+ const blue = { normal: "rgb(0, 0, 255)", opacity: 0.5 };
15
+ const rasterize = (): GlyphBitmap => {
16
+ const [width, height] = [20 + 2 * SDF_BUFFER, 40 + 2 * SDF_BUFFER];
17
+ return {
18
+ data: new Uint8ClampedArray(width * height),
19
+ width,
20
+ height,
21
+ glyphWidth: 20,
22
+ glyphHeight: 40,
23
+ glyphLeft: 0,
24
+ glyphTop: 30,
25
+ glyphAdvance: 24,
26
+ };
27
+ };
28
+ const idle: StructureState = { relationsActive: false };
29
+ const scene = (groups: ReadonlyArray<Group>, rest: Partial<Scene> = {}): Scene => ({ size_deprecated: vec3(1, 1, 1), groups, ...rest });
30
+ const part = (id: string, x: number, material = red): Group => group([box(vec3(10, 10, 10), material)], vec3(x, 0, 0), vec3Zero, [], { id });
31
+ 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
+ );
36
+ const relation = (id: string): NonNullable<Scene["relations"]>[number] => ({ id, pos: vec3Zero, groups: [group([box(vec3(1, 1, 1), red)])] });
37
+ const batches = (built: BuiltScene): ReadonlyArray<BatchedMesh> => built.root.children.filter((o): o is BatchedMesh => o instanceof BatchedMesh);
38
+
39
+ /** The id of the group under the screen center for a camera at `x` looking down -z */
40
+ function pickId(built: BuiltScene, x: number): string | undefined {
41
+ const camera = new PerspectiveCamera(45, 1, 1, 1000);
42
+ camera.position.set(x, 0, 100);
43
+ camera.updateMatrixWorld();
44
+ built.root.updateMatrixWorld();
45
+ const raycaster = new Raycaster();
46
+ raycaster.setFromCamera(new Vector2(0, 0), camera);
47
+ const hits = raycaster.intersectObjects(getPickables(built), false);
48
+ const item = hits.length > 0 ? getPickedItem(built, hits[0]) : undefined;
49
+ const { compiled }: { readonly compiled: CompiledScene } = built;
50
+ const target = item === undefined ? undefined : compiled.targets[compiled.items[item].target];
51
+ return target?.kind === "group" || target?.kind === "dimension" ? target.id : undefined;
52
+ }
53
+
54
+ describe("buildScene", () => {
55
+ it("draws solids of one material class in one batch, whatever their geometry", () => {
56
+ const built = build(scene([part("a", 0), part("b", 20), group([sphere(5, red)], vec3(40, 0, 0)), part("c", 60, blue)]));
57
+ expect(batches(built).map((b) => b.instanceCount)).toEqual([3, 1]);
58
+ expect(built.parts).toHaveLength(2);
59
+ });
60
+
61
+ it("picks the group under the pointer", () => {
62
+ const built = build(scene([part("a", 0), part("b", 20)]));
63
+ expect([pickId(built, 0), pickId(built, 20), pickId(built, 100)]).toEqual(["a", "b", undefined]);
64
+ });
65
+
66
+ it("picks text by its glyphs", () => {
67
+ const built = build(scene([group([text(vec3Zero, "AB", 10, red)], vec3Zero, vec3Zero, [], { id: "label" })]));
68
+ expect(built.root.children.some((o) => o instanceof InstancedMesh)).toBe(true);
69
+ expect(pickId(built, 0)).toBe("label");
70
+ });
71
+
72
+ it("repaints the hovered and selected targets", () => {
73
+ const built = build(scene([part("a", 0), part("b", 20)]));
74
+ const [batch] = batches(built);
75
+ const color = (instance: number): ReadonlyArray<number> =>
76
+ batch
77
+ .getColorAt(instance, new Vector4())
78
+ .toArray()
79
+ .map((v) => Math.round(v * 100) / 100);
80
+ const before = color(1);
81
+ const state: PaintState = { ...idle, dimensionsClickable: true, hoveredTarget: 1 };
82
+ paintScene(built, state);
83
+ expect(color(1)).not.toEqual(before);
84
+ expect(color(0)).toEqual([1, 0, 0, 1]);
85
+ paintScene(built, { ...state, hoveredTarget: undefined, selectedIds: { a: true } });
86
+ expect(color(1)).toEqual(before);
87
+ expect(color(0)).not.toEqual([1, 0, 0, 1]);
88
+ });
89
+
90
+ it("keeps dimensions in their own group, and doesn't pick hidden ones", () => {
91
+ // Its text is centered half a text height of 44 above the line, here at the origin
92
+ const dimension = alignedDimension(vec3(-500, 78, 0), vec3(500, 78, 0), vec3(0, -22, 0), "1000", ["front"], undefined, "dim");
93
+ const built = build(scene([], { dimensions_deprecated: { dimensions: [dimension], material: red } }));
94
+ expect(built.dimensions.children.length).toBeGreaterThan(0);
95
+ expect(pickId(built, 0)).toBe("dim");
96
+ built.dimensions.visible = false;
97
+ expect(pickId(built, 0)).toBeUndefined();
98
+ });
99
+
100
+ it("builds the active relations only", () => {
101
+ const s = scene([], { relations: [relation("r1"), relation("r2")] });
102
+ expect(build(s).parts).toHaveLength(0);
103
+ // A relation's box and its outline
104
+ expect(batches(build(s, { relationsActive: true }, ["r1"])).map((b) => b.instanceCount)).toEqual([1, 1]);
105
+ });
106
+
107
+ it("fades and moves disabled groups to a transparent batch", () => {
108
+ const built = build(scene([part("a", 0), part("b", 20)]), { relationsActive: true, activeComponents: { b: "Accept" } });
109
+ expect(batches(built).map((b) => [b.instanceCount, b.material.transparent])).toEqual([
110
+ [1, true],
111
+ [1, false],
112
+ ]);
113
+ });
114
+
115
+ it("animates the instances of animated groups", () => {
116
+ const spin = { transform: { rot: vec3(0, 0, Math.PI), trans: vec3Zero }, duration: 1000 };
117
+ const built = build(scene([group([box(vec3(1, 1, 1), red)], vec3(5, 0, 0), vec3Zero, [], {}, spin)]));
118
+ expect(built.animatedItems).toHaveLength(1);
119
+ animateScene(built, 0.5);
120
+ const position = new Vector3(1, 0, 0).applyMatrix4(batches(built)[0].getMatrixAt(0, new Matrix4()));
121
+ expect(position.toArray().map((v) => Math.round(v * 1000) / 1000 + 0)).toEqual([5, 1, 0]);
122
+ });
123
+
124
+ it("hides culled lines that face away from the camera", () => {
125
+ const built = build(scene([group([culledLine(vec3(0, 0, 0), vec3(10, 0, 0), vec3(0, 0, 1), 1, red)])]));
126
+ const line = built.parts.find((p) => p.kind === "line")!;
127
+ const endX = (): number => (line.object.geometry.getAttribute("instanceEnd") as unknown as { readonly getX: (i: number) => number }).getX(0);
128
+ cullScene(built, new Vector3(0, 0, -100));
129
+ expect(endX()).toBe(0);
130
+ cullScene(built, new Vector3(0, 0, 100));
131
+ expect(endX()).toBe(10);
132
+ });
133
+
134
+ it("disposes its batches and materials", () => {
135
+ const built = build(scene([part("a", 0)]));
136
+ const disposed: Array<string> = [];
137
+ batches(built)[0].material.addEventListener("dispose", () => disposed.push("material"));
138
+ disposeScene(built);
139
+ expect(disposed).toEqual(["material"]);
140
+ });
141
+ });
@@ -0,0 +1,270 @@
1
+ import {
2
+ type BufferGeometry,
3
+ Group,
4
+ type Intersection,
5
+ Matrix4,
6
+ type Mesh,
7
+ type MeshBasicMaterial,
8
+ type Object3D,
9
+ type Texture,
10
+ type Vector3,
11
+ Vector4,
12
+ } from "three";
13
+ import type { PaintState, StructureState, TargetState } from "../appearance/appearance.js";
14
+ import { getHighlight, getItemColor, getMaterialClass, getTargetStates } from "../appearance/appearance.js";
15
+ import type { CompiledScene } from "../compile/compile.js";
16
+ import { getItemMatrix, transform } from "../compile/compile.js";
17
+ import { createSolidGeometry, getSolidShape, keepGeometries } from "../geometry/geometry.js";
18
+ import type { GlyphAtlas } from "../text/text.js";
19
+ import type { TextureCache } from "./materials.js";
20
+ import { createClassMaterial, createImageMaterial, getImageUrl, loadTexture } from "./materials.js";
21
+ import type { Part } from "./parts.js";
22
+ import {
23
+ createImagePart,
24
+ createLinePart,
25
+ createSolidPart,
26
+ createTextPart,
27
+ cullLines,
28
+ disposePart,
29
+ flushPart,
30
+ getHitSlot,
31
+ paintSlot,
32
+ placeSlot,
33
+ } from "./parts.js";
34
+
35
+ /** Compiled items → parts, with a slot per item, and painting, animating, culling, picking and disposing them */
36
+
37
+ export type BuildOptions = {
38
+ readonly structure: StructureState;
39
+ readonly activeRelations?: ReadonlyArray<string>;
40
+ /** Geometries by shape key, shared between builds */
41
+ readonly geometries: Map<string, BufferGeometry>;
42
+ readonly atlas: GlyphAtlas;
43
+ /** Without it images aren't shown */
44
+ readonly textures?: TextureCache;
45
+ readonly materialStateImages?: Record<string, string>;
46
+ readonly useAlphaTest?: boolean;
47
+ readonly onTextureLoaded?: () => void;
48
+ };
49
+
50
+ export type BuiltScene = {
51
+ readonly compiled: CompiledScene;
52
+ readonly targetStates: ReadonlyArray<TargetState>;
53
+ readonly root: Group;
54
+ /** Holds the dimensions, shown or hidden by its `visible` */
55
+ readonly dimensions: Group;
56
+ readonly parts: ReadonlyArray<Part>;
57
+ /** Each compiled item's part and slot, -1 for items not built */
58
+ readonly partOfItem: Int32Array;
59
+ readonly slotOfItem: Int32Array;
60
+ readonly itemsOfPart: ReadonlyArray<ReadonlyArray<number>>;
61
+ readonly itemsOfTarget: ReadonlyArray<ReadonlyArray<number>>;
62
+ readonly animatedItems: ReadonlyArray<number>;
63
+ /** Each target's painted highlight, -1 before the first paint */
64
+ readonly highlights: Int8Array;
65
+ readonly partOfObject: ReadonlyMap<Object3D, number>;
66
+ readonly state: { disposed: boolean };
67
+ };
68
+
69
+ export function buildScene(compiled: CompiledScene, options: BuildOptions): BuiltScene {
70
+ const targetStates = getTargetStates(compiled.targets, options.structure);
71
+ const active = new Set(options.activeRelations);
72
+ const groups = new Map<string, Array<number>>();
73
+ const itemsOfTarget = compiled.targets.map((): Array<number> => []);
74
+ const animatedItems: Array<number> = [];
75
+ compiled.items.forEach((item, i) => {
76
+ const target = compiled.targets[item.target]!;
77
+ if (target.kind === "relation" && !active.has(target.relation.id)) {
78
+ return;
79
+ }
80
+ // Items of the same part key are drawn by one part
81
+ const g = item.mesh.geometry;
82
+ const container = item.style === "dimension" ? "dimension" : "scene";
83
+ const key =
84
+ item.role === "solid"
85
+ ? `${container} solid ${JSON.stringify(getMaterialClass(item, targetStates[item.target]!))}`
86
+ : item.role === "line"
87
+ ? `${container} line ${g.type === "Line" || g.type === "CulledLine" ? g.thickness : 0}`
88
+ : item.role === "text"
89
+ ? `${container} text`
90
+ : `image ${i}`;
91
+ const group = groups.get(key);
92
+ if (group) {
93
+ group.push(i);
94
+ } else {
95
+ groups.set(key, [i]);
96
+ }
97
+ itemsOfTarget[item.target]!.push(i);
98
+ if (item.animation !== undefined) {
99
+ animatedItems.push(i);
100
+ }
101
+ });
102
+
103
+ const [root, dimensions] = [new Group(), new Group()];
104
+ root.add(dimensions);
105
+ const usedShapes = new Set<string>();
106
+ const itemsOfPart = [...groups.values()];
107
+ const partOfItem = new Int32Array(compiled.items.length).fill(-1);
108
+ const slotOfItem = new Int32Array(compiled.items.length).fill(-1);
109
+ const state = { disposed: false };
110
+ const parts = itemsOfPart.map((indices, p) => {
111
+ indices.forEach((i, slot) => {
112
+ partOfItem[i] = p;
113
+ slotOfItem[i] = slot;
114
+ });
115
+ const part = createPart(compiled, indices, targetStates, options, usedShapes, state);
116
+ (compiled.items[indices[0]!]!.style === "dimension" ? dimensions : root).add(part.object);
117
+ return part;
118
+ });
119
+ keepGeometries(options.geometries, usedShapes);
120
+
121
+ const built: BuiltScene = {
122
+ compiled,
123
+ targetStates,
124
+ root,
125
+ dimensions,
126
+ parts,
127
+ partOfItem,
128
+ slotOfItem,
129
+ itemsOfPart,
130
+ itemsOfTarget,
131
+ animatedItems,
132
+ highlights: new Int8Array(compiled.targets.length).fill(-1),
133
+ partOfObject: new Map(parts.map((part, p) => [part.object, p])),
134
+ state,
135
+ };
136
+ compiled.items.forEach((item, i) => {
137
+ if (partOfItem[i]! >= 0) {
138
+ placeSlot(parts[partOfItem[i]!]!, slotOfItem[i]!, getItemMatrix(compiled, item, 0, world));
139
+ }
140
+ });
141
+ paintScene(built, { ...options.structure, dimensionsClickable: false });
142
+ parts.forEach((part) => flushPart(part, true));
143
+ return built;
144
+ }
145
+
146
+ /** Repaints the targets whose highlight changed */
147
+ export function paintScene(built: BuiltScene, state: PaintState): void {
148
+ const { compiled, parts, highlights } = built;
149
+ const touched = new Set<Part>();
150
+ compiled.targets.forEach((target, t) => {
151
+ const highlight = getHighlight(target, t, state);
152
+ if (highlight !== highlights[t]) {
153
+ highlights[t] = highlight;
154
+ built.itemsOfTarget[t]!.forEach((i) => {
155
+ const part = parts[built.partOfItem[i]!]!;
156
+ paintSlot(part, built.slotOfItem[i]!, getItemColor(compiled.items[i]!, built.targetStates[t]!, highlight, color));
157
+ touched.add(part);
158
+ });
159
+ }
160
+ });
161
+ touched.forEach((part) => flushPart(part, false));
162
+ }
163
+
164
+ /** Moves the animated items to `seconds` into their animations */
165
+ export function animateScene(built: BuiltScene, seconds: number): void {
166
+ const touched = new Set<Part>();
167
+ built.animatedItems.forEach((i) => {
168
+ const part = built.parts[built.partOfItem[i]!]!;
169
+ placeSlot(part, built.slotOfItem[i]!, getItemMatrix(built.compiled, built.compiled.items[i]!, seconds, world));
170
+ touched.add(part);
171
+ });
172
+ touched.forEach((part) => flushPart(part, true));
173
+ }
174
+
175
+ export function cullScene(built: BuiltScene, cameraPosition: Vector3): void {
176
+ built.parts.forEach((part) => cullLines(part, cameraPosition));
177
+ }
178
+
179
+ /** The shown objects that can be picked; lines aren't picked */
180
+ export function getPickables(built: BuiltScene): Array<Object3D> {
181
+ const pickables: Array<Object3D> = [];
182
+ built.parts.forEach((part) => {
183
+ if (part.kind !== "line" && part.object.visible && part.object.parent?.visible !== false) {
184
+ pickables.push(part.object);
185
+ }
186
+ });
187
+ return pickables;
188
+ }
189
+
190
+ /** The index of the compiled item hit */
191
+ export function getPickedItem(built: BuiltScene, hit: Intersection): number | undefined {
192
+ const p = built.partOfObject.get(hit.object);
193
+ const slot = p === undefined ? undefined : getHitSlot(built.parts[p]!, hit);
194
+ return slot === undefined ? undefined : built.itemsOfPart[p!]![slot];
195
+ }
196
+
197
+ export function disposeScene(built: BuiltScene): void {
198
+ built.state.disposed = true;
199
+ built.parts.forEach(disposePart);
200
+ built.root.removeFromParent();
201
+ }
202
+
203
+ function createPart(
204
+ compiled: CompiledScene,
205
+ indices: ReadonlyArray<number>,
206
+ targetStates: ReadonlyArray<TargetState>,
207
+ options: BuildOptions,
208
+ usedShapes: Set<string>,
209
+ state: { readonly disposed: boolean }
210
+ ): Part {
211
+ const items = indices.map((i) => compiled.items[i]!);
212
+ const first = items[0]!;
213
+ switch (first.role) {
214
+ case "text":
215
+ return createTextPart(
216
+ items.flatMap(({ mesh: { geometry: g } }) =>
217
+ g.type === "Text" ? [{ text: g.text, fontSize: g.fontSize, local: transform(g.pos, g.rot) }] : []
218
+ ),
219
+ options.atlas
220
+ );
221
+ case "line":
222
+ return createLinePart(
223
+ items.flatMap(({ mesh: { geometry: g } }) => (g.type === "Line" || g.type === "CulledLine" ? [g] : [])),
224
+ first.mesh.geometry.type === "Line" || first.mesh.geometry.type === "CulledLine" ? first.mesh.geometry.thickness : 1
225
+ );
226
+ default: {
227
+ const entries = items.map((item) => {
228
+ const shape = getSolidShape(item.mesh.geometry)!;
229
+ const geometry = options.geometries.get(shape.key) ?? createSolidGeometry(item.mesh.geometry);
230
+ options.geometries.set(shape.key, geometry);
231
+ usedShapes.add(shape.key);
232
+ return { geometry, local: shape.local };
233
+ });
234
+ if (first.role === "solid") {
235
+ return createSolidPart(entries, createClassMaterial(getMaterialClass(first, targetStates[first.target]!)));
236
+ }
237
+ // Images are hidden until their texture loads
238
+ const part = createImagePart(
239
+ entries[0]!.geometry,
240
+ entries[0]!.local,
241
+ createImageMaterial(first.mesh.material.backfaceCulling, options.useAlphaTest ?? true)
242
+ );
243
+ const g = first.mesh.geometry;
244
+ if (g.type === "Image" && options.textures) {
245
+ const url = getImageUrl(g.image, targetStates[first.target]!.materialState, options.materialStateImages);
246
+ loadTexture(options.textures, url)
247
+ .then((texture) => showImage(part.object, texture, state, options.onTextureLoaded))
248
+ .catch(() => undefined);
249
+ }
250
+ return part;
251
+ }
252
+ }
253
+ }
254
+
255
+ function showImage(
256
+ mesh: Mesh<BufferGeometry, MeshBasicMaterial>,
257
+ texture: Texture | undefined,
258
+ state: { readonly disposed: boolean },
259
+ onLoaded: (() => void) | undefined
260
+ ): void {
261
+ if (texture && !state.disposed) {
262
+ mesh.material.map = texture;
263
+ mesh.material.needsUpdate = true;
264
+ mesh.visible = true;
265
+ onLoaded?.();
266
+ }
267
+ }
268
+
269
+ const world = new Matrix4();
270
+ const color = new Vector4();
@@ -0,0 +1,85 @@
1
+ import { createSVG } from "abstract-image";
2
+ import {
3
+ BackSide,
4
+ CanvasTexture,
5
+ DoubleSide,
6
+ FrontSide,
7
+ LinearFilter,
8
+ LinearMipmapLinearFilter,
9
+ type Material,
10
+ MeshBasicMaterial,
11
+ MeshStandardMaterial,
12
+ SRGBColorSpace,
13
+ type Texture,
14
+ TextureLoader,
15
+ } from "three";
16
+ import type { Image } from "../../../abstract-3d.js";
17
+ import type { MaterialClass, MaterialState } from "../appearance/appearance.js";
18
+ import { ERROR_IMG_KEY } from "../appearance/appearance.js";
19
+
20
+ /** White materials, colored per instance, for each material class; images' materials and textures */
21
+
22
+ export function createClassMaterial(c: MaterialClass): Material {
23
+ const side = c.side === "front" ? FrontSide : c.side === "back" ? BackSide : DoubleSide;
24
+ 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);
26
+ }
27
+
28
+ export function createImageMaterial(backfaceCulling: boolean | undefined, useAlphaTest: boolean): MeshBasicMaterial {
29
+ return new MeshBasicMaterial({ side: backfaceCulling ? FrontSide : DoubleSide, alphaTest: useAlphaTest ? 0.8 : 0, transparent: true });
30
+ }
31
+
32
+ export function getImageUrl(image: Image, materialState: MaterialState | undefined, materialStateImages: Record<string, string> | undefined): string {
33
+ const errorUrl = materialState === "Error" ? materialStateImages?.[ERROR_IMG_KEY] : undefined;
34
+ return errorUrl ?? (image.type === "AbstractImage" ? `data:image/svg+xml,${createSVG(image.image)}` : image.url);
35
+ }
36
+
37
+ /** Each url's texture, loaded once */
38
+ export type TextureCache = Map<string, Promise<Texture | undefined>>;
39
+
40
+ export function loadTexture(cache: TextureCache, url: string): Promise<Texture | undefined> {
41
+ const svg = url.startsWith("data:image/svg+xml") || url.endsWith(".svg") || url.includes(".svg?");
42
+ const loading = cache.get(url) ?? (svg ? loadSvg(url) : loader.loadAsync(url).catch(() => undefined)).then((t) => t && applyFilters(t));
43
+ cache.set(url, loading);
44
+ return loading;
45
+ }
46
+
47
+ export function disposeTextures(cache: TextureCache): void {
48
+ cache.forEach((loading) => {
49
+ loading.then((texture) => texture?.dispose()).catch(() => undefined);
50
+ });
51
+ cache.clear();
52
+ }
53
+
54
+ const loader = new TextureLoader();
55
+ const svgSize = 512;
56
+
57
+ /** SVGs are rasterized at up to 512 px, as their own size is often small; from the r3f renderer's react-image-material.tsx */
58
+ function loadSvg(url: string): Promise<Texture | undefined> {
59
+ const image = new Image();
60
+ image.src = url;
61
+ return image.decode().then(
62
+ () => rasterize(image),
63
+ () => undefined
64
+ );
65
+ }
66
+
67
+ function rasterize(image: HTMLImageElement): Texture | undefined {
68
+ const scale = svgSize / Math.max(image.naturalWidth, image.naturalHeight);
69
+ const canvas = document.createElement("canvas");
70
+ Object.assign(canvas, { width: image.naturalWidth * scale, height: image.naturalHeight * scale });
71
+ canvas.getContext("2d")?.drawImage(image, 0, 0, canvas.width, canvas.height);
72
+ return new CanvasTexture(canvas);
73
+ }
74
+
75
+ function applyFilters(texture: Texture): Texture {
76
+ Object.assign(texture, {
77
+ colorSpace: SRGBColorSpace,
78
+ minFilter: LinearMipmapLinearFilter,
79
+ magFilter: LinearFilter,
80
+ generateMipmaps: true,
81
+ anisotropy: 4,
82
+ });
83
+ texture.needsUpdate = true;
84
+ return texture;
85
+ }