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,277 @@
1
+ import {
2
+ BatchedMesh,
3
+ type BufferGeometry,
4
+ FrontSide,
5
+ InstancedBufferAttribute,
6
+ InstancedMesh,
7
+ type InterleavedBuffer,
8
+ type InterleavedBufferAttribute,
9
+ type Intersection,
10
+ type Material,
11
+ Matrix4,
12
+ Mesh,
13
+ type MeshBasicMaterial,
14
+ PlaneGeometry,
15
+ ShaderMaterial,
16
+ Vector3,
17
+ type Vector4,
18
+ } from "three";
19
+ import { LineMaterial } from "three/addons/lines/LineMaterial.js";
20
+ import { LineSegments2 } from "three/addons/lines/LineSegments2.js";
21
+ import { LineSegmentsGeometry } from "three/addons/lines/LineSegmentsGeometry.js";
22
+ import type { Vec3 } from "../../../abstract-3d.js";
23
+ import type { GlyphAtlas } from "../text/text.js";
24
+ import { layoutText, SDF_EDGE } from "../text/text.js";
25
+
26
+ /** The drawn objects: solids of a material class in one batch, all text in one instanced mesh, lines per thickness, images as meshes; each item a slot */
27
+
28
+ export type Segment = {
29
+ readonly start: Vec3;
30
+ readonly end: Vec3;
31
+ /** The front facing direction of a culled line, hidden while it faces away from the camera */
32
+ readonly normal?: Vec3;
33
+ };
34
+
35
+ export type TextEntry = { readonly text: string; readonly fontSize: number; readonly local: Matrix4 };
36
+
37
+ export type Part =
38
+ | { readonly kind: "solid"; readonly object: BatchedMesh; readonly locals: ReadonlyArray<Matrix4> }
39
+ | {
40
+ readonly kind: "text";
41
+ readonly object: InstancedMesh;
42
+ readonly locals: ReadonlyArray<Matrix4>;
43
+ readonly glyphs: ReadonlyArray<Matrix4>;
44
+ /** The first glyph of each slot, and one past the last */
45
+ readonly firsts: ReadonlyArray<number>;
46
+ readonly slotOfGlyph: Int32Array;
47
+ readonly colors: InstancedBufferAttribute;
48
+ }
49
+ | {
50
+ readonly kind: "line";
51
+ readonly object: LineSegments2;
52
+ readonly segments: ReadonlyArray<Segment>;
53
+ /** World start, end and normal of each segment, while the drawn positions collapse hidden ones */
54
+ readonly world: Float32Array;
55
+ readonly visible: Uint8Array;
56
+ readonly positions: InterleavedBuffer;
57
+ readonly colors: InterleavedBuffer;
58
+ }
59
+ | { readonly kind: "image"; readonly object: Mesh<BufferGeometry, MeshBasicMaterial>; readonly local: Matrix4 };
60
+
61
+ /** Per-instance color for hover and selection, as three.js's BatchedMesh examples color instances */
62
+ export function createSolidPart(entries: ReadonlyArray<{ readonly geometry: BufferGeometry; readonly local: Matrix4 }>, material: Material): Part {
63
+ const geometries = [...new Set(entries.map((e) => e.geometry))];
64
+ const vertexCount = geometries.reduce((sum, g) => sum + g.getAttribute("position").count, 0);
65
+ const batch = new BatchedMesh(
66
+ entries.length,
67
+ vertexCount,
68
+ geometries.reduce((sum, g) => sum + g.index!.count, 0),
69
+ material
70
+ );
71
+ const ids = new Map(geometries.map((g) => [g, batch.addGeometry(g)]));
72
+ entries.forEach((e) => batch.addInstance(ids.get(e.geometry)!));
73
+ return { kind: "solid", object: batch, locals: entries.map((e) => e.local) };
74
+ }
75
+
76
+ export function createTextPart(entries: ReadonlyArray<TextEntry>, atlas: GlyphAtlas): Part {
77
+ const layouts = entries.map((e) => layoutText(atlas, e.text, e.fontSize));
78
+ const quads = layouts.flat();
79
+ const geometry = new PlaneGeometry();
80
+ geometry.setAttribute("glyphRect", new InstancedBufferAttribute(Float32Array.from(quads.flatMap((q) => q.glyph.rect)), 4));
81
+ const colors = new InstancedBufferAttribute(new Float32Array(quads.length * 4), 4);
82
+ geometry.setAttribute("glyphColor", colors);
83
+ // Glyphs colored per instance, one draw call as troika's BatchedText; edges antialiased by the distance field's fwidth, as troika does
84
+ const material = new ShaderMaterial({
85
+ side: FrontSide,
86
+ transparent: true,
87
+ depthWrite: false,
88
+ uniforms: { atlas: { value: atlas.texture } },
89
+ defines: { SDF_EDGE },
90
+ vertexShader: textVertexShader,
91
+ fragmentShader: textFragmentShader,
92
+ });
93
+ const mesh = new InstancedMesh(geometry, material, quads.length);
94
+ // Drawn after the opaque batches, and never culled as a whole, as its glyphs move with their items
95
+ Object.assign(mesh, { renderOrder: 1, frustumCulled: false });
96
+ return {
97
+ kind: "text",
98
+ object: mesh,
99
+ locals: entries.map((e) => e.local),
100
+ glyphs: quads.map((q) => new Matrix4().makeTranslation(q.x, q.y, 0).scale(new Vector3(q.width, q.height, 1))),
101
+ firsts: layouts.reduce<ReadonlyArray<number>>((acc, l, i) => [...acc, acc[i]! + l.length], [0]),
102
+ slotOfGlyph: Int32Array.from(layouts.flatMap((l, slot) => l.map(() => slot))),
103
+ colors,
104
+ };
105
+ }
106
+
107
+ export function createLinePart(segments: ReadonlyArray<Segment>, thickness: number): Part {
108
+ const geometry = new LineSegmentsGeometry().setPositions(new Float32Array(segments.length * 6)).setColors(new Float32Array(segments.length * 6));
109
+ const line = new LineSegments2(geometry, new LineMaterial({ linewidth: thickness, vertexColors: true }));
110
+ // Positions change after the geometry's bounds were computed
111
+ line.frustumCulled = false;
112
+ return {
113
+ kind: "line",
114
+ object: line,
115
+ segments,
116
+ world: new Float32Array(segments.length * 9),
117
+ visible: new Uint8Array(segments.length).fill(1),
118
+ positions: (geometry.getAttribute("instanceStart") as InterleavedBufferAttribute).data,
119
+ colors: (geometry.getAttribute("instanceColorStart") as InterleavedBufferAttribute).data,
120
+ };
121
+ }
122
+
123
+ /** Hidden until its texture is set */
124
+ export function createImagePart(geometry: BufferGeometry, local: Matrix4, material: MeshBasicMaterial): Extract<Part, { readonly kind: "image" }> {
125
+ const mesh = new Mesh(geometry, material);
126
+ Object.assign(mesh, { matrixAutoUpdate: false, visible: false });
127
+ return { kind: "image", object: mesh, local };
128
+ }
129
+
130
+ /** Places the slot's item by its world matrix */
131
+ export function placeSlot(part: Part, slot: number, matrix: Matrix4): void {
132
+ switch (part.kind) {
133
+ case "solid":
134
+ part.object.setMatrixAt(slot, scratch.multiplyMatrices(matrix, part.locals[slot]!));
135
+ break;
136
+ case "text": {
137
+ const text = scratch.multiplyMatrices(matrix, part.locals[slot]!);
138
+ for (let i = part.firsts[slot]!; i < part.firsts[slot + 1]!; i++) {
139
+ part.object.setMatrixAt(i, glyph.multiplyMatrices(text, part.glyphs[i]!));
140
+ }
141
+ break;
142
+ }
143
+ case "line": {
144
+ const { start, end, normal } = part.segments[slot]!;
145
+ part.world.set(vector.set(start.x, start.y, start.z).applyMatrix4(matrix).toArray(), slot * 9);
146
+ part.world.set(vector.set(end.x, end.y, end.z).applyMatrix4(matrix).toArray(), slot * 9 + 3);
147
+ part.world.set(normal ? vector.set(normal.x, normal.y, normal.z).transformDirection(matrix).toArray() : [0, 0, 0], slot * 9 + 6);
148
+ writeSegment(part, slot);
149
+ break;
150
+ }
151
+ default:
152
+ part.object.matrix.multiplyMatrices(matrix, part.local);
153
+ part.object.matrixWorldNeedsUpdate = true;
154
+ }
155
+ }
156
+
157
+ export function paintSlot(part: Part, slot: number, rgba: Vector4): void {
158
+ switch (part.kind) {
159
+ case "solid":
160
+ part.object.setColorAt(slot, rgba);
161
+ break;
162
+ case "text":
163
+ for (let i = part.firsts[slot]!; i < part.firsts[slot + 1]!; i++) {
164
+ part.colors.setXYZW(i, rgba.x, rgba.y, rgba.z, rgba.w);
165
+ }
166
+ break;
167
+ case "line":
168
+ part.colors.array.set([rgba.x, rgba.y, rgba.z, rgba.x, rgba.y, rgba.z], slot * 6);
169
+ break;
170
+ default:
171
+ part.object.material.color.setRGB(rgba.x, rgba.y, rgba.z);
172
+ part.object.material.opacity = rgba.w;
173
+ }
174
+ }
175
+
176
+ /** Uploads what placing and painting changed, and the bounds for culling and picking when slots moved */
177
+ export function flushPart(part: Part, moved: boolean): void {
178
+ switch (part.kind) {
179
+ case "solid":
180
+ if (moved) {
181
+ part.object.computeBoundingBox();
182
+ part.object.computeBoundingSphere();
183
+ }
184
+ break;
185
+ case "text":
186
+ part.object.instanceMatrix.needsUpdate = true;
187
+ part.colors.needsUpdate = true;
188
+ part.object.boundingSphere = null;
189
+ break;
190
+ case "line":
191
+ part.positions.needsUpdate = true;
192
+ part.colors.needsUpdate = true;
193
+ break;
194
+ default:
195
+ }
196
+ }
197
+
198
+ /** The slot of a hit on the part's object; lines aren't picked */
199
+ export function getHitSlot(part: Part, hit: Intersection): number | undefined {
200
+ switch (part.kind) {
201
+ case "solid":
202
+ return hit.batchId;
203
+ case "text":
204
+ return hit.instanceId === undefined ? undefined : part.slotOfGlyph[hit.instanceId];
205
+ case "line":
206
+ return undefined;
207
+ default:
208
+ return 0;
209
+ }
210
+ }
211
+
212
+ /** Hides the culled lines facing away from a camera at `cameraPosition`; the r3f renderer's culled line shader's test, on the CPU per camera move */
213
+ export function cullLines(part: Part, cameraPosition: Vector3): void {
214
+ if (part.kind !== "line") {
215
+ return;
216
+ }
217
+ part.segments.forEach((segment, slot) => {
218
+ const w = slot * 9;
219
+ const mid = vector
220
+ .fromArray(part.world, w)
221
+ .add(normal.fromArray(part.world, w + 3))
222
+ .multiplyScalar(0.5);
223
+ const facing = !segment.normal || normal.fromArray(part.world, w + 6).dot(mid.subVectors(cameraPosition, mid)) > 0 ? 1 : 0;
224
+ if (facing !== part.visible[slot]) {
225
+ part.visible[slot] = facing;
226
+ writeSegment(part, slot);
227
+ part.positions.needsUpdate = true;
228
+ }
229
+ });
230
+ }
231
+
232
+ export function disposePart(part: Part): void {
233
+ if (part.kind === "solid") {
234
+ part.object.dispose();
235
+ } else if (part.kind !== "image") {
236
+ part.object.geometry.dispose();
237
+ }
238
+ const materials = part.object.material;
239
+ (Array.isArray(materials) ? materials : [materials]).forEach((m: Material) => m.dispose());
240
+ }
241
+
242
+ /** A hidden segment collapses to its start */
243
+ function writeSegment(part: Extract<Part, { readonly kind: "line" }>, slot: number): void {
244
+ const w = slot * 9;
245
+ part.positions.array.set(
246
+ part.visible[slot] ? part.world.subarray(w, w + 6) : [...part.world.subarray(w, w + 3), ...part.world.subarray(w, w + 3)],
247
+ slot * 6
248
+ );
249
+ }
250
+
251
+ const textVertexShader = /* glsl */ `
252
+ attribute vec4 glyphRect;
253
+ attribute vec4 glyphColor;
254
+ varying vec2 vUv;
255
+ varying vec4 vColor;
256
+ void main() {
257
+ vUv = glyphRect.xy + uv * glyphRect.zw;
258
+ vColor = glyphColor;
259
+ gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1.0);
260
+ }`;
261
+
262
+ const textFragmentShader = /* glsl */ `
263
+ uniform sampler2D atlas;
264
+ varying vec2 vUv;
265
+ varying vec4 vColor;
266
+ void main() {
267
+ float distance = texture2D(atlas, vUv).r;
268
+ float edge = fwidth(distance) * 0.7;
269
+ gl_FragColor = vec4(vColor.rgb, vColor.a * smoothstep(SDF_EDGE - edge, SDF_EDGE + edge, distance));
270
+ #include <tonemapping_fragment>
271
+ #include <colorspace_fragment>
272
+ }`;
273
+
274
+ const scratch = new Matrix4();
275
+ const glyph = new Matrix4();
276
+ const vector = new Vector3();
277
+ const normal = new Vector3();
@@ -0,0 +1,53 @@
1
+ import { OrthographicCamera, PerspectiveCamera } from "three";
2
+ import { describe, expect, it } from "vitest";
3
+ import { vec3 } from "../../../abstract-3d.js";
4
+ import type { FitInput } from "./camera.js";
5
+ import { fitCamera } from "./camera.js";
6
+
7
+ const input: FitInput = { view: "front", size: vec3(4000, 2000, 1000), canvas: { width: 800, height: 400 }, bufferZones: {}, fitPadding: 0 };
8
+ const tan = Math.tan((45 * Math.PI) / 360);
9
+ const fitPerspective = (partial: Partial<FitInput> = {}): readonly [PerspectiveCamera, ReturnType<typeof fitCamera>] => {
10
+ const camera = new PerspectiveCamera(45);
11
+ return [camera, fitCamera(camera, { ...input, ...partial })];
12
+ };
13
+ const fitOrthographic = (partial: Partial<FitInput> = {}): ReadonlyArray<number> => {
14
+ const camera = new OrthographicCamera();
15
+ fitCamera(camera, { ...input, ...partial });
16
+ return [camera.left, camera.right, camera.top, camera.bottom, camera.zoom];
17
+ };
18
+
19
+ describe("fitCamera", () => {
20
+ it("fits nothing into an empty canvas", () => {
21
+ expect(fitPerspective({ canvas: { width: 0, height: 400 } })[1]).toBeUndefined();
22
+ });
23
+
24
+ it("backs a perspective camera off until the scene's width and height fit, plus half its depth", () => {
25
+ const [camera, placement] = fitPerspective();
26
+ expect(placement!.distance).toBeCloseTo(Math.max(1000 / tan, 2000 / (tan * 2)) + 500);
27
+ expect(placement!.direction).toEqual(vec3(0, 0, 1));
28
+ expect(camera.aspect).toBe(2);
29
+ });
30
+
31
+ it("pads the distance", () => {
32
+ expect(fitPerspective({ fitPadding: 0.1 })[1]!.distance).toBeCloseTo(fitPerspective()[1]!.distance * 1.1);
33
+ });
34
+
35
+ it("fits into the area inside the buffer zones, and offsets the view to it", () => {
36
+ const [camera] = fitPerspective({ bufferZones: { left: 200, top: 100 } });
37
+ expect(camera.aspect).toBeCloseTo(600 / 300);
38
+ expect(camera.view).toMatchObject({ enabled: true, fullWidth: 600, fullHeight: 300, offsetX: -200, offsetY: -100, width: 800, height: 400 });
39
+ });
40
+
41
+ it("looks along each view, with the scene's size seen from it", () => {
42
+ expect(fitPerspective({ view: "top" })[1]!.direction).toEqual(vec3(0, 1, 0));
43
+ expect(fitPerspective({ view: "left" })[1]!.direction).toEqual(vec3(-1, 0, 0));
44
+ // From the top, the scene is 4000 wide and 1000 high on screen, and 2000 deep
45
+ expect(fitPerspective({ view: "top" })[1]!.distance).toBeCloseTo(Math.max(500 / tan, 2000 / (tan * 2)) + 1000);
46
+ });
47
+
48
+ it("frames orthographic cameras around the scene, extended by the buffer zones", () => {
49
+ expect(fitOrthographic()).toEqual([-2000, 2000, 1000, -1000, 1]);
50
+ // The usable area is 400 x 400, so the scene's 4000 width sets 10 world units per pixel
51
+ expect(fitOrthographic({ bufferZones: { right: 400 } })).toEqual([-2000, 6000, 2000, -2000, 1]);
52
+ });
53
+ });
@@ -0,0 +1,82 @@
1
+ import type { OrthographicCamera, PerspectiveCamera } from "three";
2
+ import { exhaustiveCheck } from "ts-exhaustive-check";
3
+ import type { Vec3, View } from "../../../abstract-3d.js";
4
+ import { vec3 } from "../../../abstract-3d.js";
5
+
6
+ /** Fitting the camera to the scene, inside buffer zones of the canvas that UI covers; the fit math of the r3f renderer's react-camera.tsx */
7
+
8
+ export type CameraOptions =
9
+ | { readonly type: "Perspective"; readonly near?: number; readonly far?: number; readonly fov?: number }
10
+ | { readonly type: "Orthographic"; readonly near?: number; readonly far?: number };
11
+
12
+ /** Margins of the canvas, in CSS pixels, that the scene is fitted inside of */
13
+ export type BufferZones = { readonly left?: number; readonly right?: number; readonly top?: number; readonly bottom?: number };
14
+
15
+ export type FitInput = {
16
+ readonly view: View;
17
+ /** The scene's size */
18
+ readonly size: Vec3;
19
+ /** In CSS pixels */
20
+ readonly canvas: { readonly width: number; readonly height: number };
21
+ readonly bufferZones: BufferZones;
22
+ readonly fitPadding: number;
23
+ };
24
+
25
+ /** Where to place the camera: its distance from the target, and the direction from the target to it */
26
+ export type Placement = { readonly distance: number; readonly direction: Vec3 };
27
+
28
+ /** Sets the camera's projection to fit the scene into the usable area of the canvas, and returns where to place it */
29
+ export function fitCamera(
30
+ camera: PerspectiveCamera | OrthographicCamera,
31
+ { view, size: sceneSize, canvas, bufferZones, fitPadding }: FitInput
32
+ ): Placement | undefined {
33
+ if (canvas.width === 0 || canvas.height === 0) {
34
+ return undefined;
35
+ }
36
+ const [direction, size, sceneAspect] = getViewTransform(view, sceneSize);
37
+ const { left = 0, right = 0, top = 0, bottom = 0 } = bufferZones;
38
+ const usableW = Math.max(1, canvas.width - left - right);
39
+ const usableH = Math.max(1, canvas.height - top - bottom);
40
+ const usableAspect = usableW / usableH;
41
+ camera.zoom = 1;
42
+
43
+ if ("isOrthographicCamera" in camera) {
44
+ const pad = 1 + fitPadding;
45
+ // Constrained by the scene's width when it's wider than the usable area, else by its height
46
+ const [halfW, halfH] =
47
+ sceneAspect > usableAspect
48
+ ? [(size.x / 2) * pad, (size.x / 2 / usableAspect) * pad]
49
+ : [((usableAspect * size.y) / 2) * pad, (size.y / 2) * pad];
50
+ // Extended by the buffer zones, at the world units per CSS pixel of the usable area
51
+ const [wpp, hpp] = [(halfW * 2) / usableW, (halfH * 2) / usableH];
52
+ Object.assign(camera, { left: -halfW - left * wpp, right: halfW + right * wpp, top: halfH + top * hpp, bottom: -halfH - bottom * hpp });
53
+ camera.updateProjectionMatrix();
54
+ const fovTan = Math.tan((45 * Math.PI) / 360);
55
+ return { distance: size.z * 0.5 + Math.max(size.x, size.y) * 2 * fovTan, direction };
56
+ }
57
+
58
+ // The field of view describes the usable area; the view offset extends the rendered frustum from it to the full canvas
59
+ const tan = Math.tan((camera.fov * Math.PI) / 360);
60
+ const distance = Math.max(size.y / 2 / tan, size.x / 2 / (tan * usableAspect)) + size.z * 0.5;
61
+ camera.aspect = usableAspect;
62
+ camera.setViewOffset(usableW, usableH, -left, -top, canvas.width, canvas.height);
63
+ camera.updateProjectionMatrix();
64
+ return { distance: distance * (1 + fitPadding), direction };
65
+ }
66
+
67
+ /** The direction to look from, the scene's size as seen on screen and its aspect */
68
+ function getViewTransform(view: View, size: Vec3): readonly [Vec3, Vec3, number] {
69
+ switch (view) {
70
+ case "front":
71
+ case "back":
72
+ return [vec3(0, 0, view === "front" ? 1 : -1), size, size.x / size.y];
73
+ case "top":
74
+ case "bottom":
75
+ return [vec3(0, view === "top" ? 1 : -1, 0), vec3(size.x, size.z, size.y), size.x / size.z];
76
+ case "right":
77
+ case "left":
78
+ return [vec3(view === "right" ? 1 : -1, 0, 0), vec3(size.z, size.y, size.x), size.z / size.y];
79
+ default:
80
+ return exhaustiveCheck(view);
81
+ }
82
+ }
@@ -0,0 +1,65 @@
1
+ import type { OrthographicCamera, PerspectiveCamera, Vector3 } from "three";
2
+ import { OrbitControls } from "three/addons/controls/OrbitControls.js";
3
+
4
+ /** The r3f renderer's OrbitControls, with drei's damping default */
5
+
6
+ export type ControlsOptions = Partial<Omit<OrbitControls, "object" | "domElement" | "target" | "cursor">> & {
7
+ readonly onStart?: () => void;
8
+ readonly onChange?: () => void;
9
+ readonly onEnd?: () => void;
10
+ };
11
+
12
+ export type Controls = {
13
+ readonly orbit: OrbitControls;
14
+ readonly current: { options: ControlsOptions };
15
+ /** The user drags the camera, and it moved since the drag started */
16
+ readonly drag: { active: boolean; moved: boolean };
17
+ };
18
+
19
+ export function createControls(
20
+ camera: PerspectiveCamera | OrthographicCamera,
21
+ element: HTMLElement,
22
+ options: ControlsOptions,
23
+ onInput: () => void
24
+ ): Controls {
25
+ const controls: Controls = { orbit: new OrbitControls(camera, element), current: { options }, drag: { active: false, moved: false } };
26
+ configureControls(controls, options);
27
+ controls.orbit.addEventListener("start", () => onDragStart(controls, onInput));
28
+ controls.orbit.addEventListener("change", () => onDragChange(controls, onInput));
29
+ controls.orbit.addEventListener("end", () => onDragEnd(controls));
30
+ return controls;
31
+ }
32
+
33
+ /** Sets the options that are defined, so ones left out keep their defaults */
34
+ export function configureControls(controls: Controls, options: ControlsOptions): void {
35
+ controls.current.options = options;
36
+ controls.orbit.enableDamping = true;
37
+ Object.entries(options as Record<string, unknown>).forEach(([key, value]) => {
38
+ if (value !== undefined && typeof value !== "function") {
39
+ Object.assign(controls.orbit, { [key]: value });
40
+ }
41
+ });
42
+ }
43
+
44
+ export function setPose(controls: Controls, position: Vector3, target: Vector3): void {
45
+ controls.orbit.target.copy(target);
46
+ controls.orbit.object.position.copy(position);
47
+ controls.orbit.update();
48
+ }
49
+
50
+ function onDragStart(controls: Controls, onInput: () => void): void {
51
+ Object.assign(controls.drag, { active: true, moved: false });
52
+ controls.current.options.onStart?.();
53
+ onInput();
54
+ }
55
+
56
+ function onDragChange(controls: Controls, onInput: () => void): void {
57
+ controls.drag.moved = controls.drag.active;
58
+ controls.current.options.onChange?.();
59
+ onInput();
60
+ }
61
+
62
+ function onDragEnd(controls: Controls): void {
63
+ Object.assign(controls.drag, { active: false, moved: false });
64
+ controls.current.options.onEnd?.();
65
+ }
@@ -0,0 +1,93 @@
1
+ import type { OrthographicCamera, PerspectiveCamera, WebGLRenderer } from "three";
2
+ import type { GizmoAxisOptions, GizmoOptions } from "three-viewport-gizmo";
3
+ import { ViewportGizmo } from "three-viewport-gizmo";
4
+
5
+ /**
6
+ * A viewcube or axes in a corner of the canvas that turns with the camera; clicking it turns the camera to look from that side. three-viewport-gizmo,
7
+ * with the options of drei's GizmoViewcube and GizmoViewport that the r3f renderer took
8
+ */
9
+
10
+ export type GizmoAlignment = `${"top" | "center" | "bottom"}-${"left" | "center" | "right"}`;
11
+
12
+ export type ViewcubeOptions = {
13
+ /** A CSS font; its weight and family are used */
14
+ readonly font?: string;
15
+ readonly opacity?: number;
16
+ readonly color?: string;
17
+ readonly hoverColor?: string;
18
+ readonly textColor?: string;
19
+ readonly strokeColor?: string;
20
+ /** Labels of the +x, -x, +y, -y, +z and -z faces */
21
+ readonly faces?: ReadonlyArray<string>;
22
+ /** Clicking also moves the target back to the scene's center and fits the scene */
23
+ readonly resetZoomAndPanOnClick?: boolean;
24
+ };
25
+
26
+ export type AxesOptions = {
27
+ readonly axisColors?: readonly [string, string, string];
28
+ readonly labels?: readonly [string, string, string];
29
+ readonly labelColor?: string;
30
+ readonly hideNegativeAxes?: boolean;
31
+ readonly disabled?: boolean;
32
+ readonly font?: string;
33
+ readonly resetZoomAndPanOnClick?: boolean;
34
+ };
35
+
36
+ export type ControlsHelper = (
37
+ | { readonly type: "Viewcube"; readonly viewcubeProps?: ViewcubeOptions }
38
+ | { readonly type: "Viewport"; readonly viewportProps?: AxesOptions }
39
+ ) & {
40
+ /** The margin is from the canvas' corner to the gizmo's center, in CSS pixels */
41
+ readonly props?: { readonly alignment?: GizmoAlignment; readonly margin?: readonly [number, number] };
42
+ };
43
+
44
+ const size = 110;
45
+ const axes = ["x", "nx", "y", "ny", "z", "nz"] as const;
46
+
47
+ export function createGizmo(
48
+ camera: PerspectiveCamera | OrthographicCamera,
49
+ renderer: WebGLRenderer,
50
+ container: HTMLElement,
51
+ helper: ControlsHelper
52
+ ): ViewportGizmo {
53
+ const [mx, my] = helper.props?.margin ?? [80, 80];
54
+ const options = helper.type === "Viewcube" ? helper.viewcubeProps : helper.viewportProps;
55
+ // "600 24px Helvetica, Arial" is weight 600 of Helvetica, Arial
56
+ const [, weight = 600, family = "Helvetica, Arial, sans-serif"] = /^(?:(\S+)\s+)?[\d.]+px\s+(.+)$/.exec(options?.font ?? "") ?? [];
57
+ const gizmo = new ViewportGizmo(camera, renderer, {
58
+ container,
59
+ size,
60
+ placement: helper.props?.alignment ?? "bottom-right",
61
+ offset: { left: mx - size / 2, right: mx - size / 2, top: my - size / 2, bottom: my - size / 2 },
62
+ font: { family, weight },
63
+ ...(helper.type === "Viewcube" ? getViewcubeOptions(helper.viewcubeProps ?? {}) : getAxesOptions(helper.viewportProps ?? {})),
64
+ });
65
+ gizmo.enabled = !(helper.type === "Viewport" && helper.viewportProps?.disabled);
66
+ return gizmo;
67
+ }
68
+
69
+ function getViewcubeOptions(options: ViewcubeOptions): GizmoOptions {
70
+ const { opacity = 1, color = "#f4f5f7", hoverColor = "#cfe0ff", textColor = "#333", strokeColor = "#9aa0a6" } = options;
71
+ const labels = options.faces ?? ["Right", "Left", "Top", "Bottom", "Front", "Back"];
72
+ const face: GizmoAxisOptions = { color, opacity, labelColor: textColor, hover: { color: hoverColor, labelColor: textColor, opacity } };
73
+ return {
74
+ type: "cube",
75
+ background: { color: strokeColor, opacity, hover: { color: strokeColor, opacity } },
76
+ corners: { color, hover: { color: hoverColor } },
77
+ edges: { color, hover: { color: hoverColor } },
78
+ ...Object.fromEntries(axes.map((axis, i) => [axis, { ...face, label: (labels[i] ?? "").toUpperCase() }])),
79
+ };
80
+ }
81
+
82
+ function getAxesOptions(options: AxesOptions): GizmoOptions {
83
+ const { axisColors = ["#ff3653", "#8adb00", "#2c8fff"], labels = ["X", "Y", "Z"], labelColor = "#000" } = options;
84
+ return {
85
+ type: "sphere",
86
+ ...Object.fromEntries(
87
+ axes.map((axis, i) => [
88
+ axis,
89
+ { color: axisColors[Math.floor(i / 2)], labelColor, ...(i % 2 ? { enabled: !options.hideNegativeAxes } : { label: labels[i / 2] }) },
90
+ ])
91
+ ),
92
+ };
93
+ }