Skip to content

stl2step 1.4.0 — engine only (downloads on v1.4.1)

Choose a tag to compare

@BlinkingSun BlinkingSun released this 06 Sep 17:46
· 2 commits to main since this release

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_ToroidalSurface face 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 at u0 / 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-red S19.mouth-round-not-built). The torus closed-360 gate uses bandArc + asin(min(1, τ/hmin)) (θ_q) instead of the retired 1.5·bandArc hand constant; U-3 probe warnings are discarded on topology revert.
  • Torus math — the circle-on-torus class (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). circleOnTorusMax returns, 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 — exactly 0 on every v-iso circle, in particular on the two tangency circles a mouth round is bounded by (S04: rho = 10 @ z = 22 and rho = 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. With pointTorusDist, torusVIsoCircle (the tier-1 bounds a blend patch carries) and torusVOfProfilePoint. 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; exactMaxAtBind still returns unhandled-surface for 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_DIAG the DIAG_LAWUNION line no longer reports the STL2STEP_UNION door'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 under max(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. sigma is U-R5's closed-form supremum max over the claim's welded edges of (R − dist(axis, midpoint)), punctures are U-R6's non-claim vertices within sigma of the surface, domainFaces counts U-R7's components of the claim plus the on-surface stars of its punctures, pinchVertices counts 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 TU src/refit_union_census.cpp (claimChordSagitta, unionCensus, declared in refit_internal.hpp; pure MeshView measurement, no Region, no build path) and unit target union_census_unit: sigma against the regular-ring closed form and a numerical supremum along every welded edge at three radii × three densities, world and placed frames, to Precision::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 55 unhandled edges on the 9-fixture edge-class red line are plane∩cylinder ellipses whose plane-side pcurve is the exact 2-D ELLIPSE GeomAPI::To2d writes and whose cylinder-side pcurve is one degree-8 Bézier with affine u and a sinusoid-approximating v. ellipseOnPlaneMax is the EXACT supremum of |E(t) − S(pc(t))| = |W + A cos t + B sin t| over [f, l]: the tan(t/2) quartic's real roots (Ferrari, threshold-free shift, fixed Newton polish) plus the endpoints; W = 0 collapses to σ_max([A B]) (ellipseTrigSupZeroOffset), the shipped case to exactly 0.0, and for a = b it is ≤ circleOnPlaneMax (the D-S3-111 triangle bound) — the sanity relation between the two classes. ellipseOnCylMax is a certified UPPER BOUND and says so in its name: the section identity (ellipseIsCylinderSection: ρ ≡ R, θ = θ₀ ± t, z = z₀ + Λ cos(t − ψ), residual carried exactly), ε_u exact 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/a and ellipseVertexBound = √(s² + h²) + s²/(2ρ_min) with |h| ≤ δ_pl, |s| ≤ (δ_cy + δ_pl|cos γ|)/sin γ; the class refuses (unhandled-ellipse-grazing) where that exceeds meshTolCap — 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; exactMaxAtBind still returns unhandled-other for an ellipse until the bind lane binds through these classes (SPEC-ellipse PART B), so edgeUnhandled is unchanged by construction and B0 holds trivially.
  • The ELLIPSE bind classellipse-on-plane (EXACT) and ellipse-on-cylinder / ellipse-on-cylinder-bound (a certified upper bound) in src/refit_ellipse_bind.{hpp,cpp} on top of src/refit_ellipse_math.{hpp,cpp}, reached by two branches in exactMaxAtBind (D-140-9 §1/§3). unhandled on 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-1 Geom_Ellipse with no closed-form supremum. The plane side is exact: both curves are evaluated at the same t, so E(t) − S(pc(t)) = W + A cos t + B sin t identically, 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 |Δθ| ≤ ε_u exactly, from the poles, by the convex hull at the Greville abscissae — while the axial coordinate is z₀ + Λ cos(t−ψ), and no NURBS reproduces cos t in t, so the axial residual is bounded per span by L_n·max_j |v_pc(x_j) − z(x_j)| + Λ h^{n+1}/(2^{2n+1}(n+1)!) at the n+1 Chebyshev 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: unhandled 55 → 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, residue ellipse.s16-post-bind-tolerance), with overTol, overCap and polylineTier2 unchanged 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 plus smoothContainedRegions/smoothContainedTriangles) was reverted by D-140-5's sunset. Changes 1–2 — regionClosureHealEligible dropping builtAs == BuiltAs::Single in favour of regionBuiltAnalytic(...), and fallbackGuardPass == 0 widened to <= 2 — were kept as "dormant" by D-140-5, then reverted by D-140-12: on Linux they were live, not dormant, and let S16-R2-ChainUnstable complete the analytic path (spike-s16r2-linux: first build at 78f85b4). Restored to the 3bf0575 form: eligibility requires builtAs == Single; the heal stays behind fallbackGuardPass == 0.
  • Two RESULT keys, smoothContainedRegions and smoothContainedTriangles (D-130-10 declaration; RefitStats::containedRegions / containedTriangles, summed in Converter::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 at 3bf0575 and keeps reporting itself through DIAG_HEAL / DIAG_HEAL_SUM). The warning smooth: contained N region(s) to facets -- component built is emitted only when N > 0. DIAG_CONTAIN is emitted only from the J6 handoff, under diagP2Enabled(), with a real freeBefore > 0.
    The site never shipped live containment on any artefact this project owned (keys 0/0, DIAG_CONTAIN count 0). 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 and 3bf0575 Linux 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 at u = 90° and u = 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_corpus asserts the topology on its own welded float32 mesh before any engine run and prints the S20-UNION census: union bore sigma=0.0214118609 mm = 3240.66178 q, edgePieces=38, punctures=92, domainFaces=1, innerWires=2, windingPerWire=[0,0], pinchVertices=40; control bore domainFaces=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. exactVolume is 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 against BRepGProp::VolumeProperties on 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 as S20.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) and Body244 (6268 tris; builtPlanes/builtCylinders 364/178 (D-140-3 record), volumeDeltaPct = 0.000099, rid=1 explodes to 148 facets under expected-red Body244.cyl360.rid1). shelf_bracket is the analytic-plus-facet-island proof on real geometry that S17_hole_vs_island was to assert synthetically; S17 is withdrawn this cycle under expected-red S17.cap-claimed-by-a2 (D-140-4 §4).
  • stl2step_grade, the mesh-oracle recovery grader (tests/grade/, targets stl2step_grade + stl2step_grade_selftest; D-140-1). Independent of stl2step::core (grafts only stl_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's mustRemainFaceted TorusNYI is cleared and recoverable gains torusRec(7.0, 3.0, {25,25,22}, +Z, 1, true) (STL bytes unchanged); S19_mouth_round is 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-red S19.mouth-round-not-built stays; gates_full I-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 beside dumpR2Probe: pcurve and contour readings of v(F)=(1/3)[kA+p·N], the §2.5 closes bracket, and the S01/S02/S03 unit (volid_identity 17/17). faceVolumeContribution and the caller probe are untouched; no product read of closes. Knob unset/0 is a no-op (OFF≡ON for STEP bytes). Measured closes 8/9 (plate volid.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 mm matchTol, fires 0 times post-heal) — so refit_build.cpp:15877-15925 reverts to its 3bf0575 form, dropping the containment round, smoothContainedRegions/smoothContainedTriangles, resetContained(), and the smooth: contained ... warning.
  • J6 eligibility and heal reachability restored to 3bf0575 (D-140-12, completing D-140-5): D-140-5 had left change 1 (regionClosureHealEligibleregionBuiltAnalytic) and change 2 (fallbackGuardPass <= 2) in place as widenings of a "live" heal. On Linux those two lines were the first-build at 78f85b4 for S16-R2-ChainUnstable (spike-s16r2-linux: smoothRevertedComponents 1→0, polylineTier2 0→25, overCap 0→1). Restored: builtAs == BuiltAs::Single and fallbackGuardPass == 0. MacOS B0 is unchanged because the path was dormant there; Linux S16-R2 returns to the revert contract.
  • The STL2STEP_UNION door (D-140-8 U-R14): cycle 1 did not close U-R9 (plate seamed cylinder with 2 inner window wires = UnorientableShape st=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) declared mustRemainFaceted with S19.mouth-round-not-built / TorusNYI wording; closed-form geometry kept in deferred comments. STL bytes unchanged; p1_compose_gate green.
  • 1.4.0 instrument seeds (lane seeds-140, D-130-24) — Body244 / shelf_bracket live volumeBudgetMM3 from volumeBudgetMM3(|meshVolume|) (D-140-3 §4(b)); edge-class new cells for Body244, shelf_bracket, S19_mouth_round, S20_cross_bore_union from edge_class_gate.py --measure; S11-b expected-red shrink; S20 live[] disposition ESCALATE (S20.union-not-built sunset). S04 / recoverable7 torus rows left unseeded. Body28 polylineTier2 left at 1442 (macOS still 1442). S16-R2 B0 UNCERTAINTY exception retired (D-140-13): canonical STEP identical to 3bf0575 on all platforms.
  • Per-run scratch dirs in corpus engine checkrun_engine_check.sh writes fixture artefacts under mktemp -d instead of fixed /tmp/${id}_* paths, so concurrent ctest batteries no longer truncate each other's RESULT JSON.
  • Windows volid_identity portable launch (5ff406b; D-140-10): MSVC-hosted volid_identity uses _putenv/_popen instead of the POSIX setenv/popen path — no shell prefix — so the unit test launches on Windows CI.
  • gates_full ctest 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: regionClosureHealEligibleregionBuiltAnalytic; change 2: fallbackGuardPass <= 2) were live on Linux and let S16-R2-ChainUnstable take the analytic path where macOS still reverted. Restored builtAs == BuiltAs::Single and fallbackGuardPass == 0.
  • A component with refused-mint composites takes the facet fallback when its shell fails (D-140-13; D-140-2): buildWithInners still returns true after a failed prepare() on a mintRefused polyline-rim face — those faces can ship in a closing shell (shelf_bracket 32/20, Body244 builtPlanes/builtCylinders 364/178 (D-140-3 record)). When the component's shell then fails to close or validate, buildFaces takes the existing ChainUnstable → all-facet path instead of returning false, so S16-R2 ships as facets the way 3bf0575 did. 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 own RefitStats and 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): buildWithInners still returns true after a failed prepare() 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, buildFaces takes the existing ChainUnstable → all-facet path instead of returning false, so S16-R2 ships as facets the way 3bf0575 did. 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_ToroidalSurface ships. The torus closed-360 gate uses bandArc + asin(min(1, τ/hmin)) (θ_q) instead of the retired 1.5·bandArc hand constant.
  • The OpenCASCADE toolkits the engine uses are named explicitly (github issue #1): stl2step_core linked 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's ld --as-needed when 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 by if(TARGET ...) with a find_library fallback and the pre-7.8 TKSTL/TKSTEP* spellings as alternates.
  • takeFullCap may not mint against an unbuilt partner (D-140-2): a Seamed360 cap facing an island / eprimeFill / exploded / non-analytic region keeps the mesh polyline both sides already share, instead of inventing a full-2π CIRCLE TShape that the fill cannot carry. shelf_bracket's eight through-holes ship as cylinders; the shell closes with no J6: line. Body244 ships builtPlanes/builtCylinders 364/178 (D-140-3 record; volumeDeltaPct = 0.000099); the shell closes with no J6: line.
  • Corpus checker parses sidecars linearly (MSVC hosted CI): check_corpus.cpp parsed each sidecar with std::regex patterns chaining [\s\S]*? across the document; MSVC throws regex_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.json gains a grader section and grade_selftest the XFAIL/XPASS reporting that reads it — a listed check that fails is reported XFAIL and excluded from the exit code; a listed check that passes is reported XPASS, 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-seeds still reverses seedList(), which still drives the growth for every seed, and grade_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-seeds and a third geometry-derived permutation (--seed-order n, triangle-centroid order), with no grade changed. The empty grader section stays: the machinery is what landed, the deferral was not.
  • Same-surface union engine → 1.5 (D-140-8): the STL2STEP_UNION door 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-red S20.union-not-built.