abstract-3d 3.1.2 → 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 (216) 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 +36 -16
  174. package/src/custom_typings/n8ao.d.ts +11 -0
  175. package/src/renderers/dxf/dxf-encoding/dxf-build.ts +1 -2
  176. package/src/renderers/index.ts +1 -0
  177. package/src/renderers/react/index.ts +2 -5
  178. package/src/renderers/react/react.tsx +96 -181
  179. package/src/renderers/three/appearance/appearance.test.ts +116 -0
  180. package/src/renderers/three/appearance/appearance.ts +132 -0
  181. package/src/renderers/three/build/build.test.ts +141 -0
  182. package/src/renderers/three/build/build.ts +270 -0
  183. package/src/renderers/three/build/materials.ts +85 -0
  184. package/src/renderers/three/build/parts.ts +277 -0
  185. package/src/renderers/three/camera/camera.test.ts +53 -0
  186. package/src/renderers/three/camera/camera.ts +82 -0
  187. package/src/renderers/three/camera/controls.ts +65 -0
  188. package/src/renderers/three/camera/gizmo.ts +93 -0
  189. package/src/renderers/three/compile/compile.test.ts +113 -0
  190. package/src/renderers/three/compile/compile.ts +151 -0
  191. package/src/renderers/three/geometry/geometry.test.ts +95 -0
  192. package/src/renderers/three/geometry/geometry.ts +179 -0
  193. package/src/renderers/three/index.ts +15 -0
  194. package/src/renderers/three/quality/quality.test.ts +30 -0
  195. package/src/renderers/three/quality/quality.ts +95 -0
  196. package/src/renderers/three/text/text.test.ts +79 -0
  197. package/src/renderers/three/text/text.ts +128 -0
  198. package/src/renderers/three/viewer/options.ts +98 -0
  199. package/src/renderers/three/viewer/overlays.ts +71 -0
  200. package/src/renderers/three/viewer/viewer.ts +540 -0
  201. package/lib/renderers/react/react-hotspot.d.ts +0 -27
  202. package/lib/renderers/react/react-hotspot.d.ts.map +0 -1
  203. package/lib/renderers/react/react-hotspot.js +0 -37
  204. package/lib/renderers/react/react-hotspot.js.map +0 -1
  205. package/src/custom_typings/react-three-fiber.d.ts +0 -9
  206. package/src/custom_typings/react-three-postprocessing.d.ts +0 -1
  207. package/src/renderers/react/react-camera.tsx +0 -391
  208. package/src/renderers/react/react-dimension.tsx +0 -122
  209. package/src/renderers/react/react-group.test.ts +0 -56
  210. package/src/renderers/react/react-group.tsx +0 -202
  211. package/src/renderers/react/react-image-material.tsx +0 -139
  212. package/src/renderers/react/react-material.tsx +0 -91
  213. package/src/renderers/react/react-mesh.tsx +0 -512
  214. package/src/renderers/react/react-relation.tsx +0 -86
  215. package/src/renderers/react/react-scene.tsx +0 -111
  216. package/src/renderers/react/react-types.ts +0 -9
@@ -0,0 +1,95 @@
1
+ import { N8AOPass } from "n8ao";
2
+ import type { OrthographicCamera, PerspectiveCamera, Scene, WebGLRenderer } from "three";
3
+ import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
4
+ import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
5
+
6
+ /**
7
+ * Quality tiers that spend the GPU where it shows: the camera is still most of the time, and a still frame can afford ambient occlusion that a moving
8
+ * one can't on a weak GPU. Low renders directly; medium adds ambient occlusion at rest, faded in; high has it while moving too. Effects only at rest
9
+ * is how progressive model viewers such as Sketchfab's stay fast; the AO is N8AO, configured as the r3f renderer's
10
+ */
11
+
12
+ export type Tier = "low" | "medium" | "high";
13
+ export type Quality = Tier | "auto";
14
+
15
+ /** `auto` starts at medium, and drops to low for good once moving frames average slower than 40 per second */
16
+ export type TierGovernor = { readonly quality: Quality; tier: Tier; sum: number; count: number };
17
+
18
+ export type RenderPath = {
19
+ readonly tier: Tier;
20
+ /** With ambient occlusion, faded in by its intensity over the first frames at rest */
21
+ readonly composer: EffectComposer | undefined;
22
+ readonly ao: { readonly configuration: { intensity: number } } | undefined;
23
+ readonly fade: { frame: number };
24
+ };
25
+
26
+ const pixelRatioCaps: Record<Tier, number> = { low: 1, medium: 1.5, high: 2 };
27
+ const windowSize = 30;
28
+ const slowFrameMs = 25;
29
+ /** Longer intervals are pauses between moves, not slow frames */
30
+ const pauseMs = 250;
31
+ const fadeFrames = 6;
32
+ const intensity = 2;
33
+
34
+ /** Records the time between two frames while the camera moves; true when that dropped the tier */
35
+ export function recordFrame(governor: TierGovernor, intervalMs: number): boolean {
36
+ if (governor.quality !== "auto" || governor.tier === "low" || intervalMs > pauseMs) {
37
+ return false;
38
+ }
39
+ governor.sum += intervalMs;
40
+ governor.count += 1;
41
+ if (governor.count < windowSize) {
42
+ return false;
43
+ }
44
+ const slow = governor.sum / governor.count > slowFrameMs;
45
+ Object.assign(governor, { sum: 0, count: 0 }, slow ? { tier: "low" } : {});
46
+ return slow;
47
+ }
48
+
49
+ /** Also sets the renderer's pixel ratio, capped by the tier */
50
+ export function createRenderPath(renderer: WebGLRenderer, scene: Scene, camera: PerspectiveCamera | OrthographicCamera, tier: Tier): RenderPath {
51
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, pixelRatioCaps[tier]));
52
+ if (tier === "low") {
53
+ return { tier, composer: undefined, ao: undefined, fade: { frame: 0 } };
54
+ }
55
+ const composer = new EffectComposer(renderer);
56
+ const ao = new N8AOPass(scene, camera);
57
+ Object.assign(ao.configuration, {
58
+ aoRadius: 40,
59
+ distanceFalloff: 0.2,
60
+ screenSpaceRadius: true,
61
+ halfRes: true,
62
+ denoiseSamples: 4,
63
+ gammaCorrection: false,
64
+ });
65
+ // Multisampled, as SMAA, which N8AO suggests, would lose the antialiased alpha at edges over the transparent background
66
+ ao.beautyRenderTarget.samples = 4;
67
+ composer.addPass(ao);
68
+ // Tone mapping and sRGB, which rendering to the screen otherwise does
69
+ composer.addPass(new OutputPass());
70
+ return { tier, composer, ao, fade: { frame: 0 } };
71
+ }
72
+
73
+ /** Renders a frame; true while the frame at rest needs more frames to fade its ambient occlusion in */
74
+ export function renderPath(
75
+ path: RenderPath,
76
+ renderer: WebGLRenderer,
77
+ scene: Scene,
78
+ camera: PerspectiveCamera | OrthographicCamera,
79
+ moving: boolean
80
+ ): boolean {
81
+ if (!path.composer || !path.ao || (moving && path.tier === "medium")) {
82
+ path.fade.frame = 0;
83
+ renderer.render(scene, camera);
84
+ return false;
85
+ }
86
+ path.fade.frame = path.tier === "high" ? fadeFrames : Math.min(fadeFrames, path.fade.frame + 1);
87
+ path.ao.configuration.intensity = (intensity * path.fade.frame) / fadeFrames;
88
+ path.composer.render();
89
+ return path.fade.frame < fadeFrames;
90
+ }
91
+
92
+ export function disposeRenderPath(path: RenderPath): void {
93
+ path.composer?.passes.forEach((p) => p.dispose());
94
+ path.composer?.dispose();
95
+ }
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { GlyphBitmap } from "./text.js";
3
+ import { createGlyphAtlas, getGlyph, layoutText, RASTER_SIZE, SDF_BUFFER } from "./text.js";
4
+
5
+ /** Glyphs of 20 px ink, 30 px of it above the baseline, a 24 px advance, filled with their char code */
6
+ const rasterize = (char: string): GlyphBitmap => {
7
+ const [glyphWidth, glyphHeight] = char === " " ? [0, 0] : [20, 40];
8
+ const [width, height] = [glyphWidth + 2 * SDF_BUFFER, glyphHeight + 2 * SDF_BUFFER];
9
+ return {
10
+ data: new Uint8ClampedArray(width * height).fill(char.charCodeAt(0)),
11
+ width,
12
+ height,
13
+ glyphWidth,
14
+ glyphHeight,
15
+ glyphLeft: 2,
16
+ glyphTop: 30,
17
+ glyphAdvance: 24,
18
+ };
19
+ };
20
+ const em = (px: number): number => px / RASTER_SIZE;
21
+ const round = (v: number): number => Math.round(v * 1e4) / 1e4;
22
+
23
+ describe("getGlyph", () => {
24
+ it("packs each glyph once, left to right, and writes its bitmap", () => {
25
+ const atlas = createGlyphAtlas(rasterize, 64);
26
+ const a = getGlyph(atlas, "A");
27
+ expect(getGlyph(atlas, "A")).toBe(a);
28
+ const b = getGlyph(atlas, "B");
29
+ const data = atlas.texture.image.data as Uint8Array;
30
+ expect([data[0], data[28]]).toEqual([65, 66]);
31
+ expect(a.rect.map(round)).toEqual([0, round(48 / 64), round(28 / 64), round(-48 / 64)]);
32
+ expect(b.rect[0]).toBe(28 / 64);
33
+ });
34
+
35
+ it("starts a new row when a row is full", () => {
36
+ const atlas = createGlyphAtlas(rasterize, 128);
37
+ ["A", "B", "C", "D"].forEach((char) => getGlyph(atlas, char));
38
+ expect(getGlyph(atlas, "E").rect.slice(0, 2)).toEqual([0, 96 / 128]);
39
+ });
40
+
41
+ it("gives glyphs their bitmap's position and size in em, from the pen at the baseline", () => {
42
+ expect(getGlyph(createGlyphAtlas(rasterize, 64), "A")).toMatchObject({
43
+ left: em(2 - SDF_BUFFER),
44
+ top: em(30 + SDF_BUFFER),
45
+ width: em(28),
46
+ height: em(48),
47
+ advance: em(24),
48
+ });
49
+ });
50
+
51
+ it("leaves glyphs without ink out of the atlas", () => {
52
+ const atlas = createGlyphAtlas(rasterize, 64);
53
+ expect(getGlyph(atlas, " ").width).toBe(0);
54
+ expect(getGlyph(atlas, "A").rect[0]).toBe(0);
55
+ });
56
+ });
57
+
58
+ describe("layoutText", () => {
59
+ const atlas = createGlyphAtlas(rasterize, 256);
60
+
61
+ it("centers a line on the origin", () => {
62
+ const advance = 24 + 0.001 * 40;
63
+ const quads = layoutText(atlas, "AA", 40);
64
+ expect(quads.map((q) => round(q.x))).toEqual([round(-advance + 2 - SDF_BUFFER + 14), round(2 - SDF_BUFFER + 14)]);
65
+ expect(quads.map((q) => [round(q.width), round(q.height)])).toEqual([
66
+ [28, 48],
67
+ [28, 48],
68
+ ]);
69
+ });
70
+
71
+ it("stacks lines 1.5 font sizes apart, centered vertically as a block", () => {
72
+ // The block is 120 high; each baseline is half the 20 of leading and an ascent of 32 below its line's top, and the glyphs' centers 10 above it
73
+ expect(layoutText(atlas, "A\nA", 40).map((q) => round(q.y))).toEqual([28, -32]);
74
+ });
75
+
76
+ it("skips glyphs without ink but keeps their advance", () => {
77
+ expect(layoutText(atlas, "A A", 40)).toHaveLength(2);
78
+ });
79
+ });
@@ -0,0 +1,128 @@
1
+ import TinySDF from "@mapbox/tiny-sdf";
2
+ import { DataTexture, LinearFilter, RedFormat, UnsignedByteType } from "three";
3
+
4
+ /**
5
+ * Text as signed distance field glyphs from the system font, packed into one atlas texture, so all text draws in one call. The glyph atlas is Mapbox
6
+ * GL JS's approach (its @mapbox/tiny-sdf rasterizes), and the distance field text Valve's (Green, "Improved Alpha-Tested Magnification for Vector
7
+ * Textures and Special Effects", SIGGRAPH 2007)
8
+ */
9
+
10
+ export const RASTER_SIZE = 40;
11
+ export const SDF_BUFFER = 4;
12
+ /** The distance field's value at the glyph's edge, from tiny-sdf's default cutoff of 0.25 */
13
+ export const SDF_EDGE = 0.75;
14
+ const LINE_HEIGHT = 1.5;
15
+ const LETTER_SPACING = 0.001;
16
+ /** Of the font size, above the baseline, to center a line's glyphs in it */
17
+ const ASCENT = 0.8;
18
+
19
+ export type GlyphBitmap = ReturnType<TinySDF["draw"]>;
20
+ export type Rasterize = (char: string) => GlyphBitmap;
21
+
22
+ export type Glyph = {
23
+ /** The glyph's place in the atlas as a uv offset and scale, for a quad with uv (0, 0) at its bottom left */
24
+ readonly rect: readonly [number, number, number, number];
25
+ /** The bitmap's left and top edges and size, in em, from the pen at the baseline; no width for glyphs without ink */
26
+ readonly left: number;
27
+ readonly top: number;
28
+ readonly width: number;
29
+ readonly height: number;
30
+ readonly advance: number;
31
+ };
32
+
33
+ export type GlyphAtlas = {
34
+ readonly texture: DataTexture;
35
+ readonly size: number;
36
+ readonly glyphs: Map<string, Glyph>;
37
+ /** Where the next glyph goes: the current row's top and next x, and its height */
38
+ readonly shelf: { x: number; y: number; height: number };
39
+ readonly rasterize: Rasterize | undefined;
40
+ readonly sdf: { current?: TinySDF };
41
+ };
42
+
43
+ /** A quad of a laid out text, with its center and size in the text's units, centered on the origin */
44
+ export type GlyphQuad = {
45
+ readonly glyph: Glyph;
46
+ readonly x: number;
47
+ readonly y: number;
48
+ readonly width: number;
49
+ readonly height: number;
50
+ };
51
+
52
+ /** Without `rasterize`, glyphs are rasterized with the OS's built-in Helvetica or Arial, on a canvas created at first use */
53
+ export function createGlyphAtlas(rasterize?: Rasterize, size = 1024): GlyphAtlas {
54
+ const texture = new DataTexture(new Uint8Array(size * size), size, size, RedFormat, UnsignedByteType);
55
+ Object.assign(texture, { minFilter: LinearFilter, magFilter: LinearFilter, generateMipmaps: false });
56
+ return { texture, size, glyphs: new Map(), shelf: { x: 0, y: 0, height: 0 }, rasterize, sdf: {} };
57
+ }
58
+
59
+ export function getGlyph(atlas: GlyphAtlas, char: string): Glyph {
60
+ const cached = atlas.glyphs.get(char);
61
+ if (cached) {
62
+ return cached;
63
+ }
64
+ atlas.sdf.current ??= atlas.rasterize
65
+ ? undefined
66
+ : new TinySDF({ fontSize: RASTER_SIZE, buffer: SDF_BUFFER, radius: 10, fontFamily: "Helvetica, Arial, sans-serif" });
67
+ const bitmap = atlas.rasterize?.(char) ?? atlas.sdf.current!.draw(char);
68
+ const rect = bitmap.width > 2 * SDF_BUFFER && bitmap.height > 2 * SDF_BUFFER ? packGlyph(atlas, bitmap) : undefined;
69
+ const glyph: Glyph = {
70
+ rect: rect ?? [0, 0, 0, 0],
71
+ left: (bitmap.glyphLeft - SDF_BUFFER) / RASTER_SIZE,
72
+ top: (bitmap.glyphTop + SDF_BUFFER) / RASTER_SIZE,
73
+ width: rect ? bitmap.width / RASTER_SIZE : 0,
74
+ height: rect ? bitmap.height / RASTER_SIZE : 0,
75
+ advance: bitmap.glyphAdvance / RASTER_SIZE,
76
+ };
77
+ atlas.glyphs.set(char, glyph);
78
+ return glyph;
79
+ }
80
+
81
+ /** Copies the bitmap to the atlas, left to right in rows, or leaves it out when the atlas is full; shelf packing, as @mapbox/shelf-pack does */
82
+ function packGlyph({ texture, size, shelf }: GlyphAtlas, bitmap: GlyphBitmap): Glyph["rect"] | undefined {
83
+ if (shelf.x + bitmap.width > size) {
84
+ Object.assign(shelf, { x: 0, y: shelf.y + shelf.height, height: 0 });
85
+ }
86
+ if (shelf.y + bitmap.height > size) {
87
+ return undefined;
88
+ }
89
+ const { x, y } = shelf;
90
+ const data = texture.image.data as Uint8Array;
91
+ for (let row = 0; row < bitmap.height; row++) {
92
+ data.set(bitmap.data.subarray(row * bitmap.width, (row + 1) * bitmap.width), (y + row) * size + x);
93
+ }
94
+ Object.assign(shelf, { x: x + bitmap.width, height: Math.max(shelf.height, bitmap.height) });
95
+ texture.needsUpdate = true;
96
+ // Bitmap rows run top down from the texture's v = 0, while the quad's uv runs bottom up
97
+ return [x / size, (y + bitmap.height) / size, bitmap.width / size, -bitmap.height / size];
98
+ }
99
+
100
+ /**
101
+ * Lays out the text centered on the origin, each line centered, as drei's Text (troika-three-text) did with the r3f renderer's line height and
102
+ * spacing
103
+ */
104
+ export function layoutText(atlas: GlyphAtlas, text: string, fontSize: number): ReadonlyArray<GlyphQuad> {
105
+ const lineHeight = LINE_HEIGHT * fontSize;
106
+ const lines = text.split("\n");
107
+ return lines.flatMap((line, i) => {
108
+ const glyphs = Array.from(line, (char) => getGlyph(atlas, char));
109
+ const advances = glyphs.map((g) => (g.advance + LETTER_SPACING) * fontSize);
110
+ const baseline = (lines.length * lineHeight) / 2 - i * lineHeight - (lineHeight - fontSize) / 2 - ASCENT * fontSize;
111
+ const pen = { x: -advances.reduce((sum, a) => sum + a, 0) / 2 };
112
+ return glyphs.flatMap((glyph, j) => {
113
+ const x = pen.x;
114
+ pen.x += advances[j]!;
115
+ return glyph.width === 0
116
+ ? []
117
+ : [
118
+ {
119
+ glyph,
120
+ x: x + (glyph.left + glyph.width / 2) * fontSize,
121
+ y: baseline + (glyph.top - glyph.height / 2) * fontSize,
122
+ width: glyph.width * fontSize,
123
+ height: glyph.height * fontSize,
124
+ },
125
+ ];
126
+ });
127
+ });
128
+ }
@@ -0,0 +1,98 @@
1
+ import type { Object3D, Texture } from "three";
2
+ import type { Group, Relation, Scene, Vec3, View } from "../../../abstract-3d.js";
3
+ import type { MaterialState } from "../appearance/appearance.js";
4
+ import type { BufferZones, CameraOptions } from "../camera/camera.js";
5
+ import type { ControlsOptions } from "../camera/controls.js";
6
+ import type { ControlsHelper } from "../camera/gizmo.js";
7
+ import type { Quality } from "../quality/quality.js";
8
+ import type { MountOverlays } from "./overlays.js";
9
+
10
+ /** The viewer's options, and what passing each of them in an update redoes; telling which actually changed is the caller's, e.g. React's dependencies */
11
+
12
+ export type Popover<Content> = { readonly id: string; readonly pos: Vec3; readonly content: Content };
13
+
14
+ export type SceneEvent = {
15
+ readonly nativeEvent: MouseEvent;
16
+ /** Where the pointer hit, in world space */
17
+ readonly point: Vec3;
18
+ };
19
+
20
+ /** A group's data: of the top-level group, and of the group the hit mesh is in */
21
+ type Data = Record<string, string> | undefined;
22
+
23
+ export type ViewerOptions<Content> = {
24
+ /** Undefined shows the fallback */
25
+ readonly scene?: Scene;
26
+ /** The id of a top-level group, which its hover, selection, material state and events are by; groups without an id can't be clicked */
27
+ readonly createGroupId?: (g: Group) => string;
28
+ readonly selectedIds?: Record<string, boolean>;
29
+ readonly hoveredIds?: ReadonlyArray<string>;
30
+ readonly activeComponents?: Record<string, MaterialState>;
31
+ /**
32
+ * The ids of the relations whose shape is drawn to be clicked, e.g. where a component can be inserted. While defined, even when empty, only the
33
+ * groups whose material state is Accept can be clicked
34
+ */
35
+ readonly activeRelations?: ReadonlyArray<string>;
36
+ readonly showDimensions?: boolean;
37
+ /** View, buffer zones and padding are read when the scene is fitted: first, and for a new camera or canvas size */
38
+ readonly camera?: CameraOptions;
39
+ readonly view?: View;
40
+ readonly bufferZones?: BufferZones;
41
+ readonly fitPadding?: number;
42
+ readonly controls?: ControlsOptions;
43
+ readonly gizmo?: ControlsHelper;
44
+ /** Replace the default lights, a weak ambient light, a key light from the upper left and two fills */
45
+ readonly lights?: ReadonlyArray<Object3D> | ((scene: Scene) => ReadonlyArray<Object3D>);
46
+ /** Lights faces by which way they turn and gives metal reflections: `studio` by default, `room` for three's RoomEnvironment, or null for none */
47
+ readonly environment?: Texture | "studio" | "room" | null;
48
+ readonly environmentIntensity?: number;
49
+ /** `low` by default, without ambient occlusion */
50
+ readonly quality?: Quality;
51
+ readonly useAlphaTest?: boolean;
52
+ readonly materialStateImages?: Record<string, string>;
53
+ readonly popovers?: ReadonlyArray<Popover<Content>>;
54
+ readonly getTooltips?: (id: string) => ReadonlyArray<Popover<Content>>;
55
+ /** Mounts popover and tooltip content, e.g. as React portals; without it, strings and DOM nodes are mounted directly */
56
+ readonly onOverlays?: MountOverlays<Content>;
57
+ /** A built scene's shaders are compiled, so its first frame renders without a stall, e.g. to hide a fallback */
58
+ readonly onLoad?: () => void;
59
+ readonly onClickGroup?: (id: string, rootData: Data, data: Data, event: SceneEvent) => void;
60
+ /** Called with no id when the pointer leaves the group */
61
+ readonly onHoverGroup?: (id: string | undefined, rootData: Data, data: Data, event: SceneEvent) => void;
62
+ readonly onContextMenuGroup?: (id: string, rootData: Data, data: Data, left: number, top: number, event: SceneEvent) => void;
63
+ readonly onClickRelation?: (relation: Relation, event: SceneEvent) => void;
64
+ };
65
+
66
+ /** What an update has to redo, in the order it's redone */
67
+ export type Change = "compile" | "build" | "camera" | "controls" | "gizmo" | "lights" | "quality" | "paint" | "dimensions" | "overlays";
68
+
69
+ /** What passing each option redoes; callbacks and what's read later need nothing */
70
+ export const effects: { readonly [K in keyof ViewerOptions<unknown>]-?: ReadonlyArray<Change> } = {
71
+ scene: ["compile", "build", "lights", "overlays"],
72
+ createGroupId: ["compile", "build"],
73
+ activeComponents: ["build"],
74
+ activeRelations: ["build"],
75
+ useAlphaTest: ["build"],
76
+ materialStateImages: ["build"],
77
+ selectedIds: ["paint"],
78
+ hoveredIds: ["paint"],
79
+ showDimensions: ["dimensions"],
80
+ camera: ["camera"],
81
+ view: [],
82
+ bufferZones: [],
83
+ fitPadding: [],
84
+ controls: ["controls"],
85
+ gizmo: ["gizmo"],
86
+ lights: ["lights"],
87
+ environment: ["lights"],
88
+ environmentIntensity: ["lights"],
89
+ quality: ["quality"],
90
+ popovers: ["overlays"],
91
+ getTooltips: ["overlays"],
92
+ onOverlays: [],
93
+ onLoad: [],
94
+ onClickGroup: ["paint"],
95
+ onHoverGroup: [],
96
+ onContextMenuGroup: [],
97
+ onClickRelation: [],
98
+ };
@@ -0,0 +1,71 @@
1
+ import { Scene, type Vector3 } from "three";
2
+ import { CSS2DObject, CSS2DRenderer } from "three/addons/renderers/CSS2DRenderer.js";
3
+
4
+ /**
5
+ * HTML elements over the canvas at 3D anchors, e.g. popovers and tooltips, placed by three.js's CSS2DRenderer; their content is mounted by the app,
6
+ * or directly
7
+ */
8
+
9
+ export type Overlay<Content> = {
10
+ readonly key: string;
11
+ /** In world space */
12
+ readonly position: Vector3;
13
+ readonly content: Content;
14
+ };
15
+
16
+ export type MountedOverlay<Content> = { readonly key: string; readonly element: HTMLElement; readonly content: Content };
17
+
18
+ /** Mounts the overlays' content into their elements, called when the set of overlays or their content changes */
19
+ export type MountOverlays<Content> = (overlays: ReadonlyArray<MountedOverlay<Content>>) => void;
20
+
21
+ export type OverlayLayer<Content> = {
22
+ readonly renderer: CSS2DRenderer;
23
+ readonly scene: Scene;
24
+ readonly mounted: Map<string, { readonly object: CSS2DObject; overlay: Overlay<Content> }>;
25
+ };
26
+
27
+ export function createOverlayLayer<Content>(container: HTMLElement): OverlayLayer<Content> {
28
+ const renderer = new CSS2DRenderer();
29
+ renderer.domElement.style.cssText = "position:absolute;inset:0;overflow:hidden;pointer-events:none";
30
+ container.append(renderer.domElement);
31
+ return { renderer, scene: new Scene(), mounted: new Map() };
32
+ }
33
+
34
+ /** Without `mount`, strings and DOM nodes are mounted as they are */
35
+ export function setOverlays<Content>(
36
+ layer: OverlayLayer<Content>,
37
+ overlays: ReadonlyArray<Overlay<Content>>,
38
+ mount: MountOverlays<Content> | undefined
39
+ ): void {
40
+ const keys = new Set<string>();
41
+ const changed = { current: false };
42
+ overlays.forEach((overlay) => {
43
+ keys.add(overlay.key);
44
+ const existing = layer.mounted.get(overlay.key);
45
+ changed.current ||= existing?.overlay.content !== overlay.content;
46
+ const object = existing?.object ?? new CSS2DObject(document.createElement("div"));
47
+ object.position.copy(overlay.position);
48
+ if (existing) {
49
+ existing.overlay = overlay;
50
+ } else {
51
+ object.element.style.pointerEvents = "none";
52
+ layer.scene.add(object);
53
+ layer.mounted.set(overlay.key, { object, overlay });
54
+ }
55
+ });
56
+ layer.mounted.forEach(({ object }, key) => {
57
+ if (!keys.has(key)) {
58
+ // Its element is removed with it
59
+ layer.scene.remove(object);
60
+ layer.mounted.delete(key);
61
+ changed.current = true;
62
+ }
63
+ });
64
+ if (changed.current && mount) {
65
+ mount(Array.from(layer.mounted, ([key, { object, overlay }]) => ({ key, element: object.element, content: overlay.content })));
66
+ } else if (changed.current) {
67
+ layer.mounted.forEach(({ object, overlay: { content } }) =>
68
+ object.element.replaceChildren(content instanceof Node ? content : String(content ?? ""))
69
+ );
70
+ }
71
+ }