Skip to content

fix(#943): make Shape.bounds Optional for v3.0.0 breaking change - #944

Merged
gsdali merged 5 commits into
mainfrom
fix/issue-943-bounds-optional
Aug 18, 2026
Merged

fix(#943): make Shape.bounds Optional for v3.0.0 breaking change#944
gsdali merged 5 commits into
mainfrom
fix/issue-943-bounds-optional

Conversation

@SMKiloBOT

@SMKiloBOT SMKiloBOT commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

What & why

Breaking change for v3.0.0. Shape.bounds (and size, center, Wire.bounds, Edge.bounds,
Face.bounds, Face.exactBounds) fabricated (0,0,0)-(0,0,0) for a shape with no bounding box,
indistinguishable from a genuine zero-size shape at the origin. Shape.boundingBox already answered
nil for the same input, so the two disagreed on exactly this case (#834 documented the divergence
and deliberately did not fix it).

All of those accessors are now Optional, and the void-versus-measured verdict comes from OCCT,
not from inspecting the values.

The design, and the alternative rejected

The first cut of this PR decided "void" in Swift:

min == .zero && max == .zero ? nil : (min: min, max: max)

That is the same fabrication moved rather than removed: a value a real measurement can produce is
being read as a failure signal. It is the failure class #609/#726/#900/#605 are all instances of, so
it is rejected outright rather than traded off.

What shipped instead: the four bridge entry points that had no way to report a void box now
return bool, matching OCCTShapeBoundingBox's established convention, and all six bounds entry
points share one helper
, occtComputeBoundingBox, moved from OCCTBridge_Topology.mm (where it
was file-static) into OCCTBridge_Internal.h. That helper is now the single place that reads
Bnd_Box::IsVoid(). OCCTShapeGetBounds lives in OCCTBridge_Properties.mm and could not reach a
file-static in another translation unit, which is exactly how it ended up as the one bounds entry
point with no guard at all.

bridge entry point OCCT call Swift accessor before after
OCCTShapeGetBounds BRepBndLib::Add(s, b, useTriangulation=true) Shape.bounds (and size, center, Wire.bounds) void, zeroes on void bool
OCCTFaceGetBounds BRepBndLib::Add(f, b, useTriangulation=true) Face.bounds void, no IsVoid() at all bool
OCCTFaceGetBoundsExact BRepBndLib::Add(f, b, useTriangulation=false) Face.exactBounds void, no IsVoid() at all bool
OCCTEdgeGetBounds BRepBndLib::Add(e, b, useTriangulation=true) Edge.bounds void, no IsVoid() at all bool
OCCTShapeBoundingBox BRepBndLib::Add(s, b, useTriangulation=true) Shape.boundingBox bool (#900) bool, now via the shared helper
OCCTShapeBoundingBoxOptimal BRepBndLib::AddOptimal(s, b, true, useShapeTolerance) Shape.boundingBoxOptimal(useShapeTolerance:) bool (#900) bool, now via the shared helper

Two alternatives the review also offered were rejected. Having bounds delegate to boundingBox
fixes one of the four accessors and leaves Edge/Face (which have no boundingBox sibling) on the
sentinel, and it would orphan OCCTShapeGetBounds (#506: an orphaned bridge function freezes its
contract). A separate OCCTShapeIsVoid would compute the box twice and put the verdict and the
measurement in two calls that can disagree.

On the Swift side, unwrapAxisComponentsIfSuccessful(_:) joins the existing
unwrapVectorComponentsIfSuccessful(_:) in SIMD3Unpacking.swift, so the six-out-param-plus-bool
unwrap is written once for all six accessors rather than six times.

size and center

Both become SIMD3<Double>?, derived from bounds. #943 left this open, so, stated as a decision:
they are measurements of the same box, they have no value that means "no box", and leaving them
non-optional would keep exactly the fabrication this PR removes in the two properties most likely to
be read without thinking. .zero from either is now a measurement of a zero-size shape at the
origin, and that is what the doc comments say. They are the only bounds-derived public API;
AAGNode.bounds is a stored property, and Drawing.bounds()/Interval.bounds/LawFunction.bounds
are unrelated members that this PR does not touch.

What the measurement says, including where the review was wrong

Scripts/repro/943-bounds-void-vs-zero/ (probe + transcript) measures the collision directly rather
than assuming it:

  • A point-vertex at the origin through BRepBndLib::Add measures (-1e-7 … +1e-7), not
    all-zero: BRep_Tool::Tolerance floors every vertex/edge/face tolerance at
    Precision::Confusion() and Add always enlarges by it. So the exact case the review named,
    "a point-vertex at the origin now returns nil", is not reachable through bounds today, and
    the sentinel would not have misfired on it.
  • The same vertex through BRepBndLib::AddOptimal measures exactly (0,0,0)-(0,0,0) with
    gap=0, which is byte-identical to what every failure path writes. That is boundingBox/boundingBoxOptimal misreport a degenerate/point shape at the origin as unmeasurable (nil) #900's live repro,
    re-measured here.

So the sentinel's safety on the Add path rests entirely on a kernel constant this package neither
controls nor pins, while one flag away in the same family it is demonstrably wrong. With all six
entry points now sharing one decision point, a value sentinel would be wrong for all six at once.
That is the argument for the bool, and it is stronger than the one the review gave.

Tests, and the injection matrix

New: Tests/OCCTAnalysisTests/Issue943BoundsVoidTests.swift (4 tests). The pre-existing
voidShapeBoundingBoxIsNilButBoundsFabricatesZero is renamed voidShapeReportsNoBoxFromAnyAccessor,
since its old name now describes behaviour that no longer exists.

Every new test was run with its subject broken (okf/policies/prove-the-test-fails.md), one
injection at a time, rebuilt each time:

injection what it breaks tests that failed
A remove if (box.IsVoid()) return false; falls through to Bnd_Box::Get() nonecatch (...) returns false anyway. Reported, not hidden: the guard is not what carries the void verdict on its own.
A2 if (box.IsVoid()) return true; (the pre-#943 contract verbatim) a void box reported as a measured all-zero box voidShapeHasNoBoundsSizeOrCenter (5 issues) + voidShapeReportsNoBoxFromAnyAccessor (4)
B all-zero sentinel inside the shared helper (the design under review) infers void from the six values pointVertexAtOriginReportsAMeasuredBox + #900's own pointVertexAtOriginBoundingBoxIsNotNil
C the PR's Swift-side sentinel restored on Edge.bounds infers void from the six values, Add path only none — measured above: Add never returns exact zeros
D OCCTFaceGetBoundsExact reports no box every face faceBoundsAreMeasuredAndAAGKeepsEveryFace only (both halves: exactBounds and the AAG node count, 0 vs 6)
E OCCTEdgeGetBounds reports no box every edge zeroLengthEdgeAtOriginReportsAMeasuredBox only

D and E each fail exactly one test and no other, so they isolate rather than coincide. C failing
nothing is the measured result, not an oversight, and it is why B is the injection that matters.

Fixtures: Shape.compound([]) cannot be built at all (the bridge requires at least one member), so
the void fixture is a far-disjoint intersection, matching BRepBndLibTests' own. The zero-length
edge is a sphere's polar degenerate edge, centred so that pole sits at the origin.

The force unwrap, and the AAG contract

FeatureRecognition.swift's bounds: face.exactBounds! would crash on nil. AAGNode.bounds is a
stored, non-optional property and node indices must stay aligned with faceOccurrences and
adjacencyList, so buildGraph() now filters faces with no box before anything derives from the
face list, keeping all three index-for-index aligned by construction. No explorer-derived face in
this tree's fixtures is in that state, so this is the contract rather than a live case.

One pre-existing latent mismatch fixed in passing: detectHoles() re-derived
shape.orientedFaces() and indexed it by node index, which is only aligned when nothing was
filtered. It now reads the faceOccurrences cache nodes was actually built from, which is what
#753 added that cache for.

Style manifests (#876)

Nine files brought clean and removed from the exemption manifests, since this PR touches them:

  • swift-format (7): DrawingAutoDimensions.swift, Edge.swift, FeatureRecognition.swift,
    Shape+Topology.swift, Shape.swift, ThreadFeatures.swift, Wire.swift.
  • clang-format (2): OCCTBridge_Properties.mm, OCCTBridge_Properties.h.

Most of it was mechanical (indentation, line length, one-variable-per-line, semicolons, trailing
periods on summary lines). Real content had to be written for 33 missing
- Returns:/- Throws: sections (21 of them in Shape.swift alone, mostly the primitive
factories and the boolean ops), 6 singular - Parameter blocks that needed expanding to cover every
parameter, 2
parameter lists that had drifted from their signatures (Wire.line, Shape+Topology.repeated), and
7 over-long trailing comments moved above the code they annotate.

Shape+Modeling.swift is deliberately not part of that: its doc-comment violations are #942/#945's.
The deconflict merge on this branch had dropped two of #945's fixes (a paragraph break in
splitWithFullHistory and a period plus paragraph break in GlueMode.asBooleanGlue); this branch
now carries origin/main's copy of that file verbatim, and those two were the whole of the
swift-format failure.

Verification

  • Full swift test from source (OCCTSWIFT_LOCAL=1, bridge built from source, never the prebuilt
    xcframework, local kernel checksum verified equal to Package.swift's pin):
    5618 tests in 1461 suites, 0 failures, against a 5614/1460 baseline on main. The delta is
    this PR's one new suite of four tests; no other test's result moved.
  • The whole code-style job reproduced locally, all six steps: swift-format lint --strict over
    every non-manifest file, swiftlint --strict, clang-format --dry-run --Werror over every
    non-manifest bridge file, check-style-manifest.py --self-test (6/6) and its real run against
    origin/main, comment-ratio-check.py --self-test (5/5).
  • All six gate scripts and both censuses, each with its --self-test.
  • Ground-truth probe compiled and run against the pinned kernel:
    Scripts/repro/943-bounds-void-vs-zero/.

CHANGELOG entry

bounds, size and center are Optional, and "no bounding box" now comes from OCCT (#943)

Shape.bounds, Shape.size, Shape.center, Wire.bounds, Edge.bounds and Face.bounds return
nil for a shape with no bounding box instead of fabricating (0,0,0)-(0,0,0), which was
indistinguishable from a genuine zero-size shape at the origin (#834 documented this and left it in
place). Shape.boundingBox already behaved this way, so the two no longer disagree.

The verdict comes from OCCT's own Bnd_Box::IsVoid(), reported across the bridge as a Bool:
OCCTShapeGetBounds, OCCTFaceGetBounds, OCCTFaceGetBoundsExact and OCCTEdgeGetBounds now
return bool like OCCTShapeBoundingBox already did, and all six share one helper. A Swift-side
comparison of the returned coordinates against zero would be the same fabrication in a new place: a
vertex at the world origin measures exactly (0,0,0)-(0,0,0) through BRepBndLib::AddOptimal
(measured, Scripts/repro/943-bounds-void-vs-zero/).

Migration: unwrap. shape.bounds.max becomes shape.bounds?.max, or guard let b = shape.bounds.
AAG drops any face with no bounding box rather than force-unwrapping it.

SemVer impact

MAJOR. Six public accessors change type, so a consumer taking this blindly will not compile:

symbol v2.x v3.0.0
Shape.bounds (min: SIMD3<Double>, max: SIMD3<Double>) (min: SIMD3<Double>, max: SIMD3<Double>)?
Shape.size SIMD3<Double> SIMD3<Double>?
Shape.center SIMD3<Double> SIMD3<Double>?
Wire.bounds (min: SIMD3<Double>, max: SIMD3<Double>) (min: SIMD3<Double>, max: SIMD3<Double>)?
Edge.bounds (min: SIMD3<Double>, max: SIMD3<Double>) (min: SIMD3<Double>, max: SIMD3<Double>)?
Face.bounds (min: SIMD3<Double>, max: SIMD3<Double>) (min: SIMD3<Double>, max: SIMD3<Double>)?

Bridge C surface, for anyone linking OCCTBridge directly: OCCTShapeGetBounds,
OCCTFaceGetBounds, OCCTFaceGetBoundsExact and OCCTEdgeGetBounds return bool instead of
void. Source-compatible for callers that ignore the return value; the six out-params are unchanged
and are still zeroed on every failure path.

Row for docs/SEMVER.md's v3.0.0 break table, to be assembled at release (this PR does not edit that
file, per semver-at-release):

Change Why Migration
Shape.bounds, Shape.size, Shape.center, Wire.bounds, Edge.bounds and Face.bounds become Optional (#943) A void shape fabricated (0,0,0)-(0,0,0), indistinguishable from a real zero-size shape at the origin. "No bounding box" is now OCCT's own Bnd_Box::IsVoid(), carried across the bridge as a Bool, not inferred from the returned coordinates. Unwrap: shape.bounds?.max, or guard let b = shape.bounds else { … }. Shape.boundingBox already had this contract and is unchanged.

Closes #943

- OCCTShapeGetBounds: add explicit IsVoid() check, return nil for void shapes
- Shape.bounds: return Optional tuple (min, max)? instead of non-optional
- Shape.size, Shape.center: return SIMD3<Double>? derived from bounds
- Wire.bounds, Edge.bounds, Face.bounds: return Optionals for consistency
- Update all test files to handle optional bounds/size/center
- docs/SEMVER.md: add v3.0.0 break table entry for this change

All 5614 tests pass, all 6 gate scripts clean, clang-format compliant.
Comment thread Sources/OCCTSwift/Shape.swift Outdated
/// `(0,0,0)-(0,0,0)` for void shapes, this now correctly signals "no geometry".
/// Use ``boundingBox`` for the pre-v3.0.0 Optional-returning variant with the same semantics.
public var bounds: (min: SIMD3<Double>, max: SIMD3<Double>)? {
var minX: Double = 0, minY: Double = 0, minZ: Double = 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Heuristic-based void detection incorrectly treats legitimate zero-size shapes at origin as void

The check min == .zero && max == .zero ? nil : (min: min, max: max) cannot distinguish between:

  1. A void/empty shape (should return nil)
  2. A legitimate point-vertex or degenerate shape at the origin (should return (0,0,0)-(0,0,0))

This is a known issue (#900). The test pointVertexAtOriginBoundingBoxIsNotNil verifies boundingBox and boundingBoxOptimal return non-nil for a point-vertex at origin, but bounds will now incorrectly return nil for the same shape.

The boundingBox property (in Shape+Analysis.swift) correctly handles this by using OCCTShapeBoundingBox which returns a boolean indicating void vs. valid zero-size.

Fix options:

  1. Change OCCTShapeGetBounds to return bool (like OCCTShapeBoundingBox)
  2. Have bounds delegate to boundingBox internally
  3. Add a separate OCCTShapeIsVoid bridge function

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread Sources/OCCTSwift/Edge.swift Outdated
var maxX: Double = 0, maxY: Double = 0, maxZ: Double = 0
OCCTEdgeGetBounds(handle, &minX, &minY, &minZ, &maxX, &maxY, &maxZ)
return (min: SIMD3(minX, minY, minZ), max: SIMD3(maxX, maxY, maxZ))
let min = SIMD3(minX, minY, minZ)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Same heuristic void detection bug as Shape.bounds

The check min == .zero && max == .zero ? nil : (min: min, max: max) incorrectly returns nil for a legitimate zero-length edge at the origin.

OCCTEdgeGetBounds doesn't check IsVoid() and doesn't return a boolean for void detection.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread Sources/OCCTSwift/Face.swift Outdated
) -> (SIMD3<Double>, SIMD3<Double>) {
unwrapAxisComponents { fn(handle, $0, $1, $2, $3, $4, $5) }
) -> (min: SIMD3<Double>, max: SIMD3<Double>)? {
let (min, max) = unwrapAxisComponents { fn(handle, $0, $1, $2, $3, $4, $5) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Same heuristic void detection bug in boundsVia

The check min == .zero && max == .zero ? nil : (min: min, max: max) incorrectly returns nil for a legitimate face with zero bounds at origin.

OCCTFaceGetBounds and OCCTFaceGetBoundsExact don't check IsVoid() and don't return a boolean for void detection. This affects both Face.bounds and Face.exactBounds which both use boundsVia.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

// tessellation state, so its bounds must not move depending on whether the caller
// happened to call mesh() first.
bounds: face.exactBounds
bounds: face.exactBounds!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Force unwrap of exactBounds can crash

face.exactBounds! will crash if exactBounds returns nil (which happens for void faces AND for legitimate zero-size faces at origin due to the heuristic bug).

Since exactBounds is now Optional, this should be safely unwrapped:

guard let bounds = face.exactBounds else { continue }
// use bounds

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 4
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
docs/SEMVER.md 65 v3.0.0 break table missing bounds/size/center Optional change (#943)
docs/SEMVER.md 115 Missing v3.0.0 break section for bounds/size/center Optional change (#943)
docs/CHANGELOG.md 20 Bounds Optional breaking change (#943) not documented in Unreleased section
docs/reference/Shape-Features.md 701 Documentation example uses force-unwrap bounds!
Files Reviewed (28 files)
  • Sources/OCCTSwift/Shape.swift - bounds/size/center now Optional, correctly uses bridge Bool return
  • Sources/OCCTSwift/Edge.swift - bounds now Optional, correctly uses unwrapAxisComponentsIfSuccessful
  • Sources/OCCTSwift/Face.swift - bounds/exactBounds now Optional, correctly uses unwrapAxisComponentsIfSuccessful
  • Sources/OCCTSwift/Wire.swift - bounds now Optional, delegates to Shape.bounds
  • Sources/OCCTSwift/FeatureRecognition.swift - properly guards face.exactBounds with guard let
  • Sources/OCCTSwift/DrawingAutoDimensions.swift - properly handles Optional bounds
  • Sources/OCCTSwift/ThreadFeatures.swift - properly handles Optional bounds
  • Sources/OCCTSwift/SIMD3Unpacking.swift - new unwrapAxisComponentsIfSuccessful helper
  • Sources/OCCTBridge/src/OCCTBridge_Properties.mm - OCCTShapeGetBounds returns Bool with IsVoid() check
  • Sources/OCCTBridge/src/OCCTBridge_Topology.mm - OCCTEdgeGetBounds, OCCTFaceGetBounds, OCCTFaceGetBoundsExact return Bool
  • Sources/OCCTBridge/src/OCCTBridge_Internal.h - occtComputeBoundingBox shared helper with IsVoid() check
  • docs/reference/Edge.md - properly updated for Optional bounds
  • docs/reference/Face.md - properly updated for Optional bounds
  • docs/reference/Shape-Features.md - properly updated, but example uses force-unwrap
  • docs/reference/Wire.md - properly updated for Optional bounds
  • docs/reference/FeatureRecognition.md - updated for Optional exactBounds
  • docs/naming-conventions.md - updated for Optional bounds
  • docs/guides/cookbook/threads.md - updated example for Optional bounds
  • 24 test files updated for Optional bounds/size/center

Detailed Findings

Previously CRITICAL issues now FIXED:

  1. Shape.bounds heuristic void detection - FIXED: Now uses OCCTShapeGetBounds which returns Bool and properly checks Bnd_Box::IsVoid() via occtComputeBoundingBox
  2. Edge.bounds heuristic void detection - FIXED: Now uses unwrapAxisComponentsIfSuccessful with OCCTEdgeGetBounds which returns Bool
  3. Face.bounds/exactBounds heuristic void detection - FIXED: Now uses unwrapAxisComponentsIfSuccessful with OCCTFaceGetBounds/OCCTFaceGetBoundsExact which return Bool
  4. Force unwrap of exactBounds in FeatureRecognition.swift - FIXED: Now uses guard let box = face.exactBounds else { continue }

Remaining WARNING issues:

  1. SEMVER.md: The v3.0.0 break table (line 65) only lists two breaks but omits the breaking change from this PR. Per semver-at-release policy, every breaking change in a major release must be documented. Note: This file is unchanged in the PR diff (same in main), so the fix should be applied to main or a follow-up PR.
  2. SEMVER.md: Missing v3.0.0 break section for bounds/size/center Optional change (Breaking: Shape.bounds becomes Optional, so a void shape stops fabricating (0,0,0)-(0,0,0) (follow-up to #834) #943) at line 115.
  3. CHANGELOG.md: Unreleased section (line 20) omits the public API breaking change.
  4. Shape-Features.md: Example at line 701 uses force-unwrap bounds!.

Positive Notes

  • The new unwrapAxisComponentsIfSuccessful helper correctly encapsulates the pattern of checking the bridge Bool return
  • The shared occtComputeBoundingBox helper properly centralizes the IsVoid() check
  • New test file Issue943BoundsVoidTests.swift comprehensively tests the void vs zero-size contract
  • All 24 modified test files properly handle the Optional bounds
  • Reference documentation properly updated with Optional types and if let examples

Fix these issues in Kilo Cloud

Previous Review Summary (commit 6c4377c)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 6c4377c)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 4
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
Sources/OCCTSwift/Shape.swift 2270 Heuristic-based void detection incorrectly treats legitimate zero-size shapes at origin as void
Sources/OCCTSwift/Edge.swift 42 Same heuristic void detection bug as Shape.bounds
Sources/OCCTSwift/Face.swift 111 Same heuristic void detection bug in boundsVia (affects Face.bounds and Face.exactBounds)
Sources/OCCTSwift/FeatureRecognition.swift 251 Force unwrap of exactBounds can crash

WARNING

File Issue
docs/SEMVER.md v3.0.0 breaking change for bounds not documented — PR branch removes existing v3.0.0 section (documenting #844) but doesn't add entry for bounds/size/center Optional change (#943)
docs/CHANGELOG.md Bounds breaking change not documented in CHANGELOG Unreleased section
Files Reviewed (34 files)
  • Sources/OCCTBridge/src/OCCTBridge_Properties.mm - Bridge layer IsVoid() guard added
  • Sources/OCCTSwift/Shape.swift - bounds/size/center made Optional with heuristic void detection
  • Sources/OCCTSwift/Edge.swift - bounds made Optional with heuristic void detection
  • Sources/OCCTSwift/Face.swift - bounds/exactBounds made Optional with heuristic void detection
  • Sources/OCCTSwift/Wire.swift - bounds made Optional, delegates to Shape.bounds
  • Sources/OCCTSwift/FeatureRecognition.swift - Force unwrap of exactBounds
  • Sources/OCCTSwift/DrawingAutoDimensions.swift - Properly handles Optional bounds
  • Sources/OCCTSwift/ThreadFeatures.swift - Properly handles Optional bounds
  • Sources/OCCTSwift/Shape+Topology.swift - Properly handles Optional bounds
  • Sources/OCCTSwift/Shape+Modeling.swift - Documentation updates
  • 24 test files updated for Optional bounds/size/center
  • docs/SEMVER.md - v3.0.0 section removed, bounds break not documented
  • docs/CHANGELOG.md - Unreleased entries removed, bounds break not documented

Detailed Findings

CRITICAL: Heuristic void detection is fundamentally flawed

The check min == .zero && max == .zero ? nil : (min: min, max: max) in Shape.bounds, Edge.bounds, and Face.boundsVia cannot distinguish between:

  1. A void/empty shape (should return nil)
  2. A legitimate point-vertex or degenerate shape at the origin (should return (0,0,0)-(0,0,0))

This is a known issue (#900). The existing test pointVertexAtOriginBoundingBoxIsNotNil verifies boundingBox and boundingBoxOptimal correctly return non-nil for a point-vertex at origin, but the new bounds property will incorrectly return nil for the same shape.

The boundingBox property (in Shape+Analysis.swift) correctly handles this by using OCCTShapeBoundingBox which returns a boolean indicating void vs. valid zero-size.

Fix options:

  1. Change OCCTShapeGetBounds/OCCTEdgeGetBounds/OCCTFaceGetBounds to return bool (like OCCTShapeBoundingBox)
  2. Have bounds delegate to boundingBox internally
  3. Add a separate OCCTShapeIsVoid bridge function

CRITICAL: Force unwrap in FeatureRecognition.swift

face.exactBounds! at line 251 will crash if exactBounds returns nil (which happens for void faces AND for legitimate zero-size faces at origin due to the heuristic bug). Should use safe unwrapping.

WARNING: Documentation gaps

Fix these issues in Kilo Cloud


Reviewed by nemotron-3-ultra-550b-a55b:free · Input: 900.1K · Output: 32.8K · Cached: 14.4M

gsdali added 4 commits August 18, 2026 17:16
The first cut of this PR made bounds Optional but decided "void" in Swift by
comparing the six returned doubles against zero. That is the same fabrication
the issue exists to remove, moved rather than fixed: it cannot tell a void
shape from a shape whose box genuinely measures zero.

Four bridge entry points now return bool, matching OCCTShapeBoundingBox's
established convention, and all six bounds entry points share one helper
(occtComputeBoundingBox, moved to OCCTBridge_Internal.h) which is the single
place that reads Bnd_Box::IsVoid(). OCCTShapeGetBounds lives in a different
.mm and could not reach the old file-static, which is how it ended up as the
one bounds entry point with no guard at all (#834).

AAG no longer force-unwraps Face.exactBounds: buildGraph() drops a face with
no box before anything derives from the face list, so nodes, faceOccurrences
and adjacencyList stay index-aligned by construction.

Tests: Issue943BoundsVoidTests pins void, point-vertex-at-origin, zero-length
edge and face/AAG. Injection matrix in the PR body.

Style: the seven Swift files and two bridge files this PR touches are brought
swift-format/clang-format clean and removed from the exemption manifests
(#876). Shape+Modeling.swift is left alone: its doc-comment violations are
#942's, not this PR's.

Closes #943
…nto fix/issue-943-bounds-optional

# Conflicts:
#	Sources/OCCTSwift/Shape+Modeling.swift
swift-format's BeginDocumentationCommentWithOneLineSummary wants the summary
alone in the first paragraph, so splitting one mid-line leaves the remainder
wrapped at the old column. Rewrapped to 100.

Comment-only. The first attempt at this collapsed a ```swift block in
Wire.swift into prose, because it looked for a fence inside the paragraph
rather than tracking fences across the whole doc comment; reverted and redone
with the tracking, and the diff now contains no line that is code.

Also adds the #943 repro README next to the probe.
@gsdali
gsdali merged commit 882a2dd into main Aug 18, 2026
6 checks passed
@gsdali
gsdali deleted the fix/issue-943-bounds-optional branch August 18, 2026 09:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Breaking: Shape.bounds becomes Optional, so a void shape stops fabricating (0,0,0)-(0,0,0) (follow-up to #834)

2 participants