Viewer AI authoring operation matrix (P15A)¶
Reviewed native authoring lets the assistant propose creation, copy/array, deletion, placement and relationship edits. The viewer's own builders and modelling commands carry them out after review. This page records what is supported, how each operation is previewed, committed, undone and proven, and what is refused. It belongs to P15A in the implementation ledger; the user-facing description is in the assistant guide.
Contract¶
model.authoring (version 1, apps/viewer/src/lib/actions/model-authoring.ts) is a sibling of P04's model.changes, not a new version of it. Data corrections compare one scalar with an expected scalar and are keyed per value. Authoring creates identities, takes lengths in a declared unit and frame, and refers to elements created earlier in the same batch. Folding it into the model.changes union would have changed changeKey, changeField, the scalar value reader and the receipt row semantics for every existing producer. As a sibling, model.changes v1 stays byte-for-byte compatible. Both kinds share:
- GlobalId resolution (
resolve-global-id.ts, now also finding elements created this session); - the staleness rule (batch digest plus
mutationVersion); - one
runTransactionper model, all-or-nothing across models; - the receipt type, library and Changes-panel list;
undoModelChangesthroughrevertChangeOperation.
A ModelChangeReceipt carries kind: 'model.authoring' for authoring; old receipts (no kind) decode unchanged.
Each batch declares "units": "m" | "mm" and "frame": "storey-local". Coordinates are storey-local [x, y, z], Z up, which is the frame the in-store builders take. Angles are degrees, counter-clockwise from above. A missing unit or any other frame is refused. Verbatim native expected snapshots retain their original mixed units and source IDs; they are comparison pins, not writer lengths. Every command length must fall in a builder-plausible metre range, so 200 declared as metres for a wall thickness is refused as a probable unit mistake. Existing elements are addressed as {globalId, ifcClass, name} and must still have that class (or a subtype) and name. Elements created earlier in the batch are addressed by {ref}; a ref must name an earlier creating operation. A batch holds at most 200 operations.
Matrix¶
Preview always starts with resolution, the expected-state checks and the native edit gate (mutationDenial). Then a dry run executes the ready rows of each model, in batch order, against a draft of the model's overlay (MutablePropertyView.prepareAtomic). The draft is never published, so the builders' own validation decides, with earlier operations of the batch in place. A row whose builder refuses is Refused by the model, with the builder's message. A row that uses a refused or excluded creation is Needs another row. The 3D ghosts are drawn on the proposal overlay channel and cleared on apply, hide and unmount.
| Operation | Native API (commit) | Preview validation | Ghost | Undo | Export/reparse proof | Limits |
|---|---|---|---|---|---|---|
element.reassignStorey |
Canonical reassignElementsToStoreyInStore through native modelling history |
Explicit same-model product/source/destination; complete current plan and loaded-source/view pin; atomic dependency/ownership/frame validation | No displaced ghost: identities and world frame stay fixed; review reports dependency count | One grouped native batch, complete graph/journal restoration | Real Bonsai imported hosts/openings and authored walls, native WASM m/mm translated/rotated/tilted frames, both rich and attached assistant wires, mounted Apply/Undo | 200 roots, 5,000 dependent products; local placements only; ambiguous/shared/cyclic/unreadable ownership or frames refuse; native candidate capture bounded to20 storeys |
element.create IfcWall, IfcSlab, IfcRoof, IfcPlate, IfcColumn, IfcBeam, IfcMember, IfcSpace |
Store addWall / addSlab / addRoof / addPlate / addColumn / addBeam / addMember / addSpace (addOrdinaryElementInStore), explicit generated GlobalId |
Storey GlobalId resolves to an IfcBuildingStorey; dry run of addOrdinaryElementInStore with the same storey-placement preparation |
Native footprint prism or parameterised section sweep in the shared native linear profile frame | One batch with the rest | Created wall found by GlobalId, contained in its storey, no dangling # reference |
Rectangle or bounded polygon footprints; all nine native parameterised profile sections for beams/columns/members. Positive fillet radii are exported; each review row names the rounded details omitted by its sharp-corner preview. No topology/engineering validity certification; no type/layer defaults applied |
hosted.edit Offset/Sill/OverallWidth/OverallHeight |
Canonical editHostedElementInStore through recordModellingEdit |
Complete native binding/position/size snapshot; source pin; native fit/overlap/placement/schema checks on an unpublished draft and again at commit | Existing hosted-slide post-edit opening bounds; limitation shown before Apply | One transaction | Stable host/opening/filling identities and effective metadata; independent STEP reparse | Wall hosts, existing native IFC2X3/IFC4/IFC4X3 source; bare-opening movement, sizing only with readable door/window geometry; bounding preview is not detailed cut/filling solid parity |
hosted.create door, window, opening |
Store addHostedFill (addHostedElementInStore) in the transaction's batch |
Host is a wall (existing or created in the batch); dry run of the hosted builder (fit, overlap, schema) | Cut box along the host axis at offset/sill | Same | IfcRelVoidsElement voids the created wall; IfcRelFillsElement fills the opening |
Wall hosts only; slab openings refused by the contract |
walls.join |
bim.store.joinWalls through recordModellingEdit |
Both walls; dry run of joinWallsInStore |
None (row text) | Same | IfcRelConnectsPathElements exported |
IFC2X3/IFC4/IFC4X3 only (builder refusal) |
type.assign |
bim.store.addElementType (for a new type) and assignType through recordModellingEdit |
Expected current type name; type GlobalId resolves to an IfcTypeObject with the stated name; Already of this type |
None | Same | Reparsed IfcRelDefinesByType names the type |
New types take only ifcClass and Name |
type.detach |
Model inspector detachFromType through recordModellingEdit |
Exact current type GlobalId/Name, expected occurrence class/name, source/view revision and edit gate |
Resulting geometry explicitly unavailable | Same canonical writer, one grouped Undo/Redo | Independently reparsed relationship removes only selected occurrence; type and shared peers survive | Deletes an emptied relationship; never fabricates inherited geometry |
classification.add |
Native Properties Add classification, shared draft writer | Explicit Classification.Name, schema-specific Reference.Identification/ItemReference, optional Name, expected target identity and source/edit gates |
Metadata only; no geometric preview | Same native construction, grouped Undo/Redo | Independent STEP reparse retains old assignments and adds the supplied code | Existing targets only; native current-Name system reuse; no classification inference |
material.assign |
bim.store.addMaterial (when create) and assignMaterial through recordModellingEdit |
Expected current material name; exactly one IfcMaterial of that name, or create: true; Already this material |
None | Same | Reparsed IfcRelAssociatesMaterial names the material |
Single IfcMaterial only; use material.layers for reviewed layer sets |
material.layers |
Shared Model inspector layer constructor and coupled native wall-section writer | Complete nativeLayers.expected in both capture routes; expected occurrence/type populations, peer identities and source revisions |
Native wall draft body when available, with limits disclosed; type/panel assignments disclose unchanged dimensions and unavailable changed-body preview | Grouped native Undo/Redo, including material graph and coupled wall body | Committed SketchUp source plus native authored wall/panels: independent ordered layer/material readback, source/type peer identities, SI/metre/millimetre variants; real WASM wall body matches ghost | Existing wall/slab/roof/plate, explicit occurrence/type scope; non-root materials bind by model/expressId/current Name; complete type population/peers required for type scope; no unassigned defaults or engineering inference |
element.move |
commitElementTransform (the Move tool's commit: hosted openings and fillings travel, joined walls follow) |
Transform planner refusals; storey workplane; optional from origin within 1 mm |
Element meshes moved | Same | Reparsed placement origin moved by the delta | Horizontal only; vertical moves and storey changes are refused |
element.rotate |
commitElementTransform, optional explicit storey-local pivot; otherwise native placement origin |
Upright placement with explicit RefDirection; optional fromDeg; complete current nativePlacement required for supplied pivot |
Native meshes turned about the same pivot | Same | Reparsed angle and placement; grouped Undo | About Z only |
element.align |
Canonical native Align planner/writer and carrier/join hooks | Complete native placement pins for reference and all targets; explicit fresh native geometry preparation with owning source/view/workplane leases | Native bounds prisms for moving roots; carried dependants follow their host | Same | Native mesh bounds, stationary reference, STEP ownership and grouped Undo | Earlier same-model geometry edits must be applied first; bounds preview is not a full solid |
element.split |
Existing splitElementsInStore inside the shared reviewed transaction and recordModellingCommit |
Full native shape/provenance/placement snapshot, exact model-local unique identity, pinned source/view revision; native dry-run and write recheck | Existing marker mesh primitives at the native cut; direct storey parent proof, explicit unavailable projection, no resulting-solid or engineering parity claim | Same canonical writer; explicit unique-target rows with no truncation | Independently reparsed piece shapes/frames, larger-piece identity, one derived identity, native metadata/opening policy; one grouped undo | Supported native wall/linear/plan slab-like targets only; native boundary refusals remain authoritative |
element.resize |
Full commitElementSize: canonical size editor through recordModellingEdit, plus occurrence-only material layer clone/rescale |
Expected full native editable dimensions; shared geometry/layer write on an unpublished draft; native hosted-fit and shared-ownership refusal | Native post-edit push/pull prism or section sweep; bounded outline, direct storey-frame proof; each row discloses unavailable placement/geometry, sharp fillets and outer-body-only preview | Same, including geometry and layers | Stable identity, reparsed dimensions and layer totals; sibling/type unchanged | Wall height/thickness, flat slab/roof/plate thickness, straight beam/column/member length and rectangular cross section; fixed start/end rule. Existing GlobalId targets only |
element.profile |
Existing native setElementProfile section writer inside the reviewed transaction |
Expected native section; shared centred-section reader/validator and emit core on an unpublished draft | Existing section sweep with current native axes; sharp-corner fillet disclosure or explicit preview unavailability | Same | Reparsed native profile class/dimensions; undo restores the original section | Nine existing native variants on centred straight beam/column/member extrusions; no arbitrary geometry inferred |
element.copy / element.array |
Native copyElements / copyBatchInStore; arrays use arrayCopyTransforms |
Pinned source class/name, optional source placement, same-model target storey, unique output refs, finite declared-unit transforms; unpublished native copy dry run | Exact native copyGhosts; first 64 placements, missing source/draft meshes explicitly disclosed |
Same transaction/receipt; one applied identity per copied root | Fresh GUIDs, native void/fill dependents, placements and units survive export/reparse; real Bonsai source/copy WASM geometry and cut volume agree | One root per operation, 200 copy roots per proposal; existing source before earlier edits, native hosted/assembly/work refusals; copied refs feed copies/type/material and wall-only hosted/join operations; hosted cut ghosts on draft copies are unavailable |
element.trimExtend IfcWall / IfcBeam / IfcMember |
Canonical trimExtendElementInStore inside the native modelling transaction |
Verbatim nativeTrimExtendExpected target pin, existing wall boundary pin or explicit line; whole-batch unpublished native dry run; captured source/overlay freshness |
Native post-edit wall outer body or section sweep in its actual sloped frame; sharp-corner fillet omissions, adjoining wall updates and dependent-boundary ghost unavailability disclosed | One compound native batch; host refit and joins included | Both ends, trim/extend, GUIDs, hosted placement, native join, m/mm files and commands, N-model isolation; real WASM sloped hollow/rounded section bounds and volume | Native reach, host-crossing and shared-geometry refusals remain authoritative. Expected source fields are verbatim mixed native units; explicit click/line lengths use batch units |
curtainWall.create |
curtainWallLayout, addCurtainWallToStore and existing complete hierarchy publication |
Exact native params, dimensionless counts vs declared-unit offsets/profiles, one/N model ownership, unique complete Root population, source lease, per-wall panel and additive part bounds | Actual command rectangular-member ghosts; differing/other profiles, nondefault panel thickness, unreadable frames or live-edited source placement chains explicitly unavailable | Complete root/member/panel history, hierarchy, remesh and one-batch Undo/Redo | Independent IFC2X¾/4X3 reparse, complete aggregate/containment, every-part native WASM geometry and supported world-bound/volume preview proof | Straight native aggregate only; no curved curtain systems or inferred engineering suitability |
stair.create, railing.create |
addStairToStore, addRailingToStore through existing viewer actions |
Explicit native PascalCase params, storey, declared m/mm lengths; stair Direction radians; bounded steps/posts; native schema/ownership/unit validation | Existing command ghosts: solid steps omit waist; square rail/posts approximate round sections; custom diameters/vertical rail segments unavailable | Native one-batch history, hierarchy and flight/railing remesh | Independent IFC2X3/IFC4/IFC4X3 reparse, GUIDs, containment/aggregation, native WASM body bounds and volume | Straight single-flight stairs and explicit polyline railings only; no inferred engineering suitability |
stair.resize |
readStairDimensions, editStairDimensionsInStore |
Verbatim pure native expected snapshot plus Width/RiserHeight/TreadLength/WaistThickness patch | Native edit preview unavailable, disclosed | Existing native history/remesh | Independent dimension reparse and original root/flight identity on undo | No NumberOfRisers patch or waist-removal operation |
stair.delete, railing.delete, stair.replace, railing.replace |
Native stair assembly removal, generic railing removal, replaceElementInStore |
Stair/railing targets only; native assembly/host/foreign ownership refusal; new replacement storey/params | Existing replacement command ghost with same disclosures; removal body unavailable | Existing native removal completion and one-batch undo/redo | Independent no-dangling-reference export, replacement GUID, complete root/flight removal and original identity restoration | Replacement creates a new product record; default fresh or explicitly supplied unique GlobalId; no general product replacement expansion |
element.delete |
Store removeEntity |
Expected class/name; allowed classes (walls, slabs, roofs, plates, columns, beams, members, spaces, doors, windows, coverings, furnishing, proxies); refused for assemblies and for hosts of openings; dry run of the removal | Element meshes in red | Same | Element absent after reparse, no dangling # reference |
A deleted door or window leaves its opening; openings themselves are not deleted |
Commit re-runs the preview and refuses a stale one. It writes each model's approved rows inside one runTransaction with the store's gated actions, so the edit gate, collaboration role and workflow lock apply. The created, hosted, joined, retyped, re-materialled, moved and turned elements are re-meshed through the wasm re-mesh service (created cause when anything was created, else hostsChanged for moves). Undo and redo re-mesh the same batch.
Explicit refusals¶
Not offered and refused by the contract with a reason: grids, free-standing doors and windows, vertical moves, independent storey or containment changes (copy target storey is supported), group and zone membership, arbitrary layer-set authoring, and any operation on IFC5/IFCX models that the builders refuse. Each has a native tool in the Model workspace. Adding one here needs its own parameter validation, preview and export proof. Structural intent and missing dimensions are never invented: the guidance tells the provider to ask instead.
Evidence¶
model-authoring-copy*.test.ts* (#7202) uses the committed SketchUp source for draft/commit/export/undo/ref/dependency/federation proofs, the committed Bonsai source and real canonical WASM for ghost/export cut-volume and opening retention, and the stated two-storey invariant fixture for metre/millimetre files and polar target-storey placements. The mounted native review discloses root counts and ghost limits and excludes dependent edits when their copy is unapproved. Copy previews retain the loaded source/store/overlay identities, the native overlay revision (including skip-history writes), and the loader full source hash when available; they conservatively refuse reloads rather than inventing a sampled fingerprint. Native copying and coordinate conversion are reused, with no alternative pipeline.
Tests run against the committed SketchUp-authored building-architecture.ifc (IFC4, millimetres), using apps/viewer/src/test/authoring-sample-fixture.ts:
lib/actions/model-authoring.test.ts(8 tests) covers:- contract refusals: units, frame, unit mistakes, classes, refs, vertical moves;
- preview resolution with no writes;
- builder refusals and blocked dependents;
- the edit gate;
- one undo batch with re-mesh, export/reparse referential integrity (containment, type, material, void, fill, join), and undo;
- move, rotate and delete with reparse proof, expected-origin conflict and single-use previews;
- session-created GlobalIds and refusal to delete a host;
- receipt decoding.
lib/actions/model-authoring-ghost.test.tschecks that the wall and window ghosts match the committed builder geometry in the storey frame.components/viewer/actions/ModelAuthoringReview.test.tsxmounts the card and covers the Edit-mode gate, dependent exclusion, the ghost upload and clear, apply, the receipt and undo.components/viewer/assistant/ModelChangeProposal.test.tsxcovers the authoring proposal card, the native review and a refused malformed answer.
The copy-family tests also cover a pinned native write with two loaded models and refusal of ambiguous GlobalIds. No real provider answer or viewport screenshot is recorded yet.
Trim/Extend #7262 acceptance uses the committed SketchUp source, the stated native metre/millimetre storey fixture, real canonical WASM and the actual controlled Assistant SSE transport. It does not establish a live provider answer or rendered viewport quality. The selected target snapshot comes from the same native readers used by the writer; source/revision changes invalidate approval.