3D App Family: Interface Specification
Resolver, trays, and the binding between them. Companion to the UX benchmark and HIG brief. This is the build-facing document: it defines the shared selection taxonomy, the selection-to-tool resolver, both tray components, and the selection-sync layer that connects all of them.
Scope note. The operation and step tables below are populated for a representative direct-plus-parametric solid and mesh modelling member. They illustrate the pattern, they are not a claim about any one app. Each family member registers its own operation and step descriptors against the same resolver and the same tray components. Lighter members can omit the right tray entirely (see Part 3).
How the parts connect:
Canvas pick ─┐
Items tray ──┼──publish──▶ SelectionModel ──signature──▶ Resolver ──▶ Tool strip
History tray─┘ │
└──highlight + scroll──▶ Canvas, Items tray, History tray
One SelectionModel is the single source of truth. The resolver reads its signature to produce tools. The trays and canvas read it to highlight. All three surfaces can write to it. Part 4 specifies the contract.
Part 0. Shared vocabulary
0.1 Entity types
The taxonomy is extensible per family member. Baseline:
- Document scope: Project, Workspace (Modeling, Visualization, Drawing), Drawing, RenderScene.
- Containers: Folder, Component (a named group of bodies and sketches that can be instanced).
- Geometry: Body (BREP solid or sheet), Surface, Mesh (polygon), Curve, PointCloud.
- Construction: Plane, Axis, Point.
- Reference: Image.
- Sub-entities (selectable parts of geometry): Face, Edge, Vertex for BREP; MeshFace, MeshEdge, MeshVertex for mesh; SketchCurve, SketchPoint, SketchConstraint for sketches.
0.2 Selection states
The resolver and the trays both key off the same five states:
- Empty. Nothing selected.
- Single. One entity of type T.
- Homogeneous multi. N entities, all type T.
- Heterogeneous multi. Mixed types.
- Related multi. A homogeneous or mixed set carrying a relationship flag that unlocks specific operations (two faces on different bodies, two closed profiles, an edge loop, coplanar faces).
0.3 Selection signature
The signature is the hashable summary the resolver consumes:
counts: map of EntityType to count.relationships: flags such assameBody,differentBodies,coplanar,closedProfile,edgeLoop,sharesPlane.workspaceandmode: gate which operations are valid at all.
Determinism rule: the same signature always yields the same resolved tool set. This is what lets muscle memory form across the family.
Part 1. Selection-to-tool resolver
1.1 What it is
A pure function. Given a selection signature and a context (workspace, mode, registered operations), it returns an ordered tool set: a primary operation that may auto-activate, a visible suggested strip, and an overflow set reachable through More or the command palette.
It is data-driven. Operations are not hardcoded per selection. Each operation declares a predicate over the signature, and the resolver evaluates all registered operations against the current selection, filters to the applicable ones, ranks them, and picks a default. Adding a tool means registering a descriptor, not editing the resolver.
1.2 Operation descriptor
struct OperationDescriptor {
let id: OperationID
let label: String
let icon: SymbolName
let group: OperationGroup // create, sketch, modify, transform, boolean, pattern, annotate
let accepts: (SelectionSignature) -> Bool // applicability predicate
let rank: Int // static priority, lower is higher
let activation: Activation // .autoGizmo(GizmoKind) | .openTool | .immediate
let producesHistoryStep: Bool
let workspaces: Set<Workspace>
let modes: Set<EditMode>
}
struct ResolvedToolSet {
let primary: OperationDescriptor? // may auto-activate per its `activation`
let visible: [OperationDescriptor] // surfaced strip, count capped per device
let overflow: [OperationDescriptor] // behind More, and palette-filtered to selection
}
func resolve(_ s: SelectionSignature, registry: [OperationDescriptor]) -> ResolvedToolSet
1.3 Ranking
Applied in order:
- Workspace and mode gate. Drop anything not valid in the current workspace or mode.
- Applicability. Keep operations whose
accepts(signature)is true. - Specificity. An operation that exactly matches the selection type ranks above a general one. Fillet on a selected edge outranks Delete.
- Static rank. Designer-assigned priority within group.
- Recency, capped. Optional. May reorder the overflow only. The visible strip stays stable for a given signature so the layout never feels arbitrary. Do not let frequency churn the top slots.
1.4 Activation
.autoGizmo: the primary operation activates immediately and draws its gizmo on the geometry. Commit by tapping empty space, cancel by Escape, back gesture, or two-finger tap..openTool: surfaces the tool but waits for explicit tap..immediate: one-shot, no gizmo (Delete, Hide).
Only assign .autoGizmo where the default is unambiguous and reversible, so selecting never destroys or commits without intent.
1.5 Reference resolution table
Visible strip capped at roughly 5 on iPhone, 7 on iPad, more on Mac. Everything else falls to overflow. Auto-activating primaries marked with a dot.
| Selection | Primary (• auto) | Visible suggested | Overflow / palette |
|---|---|---|---|
| Empty (Modeling) | none | Sketch, Primitive, Import | Plane, Axis, Image, Paste |
| Vertex (1) | none | Move, Chamfer vertex, Measure | Coincident, Delete |
| Edge (1) | • Fillet/Chamfer (drag) | Fillet, Chamfer, Move edge, Split edge | Measure, Select loop, Delete |
| Edges (N) | • Fillet (drag) | Fillet, Chamfer, Bridge | Variable fillet, Delete, Measure |
| Edge loop | none | Fillet loop, Offset, Bridge | Convert to sketch, Delete |
| Face (1) | • Push/Pull (gizmo) | Offset, Move face, Sketch on face, Delete face | Draft, Replace, Split, Extract surface |
| Faces (N, same body) | • Push/Pull | Offset, Shell (remove these), Combine | Draft, Delete, Measure |
| Faces (2, different bodies) | none | Align, Replace face | Measure gap, Match orientation |
| Coplanar faces | none | Merge, Sketch on, Offset | Extract, Delete |
| Body (1, BREP) | • Move/Rotate (gizmo) | Scale, Copy, Shell, Material, Sketch on | Mirror, Split, Convert to mesh, Export |
| Bodies (N) | none | Union, Subtract, Intersect, Align | Pattern, Group to component, Move |
| Sketch (1) | • Extrude (drag) | Extrude, Revolve, Edit sketch, Offset | Sweep, Project, Thicken |
| Sketches (2+) | none | Loft, Sweep (profile + path) | Boundary surface, Align |
| Sketch curve | none | Trim, Extend, Offset, Constrain | Fillet 2D, Split, Convert |
| Mesh body (1) | • Move/Rotate | Remesh, Decimate, Smooth, Convert to BREP | Scale, Mirror, Material, Export |
| Mesh faces (N) | none | Extrude, Inset, Subdivide, Delete | Bridge, Flip normals |
| Plane | none | Sketch on, Mirror about, Section view | Offset plane, Reorient, Delete |
| Axis | none | Revolve about, Circular pattern, Measure | Reorient, Delete |
| Point | none | Move, Place primitive, Measure | Constrain, Delete |
| Image | none | Reposition, Set opacity, Lock | Calibrate scale, Trace, Delete |
| Heterogeneous mix | none | Move, Group, Hide, Delete | Measure, Align, Export selection |
The heterogeneous row is the floor: when types are mixed, fall back to the intersection of universally applicable operations. Never show an operation that would silently act on only part of the selection.
Part 2. Left tray: structure
The left tray answers “what is in this model”. Two stacked components: the workspace switcher and the items browser. It is a selection surface, not a read-only list.
2.1 Workspace switcher
- Segmented control or compact list: Modeling, Visualization, Drawing.
- Switching changes the canvas purpose, the valid operation set (via the resolver workspace gate), and which trays are relevant. Drawing hides the right history tray and shows sheet structure instead.
- Doubles as CRUD for the workspace’s documents: add, rename, duplicate, delete drawings or render scenes.
- State:
activeWorkspace, persisted per project.
2.2 Items browser
- Hierarchy: Folder and Component containers nest Bodies, Sketches, Construction geometry, Meshes, Images. Disclosure chevrons expand containers.
- Row anatomy: type icon, name (inline editable on double-tap or F2), visibility toggle (eyeball), optional lock, trailing context affordance.
- Filter bar: All Items dropdown filters to one type. Text search within the project.
- Multi-select: tap, shift-tap range, cmd or ctrl tap to add. The resulting set publishes to
SelectionModeland drives the resolver, identical to a canvas pick. - Drag: reorder within a container, drag into folders, drag out.
- Overflow (three dots): Show Hidden Items, Expand all, Collapse all, Sort by name or type or creation order.
- Reveal in Items: a canvas context-menu command that scrolls the tray to the picked entity and flashes its row.
2.3 States
Collapsed to an icon rail, expanded, width-adjustable. Empty state with a create prompt. Filtered state. Search-active state. Multi-select state. Drag-reorder state with valid and invalid drop targets.
2.4 Per-entity exposure
| Entity | Row shows | Inline | Context menu | Notes |
|---|---|---|---|---|
| Folder | name, child count, chevron | visibility (cascades), rename | New subfolder, Ungroup, Delete | Visibility cascades to children |
| Component | name, chevron, instance badge | isolate, visibility | Dissolve, Make unique, Rename | Instanced edits propagate |
| Body (BREP) | name, solid/sheet icon, eyeball, lock | rename, hide | Isolate, Move to folder, Copy, Material, Delete | Selecting feeds resolver Body row |
| Mesh | name, poly-count badge, eyeball | rename, hide | Remesh, Decimate, Convert to BREP, Delete | Poly count is a health signal |
| Sketch | name, eyeball | edit sketch, hide | Used by (jumps to history steps), Delete | Used-by links to the right tray |
| Plane / Axis / Point | name, eyeball | rename | Sketch on (plane), Reorient, Delete | Hidden by default, toggle to show |
| Image | name, eyeball, opacity inline | reposition, lock | Calibrate scale, Trace, Delete | Opacity edits are non-destructive |
| Drawing (Drawing ws) | name, sheet size | open, rename | Duplicate, Delete | Lives in the Drawing workspace list |
| RenderScene (Visualization) | name, thumbnail | open | Duplicate, Delete | Appearance state, not geometry |
Part 3. Right tray: process and history
The right tray answers “how it was built”. It is the construction graph: an ordered timeline of feature steps that produced the geometry. It is the part graph made directly editable.
Optional by design. Closed, the app is in pure direct-modelling mode. Open, it is parametric. The state persists per project. Family members with no construction sequence (a mesh viewer, a measurement tool) omit the right tray entirely, and the architecture treats it as a module, not a fixture. When present it behaves identically across the family.
3.1 Step card
- Collapsed: type icon, name (inline editable, defaults to a generic like
fillet034and should be renamed), status indicator (ok, warning, error, suppressed), drag handle. - Expanded: the feature’s parameters, editable in place. The card is the editor, so there is no separate properties dialog. Below the parameters, the inputs it consumes (links to the entities, selectable) and the outputs it produces.
- Parameter edits propagate. Change an upstream value and downstream steps rebuild. Show a rebuild indicator while recomputing.
3.2 Graph editing
- Reorder: drag a step up or down. Order changes the result (move a cut above a shell and the wall shells around the cut). Validate against dependencies: a step cannot move before its inputs exist. Mark invalid drops, do not allow the drop.
- Breakpoint: insert via context menu, rolls the model back to that point and suppresses everything after it. Drag the breakpoint bar to move the rollback point. Edit upstream, then move the breakpoint down to reapply. Non-destructive time travel.
- Suppress: toggle a single step off without deleting it.
- Diagnostics: if a reorder or parameter change breaks a downstream step, flag the failing step, show the cause, and offer to revert the change.
3.3 Step context menu (three dots)
Rename, Edit, Suppress or Unsuppress, Insert breakpoint, Roll to here, Show dependencies, Delete.
3.4 States
Closed (direct mode), open, step-collapsed, step-expanded, rolled-back (breakpoint active), rebuilding, error. Reordering with valid and invalid drop indication.
3.5 Per-step exposure
| Step | Card (collapsed) | Editable params | Consumes | Produces |
|---|---|---|---|---|
| Sketch | sketch icon, plane name | plane or face, edit-sketch | Plane or Face | Sketch curves |
| Extrude | direction arrow | distance, direction, taper, op (new/add/cut) | Sketch or Face | Body |
| Revolve | angle badge | angle, axis | Sketch + Axis | Body |
| Fillet | radius badge | radius, edge list | Edges | Modifies body |
| Chamfer | setback badge | setback, edges | Edges | Modifies body |
| Shell | thickness badge | thickness, removed faces | Body + Faces | Modifies body |
| Boolean | op icon | operation, operands | Bodies | Body |
| Pattern | count badge | count, spacing or angle, axis | Body + Axis | Bodies |
| Mirror | plane icon | plane | Body + Plane | Body |
| Loft / Sweep | profile count | profiles, path, guides | Sketches | Body |
| Transform | vector badge | translation, rotation | Body | Modifies body |
| Mesh op | type icon | target count or ratio | Mesh | Modifies mesh |
Appearance and material are deliberately not history steps. They are attributes attached to a body, set in Visualization, and they do not participate in the construction graph. This keeps the DAG geometric and the history readable.
Part 4. Canvas-to-tree binding
The selection-sync layer. This is the connective tissue between the resolver and the two trays, and the thing that makes the trays live surfaces rather than dead lists.
4.1 Single source of truth
@Observable
final class SelectionModel {
private(set) var selection: Selection // entities + sub-entities
var signature: SelectionSignature { /* derived */ }
func select(_ items: [EntityRef], from source: SelectionSource)
func add(_ items: [EntityRef], from source: SelectionSource)
func clear(from source: SelectionSource)
}
enum SelectionSource { case canvasPick, itemsTray, historyTray, command }
- The resolver subscribes to
signatureand recomputesResolvedToolSeton every change. - The Items tray subscribes: highlight and scroll-into-view the matching rows.
- The History tray subscribes: highlight the step that produced the selected geometry.
- The canvas subscribes: draw selection highlights and the primary gizmo.
4.2 Bidirectional flow
Selection can originate from any surface and every surface reflects it:
- Canvas pick to all trays: pick geometry, both trays highlight and scroll. This is Reveal in Items and Reveal in History happening automatically.
- Items row tap to canvas and history: selects the entity, canvas highlights, history highlights the creating step.
- History step tap to canvas and items: selects that step’s output geometry.
- Command or API to all: a palette command or script can set selection, and all surfaces follow.
The from source parameter prevents feedback loops: a surface ignores change notifications it originated.
4.3 Hover preview
On pointer or Pencil hover over a tray row, preview-highlight the entity in the canvas without committing selection. Releases on hover-out. This gives fast scanning without disturbing the active selection or the resolved tool set.
4.4 SwiftUI mapping
SelectionModelis@Observable, injected into the environment.- The resolver is a pure function, recomputed in a derived property or a small view model observing
signature. - The tool strip, both trays, and the canvas overlay are views observing the model. No surface owns selection state, they all read and write the one model.
Part 5. Handoff notes and open decisions
5.1 Persistent sub-entity identity: BRepGraph
The right tray’s reorder, edit, and breakpoint behaviour all assume an edge or face keeps a stable identity across rebuilds: when an upstream parameter changes and the body recomputes, the fillet that referenced an edge has to find the same edge again. This is the topological naming problem, and it is handled at the kernel layer by OCCT 8’s BRepGraph rather than left to the UI.
BRepGraph is a graph-based, mutation-aware representation of BRep topology, complementary to TopoDS_Shape, with roundtrip conversion to and from it. The facilities the tray depends on:
- Typed node identifiers (BRepGraph_NodeId). Stable handles for faces, edges and vertices. A history step stores node IDs for the geometry it consumes, not transient TopoDS pointers, so a reference survives recompute.
- Mutation tracking and History. The graph is mutation-aware and tracks history, so an edit propagates through dependents with identities preserved, which is exactly the propagation the right tray visualises.
- Bidirectional traversal with O(1) reverse indices. Edge-to-face and vertex-to-edge are direct lookups, which makes the canvas-to-tree binding (Part 4) and Show dependencies cheap.
- EditorView and roundtrip to TopoDS_Shape. Edits run against the graph, then convert back so the rest of the OCCT algorithm set still applies.
Implication for the tray: a step’s Consumes references (Part 3.5) are BRepGraph node IDs. Show dependencies and reorder validation read the graph’s parent and child explorers directly. Direct-mode-only members that never recompute can skip this, another reason the right tray is a module.
Note: BRepGraph shipped in OCCT 8.0 and its API is recent, so pin the version and treat class and method names as subject to minor change across point releases.
5.2 Open decisions
- Direct edits in hybrid mode. Do direct manipulations always create history steps, or can some be fast and ephemeral. Recommend: record them as steps so history stays complete, with a setting for users who want pure direct mode.
- Resolver personalization. How much recency reordering, if any. Recommend: overflow only, visible strip frozen per signature.
- Reorder strictness. How aggressively to block dependency-violating drops versus allowing them and flagging the break. Recommend: block hard violations, flag soft ones.
- Gizmo defaults. Which selections earn
.autoGizmo. Recommend: only Body, Face, Edge, and Sketch primaries, where the default is unambiguous and reversible. - Tray presence per family member. Confirm which members ship the right tray at all. Lighter tools take the left tray only.
5.3 What design receives from this
- The resolution table (Part 1.5) as the layout contract for the dynamic tool strip, per selection signature.
- Both per-type exposure tables (Parts 2.4 and 3.5) as the row and card content contracts.
- The state lists (Parts 2.3 and 3.4) as the screens to design.
- The binding contract (Part 4) as the interaction model the prototype must honour, so selecting anywhere highlights everywhere.