Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ OCCTSwiftIO built clean with zero errors while carrying three real breaks in its
Dependencies resolve against local siblings when present (`../OCCTSwift` and friends), else the
published URLs. No binary lives in this repo.

**Expected baseline: 330 tests across 28 suites, all passing.**
**Expected baseline: 343 tests across 30 suites, all passing.**

## Face identity is `IsSame`, and that decision is settled

Expand Down Expand Up @@ -113,9 +113,45 @@ Three behaviours look like the resolver's job and are not, so they stayed where
Pulling any of them down would drag presentation and viewport state into the bridge layer, which is
the thing this consolidation exists to prevent.

Still outstanding from the same epic: the two parallel selection systems (`select`,
`clearSelection`, `remove`, `removeAll` in both `OCCTSwiftAIS` and `OCCTSwiftCADKit`) are issue #3,
and each remaining collision gets its own behaviour matrix before either copy is deleted.
## One selection, held by `InteractiveContext`

Phase 3 of ecosystem#43, done in
[OCCTSwiftInteraction#3](https://github.com/SecondMouseAU/OCCTSwiftInteraction/issues/3).

`OCCTSwiftAIS.InteractiveContext.selection` is the only selection store in this package.
`OCCTSwiftCADKit.CADViewportService` used to keep a second one alongside the context it already
owned; it now drives that one and projects it:

- `CADViewportService.selection` is `interactiveContext.selection` enriched into `PickedEntity`
values. Mirrored into stored state so SwiftUI observation fires, exactly as `bodies` mirrors
`interactiveContext.bodies`. Ordered by (body id, kind, ordinal), since the store is a `Set`.
- `CADViewportService.selectionModes` **is** `interactiveContext.selectionMode`, not a copy.
Initialised to `[.face]` at `init`, overriding the context's `[.body]` default.
- `select`, `clearSelection` and the selection pruning inside `remove`/`removeAll` all go
through the context.

`InteractiveContext.select(_:scheme:)` gained the four-scheme parameter from CADKit's version.
`select(_:)` is untouched and still means `.add`; the scheme parameter is deliberately not
defaulted, because a default would silently retune every existing call site to `.replace`.

What did **not** merge, and why:

- **`PickedFaceInfo`/`PickedEdgeInfo`/`PickedVertexInfo` survive** as presentation types, now
storing a `SubShapeRef` and forwarding `shape`/`uid`/`ordinal` to it. Two of their fields
(`scalarValue` for a per-triangle field, `description`) cannot be recovered from a ref after
the fact.
- **The highlight systems stay separate.** AIS paints `triangleStyles`; CADKit builds aggregate
highlight bodies. CADKit already uses `triangleStyles` for scalar fields, so they would
overwrite each other. That is also why CADKit's bodies are not registered as context entries.
- **The clip-plane pre-filter stays in CADKit.** It tests the picked primitive's position, which
an AIS `SelectionFilter` (which sees a resolved `SubShape`) cannot express.
- **`Axis` in both targets was never a collision**: AIS's is nested inside `ManipulatorWidget`.
- **`SelectionSummary` was never a duplicate of OCCTSwiftUX's.** Different fields, different
inputs, no shared consumer, and OCCTSwiftUX does not depend on this package at all. Resolved
by naming: CADKit's is now `SelectionMeasurements`, with a deprecated alias.

`ComesFromDecomposition` is settled and is **not** going on `SubShapeRef`: `SubShape` is a sum
type, so `ref == nil` already is "whole body", and the resolver can never mint a whole-body ref.

## Where things are

Expand All @@ -127,7 +163,7 @@ Each module kept its own documentation through the merge rather than having it c
| Getting started | none | [guide](docs/guides/getting-started-OCCTSwiftAIS.md) | [guide](docs/guides/getting-started-OCCTSwiftCADKit.md) |
| Module notes | this file | [docs/module-notes/OCCTSwiftAIS.md](docs/module-notes/OCCTSwiftAIS.md) | [docs/module-notes/OCCTSwiftCADKit.md](docs/module-notes/OCCTSwiftCADKit.md) |
| okf component | [okf/components/OCCTSwiftTools.md](okf/components/OCCTSwiftTools.md) | [okf/components/OCCTSwiftAIS.md](okf/components/OCCTSwiftAIS.md) | [okf/components/OCCTSwiftCADKit.md](okf/components/OCCTSwiftCADKit.md) |
| Changelog | [docs/CHANGELOG-OCCTSwiftTools.md](docs/CHANGELOG-OCCTSwiftTools.md) | [docs/CHANGELOG-OCCTSwiftAIS.md](docs/CHANGELOG-OCCTSwiftAIS.md) | none |
| Changelog | [docs/CHANGELOG-OCCTSwiftTools.md](docs/CHANGELOG-OCCTSwiftTools.md) | [docs/CHANGELOG-OCCTSwiftAIS.md](docs/CHANGELOG-OCCTSwiftAIS.md) | [docs/CHANGELOG-OCCTSwiftCADKit.md](docs/CHANGELOG-OCCTSwiftCADKit.md) |

`docs/module-notes/*.md` are the pre-merge `CLAUDE.md` files kept verbatim. They still speak as
though their module is its own repository; read them for module-specific traps, not for repo layout.
Expand Down
7 changes: 1 addition & 6 deletions Sources/OCCTSwiftAIS/AreaSelection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,7 @@ extension InteractiveContext {
}
}

switch scheme {
case .replace: setSelection(Selection(matched))
case .add: setSelection(Selection(selection.subshapes.union(matched)))
case .remove: setSelection(Selection(selection.subshapes.subtracting(matched)))
case .xor: setSelection(Selection(selection.subshapes.symmetricDifference(matched)))
}
applySelection(matched, scheme: scheme)
}

private func project(
Expand Down
63 changes: 52 additions & 11 deletions Sources/OCCTSwiftAIS/InteractiveContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,29 +252,57 @@ public final class InteractiveContext: ObservableObject {

/// Add a sub-shape to the current selection.
///
/// Idempotent.
/// Idempotent. Exactly `select(subshape, scheme: .add)`; kept as its own
/// method (rather than giving `select(_:scheme:)` a default) so that every
/// existing `select(x)` call site keeps meaning "add", which is what it has
/// always meant. A defaulted `scheme:` would have silently retuned all of
/// them to `.replace`.
public func select(_ subshape: SubShape) {
var s = selection.subshapes
s.insert(subshape)
selection = Selection(s)
select(subshape, scheme: .add)
}

/// Combine one sub-shape with the current selection per `scheme`.
///
/// The scheme parameter came from `OCCTSwiftCADKit.CADViewportService.select(_:scheme:)`,
/// which had all four schemes where this context had only `.add` (`select`) and
/// `.remove` (`deselect`). That service now drives this selection rather than keeping
/// a parallel one (OCCTSwiftInteraction#3, phase 3 of ecosystem#43), so the schemes
/// live here with the state.
///
/// Semantics match `SelectionScheme` everywhere else in this target, including area
/// selection: `.replace` assigns, `.add` inserts if absent, `.remove` drops it, `.xor`
/// toggles it.
public func select(_ subshape: SubShape, scheme: SelectionScheme) {
applySelection([subshape], scheme: scheme)
}

public func deselect(_ subshape: SubShape) {
var s = selection.subshapes
s.remove(subshape)
selection = Selection(s)
select(subshape, scheme: .remove)
}

public func clearSelection() {
selection = Selection()
}

/// Replace `selection` wholesale.
/// Combine a whole candidate set with the current selection per `scheme`.
///
/// Used by `AreaSelection.swift` after combining a rectangle/lasso match
/// set with the existing selection per `SelectionScheme`; kept internal
/// since `select`/`deselect`/`clearSelection` are the intended public
/// The one place the scheme rules are written down: `select(_:scheme:)` passes a
/// single-element set, `AreaSelection.swift` passes a rectangle/lasso match set.
/// Internal, since `select`/`deselect`/`clearSelection` are the intended public
/// mutation surface.
func applySelection(_ incoming: Set<SubShape>, scheme: SelectionScheme) {
let current = selection.subshapes
switch scheme {
case .replace: selection = Selection(incoming)
case .add: selection = Selection(current.union(incoming))
case .remove: selection = Selection(current.subtracting(incoming))
case .xor: selection = Selection(current.symmetricDifference(incoming))
}
}

/// Replace `selection` wholesale.
///
/// Kept internal for the same reason as `applySelection(_:scheme:)`.
func setSelection(_ newSelection: Selection) {
selection = newSelection
}
Expand Down Expand Up @@ -354,6 +382,19 @@ public final class InteractiveContext: ObservableObject {
entriesByID[object.id]?.bodyID
}

/// Whether `bodyID` names a body this context displays as a selectable
/// `InteractiveObject`, that is, one added via `display(_:style:)`.
///
/// Public so a host that composites its own bodies into `bodies` alongside this
/// context's (`OCCTSwiftCADKit.CADViewportService` does) can tell whose pick it is
/// looking at. Since the two share one selection, a host that clears the selection on an
/// unresolved pick has to leave this context's own picks alone, or it wipes a selection
/// it never owned. False for internal bodies (manipulator handles, dimensions), which
/// are not selectable objects.
public func displaysBody(withID bodyID: String) -> Bool {
entriesByBodyID[bodyID] != nil
}

/// The source `ViewportBody` for `object`, or nil if the object is not displayed
/// or its tessellation produced no mesh.
func sourceBody(for object: InteractiveObject) -> ViewportBody? {
Expand Down
Loading
Loading