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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

A comprehensive Swift wrapper for [OpenCASCADE Technology (OCCT)](https://www.opencascade.com/) 8.0.0p1, providing B-Rep solid modeling for macOS and iOS. **v1.0.0 — SemVer-stable as of 2026-05-07.**

**4,301 wrapped operations** | macOS 12+ / iOS 15+ / visionOS 1+ / tvOS 15+ (arm64) | OCCT 8.0.0p1
**4,304 wrapped operations** | macOS 12+ / iOS 15+ / visionOS 1+ / tvOS 15+ (arm64) | OCCT 8.0.0p1

## Quick Start

Expand Down
18 changes: 12 additions & 6 deletions Scripts/repro/censuses/ClusterB.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ enum ClusterB {
record("fillet-edges", "filleted(edges:radius:)",
duplicate: volumesAgree(dup, single) ? "N/A: uniform radius (dup vol \(fmt(dup)) == single \(fmt(single)))" : "differs (dup \(fmt(dup)) vs single \(fmt(single)))",
outOfRange: outOfRange == nil ? "REJECT (nil)" : "measured non-nil",
declined: declined != nil ? "SKIP (non-nil, area \(fmt(declined?.surfaceArea)) vs unfilleted \(fmt(shellPlainArea)))" : "REJECT (nil)",
declined: declined != nil ? "SKIP (non-nil, area \(fmt(declined?.surfaceArea)) vs unfilleted \(fmt(shellPlainArea))) -- #639 fixed: filletedWithReport(edges:radius:) sibling reports declinedEdgeIndices" : "REJECT (nil)",
empty: box.filleted(edges: [], radius: 2.0) == nil ? "REJECT (nil)" : "non-nil",
note: "out-of-range probe: compound.edges()[15] (.index 15) passed to box (12 edges) -- Cluster A's shared fixture reused, #694")
}
Expand All @@ -174,7 +174,7 @@ enum ClusterB {
record("fillet-linear", "filleted(edges:startRadius:endRadius:)",
duplicate: volumesAgree(dup, single) ? "N/A: uniform law (dup vol \(fmt(dup)) == single \(fmt(single)))" : "differs (dup \(fmt(dup)) vs single \(fmt(single)))",
outOfRange: outOfRange == nil ? "REJECT (nil)" : "measured non-nil",
declined: declined != nil ? "SKIP (non-nil, area \(fmt(declined?.surfaceArea)) vs unfilleted \(fmt(shellPlainArea)))" : "REJECT (nil)",
declined: declined != nil ? "SKIP (non-nil, area \(fmt(declined?.surfaceArea)) vs unfilleted \(fmt(shellPlainArea))) -- #639 fixed: filletedWithReport(edges:startRadius:endRadius:) sibling reports declinedEdgeIndices" : "REJECT (nil)",
empty: box.filleted(edges: [], startRadius: 1.0, endRadius: 3.0) == nil ? "REJECT (nil)" : "non-nil")
}

Expand Down Expand Up @@ -251,7 +251,7 @@ enum ClusterB {
record("fillet-evolving", "filletEvolving(_:)",
duplicate: dupVerdict,
outOfRange: outOfRange == nil ? "REJECT (nil)" : "measured non-nil",
declined: declined != nil ? "SKIP (non-nil, area \(fmt(declined?.surfaceArea)) vs unfilleted \(fmt(shellPlainArea))) -- the #639 gap: no count of how many were skipped" : "REJECT (nil)",
declined: declined != nil ? "SKIP (non-nil, area \(fmt(declined?.surfaceArea)) vs unfilleted \(fmt(shellPlainArea))) -- #639 fixed: filletEvolvingWithReport(_:) now reports declinedEdgeIndices" : "REJECT (nil)",
empty: box.filletEvolving([]) == nil ? "REJECT (nil)" : "non-nil")
}

Expand All @@ -266,7 +266,7 @@ enum ClusterB {
record("fillet-edges (with history)", "filletedWithFullHistory(radius:edges:)",
duplicate: volumesAgree(dup, one) ? "N/A: uniform radius (dup \(fmt(dup)) == single-edge \(fmt(one)), both bigger than two-distinct-edges \(fmt(both)) -- fewer edges removed means less material gone)" : "differs (dup \(fmt(dup)) vs single \(fmt(one)))",
outOfRange: outOfRange == nil ? "REJECT (nil)" : "measured non-nil",
declined: declined != nil ? "SKIP (non-nil, area \(fmt(declined?.surfaceArea)) vs unfilleted \(fmt(shellPlainArea)))" : "REJECT (nil)",
declined: declined != nil ? "SKIP (non-nil, area \(fmt(declined?.surfaceArea)) vs unfilleted \(fmt(shellPlainArea))) -- #639: needed no new API, history.record(of:)'s !isDeleted && generated.isEmpty already names the declined set" : "REJECT (nil)",
empty: box.filletedWithFullHistory(radius: 1.0, edges: []) == nil ? "REJECT (nil)" : "non-nil")
}

Expand Down Expand Up @@ -501,6 +501,12 @@ enum ClusterB {

let foreignEdge = foreignShape.edges()[0]
let foreignAdded = builderForeign.addEdge(foreignEdge, radius: 2.0)
// #639 correction: contour(for:) -- present in this class before #639, unrelated to
// it -- already answers "did this added edge make it into a contour" for exactly this
// foreign-edge case, the same Contour(E) == 0 signal it gives a same-shape edge OCCT
// declines on geometric grounds. The original census note here ("no per-edge signal
// beyond addEdge's own Bool return") never tried this query and was wrong.
let foreignContour = builderForeign.contour(for: foreignEdge)
let foreignBuiltVolume = builderForeign.build()?.volume
let foreignNoOp = foreignBuiltVolume.map { abs($0 - (box.volume ?? -1)) < 1e-6 } ?? false

Expand All @@ -509,9 +515,9 @@ enum ClusterB {
record("fillet (class API)", "FilletBuilder.addEdge(_:radius:)",
duplicate: dupVerdict,
outOfRange: "N/A: takes an Edge object, not an index -- no out-of-range index to name",
declined: "foreign edge (belongs to a different Shape entirely): addEdge returned \(foreignAdded), build() \(foreignBuiltVolume == nil ? "REJECT (nil)" : (foreignNoOp ? "NO-OP (non-nil, volume unchanged)" : "non-nil, volume changed"))",
declined: "foreign edge (belongs to a different Shape entirely): addEdge returned \(foreignAdded), contour(for:) == \(foreignContour), build() \(foreignBuiltVolume == nil ? "REJECT (nil)" : (foreignNoOp ? "NO-OP (non-nil, volume unchanged)" : "non-nil, volume changed"))",
empty: "build() with zero addEdge calls: \(emptyResult == nil ? "REJECT (nil)" : "non-nil")",
note: "no index resolution at all in this path: no occtUseSubShapesByIndex, no occtValidFilletRadius -- #639's declined-edge gap applies here too, and there is no per-edge signal beyond addEdge's own Bool return")
note: "no index resolution at all in this path: no occtUseSubShapesByIndex, no occtValidFilletRadius -- but contour(for:) == 0 already reports the decline per edge (#639 correction), readable right after addEdge with no build() required")
}

// MARK: - ChamferBuilder class API [takes Edge objects directly, not indices]
Expand Down
23 changes: 14 additions & 9 deletions Scripts/repro/cluster-b-fillet-edge-contract/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ python3 Scripts/repro/cluster-b-fillet-edge-contract/classify_fillet_sites.py --

| family | entry point | duplicate index | out-of-range index | OCCT-declined index | empty list |
|---|---|---|---|---|---|
| fillet-edges | `filleted(edges:radius:)` | N/A: uniform radius (dup 991.415927 == single 991.415927) | REJECT (nil) | SKIP (non-nil, area 465.097336 vs unfilleted 500.000000) | REJECT (nil) |
| fillet-linear | `filleted(edges:startRadius:endRadius:)` | N/A: uniform law (dup 990.523733 == single 990.523733) | REJECT (nil) | SKIP (non-nil, area 465.097336 vs unfilleted 500.000000) | REJECT (nil) |
| fillet-edges | `filleted(edges:radius:)` | N/A: uniform radius (dup 991.415927 == single 991.415927) | REJECT (nil) | SKIP (non-nil, area 465.097336 vs unfilleted 500.000000) -- **#639 fixed**: `filletedWithReport(edges:radius:)` sibling reports `declinedEdgeIndices` | REJECT (nil) |
| fillet-linear | `filleted(edges:startRadius:endRadius:)` | N/A: uniform law (dup 990.523733 == single 990.523733) | REJECT (nil) | SKIP (non-nil, area 465.097336 vs unfilleted 500.000000) -- **#639 fixed**: `filletedWithReport(edges:startRadius:endRadius:)` sibling reports `declinedEdgeIndices` | REJECT (nil) |
| fillet-edges (per-edge radius) | `blendedEdges(_:)` | **OVERWRITE: last radius wins** (dup 946.349541 == last-only 946.349541, != first-only 991.415927) -- #633 | REJECT (nil) | SKIP (non-nil, area 465.097336 vs unfilleted 500.000000) | REJECT (nil) |
| fillet-variable | `filletedVariable(edgeIndex:radiusProfile:)` | N/A: scalar edgeIndex, no list | REJECT (nil) | REJECT (nil): single edge, no partial fillet possible | N/A: no list; `radiusProfile` needs >=2 points, stricter than `filletEvolving`'s >=1 |
| fillet-evolving | `filletEvolving(_:)` | **OVERWRITE: last law wins** (dup 946.349541 == last-only 946.349541), documented on `EvolvingFilletEdge`, same mechanism as #633 | REJECT (nil) | SKIP (non-nil, area 465.097336) -- **the #639 gap**: no count of how many were skipped | REJECT (nil) |
| fillet-edges (with history) | `filletedWithFullHistory(radius:edges:)` | N/A: uniform radius (dup 997.853982 == single-edge 997.853982) | REJECT (nil) | SKIP (non-nil, area 465.097336) | REJECT (nil) |
| fillet-evolving | `filletEvolving(_:)` | **OVERWRITE: last law wins** (dup 946.349541 == last-only 946.349541), documented on `EvolvingFilletEdge`, same mechanism as #633 | REJECT (nil) | SKIP (non-nil, area 465.097336) -- **#639 fixed**: `filletEvolvingWithReport(_:)` now reports `declinedEdgeIndices` | REJECT (nil) |
| fillet-edges (with history) | `filletedWithFullHistory(radius:edges:)` | N/A: uniform radius (dup 997.853982 == single-edge 997.853982) | REJECT (nil) | SKIP (non-nil, area 465.097336) -- **#639**: needed no new API; `history.record(of:)`'s `!isDeleted && generated.isEmpty` already names the declined set | REJECT (nil) |
| fillet-variable (with history) | `filletedWithFullHistory(edge:startRadius:endRadius:)` | N/A: scalar edge index, no list | REJECT (nil) | REJECT (nil): single edge | N/A: no list |
| blend (BiTgte_Blend) | `biTgteBlend(edgeIndices:radius:)` | N/A: uniform radius over a set; `BiTgte_Blend` keys edges into an `IndexedMap` (dup 1000.000000 == single 1000.000000) | REJECT (nil), fixed by #613 | **NO-OP** on a convex edge (non-nil, volume unchanged): structurally different from a skip-from-a-batch, see note below | REJECT (nil) |
| chamfer (two distances) | `chamferedTwoDistances(_:)` | **FIRST WINS** (dup 995.000000 == first-only 995.000000, != last-only 980.000000) -- the OPPOSITE of #633's last-wins | REJECT (nil) | REJECT (nil): single-edge batch, whole call fails | REJECT (nil) |
Expand All @@ -69,7 +69,7 @@ python3 Scripts/repro/cluster-b-fillet-edge-contract/classify_fillet_sites.py --
| offset-per-face | `offsetPerFace(defaultOffset:faceOffsets:)` | N/A: `faceOffsets` is a `Dictionary`, a duplicate key cannot be constructed | REJECT (nil), fixed by **#541** (see "Corrections" below) | N/A: `BRepOffset_MakeOffset` has no per-face decline analogous to `Add()` on a free-boundary edge | ACCEPTS (non-nil): applies `defaultOffset` uniformly, a legitimate no-override request |
| 2D fillet (face) | `fillet2D(vertexIndices:radii:)` | REJECT (nil) on a duplicated vertex index | REJECT (nil), fixed by #568 | UNMEASURED: no open/degenerate planar-face fixture built here | REJECT (nil), Swift-side guard |
| 2D chamfer (face) | `chamfer2D(edgePairs:distances:)` | REJECT (nil), order-independent, fixed by #705 (was **CRASH (SIGSEGV, uncatchable)**, see "New findings" below) | REJECT (nil), fixed by #568 | UNMEASURED, same reason as `fillet2D` | REJECT (nil), Swift-side guard |
| fillet (class API) | `FilletBuilder.addEdge(_:radius:)` | **OVERWRITE: last radius wins**, same mechanism as #633, unaudited by #489/#520/#568 | N/A: takes an `Edge`, not an index | foreign edge (a different `Shape` entirely): `addEdge` returns `true`, `build()` REJECT (nil) | `build()` with zero `addEdge` calls: REJECT (nil) |
| fillet (class API) | `FilletBuilder.addEdge(_:radius:)` | **OVERWRITE: last radius wins**, same mechanism as #633, unaudited by #489/#520/#568 | N/A: takes an `Edge`, not an index | foreign edge (a different `Shape` entirely): `addEdge` returns `true`, `contour(for:) == 0`, `build()` REJECT (nil) -- **#639 correction**: `contour(for:)` already reports the decline per edge | `build()` with zero `addEdge` calls: REJECT (nil) |
| chamfer (class API) | `ChamferBuilder.addEdge(_:distance:)` | **FIRST WINS**, matches `chamferedTwoDistances` -- `addEdge` itself prefers the first call, not just the bridge's hand-rolled loop | N/A: takes an `Edge`, not an index | foreign edge: `addEdge` returns `true`, `build()` REJECT (nil) | `build()` with zero `addEdge` calls: REJECT (nil) |

Full command output (with the `note` column this table drops for width) is reproduced exactly by
Expand Down Expand Up @@ -123,10 +123,15 @@ family, not just deciding to reject/dedupe/document.
of any kind.** No `occtUseSubShapesByIndex`, no `occtValidFilletRadius`. A foreign edge (one
belonging to an entirely different `Shape`) is silently accepted by `addEdge` (returns `true`)
and produces no built result (`build()` returns `nil`) with no signal as to why. This is a
second, independent access path into the same OCCT builder classes the free functions wrap, and
it inherits the #639 declined-edge-reporting gap on its own terms: there is no per-edge report
at all, only `addEdge`'s own `Bool` return, which per the above is `true` even for a foreign
edge that changes nothing.
second, independent access path into the same OCCT builder classes the free functions wrap.
**Correction (#639): the claim that "there is no per-edge report at all" was wrong, and #639
found it by trying the query this census never tried.** `FilletBuilder.contour(for:)` --
present since before this census, unrelated to #639 -- already answers exactly this: `Contour(E)
== 0` for the foreign edge above (measured directly: `addEdge` returns `true`,
`contour(for: foreignEdge)` returns `0`, `build()` returns `nil`), the identical signal it
reports for a same-shape edge OCCT declines on geometric grounds. Neither this census nor #639's
own issue text noticed the query already existed; #639 documents the recipe rather than adding
new bridge code for this class.
3. **`offsetPerFace`'s reject-not-skip fix is attributed to #541 in the bridge's own comment, not
#568.** See "Corrections" below.

Expand Down
25 changes: 22 additions & 3 deletions Sources/OCCTBridge/include/OCCTBridge_Modeling.h
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,18 @@ OCCTShapeRef OCCTDrawingGetEdges(OCCTDrawingRef drawing, OCCTEdgeType edgeType);
/// the whole call, #520)
/// @param edgeCount Number of edges to fillet
/// @param radius Fillet radius; must be > 0, or the call fails without touching OCCT
/// @param declinedEdgeIndices Optional (may be NULL): filled with the 0-based indices, from
/// `edgeIndices`, that OCCT declined to fillet (#639) -- a free-boundary edge, e.g. Must have
/// capacity >= edgeCount when non-NULL.
/// @param outDeclinedCount Optional (may be NULL): set to the number of entries written to
/// `declinedEdgeIndices`. **Read it only when the returned shape is non-NULL.** It is zeroed on
/// entry, so an early failure leaves 0, but a Build() failure returns NULL with the count already
/// written, and that count describes a shape the caller never receives.
/// @return Filleted shape, or NULL on failure
OCCTShapeRef OCCTShapeFilletEdges(OCCTShapeRef shape, const int32_t* edgeIndices,
int32_t edgeCount, double radius);
int32_t edgeCount, double radius,
int32_t* _Nullable declinedEdgeIndices,
int32_t* _Nullable outDeclinedCount);

/// Fillet specific edges with linear radius interpolation
///
Expand All @@ -285,9 +294,14 @@ OCCTShapeRef OCCTShapeFilletEdges(OCCTShapeRef shape, const int32_t* edgeIndices
/// @param edgeCount Number of edges to fillet
/// @param startRadius Radius at the start of each named edge's law; must be > 0
/// @param endRadius Radius at the end of each named edge's law; must be > 0
/// @param declinedEdgeIndices Optional (may be NULL): same #639 reporting contract as
/// OCCTShapeFilletEdges.
/// @param outDeclinedCount Optional (may be NULL): same contract as OCCTShapeFilletEdges.
/// @return Filleted shape, or NULL on failure
OCCTShapeRef OCCTShapeFilletEdgesLinear(OCCTShapeRef shape, const int32_t* edgeIndices,
int32_t edgeCount, double startRadius, double endRadius);
int32_t edgeCount, double startRadius, double endRadius,
int32_t* _Nullable declinedEdgeIndices,
int32_t* _Nullable outDeclinedCount);

/// Add draft angle to faces for mold release
/// @param shape The shape to draft
Expand Down Expand Up @@ -1354,11 +1368,16 @@ typedef struct {
/// every radius must be > 0, and each edge's parameters must lie in [0, 1] and strictly increase
/// @param pointCounts Array of how many radius points per edge; fewer than 1 for any edge rejects
/// the call, since a contour with no radius SIGSEGVs in Build()
/// @param declinedEdgeIndices Optional (may be NULL): same #639 reporting contract as
/// OCCTShapeFilletEdges.
/// @param outDeclinedCount Optional (may be NULL): same contract as OCCTShapeFilletEdges.
/// @return Filleted shape, or NULL on failure
OCCTShapeRef OCCTShapeFilletEvolving(OCCTShapeRef shape,
const int32_t* edgeIndices, int32_t edgeCount,
const OCCTFilletRadiusPoint* radiusPoints,
const int32_t* pointCounts);
const int32_t* pointCounts,
int32_t* _Nullable declinedEdgeIndices,
int32_t* _Nullable outDeclinedCount);

// MARK: - Per-Face Variable Offset (v0.38.0)

Expand Down
Loading