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
package/src/framework/viewer.js
CHANGED
|
@@ -4,12 +4,14 @@ import { toCreasedNormals } from "three/addons/utils/BufferGeometryUtils.js";
|
|
|
4
4
|
import { LineSegments2 } from "three/addons/lines/LineSegments2.js";
|
|
5
5
|
import { LineSegmentsGeometry } from "three/addons/lines/LineSegmentsGeometry.js";
|
|
6
6
|
import { LineMaterial } from "three/addons/lines/LineMaterial.js";
|
|
7
|
+
import { createCutaway } from "./cutaway.js";
|
|
8
|
+
import { addViewerLights } from "./viewer-lighting.js";
|
|
7
9
|
|
|
8
10
|
export function createViewer(container, part) {
|
|
9
11
|
const names = Object.keys(part.parts);
|
|
10
12
|
|
|
11
13
|
// --- renderer / scene / camera --------------------------------------------
|
|
12
|
-
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
14
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true, stencil: true });
|
|
13
15
|
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
|
14
16
|
container.appendChild(renderer.domElement);
|
|
15
17
|
|
|
@@ -32,10 +34,7 @@ export function createViewer(container, part) {
|
|
|
32
34
|
controls.autoRotateSpeed = 1.6;
|
|
33
35
|
|
|
34
36
|
// --- lights + grid --------------------------------------------------------
|
|
35
|
-
scene
|
|
36
|
-
const key = new THREE.DirectionalLight(0xffffff, 1.4);
|
|
37
|
-
key.position.set(8, 14, 10);
|
|
38
|
-
scene.add(key);
|
|
37
|
+
addViewerLights(scene);
|
|
39
38
|
// 1 cm grid (mm units): 300 mm wide, 30 divisions -> 10 mm (1 cm) squares.
|
|
40
39
|
const GRID_SIZE = 300, GRID_DIVS = 30;
|
|
41
40
|
let floorY = 0; // world Y of the grid plane; set to the part's bbox bottom in frameTo
|
|
@@ -86,7 +85,7 @@ export function createViewer(container, part) {
|
|
|
86
85
|
|
|
87
86
|
// CAD-style feature edge lines (anti-aliased "fat" lines), one per sub-part.
|
|
88
87
|
const EDGE_ANGLE = 35; // deg — OCCT fallback threshold (Manifold supplies seam-aware edges)
|
|
89
|
-
const lineMaterial = new LineMaterial({ color:
|
|
88
|
+
const lineMaterial = new LineMaterial({ color: THEME.dark.line, linewidth: 1.0 }); // ~10% lighter, 1 px
|
|
90
89
|
lineMaterial.resolution.set(1, 1); // real size set by resize() below
|
|
91
90
|
const subLines = Object.fromEntries(
|
|
92
91
|
names.map((n) => [n, new LineSegments2(new LineSegmentsGeometry(), lineMaterial)])
|
|
@@ -96,6 +95,32 @@ export function createViewer(container, part) {
|
|
|
96
95
|
partsGroup.add(l);
|
|
97
96
|
}
|
|
98
97
|
|
|
98
|
+
// The cutaway plane lives in world space, so its initial/reset bounds must
|
|
99
|
+
// include the pivot rotation and the per-view recentering transform.
|
|
100
|
+
const _worldBounds = new THREE.Box3();
|
|
101
|
+
function getVisibleWorldBounds() {
|
|
102
|
+
_worldBounds.makeEmpty();
|
|
103
|
+
for (const mesh of Object.values(subMesh)) {
|
|
104
|
+
if (!mesh.visible || !mesh.geometry) continue;
|
|
105
|
+
mesh.updateWorldMatrix(true, false);
|
|
106
|
+
_worldBounds.expandByObject(mesh);
|
|
107
|
+
}
|
|
108
|
+
return _worldBounds;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const cutaway = createCutaway({
|
|
112
|
+
renderer,
|
|
113
|
+
scene,
|
|
114
|
+
camera,
|
|
115
|
+
orbitControls: controls,
|
|
116
|
+
domElement: renderer.domElement,
|
|
117
|
+
getBounds: getVisibleWorldBounds,
|
|
118
|
+
edgeColor: THEME.dark.line,
|
|
119
|
+
});
|
|
120
|
+
for (const name of names) {
|
|
121
|
+
cutaway.setSubpart(name, subMesh[name], subLines[name]);
|
|
122
|
+
}
|
|
123
|
+
|
|
99
124
|
// Smooth shading within CREASE_ANGLE of a shared edge, hard edge past it — so the
|
|
100
125
|
// round body and helical groove read smooth while bore rims, drum faces, and
|
|
101
126
|
// groove walls stay crisp. Lower = more hard edges; raise toward Math.PI/3 for
|
|
@@ -135,9 +160,13 @@ export function createViewer(container, part) {
|
|
|
135
160
|
const subCache = Object.fromEntries(names.map((n) => [n, null]));
|
|
136
161
|
|
|
137
162
|
function setSubGeometry(name, payload) {
|
|
138
|
-
const prev = subCache[name];
|
|
163
|
+
const prev = subCache[name];
|
|
164
|
+
const next = buildGeometry(payload);
|
|
165
|
+
subCache[name] = next;
|
|
166
|
+
// Section helpers must stop referring to the old buffers before those
|
|
167
|
+
// buffers are released.
|
|
168
|
+
cutaway.updateGeometry(name, next);
|
|
139
169
|
if (prev) { prev.userData.edges?.dispose(); prev.dispose(); }
|
|
140
|
-
subCache[name] = buildGeometry(payload);
|
|
141
170
|
}
|
|
142
171
|
|
|
143
172
|
// Cache queries for the app's regenerate loop (so it never reaches into subCache).
|
|
@@ -178,6 +207,7 @@ export function createViewer(container, part) {
|
|
|
178
207
|
subLines[name].visible = on;
|
|
179
208
|
}
|
|
180
209
|
if (frame) frameTo(visibleNames);
|
|
210
|
+
cutaway.setVisible(visibleNames);
|
|
181
211
|
}
|
|
182
212
|
|
|
183
213
|
// Re-frame whatever is currently visible (the reframe button).
|
|
@@ -185,7 +215,19 @@ export function createViewer(container, part) {
|
|
|
185
215
|
frameTo(names.filter((n) => subMesh[n].visible && subCache[n]));
|
|
186
216
|
}
|
|
187
217
|
|
|
188
|
-
|
|
218
|
+
let autoRotateRequested = true;
|
|
219
|
+
function syncAutoRotate() {
|
|
220
|
+
controls.autoRotate = autoRotateRequested && !cutaway.isEnabled;
|
|
221
|
+
}
|
|
222
|
+
function setAutoRotate(on) {
|
|
223
|
+
autoRotateRequested = !!on;
|
|
224
|
+
syncAutoRotate();
|
|
225
|
+
}
|
|
226
|
+
function setCutawayEnabled(on) {
|
|
227
|
+
const changed = cutaway.setEnabled(on);
|
|
228
|
+
syncAutoRotate();
|
|
229
|
+
return changed;
|
|
230
|
+
}
|
|
189
231
|
|
|
190
232
|
// Swap the scene background, grid, and edge-line colors for the given theme.
|
|
191
233
|
function setTheme(mode) {
|
|
@@ -196,11 +238,13 @@ export function createViewer(container, part) {
|
|
|
196
238
|
grid.position.y = floorY; // keep the floor at the bbox bottom across theme swaps
|
|
197
239
|
scene.add(grid);
|
|
198
240
|
lineMaterial.color.set(t.line);
|
|
241
|
+
cutaway.setTheme(mode, t.line);
|
|
199
242
|
}
|
|
200
243
|
|
|
201
244
|
function hideAssembly() {
|
|
202
245
|
for (const m of Object.values(subMesh)) m.visible = false;
|
|
203
246
|
for (const l of Object.values(subLines)) l.visible = false;
|
|
247
|
+
cutaway.setVisible([]);
|
|
204
248
|
}
|
|
205
249
|
|
|
206
250
|
// --- resize ---------------------------------------------------------------
|
|
@@ -211,6 +255,7 @@ export function createViewer(container, part) {
|
|
|
211
255
|
camera.aspect = w / h;
|
|
212
256
|
camera.updateProjectionMatrix();
|
|
213
257
|
lineMaterial.resolution.set(w, h); // fat lines need the viewport size for px width
|
|
258
|
+
cutaway.setViewportSize(w, h, renderer.getPixelRatio());
|
|
214
259
|
}
|
|
215
260
|
const ro = new ResizeObserver(resize);
|
|
216
261
|
ro.observe(container);
|
|
@@ -219,7 +264,9 @@ export function createViewer(container, part) {
|
|
|
219
264
|
// --- render loop ----------------------------------------------------------
|
|
220
265
|
renderer.setAnimationLoop(() => {
|
|
221
266
|
controls.update();
|
|
267
|
+
if (cutaway.isEnabled) cutaway.updateForCamera();
|
|
222
268
|
renderer.render(scene, camera);
|
|
269
|
+
cutaway.renderOverlay(renderer, camera);
|
|
223
270
|
});
|
|
224
271
|
|
|
225
272
|
// --- camera state (read/write for persistence; mount.js owns storage) -------
|
|
@@ -265,6 +312,7 @@ export function createViewer(container, part) {
|
|
|
265
312
|
controls.dispose();
|
|
266
313
|
for (const t of flashTimers) clearTimeout(t);
|
|
267
314
|
flashTimers.clear();
|
|
315
|
+
cutaway.dispose();
|
|
268
316
|
for (const n of names) {
|
|
269
317
|
const g = subCache[n];
|
|
270
318
|
if (g) { g.userData.edges?.dispose(); g.dispose(); subCache[n] = null; }
|
|
@@ -279,5 +327,30 @@ export function createViewer(container, part) {
|
|
|
279
327
|
renderer.domElement.remove();
|
|
280
328
|
}
|
|
281
329
|
|
|
282
|
-
return {
|
|
330
|
+
return {
|
|
331
|
+
showAssembly,
|
|
332
|
+
hideAssembly,
|
|
333
|
+
setSubGeometry,
|
|
334
|
+
hasSubMesh,
|
|
335
|
+
subTriangles,
|
|
336
|
+
frame,
|
|
337
|
+
setAutoRotate,
|
|
338
|
+
setTheme,
|
|
339
|
+
getCameraState,
|
|
340
|
+
setCameraState,
|
|
341
|
+
onCameraEnd,
|
|
342
|
+
camera,
|
|
343
|
+
domElement: renderer.domElement,
|
|
344
|
+
_subMeshes: subMesh,
|
|
345
|
+
flashPoint,
|
|
346
|
+
cutawaySupported: () => cutaway.isSupported,
|
|
347
|
+
cutawayEnabled: () => cutaway.isEnabled,
|
|
348
|
+
setCutawayEnabled,
|
|
349
|
+
flipCutaway: cutaway.flip,
|
|
350
|
+
resetCutaway: cutaway.reset,
|
|
351
|
+
isWorldPointVisible: cutaway.isPointVisible,
|
|
352
|
+
registerCutawayMaterial: cutaway.registerClippableMaterial,
|
|
353
|
+
onCutawayHandleHover: cutaway.onHandleHoverChange,
|
|
354
|
+
dispose,
|
|
355
|
+
};
|
|
283
356
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Demo part — a cross bracket. Showcases the 2-D Shape2D toolkit end to end:
|
|
2
|
+
// • union — two rounded-rectangle bars fused into a plus
|
|
3
|
+
// • intersect — optionally clipped to a disc so the arm tips round off
|
|
4
|
+
// • cutAll — four corner bolt holes drilled in one batch
|
|
5
|
+
// • cut — a central bore
|
|
6
|
+
// • offset — an optional print-clearance grow with rounded corners
|
|
7
|
+
// The rounded corners and circular holes are true arcs (curve-native profiles), so
|
|
8
|
+
// the whole outline stays curve-exact. Open /bracket.html after `npm run dev`.
|
|
9
|
+
import { roundedRectPolygon, circleProfile } from "partforge/geometry";
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
meta: { title: "Cross bracket", units: "mm", background: 0x15181d },
|
|
13
|
+
parameters: [
|
|
14
|
+
{
|
|
15
|
+
id: "size",
|
|
16
|
+
title: "Bracket",
|
|
17
|
+
description: "A plus-shaped plate made by **union**-ing a horizontal and a vertical rounded bar.",
|
|
18
|
+
advanced: [
|
|
19
|
+
{ key: "span", label: "Span", unit: "mm", min: 30, max: 100, step: 1,
|
|
20
|
+
description: "Overall arm length, tip to tip, in both directions." },
|
|
21
|
+
{ key: "bar", label: "Arm width", unit: "mm", min: 10, max: 40, step: 1,
|
|
22
|
+
description: "Width of each arm." },
|
|
23
|
+
{ key: "corner", label: "Corner radius", unit: "mm", min: 0, max: 12, step: 0.5,
|
|
24
|
+
description: "Rounding on the bar corners (arcs, kept exact to STEP)." },
|
|
25
|
+
{ key: "thickness", label: "Thickness", unit: "mm", min: 2, max: 10, step: 0.5,
|
|
26
|
+
description: "Plate thickness." },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "holes",
|
|
31
|
+
title: "Holes",
|
|
32
|
+
description: "Four corner bolt holes (**cutAll** in one batch) and an optional central bore (**cut**).",
|
|
33
|
+
advanced: [
|
|
34
|
+
{ key: "hole_d", label: "Bolt hole ø", unit: "mm", min: 2, max: 10, step: 0.5,
|
|
35
|
+
description: "Diameter of the four corner holes." },
|
|
36
|
+
{ key: "inset", label: "Hole inset", unit: "mm", min: 5, max: 24, step: 1,
|
|
37
|
+
description: "How far the corner holes sit in from the arm tips." },
|
|
38
|
+
{ key: "center_d", label: "Center bore ø", unit: "mm", min: 0, max: 30, step: 1,
|
|
39
|
+
description: "Central through-bore. Set to 0 for none." },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "shape",
|
|
44
|
+
title: "Shape ops",
|
|
45
|
+
toggles: [
|
|
46
|
+
{ key: "clip", label: "Clip arms to a disc (intersect)", on: 1,
|
|
47
|
+
description: "**Intersect** the cross with a circle so the four arm tips are rounded off to a common radius." },
|
|
48
|
+
],
|
|
49
|
+
advanced: [
|
|
50
|
+
{ key: "clearance", label: "Print-clearance offset", unit: "mm", min: 0, max: 1, step: 0.1,
|
|
51
|
+
description: "**Offset** the whole outline outward (round corners) for a looser slip fit. 0 = none." },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
defaults: { span: 60, bar: 22, corner: 4, thickness: 4, hole_d: 5, inset: 8, center_d: 16, clip: 0, clearance: 0 },
|
|
56
|
+
parts: {
|
|
57
|
+
bracket: {
|
|
58
|
+
label: "Cross bracket",
|
|
59
|
+
views: ["bracket"],
|
|
60
|
+
export: { name: "cross-bracket" },
|
|
61
|
+
build: (k, p) => {
|
|
62
|
+
const barH = k.shape2d(roundedRectPolygon(p.span, p.bar, p.corner));
|
|
63
|
+
const barV = k.shape2d(roundedRectPolygon(p.bar, p.span, p.corner));
|
|
64
|
+
let plate = barH.union(barV); // union
|
|
65
|
+
if (p.clip) plate = plate.intersect(k.shape2d(circleProfile(p.span / 2))); // intersect
|
|
66
|
+
const d = p.span / 2 - p.inset;
|
|
67
|
+
const holes = [[d, d], [-d, d], [d, -d], [-d, -d]].map((c) => circleProfile(p.hole_d / 2, c));
|
|
68
|
+
plate = plate.cutAll(holes); // batch cut
|
|
69
|
+
if (p.center_d > 0) plate = plate.cut(k.shape2d(circleProfile(p.center_d / 2))); // cut
|
|
70
|
+
if (p.clearance) plate = plate.offset(p.clearance, { corners: "round" }); // offset
|
|
71
|
+
return k.extrude({ profile: plate, h: p.thickness });
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
views: { bracket: { label: "Cross bracket" } },
|
|
76
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Demo part — a lettered nameplate. Showcases k.text2d (the vector-text feature):
|
|
2
|
+
// two lines of text with counters and curves (P A R T F O R G E, digits) resolved to
|
|
3
|
+
// exact curve regions, then either raised above (emboss) or cut into (deboss) a
|
|
4
|
+
// rounded plate. The plate itself is a Shape2D rounded-rectangle, so the part also
|
|
5
|
+
// exercises shape2d + the extrude/boolean path. Open /nameplate.html after `npm run dev`.
|
|
6
|
+
import { roundedRectPolygon } from "partforge/geometry";
|
|
7
|
+
|
|
8
|
+
const LABEL = "PARTFORGE\nv0.20";
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
meta: { title: "Nameplate", units: "mm", background: 0x15181d },
|
|
12
|
+
parameters: [
|
|
13
|
+
{
|
|
14
|
+
id: "text",
|
|
15
|
+
title: "Lettering",
|
|
16
|
+
description: "The text is a two-line label (`PARTFORGE` over `v0.20`). It is resolved to exact glyph curves — counters in **A R O G** and the **0** stay open — and sized by cap height.",
|
|
17
|
+
advanced: [
|
|
18
|
+
{ key: "size", label: "Cap height", unit: "mm", min: 4, max: 16, step: 0.5,
|
|
19
|
+
description: "Height of the uppercase letters. The second line scales with it." },
|
|
20
|
+
{ key: "depth", label: "Relief depth", unit: "mm", min: 0.4, max: 3, step: 0.1,
|
|
21
|
+
description: "How far the lettering is raised above (emboss) or recessed into (engrave) the plate face." },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "plate",
|
|
26
|
+
title: "Plate",
|
|
27
|
+
description: "A rounded-rectangle backing plate, sized automatically from the text bounding box plus the border.",
|
|
28
|
+
advanced: [
|
|
29
|
+
{ key: "margin", label: "Border", unit: "mm", min: 2, max: 12, step: 0.5,
|
|
30
|
+
description: "Clear space between the lettering and the plate edge." },
|
|
31
|
+
{ key: "corner", label: "Corner radius", unit: "mm", min: 0, max: 10, step: 0.5,
|
|
32
|
+
description: "Rounding on the plate corners (clamped so it never exceeds half the shorter side)." },
|
|
33
|
+
{ key: "thickness", label: "Thickness", unit: "mm", min: 1.5, max: 8, step: 0.5,
|
|
34
|
+
description: "Plate thickness." },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "style",
|
|
39
|
+
title: "Style",
|
|
40
|
+
toggles: [
|
|
41
|
+
{ key: "engrave", label: "Engrave (recessed)", on: 1,
|
|
42
|
+
description: "Cut the lettering into the top face instead of raising it above the plate." },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
defaults: { size: 8, depth: 1.2, margin: 4, corner: 3, thickness: 3, engrave: 0 },
|
|
47
|
+
parts: {
|
|
48
|
+
plate: {
|
|
49
|
+
label: "Nameplate",
|
|
50
|
+
views: ["plate"],
|
|
51
|
+
export: { name: "nameplate" },
|
|
52
|
+
build: (k, p) => {
|
|
53
|
+
const text = k.text2d(LABEL, { size: p.size, align: "center", valign: "middle", lineHeight: p.size * 1.7 });
|
|
54
|
+
const bb = text.boundingBox();
|
|
55
|
+
const w = (bb.max[0] - bb.min[0]) + 2 * p.margin;
|
|
56
|
+
const h = (bb.max[1] - bb.min[1]) + 2 * p.margin;
|
|
57
|
+
const corner = Math.max(0, Math.min(p.corner, Math.min(w, h) / 2 - 0.5));
|
|
58
|
+
const plate = k.extrude({ profile: k.shape2d(roundedRectPolygon(w, h, corner)), h: p.thickness }).label("Plate");
|
|
59
|
+
const relief = k.extrude({ profile: text, h: p.depth });
|
|
60
|
+
return p.engrave
|
|
61
|
+
? plate.cut(relief.translate([0, 0, p.thickness - p.depth]))
|
|
62
|
+
: plate.union(relief.translate([0, 0, p.thickness]).label("Lettering"));
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
views: { plate: { label: "Nameplate" } },
|
|
67
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Dev/CI worker-safety fixture (Task 3): a real text-bearing part, wired through
|
|
2
|
+
// the actual app/worker path so scripts/check-app.mjs can boot it in real Chromium.
|
|
3
|
+
// This is the definitive proof that paper.js (via resolveCurveFill) actually
|
|
4
|
+
// executes inside the Vite geometry Web Worker, not just under Node/vitest.
|
|
5
|
+
export default {
|
|
6
|
+
meta: { title: "Text worker smoke", units: "mm", background: 0x15181d },
|
|
7
|
+
parameters: [],
|
|
8
|
+
defaults: {},
|
|
9
|
+
parts: {
|
|
10
|
+
text: {
|
|
11
|
+
label: "Text",
|
|
12
|
+
views: ["text"],
|
|
13
|
+
export: { name: "text-smoke" },
|
|
14
|
+
build: (k) => k.extrude({
|
|
15
|
+
profile: k.text2d("B8&", { size: 10, align: "center", valign: "middle" }),
|
|
16
|
+
h: 2,
|
|
17
|
+
}).label("Resolved text"),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
views: { text: { label: "Text" } },
|
|
21
|
+
};
|
package/src/testing/manifold.js
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
// in the same process — they crash together.)
|
|
4
4
|
import Module from "manifold-3d";
|
|
5
5
|
import { createManifoldKernel } from "../framework/geometry/manifold-backend.js";
|
|
6
|
+
import { resolveFonts } from "../framework/fonts.js";
|
|
6
7
|
|
|
7
|
-
export async function bootManifoldKernel({ quality = "preview" } = {}) {
|
|
8
|
+
export async function bootManifoldKernel({ quality = "preview", fonts } = {}) {
|
|
8
9
|
const wasm = await Module();
|
|
9
10
|
wasm.setup();
|
|
10
|
-
|
|
11
|
+
const kernel = createManifoldKernel(wasm, { quality });
|
|
12
|
+
if (fonts) { const opentype = (await import("opentype.js")).default;
|
|
13
|
+
for (const [name, buf] of await resolveFonts(fonts)) kernel._fonts.set(name, opentype.parse(buf)); }
|
|
14
|
+
return kernel;
|
|
11
15
|
}
|
package/src/testing/occt.js
CHANGED
|
@@ -5,8 +5,9 @@ import { fileURLToPath } from "url";
|
|
|
5
5
|
import path from "path";
|
|
6
6
|
import fs from "fs";
|
|
7
7
|
import { createOcctKernel } from "../framework/geometry/occt-backend.js";
|
|
8
|
+
import { resolveFonts } from "../framework/fonts.js";
|
|
8
9
|
|
|
9
|
-
export async function bootOcctKernel() {
|
|
10
|
+
export async function bootOcctKernel({ fonts } = {}) {
|
|
10
11
|
const require = createRequire(import.meta.url);
|
|
11
12
|
globalThis.require = globalThis.require ?? require;
|
|
12
13
|
globalThis.__dirname = globalThis.__dirname ?? path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -14,5 +15,8 @@ export async function bootOcctKernel() {
|
|
|
14
15
|
const OC = await init({ wasmBinary: fs.readFileSync(require.resolve("replicad-opencascadejs/src/replicad_single.wasm")) });
|
|
15
16
|
const replicad = await import("replicad");
|
|
16
17
|
replicad.setOC(OC);
|
|
17
|
-
|
|
18
|
+
const kernel = createOcctKernel(replicad);
|
|
19
|
+
if (fonts) { const opentype = (await import("opentype.js")).default;
|
|
20
|
+
for (const [name, buf] of await resolveFonts(fonts)) kernel._fonts.set(name, opentype.parse(buf)); }
|
|
21
|
+
return kernel;
|
|
18
22
|
}
|