diff --git a/docs/research/aetheris-evaluation/README.md b/docs/research/aetheris-evaluation/README.md new file mode 100644 index 0000000..9023bc7 --- /dev/null +++ b/docs/research/aetheris-evaluation/README.md @@ -0,0 +1,145 @@ +# Aetheris evaluation (2026-10-14) + +Serves the operator's request to evaluate https://github.com/yuechen-li-dev/Aetheris (a C# AGPL-3.0 B-rep kernel) for capability and ideas, campaign brush-tree, session self-2026-10-14-ag. Facts only, measured at Aetheris 335dfb07df on Yggdrasil; Self's reading is in the conversation, not here. The torture harness is in `harness/`. + +# Aetheris evaluation facts + +Measured at Aetheris commit 335dfb07df97295c541ab3fcf50e37b9f97b1846 (2026-10-13, "Claude Perf optimization M3"), cloned to +`\eyes-aetheris\repo` (read only on Starfire; every build/run on Yggdrasil via ygg-verify.sh, image mcr.microsoft.com/dotnet/sdk:10.0, Linux). +Tags: [V] measured or read this pass; [S] stated by the repo's own docs only, not verified; [M] the author's marketing claim. +Job logs: `\eyes-aetheris\jobs\job1..job9.log` (job2 = tests, job6 = torture+canonical, job7/8/9 = OCCT detail probes). +Harness (mine, not Aetheris's): `harness\Program.cs`, `harness\cases.json`, `jobs\ocp_check.py`, `jobs\inspect.py`. + +## Q1 Shape of the codebase + +- [V] History: 2002 commits, all by one author (Yuechen Li), first 2026-02-25, last 2026-10-13. GitHub: 47 stars, AGPL-3.0 (gh api). 5119 files, 1545 .cs files, 293,876 lines of C#. +- [V] Lines of C# (git ls-files, `.cs`): Kernel.Firmament 66,280; Kernel.Core 57,373; Kernel.Firmament.Tests 40,718; Kernel.Core.Tests 25,513; CLI 12,119; CLI.Tests 10,172; Continuum 8,511; Firmament.FrictionLab 8,244; Surfacing 6,217; SheetMetal 5,749; StandardLibrary 3,831; the rest each under 3.7k. ~50 projects in `Aetheris.slnx`. +- [V] Test attribute counts ([Fact]/[Theory] lines): Firmament.Tests 1886, Core.Tests 909, CLI.Tests 379, FrictionLab.Tests 225, Continuum.Tests 135, Modules.Tests 94, SheetMetal.Tests 85, others smaller. 15 xunit projects. +- [V] CI: one workflow `.github/workflows/dotnet.yml`; the main job is `ubuntu-latest`: restore/build server and kernel tests, `dotnet test Aetheris.Kernel.Core.Tests --filter "Category!=SlowCorpus"`, then build and test the whole solution (`-m:1`). A Windows job (`public-library-build`, `workflow_dispatch` only) packs NuGet libraries. It also checks out a second repo, `yuechen-li-dev/Copeland` at a pinned commit (`scripts/managed-wgsl-commit-pin.txt`), packs it into a local NuGet feed (`.github/actions/prepare-managed-wgsl`). +- [V] Dependencies: Kernel.Core has one NuGet package, `Machina.Typography.OpenFont` (csproj). Kernel.Firmament adds `Copeland.TS.Backend.Wgsl 0.1.0-preview.1-a3.16c0ce996358af80`, which is NOT on nuget.org (restore error NU1101 `Copeland.TS`, job1.log) and has to be built from the Copeland repo. Copeland's licence is GPL-3.0 (gh api repos/yuechen-li-dev/Copeland/license). Other packages: Rhino3dm (3dm import), EF Core Sqlite, Roslyn, Swashbuckle, WebView2, DocumentFormat.OpenXml, PDFtoImage (each one project). +- [V] No OCCT or other native geometry kernel: no P/Invoke except one DllImport in `Aetheris.CLI/CadAssistantInspection.cs`; grep for opencascade/occt in Kernel.Core and Kernel.Firmament .cs finds nothing. OCCT appears only as an interop/validation reference in docs and tests. Geometry code is all C#. +- [V] Surface types (`Aetheris.Kernel.Core/Geometry/Surfaces/`): PlaneSurface, CylinderSurface, ConeSurface, SphereSurface, TorusSurface, LinearExtrusionSurface, SurfaceOfRevolutionSurface, BSplineSurfaceWithKnots (+ BSplineSurfaceRationalReduction), HelicalRibSurfaceRealizer. Curves (`Geometry/Curves/`): Line3, Circle3, Ellipse3, Hyperbola3, BSpline3Curve, CylindricalHelix3, CylinderCylinderIntersectionCurve, plus pcurves. +- [V] So "NURBS genuinely absent" is false as stated: B-spline curve and surface types exist in the kernel, and the STEP outputs of the Perforation/Presentation fixtures contain `B_SPLINE_CURVE_WITH_KNOTS` (job7 ZERO probe). Docs say products are "rational-free"/non-rational (docs/public/reference/supported-features.md, README.md:"exact analytical geometry as its foundation"). +- [V] Booleans are not a general intersection engine. `Brep/Boolean/BrepBoolean.cs` (2158 lines) plus ~20 sibling files (8,346 lines total) classify the pair into hard-coded recognition families (`BooleanExecutionClass { PlanarOnly, PlanarWithAnalyticHole, UnsupportedGeneralCase }`, BrepBoolean.cs:36-41; `BooleanTopLevelCase` enum at :74-84): axis-aligned box vs box (orthogonal cells), box with a world-Z cylinder/cone/sphere hole ("safe composition"), cylinder-root centre bore / keyway, prismatic through-cuts. Selection is by `JudgmentEngine` utility scoring (Kernel.Core/Judgment). Everything else returns an Error diagnostic `NotImplemented`. Tolerances: `ToleranceContext.Default` = linear 1e-6, angular 1e-9, relative 1e-12 (Numerics/ToleranceContext.cs:36); the family validators compare against `tolerance.Linear`. +- [V] No exact surface-surface intersection curve solver is used by the boolean entry point; the intersection-curve classes (CylinderCylinderIntersectionCurve, TransverseConePlaneIntersection) are used by feature builders, not by arbitrary boolean pairs (observed behaviour in Q3: Steinmetz refused). +- [V] Firmament does not expose CSG: docs/public/firmament/geometry.md:136 states "public Union / Subtract / Intersect, and general CSG trees are not Preview 3 Firmament features"; `scripts/Test-CanonicalFixtures.ps1` lists `Union|Subtract|Intersect` as a forbidden "legacy arbitrary Boolean authoring" pattern in canonical fixtures. Firmament authors feature-based models (Base, Boss, Pocket, Hole, Slot, Revolve, Loft, SectionChain...). Pocket is documented as "intentionally not arbitrary solid subtraction" (geometry.md:90). +- [V] Firmament -> kernel for CSG-ish work goes through `Aetheris.Kernel.Firmament/Materializer/CirBrepMaterializer.cs`: a registry of exactly three strategies, `subtract(box,cylinder)`, `subtract(box,box)`, `subtract(box,torus)` (the last one an "Unsupported" strategy), chosen by JudgmentEngine, from an SDF graph (`Aetheris.Continuum.Backends.Sdf`) (CirBrepMaterializer.cs:11-18). +- [S] README claims Aetheris is "a solved problem" relative to Parasolid/ACIS/OpenCascade; supported-features.md labels nearly everything "Bounded"/"Experimental" and says "general blend networks ... remain unsupported". + +## Q2 Build and tests on Linux (Yggdrasil container) + +- [V] Builds clean on Linux: `dotnet build` of Kernel.Core.Tests, Kernel.Firmament.Tests and Aetheris.CLI, Release, 0 errors, 0 warnings in the shown tails (job1/job2), after packing Copeland at the pin by hand. SDK 10.0 image; global.json asks 10.0.203 with latestFeature rollForward. +- [V] `Aetheris.Kernel.Core.Tests` (filter `Category!=SlowCorpus`): Passed 1063, Failed 0, Skipped 0, 27 s (job1.log). +- [V] `Aetheris.Kernel.Firmament.Tests`: Passed 2131, Failed 0, Skipped 0, 4 m 35 s (job2.log). +- [V] SlowCorpus lane (`Category=SlowCorpus`, 52 occurrences in Core.Tests): the run produced no output lines in my tail (job2.log "CORE-SLOW" empty); not established whether it ran zero tests or lacks the NIST corpus files. Unmeasured. +- [-] The other 13 test projects (CLI.Tests, Continuum.Tests, SheetMetal.Tests, Server.Tests, etc.) were not run; the web/TypeScript parts (Telos, Cadmata) were not built. +- [S] README/known-issues: "Release binaries are qualified for Windows x64 only. Framework tests on other platforms are not a binary-support promise." CI nonetheless runs the kernel and full solution tests on ubuntu-latest. + +## Q3 Boolean torture (35 cases through `BrepBoolean.Union/Subtract/Intersect` via the public Core API; plus 11 Firmament models; plus 320 canonical fixtures) + +Method: `harness/Program.cs` is a console app compiled as assembly `Aetheris.Kernel.Core.Tests` (to reuse the kernel's InternalsVisibleTo, same as the repo's own tests) that builds `BrepPrimitives.CreateBox/Cylinder/Sphere/Torus`, places them with a copy of the repo tests' `TransformBody` helper (extended to Sphere and Torus; plane/cylinder/line/circle only otherwise), runs the boolean, then `Step242Exporter.ExportBody`. Cases in `harness/cases.json`. Primitives are origin-centred; translation/rotation only. Caveat: the test helper does not transform cone surfaces or ellipse curves, which the cases do not use. +Results (job6.log; "vol" is Aetheris's own mass-property status): + +| # | case | outcome | +|---|---|---| +| 01 | coplanar face union, box+box face contact | OK, exported | +| 02 | flush slab subtract (coplanar top and sides) | OK | +| 03 | coplanar top pocket | OK | +| 04 | coincident-edge union (edge-only contact) | REFUSED "edge-only/point-only unions are non-manifold" | +| 05 | coincident-edge subtract | REFUSED "rejects tangent/zero-thickness subtract contact" | +| 06 | tangent cylinders union | REFUSED "only supports recognized axis-aligned boxes" | +| 07/08/09 | Steinmetz: cyl r5 x cyl r5 intersect / union / subtract | all REFUSED (07, 08 "only supports ... boxes"; 09 "supports only world-Z aligned root and center-bore cylinders") | +| 10 | subtract leaving two 1e-7 side walls | REFUSED, with the misleading message "subtraction fully removes the left box" | +| 11 | pocket leaving a 1e-7 floor | OK, exported (no sliver refusal at kernel level) | +| 12 | pocket leaving a 1e-4 floor | OK | +| 13/14/15/16 | spheres: tangent / 1e-9 / 1e-4 overlap / 6-apart union | all REFUSED "only supports ... boxes" | +| 17 | box minus sphere centred on top face | OK, exported (see Q4: wrong volume) | +| 18 | torus half cut by a box | REFUSED "only supports ... boxes" | +| 19/20 | torus union cyl / torus minus cyl | REFUSED | +| 21 | 10x10 hole grid, 100 sequential cylinder subtracts | OK, 100/100 steps, 954 ms | +| 22 | two tangent holes | REFUSED "tangent safe-hole composition is rejected to avoid zero-thickness geometry" | +| 23 | hole tangent to side wall | REFUSED with a long explanatory message ("would leave a wall of zero thickness along a line") | +| 24 | two holes with a 1e-7 web | REFUSED (same "tangent" message: 1e-7 < tolerance 1e-6 treated as tangent) | +| 25 | perpendicular cylinder through cylinder, union | REFUSED | +| 26 | coaxial tube (cyl minus smaller coaxial cyl) | OK, exported (see Q4: OCCT segfaults reading it) | +| 27 | blind hole from top | OK | +| 28 | oblique (30 deg) through hole | OK (Aetheris mass-property call threw ArgumentOutOfRangeException; STEP exported) | +| 29 | box intersect sphere | REFUSED | +| 30 | box union box overlapping at an offset corner | OK | +| 31 | blind pocket leaving a 1e-7 side wall | REFUSED "bounded orthogonal pocket family only supports a single mouth or one opposite-face through-slot" (refused for family reasons, not sliver) | +| 32 | union of boxes overlapping by a 1e-7 slab | OK | +| 33 | union with partial face contact | OK | +| 34 | sphere fully inside box subtract (void) | OK, exported as BREP_WITH_VOIDS (see Q4) | +| 35 | cylinder minus enclosed sphere | REFUSED "does not support analytic tool surface kind 'Sphere'" | + +- [V] Score: 35 cases: 14 succeeded, 21 refused (typed `NotImplemented` errors, no exceptions), 0 crashed, 0 timeouts. Every refusal message names a missing family rather than the geometry's true difficulty; only 04, 05, 22, 23, 24 are refusals of degeneracy (tangent/edge contact/zero-thickness). +- [V] All general curved-curved cases (tangent cylinders, Steinmetz x3, spheres x4, torus x3, perpendicular cylinders) are refused; none produced a result. The Steinmetz case is not computed. +- [V] A kernel-level sliver of thickness 1e-7 is NOT refused when it is a pocket floor (11) or a union overlap slab (32); the refusal claim holds for hole-hole and hole-wall tangency (22-24) and for pocket floor policy at the Firmament layer (below). +- [V] Firmament models (`harness/fa/*.firmament`, job6.log "FIRMAMENT-TORTURE"): 11 models via `aetheris build`: 4 built (fa05 hole poking through the edge, fa06 two holes with ample web, fa07 10x10 hole grid, fa10 pocket with a 3 mm floor), 7 refused with named diagnostics: + - fa01 hole tangent to side face: `FirmamentV2.SemanticHoleBuild: rejected: Hole 'Base.H0' ... is exactly tangent to the +X face ... zero thickness along a line` (Materializer/AirHoleFootprint.cs:55) + - fa02 tangent pair and fa03 1e-7 web: `firmament-v2-semantic-hole-overlap ... would be tangent to previously accepted hole` (AirHoleCompositeMaterializer.cs:80; Core BrepBooleanSafeCompositionGraphValidator.cs:229) + - fa04 overlapping holes: `overlaps previously accepted hole ... overlapping safe-hole composition is not supported` + - fa11 pocket with floor 9.9999999 mm deep in 10 mm host, required floor 1e-7: `firmament-pocket-through-depth ... remainingFloor=9.99999993922529E-08mm requiredFloor=1E-07mm` (float rounding put the floor just under the required value) + - fa12 required floor 0 mm: `firmament-pocket-minimum-floor-policy-invalid` + - fa13 floor 2 mm < required 3 mm: `firmament-pocket-minimum-floor-thickness` (PrismaticProfileComposition.cs:404) +- [V] Canonical fixtures (`fixtures/Canonical`, 320 `.firmament` files) through `aetheris build` (job6.log): 249 exit 0, 71 exit non-zero. The 71 are almost all not buildable by the `build` verb: garments (11, "garment-step-export-unsupported ... use garment build"), scenes (8, "scene-step-export-unsupported"), FEA/Analysis files (7, "firmament-v2-canonical-declaration-unknown:Analysis"), gear pairs (7, "gear-build-body-count-invalid ... use Assembly"), BodyState (6, "section-chain-section-count-invalid:minimum=2"), assembly components and Guitar/Atlas sub-files (~30, parse errors for partial files or "AssemblyIR must be successfully materialized"), 1 `Pipeline/profile-with-inner-loop` ("profile-segment-identity-collision"). I did not confirm each as expected-failure; they are reported as the CLI printed them. + +## Q4 STEP validity checked independently with OpenCASCADE + +Method: cadquery-ocp (pip, Ubuntu 24.04, Python 3.12) `STEPControl_Reader` default settings (`ReadFile`, `TransferRoots`, `OneShape`), `BRepCheck_Analyzer.IsValid`, `TopExp_Explorer` counts, `BRepGProp.VolumeProperties`; volume reference from an OCCT boolean (`BRepAlgoAPI_Fuse/Cut/Common`) on identical OCCT primitives and from analytic formulas. (job7.log OCP-CASES, job8.log INSPECT.) + +- [V] Harness cases that produced STEP (14): BRepCheck_Analyzer.IsValid = True for the 12 that OCCT could read. Detail: + +| case | OCCT reads as | volume Aetheris STEP | OCCT reference | +|---|---|---|---| +| 01 face union | 1 solid, 1 shell, 6 faces | 2000 | 2000 | +| 02 flush slab | 1 solid, 6 faces | 500 | 500 | +| 03 top pocket | compound, 0 solids, 4 shells, 14 faces | 920 | 920 | +| 11 floor 1e-7 | compound, 0 solids, 6 shells, 16 faces | 839.999992 | 840.000001 | +| 12 floor 1e-4 | compound, 0 solids, 4 shells, 14 faces | 840.0016 | 840.0016 | +| 17 box minus sphere (dimple) | 1 solid, 7 faces | **8261.799** | **7738.201** (= 8000 - 261.8); Aetheris's result is 8000 + 261.8, a bump, not a dimple | +| 21 hole grid 10x10 | 1 solid, 1 shell, 106 faces | 87433.629386 | 87433.629386 (analytic 87433.6294) | +| 27 blind hole | 1 solid, 8 faces | 3858.628331 | 3858.628331 | +| 28 oblique hole | 1 solid, 7 faces | 3673.516114 | 3673.516114 | +| 30 offset corner union | compound, 0 solids, 3 shells, 18 faces | 1875 | 1875 | +| 32 1e-7 slab union | 1 solid, 6 faces | 1999.99999 | 1999.999993 | +| 33 partial face contact | single SHELL, 0 solids, 10 faces | 1500 | 1500 | +| 26 coaxial tube | OCCT **segfaults** inside `TransferRoots` (python faulthandler: job9.log SEGV-26) | n/a | 904.1 analytic | +| 34 enclosed sphere void | OCCT **segfaults** inside `TransferRoots` (BREP_WITH_VOIDS + ORIENTED_CLOSED_SHELL .F., job8.log STEP-HEAD-34) | n/a | 7886.9 analytic | + +- [V] Every STEP file contains one `MANIFOLD_SOLID_BREP` over one `CLOSED_SHELL` (job8 INSPECT), but for cases 03, 11, 12, 30, 33 OCCT returns no SOLID. Inspecting the case-03 file (job9.log STEPFILE-03, saved as `jobs/case03.step`): 38 `EDGE_CURVE`s, 14 faces, 20 vertices; of the 56 `ORIENTED_EDGE` uses, 18 edges are used twice and 20 edges are used once. A closed shell requires every edge used twice, so this "CLOSED_SHELL" has 20 unmatched edges: faces are fragmented at coplanar/face-contact boundaries without edge sharing. OCCT's volume still sums to the right number; BRepCheck calls the result valid; downstream tooling expecting one solid gets a compound of shells. +- [V] Case 26's file (first 70 non-point lines, `jobs/case26.step`): two sets of three faces, circles r=5 and r=3, each end face a full disc with a single outer loop, no annulus; i.e. the "tube" is expressed as two cylinder shells, not a through-bore. Remaining entities were not inspected. +- [V] Case 17 (box minus sphere): both Aetheris's tessellated volume (8263.2) and OCCT's volume of Aetheris's STEP (8261.8) say the exported body is box + hemisphere. Cause not isolated (could be face orientation of the sphere face in the STEP, `ADVANCED_FACE(...,#119 SPHERICAL_SURFACE, .T.)`, or the harness's translated sphere frame). +- [V] Aetheris's own `BrepMassProperties` returned status `Unavailable` (volume 0) for cases 03, 11, 12, 26, 30, 33, and threw ArgumentOutOfRangeException for 28; `NumericalWithBound` (tessellation based, "not authoritative") for the others. +- [V] Firmament outputs (job6.log OCP-FA): fa05, fa06, fa07, fa10 all read as 1 solid, 1 shell, BRepCheck valid; fa07 (10x10 holes) 106 faces, volume 87433.629386 equals the analytic value; fa10 volume 13648.0 equals 16000-2352 exactly; fa05 8326.43, fa06 8434.51. +- [V] Canonical STEP outputs (253 files from job6, job6.log OCP-CANON, `jobs/canon_dir.txt`): all 253 read by OCCT, BRepCheck valid 253/253. Solid counts: 199 files with 1 solid; 48 multi-solid (assemblies and scenes, 2 to 1366 solids); 6 with 0 solids: SectionChain/open-chain (1 shell, 4 faces, intended open) and 5 where OCCT reported 1 root transferred but 0 solids/shells/faces and volume 0 (Perforation cylindrical-grid-8, -24, -ordning-blank, -staggered-32, Presentation ordning-utensil-holder-blended; files of 0.86 MB to 18 MB with many `B_SPLINE_CURVE_WITH_KNOTS`, `PCURVE`, `EDGE_CURVE`; a rebuild confirmed `success: true` from the CLI, job7 ZBUILD). Why those read as empty was not determined. +- [S] The "no healing needed" claim: the measurement above uses OCCT's default reader (which applies its own shape processing); I did not disable it. + +## Q5 GPU renderer ("Three Telos") + +- [V] Location `Aetheris.Web.Runtime/telos/` (TypeScript package `@aetheris/three-telos`, AGPL-3.0-only): 3,169 lines of TS/WGSL (host.ts 1067, device.ts 284, temporal.ts 302...). First commit touching it 2026-10-05; 6 commits, latest 2026-10-07. +- [V] It is a WebGPU renderer (`navigator.gpu`, `@webgpu/types`), not CUDA/Vulkan/native. It owns device, attachments, camera, ray queries, mesh/field/line submission; Three.js 0.183.2 is used only for math, input adapters and CPU triangle picks ("Three supplies math and input adapters, never a GPU renderer", telos/README.md). +- [V] Two draw paths: (a) rasterising tessellated triangle meshes (`src/shaders/mesh.wgsl`, `line.wgsl`, `background.wgsl`, 4x MSAA, experimental temporal AA); (b) "field" draws: a per-occurrence quad whose fragment shader sphere-traces a signed distance function with a fixed 512-step budget, normal by central differences, writes `frag_depth` (`Aetheris.Kernel.Firmament/Display/telos-field.v.ts`; docs/release/THREE-TELOS-X0.md). The WGSL is compiled from typed shader source by the Copeland compiler (GPL-3.0 repo), browsers receive WGSL only. The field path is documented only for the cylinder/sphere smoke shaders; "Full CIR-DISPLAY-X1B packet/admission/field default qualification is not claimed" (X0.md), and docs/release/CIR-DISPLAY-X1.md is an "Honest stop" (Cadmata/Helios cannot yet use the field path by default). +- [V] It does not ray trace the B-rep exactly. The B-rep is tessellated by `Brep/Tessellation/*` for the mesh path; picking is CPU (Three Raycaster/server BRep query). +- [V] Headless: the only automation found is Playwright with `chromium.launch({executablePath: msedge.exe, headless: true})` hard-coded to a Windows Edge path (`scripts/display-qa-x1-playwright.mjs:12,51`) and a `tests/browser-witness.ts`; unit tests run with `node --test` on camera/inspection/overlay/temporal logic. Not run in the container; no WebGPU device was tried. + +## Q6 Mechanisms + +- Degeneracy refusal [V]: it is not a type-system or compile-time proof. It is a layered set of runtime checks that return Error diagnostics before geometry is emitted: + 1. Kernel family validators compare geometry to `ToleranceContext.Linear` (1e-6): tangent hole-hole (`BrepBooleanSafeCompositionGraphValidator.cs:224-233`, `:827`), hole-wall tangency (`BrepBooleanCylinderRecognition.cs:452`), box-box edge/point-only contact ("non-manifold"), tangent spherical cavity (test `Subtract_BoxSphereTangentBoundary_ReturnsTangentContactDiagnostic`, BrepBooleanTests.cs:1555). + 2. Firmament semantic layer: `AirHoleFootprint.cs:55`, `AirHoleCompositeMaterializer.cs:80`, `PrismaticProfileComposition.cs:404` (pocket `MinimumFloorThickness`), profile-level checks (explicit polygons reject degenerate area and self-intersection, boundaries.md:62), template `Require` clauses. + 3. Anything outside the recognised families is refused wholesale (Q3), which also blocks most degenerate cases by construction. + Observed gap: 1e-7 pocket floors and 1e-7 overlap slabs pass the kernel API (cases 11, 32) and only the Firmament Pocket policy refuses (fa11-fa13). +- Firmament -> kernel [V]: V2 frontend in `Kernel.Firmament/FirmamentV2` (parse, static authoring, Concepts/Templates, validation, lowering), features lowered to "section-stack" operations and `PrismaticProfileComposition`/`AirHole*` materializers, then `BrepPrimitives`/`BrepBoolean`/`Step242Exporter`. V1 TOON/JSON path is kept as frozen compatibility (Kernel.Firmament/README.md). Ownership audit at `docs/development/architecture/system/artifacts/archaeology-m1/`. +- "Constraints erased at compile time" [V]: Firmament has no 2D constraint solver; layout is explicit coordinates in a `Concept Struct Layout On XY { Rect2 Stock { Center; Size } }` block and profiles are built by naming edges with `|>` (fixtures/Canonical/Integration/machined-mounting-block.firmament). Static Records/arrays/Tables, `with` derivation, `Template<...>` specialisation and `Pattern ... Over` are expanded and discarded before geometry (docs/public/firmament/overview.md; templates.md:100-108: `Require Name => BooleanExpression` is finite comparisons "not a general constraint solver", failure stops specialisation before materialisation). Units are part of value types and checked by the binder. +- Forge Host [V]: `Aetheris.Forge.Host` (1.4k lines) is a process protocol "Forge Host Protocol v1": commands `list`, `describe `, `invoke --request - --out ` with a JSON envelope (`protocolVersion`, `arguments` with unit strings like "120 mm", `artifacts`), response JSON with `success`; `describe` returns signature, units, defaults, enum cases, nested Record fields and named `Require` constraints. Clients in Python, Go, Rust, TypeScript shipped in `samples/forge-interop-x1/`; released as NativeAOT exe (Windows-qualified). Doc: docs/public/forge/interop.md; code `Aetheris.Forge.Host/ForgeProtocolHost.cs`, `Aetheris.Forge.KernelSDK/KernelSdk.cs`. +- LLM authoring [V]: repo-level `AGENTS.md` (use the CLI as ground truth, convergence rule: Success / Meaningful progression / Honest stop, fixture-location policy); a skill `docs/public/skills/firmament-modeling-strategy/SKILL.md` (+ `agents/openai.yaml`) on planning shape spines, separating measured vs estimated dimensions, and reporting compiler gaps with reproducible evidence; canonical cookbook `fixtures/Canonical/README.md`; `docs/development` has language audits and grammar inventory. README credits models (GPT-5/6, Claude 4/5, Gemini 3) for both the code and the showcase parts. +- GPU/CIR [V]: see Q5; the shared SDF "CIR" graph (`Aetheris.Continuum`, 8.5k lines) feeds both the field shader and the materializer strategies in Q1. + +## Q7 Licence + +- [V] `LICENSE` is the 661-line unmodified GNU AGPL v3 text; every csproj inherits `PackageLicenseExpression AGPL-3.0-only` from `Directory.Build.props`; the Telos package.json says AGPL-3.0-only. +- [V] README (section "License and acknowledgments"): "Aetheris code is licensed under the GNU Affero General Public License v3.0. Alternative licensing is available on request. Third-party assets retain their own terms and provenance; see THIRD_PARTY_NOTICES.md." The same line appears in docs/release/*README*.md and the preview-3 notes. +- [V] CONTRIBUTING.md ("Contributor license agreement status"): intended CLA lets contributors keep copyright and grants the owner "non-exclusive rights to distribute contributions under AGPL-3.0 and alternative licenses", with a commitment to continued AGPL availability; `docs/legal/CLA-CANDIDATE.md` is "candidate only; pending review by qualified human legal counsel", no acceptance mechanism is defined, maintainers "should not represent a contribution as having accepted that draft". Companion `CLA-COUNSEL-QUESTIONS.md`. +- [V] All 2002 commits are by the one author, so no third-party contributions exist yet to cover. NIST STEP models and the Stanford Bunny are noted as not under the Aetheris licence (README, THIRD_PARTY_NOTICES.md; earcut adapted from Mapbox, ISC). +- [V] Kernel.Firmament depends at build time on the Copeland repo, licensed GPL-3.0 (gh api), same author. diff --git a/docs/research/aetheris-evaluation/harness/Harness.csproj b/docs/research/aetheris-evaluation/harness/Harness.csproj new file mode 100644 index 0000000..af86f96 --- /dev/null +++ b/docs/research/aetheris-evaluation/harness/Harness.csproj @@ -0,0 +1,13 @@ + + + Exe + net10.0 + Aetheris.Kernel.Core.Tests + Harness + enable + enable + + + + + diff --git a/docs/research/aetheris-evaluation/harness/Program.cs b/docs/research/aetheris-evaluation/harness/Program.cs new file mode 100644 index 0000000..240a17c --- /dev/null +++ b/docs/research/aetheris-evaluation/harness/Program.cs @@ -0,0 +1,161 @@ +using System.Diagnostics; +using System.Text.Json; +using Aetheris.Kernel.Core.Brep; +using Aetheris.Kernel.Core.Brep.Boolean; +using Aetheris.Kernel.Core.Brep.Verification; +using Aetheris.Kernel.Core.Geometry; +using Aetheris.Kernel.Core.Geometry.Curves; +using Aetheris.Kernel.Core.Geometry.Surfaces; +using Aetheris.Kernel.Core.Math; +using Aetheris.Kernel.Core.Results; +using Aetheris.Kernel.Core.Step242; +using Aetheris.Kernel.Core.Topology; +using Aetheris.Kernel.Core.Diagnostics; + +static class P +{ + static (double c, double s) CS(double deg) + { + var d = ((deg % 360) + 360) % 360; + if (d == 0) return (1, 0); + if (d == 90) return (0, 1); + if (d == 180) return (-1, 0); + if (d == 270) return (0, -1); + var r = deg * Math.PI / 180; + return (Math.Cos(r), Math.Sin(r)); + } + + static Transform3D Make(double rxDeg, double ryDeg, double[] p) + { + var (cx, sx) = CS(rxDeg); + var (cy, sy) = CS(ryDeg); + var rx = Transform3D.FromRowMajor(new double[] { 1, 0, 0, 0, 0, cx, sx, 0, 0, -sx, cx, 0, 0, 0, 0, 1 }); + var ry = Transform3D.FromRowMajor(new double[] { cy, 0, -sy, 0, 0, 1, 0, 0, sy, 0, cy, 0, 0, 0, 0, 1 }); + var t = Transform3D.CreateTranslation(new Vector3D(p[0], p[1], p[2])); + return rx * ry * t; + } + + static BrepBody Tr(BrepBody body, Transform3D t) + { + var g = new BrepGeometryStore(); + foreach (var c in body.Geometry.Curves) + { + g.AddCurve(c.Key, c.Value.Kind switch + { + CurveGeometryKind.Line3 => CurveGeometry.FromLine(new Line3Curve(t.Apply(c.Value.Line3!.Value.Origin), t.Apply(c.Value.Line3.Value.Direction))), + CurveGeometryKind.Circle3 => CurveGeometry.FromCircle(new Circle3Curve(t.Apply(c.Value.Circle3!.Value.Center), t.Apply(c.Value.Circle3.Value.Normal), c.Value.Circle3.Value.Radius, t.Apply(c.Value.Circle3.Value.XAxis))), + _ => c.Value + }); + } + foreach (var s in body.Geometry.Surfaces) + { + g.AddSurface(s.Key, s.Value.Kind switch + { + SurfaceGeometryKind.Plane => SurfaceGeometry.FromPlane(new PlaneSurface(t.Apply(s.Value.Plane!.Value.Origin), t.Apply(s.Value.Plane.Value.Normal), t.Apply(s.Value.Plane.Value.UAxis))), + SurfaceGeometryKind.Cylinder => SurfaceGeometry.FromCylinder(new CylinderSurface(t.Apply(s.Value.Cylinder!.Value.Origin), t.Apply(s.Value.Cylinder.Value.Axis), s.Value.Cylinder.Value.Radius, t.Apply(s.Value.Cylinder.Value.XAxis))), + SurfaceGeometryKind.Sphere => SurfaceGeometry.FromSphere(new SphereSurface(t.Apply(s.Value.Sphere!.Value.Center), t.Apply(s.Value.Sphere.Value.Axis), s.Value.Sphere.Value.Radius, t.Apply(s.Value.Sphere.Value.XAxis))), + SurfaceGeometryKind.Torus => SurfaceGeometry.FromTorus(new TorusSurface(t.Apply(s.Value.Torus!.Value.Center), t.Apply(s.Value.Torus.Value.Axis), s.Value.Torus.Value.MajorRadius, s.Value.Torus.Value.MinorRadius, t.Apply(s.Value.Torus.Value.XAxis))), + _ => s.Value + }); + } + var vp = new Dictionary(); + foreach (var v in body.Topology.Vertices) + if (body.TryGetVertexPoint(v.Id, out var pt)) vp[v.Id] = t.Apply(pt); + return new BrepBody(body.Topology, g, body.Bindings, vp, body.SafeBooleanComposition); + } + + static KernelResult Prim(JsonElement e) + { + var k = e.GetProperty("k").GetString()!; + var d = e.GetProperty("d").EnumerateArray().Select(x => x.GetDouble()).ToArray(); + var p = e.GetProperty("p").EnumerateArray().Select(x => x.GetDouble()).ToArray(); + var rx = e.GetProperty("rx").GetDouble(); + var ry = e.GetProperty("ry").GetDouble(); + KernelResult r; + if (k == "box") r = BrepPrimitives.CreateBox(d[0], d[1], d[2]); + else if (k == "cyl") r = BrepPrimitives.CreateCylinder(d[0], d[1]); + else if (k == "sph") r = BrepPrimitives.CreateSphere(d[0]); + else if (k == "tor") r = BrepPrimitives.CreateTorus(d[0], d[1]); + else throw new Exception("kind " + k); + if (!r.IsSuccess) return r; + if (rx == 0 && ry == 0 && p.All(x => x == 0)) return r; + return KernelResult.Success(Tr(r.Value, Make(rx, ry, p))); + } + + static string Short(IEnumerable ds, int n = 3) + => string.Join(" || ", ds.Take(n).Select(d => "[" + d.Severity + "/" + d.Code + "] " + d.Message)).Replace("\n", " "); + + static int Main(string[] a) + { + var cases = JsonDocument.Parse(File.ReadAllText(a[0])).RootElement; + var outDir = a[1]; + Directory.CreateDirectory(outDir); + var only = a.Length > 2 ? a[2] : null; + foreach (var c in cases.EnumerateArray()) + { + var name = c.GetProperty("name").GetString()!; + if (only != null && !name.StartsWith(only)) continue; + var line = new Dictionary { ["name"] = name }; + var sw = Stopwatch.StartNew(); + var cc = c; + var task = Task.Run(() => + { + var cur = Prim(cc.GetProperty("a")); + if (!cur.IsSuccess) throw new Exception("PRIM(a): " + Short(cur.Diagnostics)); + var op = cc.GetProperty("op").GetString(); + int i = 0; + int n = cc.GetProperty("bs").GetArrayLength(); + foreach (var b in cc.GetProperty("bs").EnumerateArray()) + { + var rb = Prim(b); + if (!rb.IsSuccess) throw new Exception("PRIM(b" + i + "): " + Short(rb.Diagnostics)); + if (op == "union") cur = BrepBoolean.Union(cur.Value, rb.Value); + else if (op == "subtract") cur = BrepBoolean.Subtract(cur.Value, rb.Value); + else cur = BrepBoolean.Intersect(cur.Value, rb.Value); + i++; + if (!cur.IsSuccess) { line["failedAtStep"] = i + "/" + n; return cur; } + } + line["stepsDone"] = i + "/" + n; + return cur; + }); + try + { + if (!task.Wait(TimeSpan.FromSeconds(180))) { line["status"] = "TIMEOUT"; } + else + { + var r = task.Result; + if (!r.IsSuccess) { line["status"] = "REFUSED"; line["msg"] = Short(r.Diagnostics); } + else + { + line["status"] = "OK"; + line["warn"] = Short(r.Diagnostics.Where(d => d.Severity != KernelDiagnosticSeverity.Info), 2); + try + { + var m = BrepMassProperties.Evaluate(r.Value); + line["vol"] = m.Status + ":" + m.AbsoluteVolume.ToString("R") + ":" + m.EvaluationMethod; + } + catch (Exception ex) { line["vol"] = "EX " + ex.GetType().Name; } + try + { + var s = Step242Exporter.ExportBody(r.Value); + if (s.IsSuccess) { File.WriteAllText(Path.Combine(outDir, name + ".step"), s.Value); line["step"] = "exported"; } + else line["step"] = "EXPORT_REFUSED " + Short(s.Diagnostics); + } + catch (Exception ex) { line["step"] = "EXPORT_CRASH " + ex.GetType().Name + ": " + ex.Message; } + } + } + } + catch (AggregateException ex) + { + var m = ex.InnerException?.Message ?? ex.Message; + bool prim = m.StartsWith("PRIM"); + line["status"] = prim ? "HARNESS_PRIM_FAIL" : "CRASH"; + line["msg"] = (ex.InnerException?.GetType().Name + ": " + m).Replace("\n", " "); + if (!prim) line["stack"] = (ex.InnerException?.StackTrace ?? "").Split('\n').Take(3).Select(s => s.Trim()).ToArray(); + } + line["ms"] = sw.ElapsedMilliseconds; + Console.WriteLine("RESULT " + JsonSerializer.Serialize(line)); + } + return 0; + } +} diff --git a/docs/research/aetheris-evaluation/harness/cases.json b/docs/research/aetheris-evaluation/harness/cases.json new file mode 100644 index 0000000..84c0f4a --- /dev/null +++ b/docs/research/aetheris-evaluation/harness/cases.json @@ -0,0 +1,2698 @@ +[ +{ +"name": "01_coplanar_face_union", +"op": "union", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +10, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 2000, +"note": "" +}, +{ +"name": "02_coplanar_flush_slab_subtract", +"op": "subtract", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +10, +10, +5 +], +"p": [ +0, +0, +2.5 +], +"rx": 0, +"ry": 0 +} +], +"exp": 500, +"note": "" +}, +{ +"name": "03_coplanar_top_pocket", +"op": "subtract", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +4, +4, +5 +], +"p": [ +0, +0, +2.5 +], +"rx": 0, +"ry": 0 +} +], +"exp": 920, +"note": "" +}, +{ +"name": "04_coincident_edge_union", +"op": "union", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +10, +10, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 2000, +"note": "edge-only contact" +}, +{ +"name": "05_coincident_edge_subtract", +"op": "subtract", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +10, +10, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 1000, +"note": "edge-only contact" +}, +{ +"name": "06_tangent_cylinders_union", +"op": "union", +"a": { +"k": "cyl", +"d": [ +5, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +5, +10 +], +"p": [ +10, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 1570.7963267948967, +"note": "" +}, +{ +"name": "07_steinmetz_intersect", +"op": "intersect", +"a": { +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +0, +0, +0 +], +"rx": 90, +"ry": 0 +} +], +"exp": 666.6666666666666, +"note": "" +}, +{ +"name": "08_steinmetz_union", +"op": "union", +"a": { +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +0, +0, +0 +], +"rx": 90, +"ry": 0 +} +], +"exp": 2474.925986923127, +"note": "" +}, +{ +"name": "09_steinmetz_subtract", +"op": "subtract", +"a": { +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +0, +0, +0 +], +"rx": 90, +"ry": 0 +} +], +"exp": 904.1296601282301, +"note": "" +}, +{ +"name": "10_sliver_wall_1e-7", +"op": "subtract", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +9.9999998, +20, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 2e-05, +"note": "two walls 1e-7 thick" +}, +{ +"name": "11_sliver_floor_1e-7", +"op": "subtract", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +4, +4, +20 +], +"p": [ +0, +0, +5.0000001 +], +"rx": 0, +"ry": 0 +} +], +"exp": 840.0000016, +"note": "floor 1e-7" +}, +{ +"name": "12_thin_floor_1e-4", +"op": "subtract", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +4, +4, +20 +], +"p": [ +0, +0, +5.0001 +], +"rx": 0, +"ry": 0 +} +], +"exp": 840.0016, +"note": "floor 1e-4" +}, +{ +"name": "13_spheres_tangent_union", +"op": "union", +"a": { +"k": "sph", +"d": [ +5 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +5 +], +"p": [ +10, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 1047.1975511965977, +"note": "exactly tangent" +}, +{ +"name": "14_spheres_neartangent_1e-9", +"op": "union", +"a": { +"k": "sph", +"d": [ +5 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +5 +], +"p": [ +9.999999999, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "gap-overlap 1e-9" +}, +{ +"name": "15_spheres_neartangent_1e-4", +"op": "union", +"a": { +"k": "sph", +"d": [ +5 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +5 +], +"p": [ +9.9999, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "overlap 1e-4" +}, +{ +"name": "16_spheres_overlap_union", +"op": "union", +"a": { +"k": "sph", +"d": [ +5 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +5 +], +"p": [ +6, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 938.2890058721515, +"note": "" +}, +{ +"name": "17_sphere_in_box_dimple", +"op": "subtract", +"a": { +"k": "box", +"d": [ +20, +20, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +5 +], +"p": [ +0, +0, +10 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "sphere centred on top face" +}, +{ +"name": "18_torus_half_cut", +"op": "subtract", +"a": { +"k": "tor", +"d": [ +10, +3 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +40, +40, +10 +], +"p": [ +0, +0, +-5 +], +"rx": 0, +"ry": 0 +} +], +"exp": 888.2643960980423, +"note": "" +}, +{ +"name": "19_torus_union_cyl_through_hole", +"op": "union", +"a": { +"k": "tor", +"d": [ +10, +3 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +7, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "" +}, +{ +"name": "20_torus_minus_cyl_inner_tangent", +"op": "subtract", +"a": { +"k": "tor", +"d": [ +10, +3 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +7, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "cyl r=7 tangent to torus inner equator" +}, +{ +"name": "21_hole_grid_10x10", +"op": "subtract", +"a": { +"k": "box", +"d": [ +100, +100, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +45, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 87433.62938564083, +"note": "" +}, +{ +"name": "22_hole_tangent_pair", +"op": "subtract", +"a": { +"k": "box", +"d": [ +30, +30, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +-5, +0, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +5, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "two holes tangent to each other" +}, +{ +"name": "23_hole_tangent_to_side", +"op": "subtract", +"a": { +"k": "box", +"d": [ +30, +30, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +10, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "hole tangent to side wall" +}, +{ +"name": "24_hole_thin_web_1e-7", +"op": "subtract", +"a": { +"k": "box", +"d": [ +30, +30, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +-5, +0, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +5.0000001, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "web between holes 1e-7" +}, +{ +"name": "25_cyl_perp_T_union", +"op": "union", +"a": { +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +3, +20 +], +"p": [ +0, +0, +0 +], +"rx": 90, +"ry": 0 +} +], +"exp": null, +"note": "perpendicular smaller cylinder through" +}, +{ +"name": "26_coaxial_tube", +"op": "subtract", +"a": { +"k": "cyl", +"d": [ +5, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +3, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 502.6548245743669, +"note": "" +}, +{ +"name": "27_box_minus_cyl_blind", +"op": "subtract", +"a": { +"k": "box", +"d": [ +20, +20, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +3, +5 +], +"p": [ +0, +0, +2.5 +], +"rx": 0, +"ry": 0 +} +], +"exp": 3858.6283305884594, +"note": "blind hole from top" +}, +{ +"name": "28_box_minus_cyl_oblique", +"op": "subtract", +"a": { +"k": "box", +"d": [ +20, +20, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +3, +30 +], +"p": [ +0, +0, +0 +], +"rx": 30, +"ry": 0 +} +], +"exp": null, +"note": "oblique through-hole" +}, +{ +"name": "29_box_intersect_sphere", +"op": "intersect", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +6 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "sphere bigger than box inscribed" +}, +{ +"name": "30_box_union_box_overlap_offset", +"op": "union", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +5, +5, +5 +], +"rx": 0, +"ry": 0 +} +], +"exp": 1875, +"note": "" +}, +{ +"name": "31_sliver_side_wall_pocket_1e-7", +"op": "subtract", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +4.9999999, +4, +10 +], +"p": [ +-2.49999995, +0, +5 +], +"rx": 0, +"ry": 0 +} +], +"exp": 900.000002, +"note": "blind pocket leaving 1e-7 side wall" +}, +{ +"name": "32_union_overlap_slab_1e-7", +"op": "union", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +9.9999999, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 1999.99999, +"note": "boxes overlap by a 1e-7 slab" +}, +{ +"name": "33_union_partial_face_contact", +"op": "union", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +10, +5, +10 +], +"p": [ +10, +2.5, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 1500, +"note": "face contact, B face inside A face" +}, +{ +"name": "34_sphere_inside_box_void", +"op": "subtract", +"a": { +"k": "box", +"d": [ +20, +20, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +3 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 7886.902664470767, +"note": "sphere fully enclosed" +}, +{ +"name": "35_cyl_minus_sphere", +"op": "subtract", +"a": { +"k": "cyl", +"d": [ +5, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +3 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "sphere enclosed in cylinder" +} +] \ No newline at end of file diff --git a/docs/research/aetheris-evaluation/harness/edges.py b/docs/research/aetheris-evaluation/harness/edges.py new file mode 100644 index 0000000..a693b05 --- /dev/null +++ b/docs/research/aetheris-evaluation/harness/edges.py @@ -0,0 +1,9 @@ +import sys, re, collections +for f in sys.argv[1:]: + t = open(f).read() + ec = set(re.findall(r"#(\d+)=EDGE_CURVE", t)) + uses = collections.Counter() + for m in re.finditer(r"ORIENTED_EDGE\([^,]*,\*,\*,#(\d+),", t): + uses[m.group(1)] += 1 + hist = collections.Counter(uses.values()) + print("EDGES", f.split("/")[-1], "edge_curves", len(ec), "oriented_edge_uses_per_edge_histogram", dict(hist), "edges_never_used", len(ec - set(uses))) diff --git a/docs/research/aetheris-evaluation/harness/inspect.py b/docs/research/aetheris-evaluation/harness/inspect.py new file mode 100644 index 0000000..616ed6d --- /dev/null +++ b/docs/research/aetheris-evaluation/harness/inspect.py @@ -0,0 +1,29 @@ +import sys, re, collections, math +from OCP.STEPControl import STEPControl_Reader +from OCP.TopAbs import TopAbs_SOLID, TopAbs_SHELL, TopAbs_FACE, TopAbs_COMPOUND +from OCP.TopExp import TopExp_Explorer +from OCP.BRepCheck import BRepCheck_Analyzer +from OCP.GProp import GProp_GProps +from OCP.BRepGProp import BRepGProp +from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds +from OCP.TopoDS import TopoDS +def vol(s): + p = GProp_GProps(); BRepGProp.VolumeProperties_s(s, p); return round(p.Mass(), 6) +KEYS = ["MANIFOLD_SOLID_BREP","BREP_WITH_VOIDS","CLOSED_SHELL","OPEN_SHELL","ORIENTED_CLOSED_SHELL","SHELL_BASED_SURFACE_MODEL","ADVANCED_FACE","SPHERICAL_SURFACE","CYLINDRICAL_SURFACE","PLANE","FACE_BOUND","FACE_OUTER_BOUND"] +for f in sys.argv[1:]: + t = open(f).read() + ents = collections.Counter(re.findall(r"=\s*([A-Z_0-9]+)\s*\(", t)) + print("INSPECT", f.split("/")[-1], {k: ents[k] for k in KEYS if ents[k]}) + rd = STEPControl_Reader(); rd.ReadFile(f); rd.TransferRoots(); s = rd.OneShape() + print(" OCCT shape type", s.ShapeType(), "valid", BRepCheck_Analyzer(s).IsValid(), "vol", vol(s)) + for kind, nm in ((TopAbs_SOLID, "solid"), (TopAbs_SHELL, "shell")): + ex = TopExp_Explorer(s, kind); i = 0 + while ex.More(): + sh = ex.Current() + try: + fb = ShapeAnalysis_FreeBounds(sh) + nopen = fb.GetOpenWires().NbChildren() if hasattr(fb.GetOpenWires(), "NbChildren") else "?" + except Exception as e: + nopen = "err" + print(" ", nm, i, "vol", vol(sh), "valid", BRepCheck_Analyzer(sh).IsValid(), "freeOpenWires", nopen) + i += 1; ex.Next() diff --git a/docs/research/truck-evaluation/README.md b/docs/research/truck-evaluation/README.md new file mode 100644 index 0000000..161fbf8 --- /dev/null +++ b/docs/research/truck-evaluation/README.md @@ -0,0 +1,122 @@ +# truck evaluation (2026-10-14) + +Serves the operator's request to evaluate truck (https://github.com/ricosjp/truck), a Rust B-rep kernel, after the Aetheris evaluation (`../aetheris-evaluation/`), same method and cases. Campaign brush-tree, session self-2026-10-14-ag. Facts only, measured on Yggdrasil at truck 88ed0052. + + +Serves the operator's request to evaluate https://github.com/ricosjp/truck (Rust B-rep CAD kernel), campaign brush-tree, session self-2026-10-14-ag. Same method and cases as `F:\Projects\GameCult.Geometry.Csg\docs\research\aetheris-evaluation\README.md`. Facts only; no recommendation. + +Measured at truck commit `88ed005249e5e3a6b07f62425399435905cd3ab6` (master, 2026-09-28, "Merge branch 'cargo-upgrade-20260928'"), cloned read-only to `\eyes-truck\repo`. Nothing from truck was built or run on Starfire; every build/test/run was on Yggdrasil through `ygg-verify.sh` (image eureka-verify-rust, rustc/cargo 1.95.0, Debian 12, 4 CPUs, 6 GiB). +`` = `C:\Users\Meta\AppData\Local\Temp\claude\F--Projects-CultLib\5ea0b87f-66d0-4b24-8d36-45015e4cedc1\scratchpad`. +Tags: [V] measured or read this pass; [S] stated by the repo's own docs only; [M] marketing claim. +Logs: `eyes-truck\jobs\job1.log` (truck's own tests), `job2.log` (first harness run, tol 0.05), `job4.log` (harness: 35 cases x tol 0.05/0.01/0.2, OCCT checks, truck reading OCCT STEP), `job5.log` (licences, wasm, determinism), `job6.log` (STEP entity census, case-25 stage timing, tolerance sweep). `jobs\analyze.py` digests job4. +Harness (mine, not truck's): `eyes-truck\harness\` = local git repo (`src\main.rs`, `check.py`, `cases.json` copied unchanged from the Aetheris harness, `job.sh`, `job5.sh`, `job6.sh`); depends on truck by git rev 88ed0052. Built `--release`. + +## Q1 Shape + +- [V] Repo: GitHub + GitLab (CI is `.gitlab-ci.yml`); 2872 commits since 2020-05-18; 273 `.rs` files, 71,555 lines of Rust (git ls-files). GitHub: 1621 stars, 41 open issues, not archived, Apache-2.0 (GitHub API). README: "Truck is a RUst Cad Kernel"; concept "Rust and WebGPU", "re-implementing the B-rep with NURBS". +- [V] Maintainers: one organisation's staff. Commit authors (all history): Yoshinori Tanimura 2482 + alias "Tanimura Yoshinori" 110, "truck" 244, then Moritz Moeller 9, Greg Rauhoeft 5, Jinlei Li 3, others 1-3. Since 2025-10-14: 253 commits, by Tanimura (127+72), "truck" 52, thayashi-tech 1. Commits per calendar year: 2022 298, 2023 393, 2024 389, 2025 325, 2026 (to 09-28) 189. Many recent commits are titled "cargo upgrade" (git log -- truck-shapeops: the last five are all "cargo upgrade", 2026-05-25..2026-09-28). +- [V] Releases: 73 git tags; newest tag/crates.io release is 2024-09-20 (truck-modeling 0.6.0, shapeops 0.4.0, stepio 0.3.0, meshalgo 0.4.0, geometry 0.5.0...; crates.io API). master is two years of unreleased work: CHANGELOG "Unreleased" lists EdgeBlendSurface, Offset geometry, fillet for a single edge, truck-drafting (2D crate), truck-assembly (assembly read/write in STEP), higher-order derivatives, wgpu v26. The git-rev dependency I measured is this unreleased master, not the crates.io 0.4.0 shapeops. crates.io downloads: truck-geometry 130k, truck-modeling 127k, truck-stepio 103k, truck-meshalgo 60k, truck-shapeops 13k. +- [V] Crates (version, Rust lines): truck-base 0.5.0 (3,207), truck-geotrait 0.4.0 (2,211), truck-geometry 0.5.0 (15,200: B-spline/NURBS, knot vectors, decorators), truck-topology 0.6.0 (5,956: vertex/edge/wire/face/shell/solid), truck-polymesh 0.6.0 (2,901: mesh structs, OBJ/STL), truck-meshalgo 0.4.0 (6,617: tessellation via `spade` CDT, mesh filters/analyzers, VTK), truck-modeling 0.6.0 (3,878: builder, tsweep/rsweep/homotopy), truck-shapeops 0.4.0 (8,531: booleans, fillet, healing), truck-stepio 0.3.0 (10,537: STEP in/out via `ruststep`), truck-assembly 0.1.0 (1,740), truck-drafting 0.1.0 (1,883, 2D), truck-derivers 0.1.0 (1,344, derive macros), truck-platform 0.6.0 (2,981, wgpu), truck-rendimpl 0.6.0 (3,797, wgpu rendering), truck-js 0.2.0 (592, wasm-bindgen wrapper). Plus example-pages-generator and readme-generator in the workspace. +- [V] Licence: repo `LICENSE` is Apache-2.0; 13 crate manifests say `license = "Apache-2.0"`; `truck-assembly` and `truck-drafting` manifests carry no `license` field (cargo metadata: truck-assembly licence null). Transitive dependency tree of the harness (truck-modeling+shapeops+meshalgo+stepio, 135 packages, cargo metadata, job5.log): MIT OR Apache-2.0 85, Apache-2.0 15, MIT 14, Apache-2.0 OR MIT 5, MIT/Apache-2.0 4, Unlicense OR MIT 3, Zlib OR Apache-2.0 OR MIT 2, BSD-2-Clause 1, Zlib 1, Unlicense/MIT 1, MIT OR Apache-2.0 OR LGPL-2.1-or-later 1 (r-efi, an OR), no licence field 2 (truck-assembly, my harness). No GPL/AGPL. Dependencies that matter: ruststep 0.4.0 (Apache-2.0, last published 2024-09-20), spade 2.15.1 (MIT OR Apache-2.0), cgmath (via truck-base), rayon, vtkio, nom 3.2.1 and quick-xml 0.22.0 (cargo prints a "future-incompat" warning for both, job4.log). +- [V] MSRV: no `rust-version` in any Cargo.toml and no MSRV text anywhere (grep). Editions: 2021 for most crates, 2024 for truck-assembly and truck-drafting (so those need rustc >= 1.85). Builds on 1.95.0. +- [V] CI: GitLab (`.gitlab-ci.yml`) jobs `cpu-test` (docker image, `cargo make ci-cpu-test`), `cpu-nightly-test`, `meshalgo-features-buildcheck`, `gpu-test` (artifacts: images), `wasm-test` (deno lint + `cargo make wasm-test`), `page-build`, deploy of example pages. `Makefile.toml` cpu-test = `cargo test -p truck-base -p truck-geotrait -p truck-geometry -p truck-topology -p truck-polymesh -p truck-meshalgo -p truck-modeling -p truck-shapeops -p truck-stepio --features derive --features polynomial`. Tests that use sample data need the git submodule `resources` (github.com/ricosjp/resources4truck). +- [V] Surface/curve types. truck-geometry: Plane, Sphere, Torus (specifieds/), Circle, Line, Hyperbola, Parabola; BSplineCurve/Surface, NurbsCurve/Surface; decorators RevolutedCurve, ExtrudedCurve, Processor (matrix-transformed), PCurve, IntersectionCurve, TrimmedCurve, AfSurface, EdgeBlend, RbfSurface, Offset, homotopy. The ready-made enums in truck-modeling are narrower: `Surface` = {Plane, BSplineSurface, NurbsSurface, RevolutedCurve(Processor, Matrix4>)} (truck-modeling/src/geometry.rs:126-135); `Curve` = {Line, BSplineCurve, NurbsCurve, IntersectionCurve} (:24-32). So a cylinder, sphere or torus built with the modeling builder is a surface of revolution (Line or NURBS circle swept), not an analytic Cylinder/Sphere/Torus surface type. NURBS is first-class. +- [V] How booleans work (truck-shapeops/src/transversal/integrate/mod.rs; entry points `and(solid0, solid1, tol)` and `or(...)` returning `Option`; there is no subtract, the examples invert the second solid with `solid.not()` and call `and`): both shells are triangulated at `tol` (`shell.triangulation(tol)`); triangle-triangle intersections give polylines (`polyline_construction`); polylines are re-fitted onto the two true surfaces by Newton iteration (`IntersectionCurve`, `search_triple(..., 100)`, intersection_curve/mod.rs); faces are divided by the loops, classified and/or by crossing a ray against the other mesh; new edges are replaced by a quadratic B-spline approximation of the intersection curve within `tol` (`BSplineCurve::quadratic_approximation(ic, range, tol, 100)`, integrate/mod.rs:`altshell_to_shell`). So it is a general surface-surface path through tessellation, not a table of special cases. `tol` is both the tessellation tolerance and the intersection-curve approximation tolerance; truck-base `TOLERANCE` = 1e-6 (tolerance.rs:6) is the geometric coincidence tolerance (`near`). A non-positive tol hits `nonpositive_tolerance!`. Failure is `None` with no message; some failures are panics (Q3). +- [S] Documented limits (truck-shapeops/src/lib.rs:4-9): "Boolean operations are currently supported only for shapes where faces intersect transversally. Cases where faces are tangent to each other are not yet supported. ... performance optimization using BSP ... remains a future task." Fillet: "single edge whose end vertices are each adjacent to exactly three faces. Continuous edges are currently unsupported." truck-stepio lib.rs:1-6 states "Shapes created by set operations cannot be output yet" (contradicted by my measurement in Q4: 8 boolean results exported and read as valid by OCCT). +- [V] Also shipped in shapeops: `healing` (SplitClosedEdgesAndFaces, RobustSplitClosedEdgesAndFaces) and `fillet` (`simple_fillet`, `fillet_with_side`). + +## Q2 Build and own tests on Linux + +- [V] Job1 (job1.log), image rust 1.95.0, after `git submodule update --init resources` (fetched resources4truck at 9bf9de14): `cargo test --no-fail-fast -p truck-base -p truck-geotrait -p truck-geometry -p truck-topology -p truck-polymesh -p truck-meshalgo -p truck-modeling -p truck-shapeops -p truck-stepio -p truck-assembly -p truck-drafting -p truck-derivers --features derive --features polynomial` (debug profile): exit 0, 745 passed, 0 failed, 6 ignored (unit + integration + doc-tests). +- [V] Per crate (cargo prints crates alphabetically; the crate attribution below is inferred from that order and the test file names, passed counts unit+integration / doc-tests): assembly 2 / 39; base 28 / 55; derivers 0 / 0; drafting 20 / 25; geometry 82 / 139; geotrait 14 / 0; meshalgo 42 / 21; modeling 24 / 25; polymesh 13 / 15; shapeops 24 / 0 (21 unit + 3 in tests/fillet.rs); stepio 53 / 5 (3 unit + 39 tests/input + 3 tests/io + 8 tests/output); topology 7 / 112. +- [V] Not run: truck-platform, truck-rendimpl, truck-js tests (GPU/wasm CI jobs), nightly lane. `large-solid-torus` test is `#[ignore]`d (1 ignored in topology). +- [V] Compile times (4 CPUs, cold cache): my harness release build 1 m 06 s (job4.log); `truck-js` wasm release build 1 m 18 s (job5.log). +- [V] `cargo build --release --target wasm32-unknown-unknown -p truck-js` succeeds after `rustup target add wasm32-unknown-unknown`: `truck_js.wasm` 3,928,263 bytes (workspace sets opt-level "z" for truck-js). Not run in a browser. truck-js exports 12 builder fns, 20 shape, 5 polygon, 4 shapeops (and/or/not), 4 step_in wasm_bindgen items (grep counts). + +## Q3 Boolean torture (the Aetheris cases.json: 35 cases, same ops/geometry/expected volumes) + +Method: `harness\src\main.rs` builds primitives with truck-modeling (box = 3 tsweeps then scaled; cylinder = rsweep circle + tsweep; sphere = rsweep semicircle + `cone`; torus = two rsweeps), centred as in the Aetheris harness, rotates about X by rx, translates, flips any primitive whose tessellated volume is negative with `Solid::not()`. union = `or`, intersect = `and`, subtract = `and(a, not(b))`, sequential for multi-step cases. Each case is its own process with a 150 s timeout. "refused" = `None` (no message exists in the API). Release build. Tolerance `tol` is truck's only knob, so I ran three: 0.05, 0.01, 0.2 (model sizes are 10-100 units). Reference volumes: the cases' analytic `exp` plus OCCT booleans on identical primitives (`check.py ref`). + +Per-case outcome (S = succeeded, R = refused/None, P = panic; boolean time at tol 0.05 / 0.01 / 0.2 in ms; steps for case 21 in parentheses): + +| # | case | tol 0.05 | tol 0.01 | tol 0.2 | +|---|---|---|---|---| +| 01 | coplanar face union | R 2 | R 2 | R 2 | +| 02 | flush slab subtract | R 3 | R 2 | R 2 | +| 03 | coplanar top pocket | P 3 | P 4 | P 3 | +| 04 | coincident-edge union | R 2 | R 2 | R 2 | +| 05 | coincident-edge subtract | R 4 | R 2 | R 2 | +| 06 | tangent cylinders union | R 21 | R 34 | R 18 | +| 07/08/09 | Steinmetz intersect / union / subtract | R 23/30/26 | R 41/40/42 | R 16/16/18 | +| 10 | sliver wall 1e-7 | R 4 | R 2 | R 2 | +| 11 | pocket leaving 1e-7 floor | S 2 | S 2 | S 2 | +| 12 | pocket leaving 1e-4 floor | S 2 | S 2 | S 2 | +| 13 | spheres exactly tangent, union | S 17 | S 90 | S 13 | +| 14 | spheres 1e-9 apart/overlap, union | S 17 | S 90 | S 15 | +| 15 | spheres overlap 1e-4 | R 59 | R 152 | R 44 | +| 16 | spheres overlap (6 apart) | R 306 | R 1044 | R 159 | +| 17 | box minus sphere on top face | R 13 | R 65 | R 11 | +| 18 | torus half cut by box | P 1311 | P 2320 | P 757 | +| 19 | torus union cylinder | R 105 | R 163 | R 52 | +| 20 | torus minus cylinder (tangent) | R 96 | R 151 | R 51 | +| 21 | 10x10 hole grid (100 steps) | P at step 3 (2940 ms) | R at step 1 | R at step 2 | +| 22 | two tangent holes | R 743 | R 1114 | R 193 | +| 23 | hole tangent to side wall | R 26 | R 61 | R 19 | +| 24 | two holes 1e-7 web | P (step 2) 3667 | R (step 2) 1039 | P (step 2) 913 | +| 25 | perpendicular cylinders union | S 12-14 s (see below) | R 7799 | S 1587 | +| 26 | coaxial tube (cyl minus coaxial cyl) | S 3384 | R 8089 | S 333 | +| 27 | blind hole from top | P 144 | R 178 | R 49 | +| 28 | oblique 30 deg through hole | S 3706 | R 3096 | S 299 | +| 29 | box intersect sphere | R 1711 | R 7098 | R 837 | +| 30 | offset corner union | S 2 | S 2 | S 2 | +| 31 | pocket leaving 1e-7 side wall | R 2 | R 2 | R 2 | +| 32 | union overlap by 1e-7 slab | R 4 | R 2 | R 5 | +| 33 | partial face contact union | R 2 | R 2 | R 2 | +| 34 | sphere fully inside box (void) | P 11 | P 15 | P 10 | +| 35 | cylinder minus enclosed sphere | P 28 | P 52 | P 14 | + +- [V] Score per tolerance, 35 cases, succeeded / refused / panicked / timed out: tol 0.05: 8 / 20 / 7 / 0 (case 25 at 0.05: boolean returned S in ~12-14 s in 6 of 8 runs; in 2 of 8 runs the harness process hit the 150 s timeout later, see below); tol 0.01: 5 / 26 / 4 / 0; tol 0.2: 8 / 22 / 5 / 0. Union over the three tolerances: 8 distinct cases ever succeed (11, 12, 13, 14, 25, 26, 28, 30); 7 cases panic at least once (03, 18, 21, 24, 27, 34, 35); the other 20 are refused at every tol. Invalid STEP among the successes: 0 of 21 exported files (Q4). +- [V] Panic causes (message captured with a backtrace filter, job4.log): "This shell is not oriented and closed." at truck-topology/src/lib.rs:187 reached from `truck_shapeops::transversal::integrate::and` (frame 6) for cases 03, 18, 24, 27, 34, 35: the boolean assembled a shell that fails `Solid::new`'s closedness check. And `called Option::unwrap() on a None value` at truck-geometry/src/decorators/intersection_curve.rs:220:67 (`IntersectionCurve::subs` -> `search_triple(t,100).unwrap()`) for case 21 at step 3 (tol 0.05) and in tessellation of results from cases 25/26/28 (below). Cases 34 and 35 (a solid wholly inside the other: no intersection curves) panic at every tol. +- [V] Outcome depends on tol non-monotonically: case 25 succeeds at 0.5, 0.2, 0.1, 0.05 and is refused at 0.02, 0.01, 0.005; case 26 succeeds at 0.5, 0.2, 0.1, 0.05, 0.02 and is refused at 0.01 and 0.005; case 28 succeeds at 0.2 and 0.05, refused at 0.01 (job6.log sweep, job4.log). Case 27 is R at 0.5, P at 0.1/0.05/0.02, R at 0.2/0.01/0.005. Hole grid (21): never gets past 2 of 100 holes at any of 0.5, 0.2, 0.1, 0.05, 0.02, 0.01, 0.005 (P at 0.1 and 0.05, R otherwise). Coplanar union (01): R at 0.5, 0.1, 0.02, 0.005 as well. Box-minus-sphere dimple (17): R at 0.5, 0.1, 0.02, 0.005 as well. +- [V] Time per op (release, tol 0.05, boolean call only): planar box cases 2-4 ms; sphere/sphere 17 ms (13 at 0.2, 90 at 0.01); coaxial tube 3.4 s (0.33 s at 0.2, 4.2 s at 0.02); perpendicular cylinders 12-14 s (1.6 s at 0.2, 0.28 s at 0.5, 5.8 s at 0.1); oblique through hole 3.7 s (0.3 s at 0.2); box/sphere refusal 1.7 s; torus/box panic 1.3 s; 10x10 grid first two holes 2.9 s total. Cost grows as tol shrinks for the curved cases. +- [V] Determinism: three repeats at tol 0.05 of cases 11, 13, 26, 28, 30 gave identical face counts and mesh volumes to ~1e-12 (job5.log). Case 25 at 0.05: 2 of 8 runs ended at the 150 s timeout (job4.log rc=124, job5.log run 2) with the stderr of the job4 run holding ruststep "Lookup failed for #219 ..." lines (emitted when truck reads STEP); in 3 runs with stage timing (job6.log) boolean 13.3-14.6 s, meshes +1.0 s, STEP write, re-read +20 ms, no hang. Which stage hung in the 2 timeouts was not captured. +- [V] Against the Aetheris result on the same 35 cases (14 succeeded / 21 refused / 0 crashed): both succeed on 11, 12, 26, 28, 30. truck succeeds where Aetheris refused: 13, 14 (spheres), 25 (perpendicular cylinders, coarse tol only). Aetheris succeeded where truck refused or panicked at every tol: 01, 02, 03, 17, 21, 27, 32, 33, 34. Steinmetz (07-09), tangent cylinders (06), torus (18-20), 15, 16, 22-24, 29, 31, 35 fail in both. + +## Q4 STEP: export checked with OpenCASCADE; STEP import + +Method: truck-stepio `out::StepDisplay` of `solid.compress()`; checked with cadquery-ocp (pip, Debian 12, Python 3.11, installed in the container) `STEPControl_Reader` + `BRepCheck_Analyzer.IsValid` + `TopExp_Explorer` counts + `BRepGProp.VolumeProperties`; each file in its own subprocess (job4.log "== check truck output"; reference volumes from OCCT booleans on identical primitives, "== ref"). + +- [V] Files: 21 STEP files exported (cases 11, 12, 13, 14, 30 at 3 tolerances; 25, 26, 28 at 0.05 and 0.2 where they succeeded). OCCT read 21/21, `IsValid` True for 21/21, no crashes. Header `FILE_SCHEMA(('ISO-10303-203'))`. +- [V] Volumes (OCCT of truck's STEP vs OCCT boolean reference / analytic): 11: 840.0000016 vs 840.000001; 12: 840.0016 vs 840.0016; 13 and 14: 2 solids, 2 shells, 24 faces, 1047.19755 vs 1047.19755; 30: 1 solid, 12 faces, 1875.0 vs 1875.0; 25 (perpendicular cylinders): 10 faces, 1865.747 at tol 0.05 (-0.064% vs 1866.935), 1870.690 at 0.2 (+0.20%); 26 (tube): 6 faces, 502.303 at 0.05 (-0.07% vs 502.655), 504.539 at 0.2 (+0.38%); 28 (oblique hole): 8 faces, 3673.266 at 0.05 (-0.007% vs 3673.516), 3680.588 at 0.2 (+0.19%). So exact for planar/analytic-intersection results and tolerance-bound for results that need new intersection curves. +- [V] Entity census of the exports (job6.log): faces are `FACE_SURFACE` over `PLANE` or `SURFACE_OF_REVOLUTION` (spheres: 24 SURFACE_OF_REVOLUTION + AXIS1_PLACEMENT); new edges are `INTERSECTION_CURVE` + `B_SPLINE_CURVE_WITH_KNOTS` (case 11, a planar pocket with 1e-7 floor, still carries 4 INTERSECTION_CURVE + 4 B_SPLINE_CURVE_WITH_KNOTS; case 30 carries 6 of each). Solid is `MANIFOLD_SOLID_BREP`; case 13/14 (two disjoint shells in one Solid) is written as `BREP_WITH_VOIDS` + `ORIENTED_CLOSED_SHELL`, which OCCT reads as 2 solids and volume 1047.2 (a union of two spheres), so the second shell is not subtracted as a void. +- [V] Truck's STEP writer supports (truck-stepio/src/out/geometry.rs): PLANE (:505), SPHERICAL_SURFACE (:537), TOROIDAL_SURFACE (:575), B_SPLINE_SURFACE_WITH_KNOTS (:621), rational B-spline (:677-680), SURFACE_OF_REVOLUTION (:741); no CYLINDRICAL_SURFACE writer line was found by that grep (cylinders from the builder are SURFACE_OF_REVOLUTION). +- [V] Truck reads STEP: yes (`truck_stepio::r#in`, `Table::from_step`, `step_assy`, `to_compressed_shell`; tests read OCCT-written cube, cone, cylinder, sphere, torus and four `abc-*` files from the `resources` submodule, tests/input/tessellate_shape.rs:5-15; input handles CylindricalSurface, SphericalSurface, ToroidalSurface, BSpline, assemblies). My probe: I had OCCT write all 35 reference results (`ref_NN.step`) and read each with truck (`Table::from_step`, every shell `to_compressed_shell`, `triangulation(0.01)`, `put_together_same_attrs`, volume; job4.log "== truck reads OCCT-written STEP"): no panic on any of 35 files; for 16 of them the tessellated shells are all `Closed` with summed volume within 2% of OCCT's (01-05, 11, 12, 13, 17, 21, 26, 27, 30-33); for the rest the shell is Irregular/Regular/Oriented, or 0 triangles, or a wrong volume (06: 783+431 vs 1570.8; 07: 9.6 vs 666.7; 08: 1937 vs 2475; 09: 2 x 481.9 vs 904.1; 14, 16: 0 triangles; 25: 1370 vs 1867; 28: 3892.8 vs 3673.5); cases 18-20, 22-24 and 29 read with volumes within 0.4% but shells not Closed. Reading ref_21 (the 100-hole plate) took 25.8 s in release. These are reads of OCCT trimmed surfaces and intersection-curve edges, tessellated at the repo-test pattern; I did not separate reader faults from tessellation faults. +- [V] Truck re-reading its own exported booleans (same probe, `step_reread` in job4.log): most results fail to come back as a closed mesh (11 and 12: shells "Oriented" with 2-10 triangles and volumes 0 / 750; 13 and 14: Irregular, negative volumes -1563.8 and -1648.7; 30 at 0.01: Oriented, 4 triangles, volume 10; 30 at 0.05 and 0.2: Closed with 0 triangles). Results vary between tolerance runs for the same geometry (case 30: 0 / 4 / 0 triangles at 0.05 / 0.01 / 0.2). + +## Q5 Tessellation (truck-meshalgo) of boolean results + +Method: `solid.triangulation(tol).to_polygon()` then `put_together_same_attrs(TOLERANCE*50).remove_degenerate_faces()`; `shell_condition()` and `volume()` from meshalgo; plus an independent check of the written OBJ (`check.py obj`: weld at 1e-6, every directed edge must pair with its reverse; signed volume). + +- [V] Planar results (cases 11, 12, 30): 28 / 28 / 36 triangles, identical at tol 0.1, 0.01, 0.001, 0.5-1 ms, Closed, independent OBJ check watertight and oriented, volumes exact (840.0000016, 840.0016, 1875.0). +- [V] Spheres (cases 13/14, two spheres r=5): tol 0.1: 992 triangles, 6 ms, Closed, volume 1015.59; tol 0.01: 12,656-12,684 triangles, 111-153 ms, volume 1044.54 (analytic 1047.20); tol 0.001: 78,928 triangles, 1.1-1.8 s, Closed, volume 1046.76. Case 14 is watertight by the independent check at 0.01; case 13 at 0.01 is `Regular` by truck and by the independent check has 9 unmatched directed edges and 9 multi-used edges. +- [V] Results with new intersection curves: case 26 (tube, boolean tol 0.05): mesh tol 0.1: 1,102 triangles, 33-39 ms, Closed, volume 500.06; tol 0.01: 8,219 triangles, 186-214 ms, `Irregular` (independent: 17 unmatched directed edges, 6 multi-used edges), volume 501.79; tol 0.001: panic (intersection_curve.rs:220 unwrap). Case 25 (boolean tol 0.2): tol 0.1: 1,218 triangles, 57 ms Closed; tol 0.01: 11,172 triangles, 272 ms `Regular` (independent: 7 unmatched, 7 multi); tol 0.001 panic. Case 28 (oblique hole, boolean tol 0.05): tol 0.1: 376 triangles, 25-30 ms `Irregular`; tol 0.01 and 0.001: panic; the same case at boolean tol 0.2: tol 0.1 gives 318 triangles `Irregular`, 0.01 and 0.001 panic. Case 26 at boolean tol 0.2 panics at all three mesh tolerances. +- [V] Tessellating the bare primitives (tol 0.01): box 1000.0 exact; cylinder r5 h10 783.98 (analytic 785.40, -0.18%); sphere r5 522.28 (523.60, -0.25%); torus 10/3 1771.57 (1776.53, -0.28%) (job2.log prims). +- [V] The docs say the mesh of a Solid is not necessarily closed and recommend `put_together_same_attrs` (tessellation/mod.rs docs on `MeshableShape::triangulation`); `RobustMeshableShape` "does not close the mesh of a solid". +- [V] Output API: `PolygonMesh` (positions/normals/uv + tri/quad/other faces), OBJ and STL writers in truck-polymesh, VTK in meshalgo. Triangles counted with `faces().triangle_iter()`. + +## Q6 Other things a consumer gets + +- [V] WASM: `truck-js` (wasm-bindgen) builds for wasm32-unknown-unknown, 3.9 MB; meshalgo has a `cfg(target_arch = "wasm32")` path without rayon; CI runs wasm tests with deno (`.gitlab-ci.yml` job wasm-test). Python bindings: none found (grep pyo3/python in manifests and READMEs). C ABI: none found. +- [V] Rendering: `truck-platform` (graphic utility on wgpu) and `truck-rendimpl` (shape and polygon-mesh visualisation) with examples app.rs, simple-shape-viewer.rs, simple-obj-viewer.rs, textured-cube.rs, rotate-objects.rs, bsp-animation.rs; CI has a `gpu-test` job. Not run here (no GPU in the container). +- [V] Modelling API (truck-modeling/examples/cylinder.rs, whole body of the cylinder): + ```rust + let vertex = builder::vertex(Point3::new(0.0, -height / 2.0, radius)); + let circle = builder::rsweep(&vertex, Point3::origin(), Vector3::unit_y(), Rad(7.0), 2); + let disk = builder::try_attach_plane(&[circle]).unwrap(); + builder::tsweep(&disk, Vector3::new(0.0, height, 0.0)) + ``` + Box = `vertex` -> `tsweep` x/y/z. Punched cube (truck-shapeops/examples/punched-cube-shapeops.rs): build `cylinder` the same way, `cylinder.not();` then `truck_shapeops::and(&cube, &cylinder, 0.05).unwrap()`. Other builder fns: `line`, `circle_arc`, `bezier`, `homotopy`, `try_wire_homotopy`, `try_attach_plane`, `translated`/`rotated`/`scaled`, `rsweep`, `cone`. The examples pass `Rad(7.0)` (a little over 2 pi) for a full revolution. +- [V] Fillet: not reachable from `truck_modeling::Surface` (no fillet variant). The fillet test (truck-shapeops/tests/fillet.rs:1-76) defines its own `Curve` and `Surface` enums with seven derive macros and `ToSameGeometry` impls, builds two NURBS faces by hand, and calls `simple_fillet(&face0, &face1, shared_edge_id, 0.3, 0.001)`; 3 such tests pass (job1.log). Limits per docs: a single edge whose end vertices each touch exactly three faces. I did not build "filleted box with a hole": it would need that custom enum pair plus a boolean on the result, and the measured boolean cannot subtract a blind hole from a box with a coplanar top (cases 03 and 27 panic). +- [V] Serialisation: `Solid`/`Shell` are serde types (examples write `serde_json::to_vec_pretty(&solid)`); `CompressedSolid` for compact form (`solid.compress()`). +- [V] Parallelism: rayon in meshalgo (non-wasm) and `Send + Sync` bounds on `ShapeOpsSurface`/`ShapeOpsCurve`. +- [V] Panics are part of the surface: `Solid::new`, `builder` calls and tessellation panic on bad input (Q3), and there is no `Result` with a reason on `and`/`or`. diff --git a/docs/research/truck-evaluation/harness/.gitignore b/docs/research/truck-evaluation/harness/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/docs/research/truck-evaluation/harness/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/docs/research/truck-evaluation/harness/Cargo.toml b/docs/research/truck-evaluation/harness/Cargo.toml new file mode 100644 index 0000000..f007e21 --- /dev/null +++ b/docs/research/truck-evaluation/harness/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "trucktorture" +version = "0.0.0" +edition = "2021" + +[dependencies] +truck-modeling = { git = "https://github.com/ricosjp/truck", rev = "88ed005249e5e3a6b07f62425399435905cd3ab6" } +truck-shapeops = { git = "https://github.com/ricosjp/truck", rev = "88ed005249e5e3a6b07f62425399435905cd3ab6" } +truck-meshalgo = { git = "https://github.com/ricosjp/truck", rev = "88ed005249e5e3a6b07f62425399435905cd3ab6" } +truck-stepio = { git = "https://github.com/ricosjp/truck", rev = "88ed005249e5e3a6b07f62425399435905cd3ab6" } +serde_json = "1" diff --git a/docs/research/truck-evaluation/harness/cases.json b/docs/research/truck-evaluation/harness/cases.json new file mode 100644 index 0000000..84c0f4a --- /dev/null +++ b/docs/research/truck-evaluation/harness/cases.json @@ -0,0 +1,2698 @@ +[ +{ +"name": "01_coplanar_face_union", +"op": "union", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +10, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 2000, +"note": "" +}, +{ +"name": "02_coplanar_flush_slab_subtract", +"op": "subtract", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +10, +10, +5 +], +"p": [ +0, +0, +2.5 +], +"rx": 0, +"ry": 0 +} +], +"exp": 500, +"note": "" +}, +{ +"name": "03_coplanar_top_pocket", +"op": "subtract", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +4, +4, +5 +], +"p": [ +0, +0, +2.5 +], +"rx": 0, +"ry": 0 +} +], +"exp": 920, +"note": "" +}, +{ +"name": "04_coincident_edge_union", +"op": "union", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +10, +10, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 2000, +"note": "edge-only contact" +}, +{ +"name": "05_coincident_edge_subtract", +"op": "subtract", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +10, +10, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 1000, +"note": "edge-only contact" +}, +{ +"name": "06_tangent_cylinders_union", +"op": "union", +"a": { +"k": "cyl", +"d": [ +5, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +5, +10 +], +"p": [ +10, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 1570.7963267948967, +"note": "" +}, +{ +"name": "07_steinmetz_intersect", +"op": "intersect", +"a": { +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +0, +0, +0 +], +"rx": 90, +"ry": 0 +} +], +"exp": 666.6666666666666, +"note": "" +}, +{ +"name": "08_steinmetz_union", +"op": "union", +"a": { +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +0, +0, +0 +], +"rx": 90, +"ry": 0 +} +], +"exp": 2474.925986923127, +"note": "" +}, +{ +"name": "09_steinmetz_subtract", +"op": "subtract", +"a": { +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +0, +0, +0 +], +"rx": 90, +"ry": 0 +} +], +"exp": 904.1296601282301, +"note": "" +}, +{ +"name": "10_sliver_wall_1e-7", +"op": "subtract", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +9.9999998, +20, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 2e-05, +"note": "two walls 1e-7 thick" +}, +{ +"name": "11_sliver_floor_1e-7", +"op": "subtract", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +4, +4, +20 +], +"p": [ +0, +0, +5.0000001 +], +"rx": 0, +"ry": 0 +} +], +"exp": 840.0000016, +"note": "floor 1e-7" +}, +{ +"name": "12_thin_floor_1e-4", +"op": "subtract", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +4, +4, +20 +], +"p": [ +0, +0, +5.0001 +], +"rx": 0, +"ry": 0 +} +], +"exp": 840.0016, +"note": "floor 1e-4" +}, +{ +"name": "13_spheres_tangent_union", +"op": "union", +"a": { +"k": "sph", +"d": [ +5 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +5 +], +"p": [ +10, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 1047.1975511965977, +"note": "exactly tangent" +}, +{ +"name": "14_spheres_neartangent_1e-9", +"op": "union", +"a": { +"k": "sph", +"d": [ +5 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +5 +], +"p": [ +9.999999999, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "gap-overlap 1e-9" +}, +{ +"name": "15_spheres_neartangent_1e-4", +"op": "union", +"a": { +"k": "sph", +"d": [ +5 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +5 +], +"p": [ +9.9999, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "overlap 1e-4" +}, +{ +"name": "16_spheres_overlap_union", +"op": "union", +"a": { +"k": "sph", +"d": [ +5 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +5 +], +"p": [ +6, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 938.2890058721515, +"note": "" +}, +{ +"name": "17_sphere_in_box_dimple", +"op": "subtract", +"a": { +"k": "box", +"d": [ +20, +20, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +5 +], +"p": [ +0, +0, +10 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "sphere centred on top face" +}, +{ +"name": "18_torus_half_cut", +"op": "subtract", +"a": { +"k": "tor", +"d": [ +10, +3 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +40, +40, +10 +], +"p": [ +0, +0, +-5 +], +"rx": 0, +"ry": 0 +} +], +"exp": 888.2643960980423, +"note": "" +}, +{ +"name": "19_torus_union_cyl_through_hole", +"op": "union", +"a": { +"k": "tor", +"d": [ +10, +3 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +7, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "" +}, +{ +"name": "20_torus_minus_cyl_inner_tangent", +"op": "subtract", +"a": { +"k": "tor", +"d": [ +10, +3 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +7, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "cyl r=7 tangent to torus inner equator" +}, +{ +"name": "21_hole_grid_10x10", +"op": "subtract", +"a": { +"k": "box", +"d": [ +100, +100, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-45, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-35, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-25, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-15, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +-5, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +5, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +15, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +25, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +35, +45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +-45, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +-35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +-25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +-15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +-5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +5, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +15, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +25, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +35, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +2, +20 +], +"p": [ +45, +45, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 87433.62938564083, +"note": "" +}, +{ +"name": "22_hole_tangent_pair", +"op": "subtract", +"a": { +"k": "box", +"d": [ +30, +30, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +-5, +0, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +5, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "two holes tangent to each other" +}, +{ +"name": "23_hole_tangent_to_side", +"op": "subtract", +"a": { +"k": "box", +"d": [ +30, +30, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +10, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "hole tangent to side wall" +}, +{ +"name": "24_hole_thin_web_1e-7", +"op": "subtract", +"a": { +"k": "box", +"d": [ +30, +30, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +-5, +0, +0 +], +"rx": 0, +"ry": 0 +}, +{ +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +5.0000001, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "web between holes 1e-7" +}, +{ +"name": "25_cyl_perp_T_union", +"op": "union", +"a": { +"k": "cyl", +"d": [ +5, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +3, +20 +], +"p": [ +0, +0, +0 +], +"rx": 90, +"ry": 0 +} +], +"exp": null, +"note": "perpendicular smaller cylinder through" +}, +{ +"name": "26_coaxial_tube", +"op": "subtract", +"a": { +"k": "cyl", +"d": [ +5, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +3, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 502.6548245743669, +"note": "" +}, +{ +"name": "27_box_minus_cyl_blind", +"op": "subtract", +"a": { +"k": "box", +"d": [ +20, +20, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +3, +5 +], +"p": [ +0, +0, +2.5 +], +"rx": 0, +"ry": 0 +} +], +"exp": 3858.6283305884594, +"note": "blind hole from top" +}, +{ +"name": "28_box_minus_cyl_oblique", +"op": "subtract", +"a": { +"k": "box", +"d": [ +20, +20, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "cyl", +"d": [ +3, +30 +], +"p": [ +0, +0, +0 +], +"rx": 30, +"ry": 0 +} +], +"exp": null, +"note": "oblique through-hole" +}, +{ +"name": "29_box_intersect_sphere", +"op": "intersect", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +6 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "sphere bigger than box inscribed" +}, +{ +"name": "30_box_union_box_overlap_offset", +"op": "union", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +5, +5, +5 +], +"rx": 0, +"ry": 0 +} +], +"exp": 1875, +"note": "" +}, +{ +"name": "31_sliver_side_wall_pocket_1e-7", +"op": "subtract", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +4.9999999, +4, +10 +], +"p": [ +-2.49999995, +0, +5 +], +"rx": 0, +"ry": 0 +} +], +"exp": 900.000002, +"note": "blind pocket leaving 1e-7 side wall" +}, +{ +"name": "32_union_overlap_slab_1e-7", +"op": "union", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +9.9999999, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 1999.99999, +"note": "boxes overlap by a 1e-7 slab" +}, +{ +"name": "33_union_partial_face_contact", +"op": "union", +"a": { +"k": "box", +"d": [ +10, +10, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "box", +"d": [ +10, +5, +10 +], +"p": [ +10, +2.5, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 1500, +"note": "face contact, B face inside A face" +}, +{ +"name": "34_sphere_inside_box_void", +"op": "subtract", +"a": { +"k": "box", +"d": [ +20, +20, +20 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +3 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": 7886.902664470767, +"note": "sphere fully enclosed" +}, +{ +"name": "35_cyl_minus_sphere", +"op": "subtract", +"a": { +"k": "cyl", +"d": [ +5, +10 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +}, +"bs": [ +{ +"k": "sph", +"d": [ +3 +], +"p": [ +0, +0, +0 +], +"rx": 0, +"ry": 0 +} +], +"exp": null, +"note": "sphere enclosed in cylinder" +} +] \ No newline at end of file diff --git a/docs/research/truck-evaluation/harness/check.py b/docs/research/truck-evaluation/harness/check.py new file mode 100644 index 0000000..d0812a7 --- /dev/null +++ b/docs/research/truck-evaluation/harness/check.py @@ -0,0 +1,151 @@ +"""Independent checks of truck output with OpenCASCADE (cadquery-ocp) and plain-python OBJ analysis. +modes: + ref OCCT reference boolean per case: volume + writes ref_NN.step + one BRepCheck / counts / volume of one STEP (own process; OCCT may segfault) + obj watertightness and volume of an OBJ mesh + all run `one` per *.step and `obj` per *.obj in subprocesses +""" +import sys, json, subprocess, math, os, glob + + +def vol_of(shape): + from OCP.GProp import GProp_GProps + from OCP.BRepGProp import BRepGProp + p = GProp_GProps() + BRepGProp.VolumeProperties_s(shape, p) + return p.Mass() + + +def count(shape, kind): + from OCP.TopExp import TopExp_Explorer + e = TopExp_Explorer(shape, kind) + n = 0 + while e.More(): + n += 1 + e.Next() + return n + + +def one(path): + from OCP.STEPControl import STEPControl_Reader + from OCP.IFSelect import IFSelect_RetDone + from OCP.BRepCheck import BRepCheck_Analyzer + from OCP.TopAbs import TopAbs_SOLID, TopAbs_SHELL, TopAbs_FACE + r = STEPControl_Reader() + st = r.ReadFile(path) + out = {"file": os.path.basename(path), "read_ok": st == IFSelect_RetDone} + if st != IFSelect_RetDone: + print("RES " + json.dumps(out)); return + nroots = r.TransferRoots() + s = r.OneShape() + out.update({"roots": nroots, "null": s.IsNull()}) + if not s.IsNull(): + out.update({"valid": BRepCheck_Analyzer(s).IsValid(), "solids": count(s, TopAbs_SOLID), + "shells": count(s, TopAbs_SHELL), "faces": count(s, TopAbs_FACE), "volume": vol_of(s)}) + print("RES " + json.dumps(out)) + + +def objcheck(path): + pts, tris = [], [] + for line in open(path): + t = line.split() + if not t: + continue + if t[0] == "v": + pts.append((float(t[1]), float(t[2]), float(t[3]))) + elif t[0] == "f": + idx = [int(x.split("/")[0]) - 1 for x in t[1:]] + for i in range(1, len(idx) - 1): + tris.append((idx[0], idx[i], idx[i + 1])) + key = {} + remap = [] + for p in pts: + k = (round(p[0] / 1e-6), round(p[1] / 1e-6), round(p[2] / 1e-6)) + remap.append(key.setdefault(k, len(key))) + edges = {} + vol = 0.0 + degenerate = 0 + for a, b, c in tris: + pa, pb, pc = pts[a], pts[b], pts[c] + vol += (pa[0] * (pb[1] * pc[2] - pb[2] * pc[1]) - pa[1] * (pb[0] * pc[2] - pb[2] * pc[0]) + pa[2] * (pb[0] * pc[1] - pb[1] * pc[0])) / 6.0 + ra, rb, rc = remap[a], remap[b], remap[c] + if len({ra, rb, rc}) < 3: + degenerate += 1 + continue + for u, v in ((ra, rb), (rb, rc), (rc, ra)): + edges[(u, v)] = edges.get((u, v), 0) + 1 + unmatched = sum(1 for (u, v), n in edges.items() if edges.get((v, u), 0) != n) + multi = sum(1 for n in edges.values() if n > 1) + print("RES " + json.dumps({"file": os.path.basename(path), "tris": len(tris), "welded_vertices": len(key), + "degenerate_after_weld": degenerate, "unmatched_directed_edges": unmatched, + "multi_edges": multi, "watertight_oriented": unmatched == 0 and multi == 0, + "volume": vol})) + + +def build_ref(cases_path, outdir): + from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder, BRepPrimAPI_MakeSphere, BRepPrimAPI_MakeTorus + from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse, BRepAlgoAPI_Cut, BRepAlgoAPI_Common + from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform + from OCP.gp import gp_Trsf, gp_Ax1, gp_Ax2, gp_Pnt, gp_Dir, gp_Vec + from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs + from OCP.BRepCheck import BRepCheck_Analyzer + cases = json.load(open(cases_path)) + o = gp_Pnt(0, 0, 0) + z = gp_Dir(0, 0, 1) + + def prim(e): + d, k = e["d"], e["k"] + if k == "box": + s = BRepPrimAPI_MakeBox(gp_Pnt(-d[0] / 2, -d[1] / 2, -d[2] / 2), d[0], d[1], d[2]).Shape() + elif k == "cyl": + s = BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, -d[1] / 2), z), d[0], d[1]).Shape() + elif k == "sph": + s = BRepPrimAPI_MakeSphere(o, d[0]).Shape() + else: + s = BRepPrimAPI_MakeTorus(gp_Ax2(o, z), d[0], d[1]).Shape() + for axis, key in ((gp_Dir(1, 0, 0), "rx"), (gp_Dir(0, 1, 0), "ry")): + if e[key]: + t = gp_Trsf(); t.SetRotation(gp_Ax1(o, axis), math.radians(e[key])) + s = BRepBuilderAPI_Transform(s, t, True).Shape() + t = gp_Trsf(); t.SetTranslation(gp_Vec(*e["p"])) + return BRepBuilderAPI_Transform(s, t, True).Shape() + + for i, c in enumerate(cases): + rec = {"case": c["name"], "idx": i, "exp": c["exp"]} + try: + cur = prim(c["a"]) + for b in c["bs"]: + bb = prim(b) + alg = {"union": BRepAlgoAPI_Fuse, "subtract": BRepAlgoAPI_Cut, "intersect": BRepAlgoAPI_Common}[c["op"]](cur, bb) + cur = alg.Shape() + rec["occt_volume"] = vol_of(cur) + rec["occt_valid"] = BRepCheck_Analyzer(cur).IsValid() + w = STEPControl_Writer(); w.Transfer(cur, STEPControl_AsIs) + w.Write(os.path.join(outdir, "ref_%02d.step" % (i + 1))) + except Exception as ex: + rec["error"] = str(ex)[:200] + print("RES " + json.dumps(rec)) + + +if __name__ == "__main__": + m = sys.argv[1] + if m == "one": + one(sys.argv[2]) + elif m == "obj": + objcheck(sys.argv[2]) + elif m == "ref": + build_ref(sys.argv[2], sys.argv[3]) + elif m == "all": + for f in sorted(glob.glob(sys.argv[2] + "/*.step")) + sorted(glob.glob(sys.argv[2] + "/*.obj")): + if os.path.basename(f).startswith("ref_"): + continue + mode = "one" if f.endswith(".step") else "obj" + try: + r = subprocess.run([sys.executable, "-I", __file__, mode, f], capture_output=True, text=True, timeout=180) + lines = [l for l in r.stdout.splitlines() if l.startswith("RES ")] + if r.returncode != 0 or not lines: + print("RES " + json.dumps({"file": os.path.basename(f), "rc": r.returncode, "stderr_tail": r.stderr[-300:]})) + else: + print(lines[0]) + except subprocess.TimeoutExpired: + print("RES " + json.dumps({"file": os.path.basename(f), "timeout": True})) diff --git a/docs/research/truck-evaluation/harness/job.sh b/docs/research/truck-evaluation/harness/job.sh new file mode 100644 index 0000000..3a1c3c9 --- /dev/null +++ b/docs/research/truck-evaluation/harness/job.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# usage: job.sh (runs inside the stopgap rust container, cwd = repo root) +TOLS="${*:-0.05}" +set -u +OUT=/tmp/out; mkdir -p $OUT +echo "== build" +cargo build --release 2>&1 | tail -30 +BIN=target/release/trucktorture +[ -x $BIN ] || { echo BUILD-FAILED; exit 1; } +echo "== prims" +$BIN prims 2>&1 | tail -8 +N=$(grep -c '"name"' cases.json) +echo "cases=$N" +for TOL in $TOLS; do +echo "== cases tol=$TOL" +for i in $(seq 0 $((N-1))); do + s=$(date +%s.%N) + timeout ${CASE_TIMEOUT:-150} $BIN run cases.json $i $TOL $OUT > /tmp/case_$i.out 2> /tmp/case_$i.err + rc=$? + e=$(date +%s.%N) + echo "CASE idx=$i rc=$rc wall=$(echo "$e - $s" | bc 2>/dev/null || echo ?)" + grep -h '^RES' /tmp/case_$i.out | cut -c1-1500 + if [ $rc -ne 0 ]; then echo "STDERR-TAIL idx=$i: $(tail -c 400 /tmp/case_$i.err | tr '\n' ' ')"; fi +done +done +echo "== python/ocp" +apt-get update -qq 2>&1 | tail -2 +apt-get install -y -qq python3 python3-pip libgl1 libglu1-mesa libxrender1 2>&1 | tail -3 +PY=python3 +$PY -m pip install -q --break-system-packages cadquery-ocp 2>&1 | tail -3 +$PY -c "import OCP; print('OCP ok')" 2>&1 | tail -2 +echo "== ref (OCCT reference booleans)" +$PY check.py ref cases.json $OUT 2>&1 | cut -c1-400 +echo "== check truck output (STEP via OCCT, OBJ independent)" +$PY check.py all $OUT 2>&1 | cut -c1-500 +echo "== truck reads OCCT-written STEP" +for f in $OUT/ref_*.step; do timeout 120 $BIN readstep $f 2>&1 | grep -h '^RES' | cut -c1-500 || echo "READSTEP-FAIL $f"; done +echo "== done" diff --git a/docs/research/truck-evaluation/harness/job5.sh b/docs/research/truck-evaluation/harness/job5.sh new file mode 100644 index 0000000..a3f965c --- /dev/null +++ b/docs/research/truck-evaluation/harness/job5.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# second probe batch: STEP entity census, determinism, tolerance sweep, licences, wasm +set -u +echo "== build"; cargo build --release 2>&1 | tail -2 +BIN=target/release/trucktorture +OUT=/tmp/out5; mkdir -p $OUT +apt-get update -qq 2>&1 | tail -1; apt-get install -y -qq python3 2>&1 | tail -1 +echo "== step census" +for spec in "25 0.2" "12 0.05" "29 0.05" "27 0.2"; do set -- $spec + timeout 150 $BIN run cases.json $1 $2 $OUT > /tmp/o.txt 2>/dev/null + f=$(ls $OUT/$(printf %02d $(($1+1)))_t$2.step 2>/dev/null) + echo "-- case idx=$1 tol=$2 file=$f" + [ -n "$f" ] && { grep -ho '^#[0-9]*= *[A-Z_0-9]*' $f | sed 's/#[0-9]*= *//' | sort | uniq -c | sort -rn | head -22 | tr '\n' ';'; echo; grep -m3 -n 'FILE_SCHEMA\|FILE_NAME\|MANIFOLD_SOLID_BREP\|CLOSED_SHELL' $f | cut -c1-120; } +done +echo "== determinism (3 runs each, tol 0.05)" +for idx in 10 12 25 27 29; do for r in 1 2 3; do + timeout 150 $BIN run cases.json $idx 0.05 $OUT 2>/dev/null | grep '^RES' | grep -o '"status":"[a-z_]*"\|"faces":[0-9]*\|"vol":[-0-9.e]*' | head -4 | tr '\n' ' '; echo " idx=$idx run=$r" +done; done +echo "== tolerance sweep" +for idx in 0 20 24 25 26 16; do for t in 0.5 0.1 0.02 0.005; do + /usr/bin/time -f "wall=%es rss=%MKB" timeout 200 $BIN run cases.json $idx $t $OUT > /tmp/o.txt 2> /tmp/e.txt + echo "idx=$idx tol=$t rc=$? $(grep '^RES' /tmp/o.txt | grep -o '"status":"[a-z_]*"\|"steps_done":[0-9]*\|"boolean_ms_total":[0-9.]*' | tr '\n' ' ') $(tail -1 /tmp/e.txt)" +done; done +echo "== licences (transitive deps of harness)" +cargo metadata --format-version 1 > /tmp/md.json 2>/dev/null +python3 -c " +import json,collections +d=json.load(open('/tmp/md.json'));c=collections.Counter();odd=[] +for p in d['packages']: + c[p.get('license') or 'NONE']+=1 + if not p.get('license') or 'GPL' in (p.get('license') or '') or 'MPL' in (p.get('license') or ''): odd.append((p['name'],p.get('license'))) +print('n_packages',len(d['packages']));print(c.most_common(12));print('odd',odd) +" +echo "== wasm build of truck-js" +rustup target add wasm32-unknown-unknown 2>&1 | tail -1 +D=$(ls -d /usr/local/cargo/git/checkouts/truck-*/88ed005 | head -1); echo "checkout=$D" +cd $D && (time cargo build --release --target wasm32-unknown-unknown -p truck-js 2>&1 | tail -4) +ls -la target/wasm32-unknown-unknown/release/*.wasm 2>&1 | head -3 +cd $D && grep -c "wasm_bindgen" truck-js/src/*.rs | tr '\n' ' ' +echo "== done" diff --git a/docs/research/truck-evaluation/harness/job6.sh b/docs/research/truck-evaluation/harness/job6.sh new file mode 100644 index 0000000..f16f875 --- /dev/null +++ b/docs/research/truck-evaluation/harness/job6.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# third probe batch: STEP entity census, case-25 stage timing, tolerance sweep +set -u +echo "== build"; cargo build --release 2>&1 | tail -2 +BIN=target/release/trucktorture +OUT=/tmp/out6; mkdir -p $OUT +echo "== step census" +for spec in "25 0.2" "12 0.05" "29 0.05" "27 0.2" "10 0.05"; do + set -- $spec + timeout 150 $BIN run cases.json $1 $2 $OUT > /tmp/o.txt 2>/dev/null + f=$(ls $OUT/$(printf %02d $(($1+1)))_t$2.step 2>/dev/null) + echo "-- case idx=$1 tol=$2 file=$f" + if [ -n "$f" ]; then + grep -ho '^#[0-9]* *= *[A-Z_0-9]*' $f | sed 's/#[0-9]* *= *//' | sort | uniq -c | sort -rn | head -24 | tr '\n' ';' + echo + fi +done +echo "== case 25 stage timing x3" +for r in 1 2 3; do + timeout 200 $BIN run cases.json 24 0.05 $OUT 2>&1 >/dev/null | grep STAGE | tr '\n' ' ' + echo " run=$r" +done +echo "== tolerance sweep" +for idx in 0 20 24 25 26 16; do + for t in 0.5 0.1 0.02 0.005; do + s0=$(date +%s%N) + timeout 200 $BIN run cases.json $idx $t $OUT > /tmp/o.txt 2> /tmp/e.txt + rc=$? + echo "idx=$idx tol=$t rc=$rc wall=$(( ($(date +%s%N)-s0)/1000000 ))ms $(grep '^RES' /tmp/o.txt | grep -o '"status":"[a-z_]*"\|"steps_done":[0-9]*\|"boolean_ms_total":[0-9.]*' | tr '\n' ' ')" + done +done +echo "== done" diff --git a/docs/research/truck-evaluation/harness/src/main.rs b/docs/research/truck-evaluation/harness/src/main.rs new file mode 100644 index 0000000..252ad1c --- /dev/null +++ b/docs/research/truck-evaluation/harness/src/main.rs @@ -0,0 +1,263 @@ +// Torture harness for truck (ricosjp/truck), mirrors the Aetheris harness cases (cases.json). +use serde_json::{json, Value}; +use std::f64::consts::PI; +use std::io::BufWriter; +use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::sync::Mutex; +use std::time::Instant; +use truck_meshalgo::prelude::*; +use truck_modeling::*; +use truck_stepio::out; + +static PANIC: Mutex = Mutex::new(String::new()); + +fn take_panic() -> String { + let mut g = PANIC.lock().unwrap(); + let s = g.clone(); + g.clear(); + s.chars().take(300).collect() +} + +fn box_(d: &[f64]) -> Solid { + let v = builder::vertex(Point3::origin()); + let e = builder::tsweep(&v, Vector3::unit_x()); + let f = builder::tsweep(&e, Vector3::unit_y()); + let c: Solid = builder::tsweep(&f, Vector3::unit_z()); + let c = builder::scaled(&c, Point3::origin(), Vector3::new(d[0], d[1], d[2])); + builder::translated(&c, Vector3::new(-d[0] / 2.0, -d[1] / 2.0, -d[2] / 2.0)) +} + +fn cyl(r: f64, h: f64) -> Solid { + let v = builder::vertex(Point3::new(r, 0.0, -h / 2.0)); + let circle = builder::rsweep(&v, Point3::new(0.0, 0.0, -h / 2.0), Vector3::unit_z(), Rad(7.0), 2); + let disk = builder::try_attach_plane(&[circle]).unwrap(); + builder::tsweep(&disk, Vector3::new(0.0, 0.0, h)) +} + +fn sph(r: f64) -> Solid { + let v0 = builder::vertex(Point3::new(0.0, r, 0.0)); + let wire: Wire = builder::rsweep(&v0, Point3::origin(), Vector3::unit_x(), Rad(PI), 3); + let shell = builder::cone(&wire, Vector3::unit_y(), Rad(7.0), 4); + Solid::new(vec![shell]) +} + +fn tor(big: f64, small: f64) -> Solid { + let v = builder::vertex(Point3::new(big, 0.0, small)); + let w = builder::rsweep(&v, Point3::new(big, 0.0, 0.0), Vector3::unit_y(), Rad(7.0), 2); + let shell = builder::rsweep(&w, Point3::origin(), Vector3::unit_z(), Rad(7.0), 2); + Solid::new(vec![shell]) +} + +fn mesh_of(s: &Solid, tol: f64) -> PolygonMesh { + let mut m = s.triangulation(tol).to_polygon(); + m.put_together_same_attrs(TOLERANCE * 50.0).remove_degenerate_faces(); + m +} + +fn orient(mut s: Solid) -> Solid { + if mesh_of(&s, 0.05).volume() < 0.0 { + s.not(); + } + s +} + +fn nums(v: &Value) -> Vec { + v.as_array().unwrap().iter().map(|x| x.as_f64().unwrap()).collect() +} + +fn prim(e: &Value) -> Solid { + let d = nums(&e["d"]); + let s = match e["k"].as_str().unwrap() { + "box" => box_(&d), + "cyl" => cyl(d[0], d[1]), + "sph" => sph(d[0]), + "tor" => tor(d[0], d[1]), + k => panic!("kind {k}"), + }; + let mut s = orient(s); + let rx = e["rx"].as_f64().unwrap(); + let ry = e["ry"].as_f64().unwrap(); + if rx != 0.0 { + s = builder::rotated(&s, Point3::origin(), Vector3::unit_x(), Rad(rx.to_radians())); + } + if ry != 0.0 { + s = builder::rotated(&s, Point3::origin(), Vector3::unit_y(), Rad(ry.to_radians())); + } + let p = nums(&e["p"]); + builder::translated(&s, Vector3::new(p[0], p[1], p[2])) +} + +fn apply(op: &str, a: &Solid, b: &Solid, tol: f64) -> Option { + match op { + "union" => truck_shapeops::or(a, b, tol), + "intersect" => truck_shapeops::and(a, b, tol), + "subtract" => { + let mut nb = b.clone(); + nb.not(); + truck_shapeops::and(a, &nb, tol) + } + o => panic!("op {o}"), + } +} + +fn mesh_stats(s: &Solid, tol: f64, objpath: Option<&str>) -> Value { + let t = Instant::now(); + let r = catch_unwind(AssertUnwindSafe(|| mesh_of(s, tol))); + let ms = t.elapsed().as_secs_f64() * 1e3; + match r { + Ok(m) => { + if let Some(p) = objpath { + let f = std::fs::File::create(p).unwrap(); + let _ = truck_meshalgo::rexport_polymesh::obj::write(&m, BufWriter::new(f)); + } + json!({"tol": tol, "ms": ms, "tris": m.faces().triangle_iter().count(), "faces": m.faces().len(), + "closed": format!("{:?}", m.shell_condition()), "vol": m.volume()}) + } + Err(_) => json!({"tol": tol, "panic": take_panic()}), + } +} + +fn step_string(s: &Solid) -> String { + let c = s.compress(); + let repr = out::StepDesign::from_model(out::StepModel::from(&c)); + out::StepDisplay::new( + out::StepHeaderDescriptor { organization_system: "trucktorture".to_owned(), ..Default::default() }, + repr, + ) + .to_string() +} + +fn reread(text: &str) -> Value { + use truck_stepio::r#in::*; + let r = catch_unwind(AssertUnwindSafe(|| { + let table = Table::from_step(text).ok_or("parse failed")?; + let mut shells = vec![]; + for (_, sh) in table.shell.iter() { + match table.to_compressed_shell(sh) { + Ok(cs) => { + let mut m = cs.triangulation(0.01).to_polygon(); + m.put_together_same_attrs(TOLERANCE * 50.0).remove_degenerate_faces(); + shells.push(json!({"closed": format!("{:?}", m.shell_condition()), "vol": m.volume(), + "tris": m.faces().triangle_iter().count()})); + } + Err(e) => shells.push(json!({"err": format!("{e:?}").chars().take(200).collect::()})), + } + } + Ok::<_, &str>(json!({"shells": shells})) + })); + match r { + Ok(Ok(v)) => v, + Ok(Err(e)) => json!({"err": e}), + Err(_) => json!({"panic": take_panic()}), + } +} + +fn run_case(cases: &Value, idx: usize, tol: f64, out_dir: &str) { + let c = &cases[idx]; + let name = c["name"].as_str().unwrap(); + let op = c["op"].as_str().unwrap(); + let bs = c["bs"].as_array().unwrap(); + let t0 = Instant::now(); + let mut step_ms: Vec = vec![]; + let r = catch_unwind(AssertUnwindSafe(|| { + let mut cur = prim(&c["a"]); + for b in bs { + let bb = prim(b); + let t = Instant::now(); + let n = apply(op, &cur, &bb, tol); + step_ms.push(t.elapsed().as_secs_f64() * 1e3); + match n { + Some(n) => cur = n, + None => return None, + } + } + Some(cur) + })); + let total = t0.elapsed().as_secs_f64() * 1e3; + eprintln!("STAGE boolean done {:.0}ms", total); + let maxstep = step_ms.iter().cloned().fold(0.0, f64::max); + let mut res = json!({"case": name, "idx": idx, "tol": tol, "steps_done": step_ms.len(), "steps_total": bs.len(), + "boolean_ms_total": total, "boolean_ms_max_step": maxstep, "exp": c["exp"]}); + match r { + Err(_) => { + res["status"] = json!("panic"); + res["panic"] = json!(take_panic()); + } + Ok(None) => res["status"] = json!("refused_none"), + Ok(Some(s)) => { + res["status"] = json!("ok"); + res["shells"] = json!(s.boundaries().len()); + res["faces"] = json!(s.boundaries().iter().map(|b| b.len()).sum::()); + let tag = format!("{}/{:02}_t{}", out_dir, idx + 1, tol); + res["mesh"] = json!([ + mesh_stats(&s, 0.1, None), + mesh_stats(&s, 0.01, Some(&format!("{tag}.obj"))), + mesh_stats(&s, 0.001, None) + ]); + eprintln!("STAGE meshes done {:.0}ms", t0.elapsed().as_secs_f64() * 1e3); + let t = Instant::now(); + let sr = catch_unwind(AssertUnwindSafe(|| step_string(&s))); + match sr { + Ok(text) => { + res["step_ms"] = json!(t.elapsed().as_secs_f64() * 1e3); + res["step_bytes"] = json!(text.len()); + std::fs::write(format!("{tag}.step"), &text).unwrap(); + eprintln!("STAGE step written {:.0}ms", t0.elapsed().as_secs_f64() * 1e3); + res["step_reread"] = reread(&text); + eprintln!("STAGE reread done {:.0}ms", t0.elapsed().as_secs_f64() * 1e3); + } + Err(_) => res["step_panic"] = json!(take_panic()), + } + } + } + println!("RES {}", res); +} + +fn read_step(path: &str) { + let text = std::fs::read_to_string(path).unwrap(); + let t = Instant::now(); + let mut v = reread(&text); + v["file"] = json!(path); + v["ms"] = json!(t.elapsed().as_secs_f64() * 1e3); + println!("RES {}", v); +} + +fn prims() { + for (n, s, exp) in [ + ("box 10x10x10", box_(&[10.0, 10.0, 10.0]), 1000.0), + ("cyl r5 h10", cyl(5.0, 10.0), PI * 25.0 * 10.0), + ("sph r5", sph(5.0), 4.0 / 3.0 * PI * 125.0), + ("tor 10,3", tor(10.0, 3.0), 2.0 * PI * PI * 10.0 * 9.0), + ] { + let raw = mesh_of(&s, 0.01).volume(); + let o = orient(s.clone()); + println!( + "RES {}", + json!({"prim": n, "analytic": exp, "mesh_vol_raw_orientation": raw, + "mesh_vol_after_orient": mesh_of(&o, 0.01).volume()}) + ); + } +} + +fn main() { + std::panic::set_hook(Box::new(|i| { + let bt = std::backtrace::Backtrace::force_capture().to_string(); + let frames: Vec = bt + .lines() + .filter(|l| l.contains("truck_") && !l.trim_start().starts_with("at ")) + .map(|l| l.trim().chars().take(70).collect::()) + .take(6) + .collect(); + *PANIC.lock().unwrap() = format!("{i}").replace('\n', " | ") + " || " + &frames.join(" ; "); + })); + let a: Vec = std::env::args().collect(); + match a[1].as_str() { + "run" => { + let cases: Value = serde_json::from_str(&std::fs::read_to_string(&a[2]).unwrap()).unwrap(); + run_case(&cases, a[3].parse().unwrap(), a[4].parse().unwrap(), &a[5]); + } + "readstep" => read_step(&a[2]), + "prims" => prims(), + _ => panic!("mode"), + } +}