Changelog
All notable changes to OCCTSwiftViewport are documented in this file.
[1.2.1] - 2026-09-19
Fixed
- The near clip plane is now measured to the scene’s bounding box, not its bounding sphere (issue #116).
CameraState.clipPlanes(sceneBounds:)derivednearfromcameraDistance - sphereRadius. For any scene that is not roughly cubic, the circumscribed sphere is much larger than the geometry, so a camera clear of the model could still be inside the sphere. That made the expression negative,nearcollapsed to thefar * 1e-4floor, and roughly four orders of magnitude of depth precision went with it, showing up as section-view artifacts on flat or elongated models.nearis nowBoundingBox.distance(to: cameraPosition). On a 20 x 20 x 2 slab viewed from 5 units out,neargoes from the 0.0038 floor to the true 4.0, and thefar/nearratio from 10,000 to about 9.6.- Behaviour is unchanged when the camera is genuinely inside the geometry: the distance to the box is
0there, so the floor still applies, and unchanged fornilbounds.
- Zoom-at-cursor no longer drifts the pivot when dynamic pivoting is turned off (issue #117).
CameraController.zoomToward(factor:cursorNormalized:aspectRatio:)always shifted the pivot toward the cursor, so apps that setdynamicPivotConfiguration.isEnabled = falsestill had their orbit centre walk across the model as the user scrolled. The zoom itself is unaffected; only the pivot shift is gated.
Added
BoundingBox.distance(to:), the minimum distance from a point to the box, returning0for a point inside or on it. Extracted for the clip-plane fix above and useful on its own wherever a sphere approximation is too coarse.CameraController.zoomTowardShiftsPivot(Bool, defaults totrue, so existing behaviour is the default).ViewportControllerkeeps it in step withViewportConfiguration.dynamicPivotConfiguration.isEnabled.
Changed
- CI installs the Metal toolchain on Xcode 27+ runners, where it is no longer present by default and shader compilation failed with
cannot execute tool 'metal'(issue #120). The step is guarded onxcodebuild -downloadComponentexisting, since the macos-15 runner’s Xcode predates it. okf/policies/context-first.mdis resynced with the ecosystem canonical. The per-class OCCT reference manual is indexed asocct-refman; this copy said it was not, which sent agents to the bundled headers or toWebFetchas a first resort.
Tests
- 228 tests in 40 suites, up from 217 in 39. The two fixes above merged without test coverage; this release backfills it rather than tagging over the gap.
BoundingBoxTests: five cases fordistance(to:)(inside, on the boundary, off one face, off a corner, and the slab case that distinguishes box distance from sphere distance).ClipPlaneTests: two cases pinning #116. The camera-inside-the-sphere case was verified to fail against the pre-fix expression and pass after it; the pre-existing “camera inside the scene” test passed either way, which is why the regression was not caught.CameraControllerZoomTests(new suite): four cases covering the pivot shift on and off, a centred cursor, and theViewportControllersync.
Documentation
README.md: the install snippet resolved from a../OCCTSwiftViewportsibling path, which the ecosystem standard reversed on 2026-08-20 in favour of URL-only resolution (a path dependency is never version-checked and drops the pin fromPackage.resolved). It now resolves from the published URL, pinned at1.2.1rather than the stale1.1.23. Test counts corrected from “37 tests across 9 suites”.docs/reference/Math.md:BoundingBox.distance(to:)documented.docs/reference/Camera.md:clipPlanes(sceneBounds:)prose corrected from sphere to box,zoomTowardShiftsPivotadded to the configuration table, andzoomTowarddocuments the gate.
Note on versioning
Tags and changelog headings diverged before this release. Sections [1.1.28] through [1.1.31] were never tagged individually: they all shipped in the v1.2.0 tag of 2026-08-19, which has no heading of its own. [1.2.1] corresponds to the v1.2.1 tag, and headings track tags from here.
[1.1.31] - 2026-08-19
Changed
- The code-style exemption manifest is now empty, so the style gate covers the whole of
Sources/OCCTSwiftViewportunconditionally (issue #113, completing the #95 rollout).scripts/style-manifest-swift.txtwas a gradual-rollout exemption list that only ever shrinks; v1.1.30 discharged 2 of its 48 entries and this release discharges the remaining 46. Nothing is grandfathered back onto it: new code complies from creation.- No behaviour change, and no API change. The 46 files were run through
swift-format format, which cleared 887 of their 954--strictdiagnostics. Every remaining code-token difference comes from one of five mechanical rules (DoNotUseSemicolons,TrailingComma,OneVariableDeclarationPerLine,UseLetInEveryBoundCaseVariable,OrderedImports), verified by stripping comments and whitespace and diffing what was left. The test suite is unchanged in count and result. - 65 hand-written doc-comment summaries, which
swift-formatcannot generate. Several record behaviour that was previously undocumented:ViewportBody.vertexColorsfalls back to the body colour on a length mismatch as well as when empty;ViewportBody.usesDirectMeshquietly takes the interleaved path when only half-populated;PickResult.init?has three distinct failure cases, not the two its old- Returns:tag listed; andNavigationCube.region(at:)also returnsnilfor a widget too small for its own padding. PBRMaterial.presetsdocumented its keys as “lowercase identifiers”; they are lowerCamelCase (brushedAluminum,chromedSteel). The doc now matches the table.
- No behaviour change, and no API change. The 46 files were run through
[1.1.30] - 2026-08-19
Changed
SelectionFilteris nowPickResultFilter(issue #111, phase 1 of the picking consolidation inecosystem#43). The name was defined twice in the stack: thisstruct, and OCCTSwiftAIS’s unrelatedSelectionFilterprotocol. They are not duplicates and have not been merged. This one runs before any topology exists, on a decodedPickResultcarrying only a body id, a kind and a primitive index; the AIS one runs after resolution and reasons about the actual face or edge. Only the shared name was the problem, since it read as though one implemented the other, and it was a genuine ambiguity for any file importing both modules.- No API break.
SelectionFilterremains as@available(*, deprecated, renamed: "PickResultFilter") public typealias, as does the underscored re-export_SelectionFilter(now aliasingPickResultFilterdirectly, alongside the new_PickResultFilter). Every static factory (all,nothing,kind(_:),kinds(_:),faces,edges,vertices,layer(_:),bodyIDs(_:),excludingBodyIDs(_:),bodyIndices(_:)) and combinator (and(_:),or(_:),negated,all(of:),any(of:)) keeps its name and now vends aPickResultFilter. ViewportController.selectionFilterkeeps its name and is now typedPickResultFilter?. The property describes the role (it gates the user-geometry selection stream, and widget-layer picks bypass it); the type describes the value. The internalViewportController.applySelectionFilter(_:filter:)keeps its name for the same reason, since it applies that property’s semantics.SelectionFilterTestsis nowPickResultFilterTests, plus one new test pinning that the deprecated spelling still resolves. 217 tests total.Picking/SelectionFilter.swiftandViews/ViewportController.swiftwere brought toswift-format --strictcompliance and removed fromscripts/style-manifest-swift.txtperokf/policies/code-style.md.
- No API break.
[1.1.29] - 2026-08-16
Added
ViewportRenderercan now render off-screen, so the live renderer’s own output is regression-testable (issue #103, follow-up to the verification gap in #101/#102). Its draw path was previously reachable only throughMTKViewDelegate.draw(in:)against a liveCAMetalDrawable, so the actual frame the viewport produces — SSAO, silhouettes, shadows, TAA and the GPU pick pass all engaged — could only be checked by eye on a device.draw(in:)and the new internalrenderHeadless(...)are now both thin wrappers over oneencodeFrame(into:)parameterized over its render targets, so the off-screen path runs every pass the live path runs rather than being a parallel, smaller reimplementation (which is exactly the duplication #101 removed elsewhere). Off-screen targets are built to the same specificationMetalViewRepresentableconfigures on the live view: BGRA8 colour,depth32Float_stencil8depth/stencil, matching MSAA sample count.- No public API added or changed. The new surface is
internal, reachable from the test target via@testable import; the liveMTKViewpath is byte-identical apart from reading its targets from a struct instead of the view. - Distinct from
OffscreenRenderer, which remains a separate, deliberately smaller headless pipeline (no SSAO, silhouettes, TAA or pick pass) forCGImage/PNG output.
- No public API added or changed. The new surface is
- Permanent differential-render harness (
ViewportRendererHeadlessTests, 7 tests): a fixed battery of 20 deterministic scenes — shaded, shaded with edges, wireframe, unlit, transparency, direct-mesh, point cloud, shadows, axes, four grid distances, ortho + pan, flat lighting, plus the SSAO-only, silhouettes-only, picking-on, selection-outline and TAA-on variants the transient harness in #102 could not cover — hashed with SHA-256 over the raw BGRA readback. SettingVIEWPORT_HEADLESS_DUMP_DIRwriteshashes.jsonplus one raw buffer per scene, so any two branches can be compared pixel-for-pixel. 216 tests total.- The pick texture is populated by an off-screen frame, so
performRegionPick(rect:)is now covered end to end rather than only through its pure clamp/decode helpers. - A negative control pins that the live grid pass really does respond to
ViewportConfiguration.gridSubdivisions(it diverges at camera distances 0.5, 3 and 200, and happens to agree at 20 — the same three-of-four split #101 measured for the headless copy), so “grid unchanged” is a real result rather than a vacuous one.
- The pick texture is populated by an off-screen frame, so
[1.1.28] - 2026-08-16
Fixed
- Headless renders drew the ground grid at different spacing levels than the live viewport (issue #101).
OffscreenRenderer’s copy of the adaptive grid-spacing math hardcoded a subdivision factor of 5 whileViewportRendererreadsViewportConfiguration.gridSubdivisions, which has defaulted to 10 since the knob was added — so the two snapped to different spacing levels at the same camera distance (visible at, for example, distances 0.5, 3 and 200; some distances happened to agree). The hardcoded copy predates nothing: the config-driven version was already in place when the headless renderer was written, so this was a transcription slip, not a deliberate headless-specific choice. Both renderers now call one config-driven function, andOffscreenRenderOptionsgainsgridBaseSpacing(default1.0) andgridSubdivisions(default10) so headless output matches the live viewport’s defaults and stays adjustable. Only affects renders withshowGrid = true(off by default). - Removed unconditional debug scaffolding left over from an April-2026 tessellation debugging session (issue #101).
ViewportRendererwrote arenderer_diag.txtfile into the consuming app’s Documents directory during pipeline setup and buffer building — an unrequested file write into every consumer’s sandbox, in release builds too. That write is gone entirely (not merely#if DEBUG-gated). The renderer’s diagnosticNSLogcalls are now#if DEBUG-only, and the one labelled “one-time” (WARNING: %d bodies have no tessellation data) finally has an actual per-instance dedup guard instead of being able to fire on every frame.
Changed
- Deduplicated the Metal setup
ViewportRendererandOffscreenRendererhand-maintained in parallel (issue #101, follow-up from the #92 duplication review). ~290 near-verbatim lines — pipeline-descriptor construction for the shaded / direct-mesh / wireframe / grid / axis / visible-point / shadow pipelines, the shadow-frustum light view-projection, the adaptive grid spacing, the per-body vertex / edge / point buffer uploads, the procedural matcap texture, the axis vertex buffer, and theUniforms/LightDataSwiftpacking — now live in internalRendererSharedSetup,RendererSharedBuffers, andUniforms+Scenehelpers that both renderers call. The CHANGELOG shows the same fix being hand-applied to both files across #26, #28, #53, #55, #57 and #83; the grid-spacing divergence above is what that tax eventually cost.- No public API removed or changed; the only public-surface delta is the two additive, defaulted
OffscreenRenderOptionsgrid fields above. - Verified pixel-identical: a headless differential harness hashed twelve scenes (shaded, shaded with edges, wireframe, unlit, transparency, direct-mesh, point clouds with per-point colours, shadows, axes, explicit ortho bounds + pixel pan, flat lighting) before and after the extraction. Every hash matched except the grid scenes, which changed exactly as the grid-spacing fix predicts.
RendererSharedSetupTests(12) pin each extracted pure function against the implementation it replaced, including both sides of the grid-spacing divergence. 208 tests total.
- No public API removed or changed; the only public-surface delta is the two additive, defaulted
ViewportRenderer.swiftandOffscreenRenderer.swiftare now fullyswift-format/SwiftLint compliant and have been removed fromscripts/style-manifest-swift.txt(49 files remain), per the code-style policy’s “fix what you touch” rule.
[1.1.27] - 2026-08-14
Fixed
CameraState.lookAt(target:from:)placed the camera at the reflection of the requested eye position through the target, not at the requested position itself (issue #98). This is the function behind an explicitrender-preview --camera-position(OCCTSwiftScripts); namedStandardViewpresets (.isometric,.top,.front,.right) do not go throughlookAtand were unaffected. Reported downstream as: requesting camera position(0, 0, 200)against an origin target rendered the -Z view instead of the +Z view, and was worked around by passing2*target - requestedPositionin, reflecting the request back before it went in so the double reflection cancelled out.- Root cause:
CameraState.position/viewDirection/upVectorexpectrotationto be a camera-to-world orientation quaternion, but the privatequaternionLookAthelper built its rotation matrix in the world-to-camera (view-matrix) convention instead:rightusedcross(up, forward)rather thancross(forward, up),correctedUpusedcross(forward, right)rather thancross(right, forward), and the forward row was stored unnegated. Confirmed as reflect-through-target specifically (not negate-the-point) using a non-origin target, since an origin target can’t tell the two failure modes apart (2*origin - P == -P). - New regression tests:
lookAt places the eye at the requested point, not its reflection(the disambiguating non-origin-target case) andlookAt is correct for an off-axis, non-origin target. 198 tests total.
- Root cause:
[1.1.26] - 2026-07-20
Added
- Batched/region GPU pick readback (issue #90).
ViewportRenderer.performRegionPick(rect:completion:)blits an arbitrary screen-space rectangle from the pick texture in one GPU round trip and returns the de-duplicated, occlusion-aware set of primitives it touches — instead of the only prior option, oneperformPick(at:)call per pixel. Filed from OCCTSwiftAIS#33’s rectangle/lasso selection, which had shipped a CPU-side vertex-projection workaround (no occlusion handling, vertex-only approximation for curved edges/face interiors) because there was no batched entry point.- New public surface on
ViewportController— not just the renderer. The renderer instance driving the live viewport was previously unreachable from outsideMetalViewportView(a private@State), soperformPick/a hypothetical renderer-onlyperformRegionPickhad no way to be called by an external consumer against the actual on-screen scene.ViewportRenderer.initnow registers itself on its controller (ViewportController.attachedRenderer, weak, internal); the controller exposes:performRegionPick(pixelRect:completion:)— pull-based (does not touchpickResult/onPick); honoursselectionFilterwith the same widget-bypass semantics ashandlePick(result:)(widget-layer picks pass through unfiltered, a user-geometry pick failing the filter is dropped).drawablePixelSize— the attached renderer’s drawable pixel size (.zerobefore the first frame), for converting a consumer’s own drag/lasso rectangle from SwiftUI view points into the pixel spaceperformRegionPickexpects.
- Each row of the GPU→CPU blit is padded to a 256-byte-aligned stride (Metal’s texture→buffer blit convention) and de-strided on readback — not assumed tightly packed — so a narrow region can’t read garbage past its first row.
- The region readback buffer grows on demand and is reused across calls (no per-pick allocation once warmed up to the largest rectangle requested so far).
RegionPickTests(24): pure-function coverage for rectangle clamping, row-stride alignment, raw-value decode/dedup (repeat pixels, sentinel exclusion, unknown object indices, first-appearance ordering),SelectionFiltercomposition, and theViewportController↔ViewportRendererattachment wiring — all headlessly verifiable without a live draw. The GPU blit/pixel path itself follows the project’s existing convention (see the direct-mesh pick pass inDirectMeshRenderingTests) of being live-verified rather than headlessly pixel-tested, since the pick texture is only populated by an actual frame. 194 tests total.- Source-compatible; no existing API changed.
- New public surface on
[1.1.25] — 2026-07-20
Fixed
swift build -c releasefailed to type-checkMetalViewportView.metalView(issue #91). The computed property built its#if os(macOS)/#elsebranches, two inferred-closure-parameter trailing closures (onScrollWheel,onMouseDown), and theColor(...)fallback branch as oneViewBuilderexpression — under-c release’s optimizer the type checker couldn’t resolve it in reasonable time, failing the build entirely (debug builds were unaffected). Reported via a downstream consumer’s CI (OCCTSwiftScripts#79) resolving this package transitively.- Split into
macOSMetalView(renderer:)/otherPlatformMetalView(renderer:)@ViewBuildermethods (one compiled per platform) plus a standalonemetalViewFallbackproperty, so the type checker resolves each piece independently instead of the whole tree at once.onScrollWheel/onMouseDownare now typed local closures rather than inferred trailing-closure literals. - No behaviour or public API change. Verified with a clean
swift build -c release(was failing, now completes) and the fullswift testsuite (170 tests, unaffected).
- Split into
[1.1.23] — 2026-07-16
Added
- Direct-mesh rendering path —
ViewportBody.directMesh(id:positions:normals:indices:color:faceIndices:edges:material:transform:)builds a body from de-interleaved position and normal arrays, uploaded as two separate GPU buffers (position @ buffer 0, normal @ buffer 2, stride 12 each) instead of one interleaved stride-6 buffer.- Why: the interleaved path walks every vertex to repack position+normal before upload — a full extra pass and an extra in-memory copy. A CAD kernel’s triangulation (OCCT’s
Poly_Triangulation) already stores the two arrays flat and contiguous, so that repack is pure overhead. Skipping it is a load-time and peak-memory win that scales with mesh size. - It also skips
NormalSmoothing, which is correct for OCCT-sourced geometry: those per-vertex normals are already analytically accurate, so re-deriving them is wasted work (and re-deriving them from face normals was the root cause of the striations fixed in 1.1.22). - Opt-in and source-compatible. Nothing changes unless you call
directMesh(...); interleaved bodies keep the existing path, includingautoSmoothNormals. Both kinds coexist in one scene. - New public API:
ViewportBody.meshPositions,ViewportBody.meshNormals, and the computedViewportBody.usesDirectMesh.ViewportBody.initgains defaultedmeshPositions:/meshNormals:parameters. - Full render-pass parity, in both
ViewportRendererandOffscreenRenderer: shaded (opaque + transparent), shadow casting, the SSAO/silhouette depth prepass, the always-on-top overlay layer, and the GPU pick pass. Each is routed by a sibling pipeline built on a direct vertex descriptor, selected per body viausesDirectMesh. - Limitations (by design):
autoSmoothNormalsand PN-triangle tessellation (renderingQuality = .enhanced) do not apply to direct bodies; becauseverticescarries the mesh vertices (for bounding box /fit(to:)/ CPU raycast), a direct body does not additionally carry B-Rep corner-vertex or per-segment edge pick indices. Face display, GPU pick, CPU raycast and edge display are unaffected. DirectMeshRenderingTests(7): differential renders proving a direct body is pixel-identical to its interleaved twin (opaque, transparent, and cast shadows), plus pipeline-construction and CPU-raycast coverage. 170 tests total.- Verified live and on device beyond the headless suite: macOS under Metal API + GPU Validation with SSAO, silhouettes, shadows, overlay and picking all enabled (zero validation errors), and GPU pick readback confirmed on an iPhone 15 Pro. New
Examples/DirectMeshVerifyrig (DirectMeshVerify_iOS/_macOSschemes) — depends only onOCCTSwiftViewport, for on-device viewport checks.
- Why: the interleaved path walks every vertex to repack position+normal before upload — a full extra pass and an extra in-memory copy. A CAD kernel’s triangulation (OCCT’s
Fixed
SceneRaycastcrash on direct-mesh bodies. The narrowphase readbody.vertexData(stride 6) indexed bybody.indices. A direct-mesh body leavesvertexDataempty, so any raycast against one indexed an empty array and trapped. The narrowphase now readsverticeswhenvertexDatais empty. Affects CPU picking,PivotStrategy, and tap-to-measure against direct bodies.
[1.1.22] — 2026-06-22
Fixed
NormalSmoothing“brushed” striations on anisotropic meshes (issue #81; root cause of OCCTSwift#255).NormalSmoothing.smoothNormalsdiscarded the per-vertex normals it was given and recomputed each from an area-weighted face-normal average. On a highly anisotropic mesh — long thin triangles along a sweep, e.g. athreadedShafthelical flank — that average is directionally biased and renders as fine periodic “brushed” striations (a shading artifact that looks like a geometric ripple).- The fix assigns the area-weighted average of the original per-vertex normals within each crease group (snapshotted before the in-place write); face normals are still used for crease detection (hard edges stay crisp).
- Smooth B-rep meshes: OCCT’s accurate analytic normals are reproduced exactly (verified byte-identical to a raw-normal render of an M10×1.5 thread) → striations gone.
- Flat meshes (imported STL, vertex normal == face normal): the vertex-normal average equals the old face-normal average → behaviour unchanged, backward-compatible.
- New
smoothInputNormalsPreserved+flatInputMatchesFaceNormalAveragetests; updated the crease test to seed realistic per-face normals. 163 tests pass.
[1.1.21] — 2026-06-20
Added
- Unlit / flat-colour display mode (issue #77). New
DisplayMode.unlitdraws each body in its constant base colour with no lighting, ambient, shadows, fresnel, curvature, or tone mapping — so vivid, colour-coded diagnostic / debug renders stay faithful and distinguishable. PreviouslyOffscreenRenderer.shadedran every body through the full PBR path (hemisphere ambient + fresnel rim + curvature; ACES tone map on the interactive post-pass), which desaturated and hue-shifted saturated colours (a bright magenta read as green-ish).shaded_fragmentearly-returnsbodyUniforms.colorwhen the newUniforms.unlitflag is set; clip-plane discard and selection tint still apply.- Both
OffscreenRenderer(options.displayMode) andViewportRenderer(controller.displayMode) set the flag;.unlitroutes through the shaded pipeline viashowsSurfaces == true. The interactive SSAO / ACES tone-map post-pass is skipped for.unlitso it can never re-desaturate. Uniformsgains auint unlitfield, kept in sync Swift↔Metal by repacking the existing 16-byte clip-pad tail (stride unchanged elsewhere).
- New
UnlitColorTests: a magenta panel renders faithfully in.unlit(R≈255, G≈38, B≈217) and measurably more saturated than.shaded. 161 tests total.
[1.1.20] — 2026-06-12
Added
- Tap-to-measure (issue #68).
ViewportController.measurementMode(.distance/.angle/.radius) now actually drives interaction: while a mode is active, taps on geometry accumulate world-space surface points and commit aViewportMeasurement(rendered by the existingMeasurementOverlay). Previously the property was published but consumed by nothing, so setting it did nothing — it read like a feature that silently didn’t work.- Point order per mode:
.distance→ start, end (2 taps);.angle→ armA, vertex, armB (3 taps);.radius→ center, edge (2 taps). - New
ViewportBody.worldHitPoint(ray:triangleIndex:)reconstructs the picked surface point in world space, respecting the body’stransform. - New controller surface:
pendingMeasurementPoints(published, for in-progress feedback),addMeasurementPoint(_:),handleMeasurementPick(result:ndc:bodies:aspectRatio:),cancelPendingMeasurement(),clearMeasurements(), andstatic pointCount(for:). Changing the mode clears in-progress points; taps register on.facepicks only and don’t disturb the selection stream. MeasurementModeis nowEquatable.
- Point order per mode:
- New
MeasurementModeTests(14). 160 tests total.
[1.1.19] — 2026-06-11
Fixed
StandardViewside views were all the top view — Z-up rewrite. Reported as “clicking anything on the view cube defaults to top”.StandardViewmixed a Y-up, front-is-identity convention with Z-axis yaws. This engine is Z-up (turntable orbits Z; the navigation cube’s top face is +Z), so the identity look direction is −Z — and yawing about Z does nothing to a −Z look. The result:.front/.back/.left/.rightall produced the identical top-down rotation, and.toplooked along −Y.- Rewritten for Z-up:
.top= identity; side views tilt onto the horizon (rotX π/2= front, look +Y, up +Z) then yaw about world Z; isometrics tilt 54.7356° from the zenith then yaw to the corner. - All ten views are now distinct, side views keep a level Z-up horizon, and the default isometric camera is a true three-face corner view (it was a tilted, front-ish two-face view).
- Regression tests: per-view look axes, pairwise distinctness, level horizons, and cube face-centre hit-test round-trips at isometric. 146 tests.
- Rewritten for Z-up:
[1.1.18] — 2026-06-11
Added
- Zoom-at-cursor / zoom-at-fingers everywhere. New
ViewportInputEventcase.pinchAtChanged(scale:centerNDC:aspectRatio:)carries the gesture centre, and newCameraController.zoomToward(factor:cursorNormalized:aspectRatio:)(the scroll-zoom pivot-shift math generalised to any factor) keeps the world point under the fingers/cursor stationary. Both platforms’ magnify gestures now route through it; scroll-wheel zoom already did.
Fixed
- Jumpy / oversensitive trackpad zoom.
ScrollCaptureMTKViewfed rawscrollingDeltaYinto a linear1 + delta·kfactor. Precise devices (trackpads, Magic Mouse) report pixel deltas — ±60 per event during a flick, i.e. an 8× zoom from a single event. Precise deltas are now normalised (÷10) at the AppKit source, so one unit means the same zoom on every device. - Zoom was not smooth.
scrollZoomnow uses an exponential mappingexp(delta·k)— smooth, symmetric (in and out cancel), and never non-positive. - Centre-locked pinch. Trackpad/touch pinch dispatched a plain
.pinchChanged→zoom(factor:)about the view centre, ignoring where the gesture actually was. Now routed through.pinchAtChanged(above). - 142 tests.
[1.1.17] — 2026-06-10
Added
GestureConfiguration.invertOrbitHorizontal/invertOrbitVertical— let a consumer flip either orbit axis independently (object-follows-finger vs. camera-orbits-object). Applied inViewportInputRouterfor both drag and inertia (endDrag).
Fixed
- Taps were being swallowed as micro-orbits, making tap-to-select feel broken. The orbit gesture’s
minimumDistancewas 1 pt, which caught the sub-pixel jitter of a tap, soSpatialTapGesturerarely fired. Raised to 8 pt, which cleanly separates a tap (select) from a drag (orbit). - 142 tests.
[1.1.16] — 2026-06-08
Fixed
- Navigation-cube drag direction (follow-up to #60/v1.1.15). Dragging the cube orbited the camera the wrong way — v1.1.15 reused the viewport’s grab-the-model sign, but the cube is a camera proxy, so it must orbit the camera around the model (opposite sign on both axes). Dragging the cube now spins the view the way the drag intends.
[1.1.15] — 2026-06-08
Changed
- Navigation cube is now drag-to-orbit, with discoverable edge/corner targets (follow-up to #60). Dragging the cube now orbits the camera (grab-and-spin, independent of the viewport’s gesture-action mapping); a press that doesn’t move past a small threshold is still treated as a tap that snaps to the region’s view. The cube draws a 3×3 grid per visible face so the edge and corner hit zones are visible, and the overlay is larger (96 pt) so they’re easier to hit on touch.
- The hit-test already resolved faces / edges / corners (verified by a new round-trip test under the isometric rotation); these changes make edges/corners practically reachable and add the expected CAD cube-drag interaction. 142 tests total.
[1.1.14] — 2026-06-08
Added
ViewportBody.isPickable(issue #63, defaulttrue). A body can now be drawn but excluded from the GPU pick buffer, so always-on-top reference geometry (datum / ground planes, overlays) no longer steals face/edge/vertex picks from the geometry behind it. Gated alongsideisVisiblein every pick sub-pass — geometry, edge, arc, vertex, and the overlay pick pass — with the object index still advancing so remaining bodies keep stable pick IDs. UnlikeselectionFilter(which rejects a pick after the GPU returns it), this stops the body from occluding the pick buffer at all, so the geometry behind it is actually sampled.- New
PickabilityTests(3). 141 tests total. Defaulttrue→ no behaviour change for existing bodies.
[1.1.13] — 2026-06-08
Fixed
- View-cube overlay now honours
ViewportConfiguration.viewCubePosition(issue #62). The overlay was hardcoded to bottom-trailing, so the cube couldn’t be moved — a real problem on iPhone where a bottom sheet covers that corner. It now maps the.topLeading/.topTrailing/.bottomLeading/.bottomTrailingconfig to the overlay alignment (within the safe area), so a host can move the cube clear of other UI. Default stays bottom-trailing (no behaviour change). - New
ViewCubePositionTests(2). 138 tests total.
[1.1.12] — 2026-06-08
Added
- Interactive 3D navigation cube (issue #60) — a Shapr3D / Fusion-style
NavigationCubeViewreplacing the orientation-gizmoViewCubeViewin the corner overlay. It renders a cube that trackscameraState.rotation, with clickable faces, edges, and corners that animate to the matching plan / elevation / isometric view.- New
NavigationCube— pure, SwiftUI-free projection + hit-testing. Taps are unprojected to a ray, intersected with the unit cube, and the frontmost surface point is classified into the 3×3-per-face grid → one of the 26ViewCubeRegions (faces / edges / corners). Robust on iOS (touch) and macOS (click + hover highlight). - New
ViewportController.goToRegion(_:duration:)— animates to a region’s orientation, preserving the current pivot / distance / projection. NavigationCube/NavigationCubeViewre-exported (_NavigationCube/_NavigationCubeView).ViewCubeViewis retained (still re-exported) for source compatibility.- New
NavigationCubeTests(5): region↔base-face mapping + 26-region lookup completeness, surface-point classification, ray-cube intersection, and end-to-end identity-rotation hit-testing (centre→face, edges, corners, miss). 136 tests total; cube rendering verified on the Vision Pro simulator.
- New
[1.1.11] — 2026-06-06
Fixed
- Z-fighting from a fixed near/far clip range (issue #57). The renderers hard-coded
near = 0.01/far = 10000(ratio 1e6), which collapses hyperbolic depth precision onto distant geometry — adjacent triangles on large / real-scale models (e.g. an mm-scale STL hundreds of units out) round to the same depth and flicker. Clip planes are now scene-adaptive: derived each frame from the camera distance and the visible scene’s bounding radius (CameraState.clipPlanes(sceneBounds:)), keepingfar/near~1e3 at any model scale. Applied in bothViewportRenderer(cached per-body AABBs) andOffscreenRenderer. Empty scenes fall back to the old wide default. No consumer tuning required; no API change. - New
ClipPlaneTests(5) covering unit / mm-scale / camera-inside / tiny / huge cases (131 tests total). Live rendering verified unchanged on the Vision Pro simulator.
Note
- Reversed-Z (the issue’s “best for CAD” option) would push precision even further but requires flipping every depth state / clear / compare; scene-adaptive planes resolve the reported flicker with far less risk and are the change shipped here. The measurement-overlay projection in
MetalViewportViewkeeps the wide range deliberately — near/far don’t affect screen-space x/y, only depth.
[1.1.10] — 2026-06-06
Fixed
OffscreenRenderernow honours per-bodytransform(issue #55). Headless renders previously drew every body at its local origin because the offscreen uniforms hard-codedmodelMatrix = identity— so transformed / instanced / assembled bodies came out piled up. The shaded, transparent, point, and shadow passes now setmodelMatrix = body.transform, and the shadow-frustum bounds use each body’s transformed AABB so shadows follow the moved geometry. MatchesViewportRenderer. Identity-transform bodies are unchanged.- New
OffscreenTransformTests(2): a body translated out of frame disappears; a+Xoffset shifts its rendered centroid right (126 tests total).
[1.1.9] — 2026-06-06
Added
- Per-body surface transparency (issue #53, closes it). A body with
color.w/effectiveMaterial.opacity< 1 now renders as a translucent surface compositing over the geometry behind it — enabling “focus one part, fade the rest” interactions.- Alpha blending (
.sourceAlpha/.oneMinusSourceAlpha) enabled on all shaded pipelines (standard, tessellated, mesh-shader) in bothViewportRendererandOffscreenRenderer. Opaque bodies (alpha = 1) are unaffected. - Translucent bodies are drawn in a dedicated pass after the opaque set, sorted back-to-front by camera distance, with depth test on / write off (new
transparentSurfaceDepthState), so they composite correctly without occluding each other in depth. Opaque bodies stay fully z-correct among themselves. - Works headlessly in
OffscreenRenderer(PNG) as well as live. - New
SurfaceTransparencyTests— a differential GPU render (opaque vs translucent front panel) asserting the body behind shows through (124 tests total). Live opaque rendering verified unchanged on the Vision Pro simulator.
- Alpha blending (
Note
- The alpha plumbing already existed end-to-end (
shaded_fragmentoutputsbodyUniforms.color.a); only blending + draw order were missing.
[1.1.8] — 2026-06-06
Added
- Analytic arc picking (follow-up to #48).
ViewportArcs are now pickable: a hit reportsPickResult.kind == .edgewithtriangleIndex= the arc’s index inViewportBody.arcs. The pick pass re-draws this frame’s adaptively-sampled arc segments (reusing the display pass’s buffer + ranges — no re-sampling) through a newpick_arcpipeline; because segment counts are per-frame adaptive, each arc is drawn separately and apick_arc_fragmentstamps the arc index (rather than[[primitive_id]]). - Fixes a latent bug found while wiring this: the arc display pass set the model matrix once (identity) instead of per body, so arcs on a transformed body drew at the wrong place; now set per draw.
- New pick-decode contract test (123 total). Builds + runs on the Vision Pro simulator with the pick pass active.
Note
- A body mixing polyline
edgesandarcsreports both askind == .edge;kindalone can’t disambiguate — prefer one edge representation per body.
[1.1.7] — 2026-06-06
Added
- Analytic arc edges (issue #48, part 3 — closes #48). New
ViewportArcprimitive (center / radius / in-plane basis / start+end angle) andViewportBody.arcs. The renderer samples arcs to line segments adaptively to their projected size each frame (ArcSampling.segmentCount), so circular feature edges render smooth at any zoom, independent of mesh density — no consumer pre-faceting. Sampled once per frame into one reused buffer and drawn per body (inheriting transform / colour) through the existing wireframe pipeline. Re-exported as_ViewportArc/_ArcSampling. - Demo shows a smooth analytic circle.
Fixed
- Arc-only / data-light bodies were dropped by the renderer.
ensureBuffersskipped any body with no vertex/edge/point buffer, so a body carrying onlyarcsnever reached the draw passes. The guard now also admits bodies with arcs. (Surfaced by on-device verification on the Vision Pro simulator.)
Tests
- New
ViewportArcTests(6): arc evaluation, adaptive segment count (scaling, clamps, behind-camera fallback), body integration. 122 total green; arc rendering confirmed on the Vision Pro simulator.
With this, #48 is complete: .cadHighQuality adaptive surface tessellation (v1.1.5) + auto normal smoothing (v1.1.6) + analytic arc edges (v1.1.7).
[1.1.6] — 2026-06-06
Added
- Renderer-side auto normal smoothing (issue #48, part 2) —
ViewportConfiguration.autoSmoothNormals(+normalSmoothingCreaseAngle). When enabled, the renderer applies crease-awareNormalSmoothingto each body’s mesh as its buffers are built, so meshes that arrive with flat / per-face normals (e.g. STL) can actually be rounded by.enhancedPhong tessellation — hard edges stay sharp via the crease angle. Computed once per body (generation-gated, cached), and the smoothed normals flow into the tessellation patch control points automatically (the patch builder reads the body vertex buffer). - Enabled by default in
.cadHighQuality. New tests: config wiring + crease preservation (116 total). - README “Smooth Round Geometry” updated to point at
autoSmoothNormals.
Analytic arc edges continue in #48 (part 3).
[1.1.5] — 2026-06-06
Added
ViewportConfiguration.cadHighQualitypreset (issue #48, part 1) — exposes the renderer’s existing screen-space-adaptive PN-triangle (Phong) tessellation for smooth round geometry. SetsrenderingQuality = .enhanced+adaptiveTessellation+ a highertessellationMaxFactor, so curved surfaces (cylinder / cone silhouettes, fillets) stay smooth at any zoom without the consumer pre-tessellating finely. The smoothness counterpart to.performance.- README “Smooth Round Geometry” section — documents the surface-smoothness knobs (
renderingQuality,adaptiveTessellation,tessellationMaxFactor,NormalSmoothing), the edge-sampling caveat, and the perf tradeoff. - Config preset test (114 total).
Adaptive Phong tessellation already existed but was gated behind .enhanced (off by default); this makes it discoverable. Auto-smoothing flat input normals and analytic arc edges continue in #48.
[1.1.4] — 2026-06-05
Added
GestureConfiguration.visionOSpreset (issue #36, Phase 2) — a spatial-input starting point for windowed / volumetric visionOS. Keeps the touch-style mapping (single pinch-drag orbits; two-handed pinch/twist zoom/roll) and raises inertia damping for more predictable settling with indirect (pinch + look) input. Re-exported via_GestureConfiguration. The demo adopts it automatically on visionOS.- New preset test (113 total). Builds + runs on the Vision Pro simulator.
The exact sensitivities are a starting point: indirect spatial input feels different from a touchscreen, so final tuning — and richer
SpatialEventGestureintegration — is best done on Vision Pro hardware. Tracked in #36 (Phase 2 refinement); immersive / XR is Phase 3.
[1.1.3] — 2026-06-05
Changed
- Reduced per-body CPU overhead in the main draw pass (issue #42, part 3) — closes #42. Each visible body’s GPU buffers and stable pick index are now resolved once per frame into a single list that the main geometry pass iterates, instead of re-filtering
bodiesand re-hashing theString-keyedbodyBufferCache[body.id]on the pass. Frustum culling moved inline on that list using the cached local AABB, dropping the separate per-frameSet<String>and its extra per-body hash. No render or API change (verified unchanged on the Vision Pro simulator); 112 tests green.
The remaining larger lever for extreme body counts — merging many small static bodies — is consumer-side and documented in the README “Performance & Scaling” section.
[1.1.2] — 2026-06-05
Added
- Per-body frustum culling (issue #42, part 2) — the main lever for large scenes. Bodies whose world-space bounds fall entirely outside the camera are skipped in the main geometry pass, so cost scales with what’s on screen rather than total body count.
- New
Frustum(Gribb–Hartmann plane extraction, Metal[0,1]depth) with a conservative AABBintersects(_:), andBoundingBox.transformed(by:)for world-space bounds (identity-transform fast path). - Per-body local AABBs are computed once and cached in the renderer’s buffer cache (no per-frame vertex rescans); the culled set is built once per frame.
- Pick-ID indices stay stable (culled bodies still advance the object index). The shadow pass is not camera-culled (off-screen casters can shadow visible geometry); bodies with no bounding box are never culled.
- New
ViewportConfiguration.enableFrustumCulling(defaulttrue— off-screen bodies aren’t visible anyway; setfalseto opt out). - New
Frustum/BoundingBox.transformedtests (8 → 112 total). Verified rendering unchanged on the Vision Pro simulator.
- New
Per-body CPU overhead reduction continues in #42 (part 3).
[1.1.1] — 2026-06-05
Added
ViewportConfiguration.performancepreset (issue #42, part 1) — a discoverable fast path for large / many-body scenes on mobile. Disables the per-frame whole-scene passes that dominate cost on big models: directional shadow map, SSAO, MSAA (sample count 1), and silhouettes. Replaces hand-assembling those levers.- README “Performance & Scaling” section — guidance on the two cost sources (per-frame passes vs per-body CPU overhead), batching many small static bodies into shared-material
ViewportBodys (keeping sub-component picking viafaceIndices), and recommended body counts. - New
ViewportConfigurationTests(2 tests → 104 total).
Renderer-side scaling (frustum culling, reduced per-body overhead) continues in #42.
[1.1.0] — 2026-06-05
Added
- visionOS platform support (windowed / shared space) — Phases 0+1 of #36. The viewport now builds and runs on visionOS, rendering through the existing UIKit
MTKView+ SwiftUI gesture path in a window / volume. Verified end-to-end on the Apple Vision Pro simulator (visionOS 26.5).Package.swiftdeclares.visionOS(.v1)(matching the OCCTSwift / OCCTSwiftTools ecosystem; the library uses no visionOS-2-only APIs).MetalViewRepresentableand theMetalViewportViewgesture / two-finger-pan code are gated#if os(iOS) || os(visionOS)(shared UIKit).- Point→pixel scale for picking is derived from the renderer’s actual drawable size (new
ViewportRenderer.lastDrawableSize) instead ofUIScreen.main(unavailable on visionOS) /NSScreen.main— more correct on all platforms and removes the screen-API dependency. - Demo (
Examples/MetalDemo) gains a visionOS target /OCCTSwiftMetalDemo_visionOSscheme. - No behaviour change on iOS / macOS; 102 tests green.
- Follow-ups tracked in #36: spatial-input polish (Phase 2), immersive / XR (Phase 3).
First minor bump in the 1.x line: a new platform is a capability expansion rather than a purely additive API, so it gets a MINOR rather than the usual PATCH.
[1.0.8] — 2026-06-05
Added
- Portable input-event model (issue #35, completing the layer). A source-neutral event type plus a single observable dispatch entry, so synthetic / XR / test input can drive the camera without any AppKit / UIKit type.
ViewportInputEvent—Sendable/Equatableenum (drag, two-finger pan, pinch, rotate, scroll, tap) with raw deltas. Re-exported as_ViewportInputEvent.ViewportController.dispatch(_:)— the single interpretation entry point. It owns gesture-action resolution, the orbit X-axis inversion, and the drag-to-zoom curve; the platform layer is now a thin native→event adapter. macOS resolves drag actions via modifiers (dragAction(for:)); iOS viasingleFingerDrag(both config fields preserved — no behaviour change).ViewportController.onInputEvent— observation hook for the whole event stream (drives the demo’s input inspector; useful for debugging / HUDs).- iOS + macOS gesture, scroll, and tap handlers rerouted through
dispatch; behaviour preserved (Views keep delta math + dynamic-pivot scheduling). - New
ViewportInputRouterTests(8 tests → 102 total, all green). iOS (arm64) + macOS demo builds verified.
Demo
- Input inspector overlay (sidebar → Debug → “Input event inspector”) showing the live
ViewportInputEventstream — for on-device verification of gesture interpretation.
Fixed
- Two-finger rotate (roll) never fired on iOS / macOS trackpad. Pinch and rotate were attached as separate
.gesture()modifiers, which SwiftUI treats as mutually exclusive, so pinch always won. Both two-finger continuous gestures are now.simultaneousGesture, so pinch + rotate coexist. (Pre-existing bug, surfaced by on-device verification of #35.)
Note
- This closes the #35 follow-up. The shipped seam is what #36 (visionOS/XR) builds on: XR / synthetic input produces
ViewportInputEvents and callsdispatch(_:)directly.
[1.0.7] — 2026-06-05
Added
- Portable input-interpretation seam (issue #35, first increment) — an
Aspect_VKeyanalogue decoupling gesture interpretation fromNSEvent/UIEvent.ViewportModifierKeys— aSendableOptionSet(.shift/.control/.option/.command) with bridging initialisers fromNSEvent.ModifierFlags(AppKit) andUIKeyModifierFlags(UIKit). Re-exported as_ViewportModifierKeys.GestureConfiguration.dragAction(for:)— resolves a drag’sGestureActionfrom modifier keys, preserving the historical priority (command → shift → option → unmodified).- The macOS drag handler now bridges native flags into
ViewportModifierKeysand calls the resolver instead of branching onNSEventinline; a stray debugprintwas removed. Behaviour is unchanged. - New
InputAbstractionTests(7 tests → 94 total, all green).
Note
- This is the modifier/key portion of #35. A full portable pointer/scroll/pinch event model remains; #35 stays open to track it. This seam is the foundation the visionOS work (#36) builds on.
[1.0.6] — 2026-06-05
Added
- Screen-space HUD overlays (issue #34) — the
Graphic3d_TransModeanalogue: orientation-only overlays pinned to viewport corners, distinct from the world-space axes/grid. Implemented as SwiftUI overlays (the ViewCube pattern), no extra Metal pass.OrientationGnomon— top-leading corner gnomon drawing the world X/Y/Z axes (red/green/blue) under the current camera rotation; back-to-front depth sorted. Re-exported as_OrientationGnomon.ScaleBarView— bottom-leading scale bar reporting the world length of a ~100-point span at the camera’s focus (pivot) depth. Re-exported as_ScaleBarView.CameraState.worldUnitsPerPoint(viewportHeightPoints:)— unified perspective (2·distance·tan(fov/2)) + orthographic (orthographicScale) world-units-per-point.ScaleBarMetrics— snaps the represented length to a nice 1/2/5×10ⁿ value, recomputes the bar’s point length, and formats an optional unit label. Re-exported as_ScaleBarMetrics.- Config:
ViewportConfiguration.showOrientationGnomon/showScaleBar/scaleBarUnitLabel(all default off / empty → source-compatible), mirrored byViewportController.showOrientationGnomon/showScaleBarpublished toggles. - New
HUDOverlayTests(10 tests → 87 total, all green). Additive, no API break (PATCH). - Note: for perspective cameras the scale bar is exact only at the pivot depth (scale varies with depth); orthographic is exact everywhere.
[1.0.5] — 2026-06-05
Added
- Selection filter chains (issue #33). A composable
SelectionFilterpredicate overPickResult, the OCCTSwiftViewport analogue of OCCT’sSelectMgr_Filter. Lets consumers constrain what the user-geometry pick stream accepts.SelectionFilteris aSendablewrapper over@Sendable (PickResult) -> Bool, exposingmatches(_:)andcallAsFunction. Re-exported as_SelectionFilter.- Built-ins:
.all/.nothing,.kind(_)/.kinds(_)/.faces/.edges/.vertices,.layer(_),.bodyIDs(_)/.excludingBodyIDs(_),.bodyIndices(_). - Composition:
.and(_),.or(_),.negated, and chain combinators.all(of:)(AND) /.any(of:)(OR). ViewportController.selectionFilter: SelectionFilter?— applied inhandlePickto the user-geometry stream. A pick that fails the filter is treated as a miss (clearspickResult), since GPU picking resolves a single primitive per pixel with no alternate candidate to fall through to. Widget-layer picks bypass the filter (that stream is owned by an external consumer, e.g. OCCTSwiftAIS manipulators).- New
SelectionFilterTests(13 tests). No public API break — additive (PATCH). - Builds on the face/edge/vertex pick discrimination shipped in v0.55.0.
- Note: depth/distance filtering is intentionally absent — the GPU
PickResultcarries no depth value; use the CPUSceneRaycastpath for distance-aware filtering.
[1.0.4] — 2026-05-26
Fixed
- Package-level dependency cycle (issue #32). OCCTSwiftViewport’s manifest declared a dependency on OCCTSwiftTools (used only by the demo executable), while OCCTSwiftTools depends back on OCCTSwiftViewport — a cycle that broke
swift buildon a fresh checkout whenever the working-copy directory wasn’t named exactlyOCCTSwiftViewport(SwiftPM resolved a stale 0.55.3 copy and failed). The demo moved into its own standalone package atExamples/MetalDemo(takes Viewport viapath: "../.."). The published OCCTSwiftViewport package now has zero external dependencies; the rootPackage.resolvedwas removed. Library consumers were never affected (SwiftPM prunes the demo-only Tools dep).project.ymlandscripts/overnight-stress.shupdated to the new demo path.
[1.0.3] — 2026-05-21
Fixed
quantize()traps on out-of-range / non-finite vertex coords (issue #30, PR #31).NormalSmoothing.quantizenow drops non-finite coords (→0) and clamps into the Int32 range (limit2e9, notFloat(Int32.max)which rounds up to 2³¹ and traps) before the trappingInt32(_: Float)init. Previously any vertex coord beyond ±21,474.8 model units, orNaN/±inf,fatalError‘d on every body load viaCADFileLoader.loadFromManifest. Only affects the welding key — clamped extremes simply don’t weld. AddedNormalSmoothingTests(3 tests). Source/binary compatible with 1.0.2.
[1.0.2] — 2026-05-09
Added
- Point-cloud rendering pipeline (issue #28, PR #29). New
visiblePointPipelinein bothViewportRendererandOffscreenRenderer.BodyPrimitiveKindenum (.mesh/.point/.wire, named to avoid colliding with the pick-sidePrimitiveKind).ViewportBody.primitiveKind/pointRadius/vertexColors(all defaulted → source-compatible);boundingBoxfalls back tovertices.- World-radius → screen-pixels via
pxPerWorldFactor / clipPosition.w(unified perspective + ortho),[[point_size]]clamped to[1, 64], premultiplied-alpha blending. Mesh + wireframe paths skip.pointbodies. - New
ViewportPointCloudRenderingTests(4 tests).
[1.0.1] — 2026-05-09
Changed
- Docs polish + UX smoke coverage. README adds an ecosystem-map cross-link;
CHANGELOG.mdmoved intodocs/with the README link updated;PBR_UPGRADE_PLAN.mdremoved (shipped in 0.50.0);DemoSmokeTestsnow covers the v0.168 / v0.169 / v1.0 demo buttons. No library API changes.
[1.0.0] — 2026-05-08
Changed
- SemVer-stable graduation (issue #21), aligning with the OCCTSwift v1.0 family pin to OCCT 8.0.0 GA (released 2026-05-07). Demo deps bumped: OCCTSwift
from: 1.0.1, OCCTSwiftToolsfrom: 1.0.0(transitively OCCTSwiftIO 1.0.0). The viewport library itself has no OCCTSwift dependency — the bump only affects the demo +project.yml. - Demo
v0.130 PointSetLibblock excised (API removed in OCCT 8.0.0 GA, no replacement). Added av1RootNodesAndEdgeRegularitydemo showingNodeKind.product/occurrenceand the consolidatedsetEdgeRegularity(edge, face1, face2, continuity).
[0.55.3] — 2026-05-07
Added
- Headless measurement overlay in
OffscreenRenderer(issue #26). Measurement annotations now composite into offscreen renders without an on-screenMTKView, so server-side / snapshot pipelines get the same dimension overlays as the interactive viewport.
[0.55.2] — 2026-05-04
Fixed
- xcodebuild conflicting-identity crash for downstream consumers (issue #27). The demo target’s
OCCTSwiftToolsdep is now a versioned remote URL (from: "0.4.1") instead of.package(path: "../OCCTSwiftTools"). Pure-SPM (swift build/swift test) tolerated the path identity, but xcodebuild’s SPM integration treated the path-identity and URL-identity as distinct packages with the same name and crashed inIDESPMWorkspaceDelegate.registerDependencyFileReferenceswhenever a downstream consumer (e.g. OCCTDesignLoop) also declaredOCCTSwiftToolsas a remote dep. Closes the option-(c) workaround that landed with #22 in v0.51.0; consumers can now adopt anything fromv0.51.0onward in xcodeproj-based projects.
project.yml updated in lockstep so the regenerated Xcode project also pulls OCCTSwiftTools by URL.
[0.55.1] — 2026-05-03
Added
- Per-triangle highlight overlay (issue #25). Replaces the “cheap-route” overlay-body pattern OCCTSwiftAIS v0.1 used for face highlighting.
ViewportBody.triangleStyles: [TriangleStyle]— empty by default, no behavioural change. When populated (count == indices.count / 3), the renderer composites each non-zero-alpha entry over the base shading at that triangle.TriangleStyleis a singleSIMD4<Float>color (16 bytes per triangle).TriangleStyle.noneand the zero-alpha default skip the highlight pass for that triangle.- New render pass between the shaded pass and the selection-outline pass.
MTLRenderPipelineState(triangle_highlight) +highlight_vertex/highlight_fragmentMSL shaders. Depth state is.lessEqual+ write disabled, so identical-position highlights win the depth tie without disturbing depth state for subsequent passes. - Per-body
triangleStyleBuffer: MTLBuffer?cached alongside the existing vertex / index / edge buffers; built only whentriangleStylesis populated, nil otherwise. The fragment shader indexes by[[primitive_id]].
Why this beats the v0.1 cheap route
- No silhouette flicker. Identical geometry depth-tests cleanly with
.lessEqualinstead of needing the bbox-diagonal nudge. - No vertex-data churn. Selection / hover state changes flip per-triangle alpha; no overlay body to spawn / kill on every selection change.
- No body-count blow-up. Style buffer scales with triangle count, not selection count.
- Hover + multi-select for free. Per-triangle style means hover at low alpha and primary selection at higher alpha can coexist without spawning N overlay bodies.
Driver
OCCTSwiftAIS v0.6+ will adopt this and drop its makeFaceOverlay(...) cheap-route helper. Body-level selection still routes through viewport.selectedBodyIDs (already works).
References
[0.55.0] — 2026-05-03
Added
- Edge / vertex picking for OCCTSwiftAIS v0.3 (issue #24). The single GPU pick pass now resolves to face / edge / vertex via a 2-bit kind tag in the high bits of the pick raw value, so consumers get a uniform pick API across all three sub-shapes.
ViewportBodygains:edgeIndices: [Int32]— per-line-segment source-edge index, parallel to the line primitives inedgesflattened. Empty by default → body is not edge-pickable.vertices: [SIMD3<Float>]— point list rendered as point sprites (8×8 px) for vertex picking. Empty by default → body is not vertex-pickable.vertexIndices: [Int32]— per-point source-vertex index. Empty defaults to identity.
PickResult.kind: PrimitiveKind(.face/.edge/.vertex). NewPrimitiveKindenum re-exported from the module.- Pick encoding: bits 0-15 objectIndex (unchanged), bits 16-29 primitiveID (14 bits, masked everywhere it’s emitted — including the existing
pick_fragmentandtessellated_pick_fragment), bits 30-31 kind.triangleIndexkeeps its name and now carries the primitive index for whatever kind matched. - New
pick_line_fragment,pick_point_vertex,pick_point_fragmentshaders; newpickLinePipeline+pickPointPipeline+pickEdgeOrPointDepthState(.lessEqual) so edges/vertices coplanar with a face win the pick over the face. - 8 new
PickResultTestspin the encoding contract that AIS will rely on.
[0.54.0] — 2026-05-03
Changed
- Bumped OCCTSwift dep to
from: "0.169.0"(was 0.168.0). v0.169 extends theImportProgresschannel to three more long-running operations:Shape.meshWithProgress(linearDeflection:angularDeflection:progress:),Exporter.writeSTEP(shape:to:progress:)/writeIGES(shape:to:progress:), andDocument.writeSTEP(to:progress:). NewExporter.ExportError.cancelledcase.
Added
- v0.169 Mesh Progress demo (
v169MeshProgress): builds a sphere, runsmeshWithProgressat fine deflection (4 progress callbacks observed end-to-end), then runs again with a cancel-after-first observer to verifyImportError.cancelled. - v0.169 Export Progress demo (
v169ExportProgress): builds a torus, runsExporter.writeSTEP(7 callbacks),Exporter.writeIGES(3 callbacks), andDocument.writeSTEP(7 callbacks) each with a recording observer, plus a STEP export with cancel-after-first that verifiesExportError.cancelled.
Both demos wired into the OCCT 8 sub-group of SpikeView and into the headless --test-all-demos runner (201 demos total).
[0.53.0] — 2026-05-03
Changed
- Bumped OCCTSwift dep to
from: "0.168.0"(was 0.165.0). Pulls in:- v0.166.0 / v0.166.1 — Swift Package Index metadata + platform-plan docs (no API change).
- v0.167.0 — visionOS + tvOS xcframework slices (no API change).
- v0.168.0 —
ImportProgressprotocol + cooperative cancellation forShape.loadSTEP/loadIGES/loadIGESRobustandDocument.load/loadSTEP.
Added
- v0.168 Import Progress demo in
OCCT8Gallery(v168ImportProgress). Writes a small fused box+cylinder STEP, then re-loads three times: with a recording observer (75 progress callbacks at last run), with an observer that requests cancellation after the first callback (catchesImportError.cancelled), and withprogress: nilto confirm source-compat. Wired into the OCCT 8 sub-group inSpikeViewand into the headless--test-all-demosrunner.
[0.52.0] — 2026-05-03
Added
- Renderer-side support for OCCTSwiftAIS manipulator widgets (issue #23):
ViewportBody.renderLayer: RenderLayer(.geometry/.overlay). Overlay bodies are drawn after the selection outline withdepthCompareFunction = .always, so manipulator arrows remain grabbable even when occluded by their target body. Overlay bodies are also rendered into the pick texture with always-pass depth.ViewportBody.pickLayer: PickLayer(.userGeometry/.widget).ViewportController.pickResultnow exposes only user-geometry picks;ViewportController.widgetPickResult(new) carries widget-layer picks. A single GPU pick pass populates both via the body’s layer. AddsonWidgetPickcallback andclearWidgetPick().ViewportBody.transform: simd_float4x4. Drives the per-body model matrix in the main, shadow, pick, and selection-outline passes — manipulator drags can now move a body without re-uploading vertex data.PickResult.pickLayerfield; the existinginit(rawValue:indexMap:)gains an optionallayerMap:parameter (existing call sites stay source-compatible).
[0.51.0] — 2026-05-03
Removed (breaking)
OCCTSwiftToolslibrary product has been extracted to its own repository: https://github.com/gsdali/OCCTSwiftTools. Consumers depending on the viewport’sOCCTSwiftToolsproduct must migrate to the standalone package (>= 0.1.0). This unblocks SwiftPM target-name uniqueness and lets OCCTSwiftTools tag its v0.1.0 release.- Removed sources:
BodyUtilities.swift,CADFileLoader.swift,CurveConverter.swift,ExportManager.swift,ScriptManifest.swift,SurfaceConverter.swift,WireConverter.swift(already mirrored in the standalone repo).
- Removed sources:
Changed
OCCTSwiftMetalDemo(executable, dev aid only — not a library product) now depends on the externalOCCTSwiftToolspackage via a local path dep (../OCCTSwiftTools) so the demo continues to build during the transition. This will be switched to a versioned remote dep onceOCCTSwiftTools v0.1.0is published.
[0.26.0] — 2026-02-15
Added
- Annotation Gallery (
AnnotationGallery.swift): Demonstrates OCCTSwift AIS annotation features — dimension measurements, text labels, and colored point clouds.- Length dimensions (point-to-point) with rendered dimension geometry (extension lines, markers)
- Radius & diameter dimensions on cylinders and spheres
- Angle dimensions between edges and from three-point configurations
- Text labels at 3D positions with marker spheres and leader lines
- Colored point cloud (50 points, spherical distribution, height-colored)
- Sidebar section: Annotation Demos
[0.25.0] — 2026-02-15
Added
- Naming Gallery (
NamingGallery.swift): Demonstrates OCCTSwift TNaming — topological naming history tracking on XDE documents.- Primitive history: creates a box, records it as a primitive, queries stored/current shape and naming history
- Modification tracking: box → filleted box, records both steps, shows old (dim) vs new (shaded) with history
- Forward/backward tracing: box − cylinder boolean, traces the naming graph in both directions
- Named selection persistence: selects a face by name and resolves it back from the naming graph
- Sidebar section: Naming Demos
[0.24.0] — 2026-02-15
Added
- Medial Axis Gallery (
MedialAxisGallery.swift): Computes and visualizes the Voronoi skeleton (medial axis) of planar shapes using OCCTSwiftMedialAxis(BRepMAT2d).- Rectangle skeleton with inscribed circle node markers
- L-shaped profile showing branching skeleton topology
- Thickness map with arcs colored red (thin) to blue (thick) by wall distance
- Custom T-profile skeleton with node/arc statistics
computeForShape(_:)API for use with STEP face selection
- Sidebar section: Medial Axis Demos
[0.23.0] — 2026-02-15
Added
- Projection Gallery (
ProjectionGallery.swift): Projects 3D curves and points onto parametric surfaces using OCCTSwiftSurface.projectCurve3D,projectCurveSegments, andprojectPoint.- Curve on cylinder (diagonal line → helix on surface)
- Curve on sphere (tilted circle → geodesic-like projection)
- Composite projection with multi-segment UV coloring
- Point projection with 20 scattered points, distance-colored lines (green=close, red=far)
- Plate Gallery (
PlateGallery.swift): Creates and deforms plate surfaces using OCCTSwiftSurface.plateThrough,nlPlateDeformed(G0), andnlPlateDeformedG1(G1).- Plate from scattered 3D points (terrain-like)
- G0 position-constrained deformation (flat → curved)
- G1 tangent-controlled deformation with direction arrows
- Sidebar sections: Projection Demos, Plate Demos
[0.22.0] — 2026-02-15
Added
- Sweep Gallery (
SweepGallery.swift): Variable-section pipe sweeps using OCCTSwiftLawFunctionandShape.pipeShellWithLaw.- Constant pipe (uniform cross-section)
- Linear taper (narrowing from start to end)
- S-curve sweep (smooth bulge)
- Interpolated sweep (custom varying profile)
- Each demo includes a 2D law function plot and spine wireframe overlay
- GD&T display: When STEP files with PMI data are loaded, dimensions, geometric tolerances, and datums are extracted and shown in a new GD&T sidebar section.
Changed
CADLoadResultnow includesdimensions: [DimensionInfo],geomTolerances: [GeomToleranceInfo], anddatums: [DatumInfo]arrays extracted from STEP documents viaDocument.dimensions,Document.geomTolerances,Document.datums.
[0.21.0] — 2026-02-15
Added
- Surface Gallery (
SurfaceGallery.swift): Interactive surface demos using OCCTSwiftSurfaceclass.- Analytic surfaces: plane, cylinder, cone, sphere, torus (arranged in a row, trimmed, as UV grids)
- Swept surfaces: extrusion along direction, revolution around axis
- Freeform surfaces: 4×4 Bezier patch with control net overlay, 3×3 Bezier patch
- Pipe surfaces: circular and elliptical cross-section pipes along curved spines
- Iso-curves: U-iso (red) and V-iso (cyan) parameter curves highlighted on a Bezier surface
- Sidebar section: Surface Demos
[0.20.0] — 2026-02-15
Added
- Curve3D Gallery (
Curve3DGallery.swift): 3D curve demos using OCCTSwiftCurve3Dclass.- 3D curve showcase: line, circle, arc, ellipse, BSpline, Bezier in 3D space
- Helix & spirals: helical interpolation, conical spiral, reversed + trimmed curves
- Curvature combs: perpendicular comb teeth proportional to local curvature, with tip envelope
- BSpline fitting: noisy point cloud with fitted curve (blue) vs exact interpolation (green)
- Sidebar section: Curve3D Demos
[0.19.0] — 2026-02-15
Added
- 3D geometry analysis in selection panel: Selecting a face or edge on STEP geometry now shows rich property data in the selection info panel.
- Face selection: surface type (Plane, Cylinder, BSpline, etc.), area, UV bounds, Gaussian curvature, mean curvature at the hit point
- Edge selection: curve type (Line, Circle, BSpline, etc.), length, parameter bounds, curvature, projection distance at the hit point
- Curvature direction overlays: Toggleable overlays rendered at the selected point.
- Faces: principal curvature directions (cyan kMin, magenta kMax) and surface normal (blue)
- Edges: tangent direction (green), normal direction (blue), center of curvature (orange sphere + gray connector line)
- Proximity detection: “Check Proximity” button in the new Analysis sidebar section. When 2+ shapes are loaded, finds closest face pairs between shape 0 and shape 1, highlights them in color, and reports self-intersection status.
- Sidebar section: Analysis (curvature overlay toggle + proximity check)
[0.18.0] — 2025-12-30
Added
- Multi-format import (STEP, STL, OBJ) with face/edge/vertex selection
- Export to OBJ and PLY formats
- Shape healing operations (sew, upgrade, direct faces, convert to BSpline)
- Point classification (inside/outside/on boundary)
[0.17.0] — 2025-12-30
Added
- Curve2D gallery with showcase, intersections, hatching, and Gcc demos
[0.16.0] — 2025-12-30
Added
- STEP file import with face/edge/vertex selection