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 @@ -121,7 +121,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` (>= 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.
- **OCCTSwift**: `https://github.com/SecondMouseAU/OCCTSwift.git` (>= 3.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 v3.0.0** (OCCTSwiftScripts#118): a correctness/consolidation major (OCCT itself stays at 8.0.1; the kernel was only rebuilt to carry two patches the v2.0.0 asset was missing), not a wrapping one. Two breaking changes, both compile errors (see `docs/SEMVER.md#v300` in the OCCTSwift repo): (1) `Selector.SubShapeType.compsolid` renamed `.compSolid`, consolidating four drifted Swift mirrors of `TopAbs_ShapeEnum` onto `ShapeType` — zero source changes needed here, since this repo already spelled it `ShapeType.compSolid` (`LoadBrep.swift`, `Pattern.swift`, `RenderPreview.swift`), the surviving spelling. (2) `Shape.bounds`/`.size`/`.center`, `Wire.bounds`, `Edge.bounds`, `Face.bounds` (and `.exactBounds`, unused here) become `Optional`: they used to fabricate `(0,0,0)-(0,0,0)` for a shape with no bounding box, indistinguishable from a genuine zero-size shape at the world origin (`Shape.boundingBox`/`boundingBoxOptimal()` already behaved correctly and are unchanged). Every `.bounds` call site in this repo now unwraps: `QueryTopology.swift`/`LoadBrep.swift`/`MeasureDeviation.swift`/`RenderPreview.swift`/`Metrics.swift` throw a named `ScriptError` on a `nil` bounding box (a real error on a loaded BREP, not a state worth papering over with `?? .zero`), and the two recipe edge-selector predicates (`recipes/01-mounting-bracket`, `recipes/03-pipe-flange`) return `false` on a `nil` bounds rather than fabricate a match. `Tests/OcctkitCommandTests/OptionalBoundsTests.swift` regression-tests the throw path directly: it constructs a genuinely void shape (the intersection of two disjoint boxes) and asserts `LoadBrepCommand.buildResponse`/`MeasureDeviationCommand.defaultDeflection` throw rather than fabricate a zero-size box. **The rest of the cohort has not yet released an OCCTSwift-3.0.0-compatible version**: OCCTSwiftTools/Mesh's latest releases (and OCCTSwiftIO's, which has no direct OCCTSwift dependency but gates via Tools) all still cap `from: "2.0.0"` (`.upToNextMajor` excludes 3.0.0), and OCCTSwiftAIS inherits the cap transitively through Tools plus has 3 of its own unfixed `.bounds` call sites (`Dimension.swift`, `AreaSelection.swift`) — confirmed by attempting a real local build against the sibling checkouts, which fails inside OCCTSwiftAIS. This repo's own fix is complete and verified (`swift build`, `swift test`, and the recipe smoke suite all pass locally against a temporarily-patched AIS sibling, reverted after verification), but neither a fresh clone/CI run nor an as-is local sibling build can resolve the graph until Tools/Mesh/IO/AIS ship their own 3.0.0 repins. Per OCCTSwiftScripts#118, this repo's release should wait for that, same situation as the 2.0.0 bump. Previously **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). Two fixes landed in this repo alongside that 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. Before that, **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 2.0.0-bump-era cohort gap (OCCTSwiftIO/Tools/AIS/Mesh all still on their "repin to 1.17.0" floors, blocking remote resolution) resolved by 2026-08-10, when OCCTSwiftIO v1.7.7, OCCTSwiftTools v1.6.3, and OCCTSwiftMesh v1.7.3 each shipped their own "repin OCCTSwift to 2.0.0" release; see the v3.0.0 cohort-gap note above this paragraph for the current (unresolved as of this writing) equivalent.
- **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
26 changes: 25 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,31 @@ let package = Package(
// 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"),
// Bumped 2.0.0 -> 3.0.0 (OCCTSwiftScripts#118): a correctness/consolidation major, OCCT
// stays at 8.0.1 (kernel rebuilt to carry two patches the v2.0.0 asset was missing).
// Two breaks, both compile errors:
// - `Selector.SubShapeType.compsolid` renamed `.compSolid`, consolidating four drifted
// Swift mirrors of TopAbs_ShapeEnum onto `ShapeType`. Zero hits here: this repo
// already spelled it `ShapeType.compSolid` (LoadBrep.swift, Pattern.swift,
// RenderPreview.swift), which was already the surviving spelling.
// - `Shape.bounds`/`.size`/`.center`, `Wire.bounds`, `Edge.bounds`, `Face.bounds` (and
// `.exactBounds`, unused here) become Optional: they used to fabricate
// `(0,0,0)-(0,0,0)` for a shape with no bounding box, indistinguishable from a
// genuine zero-size shape at the origin. Every `.bounds` call site in this repo now
// unwraps: query-topology/load-brep/measure-distance/render-preview/metrics throw a
// named ScriptError on a nil bounding box (a real error for a loaded BREP, not a
// state to paper over); the two recipe edge-selector predicates return `false` (guard
// against a mid-selection nil rather than fabricate a match).
// See docs/SEMVER.md#v300 in the OCCTSwift repo for the full break table.
//
// The rest of the cohort has not yet released an OCCTSwift-3.0.0-compatible version as of
// this bump (OCCTSwiftTools/Mesh/IO's latest releases all still cap `from: "2.0.0"`,
// i.e. `.upToNextMajor` excludes 3.0.0; OCCTSwiftAIS transitively via Tools). `from:` pins
// below are unaffected by this bump directly, but a fresh clone / CI cannot resolve the
// full graph until they catch up, same situation as the 2.0.0 bump (see git history on
// this comment block). Local builds against sibling checkouts work today because path
// dependencies bypass semver ranges entirely.
occtDep("OCCTSwift", from: "3.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
6 changes: 4 additions & 2 deletions Sources/occtkit/Commands/LoadBrep.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,10 @@ enum LoadBrepCommand: Subcommand {
}
}

static func buildResponse(bodyId: String, shape: Shape) -> Response {
let bb = shape.bounds
static func buildResponse(bodyId: String, shape: Shape) throws -> Response {
guard let bb = shape.bounds else {
throw ScriptError.message("'\(bodyId)' has no bounding box (void or degenerate shape)")
}
return Response(
bodyId: bodyId,
isValid: shape.isValid,
Expand Down
8 changes: 5 additions & 3 deletions Sources/occtkit/Commands/MeasureDeviation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ enum MeasureDeviationCommand: Subcommand {
let aShape = try GraphIO.loadBREP(at: req.a)
let bShape = try GraphIO.loadBREP(at: req.b)

let defl = req.deflection ?? defaultDeflection(for: aShape)
let defl = try req.deflection ?? defaultDeflection(for: aShape)
guard defl > 0 else { throw ScriptError.message("deflection must be positive") }

guard let aTris = TriMesh(shape: aShape, deflection: defl) else {
Expand Down Expand Up @@ -204,8 +204,10 @@ enum MeasureDeviationCommand: Subcommand {

// ── geometry helpers ────────────────────────────────────────────────

static func defaultDeflection(for shape: Shape) -> Double {
let b = shape.bounds
static func defaultDeflection(for shape: Shape) throws -> Double {
guard let b = shape.bounds else {
throw ScriptError.message("shape has no bounding box (void or degenerate shape)")
}
let diag = simd_length(b.max - b.min)
return Swift.max(diag * 0.005, 1e-6)
}
Expand Down
6 changes: 4 additions & 2 deletions Sources/occtkit/Commands/Metrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,11 @@ enum MetricsCommand: Subcommand {
wants("volume") || wants("centerOfMass") || wants("principalAxes")
? shape.volumeInertia : nil

let bb: Response.BoundingBox? = {
let bb: Response.BoundingBox? = try {
guard wants("boundingBox") else { return nil }
let b = shape.bounds
guard let b = shape.bounds else {
throw ScriptError.message("shape has no bounding box (void or degenerate shape)")
}
return .init(
min: [b.min.x, b.min.y, b.min.z],
max: [b.max.x, b.max.y, b.max.z]
Expand Down
10 changes: 8 additions & 2 deletions Sources/occtkit/Commands/QueryTopology.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ enum QueryTopologyCommand: Subcommand {
let area = face.area()
if let m = filter.minArea, area < m { continue }
if let m = filter.maxArea, area > m { continue }
let bb = face.bounds
guard let bb = face.bounds else {
throw ScriptError.message(
"face[\(i)] has no bounding box (void or degenerate face)")
}
let center = SIMD3<Double>(
(bb.min.x + bb.max.x) * 0.5,
(bb.min.y + bb.max.y) * 0.5,
Expand Down Expand Up @@ -181,7 +184,10 @@ enum QueryTopologyCommand: Subcommand {
let length = edge.length
if let m = filter.minLength, length < m { continue }
if let m = filter.maxLength, length > m { continue }
let bb = edge.bounds
guard let bb = edge.bounds else {
throw ScriptError.message(
"edge[\(i)] has no bounding box (void or degenerate edge)")
}
let center = SIMD3<Double>(
(bb.min.x + bb.max.x) * 0.5,
(bb.min.y + bb.max.y) * 0.5,
Expand Down
Loading
Loading