Skip to content

feat: deliver the post-#830 fork delta (camera-prim view restore, bolt-group rename, whole-sheet export, V07 presets) - #832

Merged
Blogbotana merged 4 commits into
Apibim-com:masterfrom
axisrow:chore/upstream-delta-1105
Nov 4, 2026
Merged

Blogbotana merged 4 commits into
Apibim-com:masterfrom
axisrow:chore/upstream-delta-1105

Conversation

@axisrow

@axisrow axisrow commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

What this changes

Second delivery wave after #830, same arrangement (one batch PR for the
delta; the branch is cut from the current master, so the diff below is
exactly the delta): 37 files, +3129/−265. This is the fork work that landed
after the #830 branch was cut. Issue references are sanitized as before:
only upstream cards are referenced; fork card numbers are dropped.

What is inside

Four pieces, four commits, each with its acceptance suite:

  • W01 saved views: restore through the camera prim layer (W01: Saved views #537) — the
    session file becomes the views layer: crates/session/src/views_layer.rs
    maps the view book to the camera-prim layer and back through the reader's
    manifest commitment. The plane becomes the standard placed camera pose;
    the display record and W01 provenance ride on bim:view:* attributes;
    empty folders ride on one bim:view:folders string[] on the views scope
    (wrong-typed records are refused); the default-view mark rides on
    bim:view:default. --file keeps its V12 meaning, legacy JSON sidecars
    (versions 1–2) still load until the next save rewrites them, and the
    window's sidecar becomes the sibling <store>.views.usda. Restoring a
    view restores its camera with provenance; the store head stays
    byte-identical across every view operation. Layer-engine changes in
    crates/layer-engine/src/authored/views.rs; design note
    docs/design/w01-saved-views.md.
  • update-bolt-group rename carried into the authored layer — the
    command was advertised and accepted, but no path carried the label: the
    stored bim:name kept its old value on reopen in the classic path and,
    since the U19 scope routing, in the scoped path alike. The rename now
    travels with the command (UpdateBoltGroup.name, None keeps the
    label), the shared amendment authoring writes a Change::Label next to
    the replaced record, the declared footprint gains the label write, the
    replay comparison matches the name, and the content fingerprint (v2)
    binds the operation id to the requested label, so a rename under a
    retried op id is reuse, never a silent replay.
    (crates/commands/src/bolt.rs, crates/steel/src/model/command.rs,
    apps/beamcraft-cli/tests/bolt_groups.rs.)
  • whole-sheet render on every export surface — paper is Y-up while SVG
    is Y-down, and both SVG writers mirrored the sheet (a 2500 mm dimension
    read 5200 and the title block sat at the top); strokes now flip into SVG
    space at emission and the PDF surface stays Y-up. The pdf branch of
    export-drawing passed only the primary projection, so a sheet document
    printed without frame, title block, views, sections, details and drawn
    objects; the sheet renderer now lays out one shared SheetLayout that
    both export_svg and the new export_pdf consume, and a --sheet that
    contradicts the document's own paper refuses. No exporter read
    Drawing::visible_drafting, so drawn objects never reached paper; a
    shared drafting renderer maps every geometry kind to the same
    contributor-original strokes with authored line types, included in bounds
    and fit checks on the loose SVG and both PDF surfaces; the loose SVG
    bounds now cover the flipped drafting so objects beyond the paper extent
    are not clipped. (crates/engine/src/drawing/,
    apps/beamcraft-cli/src/drawing_cmd.rs,
    apps/beamcraft-cli/tests/pick/drawing/sheet.rs.)
  • V07 discipline view presets (V07: Discipline view presets #509) — a preset is a named bundle of
    view display state composing existing primitives only: the element-kind
    filter, the view depth slab (the underlay), the U12 representation level,
    the name of a saved V17 colour set and the grid toggle. Defined, listed,
    applied and removed through the engine API (beamcraft_engine::presets,
    the bounded PresetBook, mirrored in beamcraft_host) and the CLI
    (views define-preset / list-presets / apply-preset / remove-preset).
    Applying writes exactly those five fields onto a saved view: the camera
    and the W01 provenance stay, the store head stays byte-identical, the
    snapshot token never moves. Persistence is a reasoned deviation from the
    saved-views precedent — presets are store-independent display state, so
    they live in a bounded, versioned personal configuration file, recorded
    in docs/design/v07-view-presets.md. The two fields the preset needs on
    the view itself are additive optional keys (bim:view:representation,
    bim:view:colourSet), so files written by earlier builds still read. The
    views layer's depth admission now mirrors the engine rule: a one-sided
    zero slab is legal.

Invariants

Same as #830, and unchanged by this wave:

  • Fork CI is not part of this PR. .github/ is untouched.
  • Zero tracked .py files.
  • Fork-only paths (fork workflows, sync tooling under tools/, docs/plan/
    owner-decision notes) are excluded; product code, tests and
    docs/design/ notes are included.
  • Commits carry no Co-Authored-By trailers.

How you verified it

On this exact tree (the branch head), on macOS:

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace — targeted instead: nextest across the
    touched crates (session, layer-engine, engine, host, commands, steel,
    cli, ui): 2027 passed, 0 failed. The full workspace runs in this
    repository's CI.

Red-first and acceptance evidence per piece:

  • Bolt rename: the regression tests were written against both paths
    (classic and scoped) and failed before the fix; they cover the rename
    committing with a declared label write, surviving a reopen and a scoped
    publish, an edit without --name keeping the label, the retry replaying,
    a different name under the same op id refused, and a malformed label
    refused without a revision.
  • Sheet render: the defect evidence is visual — the mirrored sheet (a
    2500 mm dimension reading 5200, the title block at the top), PDF sheet
    documents printing without their frame and views, and drawn objects never
    reaching paper. The whole-sheet CLI test mirrors that scenario and asserts
    produced-file coordinates (and the edit exit code), and the sheet PDF was
    rendered and compared against the audit images.
  • View presets: CLI-first — every command exercised end to end through
    the CLI, with the e2e test asserting the whole open-revision reply (store
    head byte-identical, snapshot token unmoved) and the enum refusals
    phrased so a listed option never answers unknown.
  • Camera-prim restore: layer round-trip digest stability, legacy
    sidecar migration (v1/v2, including a default-marked view), and the
    folder-record and wrong-typed-record refusals.

Never-crash

  • No new unwrap/expect/panic!/todo! outside tests — clippy
    denies them in non-test code, and the lint above runs that denial.
  • No indexing or slicing with input-derived positions; allocations
    sized by input are capped.
  • No unsafe.

Provenance

  • Nothing taken from Tekla Structures or any Trimble product.
  • No GPL/AGPL code copied in.
  • No new assets; ATTRIBUTION.md is unchanged by this diff.

Issue links and completion scope

Refs #537 (W01 saved views, the restore slice) and #509 (V07 discipline
view presets). None are linked as Fixes: which cards this delivery closes
is the maintainer's call on review. The sheet-export and bolt-rename fixes
have no upstream card; they are described here in prose.

Anything a reviewer should look at first

The V07 persistence deviation (personal config file instead of a store
sidecar) and its reasoning in docs/design/v07-view-presets.md; and the
sheet-renderer unification, where export_pdf joins export_svg on one
SheetLayout — the largest single change in the batch.

🤖 Generated with Claude Code

* feat: keep empty view folders in the views layer record

A book folder with no view lived only in the session file: the camera-prim
views layer stored each view's folder but had no record for a folder the user
created and left empty, so a restored book lost its tree. The layer profile
now authors the folders no view uses as one string[] attribute on the views
scope, sorted and unique, with the same path rules as the book; the reader
returns them beside the cameras. The layer text stays canonical, so the same
content still commits one digest.

* feat: restore saved views through the camera prim layer

The saved views were restored from a JSON sidecar while the layer engine's
views layer existed only as a profile: the acceptance's restore through API
and CLI had no USD form on disk, and a store could not hand its views to
another tool as cameras. The session file now is the views layer:
`beamcraft_host::views_layer` maps the view book to the camera-prim layer and
back through the reader's manifest commitment - the plane becomes the
standard placed camera pose, the display record and the W01 provenance ride
on `bim:view:*` attributes, and the folders no view uses come back from the
scope record. The V12 commands keep their surface: `--file` stays the
caller-chosen path, a save writes the layer form, and a legacy JSON sidecar
(versions 1 and 2) still loads until the next save rewrites it in the layer
form. The window's sidecar becomes the sibling `<store>.views.usda`, with the
legacy `<store>.views.json` still loading. Restoring a view restores its
camera with its provenance, and the store head stays byte-identical across
every view operation, so model keys and numbering never move.

Refs #537

* refactor: keep the views layer mapping in the session crate

The views-layer mapping landed in beamcraft-host, whose module-table row
does not allow the layer engine: the boundary check refused the edge, and
widening the host row would let the disk-only crate speak USD. The session
crate is the shared app-state layer both applications already depend on
for exactly this kind of per-store state, and its table row allows the
layer engine, so `views_layer` moves there wholesale. The reader plumbing
now reuses the crate's own pipeline helpers (the open request, the digest,
the control) instead of private copies, and the host manifest edits are
reverted, so the lock file stays at master's.

Refs #537

* fix: carry the default-view mark and refuse wrong-typed folder records

Two review fixes on the restore slice. The JSON sidecar round-trips the
book's default-view mark, but the layer profile had no field for it, so a
migrated default view lost its mark and the window's ensure-default pass
silently replaced it with a fresh 3D View. The mark now rides on the prim
as `bim:view:default` (bool, in the descriptor and the field table), maps
both ways, and reads as unmarked when absent, since the mark predates the
layer form; the migration test carries a default-marked view.

The reader also matched `bim:view:folders` by name and type together, so
a record of another USD type was silently ignored — the presence rule of
every other record is by name alone, with a wrong type a refusal. The
scope record now checks presence first and refuses a foreign type or a
non-array value.

Refs #537

* fix: fold the folder-record presence into the match guard

Clippy at `-D warnings` collapses the nested `if`: the scope arm now guards
on the presence of `bim:view:folders` by name alone, and the arm body keeps
the typed read that refuses a foreign type or a non-array value. No
behavior change.

Refs #537
`update-bolt-group --name` was advertised by the registry and accepted by
the CLI, but no path carried the label: the command had no name field, and
the shared amendment authoring replaced only the group's shape record, so
the stored `bim:name` kept its old value on reopen — in the classic path
and, since the U19 routing work, in the scoped path alike.

The rename now travels with the command (`UpdateBoltGroup.name`, `None`
keeps the stored label) and the one shared amendment authoring writes a
`Change::Label` next to the replaced record, which the scoped author routes
through the section layer the same way as creation. The declared footprint
gains the label write while a rename is in the batch, the replay comparison
matches the name like creation does, and the content fingerprint (v2) binds
the operation id to the requested label, so a rename under a retried op id
is reuse, never a silent replay.

Tests: a rename commits with a declared label write, survives a reopen and
a scoped publish, an edit without --name keeps the label, the retry
replays, a different name under the same op id is refused, and a malformed
label is refused without a revision.
A discipline view preset is a named bundle of view display state that composes
existing primitives only: the element-kind filter (whose reference kind is the
reference visibility), the view depth slab above and below the plane (the
underlay), the U12 representation level, the name of a saved V17 colour set and
the grid toggle. Defined, listed, applied and removed through the engine API
(beamcraft_engine::presets, the bounded PresetBook) and the CLI (views
define-preset, list-presets, apply-preset, remove-preset).

Applying a preset writes exactly those five fields onto a saved view and
nothing else: the camera and the W01 provenance stay, the store head stays
byte-identical, and the snapshot token never moves — proven by the e2e test
against the whole open-revision reply.

Persistence is a reasoned deviation from the saved-views precedent, recorded in
docs/design/v07-view-presets.md: presets are store-independent display state
(one component is a per-user colour set), so they live in a bounded, versioned
personal configuration file, not in a store sidecar. The two fields the preset
needed on the view itself ride the additive-optional-keys precedent of the
saved-views JSON format: additive optional keys in the JSON view format and two
additive attributes in the W01 views layer (bim:view:representation,
bim:view:colourSet), so files written by earlier builds still read.

Along the way the views layer's depth admission is aligned with the engine,
which is the source of truth: a zero distance on one side of the slab is
engine-legal (a plan view showing below the plane only) and the layer now
mirrors the rule. Layers written earlier still read.

Refs #509.

Also phrases the define-preset enum refusals so a listed option never answers
unknown.
Paper is Y-up while SVG is Y-down, so both SVG writers mirrored the sheet:
a 2500 mm dimension read 5200 and the title block sat at the top. Both
writers now flip strokes into SVG space at emission; the PDF surface stays
Y-up and is untouched by the flip.

The pdf branch of export-drawing passed only the primary projection, so a
sheet document printed without its frame, title block, placed views,
sections, details and drawn objects. The sheet renderer now lays out one
shared SheetLayout that both export_svg and the new export_pdf consume, and
the command resolves every derived view once for either surface. A --sheet
that contradicts a sheet document's own paper refuses instead of silently
printing on the wrong page.

No exporter read Drawing::visible_drafting, so drawn objects never reached
paper. A shared drafting renderer maps every geometry kind (line, polyline,
arc, circle, rectangle, text, hatch outline, symbol marker, free dimension)
to the same contributor-original strokes; each object paints as its own path
with its authored line type (dash patterns in SVG and PDF), and the loose
SVG and both loose/sheet PDF surfaces include it in bounds and fit checks.

The whole-sheet CLI test mirrors the visual audit scenario and asserts
produced-file coordinates, and the sheet PDF was rendered and compared
against the audit images.

Also bounds the loose export in painted space: the loose SVG viewBox used to
mix flipped dimension groups with unflipped drafted strokes, so a drawn
object beyond the view's paper extent painted outside the viewport and was
clipped; the bounds now cover the flipped drafting, with a loose-route
regression test carrying a drawn object at paper y 500. The whole-sheet CLI
test asserted a value against itself; it asserts the edit exit code instead.
@axisrow
axisrow requested a review from Blogbotana as a code owner November 4, 2026 16:39

@axisrow axisrow left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review of PR #832 (head 0641bd3)

Verdict: approved. No finding requires a change.

Scope: four commits over 37 files, about +3.1k / -265: the W01 saved-views restore through the camera-prim layer, the update-bolt-group rename carried into the authored layer, the V07 discipline view presets, and the whole-sheet upright export fix.

What I checked and found sound:

  • Sheet export. The paper-to-SVG flip happens at emission only (path_element and flipped_about, about the sheet height on the sheet route and about zero on the loose route), while the PDF surface stays Y-up and unflipped. The sheet fit check still runs in paper space over every layer plus the primary dimension groups, so nothing can slip out of the frame. The loose SVG viewBox is now computed in painted (flipped) space, which fixes the mixed-space clipping the regression test carries. The pdf branch of export-drawing resolves every derived view once for either surface and refuses a --sheet that contradicts the document paper. The coordinate assertions in the new CLI test (SVG frame edges at y 10 and 410 for A3, the PDF MediaBox and the mm-to-pt anchors) are consistent with the writers.
  • Drafting on paper. drafting_layers maps every geometry kind to contributor-original strokes, paints each object as its own path with its authored dash pattern in both SVG and PDF, and participates in bounds and fit checks on all three surfaces. Curves chord at one chord per millimetre with a 16..256 clamp; a zero-length or non-finite measure renders nothing instead of dividing by zero.
  • Saved views layer. bytes and read_layer keep the mapping canonical (identity order, sorted and unique empty folders), the folder record presence rule matches the rest of the profile (by name alone, a wrong type refuses), the default mark is additive and reads unmarked when absent, and legacy JSON sidecars still load and migrate on the next save in both the CLI and the window. The store head stays byte-identical across view operations, asserted in tests.
  • Bolt group rename. The label travels in the command, the shared amendment authoring emits Change::Label in the same transaction, the footprint declares the label write only while a rename is in the batch, and the v2 content fingerprint binds the name, so a different name under a retried op id is reuse. Both the classic and the scoped publish paths are covered by tests, including keeping the stored label when --name is absent.
  • Presets. Engine bounds are tight (finite depths, positive thickness, byte-bounded names, case-insensitive uniqueness, the 64 cap), apply_to writes exactly the five display fields, and the persistence deviation is reasoned and recorded in docs/design/v07-view-presets.md. The JSON book round trips; the two new view fields ride additive optional keys in both the JSON format and the layer attributes, so files written by earlier builds still read. The e2e test proves the acceptance: the whole open-revision reply stays byte-identical across every preset operation.

Non-blocking notes (no change requested):

  • The four preset commands accept and ignore --store; this matches the existing list-views precedent, but it advertises an option the command never reads.
  • In the layer-engine tests the new representation case mutates the token to an empty string, which exercises the writer admission rather than the reader wrong-type refusal the comment names; the wrong-type path itself is covered by the shared property machinery tested on the default mark and the folders record.
  • After a window migration the legacy .views.json stays on disk and is never read again; harmless, and deleting it would be a separate decision.

Head commit reviewed: 0641bd3, matching this checkout.

@Blogbotana
Blogbotana merged commit 66ee62e into Apibim-com:master Nov 4, 2026
91 of 97 checks passed
axisrow added a commit to axisrow/BeamCraft that referenced this pull request Nov 5, 2026
…ough the 1104/1105 delta waves

Connect the fork's master to the upstream history it has tracked only by
content since the steel wave: every upstream commit from Apibim-com#758 through the
Apibim-com#830/Apibim-com#832 delta merges lands here with its own SHA and parents. The merged
tree equals the fork master tree byte for byte — the fork already carries
every upstream change by content (Apibim-com#758-Apibim-com#827 through the 11-03 absorb, the
delta waves through the deliveries themselves) — so this merge changes no
fork behaviour; it restores the shared ancestry. The fork-only form stays:
the recorded-gate ci.yml, the no-Python rule (the upstream ci tooling does
not ride along; card #385 decides its fate), the fork policy documents and
the fork's newer fixes that the upstream copies predate. The accumulated
fork delta leaves as the next single batch pull request toward upstream.

BeamCraft-Test-Gate: {"version":1,"commit":"e692e6a5749f02be7e21f0d64911e91a098494fa","tree":"80f09fd5052e4b62b864f6de3212f27e9bf52f2e","base":"e692e6a5749f02be7e21f0d64911e91a098494fa","merge_parent":"66ee62e2446f93eab7d5bf7d011dc7e39818ff44","os":"macos","arch":"aarch64","scope":"full","ui_independent_reason":null,"ui_tested":true,"rustc":"rustc 1.99.1 (58cf8f9c3 2026-10-13)","cargo":"cargo 1.99.1 (5f94df478 2026-08-27)","nextest":"cargo-nextest 0.9.149 (3fe4ef319 2026-10-20)\nrelease: 0.9.149\ncommit-hash: 3fe4ef31972f24e0c94cfe291718eab0fe678260\ncommit-date: 2026-10-20\nhost: aarch64-apple-darwin","expected_tests":2608,"executed_tests":2608,"ignored_tests":["beamcraft-test-gate::process::process_fixture"],"source_unchanged":true,"stages":[{"name":"format","exit_code":0,"elapsed_ms":4144,"timed_out":false},{"name":"clippy","exit_code":0,"elapsed_ms":18125,"timed_out":false},{"name":"prepare","exit_code":0,"elapsed_ms":52512,"timed_out":false},{"name":"tests","exit_code":0,"elapsed_ms":229789,"timed_out":false},{"name":"doctests","exit_code":0,"elapsed_ms":1802,"timed_out":false},{"name":"docs","exit_code":0,"elapsed_ms":7032,"timed_out":false}]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants