@kupcad/ui 0.0.0-stage → 0.1.1

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 (45) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +281 -2
  3. package/package.json +65 -3
  4. package/src/ConsolePanel.svelte +139 -0
  5. package/src/ExportMenu.svelte +36 -0
  6. package/src/ParamField.svelte +390 -0
  7. package/src/ParamsPanel.svelte +216 -0
  8. package/src/PreviewPanel.svelte +372 -0
  9. package/src/ShortcutsDialog.svelte +170 -0
  10. package/src/Splitter.svelte +148 -0
  11. package/src/Viewer.svelte +175 -0
  12. package/src/ViewerPane.svelte +745 -0
  13. package/src/base64.js +16 -0
  14. package/src/console.js +78 -0
  15. package/src/controls.css +183 -0
  16. package/src/index.js +40 -0
  17. package/src/measures.js +52 -0
  18. package/src/params.js +348 -0
  19. package/src/range.js +19 -0
  20. package/src/recent.js +44 -0
  21. package/src/run-status.js +78 -0
  22. package/src/settings.js +82 -0
  23. package/src/shortcuts.js +59 -0
  24. package/src/theme.js +131 -0
  25. package/src/tokens.css +70 -0
  26. package/src/values.svelte.js +13 -0
  27. package/src/view-settings.js +81 -0
  28. package/src/viewer/appearance.js +90 -0
  29. package/src/viewer/camera.js +100 -0
  30. package/src/viewer/dispose.js +30 -0
  31. package/src/viewer/environment.js +145 -0
  32. package/src/viewer/faces.js +311 -0
  33. package/src/viewer/hover-mark.js +125 -0
  34. package/src/viewer/index.js +7 -0
  35. package/src/viewer/model.js +293 -0
  36. package/src/viewer/motion.js +96 -0
  37. package/src/viewer/section.js +166 -0
  38. package/src/viewer/testing.js +90 -0
  39. package/src/viewer/theme.js +32 -0
  40. package/src/viewer/types.d.ts +136 -0
  41. package/src/viewer/view-cube.js +314 -0
  42. package/src/viewer/viewer.js +1027 -0
  43. package/src/viewer/wheel.js +55 -0
  44. package/src/viewport-socket.js +64 -0
  45. package/src/vscode.css +49 -0
@@ -0,0 +1,311 @@
1
+ // A mesh's faces, as its feature edges bound them: the triangles joined across their edges where
2
+ // they meet at less than EDGE_ANGLE_DEG (model.js draws an edge where they meet at more). A face
3
+ // is flat when its triangles all face one way, curved otherwise (a cylinder's side); its area is
4
+ // in the scene's millimetres. A geometry's faces are found once, as the pointer first meets each;
5
+ // a face's triangles are what the viewer marks the face under the pointer with.
6
+
7
+ import { Vector3 } from 'three';
8
+ import { EDGE_ANGLE_DEG } from './model.js';
9
+
10
+ /** @import { BufferGeometry, Matrix4, Mesh } from 'three' */
11
+ /** @import { Face } from './types.js' */
12
+
13
+ /** Triangles that face ways this close (the cosine of their angle) are one flat face's. */
14
+ const FLAT_COS = Math.cos((0.5 * Math.PI) / 180);
15
+ const SMOOTH_COS = Math.cos((EDGE_ANGLE_DEG * Math.PI) / 180);
16
+ /** Two corners nearer than this, of the geometry's size, are one point: its triangles meet there. */
17
+ const SAME_POINT = 1e-6;
18
+ /** Slots in a hash table per key it is given, at least: it stays at most half full. */
19
+ const TABLE_ROOM = 2;
20
+ /** Odd numbers that spread a key's parts over a hash table's slots. */
21
+ const SPREAD = [0x9e3779b1, 0x85ebca77, 0xc2b2ae3d];
22
+ /** A face's way is an axis's when this close to it (the cosine). */
23
+ const AXIS_COS = Math.cos((1 * Math.PI) / 180);
24
+ /** The axes a face may face, and what the views call them. */
25
+ const AXES = /** @type {const} */ ([
26
+ { axis: [0, 0, 1], name: '+Z (top)' },
27
+ { axis: [0, 0, -1], name: '-Z (bottom)' },
28
+ { axis: [0, -1, 0], name: '-Y (front)' },
29
+ { axis: [0, 1, 0], name: '+Y (back)' },
30
+ { axis: [1, 0, 0], name: '+X (right)' },
31
+ { axis: [-1, 0, 0], name: '-X (left)' },
32
+ ]);
33
+
34
+ /**
35
+ * A geometry's triangles, which meet which across an edge, and the face each was found in (-1
36
+ * for none yet). A triangle's sides are numbered `triangle * 3 + corner`, each from its corner
37
+ * to the next.
38
+ * @typedef {object} Topology
39
+ * @property {Float32Array} normals a unit normal per triangle, 3 numbers each
40
+ * @property {Int32Array} edgeOf the edge each side lies on, by its ends' points
41
+ * @property {Int32Array} start where each edge's sides begin in `sides` (one more, the end)
42
+ * @property {Int32Array} sides the sides on each edge, edge by edge
43
+ * @property {Int32Array} faceOf
44
+ * @property {FaceOf[]} faces
45
+ */
46
+
47
+ /**
48
+ * A face's triangles, and its area in the scene as last found, with the mesh's matrix it was
49
+ * found for: a pointer moving over the face asks again on every frame.
50
+ * @typedef {object} FaceOf
51
+ * @property {number[]} triangles
52
+ * @property {boolean} flat
53
+ * @property {{ matrix: Matrix4, value: number } | null} area
54
+ */
55
+
56
+ /** @type {WeakMap<BufferGeometry, Topology>} */
57
+ const topologies = new WeakMap();
58
+
59
+ /**
60
+ * The face of `mesh` its triangle `triangle` is in, in the scene's terms.
61
+ * @param {Mesh} mesh
62
+ * @param {number} triangle the index a raycast's hit gives (`faceIndex`)
63
+ * @returns {Face}
64
+ */
65
+ export function faceAt(mesh, triangle) {
66
+ const geometry = mesh.geometry;
67
+ const face = faceContaining(geometry, triangle);
68
+ const area = areaOf(face, geometry, mesh.matrixWorld);
69
+ if (!face.flat) return { flat: false, area, normal: null, facing: null };
70
+ const normal = worldNormal(geometry, triangle, mesh.matrixWorld);
71
+ return {
72
+ flat: true,
73
+ area,
74
+ normal: /** @type {[number, number, number]} */ (normal.toArray()),
75
+ facing: AXES.find(({ axis }) => normal.dot(new Vector3(...axis)) >= AXIS_COS)?.name ?? null,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * The triangles of the face of `mesh` its triangle `triangle` is in: the same array for every
81
+ * triangle of the face (and every mesh of the geometry).
82
+ * @param {Mesh} mesh
83
+ * @param {number} triangle
84
+ * @returns {readonly number[]}
85
+ */
86
+ export function faceTriangles(mesh, triangle) {
87
+ return faceContaining(mesh.geometry, triangle).triangles;
88
+ }
89
+
90
+ /**
91
+ * The face of `geometry` its triangle `triangle` is in, found the first time it is asked for.
92
+ * @param {BufferGeometry} geometry
93
+ * @param {number} triangle
94
+ */
95
+ function faceContaining(geometry, triangle) {
96
+ let topology = topologies.get(geometry);
97
+ if (!topology) {
98
+ topology = topologyOf(geometry);
99
+ topologies.set(geometry, topology);
100
+ }
101
+ if (topology.faceOf[triangle] < 0) findFace(topology, triangle);
102
+ return topology.faces[topology.faceOf[triangle]];
103
+ }
104
+
105
+ /**
106
+ * The triangles of `geometry`, their normals, and which meet which: across an edge whose two
107
+ * ends each triangle has (by their points, as a mesh's faces each have corners of their own).
108
+ * Typed arrays throughout: a mesh of a million triangles took seconds, a string and an array
109
+ * made for each.
110
+ * @param {BufferGeometry} geometry
111
+ * @returns {Topology}
112
+ */
113
+ function topologyOf(geometry) {
114
+ const position = geometry.getAttribute('position');
115
+ const index = geometry.getIndex();
116
+ const count = (index ? index.count : position.count) / 3;
117
+ const sideCount = count * 3;
118
+ /** @param {number} side */
119
+ const vertex = (side) => (index ? index.getX(side) : side);
120
+ const pointOf = pointsOf(geometry);
121
+ const normals = new Float32Array(count * 3);
122
+ const a = new Vector3();
123
+ const b = new Vector3();
124
+ const c = new Vector3();
125
+ for (let t = 0; t < count; t++) {
126
+ a.fromBufferAttribute(position, vertex(t * 3));
127
+ b.fromBufferAttribute(position, vertex(t * 3 + 1));
128
+ c.fromBufferAttribute(position, vertex(t * 3 + 2));
129
+ c.sub(b)
130
+ .cross(a.sub(b))
131
+ .normalize()
132
+ .toArray(normals, t * 3);
133
+ }
134
+ // Each side's ends, the lower point first, so a side and its twin are one key
135
+ const low = new Int32Array(sideCount);
136
+ const high = new Int32Array(sideCount);
137
+ for (let side = 0; side < sideCount; side++) {
138
+ const next = side % 3 === 2 ? side - 2 : side + 1;
139
+ const p = pointOf[vertex(side)];
140
+ const q = pointOf[vertex(next)];
141
+ low[side] = Math.min(p, q);
142
+ high[side] = Math.max(p, q);
143
+ }
144
+ const edges = intern(
145
+ sideCount,
146
+ (side) => Math.imul(low[side], SPREAD[0]) ^ Math.imul(high[side], SPREAD[1]),
147
+ (side, other) => low[side] === low[other] && high[side] === high[other],
148
+ );
149
+ const start = new Int32Array(edges.count + 1);
150
+ for (let side = 0; side < sideCount; side++) start[edges.ids[side] + 1]++;
151
+ for (let edge = 0; edge < edges.count; edge++) start[edge + 1] += start[edge];
152
+ const sides = new Int32Array(sideCount);
153
+ const filled = start.slice(0, edges.count);
154
+ for (let side = 0; side < sideCount; side++) sides[filled[edges.ids[side]]++] = side;
155
+ return {
156
+ normals,
157
+ edgeOf: edges.ids,
158
+ start,
159
+ sides,
160
+ faceOf: new Int32Array(count).fill(-1),
161
+ faces: [],
162
+ };
163
+ }
164
+
165
+ /**
166
+ * The point each vertex of `geometry` is at: vertices nearer than SAME_POINT of its size are at
167
+ * one.
168
+ * @param {BufferGeometry} geometry
169
+ */
170
+ function pointsOf(geometry) {
171
+ const position = geometry.getAttribute('position');
172
+ geometry.computeBoundingBox();
173
+ const size = geometry.boundingBox?.getSize(new Vector3()).length() || 1;
174
+ const step = size * SAME_POINT;
175
+ const grid = new Float64Array(position.count * 3);
176
+ for (let v = 0; v < position.count; v++) {
177
+ grid[v * 3] = Math.round(position.getX(v) / step);
178
+ grid[v * 3 + 1] = Math.round(position.getY(v) / step);
179
+ grid[v * 3 + 2] = Math.round(position.getZ(v) / step);
180
+ }
181
+ return intern(
182
+ position.count,
183
+ (v) =>
184
+ Math.imul(grid[v * 3], SPREAD[0]) ^
185
+ Math.imul(grid[v * 3 + 1], SPREAD[1]) ^
186
+ Math.imul(grid[v * 3 + 2], SPREAD[2]),
187
+ (v, w) =>
188
+ grid[v * 3] === grid[w * 3] &&
189
+ grid[v * 3 + 1] === grid[w * 3 + 1] &&
190
+ grid[v * 3 + 2] === grid[w * 3 + 2],
191
+ ).ids;
192
+ }
193
+
194
+ /**
195
+ * Ids for `count` keys, the same for equal keys and counted from 0 in the order they first
196
+ * come: by an open-addressing table of the first key of each id.
197
+ * @param {number} count
198
+ * @param {(key: number) => number} hash
199
+ * @param {(key: number, other: number) => boolean} same
200
+ * @returns {{ ids: Int32Array, count: number }}
201
+ */
202
+ function intern(count, hash, same) {
203
+ let size = 1;
204
+ while (size < count * TABLE_ROOM) size *= 2;
205
+ const mask = size - 1;
206
+ const firsts = new Int32Array(size).fill(-1);
207
+ const ids = new Int32Array(count);
208
+ let distinct = 0;
209
+ for (let key = 0; key < count; key++) {
210
+ for (let slot = hash(key) & mask; ; slot = (slot + 1) & mask) {
211
+ const first = firsts[slot];
212
+ if (first < 0) {
213
+ firsts[slot] = key;
214
+ ids[key] = distinct++;
215
+ break;
216
+ }
217
+ if (same(key, first)) {
218
+ ids[key] = ids[first];
219
+ break;
220
+ }
221
+ }
222
+ }
223
+ return { ids, count: distinct };
224
+ }
225
+
226
+ /**
227
+ * Finds the face `seed` is in: the triangles reached across edges that turn less than the
228
+ * feature edges' angle; flat when they all face the seed's way.
229
+ * @param {Topology} topology
230
+ * @param {number} seed
231
+ */
232
+ function findFace(topology, seed) {
233
+ const { normals, edgeOf, start, sides, faceOf, faces } = topology;
234
+ const id = faces.length;
235
+ /** @param {number} t @param {number} u */
236
+ const cos = (t, u) =>
237
+ normals[t * 3] * normals[u * 3] +
238
+ normals[t * 3 + 1] * normals[u * 3 + 1] +
239
+ normals[t * 3 + 2] * normals[u * 3 + 2];
240
+ const triangles = [seed];
241
+ faceOf[seed] = id;
242
+ let flat = true;
243
+ for (let at = 0; at < triangles.length; at++) {
244
+ const t = triangles[at];
245
+ for (let side = t * 3; side < t * 3 + 3; side++) {
246
+ const edge = edgeOf[side];
247
+ for (let on = start[edge]; on < start[edge + 1]; on++) {
248
+ const u = Math.floor(sides[on] / 3);
249
+ if (faceOf[u] >= 0 || cos(t, u) < SMOOTH_COS) continue;
250
+ faceOf[u] = id;
251
+ triangles.push(u);
252
+ if (cos(seed, u) < FLAT_COS) flat = false;
253
+ }
254
+ }
255
+ }
256
+ faces.push({ triangles, flat, area: null });
257
+ }
258
+
259
+ /**
260
+ * A triangle's corners, in the scene.
261
+ * @param {BufferGeometry} geometry
262
+ * @param {number} triangle
263
+ * @param {Matrix4} matrix the mesh's to the scene
264
+ * @returns {[Vector3, Vector3, Vector3]}
265
+ */
266
+ function corners(geometry, triangle, matrix) {
267
+ const position = geometry.getAttribute('position');
268
+ const index = geometry.getIndex();
269
+ return /** @type {[Vector3, Vector3, Vector3]} */ (
270
+ [0, 1, 2].map((corner) => {
271
+ const i = index ? index.getX(triangle * 3 + corner) : triangle * 3 + corner;
272
+ return new Vector3().fromBufferAttribute(position, i).applyMatrix4(matrix);
273
+ })
274
+ );
275
+ }
276
+
277
+ /**
278
+ * The area of `face` of `geometry` in the scene, by the mesh's `matrix` to it.
279
+ * @param {FaceOf} face
280
+ * @param {BufferGeometry} geometry
281
+ * @param {Matrix4} matrix
282
+ */
283
+ function areaOf(face, geometry, matrix) {
284
+ if (face.area?.matrix.equals(matrix)) return face.area.value;
285
+ const position = geometry.getAttribute('position');
286
+ const index = geometry.getIndex();
287
+ /** @param {number} side */
288
+ const vertex = (side) => (index ? index.getX(side) : side);
289
+ const a = new Vector3();
290
+ const b = new Vector3();
291
+ const c = new Vector3();
292
+ let value = 0;
293
+ for (const t of face.triangles) {
294
+ a.fromBufferAttribute(position, vertex(t * 3)).applyMatrix4(matrix);
295
+ b.fromBufferAttribute(position, vertex(t * 3 + 1)).applyMatrix4(matrix);
296
+ c.fromBufferAttribute(position, vertex(t * 3 + 2)).applyMatrix4(matrix);
297
+ value += c.sub(b).cross(a.sub(b)).length() / 2;
298
+ }
299
+ face.area = { matrix: matrix.clone(), value };
300
+ return value;
301
+ }
302
+
303
+ /**
304
+ * @param {BufferGeometry} geometry
305
+ * @param {number} triangle
306
+ * @param {Matrix4} matrix
307
+ */
308
+ function worldNormal(geometry, triangle, matrix) {
309
+ const [a, b, c] = corners(geometry, triangle, matrix);
310
+ return c.sub(b).cross(a.sub(b)).normalize();
311
+ }
@@ -0,0 +1,125 @@
1
+ // What the pointer is over, marked: the copy under it glows a little in the selection's colour,
2
+ // and on a solid the face under it (its triangles, faces.js) more. Each is a mesh under the
3
+ // copy's, adding light to what is drawn there, so the mark shows on a part of any colour (a tint
4
+ // toward the selection's lost itself in a part of that colour), and a little in front of the
5
+ // faces, which are drawn behind their own depth (model.js). The viewer moves it as the pointer
6
+ // goes.
7
+
8
+ import {
9
+ AdditiveBlending,
10
+ BufferGeometry,
11
+ Float32BufferAttribute,
12
+ Mesh,
13
+ MeshBasicMaterial,
14
+ } from 'three';
15
+ import { faceTriangles } from './faces.js';
16
+
17
+ /** @import { Material } from 'three' */
18
+
19
+ /** How much of the selection's colour the copy under the pointer takes on, and its face. */
20
+ export const COPY_GLOW = 0.2;
21
+ export const FACE_GLOW = 0.4;
22
+ /** What a mark draws while it marks nothing. */
23
+ const NOTHING = new BufferGeometry();
24
+
25
+ export class HoverMark {
26
+ #copy = new Mesh(NOTHING, glowMaterial(COPY_GLOW));
27
+ #face = new Mesh(NOTHING, glowMaterial(FACE_GLOW));
28
+ /** The copy marked, or null. @type {Mesh | null} */
29
+ #mesh = null;
30
+ /** Its face marked: faces.js's array of the face's triangles, or null. @type {readonly number[] | null} */
31
+ #triangles = null;
32
+
33
+ /** @param {string} color a CSS colour */
34
+ constructor(color) {
35
+ this.setColor(color);
36
+ }
37
+
38
+ /** Its materials: the viewer clips them at a section, as the model's. @returns {Material[]} */
39
+ get materials() {
40
+ return [
41
+ /** @type {Material} */ (this.#copy.material),
42
+ /** @type {Material} */ (this.#face.material),
43
+ ];
44
+ }
45
+
46
+ /** @param {string} color a CSS colour */
47
+ setColor(color) {
48
+ for (const material of this.materials)
49
+ /** @type {MeshBasicMaterial} */ (material).color.set(color);
50
+ }
51
+
52
+ /**
53
+ * Marks `mesh`, a copy, and the face its triangle `triangle` is in (none: null); nothing for
54
+ * a null mesh.
55
+ * @param {Mesh | null} mesh
56
+ * @param {number | null} triangle
57
+ * @returns {boolean} whether the mark moved
58
+ */
59
+ mark(mesh, triangle) {
60
+ const triangles = mesh && triangle !== null ? faceTriangles(mesh, triangle) : null;
61
+ if (mesh === this.#mesh && triangles === this.#triangles) return false;
62
+ if (mesh !== this.#mesh) {
63
+ this.#copy.removeFromParent();
64
+ this.#mesh = mesh;
65
+ // The copy's own geometry, drawn on the side its faces are (a drawing's, both)
66
+ this.#copy.geometry = mesh ? mesh.geometry : NOTHING;
67
+ if (mesh) {
68
+ const material = /** @type {Material} */ (this.#copy.material);
69
+ const { side } = /** @type {Material} */ (mesh.material);
70
+ if (material.side !== side) {
71
+ material.side = side;
72
+ material.needsUpdate = true;
73
+ }
74
+ mesh.add(this.#copy);
75
+ }
76
+ }
77
+ this.#face.removeFromParent();
78
+ if (this.#face.geometry !== NOTHING) this.#face.geometry.dispose();
79
+ this.#triangles = triangles;
80
+ this.#face.geometry = mesh && triangles ? trianglesOf(mesh, triangles) : NOTHING;
81
+ if (mesh && triangles) mesh.add(this.#face);
82
+ return true;
83
+ }
84
+
85
+ dispose() {
86
+ this.mark(null, null);
87
+ for (const material of this.materials) material.dispose();
88
+ }
89
+ }
90
+
91
+ /**
92
+ * What adds `glow` of its colour to what is drawn under it, a little in front of the faces.
93
+ * @param {number} glow
94
+ * @returns {MeshBasicMaterial}
95
+ */
96
+ function glowMaterial(glow) {
97
+ return new MeshBasicMaterial({
98
+ transparent: true,
99
+ opacity: glow,
100
+ blending: AdditiveBlending,
101
+ depthWrite: false,
102
+ polygonOffset: true,
103
+ polygonOffsetFactor: -1,
104
+ polygonOffsetUnits: -1,
105
+ });
106
+ }
107
+
108
+ /**
109
+ * The triangles of `mesh` listed, as a geometry of their own in the mesh's space.
110
+ * @param {Mesh} mesh
111
+ * @param {readonly number[]} triangles
112
+ * @returns {BufferGeometry}
113
+ */
114
+ function trianglesOf(mesh, triangles) {
115
+ const position = mesh.geometry.getAttribute('position');
116
+ const index = mesh.geometry.getIndex();
117
+ const positions = new Float32Array(triangles.length * 9);
118
+ triangles.forEach((triangle, at) => {
119
+ for (let corner = 0; corner < 3; corner++) {
120
+ const i = index ? index.getX(triangle * 3 + corner) : triangle * 3 + corner;
121
+ positions.set([position.getX(i), position.getY(i), position.getZ(i)], at * 9 + corner * 3);
122
+ }
123
+ });
124
+ return new BufferGeometry().setAttribute('position', new Float32BufferAttribute(positions, 3));
125
+ }
@@ -0,0 +1,7 @@
1
+ // @kupcad/ui/viewer: the scene of a model the core made, for any host, Svelte or not. It imports
2
+ // nothing of the rest of the package, nor Svelte (.oxlintrc.json holds it to that);
3
+ // the Svelte component around it is @kupcad/ui's Viewer.
4
+
5
+ export { ViewCube } from './view-cube.js';
6
+ export { RENDER_MODES, Viewer, ViewerError } from './viewer.js';
7
+ export { DARK, LIGHT } from './theme.js';