abstract-3d 3.1.2 → 4.0.0-beta.2

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 (220) 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 +50 -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 +92 -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 +178 -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 +16 -0
  42. package/lib/renderers/three/build/materials.d.ts.map +1 -0
  43. package/lib/renderers/three/build/materials.js +60 -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 +17 -0
  130. package/lib/renderers/three/quality/quality.d.ts.map +1 -0
  131. package/lib/renderers/three/quality/quality.js +27 -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/shadow/shadow.d.ts +15 -0
  142. package/lib/renderers/three/shadow/shadow.d.ts.map +1 -0
  143. package/lib/renderers/three/shadow/shadow.js +81 -0
  144. package/lib/renderers/three/shadow/shadow.js.map +1 -0
  145. package/lib/renderers/three/text/text.d.ts +55 -0
  146. package/lib/renderers/three/text/text.d.ts.map +1 -0
  147. package/lib/renderers/three/text/text.js +90 -0
  148. package/lib/renderers/three/text/text.js.map +1 -0
  149. package/lib/renderers/three/text/text.test.d.ts +2 -0
  150. package/lib/renderers/three/text/text.test.d.ts.map +1 -0
  151. package/lib/renderers/three/text/text.test.js +70 -0
  152. package/lib/renderers/three/text/text.test.js.map +1 -0
  153. package/lib/renderers/three/text.d.ts +53 -0
  154. package/lib/renderers/three/text.d.ts.map +1 -0
  155. package/lib/renderers/three/text.js +151 -0
  156. package/lib/renderers/three/text.js.map +1 -0
  157. package/lib/renderers/three/viewer/options.d.ts +75 -0
  158. package/lib/renderers/three/viewer/options.d.ts.map +1 -0
  159. package/lib/renderers/three/viewer/options.js +33 -0
  160. package/lib/renderers/three/viewer/options.js.map +1 -0
  161. package/lib/renderers/three/viewer/options.test.d.ts +2 -0
  162. package/lib/renderers/three/viewer/options.test.d.ts.map +1 -0
  163. package/lib/renderers/three/viewer/options.test.js +38 -0
  164. package/lib/renderers/three/viewer/options.test.js.map +1 -0
  165. package/lib/renderers/three/viewer/overlays.d.ts +31 -0
  166. package/lib/renderers/three/viewer/overlays.d.ts.map +1 -0
  167. package/lib/renderers/three/viewer/overlays.js +43 -0
  168. package/lib/renderers/three/viewer/overlays.js.map +1 -0
  169. package/lib/renderers/three/viewer/viewer.d.ts +91 -0
  170. package/lib/renderers/three/viewer/viewer.d.ts.map +1 -0
  171. package/lib/renderers/three/viewer/viewer.js +436 -0
  172. package/lib/renderers/three/viewer/viewer.js.map +1 -0
  173. package/lib/renderers/three/viewer.d.ts +38 -0
  174. package/lib/renderers/three/viewer.d.ts.map +1 -0
  175. package/lib/renderers/three/viewer.js +435 -0
  176. package/lib/renderers/three/viewer.js.map +1 -0
  177. package/package.json +34 -16
  178. package/src/renderers/dxf/dxf-encoding/dxf-build.ts +1 -2
  179. package/src/renderers/index.ts +1 -0
  180. package/src/renderers/react/index.ts +2 -5
  181. package/src/renderers/react/react.tsx +97 -181
  182. package/src/renderers/three/appearance/appearance.test.ts +121 -0
  183. package/src/renderers/three/appearance/appearance.ts +135 -0
  184. package/src/renderers/three/build/build.test.ts +141 -0
  185. package/src/renderers/three/build/build.ts +273 -0
  186. package/src/renderers/three/build/materials.ts +91 -0
  187. package/src/renderers/three/build/parts.ts +277 -0
  188. package/src/renderers/three/camera/camera.test.ts +53 -0
  189. package/src/renderers/three/camera/camera.ts +82 -0
  190. package/src/renderers/three/camera/controls.ts +65 -0
  191. package/src/renderers/three/camera/gizmo.ts +93 -0
  192. package/src/renderers/three/compile/compile.test.ts +113 -0
  193. package/src/renderers/three/compile/compile.ts +151 -0
  194. package/src/renderers/three/geometry/geometry.test.ts +95 -0
  195. package/src/renderers/three/geometry/geometry.ts +179 -0
  196. package/src/renderers/three/index.ts +15 -0
  197. package/src/renderers/three/quality/quality.test.ts +37 -0
  198. package/src/renderers/three/quality/quality.ts +35 -0
  199. package/src/renderers/three/shadow/shadow.ts +116 -0
  200. package/src/renderers/three/text/text.test.ts +79 -0
  201. package/src/renderers/three/text/text.ts +128 -0
  202. package/src/renderers/three/viewer/options.ts +104 -0
  203. package/src/renderers/three/viewer/overlays.ts +71 -0
  204. package/src/renderers/three/viewer/viewer.ts +543 -0
  205. package/lib/renderers/react/react-hotspot.d.ts +0 -27
  206. package/lib/renderers/react/react-hotspot.d.ts.map +0 -1
  207. package/lib/renderers/react/react-hotspot.js +0 -37
  208. package/lib/renderers/react/react-hotspot.js.map +0 -1
  209. package/src/custom_typings/react-three-fiber.d.ts +0 -9
  210. package/src/custom_typings/react-three-postprocessing.d.ts +0 -1
  211. package/src/renderers/react/react-camera.tsx +0 -391
  212. package/src/renderers/react/react-dimension.tsx +0 -122
  213. package/src/renderers/react/react-group.test.ts +0 -56
  214. package/src/renderers/react/react-group.tsx +0 -202
  215. package/src/renderers/react/react-image-material.tsx +0 -139
  216. package/src/renderers/react/react-material.tsx +0 -91
  217. package/src/renderers/react/react-mesh.tsx +0 -512
  218. package/src/renderers/react/react-relation.tsx +0 -86
  219. package/src/renderers/react/react-scene.tsx +0 -111
  220. package/src/renderers/react/react-types.ts +0 -9
@@ -0,0 +1,116 @@
1
+ import {
2
+ Box3,
3
+ type Scene,
4
+ Matrix4,
5
+ Mesh,
6
+ MeshBasicMaterial,
7
+ MeshDepthMaterial,
8
+ OrthographicCamera,
9
+ PlaneGeometry,
10
+ ShaderMaterial,
11
+ UniformsUtils,
12
+ Vector3,
13
+ type WebGLRenderer,
14
+ WebGLRenderTarget,
15
+ } from "three";
16
+ import { FullScreenQuad } from "three/addons/postprocessing/Pass.js";
17
+ import { HorizontalBlurShader } from "three/addons/shaders/HorizontalBlurShader.js";
18
+ import { VerticalBlurShader } from "three/addons/shaders/VerticalBlurShader.js";
19
+ import type { BuiltScene } from "../build/build.js";
20
+
21
+ /**
22
+ * A soft shadow on the ground the scene stands on, rendered once per build: the solids seen from below by their depth, darker the closer to the
23
+ * ground, blurred into a texture on a plane, so a frame costs one textured quad. three.js's webgl_shadow_contact example, rendered once as drei's
24
+ * ContactShadows with `frames={1}`
25
+ */
26
+
27
+ export type GroundShadow = { readonly mesh: Mesh<PlaneGeometry, MeshBasicMaterial>; readonly target: WebGLRenderTarget };
28
+
29
+ /** The layer the solids are drawn on for the shadow, so text, lines, images and dimensions don't cast one */
30
+ const shadowLayer = 1;
31
+ /** Of the texture's longer side */
32
+ const resolution = 512;
33
+ /** How far behind (-z) a part's shadow falls per unit of its height, as from a light in front and above */
34
+ const lean = 0.5;
35
+ /** The blur's reach, of the solids' height */
36
+ const softness = 0.15;
37
+ const opacity = 0.8;
38
+
39
+ /** Undefined for a scene without solids; renders with `renderer`, and leaves its render target as it was */
40
+ export function createGroundShadow(renderer: WebGLRenderer, scene: Scene, built: BuiltScene): GroundShadow | undefined {
41
+ const bounds = new Box3();
42
+ built.parts.forEach((part) => {
43
+ if (part.kind === "solid" && part.object.parent !== built.dimensions && part.object.boundingBox) {
44
+ part.object.layers.enable(shadowLayer);
45
+ bounds.union(part.object.boundingBox);
46
+ }
47
+ });
48
+ if (bounds.isEmpty()) {
49
+ return undefined;
50
+ }
51
+ const size = bounds.getSize(new Vector3());
52
+ // The plane reaches past the solids by twice the blur's reach, and behind them as far as their tops' shadows fall
53
+ const [reach, behind] = [softness * size.y, lean * size.y];
54
+ const [width, depth] = [size.x + reach * 4, size.z + reach * 4 + behind];
55
+ const center = new Vector3((bounds.min.x + bounds.max.x) / 2, bounds.min.y, (bounds.min.z + bounds.max.z - behind) / 2);
56
+ const [texelsX, texelsZ] =
57
+ width > depth ? [resolution, Math.ceil((resolution * depth) / width)] : [Math.ceil((resolution * width) / depth), resolution];
58
+ const [target, blurred] = [new WebGLRenderTarget(texelsX, texelsZ), new WebGLRenderTarget(texelsX, texelsZ)];
59
+
60
+ // Looks up from the ground, seeing as far as the solids are high; the shear moves what's at height h back by lean·h. Up is +z on its screen, and
61
+ // a height h is -h in its view space
62
+ const camera = new OrthographicCamera(-width / 2, width / 2, depth / 2, -depth / 2, 0, size.y);
63
+ camera.position.copy(center);
64
+ camera.rotation.x = Math.PI / 2;
65
+ camera.layers.set(shadowLayer);
66
+ camera.updateMatrixWorld();
67
+ camera.projectionMatrix.multiply(new Matrix4().set(1, 0, 0, 0, 0, 1, lean, 0, 0, 0, 1, 0, 0, 0, 0, 1));
68
+ // Depth tested, so at each point the surface closest to the ground darkens it, not the last one drawn
69
+ const darkness = new MeshDepthMaterial();
70
+ darkness.onBeforeCompile = (shader) => {
71
+ shader.fragmentShader = shader.fragmentShader.replace(
72
+ "gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );",
73
+ "gl_FragColor = vec4( vec3( 0.0 ), 1.0 - fragCoordZ );"
74
+ );
75
+ };
76
+ const previous = renderer.getRenderTarget();
77
+ Object.assign(scene, { overrideMaterial: darkness });
78
+ renderer.setRenderTarget(target);
79
+ renderer.render(scene, camera);
80
+ Object.assign(scene, { overrideMaterial: null });
81
+
82
+ const horizontal = new ShaderMaterial({ ...HorizontalBlurShader, uniforms: UniformsUtils.clone(HorizontalBlurShader.uniforms) });
83
+ const vertical = new ShaderMaterial({ ...VerticalBlurShader, uniforms: UniformsUtils.clone(VerticalBlurShader.uniforms) });
84
+ const quad = new FullScreenQuad(horizontal);
85
+ // The blur shaders reach 4 steps; three passes of shrinking steps blend into one smooth falloff
86
+ const step = (reach * texelsX) / width / 4;
87
+ [step, step / 2, step / 4].forEach((amount) => {
88
+ Object.assign(horizontal.uniforms, { tDiffuse: { value: target.texture }, h: { value: amount / texelsX } });
89
+ quad.material = horizontal;
90
+ renderer.setRenderTarget(blurred);
91
+ quad.render(renderer);
92
+ Object.assign(vertical.uniforms, { tDiffuse: { value: blurred.texture }, v: { value: amount / texelsZ } });
93
+ quad.material = vertical;
94
+ renderer.setRenderTarget(target);
95
+ quad.render(renderer);
96
+ });
97
+ renderer.setRenderTarget(previous);
98
+ [blurred, darkness, horizontal, vertical, quad].forEach((d) => d.dispose());
99
+
100
+ // The plane faces down as the camera looked up, so its texture lines up, and is mirrored to face up
101
+ const mesh = new Mesh(
102
+ new PlaneGeometry(width, depth).rotateX(Math.PI / 2),
103
+ new MeshBasicMaterial({ map: target.texture, transparent: true, depthWrite: false, opacity })
104
+ );
105
+ mesh.position.copy(center);
106
+ mesh.scale.y = -1;
107
+ scene.add(mesh);
108
+ return { mesh, target };
109
+ }
110
+
111
+ export function disposeGroundShadow(shadow: GroundShadow): void {
112
+ shadow.mesh.removeFromParent();
113
+ shadow.mesh.geometry.dispose();
114
+ shadow.mesh.material.dispose();
115
+ shadow.target.dispose();
116
+ }
@@ -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,104 @@
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
+ /** A soft shadow on the ground under the scene, rendered once per build; on by default */
50
+ readonly groundShadow?: boolean;
51
+ /** The resolution: `low` 1, `medium` up to 1.5 and `high` up to 2 pixels per CSS pixel; `auto` by default, high until moving gets slow */
52
+ readonly quality?: Quality;
53
+ readonly useAlphaTest?: boolean;
54
+ readonly materialStateImages?: Record<string, string>;
55
+ readonly popovers?: ReadonlyArray<Popover<Content>>;
56
+ readonly getTooltips?: (id: string) => ReadonlyArray<Popover<Content>>;
57
+ /** Mounts popover and tooltip content, e.g. as React portals; without it, strings and DOM nodes are mounted directly */
58
+ readonly onOverlays?: MountOverlays<Content>;
59
+ /** A built scene's shaders are compiled, so its first frame renders without a stall, e.g. to hide a fallback */
60
+ readonly onLoad?: () => void;
61
+ readonly onClickGroup?: (id: string, rootData: Data, data: Data, event: SceneEvent) => void;
62
+ /** Called with no id when the pointer leaves the group */
63
+ readonly onHoverGroup?: (id: string | undefined, rootData: Data, data: Data, event: SceneEvent) => void;
64
+ readonly onContextMenuGroup?: (id: string, rootData: Data, data: Data, left: number, top: number, event: SceneEvent) => void;
65
+ readonly onClickRelation?: (relation: Relation, event: SceneEvent) => void;
66
+ /** A click, not a drag, on nothing clickable, e.g. the background; r3f's Canvas event of the same name */
67
+ readonly onPointerMissed?: (event: MouseEvent) => void;
68
+ };
69
+
70
+ /** What an update has to redo, in the order it's redone */
71
+ export type Change = "compile" | "build" | "shadow" | "camera" | "controls" | "gizmo" | "lights" | "quality" | "paint" | "dimensions" | "overlays";
72
+
73
+ /** What passing each option redoes; callbacks and what's read later need nothing */
74
+ export const effects: { readonly [K in keyof ViewerOptions<unknown>]-?: ReadonlyArray<Change> } = {
75
+ scene: ["compile", "build", "lights", "overlays"],
76
+ createGroupId: ["compile", "build"],
77
+ activeComponents: ["build"],
78
+ activeRelations: ["build"],
79
+ useAlphaTest: ["build"],
80
+ materialStateImages: ["build"],
81
+ selectedIds: ["paint"],
82
+ hoveredIds: ["paint"],
83
+ showDimensions: ["dimensions"],
84
+ camera: ["camera"],
85
+ view: [],
86
+ bufferZones: [],
87
+ fitPadding: [],
88
+ controls: ["controls"],
89
+ gizmo: ["gizmo"],
90
+ lights: ["lights"],
91
+ environment: ["lights"],
92
+ environmentIntensity: ["lights"],
93
+ groundShadow: ["shadow"],
94
+ quality: ["quality"],
95
+ popovers: ["overlays"],
96
+ getTooltips: ["overlays"],
97
+ onOverlays: [],
98
+ onLoad: [],
99
+ onClickGroup: ["paint"],
100
+ onHoverGroup: [],
101
+ onContextMenuGroup: [],
102
+ onClickRelation: [],
103
+ onPointerMissed: [],
104
+ };
@@ -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
+ }