partforge 0.19.0 → 0.20.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.
- package/docs/AUTHORING-PARTS.md +97 -1
- package/docs/ERROR-PATTERNS.md +6 -0
- package/package.json +3 -1
- package/src/app-bracket.js +9 -0
- package/src/app-nameplate.js +9 -0
- package/src/app-text-smoke.js +10 -0
- package/src/bracket-worker.js +3 -0
- package/src/framework/app.css +23 -6
- package/src/framework/cutaway-controls.js +155 -0
- package/src/framework/cutaway-gizmo.js +686 -0
- package/src/framework/cutaway-math.js +53 -0
- package/src/framework/cutaway-render.js +338 -0
- package/src/framework/cutaway.js +469 -0
- package/src/framework/fonts.js +36 -0
- package/src/framework/geometry/curve-fill.js +86 -0
- package/src/framework/geometry/fonts/Roboto-LICENSE.txt +93 -0
- package/src/framework/geometry/fonts/Roboto-Regular.ttf +0 -0
- package/src/framework/geometry/fonts/default-font.js +3 -0
- package/src/framework/geometry/kernel-front.js +53 -0
- package/src/framework/geometry/kernel.js +1 -1
- package/src/framework/geometry/text2d.js +98 -0
- package/src/framework/geometry-service.js +21 -2
- package/src/framework/jobs.js +9 -0
- package/src/framework/mount.js +278 -223
- package/src/framework/selection/hover.js +102 -36
- package/src/framework/selection/raycast.js +4 -1
- package/src/framework/tooltip.js +282 -0
- package/src/framework/viewer-controls.js +25 -2
- package/src/framework/viewer-lighting.js +13 -0
- package/src/framework/viewer.js +83 -10
- package/src/nameplate-worker.js +3 -0
- package/src/parts/bracket.js +76 -0
- package/src/parts/nameplate.js +67 -0
- package/src/parts/text-smoke.js +21 -0
- package/src/testing/manifold.js +6 -2
- package/src/testing/occt.js +6 -2
- package/src/text-smoke-worker.js +3 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
const PLANE_LOCAL_NORMAL = new THREE.Vector3(0, 0, 1);
|
|
4
|
+
const POINT_EPSILON = 1e-6;
|
|
5
|
+
const PARALLEL_EPSILON = 1e-6;
|
|
6
|
+
|
|
7
|
+
export function initialCutawayPose(box, camera) {
|
|
8
|
+
const position = box.getCenter(new THREE.Vector3());
|
|
9
|
+
const diagonal = Math.max(box.getSize(new THREE.Vector3()).length(), 1);
|
|
10
|
+
const normal = camera.getWorldDirection(new THREE.Vector3()).normalize();
|
|
11
|
+
const quaternion = new THREE.Quaternion().setFromUnitVectors(
|
|
12
|
+
PLANE_LOCAL_NORMAL,
|
|
13
|
+
normal,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
position,
|
|
18
|
+
quaternion,
|
|
19
|
+
size: diagonal * 1.25,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function planeFromPose(plane, normalTarget, position, quaternion, flipped) {
|
|
24
|
+
normalTarget.copy(PLANE_LOCAL_NORMAL).applyQuaternion(quaternion).normalize();
|
|
25
|
+
if (flipped) normalTarget.negate();
|
|
26
|
+
return plane.setFromNormalAndCoplanarPoint(normalTarget, position);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function pointSurvivesPlane(plane, point, epsilon = POINT_EPSILON) {
|
|
30
|
+
// three.js clipping retains the nonnegative side of a THREE.Plane.
|
|
31
|
+
return plane.distanceToPoint(point) >= -epsilon;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function axisParameterFromRay(ray, axisOrigin, axisDirection) {
|
|
35
|
+
const axis = axisDirection.clone().normalize();
|
|
36
|
+
const w0 = ray.origin.clone().sub(axisOrigin);
|
|
37
|
+
const b = ray.direction.dot(axis);
|
|
38
|
+
const d = ray.direction.dot(w0);
|
|
39
|
+
const e = axis.dot(w0);
|
|
40
|
+
const denominator = 1 - b * b;
|
|
41
|
+
|
|
42
|
+
if (Math.abs(denominator) < PARALLEL_EPSILON) return null;
|
|
43
|
+
return (e - b * d) / denominator;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function signedAngleAroundAxis(from, to, axis) {
|
|
47
|
+
const fromDirection = from.clone().normalize();
|
|
48
|
+
const toDirection = to.clone().normalize();
|
|
49
|
+
return Math.atan2(
|
|
50
|
+
axis.dot(fromDirection.clone().cross(toDirection)),
|
|
51
|
+
fromDirection.dot(toDirection),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
export const HATCH_PERIOD_CSS_PX = 5;
|
|
4
|
+
export const HATCH_LINE_CSS_PX = 1;
|
|
5
|
+
|
|
6
|
+
// Stencil/cap passes for every section are kept below all clipped surfaces.
|
|
7
|
+
// This leaves a large, deterministic ordering range for assemblies while making
|
|
8
|
+
// surface/edge ordering independent of the number of subparts.
|
|
9
|
+
const SURFACE_ORDER_BASE = 1_000_000;
|
|
10
|
+
const EDGE_ORDER_BASE = 2_000_000;
|
|
11
|
+
const SECTION_ORDER_STRIDE = 2;
|
|
12
|
+
export const CUTAWAY_OVERLAY_RENDER_ORDER = 3_000_000;
|
|
13
|
+
|
|
14
|
+
export function createHatchMaterial({ color, opacity, inkColor }) {
|
|
15
|
+
const material = new THREE.ShaderMaterial({
|
|
16
|
+
uniforms: {
|
|
17
|
+
uBase: { value: new THREE.Color(color) },
|
|
18
|
+
uInk: { value: new THREE.Color(inkColor) },
|
|
19
|
+
uOpacity: { value: opacity },
|
|
20
|
+
uPixelRatio: { value: 1 },
|
|
21
|
+
},
|
|
22
|
+
vertexShader: `
|
|
23
|
+
void main() {
|
|
24
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
25
|
+
}
|
|
26
|
+
`,
|
|
27
|
+
fragmentShader: `
|
|
28
|
+
uniform vec3 uBase;
|
|
29
|
+
uniform vec3 uInk;
|
|
30
|
+
uniform float uOpacity;
|
|
31
|
+
uniform float uPixelRatio;
|
|
32
|
+
|
|
33
|
+
const float HATCH_PERIOD_CSS_PX = ${HATCH_PERIOD_CSS_PX.toFixed(1)};
|
|
34
|
+
const float HATCH_LINE_CSS_PX = ${HATCH_LINE_CSS_PX.toFixed(1)};
|
|
35
|
+
|
|
36
|
+
void main() {
|
|
37
|
+
vec2 cssPixel = gl_FragCoord.xy / uPixelRatio;
|
|
38
|
+
float axisPixel = dot(cssPixel, normalize(vec2(1.0, 1.0)));
|
|
39
|
+
float wrapped = mod(axisPixel, HATCH_PERIOD_CSS_PX);
|
|
40
|
+
float distanceToLine = min(wrapped, HATCH_PERIOD_CSS_PX - wrapped);
|
|
41
|
+
float halfLine = HATCH_LINE_CSS_PX * 0.5;
|
|
42
|
+
float antialias = max(fwidth(axisPixel), 0.001);
|
|
43
|
+
float stripe = 1.0 - smoothstep(
|
|
44
|
+
halfLine - antialias,
|
|
45
|
+
halfLine + antialias,
|
|
46
|
+
distanceToLine
|
|
47
|
+
);
|
|
48
|
+
gl_FragColor = vec4(mix(uBase, uInk, stripe), uOpacity);
|
|
49
|
+
#include <colorspace_fragment>
|
|
50
|
+
}
|
|
51
|
+
`,
|
|
52
|
+
transparent: opacity < 1,
|
|
53
|
+
depthWrite: opacity >= 1,
|
|
54
|
+
forceSinglePass: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
material.userData.setScreenScale = (pixelRatio) => {
|
|
58
|
+
material.uniforms.uPixelRatio.value = Number.isFinite(pixelRatio) && pixelRatio > 0
|
|
59
|
+
? pixelRatio
|
|
60
|
+
: 1;
|
|
61
|
+
};
|
|
62
|
+
material.userData.setInkColor = (color) => {
|
|
63
|
+
material.uniforms.uInk.value.set(color);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return material;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stencilMaterial(side, operation, plane) {
|
|
70
|
+
const material = new THREE.MeshBasicMaterial({
|
|
71
|
+
side,
|
|
72
|
+
depthWrite: false,
|
|
73
|
+
depthTest: false,
|
|
74
|
+
colorWrite: false,
|
|
75
|
+
});
|
|
76
|
+
material.clippingPlanes = [plane];
|
|
77
|
+
material.stencilWrite = true;
|
|
78
|
+
material.stencilFunc = THREE.AlwaysStencilFunc;
|
|
79
|
+
material.stencilFail = THREE.KeepStencilOp;
|
|
80
|
+
material.stencilZFail = THREE.KeepStencilOp;
|
|
81
|
+
material.stencilZPass = operation;
|
|
82
|
+
return material;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function cloneClipped(material, plane, ownedMaterials) {
|
|
86
|
+
if (Array.isArray(material)) {
|
|
87
|
+
return material.map((entry) => cloneClipped(entry, plane, ownedMaterials));
|
|
88
|
+
}
|
|
89
|
+
const clone = material.clone();
|
|
90
|
+
clone.clippingPlanes = [plane];
|
|
91
|
+
clone.needsUpdate = true;
|
|
92
|
+
ownedMaterials.add(clone);
|
|
93
|
+
return clone;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function firstMaterial(material) {
|
|
97
|
+
return Array.isArray(material) ? material[0] : material;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function makeTransparent(material) {
|
|
101
|
+
if (Array.isArray(material)) {
|
|
102
|
+
for (const entry of material) makeTransparent(entry);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
material.transparent = true;
|
|
106
|
+
material.needsUpdate = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function setLineResolution(material, width, height) {
|
|
110
|
+
if (Array.isArray(material)) {
|
|
111
|
+
for (const entry of material) setLineResolution(entry, width, height);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (material?.isLineMaterial) material.resolution.set(width, height);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function createSectionRenderSet({
|
|
118
|
+
scene,
|
|
119
|
+
mesh,
|
|
120
|
+
edgeLines,
|
|
121
|
+
plane,
|
|
122
|
+
capGeometry,
|
|
123
|
+
order,
|
|
124
|
+
inkColor,
|
|
125
|
+
}) {
|
|
126
|
+
let originalMeshMaterial = mesh.material;
|
|
127
|
+
let originalEdgeMaterial = edgeLines?.material;
|
|
128
|
+
const originalMeshOrder = mesh.renderOrder;
|
|
129
|
+
const originalEdgeOrder = edgeLines?.renderOrder;
|
|
130
|
+
const ownedMaterials = new Set();
|
|
131
|
+
let viewportSize = null;
|
|
132
|
+
|
|
133
|
+
let clippedMeshMaterial = cloneClipped(originalMeshMaterial, plane, ownedMaterials);
|
|
134
|
+
let clippedEdgeMaterial = edgeLines
|
|
135
|
+
? cloneClipped(originalEdgeMaterial, plane, ownedMaterials)
|
|
136
|
+
: null;
|
|
137
|
+
|
|
138
|
+
const backMaterial = stencilMaterial(
|
|
139
|
+
THREE.BackSide,
|
|
140
|
+
THREE.IncrementWrapStencilOp,
|
|
141
|
+
plane,
|
|
142
|
+
);
|
|
143
|
+
const frontMaterial = stencilMaterial(
|
|
144
|
+
THREE.FrontSide,
|
|
145
|
+
THREE.DecrementWrapStencilOp,
|
|
146
|
+
plane,
|
|
147
|
+
);
|
|
148
|
+
ownedMaterials.add(backMaterial);
|
|
149
|
+
ownedMaterials.add(frontMaterial);
|
|
150
|
+
|
|
151
|
+
const sourceMaterial = firstMaterial(originalMeshMaterial);
|
|
152
|
+
const capMaterial = createHatchMaterial({
|
|
153
|
+
color: sourceMaterial?.color ?? 0x9fb4cc,
|
|
154
|
+
opacity: sourceMaterial?.opacity ?? 1,
|
|
155
|
+
inkColor,
|
|
156
|
+
});
|
|
157
|
+
capMaterial.side = THREE.DoubleSide;
|
|
158
|
+
capMaterial.stencilWrite = true;
|
|
159
|
+
capMaterial.stencilFunc = THREE.NotEqualStencilFunc;
|
|
160
|
+
capMaterial.stencilRef = 0;
|
|
161
|
+
capMaterial.stencilFail = THREE.ReplaceStencilOp;
|
|
162
|
+
capMaterial.stencilZFail = THREE.ReplaceStencilOp;
|
|
163
|
+
capMaterial.stencilZPass = THREE.ReplaceStencilOp;
|
|
164
|
+
ownedMaterials.add(capMaterial);
|
|
165
|
+
|
|
166
|
+
// three.js sorts opaque and transparent draws in separate lists. Keeping a
|
|
167
|
+
// translucent section's stencil, cap, surface, and edges in the transparent
|
|
168
|
+
// list preserves the renderOrder-based isolation between subparts.
|
|
169
|
+
if (capMaterial.transparent) {
|
|
170
|
+
backMaterial.transparent = true;
|
|
171
|
+
frontMaterial.transparent = true;
|
|
172
|
+
if (clippedEdgeMaterial) makeTransparent(clippedEdgeMaterial);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Parenting the stencil meshes to the source mesh makes their local identity
|
|
176
|
+
// transform follow every current and future source transform exactly.
|
|
177
|
+
const back = new THREE.Mesh(mesh.geometry, backMaterial);
|
|
178
|
+
const front = new THREE.Mesh(mesh.geometry, frontMaterial);
|
|
179
|
+
mesh.add(back, front);
|
|
180
|
+
|
|
181
|
+
const cap = new THREE.Mesh(capGeometry, capMaterial);
|
|
182
|
+
cap.onAfterRender = (renderer) => renderer.clearStencil();
|
|
183
|
+
scene.add(cap);
|
|
184
|
+
|
|
185
|
+
const stencilOrder = order * SECTION_ORDER_STRIDE;
|
|
186
|
+
back.renderOrder = stencilOrder;
|
|
187
|
+
front.renderOrder = stencilOrder;
|
|
188
|
+
cap.renderOrder = stencilOrder + 1;
|
|
189
|
+
|
|
190
|
+
let enabled = false;
|
|
191
|
+
let visible = mesh.visible;
|
|
192
|
+
let disposed = false;
|
|
193
|
+
|
|
194
|
+
function updateHelperVisibility() {
|
|
195
|
+
const on = enabled && visible && !disposed;
|
|
196
|
+
back.visible = on;
|
|
197
|
+
front.visible = on;
|
|
198
|
+
cap.visible = on;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
back.visible = false;
|
|
202
|
+
front.visible = false;
|
|
203
|
+
cap.visible = false;
|
|
204
|
+
|
|
205
|
+
function setEnabled(on) {
|
|
206
|
+
if (disposed) return;
|
|
207
|
+
enabled = Boolean(on);
|
|
208
|
+
if (enabled) {
|
|
209
|
+
mesh.material = clippedMeshMaterial;
|
|
210
|
+
mesh.renderOrder = SURFACE_ORDER_BASE + order * SECTION_ORDER_STRIDE;
|
|
211
|
+
if (edgeLines) {
|
|
212
|
+
edgeLines.material = clippedEdgeMaterial;
|
|
213
|
+
edgeLines.renderOrder = EDGE_ORDER_BASE + order;
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
mesh.material = originalMeshMaterial;
|
|
217
|
+
mesh.renderOrder = originalMeshOrder;
|
|
218
|
+
if (edgeLines) {
|
|
219
|
+
edgeLines.material = originalEdgeMaterial;
|
|
220
|
+
edgeLines.renderOrder = originalEdgeOrder;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
updateHelperVisibility();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function setVisible(on) {
|
|
227
|
+
if (disposed) return;
|
|
228
|
+
visible = Boolean(on);
|
|
229
|
+
updateHelperVisibility();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function setGeometry(geometry) {
|
|
233
|
+
if (disposed) return;
|
|
234
|
+
back.geometry = geometry;
|
|
235
|
+
front.geometry = geometry;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function setCapPose({ position, quaternion, size }) {
|
|
239
|
+
if (disposed) return;
|
|
240
|
+
cap.position.copy(position);
|
|
241
|
+
cap.quaternion.copy(quaternion);
|
|
242
|
+
cap.scale.setScalar(size);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function setHatchInk(color) {
|
|
246
|
+
if (disposed) return;
|
|
247
|
+
capMaterial.userData.setInkColor(color);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function setViewportSize(width, height, pixelRatio = 1) {
|
|
251
|
+
if (disposed) return;
|
|
252
|
+
viewportSize = { width, height, pixelRatio };
|
|
253
|
+
setLineResolution(clippedEdgeMaterial, width, height);
|
|
254
|
+
capMaterial.userData.setScreenScale(pixelRatio);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function disposeClipped(material) {
|
|
258
|
+
if (Array.isArray(material)) {
|
|
259
|
+
for (const entry of material) disposeClipped(entry);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (!material) return;
|
|
263
|
+
ownedMaterials.delete(material);
|
|
264
|
+
material.dispose();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function refreshSourceMaterial(
|
|
268
|
+
meshMaterial = originalMeshMaterial,
|
|
269
|
+
lineMaterial = originalEdgeMaterial,
|
|
270
|
+
) {
|
|
271
|
+
if (disposed) return false;
|
|
272
|
+
|
|
273
|
+
const nextMeshMaterial = meshMaterial === clippedMeshMaterial
|
|
274
|
+
? originalMeshMaterial
|
|
275
|
+
: meshMaterial;
|
|
276
|
+
const nextLineMaterial = lineMaterial === clippedEdgeMaterial
|
|
277
|
+
? originalEdgeMaterial
|
|
278
|
+
: lineMaterial;
|
|
279
|
+
disposeClipped(clippedMeshMaterial);
|
|
280
|
+
disposeClipped(clippedEdgeMaterial);
|
|
281
|
+
originalMeshMaterial = nextMeshMaterial;
|
|
282
|
+
originalEdgeMaterial = nextLineMaterial;
|
|
283
|
+
clippedMeshMaterial = cloneClipped(nextMeshMaterial, plane, ownedMaterials);
|
|
284
|
+
clippedEdgeMaterial = edgeLines
|
|
285
|
+
? cloneClipped(nextLineMaterial, plane, ownedMaterials)
|
|
286
|
+
: null;
|
|
287
|
+
if (viewportSize) {
|
|
288
|
+
setLineResolution(
|
|
289
|
+
clippedEdgeMaterial,
|
|
290
|
+
viewportSize.width,
|
|
291
|
+
viewportSize.height,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const source = firstMaterial(nextMeshMaterial);
|
|
296
|
+
capMaterial.uniforms.uBase.value.copy(source?.color ?? new THREE.Color(0x9fb4cc));
|
|
297
|
+
capMaterial.uniforms.uOpacity.value = source?.opacity ?? 1;
|
|
298
|
+
capMaterial.opacity = source?.opacity ?? 1;
|
|
299
|
+
capMaterial.transparent = source?.transparent ?? capMaterial.opacity < 1;
|
|
300
|
+
capMaterial.depthWrite = source?.depthWrite ?? !capMaterial.transparent;
|
|
301
|
+
capMaterial.needsUpdate = true;
|
|
302
|
+
backMaterial.transparent = capMaterial.transparent;
|
|
303
|
+
frontMaterial.transparent = capMaterial.transparent;
|
|
304
|
+
if (clippedEdgeMaterial && capMaterial.transparent) makeTransparent(clippedEdgeMaterial);
|
|
305
|
+
|
|
306
|
+
if (enabled) {
|
|
307
|
+
mesh.material = clippedMeshMaterial;
|
|
308
|
+
if (edgeLines) edgeLines.material = clippedEdgeMaterial;
|
|
309
|
+
} else {
|
|
310
|
+
mesh.material = originalMeshMaterial;
|
|
311
|
+
if (edgeLines) edgeLines.material = originalEdgeMaterial;
|
|
312
|
+
}
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function dispose() {
|
|
317
|
+
if (disposed) return;
|
|
318
|
+
setEnabled(false);
|
|
319
|
+
disposed = true;
|
|
320
|
+
mesh.remove(back, front);
|
|
321
|
+
scene.remove(cap);
|
|
322
|
+
for (const material of ownedMaterials) material.dispose();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
back,
|
|
327
|
+
front,
|
|
328
|
+
cap,
|
|
329
|
+
setEnabled,
|
|
330
|
+
setVisible,
|
|
331
|
+
setGeometry,
|
|
332
|
+
setCapPose,
|
|
333
|
+
setHatchInk,
|
|
334
|
+
setViewportSize,
|
|
335
|
+
refreshSourceMaterial,
|
|
336
|
+
dispose,
|
|
337
|
+
};
|
|
338
|
+
}
|