partforge 0.115.0 → 0.117.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 +18 -0
- package/docs/ERROR-PATTERNS.md +6 -0
- package/docs/KERNEL-CONTRACT.md +15 -1
- package/package.json +1 -1
- package/src/framework/geometry/circle-segs.js +77 -7
- package/src/framework/geometry/manifold-backend.js +22 -7
- package/src/framework/geometry/mesh-fillet.js +2 -2
- package/src/framework/geometry/mesh-roundall.js +5 -4
- package/src/framework/geometry/rounded-solids.js +17 -5
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -3650,6 +3650,24 @@ symptom first** — it maps error text → cause → fix. The invariants, one li
|
|
|
3650
3650
|
is a part that exports: the old flat 480 turned a 0.75 mm rivet into 115,200 triangles
|
|
3651
3651
|
and a body with a few hundred of them into an out-of-memory trap at export
|
|
3652
3652
|
([export-kernel-out-of-memory](ERROR-PATTERNS.md#export-kernel-out-of-memory)).
|
|
3653
|
+
**Doubly-curved surfaces are the exception on both tiers:** a sphere, a lathe's
|
|
3654
|
+
profile arcs (`torus`, `roundedCylinder`, a `revolve` of your own rounded `Shape2D`)
|
|
3655
|
+
and a `roundedBox`'s corners all spend the segment count squared (6,728 triangles
|
|
3656
|
+
for any sphere at 116; 27,000 for a tiny O-ring), so they are sized by chord
|
|
3657
|
+
tolerance — 0.02 mm at preview, never fewer than 24 segments per circle (a rivet
|
|
3658
|
+
sphere is 288 triangles, an O-ring ~6,000) and never more than the flat count, so
|
|
3659
|
+
only features under about 60 mm radius get coarser and none get finer. That is what
|
|
3660
|
+
keeps a body studded with a few hundred rivets inside a phone's memory
|
|
3661
|
+
([preview-build-too-heavy-for-phones](ERROR-PATTERNS.md#preview-build-too-heavy-for-phones)).
|
|
3662
|
+
Two consequences to know: a lathe's sweep still runs at the flat count, so a small
|
|
3663
|
+
rounded cylinder is cheaper as `roundedCylinder` than as a revolve you densify by
|
|
3664
|
+
hand; and a tolerance-sized surface measures slightly smaller than the exact one
|
|
3665
|
+
(about 0.8% on a 3 mm rounded feature), so give a `verify` volume gate on small
|
|
3666
|
+
rounded parts that much slack. Spheres and rounded features are still the costliest
|
|
3667
|
+
way to add small detail — a domed rivet is 288 triangles where a short cylinder is
|
|
3668
|
+
232 and a box is 12, and every one is a boolean operand — so prefer instancing one
|
|
3669
|
+
union of a row over a chain of per-feature booleans, and drop counts the print
|
|
3670
|
+
cannot show.
|
|
3653
3671
|
- **Display placement is view-independent**; only `place(..., { purpose: "export" })` may
|
|
3654
3672
|
depend on `view` ([view-dependent-display-place](ERROR-PATTERNS.md#view-dependent-display-place)).
|
|
3655
3673
|
- **Keep geometry backend-agnostic** (kernel calls only); only STEP requires OCCT
|
package/docs/ERROR-PATTERNS.md
CHANGED
|
@@ -829,6 +829,12 @@ between the Manifold preview and the OCCT STEP export.
|
|
|
829
829
|
- **Cause:** The mesh kernel's WASM heap ran out while building the print-quality mesh, and a WASM trap leaves that kernel instance corrupt — every later call into it fails until the worker is replaced or the page reloaded. Before partforge 0.115 the print tier meshed every circle at a flat 480 segments whatever its radius, so a part with a few hundred small spheres or cylinders (a 0.75 mm rivet was 115,200 triangles — 176 of them are 20 M before a single boolean) exhausted a 4 GB heap on its first export while its whole unioned preview was a few hundred thousand triangles. Since 0.115 print sizes circles by a 0.01 mm chord tolerance, floored at the preview count, so a part that previews normally exports at roughly the preview's cost; a part that still traps is genuinely too heavy for the browser at ANY quality — usually thousands of repeated small features, or a boolean chain whose intermediates dwarf the result.
|
|
830
830
|
- **Fix:** Reload the page (or let the host replace the kernel worker) before retrying anything — the trapped instance cannot recover. Then reduce what the export has to hold at once: build repeated detail as one union of instances rather than a chain of per-feature booleans, drop feature counts that exceed what the print can show (a 0.75 mm sphere prints as a dot), or pass `segs` to `revolve` where a coarser sweep is acceptable ([Preview vs print quality](AUTHORING-PARTS.md#conventions--gotchas)). Do NOT strip visible detail from the part to dodge a pre-0.115 trap — update partforge instead; the geometry was never the problem.
|
|
831
831
|
|
|
832
|
+
## preview-build-too-heavy-for-phones
|
|
833
|
+
|
|
834
|
+
- **Symptom:** A part that previews on a desktop crashes, reloads, or shows a blank viewer on phones — iOS Safari's "This webpage was reloaded because a problem occurred", a viewer that never finishes its first build, or (in partforge-cloud) a `sandbox_timeout` from a phone user agent — with no error text at all, because the browser killed the page rather than the build throwing.
|
|
835
|
+
- **Cause:** Peak WASM memory during the preview build exceeded what the phone allows a page (roughly 1–1.5 GB on iOS; a desktop tolerates several GB). The usual shape is hundreds of repeated small features unioned into one body: before partforge 0.116 every sphere was 6,728 triangles regardless of radius (the flat 116-segment preview count, squared), so a body with ~250 rivet spheres carried 1.7 M triangles of rivets into its booleans — measured 10 s and a 2.7 GB peak on a desktop for a 400k-triangle sub-part, against 4 s and 500 MB with the rivets as 12-triangle boxes. A boolean chain whose intermediates dwarf the result (a full-body skin intersected per groove) is the other shape.
|
|
836
|
+
- **Fix:** Update partforge (0.116 sizes spheres by chord tolerance: that body builds in ~550 MB and 120k triangles unchanged; 0.117 extends the rule to lathe profile arcs — `torus`, `roundedCylinder`, revolved rounded profiles — and `roundedBox` corners, the other two shapes that spend the count squared). Then keep the preview build's peak down the same ways the export needs: build repeated detail as one union of instances rather than a chain of per-feature booleans, cut grooves from a thin local skin rather than the whole envelope, and drop feature counts the print cannot show ([Preview vs print quality](AUTHORING-PARTS.md#conventions--gotchas)). `partforge measure` reports triangle counts per sub-part; a single preview sub-part past ~200k triangles, or a whole view past ~400k, is the range where phones start to fail.
|
|
837
|
+
|
|
832
838
|
# Hardware library
|
|
833
839
|
|
|
834
840
|
Reserved for `hardware-*` patterns (issue #30). No entries yet.
|
package/docs/KERNEL-CONTRACT.md
CHANGED
|
@@ -380,7 +380,21 @@ backends both define `print` as a **chord tolerance of 0.01 mm** (OCCT's linear
|
|
|
380
380
|
deflection; Manifold's per-circle segment rule in `geometry/circle-segs.js`, floored at
|
|
381
381
|
the preview's 116 segments so print is never coarser than preview and capped at 480), so
|
|
382
382
|
a small feature costs the export exactly what its preview cost — the property that makes
|
|
383
|
-
"if it previews, it exports" hold. Preview is a flat 116 on Manifold, a visual choice
|
|
383
|
+
"if it previews, it exports" hold. Preview is a flat 116 on Manifold, a visual choice —
|
|
384
|
+
except for the DOUBLY-CURVED family, whose triangle count is quadratic in the segment
|
|
385
|
+
count: `sphere` (8·(n/4)² triangles), a lathe's profile arcs (`revolve` of a
|
|
386
|
+
`Shape2D`, so `torus`, `roundedCylinder` and any hand-drawn rounded profile — every
|
|
387
|
+
arc sample becomes a full ring of the sweep) and a `roundedBox`'s corners (sphere
|
|
388
|
+
octants). Those are sized by chord tolerance on BOTH tiers (`doubleCurvatureSegs` in
|
|
389
|
+
`geometry/circle-segs.js`: 0.02 mm at preview, floored at 24 segments and capped at
|
|
390
|
+
the flat 116; 0.01 mm at print, floored at the preview count, capped at 480). At the
|
|
391
|
+
flat count a 0.75 mm rivet sphere was 6,728 triangles and an O-ring of that tube
|
|
392
|
+
radius 27,376, and a part carrying a few hundred rivets was a 2.7 GB preview build
|
|
393
|
+
that phones could not survive; at the floor they are 288 and ~6,000. A tolerance-sized
|
|
394
|
+
surface reads slightly smaller than the exact one (about 2π²/(3n²): 0.8% on a 3 mm
|
|
395
|
+
tube at 28 segments), which `measure` reports faithfully. A lathe's SWEEP, and
|
|
396
|
+
circles in extrusions, cylinders and outlines, keep the flat count, which the mesh
|
|
397
|
+
fillet's arc gate and the roundAll fast path are tuned to.
|
|
384
398
|
|
|
385
399
|
### Shading intent (toMesh normals and edges)
|
|
386
400
|
|
package/package.json
CHANGED
|
@@ -35,22 +35,92 @@
|
|
|
35
35
|
// helix tube's station/ring counts (TUBE in manifold-backend.js) and mesh-fillet's
|
|
36
36
|
// blend bands keep their own sizing; loft rings keep LOFT_SEGS.
|
|
37
37
|
//
|
|
38
|
+
// The exceptions are the doubly-curved surfaces — sphere, a lathe's profile arcs, a
|
|
39
|
+
// rounded box's corners — which have their own rule at the bottom of this file.
|
|
40
|
+
//
|
|
38
41
|
// Pure, dependency-free: profile.js's samplers take a `(r) => n` function in place of
|
|
39
42
|
// a count, and this is what the backend hands them.
|
|
40
43
|
|
|
41
44
|
export const SEGS = { preview: 116, print: 480 }; // full-circle segments (flat / cap)
|
|
42
45
|
export const SAGITTA_TOL = { print: 0.01 }; // mm — max chord sagitta per tier
|
|
43
46
|
|
|
47
|
+
// THE formula, shared by every tolerance-sized circle in the mesh backend: the fewest
|
|
48
|
+
// full-circle segments that keep the chord sagitta r·(1 − cos(π/n)) under `tol`,
|
|
49
|
+
// clamped to [floor, cap]. acos(1 − tol/r) is the half-angle of a chord with sagitta
|
|
50
|
+
// tol; π over it is the full-circle count. r = Infinity gives acos(1) = 0 → Infinity →
|
|
51
|
+
// the cap. A degenerate radius (0, negative, NaN, undefined) or one no larger than the
|
|
52
|
+
// tolerance takes the floor and never throws. The callers differ only in POLICY — which
|
|
53
|
+
// tolerance, which clamps — and that is all they should ever add: `circleSegs` and
|
|
54
|
+
// `sphereSegs` below, mesh-fillet's blendSegs and mesh-roundall's roundAllSegs.
|
|
55
|
+
export function segsForSagitta(r, tol, floor, cap) {
|
|
56
|
+
if (!(r > tol)) return floor;
|
|
57
|
+
return Math.min(cap, Math.max(floor, Math.ceil(Math.PI / Math.acos(1 - tol / r))));
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
// Segments per full circle for a circle of radius `r` at `quality`. A tier with no
|
|
45
|
-
// tolerance (preview, or an unknown tier) is flat
|
|
46
|
-
//
|
|
61
|
+
// tolerance (preview, or an unknown tier) is flat; print is sized by tolerance, floored
|
|
62
|
+
// at the preview count and capped at its own.
|
|
47
63
|
export function circleSegs(r, quality) {
|
|
48
64
|
const cap = SEGS[quality] ?? SEGS.preview;
|
|
49
65
|
const tol = SAGITTA_TOL[quality];
|
|
50
66
|
if (tol === undefined) return cap;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
return segsForSagitta(r, tol, SEGS.preview, cap);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// DOUBLE CURVATURE — the family whose triangle count is QUADRATIC in the segment
|
|
71
|
+
// count, so the per-circle budget above, spent on one of them, is spent squared:
|
|
72
|
+
//
|
|
73
|
+
// - `sphere`: Manifold.sphere(r, n) subdivides an octahedron n/4 times per edge,
|
|
74
|
+
// 8·(n/4)² triangles. At the flat preview 116 every sphere was 6,728 triangles
|
|
75
|
+
// whatever its radius; a 0.75 mm rivet carried a chord error of 0.0003 mm, a
|
|
76
|
+
// hundredth of a screen pixel at any zoom. A part with ~250 such rivets (the
|
|
77
|
+
// steampunk-spider feedback, "not loading on phones") unioned 1.7 M triangles of
|
|
78
|
+
// rivets into a 100k body: 10 s and a 2.7 GB peak on a desktop, which iOS Safari's
|
|
79
|
+
// content process does not survive — at 12 triangles each the same body was 4 s
|
|
80
|
+
// and 500 MB.
|
|
81
|
+
// - a lathe's PROFILE ARCS (`revolve` of a Shape2D — `torus`, `roundedCylinder`, a
|
|
82
|
+
// hand-drawn rounded profile): every arc sample becomes a full ring of the sweep,
|
|
83
|
+
// so an O-ring of 0.75 mm tube radius was 27,376 triangles. The sweep itself stays
|
|
84
|
+
// on the circle rule (its rim is what the fillet machinery reads).
|
|
85
|
+
// - a `roundedBox`'s corners: sphere octants, sampled per quarter arc AND per rim
|
|
86
|
+
// station, so a 3 mm rounded cube was 4,092 triangles (roundedBoxArcSamples in
|
|
87
|
+
// rounded-solids.js turns this count into that sample budget).
|
|
88
|
+
//
|
|
89
|
+
// All three are sized by chord tolerance on BOTH tiers, like print circles are:
|
|
90
|
+
//
|
|
91
|
+
// - preview holds DOUBLE_CURVATURE_SAGITTA_TOL.preview (0.02 mm — an absolute chord
|
|
92
|
+
// error is what screen pixels measure, so one tolerance reads equally smooth at
|
|
93
|
+
// every radius and every zoom; 0.02 mm is a fifth of a pixel at a typical
|
|
94
|
+
// 100 mm-part zoom), floored at DOUBLE_CURVATURE_FLOOR segments (24 — ~15° facets,
|
|
95
|
+
// so a tiny ball never reads as a polygon under close zoom; a rivet sphere is 288
|
|
96
|
+
// triangles instead of 6,728) and capped at the old flat count (nothing gets FINER
|
|
97
|
+
// than before; a radius of 60 mm and up keeps exactly the density it always had).
|
|
98
|
+
// - print holds the print tier's 0.01 mm, floored at the preview count for the same
|
|
99
|
+
// radius (never coarser than the preview the user approved — the same property the
|
|
100
|
+
// circle rule keeps) and capped at 480.
|
|
101
|
+
//
|
|
102
|
+
// A tolerance-sized surface reads a little SMALLER than the exact one — an inscribed
|
|
103
|
+
// polygon's area deficit is about 2π²/(3n²), so 0.8% on a 3 mm tube at 28 segments
|
|
104
|
+
// — and `measure` reports what the mesh holds. Gates on small rounded features should
|
|
105
|
+
// carry that slack; print-scale features are unaffected at the cap.
|
|
106
|
+
//
|
|
107
|
+
// Circles in extrusions, cylinders, 2-D outlines and a lathe's SWEEP stay on the flat
|
|
108
|
+
// preview count on purpose: their cost is linear in the count, and the mesh fillet's
|
|
109
|
+
// arc gate, the roundAll prism fast path and the shading policies are all tuned to
|
|
110
|
+
// that density — a spike that made EVERY preview circle tolerance-based (0.05 mm,
|
|
111
|
+
// floor 24) turned bore-rim fillets from revolve tools into planar sweeps and drew 26
|
|
112
|
+
// feature lines across a roundAll band that had none. None of the three surfaces
|
|
113
|
+
// above has a sharp edge of its own, and roundAll sizes its own balls (roundAllSegs),
|
|
114
|
+
// so this rule touches nothing tuned to 116.
|
|
115
|
+
export const DOUBLE_CURVATURE_SAGITTA_TOL = { preview: 0.02, print: SAGITTA_TOL.print }; // mm
|
|
116
|
+
export const DOUBLE_CURVATURE_FLOOR = 24; // segments
|
|
117
|
+
|
|
118
|
+
// Segments per full circle for a doubly-curved feature of radius `r` at `quality`: the
|
|
119
|
+
// fewest that keep the chord sagitta under the tier's tolerance, clamped as described
|
|
120
|
+
// above. An unknown tier facets as preview; a degenerate radius takes the floor and
|
|
121
|
+
// never throws.
|
|
122
|
+
export function doubleCurvatureSegs(r, quality) {
|
|
123
|
+
const tier = Object.hasOwn(DOUBLE_CURVATURE_SAGITTA_TOL, quality ?? "") ? quality : "preview";
|
|
124
|
+
const floor = tier === "preview" ? DOUBLE_CURVATURE_FLOOR : doubleCurvatureSegs(r, "preview");
|
|
125
|
+
return segsForSagitta(r, DOUBLE_CURVATURE_SAGITTA_TOL[tier], floor, SEGS[tier]);
|
|
56
126
|
}
|
|
@@ -2,7 +2,7 @@ import { helixTube } from "./helix-tube.js";
|
|
|
2
2
|
import { loftMesh } from "./loft.js";
|
|
3
3
|
import { resolveLoftRings, loftRingsKey } from "./loft-rings.js";
|
|
4
4
|
import { sweepMesh } from "./sweep.js";
|
|
5
|
-
import { roundedBoxRings } from "./rounded-solids.js";
|
|
5
|
+
import { roundedBoxRings, roundedBoxArcSamples } from "./rounded-solids.js";
|
|
6
6
|
import { tessellateContour, tessellateProfile } from "./profile.js";
|
|
7
7
|
import { h } from "./solid-hash.js";
|
|
8
8
|
import { ensureOutward, openEdgeCount } from "./mesh-repair.js";
|
|
@@ -19,7 +19,7 @@ import { creasedNormals } from "./creased-normals.js";
|
|
|
19
19
|
import { loftShadingPolicy, SMOOTH, BLEND } from "./shading-policy.js";
|
|
20
20
|
import { meshFillet, meshChamfer, UnsupportedEdgeError } from "./mesh-fillet.js";
|
|
21
21
|
import { meshRoundAll, prismSection, roundAllSegs } from "./mesh-roundall.js";
|
|
22
|
-
import { SEGS, circleSegs } from "./circle-segs.js";
|
|
22
|
+
import { SEGS, circleSegs, doubleCurvatureSegs } from "./circle-segs.js";
|
|
23
23
|
import { checkBooleanResult } from "./boolean-gate.js";
|
|
24
24
|
import { KernelCapabilityError } from "./errors.js";
|
|
25
25
|
import { heightfieldMesh, hashGridData } from "./heightfield.js";
|
|
@@ -51,6 +51,9 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
51
51
|
// Per-radius count for every site that facets a circle it knows the radius of; the
|
|
52
52
|
// samplers behind prism/extrude/Shape2D take it as a function (profile.js).
|
|
53
53
|
const segsAt = (r) => circleSegs(r, quality);
|
|
54
|
+
// The doubly-curved family's own per-radius count (sphere, a lathe's profile arcs,
|
|
55
|
+
// a rounded box's corners) — circle-segs.js explains why they are sized apart.
|
|
56
|
+
const dcAt = (r) => doubleCurvatureSegs(r, quality);
|
|
54
57
|
|
|
55
58
|
// Manifold/CrossSection are WASM objects with no garbage collection — every
|
|
56
59
|
// primitive and boolean op allocates a new one. Track them all and free them
|
|
@@ -187,6 +190,14 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
187
190
|
const cs = T(CrossSection.ofPolygons(regionPolys(shape._regions, segsAt), "EvenOdd"));
|
|
188
191
|
return { value: cs, pin: cs, dispose: () => cs.delete?.() };
|
|
189
192
|
});
|
|
193
|
+
// The same shape as a LATHE profile: its arcs sampled at the double-curvature count
|
|
194
|
+
// (every sample becomes a full ring of the sweep — the quadratic cost circle-segs.js
|
|
195
|
+
// describes), memoized apart from the extrude-facing tessellation above under its
|
|
196
|
+
// own key. Keyed on the tier, not a count: the count varies per arc radius.
|
|
197
|
+
const csForLathe = (shape) => cache.lookup(h("cs2d-lathe", shape._hash, quality), () => {
|
|
198
|
+
const cs = T(CrossSection.ofPolygons(regionPolys(shape._regions, dcAt), "EvenOdd"));
|
|
199
|
+
return { value: cs, pin: cs, dispose: () => cs.delete?.() };
|
|
200
|
+
});
|
|
190
201
|
|
|
191
202
|
// Copy the mesh out into JS-owned arrays (so it survives cleanup) and free the
|
|
192
203
|
// transient mesh handle.
|
|
@@ -653,13 +664,17 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
653
664
|
// spec). Reuses loftMesh's stitch/cap/winding machinery. Atomic cache
|
|
654
665
|
// node hashed from its own args, like boredCylinder.
|
|
655
666
|
roundedBox: ({ size, center, round }) => cached(
|
|
656
|
-
h("roundedBox", size, center, round.side, round.top, round.bottom, segsAt(Math.max(round.side, round.top, round.bottom))),
|
|
667
|
+
h("roundedBox", size, center, round.side, round.top, round.bottom, roundedBoxArcSamples(segsAt(Math.max(round.side, round.top, round.bottom)), dcAt(Math.max(round.side, round.top, round.bottom)))),
|
|
657
668
|
() => {
|
|
658
669
|
// One count serves every corner and rim arc, so it is sized for the largest.
|
|
659
|
-
const
|
|
670
|
+
const rMax = Math.max(round.side, round.top, round.bottom);
|
|
671
|
+
const solid = T(loftMesh(wasm, roundedBoxRings(size, round, segsAt(rMax), roundedBoxArcSamples(segsAt(rMax), dcAt(rMax)))));
|
|
660
672
|
return center ? T(solid.translate([0, 0, -size[2] / 2])) : solid;
|
|
661
673
|
}),
|
|
662
|
-
|
|
674
|
+
// Sized by the double-curvature rule (circle-segs.js: a sphere spends the per-circle
|
|
675
|
+
// count squared), on both tiers — the hash carries the count, so a sphere built at
|
|
676
|
+
// one tier never masquerades as the other's in the cache.
|
|
677
|
+
sphere: (r) => wrap(T(Manifold.sphere(r, dcAt(r))), h("sphere", r, dcAt(r))),
|
|
663
678
|
box: (min, max) => {
|
|
664
679
|
const cube = T(Manifold.cube([max[0] - min[0], max[1] - min[1], max[2] - min[2]]));
|
|
665
680
|
return wrap(T(cube.translate(min)), h("box", min, max));
|
|
@@ -779,8 +794,8 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
779
794
|
if (pts && pts._shape2d) {
|
|
780
795
|
// Keyed on the override, not the resolved density, so a cache hit never
|
|
781
796
|
// materializes the CrossSection just to measure its bounds.
|
|
782
|
-
return cached(h("revolve", pts._hash, degrees, segsOverride ?? null), () => {
|
|
783
|
-
const cs =
|
|
797
|
+
return cached(h("revolve", pts._hash, degrees, segsOverride ?? null, quality), () => {
|
|
798
|
+
const cs = csForLathe(pts); // profile arcs at the double-curvature count; the sweep below stays on the circle rule
|
|
784
799
|
const b = cs.bounds();
|
|
785
800
|
return T(cs.revolve(densityFor(Math.max(Math.abs(b.min[0]), Math.abs(b.max[0]))), degrees));
|
|
786
801
|
});
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
// `dir` only ever matches straight chains, like replicad's inDirection.
|
|
44
44
|
// Pure module: no DOM, no node:, no three — safe anywhere in the worker graph.
|
|
45
45
|
import { sweepSeedFrame } from "./sweep.js";
|
|
46
|
+
import { segsForSagitta } from "./circle-segs.js";
|
|
46
47
|
|
|
47
48
|
const TOL = 1e-4; // selector / coplanarity tolerance (mm)
|
|
48
49
|
const WELD = 1e6; // vertex weld quantization (1/WELD mm grid)
|
|
@@ -64,8 +65,7 @@ export class UnsupportedEdgeError extends Error {
|
|
|
64
65
|
// angle (≤30°) under the viewer's 35° same-surface crease threshold.
|
|
65
66
|
const BLEND_SAG = 1e-3; // mm — max chord sagitta of a blend cross-section
|
|
66
67
|
function blendSegs(segs, r) {
|
|
67
|
-
|
|
68
|
-
return Math.min(segs, Math.max(12, Math.ceil(Math.PI / Math.acos(1 - s / r))));
|
|
68
|
+
return segsForSagitta(r, Math.min(BLEND_SAG, 0.02 * r), 12, segs);
|
|
69
69
|
}
|
|
70
70
|
// One derivation for a synthetic corner arc's angular density, shared by revolveTool
|
|
71
71
|
// (which sweeps at it) and cornerHornTool (whose apothem bound below depends on it) —
|
|
@@ -28,13 +28,14 @@
|
|
|
28
28
|
// is sized from the erosion ball (2r), the larger of the two, so the coarser of
|
|
29
29
|
// the two facetings still meets the tier's sagitta tolerance.
|
|
30
30
|
|
|
31
|
+
import { segsForSagitta } from "./circle-segs.js";
|
|
32
|
+
|
|
31
33
|
// Sphere tessellation from the facet sagitta r·(1 − cos(π/segs)): pick the
|
|
32
|
-
// fewest segments that keep it under the quality tier's tolerance
|
|
34
|
+
// fewest segments that keep it under the quality tier's tolerance — its own
|
|
35
|
+
// tolerance table and 12..64 window, on the shared formula.
|
|
33
36
|
const SAGITTA_TOL = { preview: 0.05, print: 0.01 }; // mm
|
|
34
37
|
export function roundAllSegs(r, quality) {
|
|
35
|
-
|
|
36
|
-
if (!(r > tol)) return 12;
|
|
37
|
-
return Math.min(64, Math.max(12, Math.ceil(Math.PI / Math.acos(1 - tol / r))));
|
|
38
|
+
return segsForSagitta(r, SAGITTA_TOL[quality] ?? SAGITTA_TOL.preview, 12, 64);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function meshRoundAll(wasm, m, r, quality) {
|
|
@@ -34,13 +34,25 @@ export function roundedRectRing(hw, hd, rc, A) {
|
|
|
34
34
|
return pts;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Quarter-arc sample count for a rounded box's corners (and z-stations per rim
|
|
38
|
+
// zone): the corners are sphere octants, so they take the double-curvature count
|
|
39
|
+
// for the corner radius, a quarter of it per 90° arc — the facet angle the sphere
|
|
40
|
+
// floor itself has (24 → 6 → 15°). Capped at what the flat circle count used to
|
|
41
|
+
// give (ceil(circle/8): 15 at preview, up to 49 at a metre-scale print), so no
|
|
42
|
+
// rounded box gets finer than before and a large one keeps exactly its old
|
|
43
|
+
// density. Floor 2: the builder's own minimum for a valid ring.
|
|
44
|
+
export function roundedBoxArcSamples(circleSegs, doubleCurvatureSegs) {
|
|
45
|
+
return Math.max(2, Math.min(Math.ceil(circleSegs / 8), Math.ceil(doubleCurvatureSegs / 4)));
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
// Ring stack for the Manifold roundedBox: ascending-z loft ring specs
|
|
38
49
|
// [{ polygon, z }]. A (arc samples per corner AND z-stations per rim zone) is
|
|
39
|
-
// derived from the kernel's segs
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
// derived from the kernel's segs — or passed outright as `arcSamples` (the
|
|
51
|
+
// backend hands in roundedBoxArcSamples) — so the z-sampling matches the
|
|
52
|
+
// in-plane LOD. Consecutive duplicate stations (top + bottom = h) are deduped
|
|
53
|
+
// so the loft never sees a zero-height band.
|
|
54
|
+
export function roundedBoxRings([w, d, h], { side, top, bottom }, segs, arcSamples = Math.max(2, Math.ceil(segs / 8))) {
|
|
55
|
+
const A = arcSamples;
|
|
44
56
|
const st = [];
|
|
45
57
|
const push = (z, delta) => {
|
|
46
58
|
const last = st[st.length - 1];
|