@kupcad/ui 0.1.7 → 0.1.9
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.
- package/README.md +20 -9
- package/package.json +6 -5
- package/src/ConsolePanel.svelte +2 -1
- package/src/PreviewPanel.svelte +25 -11
- package/src/ViewerPane.svelte +31 -1
- package/src/console.js +12 -3
- package/src/index.js +8 -1
- package/src/params.js +91 -12
- package/src/run-status.js +5 -1
- package/src/settings.js +53 -19
- package/src/viewer/appearance.js +2 -1
- package/src/viewer/brand.js +13 -0
- package/src/viewer/environment.js +8 -0
- package/src/viewer/faces.js +20 -151
- package/src/viewer/hits.js +41 -0
- package/src/viewer/hover-mark.js +5 -0
- package/src/viewer/model.js +17 -9
- package/src/viewer/section.js +12 -4
- package/src/viewer/theme.js +7 -5
- package/src/viewer/topology.js +223 -0
- package/src/viewer/viewer.js +12 -8
package/src/viewer/faces.js
CHANGED
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
// A mesh's faces, as its feature edges bound them: the triangles joined across their edges where
|
|
2
|
-
// they
|
|
2
|
+
// they don't turn (topology.js; model.js draws an edge where they do). A face
|
|
3
3
|
// is flat when its triangles all face one way, curved otherwise (a cylinder's side); its area is
|
|
4
4
|
// in the scene's millimetres. A geometry's faces are found once, as the pointer first meets each;
|
|
5
5
|
// a face's triangles are what the viewer marks the face under the pointer with.
|
|
6
6
|
|
|
7
7
|
import { Vector3 } from 'three';
|
|
8
|
-
import {
|
|
8
|
+
import { topologyOf, turns } from './topology.js';
|
|
9
9
|
|
|
10
10
|
/** @import { BufferGeometry, Matrix4, Mesh } from 'three' */
|
|
11
11
|
/** @import { Face } from './types.js' */
|
|
12
|
+
/** @import { Topology } from './topology.js' */
|
|
12
13
|
|
|
13
14
|
/** Triangles that face ways this close (the cosine of their angle) are one flat face's. */
|
|
14
15
|
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
16
|
/** A face's way is an axis's when this close to it (the cosine). */
|
|
23
17
|
const AXIS_COS = Math.cos((1 * Math.PI) / 180);
|
|
24
18
|
/** The axes a face may face, and what the views call them. */
|
|
@@ -32,14 +26,8 @@ const AXES = /** @type {const} */ ([
|
|
|
32
26
|
]);
|
|
33
27
|
|
|
34
28
|
/**
|
|
35
|
-
* A geometry's
|
|
36
|
-
*
|
|
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
|
|
29
|
+
* A geometry's faces as found so far: the face each triangle was found in (-1 for none yet).
|
|
30
|
+
* @typedef {object} Faces
|
|
43
31
|
* @property {Int32Array} faceOf
|
|
44
32
|
* @property {FaceOf[]} faces
|
|
45
33
|
*/
|
|
@@ -53,8 +41,8 @@ const AXES = /** @type {const} */ ([
|
|
|
53
41
|
* @property {{ matrix: Matrix4, value: number } | null} area
|
|
54
42
|
*/
|
|
55
43
|
|
|
56
|
-
/** @type {WeakMap<BufferGeometry,
|
|
57
|
-
const
|
|
44
|
+
/** @type {WeakMap<BufferGeometry, Faces>} */
|
|
45
|
+
const found = new WeakMap();
|
|
58
46
|
|
|
59
47
|
/**
|
|
60
48
|
* The face of `mesh` its triangle `triangle` is in, in the scene's terms.
|
|
@@ -93,144 +81,25 @@ export function faceTriangles(mesh, triangle) {
|
|
|
93
81
|
* @param {number} triangle
|
|
94
82
|
*/
|
|
95
83
|
function faceContaining(geometry, triangle) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
84
|
+
const topology = topologyOf(geometry);
|
|
85
|
+
let faces = found.get(geometry);
|
|
86
|
+
if (!faces) {
|
|
87
|
+
faces = { faceOf: new Int32Array(topology.normals.length / 3).fill(-1), faces: [] };
|
|
88
|
+
found.set(geometry, faces);
|
|
100
89
|
}
|
|
101
|
-
if (
|
|
102
|
-
return
|
|
90
|
+
if (faces.faceOf[triangle] < 0) findFace(topology, faces, triangle);
|
|
91
|
+
return faces.faces[faces.faceOf[triangle]];
|
|
103
92
|
}
|
|
104
93
|
|
|
105
94
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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.
|
|
95
|
+
* Finds the face `seed` is in: the triangles reached across edges that don't turn; flat when
|
|
96
|
+
* they all face the seed's way.
|
|
229
97
|
* @param {Topology} topology
|
|
98
|
+
* @param {Faces} found the faces found so far, which it joins
|
|
230
99
|
* @param {number} seed
|
|
231
100
|
*/
|
|
232
|
-
function findFace(topology, seed) {
|
|
233
|
-
const { normals, edgeOf, start, sides
|
|
101
|
+
function findFace(topology, { faceOf, faces }, seed) {
|
|
102
|
+
const { normals, edgeOf, start, sides } = topology;
|
|
234
103
|
const id = faces.length;
|
|
235
104
|
/** @param {number} t @param {number} u */
|
|
236
105
|
const cos = (t, u) =>
|
|
@@ -246,7 +115,7 @@ function findFace(topology, seed) {
|
|
|
246
115
|
const edge = edgeOf[side];
|
|
247
116
|
for (let on = start[edge]; on < start[edge + 1]; on++) {
|
|
248
117
|
const u = Math.floor(sides[on] / 3);
|
|
249
|
-
if (faceOf[u] >= 0 ||
|
|
118
|
+
if (faceOf[u] >= 0 || turns(topology, t, u)) continue;
|
|
250
119
|
faceOf[u] = id;
|
|
251
120
|
triangles.push(u);
|
|
252
121
|
if (cos(seed, u) < FLAT_COS) flat = false;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// What a ray meets among a model's meshes, through a bounds tree of each geometry
|
|
2
|
+
// (three-mesh-bvh), made the first time a ray is cast at it: three.js's own raycast tests every
|
|
3
|
+
// triangle, 14 ms a pointer move at 500k triangles. The tree is indirect, so the geometry's
|
|
4
|
+
// index stays as it is and a hit's `faceIndex` is the triangle the faces (faces.js) know.
|
|
5
|
+
|
|
6
|
+
import { MeshBVH } from 'three-mesh-bvh';
|
|
7
|
+
|
|
8
|
+
/** @import { BufferGeometry, Intersection, Mesh, Raycaster } from 'three' */
|
|
9
|
+
|
|
10
|
+
/** @type {WeakMap<BufferGeometry, MeshBVH>} */
|
|
11
|
+
const trees = new WeakMap();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The hits of `raycaster`'s ray on `meshes` (not their children), nearest first, as
|
|
15
|
+
* `raycaster.intersectObjects(meshes, false)` gives them.
|
|
16
|
+
* @param {Raycaster} raycaster
|
|
17
|
+
* @param {Mesh[]} meshes
|
|
18
|
+
* @returns {Intersection[]}
|
|
19
|
+
*/
|
|
20
|
+
export function intersectMeshes(raycaster, meshes) {
|
|
21
|
+
/** @type {Intersection[]} */
|
|
22
|
+
const hits = [];
|
|
23
|
+
for (const mesh of meshes) {
|
|
24
|
+
if (!mesh.layers.test(raycaster.layers)) continue;
|
|
25
|
+
treeOf(mesh.geometry).raycastObject3D(mesh, raycaster, hits);
|
|
26
|
+
}
|
|
27
|
+
return hits.toSorted((a, b) => a.distance - b.distance);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {BufferGeometry} geometry
|
|
32
|
+
* @returns {MeshBVH}
|
|
33
|
+
*/
|
|
34
|
+
function treeOf(geometry) {
|
|
35
|
+
let tree = trees.get(geometry);
|
|
36
|
+
if (!tree) {
|
|
37
|
+
tree = new MeshBVH(geometry, { indirect: true });
|
|
38
|
+
trees.set(geometry, tree);
|
|
39
|
+
}
|
|
40
|
+
return tree;
|
|
41
|
+
}
|
package/src/viewer/hover-mark.js
CHANGED
|
@@ -19,6 +19,8 @@ import { faceTriangles } from './faces.js';
|
|
|
19
19
|
/** How much of the selection's colour the copy under the pointer takes on, and its face. */
|
|
20
20
|
export const COPY_GLOW = 0.2;
|
|
21
21
|
export const FACE_GLOW = 0.4;
|
|
22
|
+
/** A mark is drawn this far after its mesh in the order: before the next (orders are whole). */
|
|
23
|
+
const MARK_AFTER = 0.5;
|
|
22
24
|
/** What a mark draws while it marks nothing. */
|
|
23
25
|
const NOTHING = new BufferGeometry();
|
|
24
26
|
|
|
@@ -72,6 +74,9 @@ export class HoverMark {
|
|
|
72
74
|
material.needsUpdate = true;
|
|
73
75
|
}
|
|
74
76
|
mesh.add(this.#copy);
|
|
77
|
+
// Over the mesh, a drawing's shape drawn in its order (model.js), before what is next
|
|
78
|
+
this.#copy.renderOrder = mesh.renderOrder + MARK_AFTER;
|
|
79
|
+
this.#face.renderOrder = mesh.renderOrder + MARK_AFTER;
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
this.#face.removeFromParent();
|
package/src/viewer/model.js
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
DoubleSide,
|
|
10
|
-
EdgesGeometry,
|
|
11
10
|
Float32BufferAttribute,
|
|
12
11
|
BufferGeometry,
|
|
13
12
|
GreaterDepth,
|
|
@@ -21,14 +20,14 @@ import {
|
|
|
21
20
|
} from 'three';
|
|
22
21
|
import { SVGLoader } from 'three/addons/loaders/SVGLoader.js';
|
|
23
22
|
import { disposeGeometries } from './dispose.js';
|
|
23
|
+
import { LAYERS } from './environment.js';
|
|
24
|
+
import { featureEdgesOf } from './topology.js';
|
|
24
25
|
|
|
25
26
|
/** @import { Material, Object3D } from 'three' */
|
|
26
27
|
/** @import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' */
|
|
27
28
|
/** @import { Model, Part } from '@kupcad/protocol/types' */
|
|
28
29
|
/** @import { SceneModel, Shape, Theme } from './types.js' */
|
|
29
30
|
|
|
30
|
-
/** Where a solid's faces meet at more than this many degrees, an edge is drawn: its features. */
|
|
31
|
-
export const EDGE_ANGLE_DEG = 20;
|
|
32
31
|
/** A ghost is seen through. */
|
|
33
32
|
export const GHOST_OPACITY = 0.2;
|
|
34
33
|
/** How much of a highlighted solid shows where something hides it. */
|
|
@@ -147,7 +146,7 @@ function solidShape(mesh, partOf, nodeName, { edgeMaterial, highlightEdges }, th
|
|
|
147
146
|
if (highlighted) material.color.set(theme.highlight);
|
|
148
147
|
Object.assign(material, FACES_BEHIND_EDGES);
|
|
149
148
|
const edges = new LineSegments(
|
|
150
|
-
|
|
149
|
+
featureEdgesOf(mesh.geometry),
|
|
151
150
|
highlighted ? highlightEdges : edgeMaterial,
|
|
152
151
|
);
|
|
153
152
|
mesh.add(edges);
|
|
@@ -204,7 +203,9 @@ function partAbove(object, partOf, nodeName) {
|
|
|
204
203
|
/**
|
|
205
204
|
* A drawing (export_svg's) on the XY plane: each filled path a shape named by its id, with its
|
|
206
205
|
* outline; an open path its outline alone. The core negated every y, SVG's running down the
|
|
207
|
-
* page; the group turns it back.
|
|
206
|
+
* page; the group turns it back. Its paths are drawn in the file's order, each over the floor
|
|
207
|
+
* and the ones before it, as SVG draws them: all at z = 0, by depth they fought, a shape's
|
|
208
|
+
* letters lost in streaks of the plate under them, and the floor's grid over both.
|
|
208
209
|
* @param {string} svg
|
|
209
210
|
* @param {Theme} theme
|
|
210
211
|
* @returns {SceneModel}
|
|
@@ -212,8 +213,11 @@ function partAbove(object, partOf, nodeName) {
|
|
|
212
213
|
function drawing(svg, theme) {
|
|
213
214
|
const root = new Group();
|
|
214
215
|
root.scale.y = -1;
|
|
215
|
-
|
|
216
|
-
|
|
216
|
+
root.renderOrder = LAYERS.drawing;
|
|
217
|
+
let order = 0;
|
|
218
|
+
// Outlines on the background. Each thing a drawing draws is see-through to three.js, as are
|
|
219
|
+
// its fills, which it draws after everything opaque whatever their order: one list, in order
|
|
220
|
+
const edgeMaterial = new LineBasicMaterial({ color: theme.wire, transparent: true });
|
|
217
221
|
/** @type {Shape[]} */
|
|
218
222
|
const shapes = [];
|
|
219
223
|
for (const path of new SVGLoader().parse(svg).paths) {
|
|
@@ -222,6 +226,7 @@ function drawing(svg, theme) {
|
|
|
222
226
|
const filled = style.fill !== undefined && style.fill !== 'none';
|
|
223
227
|
const edges = new LineSegments(outlineOf(path, filled), edgeMaterial);
|
|
224
228
|
if (!filled) {
|
|
229
|
+
edges.renderOrder = order++;
|
|
225
230
|
root.add(edges);
|
|
226
231
|
continue;
|
|
227
232
|
}
|
|
@@ -229,11 +234,14 @@ function drawing(svg, theme) {
|
|
|
229
234
|
const material = new MeshBasicMaterial({
|
|
230
235
|
color: path.color,
|
|
231
236
|
side: DoubleSide,
|
|
232
|
-
transparent:
|
|
237
|
+
transparent: true,
|
|
233
238
|
opacity,
|
|
234
|
-
|
|
239
|
+
// Over what is drawn before it; it writes no depth then, so its outline, after it, shows
|
|
240
|
+
depthTest: false,
|
|
235
241
|
});
|
|
236
242
|
const mesh = new Mesh(new ShapeGeometry(path.toShapes()), material);
|
|
243
|
+
mesh.renderOrder = order++;
|
|
244
|
+
edges.renderOrder = order++;
|
|
237
245
|
mesh.add(edges);
|
|
238
246
|
root.add(mesh);
|
|
239
247
|
const id = /** @type {Element | undefined} */ (path.userData?.node)?.getAttribute('id');
|
package/src/viewer/section.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
// cuts capped where the plane runs inside a solid, by three.js's stencil technique
|
|
3
3
|
// (webgl_clipping_stencil): each solid's back faces count up in the stencil buffer and its front
|
|
4
4
|
// faces down, both clipped, so where the count isn't 0 the plane is inside a solid, and a plane
|
|
5
|
-
// drawn there in the cap's colour closes the cut.
|
|
5
|
+
// drawn there in the cap's colour closes the cut. A face on the plane (a cut at the bottom, or
|
|
6
|
+
// at a step's height) is kept: whether it was clipped was the rounding of each pixel's, so the
|
|
7
|
+
// count, and the cap, came and went in streaks.
|
|
6
8
|
|
|
7
9
|
import {
|
|
8
10
|
AlwaysStencilFunc,
|
|
9
11
|
BackSide,
|
|
10
12
|
DecrementWrapStencilOp,
|
|
11
|
-
DoubleSide,
|
|
12
13
|
FrontSide,
|
|
13
14
|
Group,
|
|
14
15
|
IncrementWrapStencilOp,
|
|
@@ -35,6 +36,11 @@ export const RENDER_ORDER = { stencil: 1, cap: 1.1, model: 2 };
|
|
|
35
36
|
const AXES = { x: new Vector3(1, 0, 0), y: new Vector3(0, 1, 0), z: new Vector3(0, 0, 1) };
|
|
36
37
|
/** The cap plane reaches this far past the model's bounding sphere's radius. */
|
|
37
38
|
const CAP_REACH = 4;
|
|
39
|
+
/**
|
|
40
|
+
* The plane lies this far (of the model's radius) past the offset, toward what is cut away, so
|
|
41
|
+
* a face at the offset is kept whatever the rounding: far past it, a hair to the eye.
|
|
42
|
+
*/
|
|
43
|
+
const KEEP_ON_PLANE = 1e-4;
|
|
38
44
|
const PLANE_NORMAL = new Vector3(0, 0, 1);
|
|
39
45
|
|
|
40
46
|
export class SectionView {
|
|
@@ -58,8 +64,9 @@ export class SectionView {
|
|
|
58
64
|
constructor(theme) {
|
|
59
65
|
const material = new MeshStandardMaterial({
|
|
60
66
|
color: theme.cap,
|
|
61
|
-
// Seen from the side cut away,
|
|
62
|
-
side
|
|
67
|
+
// Seen from the side cut away, its back: its normal is the plane's, which points to what
|
|
68
|
+
// is kept. From that side the solid hides it, so a face on the plane can't fight it there
|
|
69
|
+
side: BackSide,
|
|
63
70
|
stencilWrite: true,
|
|
64
71
|
stencilRef: 0,
|
|
65
72
|
stencilFunc: NotEqualStencilFunc,
|
|
@@ -95,6 +102,7 @@ export class SectionView {
|
|
|
95
102
|
// What lies past the offset, along the axis (against it when flipped), is cut away
|
|
96
103
|
this.plane.normal.copy(AXES[cut.section.axis]).multiplyScalar(cut.section.flip ? 1 : -1);
|
|
97
104
|
this.plane.constant = cut.section.flip ? -cut.section.offset : cut.section.offset;
|
|
105
|
+
this.plane.constant += bounds.radius * KEEP_ON_PLANE;
|
|
98
106
|
}
|
|
99
107
|
this.cap.visible = cut !== null && capped;
|
|
100
108
|
for (const material of [...(model ? ownMaterials(model) : []), ...also]) {
|
package/src/viewer/theme.js
CHANGED
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
// a picked part in its orangered, on the neutrals of @kupcad/ui's tokens; a section's cut
|
|
4
4
|
// faces in dodgerblue, so no other warm colour reads as the selection.
|
|
5
5
|
|
|
6
|
+
import { BRAND } from './brand.js';
|
|
7
|
+
|
|
6
8
|
/** @import { Theme } from './types.js' */
|
|
7
9
|
|
|
8
10
|
/** @type {Theme} */
|
|
9
11
|
export const LIGHT = {
|
|
10
12
|
background: '#eef1f4',
|
|
11
13
|
grid: '#c9cfd6',
|
|
12
|
-
plate:
|
|
13
|
-
edges:
|
|
14
|
-
wire:
|
|
15
|
-
selection:
|
|
14
|
+
plate: BRAND.gainsboro,
|
|
15
|
+
edges: BRAND.black,
|
|
16
|
+
wire: BRAND.black,
|
|
17
|
+
selection: BRAND.orange,
|
|
16
18
|
cap: '#0099ff',
|
|
17
19
|
ghost: '#7d8794',
|
|
18
20
|
highlight: '#d6409f',
|
|
@@ -25,7 +27,7 @@ export const DARK = {
|
|
|
25
27
|
plate: '#1f2022',
|
|
26
28
|
edges: '#0b0b0c',
|
|
27
29
|
wire: '#c9ced4',
|
|
28
|
-
selection:
|
|
30
|
+
selection: BRAND.orange,
|
|
29
31
|
cap: '#3daeff',
|
|
30
32
|
ghost: '#8a94a1',
|
|
31
33
|
highlight: '#f06cb8',
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// A geometry's triangles as a surface: which meet which across an edge, by their corners' points
|
|
2
|
+
// (a mesh's faces may each have corners of their own), and the way each faces. Found once per
|
|
3
|
+
// geometry and kept beside it: the model's feature edges are drawn from it (model.js), where two
|
|
4
|
+
// triangles turn by more than EDGE_ANGLE_DEG, and the faces they bound are found in it (faces.js),
|
|
5
|
+
// so an edge and a face's border are one.
|
|
6
|
+
|
|
7
|
+
import { BufferGeometry, Float32BufferAttribute, Vector3 } from 'three';
|
|
8
|
+
|
|
9
|
+
/** Where a solid's faces meet at more than this many degrees, an edge is drawn: its features. */
|
|
10
|
+
export const EDGE_ANGLE_DEG = 20;
|
|
11
|
+
const EDGE_COS = Math.cos((EDGE_ANGLE_DEG * Math.PI) / 180);
|
|
12
|
+
/** Two corners nearer than this, of the geometry's size, are one point: its triangles meet there. */
|
|
13
|
+
const SAME_POINT = 1e-6;
|
|
14
|
+
/** Slots in a hash table per key it is given, at least: it stays at most half full. */
|
|
15
|
+
const TABLE_ROOM = 2;
|
|
16
|
+
/** Odd numbers that spread a key's parts over a hash table's slots. */
|
|
17
|
+
const SPREAD = [0x9e3779b1, 0x85ebca77, 0xc2b2ae3d];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A geometry's triangles and which meet which across an edge. A triangle's sides are numbered
|
|
21
|
+
* `triangle * 3 + corner`, each from its corner to the next.
|
|
22
|
+
* @typedef {object} Topology
|
|
23
|
+
* @property {Float32Array} normals a unit normal per triangle, 3 numbers each
|
|
24
|
+
* @property {Uint8Array} degenerate 1 for a triangle two of whose corners are one point
|
|
25
|
+
* @property {Int32Array} edgeOf the edge each side lies on, by its ends' points
|
|
26
|
+
* @property {Int32Array} start where each edge's sides begin in `sides` (one more, the end)
|
|
27
|
+
* @property {Int32Array} sides the sides on each edge, edge by edge
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** @type {WeakMap<BufferGeometry, Topology>} */
|
|
31
|
+
const topologies = new WeakMap();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The topology of `geometry`, found the first time it is asked for.
|
|
35
|
+
* @param {BufferGeometry} geometry
|
|
36
|
+
* @returns {Topology}
|
|
37
|
+
*/
|
|
38
|
+
export function topologyOf(geometry) {
|
|
39
|
+
let topology = topologies.get(geometry);
|
|
40
|
+
if (!topology) {
|
|
41
|
+
topology = build(geometry);
|
|
42
|
+
topologies.set(geometry, topology);
|
|
43
|
+
}
|
|
44
|
+
return topology;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether triangles `t` and `u` turn by more than EDGE_ANGLE_DEG where they meet: an edge is
|
|
49
|
+
* drawn between them, and they are of two faces.
|
|
50
|
+
* @param {Topology} topology
|
|
51
|
+
* @param {number} t
|
|
52
|
+
* @param {number} u
|
|
53
|
+
*/
|
|
54
|
+
export function turns({ normals }, t, u) {
|
|
55
|
+
const cos =
|
|
56
|
+
normals[t * 3] * normals[u * 3] +
|
|
57
|
+
normals[t * 3 + 1] * normals[u * 3 + 1] +
|
|
58
|
+
normals[t * 3 + 2] * normals[u * 3 + 2];
|
|
59
|
+
return cos <= EDGE_COS;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The feature edges of `geometry` as line segments: each edge where its two triangles turn by
|
|
64
|
+
* more than EDGE_ANGLE_DEG, or with one triangle (the border of an open mesh) or more than two;
|
|
65
|
+
* a degenerate triangle has none. As three.js's EdgesGeometry draws them, which made a string for
|
|
66
|
+
* each corner: 1.2 s at 500k triangles.
|
|
67
|
+
* @param {BufferGeometry} geometry
|
|
68
|
+
* @returns {BufferGeometry}
|
|
69
|
+
*/
|
|
70
|
+
export function featureEdgesOf(geometry) {
|
|
71
|
+
const topology = topologyOf(geometry);
|
|
72
|
+
const { degenerate, start, sides } = topology;
|
|
73
|
+
const position = geometry.getAttribute('position');
|
|
74
|
+
const index = geometry.getIndex();
|
|
75
|
+
/** @param {number} side */
|
|
76
|
+
const vertex = (side) => (index ? index.getX(side) : side);
|
|
77
|
+
const drawn = [];
|
|
78
|
+
for (let edge = 0; edge + 1 < start.length; edge++) {
|
|
79
|
+
let first = -1;
|
|
80
|
+
let count = 0;
|
|
81
|
+
let turned = false;
|
|
82
|
+
for (let on = start[edge]; on < start[edge + 1]; on++) {
|
|
83
|
+
const t = Math.floor(sides[on] / 3);
|
|
84
|
+
if (degenerate[t]) continue;
|
|
85
|
+
if (count === 0) first = sides[on];
|
|
86
|
+
else turned = turns(topology, Math.floor(first / 3), t);
|
|
87
|
+
count++;
|
|
88
|
+
}
|
|
89
|
+
if (count === 1 || count > 2 || turned) drawn.push(first);
|
|
90
|
+
}
|
|
91
|
+
const positions = new Float32Array(drawn.length * 6);
|
|
92
|
+
drawn.forEach((side, i) => {
|
|
93
|
+
for (const [end, v] of [vertex(side), vertex(nextSide(side))].entries()) {
|
|
94
|
+
positions[i * 6 + end * 3] = position.getX(v);
|
|
95
|
+
positions[i * 6 + end * 3 + 1] = position.getY(v);
|
|
96
|
+
positions[i * 6 + end * 3 + 2] = position.getZ(v);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return new BufferGeometry().setAttribute('position', new Float32BufferAttribute(positions, 3));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The side after `side` around its triangle.
|
|
104
|
+
* @param {number} side
|
|
105
|
+
*/
|
|
106
|
+
function nextSide(side) {
|
|
107
|
+
return side % 3 === 2 ? side - 2 : side + 1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The triangles of `geometry`, their normals, and which meet which: across an edge whose two
|
|
112
|
+
* ends each triangle has (by their points, as a mesh's faces each have corners of their own).
|
|
113
|
+
* Typed arrays throughout: a mesh of a million triangles took seconds, a string and an array
|
|
114
|
+
* made for each.
|
|
115
|
+
* @param {BufferGeometry} geometry
|
|
116
|
+
* @returns {Topology}
|
|
117
|
+
*/
|
|
118
|
+
function build(geometry) {
|
|
119
|
+
const position = geometry.getAttribute('position');
|
|
120
|
+
const index = geometry.getIndex();
|
|
121
|
+
const count = (index ? index.count : position.count) / 3;
|
|
122
|
+
const sideCount = count * 3;
|
|
123
|
+
/** @param {number} side */
|
|
124
|
+
const vertex = (side) => (index ? index.getX(side) : side);
|
|
125
|
+
const pointOf = pointsOf(geometry);
|
|
126
|
+
const normals = new Float32Array(count * 3);
|
|
127
|
+
const a = new Vector3();
|
|
128
|
+
const b = new Vector3();
|
|
129
|
+
const c = new Vector3();
|
|
130
|
+
for (let t = 0; t < count; t++) {
|
|
131
|
+
a.fromBufferAttribute(position, vertex(t * 3));
|
|
132
|
+
b.fromBufferAttribute(position, vertex(t * 3 + 1));
|
|
133
|
+
c.fromBufferAttribute(position, vertex(t * 3 + 2));
|
|
134
|
+
c.sub(b)
|
|
135
|
+
.cross(a.sub(b))
|
|
136
|
+
.normalize()
|
|
137
|
+
.toArray(normals, t * 3);
|
|
138
|
+
}
|
|
139
|
+
// Each side's ends, the lower point first, so a side and its twin are one key
|
|
140
|
+
const low = new Int32Array(sideCount);
|
|
141
|
+
const high = new Int32Array(sideCount);
|
|
142
|
+
const degenerate = new Uint8Array(count);
|
|
143
|
+
for (let side = 0; side < sideCount; side++) {
|
|
144
|
+
const p = pointOf[vertex(side)];
|
|
145
|
+
const q = pointOf[vertex(nextSide(side))];
|
|
146
|
+
low[side] = Math.min(p, q);
|
|
147
|
+
high[side] = Math.max(p, q);
|
|
148
|
+
if (p === q) degenerate[Math.floor(side / 3)] = 1;
|
|
149
|
+
}
|
|
150
|
+
const edges = intern(
|
|
151
|
+
sideCount,
|
|
152
|
+
(side) => Math.imul(low[side], SPREAD[0]) ^ Math.imul(high[side], SPREAD[1]),
|
|
153
|
+
(side, other) => low[side] === low[other] && high[side] === high[other],
|
|
154
|
+
);
|
|
155
|
+
const start = new Int32Array(edges.count + 1);
|
|
156
|
+
for (let side = 0; side < sideCount; side++) start[edges.ids[side] + 1]++;
|
|
157
|
+
for (let edge = 0; edge < edges.count; edge++) start[edge + 1] += start[edge];
|
|
158
|
+
const sides = new Int32Array(sideCount);
|
|
159
|
+
const filled = start.slice(0, edges.count);
|
|
160
|
+
for (let side = 0; side < sideCount; side++) sides[filled[edges.ids[side]]++] = side;
|
|
161
|
+
return { normals, degenerate, edgeOf: edges.ids, start, sides };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The point each vertex of `geometry` is at: vertices nearer than SAME_POINT of its size are at
|
|
166
|
+
* one.
|
|
167
|
+
* @param {BufferGeometry} geometry
|
|
168
|
+
*/
|
|
169
|
+
function pointsOf(geometry) {
|
|
170
|
+
const position = geometry.getAttribute('position');
|
|
171
|
+
geometry.computeBoundingBox();
|
|
172
|
+
const size = geometry.boundingBox?.getSize(new Vector3()).length() || 1;
|
|
173
|
+
const step = size * SAME_POINT;
|
|
174
|
+
const grid = new Float64Array(position.count * 3);
|
|
175
|
+
for (let v = 0; v < position.count; v++) {
|
|
176
|
+
grid[v * 3] = Math.round(position.getX(v) / step);
|
|
177
|
+
grid[v * 3 + 1] = Math.round(position.getY(v) / step);
|
|
178
|
+
grid[v * 3 + 2] = Math.round(position.getZ(v) / step);
|
|
179
|
+
}
|
|
180
|
+
return intern(
|
|
181
|
+
position.count,
|
|
182
|
+
(v) =>
|
|
183
|
+
Math.imul(grid[v * 3], SPREAD[0]) ^
|
|
184
|
+
Math.imul(grid[v * 3 + 1], SPREAD[1]) ^
|
|
185
|
+
Math.imul(grid[v * 3 + 2], SPREAD[2]),
|
|
186
|
+
(v, w) =>
|
|
187
|
+
grid[v * 3] === grid[w * 3] &&
|
|
188
|
+
grid[v * 3 + 1] === grid[w * 3 + 1] &&
|
|
189
|
+
grid[v * 3 + 2] === grid[w * 3 + 2],
|
|
190
|
+
).ids;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Ids for `count` keys, the same for equal keys and counted from 0 in the order they first
|
|
195
|
+
* come: by an open-addressing table of the first key of each id.
|
|
196
|
+
* @param {number} count
|
|
197
|
+
* @param {(key: number) => number} hash
|
|
198
|
+
* @param {(key: number, other: number) => boolean} same
|
|
199
|
+
* @returns {{ ids: Int32Array, count: number }}
|
|
200
|
+
*/
|
|
201
|
+
function intern(count, hash, same) {
|
|
202
|
+
let size = 1;
|
|
203
|
+
while (size < count * TABLE_ROOM) size *= 2;
|
|
204
|
+
const mask = size - 1;
|
|
205
|
+
const firsts = new Int32Array(size).fill(-1);
|
|
206
|
+
const ids = new Int32Array(count);
|
|
207
|
+
let distinct = 0;
|
|
208
|
+
for (let key = 0; key < count; key++) {
|
|
209
|
+
for (let slot = hash(key) & mask; ; slot = (slot + 1) & mask) {
|
|
210
|
+
const first = firsts[slot];
|
|
211
|
+
if (first < 0) {
|
|
212
|
+
firsts[slot] = key;
|
|
213
|
+
ids[key] = distinct++;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
if (same(key, first)) {
|
|
217
|
+
ids[key] = ids[first];
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { ids, count: distinct };
|
|
223
|
+
}
|