stl2step 1.4.0 — engine only (downloads on v1.4.1)
Engine-only tag — no downloads here. The SolidOut 0.2.0 installers and the per-platform engine bundles for the in-app updater ship with v1.4.1. This tag's binary reports stl2step 1.3.0 because the project version was not bumped; 1.4.1 fixes that and is otherwise the same engine.
Engine changelog (1.4.0)
Added
- Torus engine — stage T transactional reclaim + U-3 face sharing the cylinder rim (D-140-6, D-140-11). Stage T admits torus candidates, but cylinder reclaim commits only when the U-3
Geom_ToroidalSurfaceface ships; a failed U-3 build restores the pre-stage-T topology and rebuilds with fresh vertices (Body28/Body11 byte-identical to the no-torus binary). The toroidal face shares the cylinder rim via explicit pcurves (Geom2d_Line((u0−f, v), (1,0))on the reused rim TShape, both seam pcurves atu0/u0+2π, and the c-rev wire). S04's boss-top round ships as a valid closed solid (TOROIDAL_SURFACE,solids=1,faceCount=9, I-checker PASS). S19 mouth rounds, the plate mouth band, and Handle pickup admit 0 tori — the certificate is honest on the real parts (smoothBuiltTori=0, expected-redS19.mouth-round-not-built). The torus closed-360 gate usesbandArc + asin(min(1, τ/hmin))(θ_q) instead of the retired1.5·bandArchand constant; U-3 probe warnings are discarded on topology revert. - Torus math — the
circle-on-torusclass (src/refit_torus_math.{hpp,cpp},torus_math_unit; D-140-6 §3(4), the D-130-3 hard gate applied verbatim; D-130-2).circleOnTorusMaxreturns, in closed form, the supremum distance from a circle COAXIAL with a ring torus to the untrimmed toroidal surface: every point of such a circle has the same(rho, z) = (r_c, h), so the distance is the constant|hypot(r_c − Rmaj, h) − Rmin|and that constant is the supremum — exactly0on every v-iso circle, in particular on the two tangency circles a mouth round is bounded by (S04:rho = 10 @ z = 22andrho = 7 @ z = 25; S19: both rims of both tori). A non-coaxial circle, a spindle or horn torus (Rmaj − Rmin <= gp::Resolution(), OCCT's own guard) and a degenerate tube are refused with-1.0/unhandled-circle-on-torus— tier 2, counted, never a widened tolerance. WithpointTorusDist,torusVIsoCircle(the tier-1 bounds a blend patch carries) andtorusVOfProfilePoint. Own translation unit for the reason the cone math has one (0d67eba): the P1 include allowlist is neither touched nor widened. No build path is edited;exactMaxAtBindstill returnsunhandled-surfacefor a torus until the build lane binds through this class. - The same-surface union census is readable with the door closed (D-140-8 U-R13; SPEC-union PART B). Under
STL2STEP_LAWBAND_DIAGtheDIAG_LAWUNIONline no longer reports theSTL2STEP_UNIONdoor's behaviour: it takes a copy of every law band's claim, applies D-140-8 U-R1…U-R4 to the copy (the D-130-13(2) same-surface islands undermax(bandResid, q), the joint-refit roll-back, the axial-extent overlap, the mutual-containment merge, in band order) and prints, per certified surface,nTri sigma punctures edgePieces domainFaces pinchVertices sizes=[..] pieces=[..] domainTris loc dir.sigmais U-R5's closed-form supremummax over the claim's welded edges of (R − dist(axis, midpoint)), punctures are U-R6's non-claim vertices withinsigmaof the surface,domainFacescounts U-R7's components of the claim plus the on-surface stars of its punctures,pinchVerticescounts U-R8's vertices whose link inside a component is two or more fans. The copy is never written back:claimTris, the band fits and every emitted Region are untouched, so the shipped STEP is byte-identical with the diag on or off and with the door open or closed (the plate:R=10.000000 nTri=255 sigma=0.019733720 punctures=8 edgePieces=6 domainFaces=1 pinchVertices=0 domainTris=279; Handle pickup:R=3.999998 nTri=52 punctures=20 edgePieces=11 domainFaces=1 pinchVertices=0 domainTris=84). New TUsrc/refit_union_census.cpp(claimChordSagitta,unionCensus, declared inrefit_internal.hpp; pureMeshViewmeasurement, no Region, no build path) and unit targetunion_census_unit:sigmaagainst the regular-ring closed form and a numerical supremum along every welded edge at three radii × three densities, world and placed frames, toPrecision::Confusion(), plus the U-R6/U-R7/U-R8 counts on hand-built interrupted rings. - Ellipse math — the two closed forms behind the ELLIPSE bind class (
src/refit_ellipse_math.{hpp,cpp},ellipse_math_unit; D-140-9 §2, D-130-2, D-S3-111; the third instance of the 130-CONE-MATH / torus-math shape). Measured first (SPEC-ellipse PART 0): all 55unhandlededges on the 9-fixture edge-class red line are plane∩cylinder ellipses whose plane-side pcurve is the exact 2-DELLIPSEGeomAPI::To2dwrites and whose cylinder-side pcurve is one degree-8 Bézier with affineuand a sinusoid-approximatingv.ellipseOnPlaneMaxis the EXACT supremum of|E(t) − S(pc(t))| = |W + A cos t + B sin t|over[f, l]: thetan(t/2)quartic's real roots (Ferrari, threshold-free shift, fixed Newton polish) plus the endpoints;W = 0collapses toσ_max([A B])(ellipseTrigSupZeroOffset), the shipped case to exactly0.0, and fora = bit is≤ circleOnPlaneMax(the D-S3-111 triangle bound) — the sanity relation between the two classes.ellipseOnCylMaxis a certified UPPER BOUND and says so in its name: the section identity (ellipseIsCylinderSection:ρ ≡ R,θ = θ₀ ± t,z = z₀ + Λ cos(t − ψ), residual carried exactly),ε_uexact from the poles by the Bernstein hull, the axial residual per knot span by Chebyshev interpolation (L_n max_j |v(x_j) − z(x_j)| + Λ H^{n+1}/(2^{2n+1}(n+1)!), the refused midpoint-Taylor form is 2⁸ = 256× looser), composed through the exact two-point identity on a cylinder. The grazing predicate (D-S3-111's open predicate):ellipseCylGrazingSin = |N̂·d̂| = cos α = b/aandellipseVertexBound = √(s² + h²) + s²/(2ρ_min)with|h| ≤ δ_pl,|s| ≤ (δ_cy + δ_pl|cos γ|)/sin γ; the class refuses (unhandled-ellipse-grazing) where that exceedsmeshTolCap— never a hand angle. Every refusal is named (unhandled-ellipse-plane-{param,degenerate},unhandled-ellipse-cyl-{oncyl,uaffine,span,degenerate}). No build path is edited;exactMaxAtBindstill returnsunhandled-otherfor an ellipse until the bind lane binds through these classes (SPEC-ellipse PART B), soedgeUnhandledis unchanged by construction and B0 holds trivially. - The ELLIPSE bind class —
ellipse-on-plane(EXACT) andellipse-on-cylinder/ellipse-on-cylinder-bound(a certified upper bound) insrc/refit_ellipse_bind.{hpp,cpp}on top ofsrc/refit_ellipse_math.{hpp,cpp}, reached by two branches inexactMaxAtBind(D-140-9 §1/§3).unhandledon the 9-fixture edge-class red line was 55, and every one of the 55 was an ellipse — an oblique plane∩cylinder cut shipping as tier-1Geom_Ellipsewith no closed-form supremum. The plane side is exact: both curves are evaluated at the samet, soE(t) − S(pc(t)) = W + A cos t + B sin tidentically, and the supremum is the maximum of|·|over the real roots of the quartic inτ = tan(t/2)plus the two endpoints. The cylinder side is a bound, and says so in its name: under the section identity (centre on axis,a|Û⊥| = b|V̂⊥| = R,Û⊥·V̂⊥ = 0) the azimuth is affine — so|Δθ| ≤ ε_uexactly, from the poles, by the convex hull at the Greville abscissae — while the axial coordinate isz₀ + Λ cos(t−ψ), and no NURBS reproducescos tint, so the axial residual is bounded per span byL_n·max_j |v_pc(x_j) − z(x_j)| + Λ h^{n+1}/(2^{2n+1}(n+1)!)at then+1Chebyshev nodes, over an exact de Casteljau subdivision (every depth is a valid bound, so the minimum over a fixed depth set is taken). The two compose on the cylinder as√(4R² sin²(ε_u/2) + max|Δz|²). Refusal is by named clause, never by silence:unhandled-ellipse-{plane-param,plane-degenerate,cyl-oncyl,cyl-uaffine,cyl-span,cyl-degenerate,grazing}; the range clause carries D-130-16's seam sub-clause, so a full-turn ellipse edge is refused rather than certified. Measured:unhandled55 → 1 (Body28 25→0, S11-b 24→0, linkage_bores_chamfer 4→0, handle-pickup 1→0; S16-R1-explode-success stays 1 under the seam clause, residueellipse.s16-post-bind-tolerance), withoverTol,overCapandpolylineTier2unchanged on every fixture and every corpus STEP byte-identical. No tolerance writer, no pcurve writer and no instrument was touched. - J6/free-edge containment, landed then fully sunset (D-140-4, D-140-5, D-140-12; SPEC-reverts-cycle2 §2). Cycle 2 landed three edits; none of them ships. Change 3 (the J6
if (j6UncollapsePass > 0)handoff plussmoothContainedRegions/smoothContainedTriangles) was reverted by D-140-5's sunset. Changes 1–2 —regionClosureHealEligibledroppingbuiltAs == BuiltAs::Singlein favour ofregionBuiltAnalytic(...), andfallbackGuardPass == 0widened to<= 2— were kept as "dormant" by D-140-5, then reverted by D-140-12: on Linux they were live, not dormant, and letS16-R2-ChainUnstablecomplete the analytic path (spike-s16r2-linux: first build at78f85b4). Restored to the3bf0575form: eligibility requiresbuiltAs == Single; the heal stays behindfallbackGuardPass == 0. - Two RESULT keys,
smoothContainedRegionsandsmoothContainedTriangles(D-130-10 declaration;RefitStats::containedRegions/containedTriangles, summed inConverter::run). Incremented at exactly one place — the J6-handoff demotion, after the admitted set shrank. Never on the seamed360-fail explode path, never on the D-S3-54 closure heal (which already fires at3bf0575and keeps reporting itself throughDIAG_HEAL/DIAG_HEAL_SUM). The warningsmooth: contained N region(s) to facets -- component builtis emitted only whenN > 0.DIAG_CONTAINis emitted only from the J6 handoff, underdiagP2Enabled(), with a realfreeBefore > 0.
The site never shipped live containment on any artefact this project owned (keys0/0,DIAG_CONTAINcount0). Sunset (D-140-4 §1(5)) fired in D-140-5 for the J6 handoff. D-140-12 then reverted changes 1–2 as well: they were not dormant on Linux — they let S16-R2-ChainUnstable build (planes/cylinders/tier2) where macOS and3bf0575Linux still revert. Eligibility and reachability do not stay. S20_cross_bore_union, the same-surface union fixture, with its own negative control (D-140-8 U-R15/U-R16). An 80×60×40 block with a mid-span slot, one Ø20 bore on +X across both walls — so it ships as two coaxial faces at disjoint axial spans, [0,30] and [50,80] — and a Ø12 cross bore on +Z at x = 15 piercing the wall-A span atu = 90°andu = 270°, strictly inside it. Both bores are 48-gon prisms, and that is the mechanism: the two prisms' intersection polyline alternates between bore-edge × cross-facet vertices (exactly on R = 10) and cross-edge × bore-facet vertices, which lie on a chord of the bore and are therefore punctures in D-140-8 U-R6's sense — the plate's eight slot-window corners, reproduced by construction.gen_corpusasserts the topology on its own welded float32 mesh before any engine run and prints theS20-UNIONcensus: union boresigma=0.0214118609mm= 3240.66178 q,edgePieces=38,punctures=92,domainFaces=1,innerWires=2,windingPerWire=[0,0],pinchVertices=40; control boredomainFaces=1,innerWires=0;axialOverlap=0. The predicate is never relaxed — a deflection-halving retry loop runs up to 8 times and the lane throws if it exhausts.exactVolumeis a closed form on the generator's own parameters (inclusion–exclusion over slot ∪ bore ∪ cross bore; the two prisms' intersection integrand is piecewise quadratic, so Simpson on the breakpoint intervals is exact, not a quadrature) and is asserted againstBRepGProp::VolumePropertieson the exact solid:closedForm=142038.9690342799 BRepGProp=142038.96903428048. The PRG multiset ceiling for R = 10 on this fixture is therefore exactly 2: over-segmentation fails (D-130-14) and over-merging fails (U-R3).src/is untouched — the union itself is expected-red asS20.union-not-built.- Two real-export corpus fixtures,
shelf_bracket(2340 tris; 32 planes + 20 cylinders + 735 facet faces in one valid closed solid,volumeDeltaPct = 0.000142) andBody244(6268 tris;builtPlanes/builtCylinders364/178 (D-140-3 record),volumeDeltaPct = 0.000099,rid=1explodes to 148 facets under expected-redBody244.cyl360.rid1). shelf_bracket is the analytic-plus-facet-island proof on real geometry thatS17_hole_vs_islandwas to assert synthetically; S17 is withdrawn this cycle under expected-redS17.cap-claimed-by-a2(D-140-4 §4). stl2step_grade, the mesh-oracle recovery grader (tests/grade/, targetsstl2step_grade+stl2step_grade_selftest; D-140-1). Independent ofstl2step::core(grafts onlystl_quant); oracle classes plane / cylinder / cone / sphere / torus from the mesh atτ = 2q; seven training grades + ctest selftests. Cycle 6 torus/cone oracles certify on the training parts (D-140-1, D-140-6). Selftest core 68/68, synthetic 25/25.- Torus fixtures — S04 recoverable +
S19_mouth_round(D-140-6 §4, SPEC-torus PART A). S04'smustRemainFacetedTorusNYI is cleared and recoverable gainstorusRec(7.0, 3.0, {25,25,22}, +Z, 1, true)(STL bytes unchanged);S19_mouth_roundis a 60×60×20 box, R=10 through hole, mouth rounds r=2 top / r=1 bottom. Generator-exact sidecars and*.exact.step; S04 now builds a toroidal face; expected-redS19.mouth-round-not-builtstays;gates_fullI-checker XFAIL on S19 registered (D-140-6 §4(e)). Fixtures only on the fixture commit; build landed on this branch. - Volume-attribution identity, default-off (
STL2STEP_VOLID; D-140-10). Diag-only instrument besidedumpR2Probe: pcurve and contour readings ofv(F)=(1/3)[kA+p·N], the §2.5closesbracket, and the S01/S02/S03 unit (volid_identity17/17).faceVolumeContributionand the caller probe are untouched; no product read ofcloses. Knob unset/0is a no-op (OFF≡ON for STEP bytes). Measuredcloses8/9 (platevolid.cone-bracket).
Removed
- The J6 handoff containment round and its two RESULT keys (D-140-5): D-140-4 §1(5)'s sunset fired — 1 529 generated candidates found no constructible
S18_uncontainable_rim(clause A, an orphaned collapsed chain within the site's 1.0 mmmatchTol, fires 0 times post-heal) — sorefit_build.cpp:15877-15925reverts to its3bf0575form, dropping the containment round,smoothContainedRegions/smoothContainedTriangles,resetContained(), and thesmooth: contained ...warning. - J6 eligibility and heal reachability restored to
3bf0575(D-140-12, completing D-140-5): D-140-5 had left change 1 (regionClosureHealEligible→regionBuiltAnalytic) and change 2 (fallbackGuardPass <= 2) in place as widenings of a "live" heal. On Linux those two lines were the first-build at78f85b4for S16-R2-ChainUnstable (spike-s16r2-linux:smoothRevertedComponents1→0,polylineTier20→25,overCap0→1). Restored:builtAs == BuiltAs::SingleandfallbackGuardPass == 0. MacOS B0 is unchanged because the path was dormant there; Linux S16-R2 returns to the revert contract. - The
STL2STEP_UNIONdoor (D-140-8 U-R14): cycle 1 did not close U-R9 (plate seamed cylinder with 2 inner window wires =UnorientableShapest=27; domain-boundary extraction yields window-sized caps and fragment loops, not a face-construction miss). The door is deleted; merge/emit stay off so no partial construction lands. S20 fixture, census, and expected-red rows stay. The union engine is deferred to 1.5.
Tests (1.4.0)
- S19_mouth_round compose expectation (lane seeds-140c, D-130-24; D-140-6 §4(e)) — generator sidecar: six box planes in
recoverable[]; recoverable6/7/8 (R=10 closed360 cylinder + two coaxial mouth-round tori) declaredmustRemainFacetedwithS19.mouth-round-not-built/ TorusNYI wording; closed-form geometry kept in deferred comments. STL bytes unchanged;p1_compose_gategreen. - 1.4.0 instrument seeds (lane seeds-140, D-130-24) — Body244 / shelf_bracket live
volumeBudgetMM3fromvolumeBudgetMM3(|meshVolume|)(D-140-3 §4(b)); edge-class new cells for Body244, shelf_bracket, S19_mouth_round, S20_cross_bore_union fromedge_class_gate.py --measure; S11-b expected-red shrink; S20 live[] disposition ESCALATE (S20.union-not-builtsunset). S04 / recoverable7 torus rows left unseeded. Body28polylineTier2left at 1442 (macOS still 1442). S16-R2 B0 UNCERTAINTY exception retired (D-140-13): canonical STEP identical to3bf0575on all platforms. - Per-run scratch dirs in corpus engine check —
run_engine_check.shwrites fixture artefacts undermktemp -dinstead of fixed/tmp/${id}_*paths, so concurrent ctest batteries no longer truncate each other's RESULT JSON. - Windows
volid_identityportable launch (5ff406b; D-140-10): MSVC-hostedvolid_identityuses_putenv/_popeninstead of the POSIXsetenv/popenpath — no shell prefix — so the unit test launches on Windows CI. gates_fullctest TIMEOUT 3600→6000 (75e4e95): budget from measured wall time on the shop PC (~3870 s Windows/MSVC; macOS 883 s, Linux 1174 s); Body244 dominates on Windows (~55 min vs ~3 min macOS) — Windows I/O pathology, 1.4.1 follow-up. Harness budget only; no gate logic change.
Fixed
- J6 sunset completion — closure-heal eligibility and guard bound restored to
3bf0575(D-140-12; D-140-5; cd6e86f): D-140-5's "dormant" widenings (change 1:regionClosureHealEligible→regionBuiltAnalytic; change 2:fallbackGuardPass <= 2) were live on Linux and let S16-R2-ChainUnstable take the analytic path where macOS still reverted. RestoredbuiltAs == BuiltAs::SingleandfallbackGuardPass == 0. - A component with refused-mint composites takes the facet fallback when its shell fails (D-140-13; D-140-2):
buildWithInnersstill returns true after a failedprepare()on a mintRefused polyline-rim face — those faces can ship in a closing shell (shelf_bracket 32/20, Body244builtPlanes/builtCylinders364/178 (D-140-3 record)). When the component's shell then fails to close or validate,buildFacestakes the existing ChainUnstable → all-facet path instead of returning false, so S16-R2 ships as facets the way3bf0575did. Components without a mintRefused face keep the previous shell-failure semantics. Cap-mint refusal is unchanged. - RESULT census counts the shipped pass, not the torus probe (D-140-6, D-140-11): when stage T admits candidates, U-3 fails, and the pre-T snapshot rebuild ships,
smoothBuiltPlanes(and the other construction counters) were left on the discarded probe's RegionSet — Body11 1603→1590, Body28 499→488, Body244 364→338 — while the STEP stayed byte-identical. The pre-T snapshot now carries its ownRefitStatsand the revert restores it (same leak class as the discarded probe warnings). - A component with refused-mint composites takes the facet fallback when its shell fails (D-140-13; D-140-2):
buildWithInnersstill returns true after a failedprepare()on a mintRefused polyline-rim face — those faces can ship in a closing shell (shelf_bracket 32/20, Body244 364/178). When the component's shell then fails to close or validate,buildFacestakes the existing ChainUnstable → all-facet path instead of returning false, so S16-R2 ships as facets the way3bf0575did. Components without a mintRefused face keep the previous shell-failure semantics. Cap-mint refusal is unchanged. - Torus U-3 probe warnings discarded on topology revert (D-140-6, D-140-11): when stage T admits candidates but U-3 fails and the pre-T snapshot rebuild ships, warnings from the discarded probe build are no longer forwarded — Body244 returns to 2 warnings (IntAna + seamed360) with byte-identical STEP.
- Transactional torus cylinder reclaim (D-140-6, D-140-11): stage T still admits candidates, but a failed U-3 torus face build restores the pre-stage-T topology and rebuilds with fresh vertices so Body28/Body11 match the no-torus binary; cylinder reclaim commits only when
Geom_ToroidalSurfaceships. The torus closed-360 gate usesbandArc + asin(min(1, τ/hmin))(θ_q) instead of the retired1.5·bandArchand constant. - The OpenCASCADE toolkits the engine uses are named explicitly (github issue #1):
stl2step_corelinked only the finder's umbrella target, which omitted TKPrim (BRepPrimAPI_MakePrism) and TKBO (BRepAlgoAPI_Fuse,BOPAlgo_GlueEnum); they arrived transitively on Homebrew and on an OCCT that ships a CMake package config, and not at all under Debian/Ubuntu'sld --as-neededwhen OCCT is installed without one (undefined reference to BRepAlgoAPI_Fuse::~BRepAlgoAPI_Fuse()/libTKBO.so: DSO missing from command line). CMakeLists.txt now links the measured minimal cover of 15 toolkits, selected byif(TARGET ...)with afind_libraryfallback and the pre-7.8 TKSTL/TKSTEP* spellings as alternates. - takeFullCap may not mint against an unbuilt partner (D-140-2): a
Seamed360cap facing an island / eprimeFill / exploded / non-analytic region keeps the mesh polyline both sides already share, instead of inventing a full-2πCIRCLETShape that the fill cannot carry. shelf_bracket's eight through-holes ship as cylinders; the shell closes with noJ6:line. Body244 shipsbuiltPlanes/builtCylinders364/178 (D-140-3 record;volumeDeltaPct = 0.000099); the shell closes with noJ6:line. - Corpus checker parses sidecars linearly (MSVC hosted CI):
check_corpus.cppparsed each sidecar withstd::regexpatterns chaining[\s\S]*?across the document; MSVC throwsregex_error(error_complexity)on the plate sidecar (Exit code 0xc0000409, no stdout). Replaced with a single forward scan; matching semantics kept.
Changed
- Grader selftest expected-red, and its first row retired in the same release (D-140-7):
tests/gates/baseline/expected-red.jsongains agradersection andgrade_selftesttheXFAIL/XPASSreporting that reads it — a listed check that fails is reportedXFAILand excluded from the exit code; a listed check that passes is reportedXPASS, counted as a failure, and names the row to delete, so the list can only shrink. The section's first and only row,grade.reverse-seed.handle-pickup(case 9's cylinder-phase claim-order dependence), was registered when the grader landed and is gone again in this same 1.4.0 cycle. What closed it: the oracle's stage-A pass no longer consumes triangles or names a seed. Each class phase now answers one boolean per edge-connected component of the unclaimed set — does this component carry the class? — over a frozen ambient set, and §5.4's growth walk is then re-run over the union of the carrying components, entered at the lowest triangle index. That domain is a function of(mesh, class order)alone, so no permutation of the seed enumeration can move it; the three mechanisms that used to move it (band-offset chop, skipped-seed slack, two competing cylinders on the same quads) are each closed by it. The probe itself is untouched and still real —--reverse-seedsstill reversesseedList(), which still drives the growth for every seed, andgrade_selftest.cpp's assertions are unchanged. Selftest core 68/68 (was 67/68), synthetic 25/25, 0 XFAIL, 0 XPASS; the seven training grades are byte-identical under file order,--reverse-seedsand a third geometry-derived permutation (--seed-order n, triangle-centroid order), with no grade changed. The emptygradersection stays: the machinery is what landed, the deferral was not. - Same-surface union engine → 1.5 (D-140-8): the
STL2STEP_UNIONdoor is deleted, not compiled-in-off. 1.4.0 ships the seed piece(s) per surface (plate cross bore 214/279 in 2 faces); S20 remains expected-redS20.union-not-built.