Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ No tests exist. No linter is configured.

**`render-preview` AIS overlays (Phase 2)**: `--show-axes` overlays an `OCCTSwiftAIS.Trihedron` (sized to half the union bbox diagonal). Position via `--axes-position origin|center|outside|x,y,z`; default is **`outside`** (anchored 20% of the diagonal beyond the bbox-min corner so all three arrows extend into the part region and stay visible regardless of where the part sits in world space, which fixes the original Phase 2 quirk where arrows hid inside parts spanning the origin). `--show-workplane xy|yz|xz` overlays an `OCCTSwiftAIS.WorkPlane` (sized 1.2× the diagonal, semi-transparent blue by default). `--highlight face[N],edge[M],vertex[K]` extracts sub-shapes from the **first** input BREP via `Shape.subShape(type:index:)` and renders them as separate ViewportBodies with an orange highlight colour (override via `--highlight-color #hex`). The face/edge/vertex IDs use the same `face[N]` / `edge[N]` / `vertex[N]` scheme that `query-topology` emits, so the two verbs cross-reference cleanly. `--highlight` is scoped to the first input only (multi-BREP scenes that want to highlight in body N should render that body solo). `--annotate-dimensions` (the fourth Phase 2 candidate) is **not implemented**. `OCCTSwiftAIS.LinearDimension` / `AngularDimension` / `RadialDimension` render via `MeasurementOverlay`, a SwiftUI Canvas inside `MetalViewportView` that isn't reachable from `OffscreenRenderer`. Filed upstream as **OCCTSwiftViewport#26** (request: `OffscreenRenderOptions.measurements` + a CGContext post-pass on the rendered image). The verb-side wiring is one parse step + array-passthrough once that lands.

**`render-preview`** is the OCCTMCP-driven headless PNG render verb (closes #24). Wraps OCCTSwiftViewport's `OffscreenRenderer` (added in OCCTSwiftViewport#18). Input: one or more BREP paths or a `--manifest`; output: a PNG at `--output`. Camera presets `iso|front|back|top|bottom|left|right` (compute view from union bounding box) or explicit `--camera-position` / `--camera-target` / `--camera-up`. `--display-mode shaded|wireframe|shaded-with-edges|flat|xray|rendered`, `--background light|dark|transparent|#hex`, `--width` / `--height` (default 800×600). The verb hops to the main actor via `MainActor.assumeIsolated` to drive the `OffscreenRenderer` because the umbrella dispatcher is `@MainActor` but the `Subcommand.run(args:)` requirement is nonisolated. The OCCTSwiftViewport dep is currently revision-pinned to a `main` commit because OffscreenRenderer hadn't been cut as a release tag yet (latest tag was v0.49.0 from 2026-03-16, pre-OffscreenRenderer); bump to a `from: "<tag>"` pin once a Viewport release ships.
**`render-preview`** is the OCCTMCP-driven headless PNG render verb (closes #24). Wraps OCCTSwiftViewport's `OffscreenRenderer` (added in OCCTSwiftViewport#18). Input: one or more BREP paths or a `--manifest`; output: a PNG at `--output`. Camera presets `iso|front|back|top|bottom|left|right` (compute view from union bounding box) or explicit `--camera-position` / `--camera-target` / `--camera-up`. `--display-mode shaded|wireframe|shaded-with-edges|flat|xray|rendered`, `--background light|dark|transparent|#hex`, `--width` / `--height` (default 800×600). The verb hops to the main actor via `MainActor.assumeIsolated` to drive the `OffscreenRenderer` because the umbrella dispatcher is `@MainActor` but the `Subcommand.run(args:)` requirement is nonisolated. The OCCTSwiftViewport dep **was** revision-pinned to a `main` commit while OffscreenRenderer had no release tag (the latest was v0.49.0 from 2026-03-16, pre-OffscreenRenderer). That is resolved: `Package.swift` now pins `from: "1.0.4"`. Kept here as the standing reason this repo avoids revision pins, since one propagates an unstable dependency to every downstream consumer.

**`mesh` / `simplify-mesh`** are the OCCTMCP-driven mesh verbs (closes #22). `mesh` wraps `Shape.mesh(parameters: MeshParameters)` (which drives `BRepMesh_IncrementalMesh`); reports `triangleCount` / `vertexCount` plus quality metrics (`minAspectRatio` / `meanAspectRatio` / `degenerateTriangles` / `nonManifoldEdges`). Returns triangle data inline (`{ "geometry": { "vertices": [...], "indices": [...] } }`) by default; if `--output <path.stl|.obj>` is supplied OR triangle count exceeds `inlineTriangleThreshold` (100K), writes the mesh to disk via `Exporter.writeSTL` / `writeOBJ` and sets `geometry: null` + `outputPath: "..."`. `simplify-mesh` wraps OCCTSwiftMesh's `Mesh.simplified(_:)` (QEM decimation backed by vendored meshoptimizer); requires exactly one of `--target-triangle-count` / `--target-reduction`, supports `--max-hausdorff-distance` cap and `--preserve-boundary` / `--preserve-topology` flags. Writes the decimated mesh to `--output` (.stl or .obj) via a small Mesh-direct ASCII writer (OCCTSwift's `writeSTL` / `writeOBJ` take `Shape`, not `Mesh`). Reports `qualityDelta.meanAspectRatioDelta` + `qualityDelta.hausdorffDistance` (in input mesh units). Mesh-domain algorithms were ruled out-of-scope for OCCTSwift core in OCCTSwift#92 and moved to OCCTSwiftMesh: same LGPL boundary, vendored meshoptimizer license chain documented in the verb header.

Expand Down
38 changes: 31 additions & 7 deletions okf/decisions/concave-edge-classifier-can-select-wrong-edges.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,37 @@ published `v2.0.0-kernel.1` prerelease with the same repro:
A T-prism with two reentrant edges reports 3 on 1.17.0 and 2 on 2.0.0-kernel.1. A box, having no
reentrant edges, is correct on both.

The fix was too involved to backport to the 1.x line, so it is carried by the 2.0.0 refactor.
Raised upstream as [OCCTSwift#695](https://github.com/SecondMouseAU/OCCTSwift/issues/695).

**So the geometric selection below is a 1.x workaround with a known end date.** When this repo
moves to the 2.0.0 line, `concaveEdges()` becomes usable for this case again, and recipe 01 could
return to it. That would be a legitimate simplification rather than a regression. Re-run the
repro above before relying on it, rather than assuming the migration carried the fix.
**No 1.x fix will land**, but not because a backport was infeasible. Upstream built, merged and
measured one (OCCTSwift PR #700, all 4646 tests green on both CI jobs) and then **reverted it**,
for what it costs the release branch rather than for any defect in it: merging main into
`refactor/381-pass1b`, which carries the whole of v2.0.0, conflicted in 8 files across 18 regions,
and those are not all mechanical. Main and the refactor branch made genuinely different, individually
correct decisions on the same lines, because the sub-shape indexing work landed only on the refactor
branch.

The backport is not lost: re-landing means reverting the revert, so if 1.x becomes untenable before
v2.0.0 ships this can be revisited. Tracked at
[OCCTSwift#695](https://github.com/SecondMouseAU/OCCTSwift/issues/695).

**Root cause, worth knowing because it generalises.** An edge or vertex index crossing the bridge
addressed a topology *occurrence* rather than a position in the deduplicated enumeration that
`edges()` / `edgeCount` / `edge(at:)` return. A 20 mm box has 12 distinct edges but 24 edge
occurrences, so the two enumerations diverge from the first repeat onwards on any ordinary solid,
and `edgeConcavities()` zips one against the other. Upstream root-cause issue is OCCTSwift#613; the
fix is #650. Treat any bridge-crossing index with the same suspicion until 2.0.0.

Neither route to the fix is open to this package. It is reachable both from the refactor branch
and from the published `v2.0.0-kernel.1` prerelease tag, but this package is released under a semver
floor of `from: "1.17.0"`, meaning `>=1.17.0 <2.0.0`, so it cannot resolve a 2.x version at all
without a major bump, and a revision pin would propagate an unstable dependency to every downstream
consumer. This repo has carried a revision pin before (OCCTSwiftViewport, before `OffscreenRenderer`
had a release tag) and the note in `CLAUDE.md` records why it was replaced with a version pin.

**So the geometric selection below is permanent for as long as this repo is on the 1.x line.** Do
not wait for a patch release; there will not be one. The workaround becomes removable only at a
2.0.0 migration, at which point `concaveEdges()` is usable for this case again and recipe 01 could
return to it as a legitimate simplification. Re-run the repro above at that point rather than
assuming the migration carried the fix.

# Why

Expand Down
5 changes: 5 additions & 0 deletions okf/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
edges, so the call returned nil and a `??` fallback hid it. Now selects the OD and raised-face
rim explicitly.
* **Creation**: Recorded the revolve-seams-cannot-be-chamfered decision.
* **Update**: Recorded that OCCTSwift#695, the concaveEdges/convexEdges reentrant-edge
misclassification, will not be backported to 1.x. A backport was built and merged upstream
(PR #700, tests green) then reverted for what it costs the 2.0.0 release branch. The geometric
edge selection in recipe 01 is therefore permanent while this repo is on the 1.x line, not a
stopgap awaiting a patch release.

## 2026-08-04

Expand Down
6 changes: 6 additions & 0 deletions recipes/01-mounting-bracket/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ by 1 mm so the resulting cut faces are clean and coincident-face artifacts are a
(OCCTSwiftScripts #105). The true inside-corner edge has no such limit (its bound is
`legLength − thickness`, 45 mm here), which is why the same `filletRadius = 8` works fine
once the correct edge is selected.
- **This is an OCCTSwift 1.x defect and no 1.x fix is coming.** It is fixed in the 2.0.0 line
(upstream OCCTSwift#695, root cause #613). A backport was built and merged upstream, then
reverted for what it costs the 2.0.0 release branch, so the geometric selection above is
permanent while this package is on 1.x rather than a stopgap. It becomes removable at a 2.0.0
migration. See `okf/decisions/concave-edge-classifier-can-select-wrong-edges.md`, which carries
the repro to re-run at that point.
- **A concave fillet adds material, it does not remove it.** Rounding the inside corner
fills part of the sharp reentrant point with a blend, so `bracket.volume` after the
fillet is *larger* than the prism's, by `filletRadius² · (1 − π/4) · width`. Do not expect
Expand Down
10 changes: 6 additions & 4 deletions recipes/01-mounting-bracket/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ let prism = Shape.extrude(profile: lProfile, direction: SIMD3(0, 0, 1), length:
// The inside corner is the one straight edge parallel to the extrusion axis (Z) that
// sits at (thickness, thickness): select it geometrically rather than trusting
// concaveEdges(), which picks the wrong edges on this shape (see the header note).
// That classifier defect is OCCTSwift 1.x only: it is fixed in the 2.0.0 line
// (verified on v2.0.0-kernel.1, upstream OCCTSwift#695). This geometric selection is
// therefore a 1.x workaround, and this recipe could return to concaveEdges() once the
// package moves to 2.0.0. Re-run the check in the OKF entry before doing so.
// That classifier defect is OCCTSwift 1.x only and will not be fixed there: the fix
// lives on the 2.0.0 refactor branch and upstream have confirmed no backport
// (OCCTSwift#695). So this geometric selection is permanent while the package is on
// 1.x, not a stopgap awaiting a patch release. It becomes removable at a 2.0.0
// migration; re-run the check in the OKF entry at that point rather than assuming
// the migration carried the fix.
let insideCornerEdges = prism.edges { edge in
guard edge.isLine else { return false }
let b = edge.bounds
Expand Down
Loading