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 @@ -117,7 +117,7 @@ No tests exist. No linter is configured.

The full cohort graduated to v1.0 on 2026-05-07 alongside OCCT 8.0.0 GA. SemVer-stable from these floors; only bump on documented breaking changes. Pre-1.0 dep history (which API landed in which 0.x tag) lives in git log; consult it when you actually need to support an older floor, otherwise treat the v1.0 surface as the contract.

- **OCCTSwift**: `https://github.com/SecondMouseAU/OCCTSwift.git` (>= 1.17.0; xcframework built against **OCCT 8.0.0p1**). The B-Rep kernel: ~400+ methods for parametric CAD, the full ISO drawings stack (Sheet/TitleBlock/ProjectionSymbol/Section2D/Hatch/AutoCentermarks/CuttingPlaneLine/CosmeticThread/SurfaceFinish/GDT/DetailView/DrawingScale), `FeatureReconstructor` for `reconstruct`, the `SheetMetal` namespace for `compose-sheet-metal`, and the XCAF surfaces (`AssemblyNode.labelId`, `Document.node(at:)`) for `inspect-assembly` / `set-metadata`. **Floored at v1.17.0** (raised in d5d31e8 for the OCCTSwift#377/#380 Pass 1a duplication and bug-fix audit; also carries the `Shape.drilled` direction fix, OCCTSwift#272, which lands between 1.12.0 and 1.12.9 and corrected recipe 01's through-holes). Previously **floored at v1.15.0**: v1.15.0 renamed the Swift wrapper class `TopologyGraph` → `BRepGraph` (OCCTSwift#335) to match the C++ package it wraps; this repo has migrated off the deprecated `TopologyGraph` typealias onto `BRepGraph` directly (OCCTSwiftScripts#78), so the floor must guarantee the `BRepGraph` symbol exists. Earlier, v1.7.0 realigned the BRepGraph wrapper to OCCT's redesigned graph model (definitions vs references/usages, persistent UIDs, controlled layers) and v1.7.1 made the derived graph reads real again: `adjacentFaces`/`faces(of:)`/`edges(of:)`/`sharedEdges`, `faceSameDomain`, `faceIsNaturalRestriction`, plus durable `UID`/`RefUID`/`ItemUID` identity. Our graph verbs (graph-validate/compact/dedup/ml, query-topology) build and run **unchanged** against it. Behaviour changes are **confined to the BRepGraph domain**: `edgeMaxContinuity`/`setEdgeRegularity` are now no-ops (use `Shape.maxContinuity` for continuity); `degenerated`/`closed`/`sameParameter`/`sameRange` setters no-op while their getters return the live derived value. The cookbook ergonomics relied on since v1.3.1, namely `Shape.circularPatternCut` (#169), orientation-normalised `Shape.sweep` + `orientedForward`/`signedVolume` (#170), `concaveEdges`/`convexEdges`/`edges(where:)` selectors (#171), are unchanged. The rest of the cohort still resolves at its existing floors (Viewport 1.0.4 / Tools 1.1.1 / AIS 1.0.2 / Mesh 1.0.0 / IO 1.7.5), all of which compile against 1.15.0.
- **OCCTSwift**: `https://github.com/SecondMouseAU/OCCTSwift.git` (>= 2.0.0; xcframework built against **OCCT 8.0.1**). The B-Rep kernel: ~400+ methods for parametric CAD, the full ISO drawings stack (Sheet/TitleBlock/ProjectionSymbol/Section2D/Hatch/AutoCentermarks/CuttingPlaneLine/CosmeticThread/SurfaceFinish/GDT/DetailView/DrawingScale), `FeatureReconstructor` for `reconstruct`, the `SheetMetal` namespace for `compose-sheet-metal`, and the XCAF surfaces (`AssemblyNode.labelId`, `Document.node(at:)`) for `inspect-assembly` / `set-metadata`. **Floored at v2.0.0** (OCCTSwiftScripts#111): a correctness major (17 breaking changes to the public Swift API; see `docs/SEMVER.md#v200` in the OCCTSwift repo), not a wrapping one. Two fixes landed in this repo alongside the bump: (1) `ShapeAnalysisResult.selfIntersectionCount` was removed (#763; always `0`, never computed), so `Heal.swift`/`GraphValidate.swift` now report `hasSelfIntersection`/`selfIntersecting` as `Bool?` via the real, opt-in `Shape.analyze(selfIntersectionTimeout:)` check (`nil` = "not checked" by default, since the check is ~3000x an ordinary scan on pathological input and both verbs would run it twice), rather than the fabricated always-`0`/always-`false` the removed field silently produced. (2) AAG builds nodes from face **occurrences** (#642): `AAGNode.faceIndex` / `PocketFeature.floorFaceIndex`/`wallFaceIndices` / `detectHoles()`'s `faceIndex` / `AAGEdge.face1Index`/`face2Index` now index `Shape.orientedFaces()`, not the `Shape.faces()` `face[N]` scheme `query-topology` emits (the two agreed automatically pre-2.0.0, since `faces()` was itself occurrence-based then). `FeatureRecognize.swift` (both the `occtkit` command and the legacy standalone target), `GraphSelect.swift`, and `GraphML.swift` all cross-reference AAG output against that `face[N]` scheme and now resolve through the new `AAGNode.distinctFaceIndex` bridge; a no-op on any shape that shares no face (every single-solid part, the only kind this repo's pre-#111 tests exercised), so it only bites a multi-solid compound with a shared face, exactly the shape a caller runs `feature-recognize`/`graph-select`/`graph-ml` against to look for cross-solid structure. `Tests/OcctkitCommandTests/AAGFaceIndexTests.swift` regression-tests the fix directly against `graph-select`/`graph-ml`'s JSON output on a split-box-compound fixture. Previously **floored at v1.17.0** (raised in d5d31e8 for the OCCTSwift#377/#380 Pass 1a duplication and bug-fix audit; also carries the `Shape.drilled` direction fix, OCCTSwift#272, which lands between 1.12.0 and 1.12.9 and corrected recipe 01's through-holes). Before that, **floored at v1.15.0**: v1.15.0 renamed the Swift wrapper class `TopologyGraph` → `BRepGraph` (OCCTSwift#335) to match the C++ package it wraps; this repo has migrated off the deprecated `TopologyGraph` typealias onto `BRepGraph` directly (OCCTSwiftScripts#78), so the floor must guarantee the `BRepGraph` symbol exists. Earlier, v1.7.0 realigned the BRepGraph wrapper to OCCT's redesigned graph model (definitions vs references/usages, persistent UIDs, controlled layers) and v1.7.1 made the derived graph reads real again: `adjacentFaces`/`faces(of:)`/`edges(of:)`/`sharedEdges`, `faceSameDomain`, `faceIsNaturalRestriction`, plus durable `UID`/`RefUID`/`ItemUID` identity. Our graph verbs (graph-validate/compact/dedup/ml, query-topology) build and run **unchanged** against it. Behaviour changes are **confined to the BRepGraph domain**: `edgeMaxContinuity`/`setEdgeRegularity` are now no-ops (use `Shape.maxContinuity` for continuity); `degenerated`/`closed`/`sameParameter`/`sameRange` setters no-op while their getters return the live derived value. The cookbook ergonomics relied on since v1.3.1, namely `Shape.circularPatternCut` (#169), orientation-normalised `Shape.sweep` + `orientedForward`/`signedVolume` (#170), `concaveEdges`/`convexEdges`/`edges(where:)` selectors (#171), are unchanged. **The rest of the cohort has not yet released a version compatible with OCCTSwift 2.0.0**: OCCTSwiftIO's latest release (v1.7.6) still requires `occtswift 1.17.0..<2.0.0` transitively (confirmed by a real remote `swift package resolve` against a fresh checkout with no local sibling), and OCCTSwiftTools/AIS/Mesh's own latest releases are all still on their "repin to 1.17.0" floors too. This repo's own bump is complete and builds clean locally via the sibling-checkout trick (every sibling repo's *own*, not-yet-released local checkout has already moved its floor to 2.0.0 too), but a fresh clone / CI run cannot resolve the dependency graph from remote until at least OCCTSwiftIO ships a 2.0.0-compatible release. Per OCCTSwiftScripts#111's own text, this repo's release should wait for that (and for OCCTSwiftTools's), same as the rest of the cohort.
- **OCCTSwiftViewport**: `https://github.com/gsdali/OCCTSwiftViewport.git` (>= 1.0.0). Provides `OffscreenRenderer`, `CameraState`, `DisplayMode`, `ViewportBody` for `render-preview`. Graduated to v1.0.0 on 2026-05-08, one day after the rest of the cohort; floor unblocked by Tools v1.0.2 (closes #45).
- **OCCTSwiftTools**: `https://github.com/gsdali/OCCTSwiftTools.git` (>= 1.0.0; resolves to v1.0.2+ for the widened Viewport constraint). Bridge layer between the B-Rep kernel and the Metal viewport. We use `CADFileLoader.shapeToBodyAndMetadata` in `render-preview` for Shape → `ViewportBody` conversion (both input bodies and highlight sub-shapes). Lives in its own repo since Viewport v0.55.0 split.
- **OCCTSwiftAIS**: `https://github.com/gsdali/OCCTSwiftAIS.git` (>= 1.0.0). Headless-friendly subset only: `Trihedron` / `WorkPlane` / `Axis` / `PointCloud` scene objects (each emits `[ViewportBody]` via `makeBodies()`) for `render-preview`'s `--show-axes` / `--show-workplane` overlays, plus the SubShape selection vocabulary for `--highlight face[N]/edge[M]/vertex[K]`. Selection / Manipulator / SwiftUI surfaces aren't relevant to a CLI. `Dimension` overlays render via a SwiftUI Canvas inside `MetalViewportView` and so can't reach `OffscreenRenderer`, so `--annotate-dimensions` is deferred (filed as OCCTSwiftViewport#26). Note: `OCCTSwiftAIS` re-exports a `DisplayMode` enum (3 cases) that collides with `OCCTSwiftViewport.DisplayMode` (6 cases); fully-qualify in `RenderPreview.swift` as `OCCTSwiftViewport.DisplayMode`.
Expand Down
33 changes: 32 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,28 @@ let package = Package(
// symbol doesn't exist before v1.15.0 (only the deprecated typealias
// does, starting there): the floor must track the rename, not just
// permit it via the open `from:` range.
occtDep("OCCTSwift", from: "1.17.0"), // ≥1.17.0: Pass 1a duplication/bug-fix audit (OCCTSwift#377/#380), continuity enum consolidation (source-compatible via deprecated aliases), Surface.drawMesh/evaluateGrid now return SurfaceGrid (not used here), arc-length/Surface.normal/Curve2D.circle edge-case bug fixes; ≥1.15.0: BRepGraph rename (OCCTSwift#335); also carries kernel crash/hang fixes through #318/#323 (patches 0003-0009) from the prior 1.12.9 floor
// Bumped 1.17.0 -> 2.0.0 (OCCTSwiftScripts#111): a correctness major (17 breaking
// changes to the public Swift API, OCCT absorbed to 8.0.1), not a wrapping one. Fixed
// in this repo alongside the bump:
// - ShapeAnalysisResult.selfIntersectionCount removed (#763; always 0, never
// computed). Heal.swift / GraphValidate.swift now report `hasSelfIntersection` /
// `selfIntersecting` as Bool? via the real `analyze(selfIntersectionTimeout:)`
// check, opt-in (nil "not checked" by default, since the check is ~3000x an
// ordinary scan on pathological input), rather than the fabricated always-0/always-false
// the removed field silently produced.
// - AAG builds nodes from face occurrences (#642): AAGNode.faceIndex /
// PocketFeature.floorFaceIndex/wallFaceIndices / detectHoles()'s faceIndex /
// AAGEdge.face1Index/face2Index now index Shape.orientedFaces(), not the
// Shape.faces() `face[N]` scheme query-topology emits (they agreed automatically
// pre-2.0.0, since faces() was itself occurrence-based then). FeatureRecognize.swift
// (both the occtkit command and the legacy standalone target), GraphSelect.swift,
// and GraphML.swift all cross-reference AAG output against that `face[N]` scheme
// and now resolve through the new `AAGNode.distinctFaceIndex` bridge; no-op on any
// shape that shares no face, which is every fixture this repo's tests used before
// Tests/OcctkitCommandTests/AAGFaceIndexTests.swift (added alongside this bump)
// started exercising a real shared-face compound.
// See docs/SEMVER.md#v200 in the OCCTSwift repo for the full break table.
occtDep("OCCTSwift", from: "2.0.0"),
// RenderPreview rasterizes through Viewport's OffscreenRenderer.
// Floored at v1.0.4: v1.0.3 fixes an uncatchable quantize() crash on
// body load (Viewport #30) and v1.0.4 makes the published Viewport
Expand Down Expand Up @@ -257,5 +278,15 @@ let package = Package(
path: "Tests/DrawingComposerTests",
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "OcctkitCommandTests",
dependencies: [
"occtkit",
"ScriptHarness",
.product(name: "OCCTSwift", package: "OCCTSwift"),
],
path: "Tests/OcctkitCommandTests",
swiftSettings: [.swiftLanguageMode(.v6)]
),
]
)
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ The `occtkit` executable is a separate target; install via the `Makefile` above

- macOS 15+
- Swift 6.0+
- [OCCTSwift](https://github.com/SecondMouseAU/OCCTSwift) `>= 1.15.0` (xcframework built against OCCT 8.0.0 GA; floor required for the `BRepGraph` rename, see OCCTSwift#335)
- [OCCTSwift](https://github.com/SecondMouseAU/OCCTSwift) `>= 2.0.0` (xcframework built against OCCT 8.0.1; a correctness major, 17 breaking changes, see OCCTSwiftScripts#111 and OCCTSwift's `docs/SEMVER.md#v200`. **Not yet resolvable from a fresh clone**: the rest of the cohort below hasn't released a version compatible with OCCTSwift 2.0.0 yet)
- [OCCTSwiftViewport](https://github.com/SecondMouseAU/OCCTSwiftViewport) `>= 1.0.0` (powers `render-preview`)
- [OCCTSwiftTools](https://github.com/SecondMouseAU/OCCTSwiftTools) `>= 1.0.0` (bridge layer used by `render-preview` for Shape→ViewportBody conversion)
- [OCCTSwiftAIS](https://github.com/SecondMouseAU/OCCTSwiftAIS) `>= 1.0.0` (headless overlays for `render-preview`'s `--show-axes` / `--show-workplane` / `--highlight`)
Expand Down
15 changes: 12 additions & 3 deletions Sources/FeatureRecognize/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,19 @@ do {
let shape = try GraphIO.loadBREP(at: path)
let aag = AAG(shape: shape)

// OCCTSwift#642 (v2.0.0): see occtkit's `feature-recognize` for the full rationale.
// floorFaceIndex/wallFaceIndices/faceIndex are AAG occurrence indices
// (Shape.orientedFaces()); resolve to Shape.faces()'s deduplicated index via
// AAGNode.distinctFaceIndex so this stays consistent with every other face[N]
// consumer (no-op on any shape that shares no face).
func distinctFace(_ occurrenceIndex: Int) -> Int {
aag.nodes[occurrenceIndex].distinctFaceIndex
}

let pockets = aag.detectPockets().map { p in
Report.Pocket(
floorFaceIndex: p.floorFaceIndex,
wallFaceIndices: p.wallFaceIndices,
floorFaceIndex: distinctFace(p.floorFaceIndex),
wallFaceIndices: p.wallFaceIndices.map(distinctFace),
zLevel: p.zLevel,
depth: p.depth,
isOpen: p.isOpen,
Expand All @@ -53,7 +62,7 @@ do {
)
}
let holes = aag.detectHoles().map { h in
Report.Hole(faceIndex: h.faceIndex, radius: h.radius, depth: h.depth)
Report.Hole(faceIndex: distinctFace(h.faceIndex), radius: h.radius, depth: h.depth)
}
try GraphIO.emitJSON(Report(pockets: pockets, holes: holes))
} catch {
Expand Down
20 changes: 17 additions & 3 deletions Sources/occtkit/Commands/FeatureRecognize.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,24 @@ enum FeatureRecognizeCommand: Subcommand {
let shape = try GraphIO.loadBREP(at: path)
let aag = AAG(shape: shape)

// OCCTSwift#642 (v2.0.0): PocketFeature.floorFaceIndex / .wallFaceIndices and
// detectHoles()'s faceIndex are now OCCURRENCE indices into Shape.orientedFaces(),
// not the `face[N]` scheme (Shape.faces(), deduplicated) query-topology emits and
// this report's own topologyRefs are documented (above) to align with. Resolve each
// through AAGNode.distinctFaceIndex, the bridge OCCTSwift added for exactly this, so
// topologyRefs keeps naming the same face query-topology would. On any shape that
// shares no face (every single-solid part) distinctFaceIndex == the occurrence index,
// so this is a no-op there; it only differs on a multi-solid compound with a face
// shared between two solids (a split result), which is exactly the shape AAG's own
// pocket/hole detection is meant to run on.
func distinctFace(_ occurrenceIndex: Int) -> Int {
aag.nodes[occurrenceIndex].distinctFaceIndex
}

let pockets = aag.detectPockets().map { p in
Report.Pocket(
floorFaceIndex: p.floorFaceIndex,
wallFaceIndices: p.wallFaceIndices,
floorFaceIndex: distinctFace(p.floorFaceIndex),
wallFaceIndices: p.wallFaceIndices.map(distinctFace),
zLevel: p.zLevel,
depth: p.depth,
isOpen: p.isOpen,
Expand All @@ -64,7 +78,7 @@ enum FeatureRecognizeCommand: Subcommand {
)
}
let holes = aag.detectHoles().map { h in
Report.Hole(faceIndex: h.faceIndex, radius: h.radius, depth: h.depth)
Report.Hole(faceIndex: distinctFace(h.faceIndex), radius: h.radius, depth: h.depth)
}

var features: [Report.Feature] = []
Expand Down
13 changes: 11 additions & 2 deletions Sources/occtkit/Commands/GraphML.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,20 @@ enum GraphMLCommand: Subcommand {
}

// Attributed face-adjacency with per-adjacency convexity, from the AAG.
//
// OCCTSwift#642 (v2.0.0): AAGEdge.face1Index/face2Index are OCCURRENCE indices into
// Shape.orientedFaces(), not `faces[].index` above (BRepGraph-native, already
// deduplicated, unaffected by #642). Left unconverted, a shared face on a multi-solid
// compound would emit a face1/face2 >= faces.count, a dangling node reference in a
// payload that IS a graph (faces[] are the nodes, faceAdjacency the edges), not merely
// a mislabeled one. Resolve through AAGNode.distinctFaceIndex so every index in this
// payload stays in the one space `faces[].index` defines (no-op on any shape that
// shares no face).
let aag = AAG(shape: shape)
let faceAdjacency: [Payload.FaceAdjacency] = aag.edges.map {
Payload.FaceAdjacency(
face1: $0.face1Index,
face2: $0.face2Index,
face1: aag.nodes[$0.face1Index].distinctFaceIndex,
face2: aag.nodes[$0.face2Index].distinctFaceIndex,
convexity: $0.convexity.label,
sharedEdgeCount: $0.sharedEdgeCount
)
Expand Down
Loading
Loading