partforge 0.163.0 → 0.164.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/bin/cli.js +9 -5
- package/docs/AUTHORING-PARTS.md +195 -36
- package/docs/ERROR-PATTERNS.md +12 -0
- package/package.json +1 -1
- package/src/framework/geometry/occt-backend.js +14 -1
- package/src/framework/jobs.js +7 -1
- package/src/framework/kernel-error.js +161 -0
- package/src/framework/lint/rules-verify.js +35 -2
- package/src/framework/oracle/bed-contact.js +94 -12
- package/src/framework/oracle/dfm-profiles.js +42 -51
- package/src/framework/oracle/gates.js +7 -1
- package/src/framework/oracle/measure.js +40 -15
- package/src/framework/oracle/verify.js +44 -20
- package/src/framework/process/fdm/descriptor.js +48 -0
- package/src/framework/process/registry.js +30 -0
- package/src/framework/process/resin/descriptor.js +19 -0
- package/src/framework/verify-metrics.js +17 -6
- package/src/framework/worker.js +7 -2
- package/types/part.d.ts +23 -9
- package/types/testing.d.ts +17 -3
package/bin/cli.js
CHANGED
|
@@ -559,14 +559,17 @@ function printVerify(v) {
|
|
|
559
559
|
for (const c of v.cases) {
|
|
560
560
|
console.log(` ${c.name}`);
|
|
561
561
|
for (const ch of c.checks) {
|
|
562
|
-
|
|
563
|
-
|
|
562
|
+
// A cost has a price (supports, a brim), so it gets `$`, not a warning sign.
|
|
563
|
+
const icon = ch.status === "pass" ? "✓" : ch.status === "fail" ? "✗" : ch.status === "warn" ? "⚠"
|
|
564
|
+
: ch.status === "cost" ? "$" : "·";
|
|
565
|
+
console.log(` ${icon} ${ch.subpart ?? "_view"} ${ch.metric} ${ch.expr} (${ch.message})${ch.accepted ? " (accepted)" : ""}`);
|
|
564
566
|
// A measurement caveat prints whatever the verdict — "passed, but sampled"
|
|
565
567
|
// is precisely the line a reader must not miss.
|
|
566
568
|
if (ch.note) console.log(` note: ${ch.note}`);
|
|
567
|
-
if (ch.status === "fail" || ch.status === "warn") {
|
|
569
|
+
if (ch.status === "fail" || ch.status === "warn" || ch.status === "cost") {
|
|
568
570
|
if (ch.location) console.log(` at [${ch.location.map((n) => n.toFixed(1)).join(", ")}]`);
|
|
569
|
-
|
|
571
|
+
// An accepted cost's remedy is moot: the user already chose to pay it.
|
|
572
|
+
if (ch.hint && !ch.accepted) console.log(` hint: ${ch.hint}${ch.pattern ? ` (${patternRef(ch.pattern)})` : ""}`);
|
|
570
573
|
}
|
|
571
574
|
}
|
|
572
575
|
}
|
|
@@ -584,7 +587,8 @@ function printVerify(v) {
|
|
|
584
587
|
: v.unevaluated?.length ? `verdict withheld (${v.unevaluated.length} check(s) not evaluated)`
|
|
585
588
|
: v.declared ? "nothing verified (every declared check skipped)"
|
|
586
589
|
: "nothing verified (no expectations declared)";
|
|
587
|
-
|
|
590
|
+
const c = v.costs?.length ?? 0;
|
|
591
|
+
console.log(` result: ${verdict}${w ? `, ${w} warning(s)` : ""}${c ? `, ${c} cost(s)` : ""}`);
|
|
588
592
|
}
|
|
589
593
|
|
|
590
594
|
function printLint(r) {
|
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -1919,6 +1919,128 @@ that tessellates nothing, unlike `toRegions()`), `fillet(r, opts?)`, `chamfer(di
|
|
|
1919
1919
|
opts?)`, `simplify(tolerance)`, `corners()`, `contains([x,y])`, `isEmpty()` (no
|
|
1920
1920
|
regions left — see the vanishing-features rule above).
|
|
1921
1921
|
|
|
1922
|
+
## Printed parts
|
|
1923
|
+
|
|
1924
|
+
3D-printed parts: FDM (filament) and resin. Everything that is not a sheet
|
|
1925
|
+
part is printed, but a part says so on purpose by declaring its print process in
|
|
1926
|
+
`verify.process`; that one key arms the process's print checks below from the first
|
|
1927
|
+
build (all of them under FDM; bed fit and min wall under `resin`). Name
|
|
1928
|
+
the process the user will print with, never one written by habit: it is the declaration
|
|
1929
|
+
the checks, the fits and the advice all read.
|
|
1930
|
+
|
|
1931
|
+
### Declaring the process
|
|
1932
|
+
|
|
1933
|
+
`process` is a profile id — `fdm-pla`, `fdm-petg` (the `fdm` process) or `resin` (the
|
|
1934
|
+
`resin` process) — or an inline object whose `base` names one and whose other keys
|
|
1935
|
+
override it: `{ base: "fdm-pla", bed: [256, 256, 256] }` for a bigger printer,
|
|
1936
|
+
`{ base: "fdm-pla", fits: { … } }` for a calibrated one (`fits` replaces the whole
|
|
1937
|
+
table). The profile numbers are in the `verify` section's table below. `accept` lists
|
|
1938
|
+
the costs the user has chosen to pay:
|
|
1939
|
+
|
|
1940
|
+
```js
|
|
1941
|
+
verify: {
|
|
1942
|
+
process: "fdm-pla",
|
|
1943
|
+
accept: ["supports"], // the user chose supports for the organic body
|
|
1944
|
+
expect: { body: { holes: 1 }, _view: { overlaps: 0 } },
|
|
1945
|
+
}
|
|
1946
|
+
```
|
|
1947
|
+
|
|
1948
|
+
### Gates, costs and advisories
|
|
1949
|
+
|
|
1950
|
+
Every print check is in exactly one tier, decided by what the reading means:
|
|
1951
|
+
|
|
1952
|
+
| tier | checks | effect on `verify.ok` |
|
|
1953
|
+
| --- | --- | --- |
|
|
1954
|
+
| **gate** — a defect in any orientation | bed fit (`bbox` within `bed`), bed contact (`bedContactPct` ≥ 50) | fails it |
|
|
1955
|
+
| **cost** — a design choice with a price | overhang (`overhangArea`, id `supports`), footprint (`footprintPct` ≥ 10, id `footprint`) | never |
|
|
1956
|
+
| **advisory** — a reading that may not matter | min wall (`minWall`), `wall` | never; a warning |
|
|
1957
|
+
|
|
1958
|
+
A **gate** is fixed before the work is done: the piece cannot be printed this way
|
|
1959
|
+
whatever the user wants. A **cost** is a fact with a number and a location — mm² of
|
|
1960
|
+
overhang on a named piece and where it sits, or the share of a piece's silhouette on
|
|
1961
|
+
the bed that actually touches it — listed in `verify().costs` (never in `warnings`) with `status: "cost"`. It
|
|
1962
|
+
does not block a part still being shaped; it informs. Say each cost to the user once,
|
|
1963
|
+
in one sentence naming the piece, the number and the alternative ("the arm overhangs
|
|
1964
|
+
by 840 mm² and prints on supports; splitting it at the elbow avoids them"), and record
|
|
1965
|
+
the answer in `accept`. An **advisory** is read, and acted on only when
|
|
1966
|
+
it is real (a thin wall the user will hold, not a sliver at a facet seam).
|
|
1967
|
+
|
|
1968
|
+
**Accepting a cost is not switching it off.** An accepted cost is still measured and
|
|
1969
|
+
still listed in `costs`, with `accepted: true`, so the number stays visible; it is
|
|
1970
|
+
simply no longer something to raise. Switching a measurement off is the inline
|
|
1971
|
+
profile form, `{ base: "fdm-pla", overhang: null }` (or `footprint: null`,
|
|
1972
|
+
`bedContact: null`), and is for a part where the reading is meaningless, not unwelcome.
|
|
1973
|
+
A threshold the user named ("under 500 mm² of supports") is an `expect`:
|
|
1974
|
+
`overhangArea: "<=500"` — still a cost, reported in `costs` and never failing `ok`;
|
|
1975
|
+
the user's own limit keeps its meaning, so it is never marked accepted, whatever
|
|
1976
|
+
`accept` says (`accept` covers only the process's default check). Lint refuses an `accept` entry the process does not define
|
|
1977
|
+
(`verify-unknown-accept`): `fdm` defines `supports` and `footprint`; `resin` prints on
|
|
1978
|
+
supports and defines none.
|
|
1979
|
+
|
|
1980
|
+
**What arms what.** Under `fdm-pla`/`fdm-petg` the profile alone arms every check in
|
|
1981
|
+
the table except `wall`, which runs only where a sub-part declares a band; overhang fires past 1 mm². `resin` arms bed fit and min wall only. `orientation: "print"` declares that `build()`
|
|
1982
|
+
is the print pose (Z up, bed at the lowest Z); it arms nothing under a named profile,
|
|
1983
|
+
and with no profile at all it arms bed contact alone. An expectation you write yourself
|
|
1984
|
+
(`overhangArea`, `bedContactPct`, `footprintPct`) arms its own measurement under any
|
|
1985
|
+
profile or none; an `overhangArea` one is measured against 45° when the profile names no
|
|
1986
|
+
angle.
|
|
1987
|
+
|
|
1988
|
+
### Orientation first
|
|
1989
|
+
|
|
1990
|
+
Decide each piece's print pose before its geometry. Put its largest flat face on the
|
|
1991
|
+
bed; when a piece has none, split it at its widest flat section so each half has one,
|
|
1992
|
+
and join the halves with pins or a dovetail. `build()` IS that pose — every check and
|
|
1993
|
+
every export reads the piece as built — and `views.assembly` poses it into place; a
|
|
1994
|
+
`print` view lays the pieces out on the bed beside each other (see "The
|
|
1995
|
+
`PartDefinition` contract"). A piece that reads well in the assembly and prints on its
|
|
1996
|
+
side is the most common source of both costs.
|
|
1997
|
+
|
|
1998
|
+
### What costs supports and what does not
|
|
1999
|
+
|
|
2000
|
+
Overhangs past the profile's angle (45° from vertical on FDM) cost supports, and so
|
|
2001
|
+
do bridges longer than `bridge` (10 mm) and any cavity that opens downward — a socket
|
|
2002
|
+
under a ball, a pocket on the underside. The check counts every flat underside, a short
|
|
2003
|
+
bridge and the ceiling of a horizontal bore included, since a mesh cannot tell a bridge
|
|
2004
|
+
from a ceiling: a bridge within `bridge` prints without support, so tell the user that
|
|
2005
|
+
rather than redesign it. Faces within 45° of
|
|
2006
|
+
vertical do not cost supports: a chamfer under a ledge instead of a square step, a
|
|
2007
|
+
teardrop bore (a round hole with a 45° peak on top) instead of a round one. Faces on
|
|
2008
|
+
the bed or within 1 mm of it are never counted, so a bottom-edge chamfer, or a small fillet, is
|
|
2009
|
+
free.
|
|
2010
|
+
|
|
2011
|
+
**Ball joints on FDM** are a split socket (two halves that clamp the ball and screw or
|
|
2012
|
+
pin together) or a pinned clevis, not a socket snapped over a sphere: the snap's inside
|
|
2013
|
+
is a downward cavity that prints on supports nobody can reach. Build the snap only when
|
|
2014
|
+
the user wants it and has accepted the supports.
|
|
2015
|
+
|
|
2016
|
+
### Small features and scale
|
|
2017
|
+
|
|
2018
|
+
A feature thinner than the profile's `minWall` (1.2 mm PLA, 1.5 mm PETG, 0.6 mm
|
|
2019
|
+
resin) prints as noise or not at all. A part with a scale parameter is checked at both
|
|
2020
|
+
ends of it: add a preset at the smallest and the largest scale and name them in
|
|
2021
|
+
`verify.cases`, so the small end's pins and walls are measured rather than described
|
|
2022
|
+
in the control's label ("below 150 mm the pins get thin").
|
|
2023
|
+
|
|
2024
|
+
### Fits
|
|
2025
|
+
|
|
2026
|
+
Size a mating feature from the profile's fit class — `press`, `push`, `slip`,
|
|
2027
|
+
`moving`, `printInPlace` — copied into `derive()` with a comment naming it
|
|
2028
|
+
(`// slip fit: fdm-pla 0.25`), and a horizontal bore enlarged by `holeUndersize`. A
|
|
2029
|
+
snap is a contact; its fit is `push` or `slip`. The numbers, and how a diametral fit
|
|
2030
|
+
becomes a `clearance` expectation, are in the `verify` section below.
|
|
2031
|
+
|
|
2032
|
+
### First layer
|
|
2033
|
+
|
|
2034
|
+
The first layer squashes wider than drawn (elephant's foot): give a bottom edge that
|
|
2035
|
+
must mate or measure true a small 45° chamfer (about 0.4 mm). Keep the underside flat
|
|
2036
|
+
and lowest: a knuckle, pin or boss that dips below it lifts the flat face off the bed
|
|
2037
|
+
and fails bed contact ([piece-hovers-above-bed](ERROR-PATTERNS.md#piece-hovers-above-bed))
|
|
2038
|
+
— size a print-in-place knuckle tangent to the bed and give it a block down to
|
|
2039
|
+
z = 0. A piece resting on a point, a line or a curve is reported as a footprint cost
|
|
2040
|
+
([piece-stands-on-nothing](ERROR-PATTERNS.md#piece-stands-on-nothing)): lay it on a
|
|
2041
|
+
flat face, add a pad, or record the user's brim with `accept: ["footprint"]`. Never
|
|
2042
|
+
emboss text on the underside; deboss it, or put it on a side or the top.
|
|
2043
|
+
|
|
1922
2044
|
## Sheet parts
|
|
1923
2045
|
|
|
1924
2046
|
Laser-cut parts from flat stock — plywood, MDF, acrylic — for a laser cutter
|
|
@@ -4008,7 +4130,7 @@ runtime a throwing probe only degrades quietly to `{ error }`.
|
|
|
4008
4130
|
|
|
4009
4131
|
**Verify block** — `verify-unknown-metric`, `verify-unknown-subpart`,
|
|
4010
4132
|
`verify-bad-expr`, `verify-bad-pair-check`, `verify-unknown-process`,
|
|
4011
|
-
`verify-unknown-orientation`,
|
|
4133
|
+
`verify-unknown-orientation`, `verify-unknown-accept`,
|
|
4012
4134
|
`verify-expect-throws` (all errors). Note `_view` also accepts the pair-wise
|
|
4013
4135
|
`contacts` / `clearance` / `held` keys, which are not scalar view metrics; they are
|
|
4014
4136
|
validated by `verify-bad-pair-check`, matching `verify.js`'s own handling.
|
|
@@ -4173,7 +4295,8 @@ carries:
|
|
|
4173
4295
|
from a sample rather than every triangle (see below); `overhangArea` sets one
|
|
4174
4296
|
naming the steepest unsupported face's angle,
|
|
4175
4297
|
- `location` — `[x, y, z]` in mm where the metric has one: `minWall` (thinnest
|
|
4176
|
-
sample point), `overhangArea` (the largest unsupported face's centroid)
|
|
4298
|
+
sample point), `overhangArea` (the largest unsupported face's centroid),
|
|
4299
|
+
`footprintPct` (the piece's lowest point; absent when its export-pose reading failed) and
|
|
4177
4300
|
`overlaps` (the center of the first offending intersection's
|
|
4178
4301
|
*bounding box* — a nearby indicator, not an exact point: when a pair overlaps in
|
|
4179
4302
|
more than one place the bbox center can fall in the empty space between regions)
|
|
@@ -4188,7 +4311,8 @@ exists, e.g. the OCCT backend or min-wall measurement turned off, matching
|
|
|
4188
4311
|
per triangle, which is unbounded work on a dense mesh, so past a sample budget
|
|
4189
4312
|
it casts from a spread, deterministic subset instead — `minWallSampled` (boolean)
|
|
4190
4313
|
and `minWallSamples` (`{ sampled, total }` or `null`) say whether that happened.
|
|
4191
|
-
A part
|
|
4314
|
+
A part checked for overhang (a profile carrying `overhang`, or an `overhangArea`
|
|
4315
|
+
expectation — see the `verify` block) also carries
|
|
4192
4316
|
`overhangArea` (mm² of unsupported downward-facing surface, `null` when not
|
|
4193
4317
|
checked or on an `exportable: false` sub-part), `overhangAngle` (the steepest such
|
|
4194
4318
|
face, degrees from vertical) and `overhangAt` (that face's centroid); the report's
|
|
@@ -4198,7 +4322,13 @@ A part checked for bed contact (an FDM profile, see the `verify` block) carries
|
|
|
4198
4322
|
when not checked or on an `exportable: false` sub-part), `bedContactArea` and
|
|
4199
4323
|
`bedHoverArea` (mm² touching and hovering), `bedHoverGap` (how far above the bed the
|
|
4200
4324
|
largest hovering face sits, `null` when nothing hovers) and `bedHoverAt` (its
|
|
4201
|
-
centroid); the report's `measuredBedContact` says whether the pass ran.
|
|
4325
|
+
centroid); the report's `measuredBedContact` says whether the pass ran. The same
|
|
4326
|
+
pass reads `footprintPct` (the share of the piece's silhouette on the bed that actually
|
|
4327
|
+
touches it, in percent; `null` when not measured or the silhouette is empty) and
|
|
4328
|
+
`footprintAt`
|
|
4329
|
+
(the piece's lowest point; `null` when its export-pose reading failed), stamped by
|
|
4330
|
+
`measuredFootprint`; a profile carrying
|
|
4331
|
+
`footprint`, or a `footprintPct` expectation, arms the pass too.
|
|
4202
4332
|
A sub-part that declares `wall` (see the `verify` section below) also carries
|
|
4203
4333
|
`wall` — `{ value, location, band, members }` or `null`: the declared band's
|
|
4204
4334
|
worst member, located, with the band it was measured against and how many rays
|
|
@@ -4302,10 +4432,11 @@ A part can declare how it should be checked, co-located with its schema, so
|
|
|
4302
4432
|
|
|
4303
4433
|
```js
|
|
4304
4434
|
verify: {
|
|
4305
|
-
process: "fdm-pla", // a DFM profile: fdm-pla | fdm-petg | resin, or an
|
|
4306
|
-
//
|
|
4307
|
-
|
|
4308
|
-
|
|
4435
|
+
process: "fdm-pla", // a DFM profile: fdm-pla | fdm-petg | resin, or an inline
|
|
4436
|
+
// { base, bed:[x,y,z], minWall, overhang, bedContact, footprint, fits, holeUndersize, bridge } object
|
|
4437
|
+
accept: ["supports"], // optional; the costs the user chose to pay (fdm: supports, footprint)
|
|
4438
|
+
orientation: "print", // optional; declares build() is the print pose (Z up, bed at
|
|
4439
|
+
// the lowest Z) — with no profile it arms bed contact
|
|
4309
4440
|
cases: ["defaults", "M3"], // optional; default = defaults + every preset
|
|
4310
4441
|
expect: { // design intent, by sub-part name (+ "_view")
|
|
4311
4442
|
spacer: { holes: 1, bbox: "<=[60,60,60]", volume: "0.4..0.6cm3" },
|
|
@@ -4318,28 +4449,38 @@ verify: {
|
|
|
4318
4449
|
```
|
|
4319
4450
|
|
|
4320
4451
|
**What the profile gives you:** a hard **bed-fit** gate (the view bbox must fit `bed`),
|
|
4321
|
-
a **min-wall** warning, and
|
|
4322
|
-
|
|
4452
|
+
a **min-wall** warning, and on the FDM profiles a **bed-contact** gate and two
|
|
4453
|
+
**costs** — overhang and footprint (below). A cost is never a failure: it is a design
|
|
4454
|
+
choice with a price, reported in `verify().costs` with its number, location and hint
|
|
4455
|
+
and `status: "cost"`, and `ok` ignores it. See "Printed parts" for the tiers and how
|
|
4456
|
+
to raise a cost with the user.
|
|
4457
|
+
|
|
4458
|
+
**Overhang — a COST, armed by the process.** `overhangArea` is the mm² of
|
|
4323
4459
|
downward-facing surface steeper than the profile's `overhang` angle (45° from vertical
|
|
4324
4460
|
on the FDM profiles; resin carries none, since it prints on supports), measured per
|
|
4325
4461
|
sub-part in its print (export) pose — not as the view shows it — with the bed at that
|
|
4326
|
-
pose's own lowest Z
|
|
4327
|
-
in the view.
|
|
4328
|
-
|
|
4329
|
-
says this part is laid out for it, and a part still being shaped, or one bound for a
|
|
4330
|
-
different process, should not be nagged about its underside. Writing your own
|
|
4462
|
+
pose's own lowest Z; it is a cost past 1 mm², with cost id `supports`, and the
|
|
4463
|
+
reported location is where the face sits in the view. A profile carrying `overhang`
|
|
4464
|
+
arms it from the first build; `orientation` takes no part. Writing your own
|
|
4331
4465
|
`overhangArea` expectation is the other way in — it arms the measurement by itself,
|
|
4332
|
-
against the profile's angle or 45° when the profile names none,
|
|
4333
|
-
|
|
4466
|
+
against the profile's angle or 45° when the profile names none, and is also how a
|
|
4467
|
+
user's limit is spelled (`overhangArea: "<=500"`). Two bands next to the bed are never
|
|
4334
4468
|
counted: the footprint itself, and faces whose centroid sits within 1 mm of the bed
|
|
4335
4469
|
(the lower curl of a bottom-edge fillet or chamfer, which prints fine). A bridge (a
|
|
4336
4470
|
flat underside spanning two supports) and the ceiling of a horizontal bore are
|
|
4337
|
-
reported as overhangs — the mesh alone cannot tell a bridge from a ceiling —
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
are skipped.
|
|
4342
|
-
|
|
4471
|
+
reported as overhangs — the mesh alone cannot tell a bridge from a ceiling — so read
|
|
4472
|
+
the location before proposing a redesign. When a sub-part's `place()` cannot be
|
|
4473
|
+
trusted as a rigid pose (it queries its solid), the reading falls back to the DISPLAY
|
|
4474
|
+
pose, so such a piece is measured as the view shows it. `exportable: false` sub-parts
|
|
4475
|
+
are skipped.
|
|
4476
|
+
|
|
4477
|
+
**`accept` records the user's choice.** `accept: ["supports"]` says the user chose to
|
|
4478
|
+
print with supports: the overhang is still measured and still listed in `costs`, with
|
|
4479
|
+
`accepted: true`, and is no longer something to raise. That is a different thing from
|
|
4480
|
+
switching the measurement off, which is the inline `{ base: "fdm-pla", overhang: null }`
|
|
4481
|
+
(and `footprint: null`) for a part where the reading means nothing. `accept` holds cost
|
|
4482
|
+
ids, not metric names — `fdm` defines `supports` and `footprint`, `resin` none — and
|
|
4483
|
+
lint refuses any other entry (`verify-unknown-accept`).
|
|
4343
4484
|
|
|
4344
4485
|
**Bed contact — a GATE, armed by the FDM profile alone.** Every exporter and slicer
|
|
4345
4486
|
lays a printed piece on its lowest point. When that point is something small — a
|
|
@@ -4362,10 +4503,9 @@ its note says how far up it sits and how much area hovers versus touches. The fi
|
|
|
4362
4503
|
almost always to trim the piece flat at the bed plane — cut away everything below
|
|
4363
4504
|
z = 0 in its print pose — or to raise or shorten the lower feature so the flat face
|
|
4364
4505
|
is the lowest; size a print-in-place knuckle so its cylinder is tangent to the bed
|
|
4365
|
-
(diameter = 2 × its centre height) and give it a support block down to z = 0.
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
orientation the piece exports. Resin prints on supports, so `resin` never checks it
|
|
4506
|
+
(diameter = 2 × its centre height) and give it a support block down to z = 0. It is a
|
|
4507
|
+
gate where overhang is a cost: an overhang is a design choice that prints on supports,
|
|
4508
|
+
but a hovering underside is a defect in whatever orientation the piece exports. Resin prints on supports, so `resin` never checks it
|
|
4369
4509
|
unless you ask: your own `bedContactPct` expectation arms it under any profile, and
|
|
4370
4510
|
`orientation: "print"` arms it on a part with no profile at all (both against 50%). A
|
|
4371
4511
|
named profile otherwise decides alone. A recess meant to clear the
|
|
@@ -4375,6 +4515,23 @@ default stands, and a part that means it declares a lower `bedContactPct` (or
|
|
|
4375
4515
|
`{ base: "fdm-pla", bedContact: null }` to switch it off). `exportable: false`
|
|
4376
4516
|
sub-parts are not judged.
|
|
4377
4517
|
|
|
4518
|
+
**Footprint — a COST, armed by the FDM profile.** Bed contact passes a piece that
|
|
4519
|
+
stands on a point, a line or a curve, since nothing flat hovers; the first layer still
|
|
4520
|
+
has almost nothing to grip, and the piece tips or pulls loose unless it prints with a
|
|
4521
|
+
brim or a raft. `footprintPct` is the share of the piece's silhouette on the bed (its
|
|
4522
|
+
outline seen from below, in the same print pose) that actually touches it, read by bed
|
|
4523
|
+
contact's pass, in percent: a plate reads 100, and so do a tube on its rim, a frame of
|
|
4524
|
+
thin bars, spread pins and a bar laid diagonally, since each stands on all of its
|
|
4525
|
+
outline; a rod on a rounded tip reads about 0, a cylinder on its side a few percent
|
|
4526
|
+
(the facets along its lowest line), under the floor, and a wide piece balanced on a
|
|
4527
|
+
narrow foot reads low too. It is a cost below the profile's `footprint` (10%), with cost id
|
|
4528
|
+
`footprint`, located at the piece's lowest point
|
|
4529
|
+
([piece-stands-on-nothing](ERROR-PATTERNS.md#piece-stands-on-nothing)). Lay the piece on
|
|
4530
|
+
a flat face or add a pad; when the user chooses the brim, `accept: ["footprint"]`
|
|
4531
|
+
records it. Your own `footprintPct` expectation arms it under any profile; `orientation`
|
|
4532
|
+
does not, and `{ base: "fdm-pla", footprint: null }` switches it off. `exportable: false`
|
|
4533
|
+
sub-parts are not judged.
|
|
4534
|
+
|
|
4378
4535
|
**What the profile tells you.** Three more fields carry the numbers a mating
|
|
4379
4536
|
feature needs. Nothing checks them; they are the one place the value is spelled,
|
|
4380
4537
|
so `derive()` copies it with a comment naming the fit (`// slip fit: fdm-pla 0.25`).
|
|
@@ -4399,7 +4556,7 @@ an inline `{ base: "fdm-pla", fits: { … } }` replaces the whole table.
|
|
|
4399
4556
|
**What `expect` gives you:** per-sub-part
|
|
4400
4557
|
assertions on the facts `measure` already reports — `holes` (through-bores / genus),
|
|
4401
4558
|
`volume`, `surfaceArea`, `triangleCount`, `bbox`, `watertight`, `minWall`,
|
|
4402
|
-
`overhangArea`, `bedContactPct`, `wall` (a range — see below), `bounds` (the axis-aligned corner
|
|
4559
|
+
`overhangArea`, `bedContactPct`, `footprintPct`, `wall` (a range — see below), `bounds` (the axis-aligned corner
|
|
4403
4560
|
positions, written in the same `{ min, max }` shape `measure` reports them — where
|
|
4404
4561
|
the geometry sits, vs `bbox` which is only its size) and `centerOfMass` (`[x,y,z]`, the volume-weighted
|
|
4405
4562
|
centroid; `null` for a degenerate/zero-volume sub-part); and `_view` assertions `bbox`,
|
|
@@ -4442,12 +4599,14 @@ verify: { expect: {
|
|
|
4442
4599
|
} }
|
|
4443
4600
|
```
|
|
4444
4601
|
|
|
4445
|
-
**Gates
|
|
4446
|
-
`
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4602
|
+
**Gates, costs and warnings:** exact facts are **gates** (a failure sets a non-zero
|
|
4603
|
+
exit code and `ok: false`), and so is `bedContactPct`, even though the profile arms it.
|
|
4604
|
+
`overhangArea` and `footprintPct` under a process are **costs**: listed in `costs`, never
|
|
4605
|
+
failing, `accepted` once `accept` names them — but an `overhangArea` or `footprintPct`
|
|
4606
|
+
you write in `expect` is still a cost, not a gate. `minWall` is computed (a ray/shot
|
|
4607
|
+
wall-thickness measurement) and reported as a **warning** — it flags walls below the
|
|
4608
|
+
profile's minimum but never fails the build. `holes`/`watertight` are Manifold-only, so
|
|
4609
|
+
those assertions **skip** on OCCT parts rather than fail.
|
|
4451
4610
|
|
|
4452
4611
|
**A wall that must stay one thickness: `wall`.** `minWall` answers "is anything too
|
|
4453
4612
|
thin"; `wall` answers "does this wall stay what I declared" — the question a bend, a
|
|
@@ -4553,8 +4712,8 @@ mass. A reference ghost (`exportable: false`) is left out entirely: it neither
|
|
|
4553
4712
|
counts nor holds, since it is never printed (lint refuses one named as the base).
|
|
4554
4713
|
A named base that is `enabled()`-off
|
|
4555
4714
|
in a case skips that case; a name that exists nowhere throws, like a `contacts`
|
|
4556
|
-
pair. It is an expectation, never a default
|
|
4557
|
-
|
|
4715
|
+
pair. It is an expectation, never a default: a part still being shaped should not
|
|
4716
|
+
fail for pieces it has not joined yet. Topological only — a part balanced on a
|
|
4558
4717
|
point passes — and the same OCCT caveat applies: a sub-part fully inside another
|
|
4559
4718
|
reads as not touching there. A view with fewer than two printed sub-parts passes with nothing to hold. A pin or axle running in a clearance bore, or a print-in-place piece, touches nothing in the model and reads as floating: `held` is for assemblies whose pieces seat on each other, so leave it off when a piece is captured with running clearance (`fits.moving`, `fits.printInPlace`) and declare that pair's `clearance` instead. A quick lap answers it `unevaluated`.
|
|
4560
4719
|
|
package/docs/ERROR-PATTERNS.md
CHANGED
|
@@ -210,6 +210,12 @@ Variant literals under this entry: `extrude: unknown bevel option`, `extrude: be
|
|
|
210
210
|
- **Cause:** Something smaller than the piece's flat underside sits lower than it in the print pose — a horizontal knuckle or pin whose cylinder dips below the plate, a boss, or lettering embossed on the bottom — so exporters and slicers rest the piece on that feature and the flat face prints in mid-air.
|
|
211
211
|
- **Fix:** Trim the piece flat at the bed plane (cut everything below z = 0 in its print pose), or raise or shorten the lower feature so the flat face is the lowest; size a print-in-place knuckle so its cylinder is tangent to the bed and give it a support block down to z = 0. A recess meant to clear the bed declares a lower `bedContactPct`. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Self-verification (the `verify` block)".
|
|
212
212
|
|
|
213
|
+
## piece-stands-on-nothing
|
|
214
|
+
|
|
215
|
+
- **Symptom:** A `footprintPct` cost from `verify` (in `verify.costs`, e.g. `0.42 not >= 10`) on a piece whose print pose rests on a point, a line or a curl — a sphere-tipped rod standing on its end, a cylinder lying on its side — while `bedContactPct` passes.
|
|
216
|
+
- **Cause:** The lowest point of the piece is not a flat face, so almost none of its silhouette on the bed actually touches it (a cylinder on its side reads a few percent, under the floor): the first layer has almost nothing to bond to, and the piece tips or pulls loose unless it prints with a brim, a raft, or in a different orientation.
|
|
217
|
+
- **Fix:** Lay the piece on its flat face in its print pose, add a flat foot or pad at its lowest point, or print it with a brim. When the user chose the brim, `verify.accept: ["footprint"]` records it and the cost reads accepted. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Printed parts".
|
|
218
|
+
|
|
213
219
|
## expect-static-across-presets
|
|
214
220
|
|
|
215
221
|
- **Symptom:** A `verify` exact gate (`holes`, `volume`, …) fails on SOME presets only — e.g. `✗ planter holes 1 (0 != 1)` on two cases while defaults pass — and the preview looks right for every preset.
|
|
@@ -877,6 +883,12 @@ between the Manifold preview and the OCCT STEP export.
|
|
|
877
883
|
- **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.
|
|
878
884
|
- **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.
|
|
879
885
|
|
|
886
|
+
## kernel-internal-error
|
|
887
|
+
|
|
888
|
+
- **Symptom:** `the geometry kernel raised an internal error it could not describe` — from a build or a STEP import, naming the operation when known (`during STEP import of "frame" (17.1 MB)`) and, when the kernel's own heap could say, the C++ exception type (`C++ exception StdFail_NotDone`, `Standard_ConstructionError`, `std::bad_alloc`, …) and the heap size. Before partforge 0.163.1 the same failure was a bare number (`{"ok":false,"error":"154591824"}`), different on every attempt.
|
|
889
|
+
- **Cause:** The OCCT kernel is a WASM build compiled without C++ exception catching, so an exception nothing inside it catches — the STEP reader's transfer refusing an entity, a construction with a zero-length direction, an allocation the heap could not grow for — reaches JavaScript as the exception's ADDRESS. The number is not a code and means nothing across runs. partforge now names the operation and reads the exception's type off the heap, but it cannot read OCCT's own message for it.
|
|
890
|
+
- **Fix:** Act on the type. `std::bad_alloc` or `Standard_OutOfMemory` (the message then says "probably too large for the in-browser kernel") is memory: a STEP import needs roughly six times its file size of kernel heap (measured on OCCT-written files: 17 MB → 100 MiB, 42 MB → 248 MiB), so a file that exhausts the 2 GB wasm32 ceiling is hundreds of megabytes, or the browser refused to grow the heap — simplify the imported model or split it. Any other type on an import is the file itself: re-export it from the CAD tool as AP214/AP242 solids (no assembly-only or surface-only exports) and try the smaller file; the same type from a build is that operation refusing its input (a `Standard_ConstructionError` is usually a zero-length direction or a degenerate profile). Retrying the identical build fails the same way — it is not transient.
|
|
891
|
+
|
|
880
892
|
# Hardware library
|
|
881
893
|
|
|
882
894
|
Reserved for `hardware-*` patterns (issue #30). No entries yet.
|
package/package.json
CHANGED
|
@@ -27,6 +27,7 @@ import { makeProfileWarner } from "./profile-warnings.js";
|
|
|
27
27
|
import { createOcctRepair } from "./occt-repair.js";
|
|
28
28
|
import { occtRoundAll } from "./occt-roundall.js";
|
|
29
29
|
import { classifyFaceGroups } from "./feature-attribution.js";
|
|
30
|
+
import { emscriptenThrowEvidence, labelImportFailure } from "../kernel-error.js";
|
|
30
31
|
import { resolveLoftRings, loftRingsKey } from "./loft-rings.js";
|
|
31
32
|
import { resolveSweepStations } from "./sweep.js";
|
|
32
33
|
import { normalizeProfile } from "./profile.js";
|
|
@@ -199,6 +200,7 @@ export function createOcctKernel(replicad) {
|
|
|
199
200
|
// registers pre-build via `_registerImport` (kernel-lifetime, untracked by the
|
|
200
201
|
// solid cache: imports are the framework's own memo, keyed by name+digest).
|
|
201
202
|
const imports = new Map();
|
|
203
|
+
const throwEvidence = (value) => emscriptenThrowEvidence(replicad.getOC?.(), value);
|
|
202
204
|
// name -> { digest, width, height, data } — depth-map grids the framework
|
|
203
205
|
// registers pre-build via `_registerImage` (ensureImages). Kernel-lifetime like
|
|
204
206
|
// `imports` above, and plain data rather than OCCT objects, so there is nothing
|
|
@@ -815,11 +817,22 @@ export function createOcctKernel(replicad) {
|
|
|
815
817
|
const prev = imports.get(name);
|
|
816
818
|
if (!prev?.error && prev?.digest === digest) return; // error entries are always upgradable
|
|
817
819
|
if (error) { imports.set(name, { error, digest }); return; }
|
|
818
|
-
|
|
820
|
+
// A STEP the reader refuses, or an OCCT exception out of its transfer,
|
|
821
|
+
// leaves here labelled with the import's name and size. The latter
|
|
822
|
+
// reaches JavaScript as a bare number from this exception-less build
|
|
823
|
+
// (kernel-error.js), and the heap is read for its C++ type BEFORE
|
|
824
|
+
// anything else runs on the kernel.
|
|
825
|
+
let shape;
|
|
826
|
+
try { shape = await importSTEP(new Blob([step])); }
|
|
827
|
+
catch (e) { throw labelImportFailure({ name, bytes: step?.byteLength, error: e, evidence: throwEvidence(e) }); }
|
|
828
|
+
imports.set(name, { shape, digest });
|
|
819
829
|
},
|
|
820
830
|
// Registration memo: undefined for an error entry, so a later registration with
|
|
821
831
|
// the same digest can upgrade it rather than being treated as a no-op repeat.
|
|
822
832
|
_importDigest: (name) => { const e = imports.get(name); return e?.error ? undefined : e?.digest; },
|
|
833
|
+
// What the kernel's heap can say about a value OCCT threw (kernel-error.js):
|
|
834
|
+
// the job loop asks this when a build throws something that is not an Error.
|
|
835
|
+
_throwEvidence: throwEvidence,
|
|
823
836
|
// Depth-map grids, registered pre-build by the framework via `_registerImage`
|
|
824
837
|
// (ensureImages). Unlike imports there is no per-format error entry: every
|
|
825
838
|
// backend can consume a normalized grid, so registration never fails.
|
package/src/framework/jobs.js
CHANGED
|
@@ -10,6 +10,7 @@ import { fontsFor, resolveFonts, forgetFontSource } from "./fonts.js";
|
|
|
10
10
|
import { normalizeOpentype, parseFont } from "./geometry/opentype-interop.js";
|
|
11
11
|
import { imageControlAllows, imageSourceAllowed, isNoImageSource } from "./image-source.js";
|
|
12
12
|
import { imagesFor, ensureImages } from "./images.js";
|
|
13
|
+
import { describeKernelThrow } from "./kernel-error.js";
|
|
13
14
|
import { ensureImports, resolveImports } from "./imports.js";
|
|
14
15
|
import { safeName } from "./safe-name.js";
|
|
15
16
|
import { vectorControlAllows, vectorSourceAllowed, isNoVectorSource } from "./vector-source.js";
|
|
@@ -651,7 +652,12 @@ export async function handle(kernel, part, msg, post, opts = {}) {
|
|
|
651
652
|
} else if (opts.jobs && Object.hasOwn(opts.jobs, msg.type)) {
|
|
652
653
|
await opts.jobs[msg.type](kernel, part, msg, post, { isStale: opts.isStale });
|
|
653
654
|
}
|
|
654
|
-
} catch (
|
|
655
|
+
} catch (thrown) {
|
|
656
|
+
// A kernel compiled without exception catching throws a bare NUMBER for an
|
|
657
|
+
// uncaught C++ exception; describeKernelThrow turns that into a readable
|
|
658
|
+
// Error (with the C++ type when the kernel's heap can say) and returns a
|
|
659
|
+
// real Error untouched, `code` included.
|
|
660
|
+
const err = describeKernelThrow(thrown, { evidence: kernel._throwEvidence?.(thrown) });
|
|
655
661
|
// `subparts` (generate jobs only) tells the reroute policy which sub-parts the
|
|
656
662
|
// failed job covered, so only those latch to OCCT — not the whole part.
|
|
657
663
|
if (err?.code === "NEEDS_IMPORT_MESH") post({ type: "needs-import-mesh", jobId: msg.jobId, subparts: msg.subparts });
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// What to say when the geometry kernel throws something that is not an Error.
|
|
2
|
+
//
|
|
3
|
+
// replicad-opencascadejs's `replicad_single` build (the OCCT kernel worker.js
|
|
4
|
+
// loads) is compiled WITHOUT C++ exception catching. An exception OCCT throws
|
|
5
|
+
// that nothing inside the WASM catches — a Standard_Failure out of the STEP
|
|
6
|
+
// reader, a std::bad_alloc when the heap cannot grow — leaves the module through
|
|
7
|
+
// Emscripten's `___cxa_throw`, whose last line is `throw ptr`: JavaScript
|
|
8
|
+
// receives the exception's ADDRESS, a bare number such as 154591824, different
|
|
9
|
+
// on every run. Stringified as-is, that number was the whole error message a
|
|
10
|
+
// part author (and partforge-cloud's agent, which read it as the part's fault
|
|
11
|
+
// and retried blindly — cloud feedback #208) got for a failed build.
|
|
12
|
+
//
|
|
13
|
+
// This module is the ONE place such a value becomes a readable Error, used by
|
|
14
|
+
// both the job loop's error post (jobs.js, worker.js) and the STEP import path
|
|
15
|
+
// (occt-backend.js). It never invents a cause: the message says the kernel
|
|
16
|
+
// raised an internal error it could not describe, names the operation when the
|
|
17
|
+
// caller knows it, and adds only what the kernel's own heap can prove —
|
|
18
|
+
// `emscriptenThrowEvidence` reads the thrown exception's C++ type name off the
|
|
19
|
+
// heap through the Itanium ABI layout Emscripten's `ExceptionInfo` uses, and
|
|
20
|
+
// says "too large for the in-browser kernel" only when that type IS an
|
|
21
|
+
// out-of-memory type, or the heap has reached the wasm32 ceiling. Anything the
|
|
22
|
+
// read cannot validate is dropped rather than guessed.
|
|
23
|
+
|
|
24
|
+
// A thrown value the caller cannot read: a number (the Emscripten case above), or
|
|
25
|
+
// an object with no non-empty string message (a bare WebAssembly.Exception, a
|
|
26
|
+
// plain object). A real Error — anything with a message — is never opaque.
|
|
27
|
+
export function isOpaqueThrow(err) {
|
|
28
|
+
if (typeof err === "number") return true;
|
|
29
|
+
if (err == null) return true;
|
|
30
|
+
if (typeof err === "string") return err.trim() === "";
|
|
31
|
+
return !(typeof err.message === "string" && err.message.trim() !== "");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Emscripten's ExceptionInfo (replicad-opencascadejs 0.23): the thrown pointer
|
|
35
|
+
// is the exception OBJECT; a 24-byte header sits before it, with the
|
|
36
|
+
// std::type_info pointer at header + 4. A std::type_info (libc++abi, wasm32)
|
|
37
|
+
// is { vtable*, const char* __type_name }, so the mangled class name is the
|
|
38
|
+
// C string at *(typeInfo + 4). test/kernel-error-occt.test.js holds this
|
|
39
|
+
// layout against the installed glue: a replicad-opencascadejs bump that moves
|
|
40
|
+
// it fails there, and until it is re-pinned the read simply yields no type
|
|
41
|
+
// name (a wrong read almost never parses as a mangled name — see below).
|
|
42
|
+
const EXCEPTION_HEADER_BYTES = 24;
|
|
43
|
+
const TYPE_OFFSET_IN_HEADER = 4;
|
|
44
|
+
const NAME_OFFSET_IN_TYPE_INFO = 4;
|
|
45
|
+
const MAX_MANGLED_NAME = 128;
|
|
46
|
+
|
|
47
|
+
// getHeapMax() in the replicad_single glue: the wasm32 build's memory ceiling.
|
|
48
|
+
export const WASM32_HEAP_CEILING_BYTES = 2 ** 31;
|
|
49
|
+
// At or past this share of the ceiling, a throw with no readable type is still
|
|
50
|
+
// called out of memory: nothing else grows a heap to the top of its range.
|
|
51
|
+
const HEAP_CEILING_SHARE = 0.9;
|
|
52
|
+
|
|
53
|
+
// C++ exception types that mean the kernel could not get memory.
|
|
54
|
+
const OUT_OF_MEMORY_TYPES = new Set(["std::bad_alloc", "std::bad_array_new_length", "Standard_OutOfMemory"]);
|
|
55
|
+
|
|
56
|
+
// Itanium-mangled class name → readable, for the shapes exception types take:
|
|
57
|
+
// `St9bad_alloc` (std::bad_alloc), `16Standard_Failure`, `N3foo3BarE`
|
|
58
|
+
// (foo::Bar). Every length prefix must match its identifier exactly and the
|
|
59
|
+
// string must be consumed whole, which is what makes a misread (a pointer that
|
|
60
|
+
// landed on arbitrary heap bytes) come back null instead of as a "type".
|
|
61
|
+
export function demangleClassName(mangled) {
|
|
62
|
+
if (typeof mangled !== "string" || !/^[A-Za-z0-9_]{1,128}$/.test(mangled)) return null;
|
|
63
|
+
let i = 0;
|
|
64
|
+
const nested = mangled[0] === "N";
|
|
65
|
+
if (nested) i++;
|
|
66
|
+
const parts = [];
|
|
67
|
+
if (mangled.startsWith("St", i)) { parts.push("std"); i += 2; }
|
|
68
|
+
while (i < mangled.length && /[0-9]/.test(mangled[i])) {
|
|
69
|
+
let j = i;
|
|
70
|
+
while (j < mangled.length && /[0-9]/.test(mangled[j])) j++;
|
|
71
|
+
const len = Number(mangled.slice(i, j));
|
|
72
|
+
if (len < 1 || j + len > mangled.length) return null;
|
|
73
|
+
const ident = mangled.slice(j, j + len);
|
|
74
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(ident)) return null;
|
|
75
|
+
parts.push(ident);
|
|
76
|
+
i = j + len;
|
|
77
|
+
}
|
|
78
|
+
if (nested ? mangled.slice(i) !== "E" : i !== mangled.length) return null;
|
|
79
|
+
if (parts.length === 0 || (parts.length === 1 && parts[0] === "std")) return null;
|
|
80
|
+
return parts.join("::");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readCString(u8, ptr, cap) {
|
|
84
|
+
if (!Number.isInteger(ptr) || ptr <= 0 || ptr >= u8.length) return null;
|
|
85
|
+
let s = "";
|
|
86
|
+
for (let i = 0; i < cap && ptr + i < u8.length; i++) {
|
|
87
|
+
const c = u8[ptr + i];
|
|
88
|
+
if (c === 0) return s;
|
|
89
|
+
s += String.fromCharCode(c);
|
|
90
|
+
}
|
|
91
|
+
return null; // ran past the cap or the heap without a terminator
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// What the kernel's heap can say about a thrown value: `{ heapBytes, typeName? }`.
|
|
95
|
+
// `module` is the Emscripten module object (what `opencascade()` resolved to,
|
|
96
|
+
// `replicad.getOC()`); the HEAP views are re-read from it on every call because
|
|
97
|
+
// the glue replaces them whenever the memory grows. Returns `{}` for a module
|
|
98
|
+
// without heap views, and never throws.
|
|
99
|
+
export function emscriptenThrowEvidence(module, value) {
|
|
100
|
+
const evidence = {};
|
|
101
|
+
const u8 = module?.HEAPU8;
|
|
102
|
+
const u32 = module?.HEAPU32;
|
|
103
|
+
if (!(u8 instanceof Uint8Array) || !(u32 instanceof Uint32Array)) return evidence;
|
|
104
|
+
evidence.heapBytes = u8.length;
|
|
105
|
+
if (!Number.isInteger(value) || value <= EXCEPTION_HEADER_BYTES || value >= u8.length) return evidence;
|
|
106
|
+
const typeInfo = u32[(value - EXCEPTION_HEADER_BYTES + TYPE_OFFSET_IN_HEADER) >> 2];
|
|
107
|
+
if (!(typeInfo > 0 && typeInfo + NAME_OFFSET_IN_TYPE_INFO + 4 <= u8.length)) return evidence;
|
|
108
|
+
const namePtr = u32[(typeInfo + NAME_OFFSET_IN_TYPE_INFO) >> 2];
|
|
109
|
+
const typeName = demangleClassName(readCString(u8, namePtr, MAX_MANGLED_NAME));
|
|
110
|
+
if (typeName) evidence.typeName = typeName;
|
|
111
|
+
return evidence;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function formatBytes(n) {
|
|
115
|
+
if (!Number.isFinite(n) || n < 0) return "unknown size";
|
|
116
|
+
if (n < 1e6) return `${Math.max(1, Math.round(n / 1e3))} KB`;
|
|
117
|
+
return `${(n / 1e6).toFixed(1)} MB`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const describeRaw = (err) => {
|
|
121
|
+
if (typeof err === "number") return `the kernel reported only an exception address, ${err}`;
|
|
122
|
+
const ctor = err?.constructor?.name;
|
|
123
|
+
return `the kernel reported only an opaque ${ctor && ctor !== "Object" ? ctor : "value"}`;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Turn a thrown value into an Error. A real Error (anything with a message) is
|
|
127
|
+
// returned AS IS — same object, nothing rewritten — so a caller can branch on
|
|
128
|
+
// its `code` exactly as before. `operation` names what was running ("STEP
|
|
129
|
+
// import of "frame" (17.1 MB)"); `subject` is what the memory clause blames
|
|
130
|
+
// ("the file" for an import, "the part" otherwise); `evidence` is
|
|
131
|
+
// `emscriptenThrowEvidence`'s answer, or nothing.
|
|
132
|
+
export function describeKernelThrow(err, { operation, subject = "the part", evidence = {} } = {}) {
|
|
133
|
+
if (!isOpaqueThrow(err)) return err;
|
|
134
|
+
const { typeName, heapBytes } = evidence ?? {};
|
|
135
|
+
let message = "the geometry kernel raised an internal error it could not describe";
|
|
136
|
+
if (operation) message += ` during ${operation}`;
|
|
137
|
+
const detail = [];
|
|
138
|
+
if (typeName) detail.push(`C++ exception ${typeName}`);
|
|
139
|
+
if (Number.isFinite(heapBytes)) detail.push(`kernel heap ${formatBytes(heapBytes)}`);
|
|
140
|
+
message += detail.length ? ` (${detail.join(", ")})` : ` (${describeRaw(err)})`;
|
|
141
|
+
const outOfMemory = typeName
|
|
142
|
+
? OUT_OF_MEMORY_TYPES.has(typeName)
|
|
143
|
+
: Number.isFinite(heapBytes) && heapBytes >= WASM32_HEAP_CEILING_BYTES * HEAP_CEILING_SHARE;
|
|
144
|
+
if (outOfMemory) {
|
|
145
|
+
message += ` — the kernel ran out of memory, so ${subject} is probably too large for the in-browser kernel; retrying without reducing it will fail the same way`;
|
|
146
|
+
}
|
|
147
|
+
const out = new Error(message, { cause: err });
|
|
148
|
+
if (typeName) out.kernelExceptionType = typeName;
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The STEP import path's wrapper: whatever `importSTEP` threw, labelled with the
|
|
153
|
+
// import's name and size. An opaque throw takes `describeKernelThrow` with the
|
|
154
|
+
// import as its operation; a real Error (replicad's "Failed to load STEP file",
|
|
155
|
+
// its "This shape has not type, it is null" for a file the reader parsed but
|
|
156
|
+
// could not transfer) keeps its text behind the same label, `cause` attached.
|
|
157
|
+
export function labelImportFailure({ name, bytes, error, evidence }) {
|
|
158
|
+
const operation = `STEP import of "${name}" (${formatBytes(bytes)})`;
|
|
159
|
+
if (isOpaqueThrow(error)) return describeKernelThrow(error, { operation, subject: "the file", evidence });
|
|
160
|
+
return new Error(`${operation} failed: ${error.message}`, { cause: error });
|
|
161
|
+
}
|