Repository navigation
feat: deliver the post-#830 fork delta (camera-prim view restore, bolt-group rename, whole-sheet export, V07 presets) - #832
Merged
Conversation
* 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
commented
Nov 4, 2026
axisrow
left a comment
Collaborator
Author
There was a problem hiding this comment.
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
approved these changes
Nov 4, 2026
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}]}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 isexactly 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:
session file becomes the views layer:
crates/session/src/views_layer.rsmaps 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:foldersstring[] on the views scope(wrong-typed records are refused); the default-view mark rides on
bim:view:default.--filekeeps 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 aview 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 notedocs/design/w01-saved-views.md.command was advertised and accepted, but no path carried the label: the
stored
bim:namekept 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,Nonekeeps thelabel), the shared amendment authoring writes a
Change::Labelnext tothe 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.)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
SheetLayoutthatboth
export_svgand the newexport_pdfconsume, and a--sheetthatcontradicts the document's own paper refuses. No exporter read
Drawing::visible_drafting, so drawn objects never reached paper; ashared 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.)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 inbeamcraft_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 onthe view itself are additive optional keys (
bim:view:representation,bim:view:colourSet), so files written by earlier builds still read. Theviews 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:
.github/is untouched..pyfiles.tools/,docs/plan/owner-decision notes) are excluded; product code, tests and
docs/design/notes are included.Co-Authored-Bytrailers.How you verified it
On this exact tree (the branch head), on macOS:
cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspace— targeted instead: nextest across thetouched 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:
(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
--namekeeping the label, the retry replaying,a different name under the same op id refused, and a malformed label
refused without a revision.
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.
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.
sidecar migration (v1/v2, including a default-marked view), and the
folder-record and wrong-typed-record refusals.
Never-crash
unwrap/expect/panic!/todo!outside tests — clippydenies them in non-test code, and the lint above runs that denial.
sized by input are capped.
unsafe.Provenance
ATTRIBUTION.mdis 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 closesis 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 thesheet-renderer unification, where
export_pdfjoinsexport_svgon oneSheetLayout— the largest single change in the batch.🤖 Generated with Claude Code