Named parameters (docs/named_parameters_design.md, stages 3-4): - ParameterTable (src/scene/named_parameters.rs): a document-wide table of named formulas, each resolved through a shared expression evaluator with cross-parameter references, cycle detection, and division-by-zero/parse error reporting. SketchConstraint::driving_param becomes DrivingValue (Literal | Named) instead of a bare f64, resolved at solve time by sketch_solve::build_constraint via Scene::named_parameters. - Persistence (src/scene/named_parameters_persist.rs): same lazy XRecord model sketch_persist.rs already established, synced around open/save. Bumps the sketch-constraint XRecord's own format version (1 -> 2) since DrivingValue's shape doesn't line up with a version-1 blob's bytes. - PARAMETERS command and its editor (src/ui/window/named_parameters.rs): add/remove/edit rows, live validation, duplicate-name detection: Apply defines the table and re-solves every constraint scope with a named-parameter-driven constraint (command_driver.rs). Distance/Angle constraint prompts (modules/draw/constrain/value.rs) now accept either a literal or an existing parameter's name, mirroring how the constraint's own value prompt already worked. - Undo scope-down carried over from this project's own established pattern (constraint-system stage 9): the geometry an Apply moves is one undo step, but the parameter table edit itself has no undo entry yet — a documented follow-up, not a silent gap. UI scale setting (src/app/settings.rs, app/mod.rs, ui/window/options.rs): a whole-interface scale percentage (50-200%), wired directly into iced's own per-window scale_factor hook rather than per-panel font-size plumbing, so it applies immediately to every panel, dialog, and the ribbon at once. Persisted alongside the existing lineweight-display-scale setting. OPTIONS/OP command dispatch (app/commands/view.rs, app/commands/mod.rs): OPTIONS was registered for autocomplete but had no dispatch arm — the Start-tab welcome screen's own button was the only way to reach it. Found live-testing the UI-scale setting, unrelated to that feature itself. "OP" is AutoCAD's own short alias. help/: a new self-contained static user-help manual (manual.md as the authored source, tools/build_help.py splits it into the committed static HTML pages plus search index) — not yet wired into the application. 64 new/updated tests across the named-parameters and constraint-prompt changes. Co-Authored-By: Claude Sonnet 5 <[email protected]>
30 KiB
Named Parameters — Design & Handoff
Status: not started. This document is the starting context for a new
coding session (mirrors how docs/parametric_system_design.md was used for
the sketch-constraint-solver project — read that document's own "Progress"
section for the sibling feature this one builds alongside). The plan file at
/Users/felix/.claude/plans/snoopy-finding-matsumoto.md indexes both.
1. What this is, and why
OpenCADStudio now has real sketch-level parametric constraints (Coincident,
Horizontal/Vertical, Parallel/Perpendicular, Equal, Tangent, Distance, Angle,
Radius — see docs/parametric_system_design.md, fully built and tested).
What it does not have is a way to drive several of those dimensional
values from one shared, named, formula-capable source — FreeCAD calls this
its Spreadsheet workbench; AutoCAD calls it the Parameters Manager
(PARAMETERS command, since AutoCAD 2010) — a table of named variables
(hole_dia, plate_len, …) that a dimensional constraint's value can
reference instead of a bare literal, so editing the named value ripples to
every constraint that references it. This is explicitly the AutoCAD-parity
gap identified in the "AutoCAD equivalent" discussion this session (see
§7 for that framing) — we are not inventing a new concept, we're filling in
a specific, well-precedented one.
This is a genuinely separate feature from the constraint solver itself: it
needs a new document-level object, an expression parser, and a change to how
driving values are stored — not new ocs_gcs solver math.
2. Prior architecture decisions (from design discussion, not yet coded)
- New document-level table object, one per document (not per-scope/sketch
— parameters are meant to be shared across a whole drawing), holding
name -> literal-or-formula. Likely home: mirrorsrc/scene/sketch_persist.rs's pattern exactly — anXRecord/Dictionaryblob on a well-known owner (that file'sXRECORD_KEY/materialize_*_for_save/load_*_from_documentshape is the direct precedent to copy, including the version-prefixed bincode envelope and the hook points intoon_file_opened/prepare_native_save/ the wasm save path / the automation"new"/"open"ops). - Expression parser/evaluator — a small formula language (
2 * hole_dia + 1.5), with dependency-cycle detection (A referencing B referencing A must be rejected, not silently infinite-loop or panic). No existing dependency in the workspace does this — check crates.io options (e.g.evalexpr,meval) against the license stack (GPLv3 app; anything permissive/LGPL is fine) before hand-rolling one. - Driving-value call sites become "literal or named reference". The
concrete, known change:
SketchConstraint::driving_paramis currentlyOption<f64>(src/scene/sketch_constraints.rs:97), consumed directly insketch_solve.rs(build_constraint, lines ~184/190/195, one per Distance/Angle/Radius) viasys.add_param(c.driving_param?, ...). This would become something likeDrivingValue::Literal(f64) | DrivingValue::Named(String), resolved against the parameter table at solve time (same rebuild-from-scratch-per-trigger model the constraint solver already uses — no incremental re-evaluation needed, consistent withrefresh_sketch_constraints's existing "full rebuild per trigger, not incremental" design choice). - Solid-history properties are a second, separate call-site family —
src/scene/model/solid_history.rs'sPROP_LENGTH/PROP_HEIGHT/etc. are plainf64fields on structs likeSolidHistoryBox(e.g.value.length, set via the generic property-editing dispatch atsolid_history.rs:2239). Whether named parameters should reach these too (so a box's height can reference the sameplate_thicknessa sketch dimension uses) is a scope decision for this session to make explicitly, not assume — see open questions below. If yes, it's the same "literal or reference" change repeated across a different, larger set of call sites (many morePROP_*constants than there areConstraintKinds).
3. Explicitly out of scope for this feature (confirmed in prior discussion)
- Live sketch-to-3D linking (extrude/sweep/loft tracking a sketch's
constraint solve) is a separate, larger project. Confirmed this session:
SolidHistorySweep/Revolve/etc. holdsweep_entity: Option<EmbeddedEntity>— a snapshotted copy of the profile geometry, not a liveHandleback into the sketch (src/scene/model/solid_history.rs, grepsweep_entity). Named parameters can reference solid-history properties (per §2's open question) without needing this link — that's a narrower, additive step. Do not conflate the two.
4. Suggested staged approach
Mirroring what worked well for the constraint-solver project (docs/ parametric_system_design.md's own staged plan): small, independently
testable stages, each with its own automated tests, progress notes appended
to this document (not a separate changelog) after each stage, live UI
verification where the stage has new UI surface.
- Data model: the parameter table type + expression parser, unit-tested in isolation (parse, evaluate, cycle detection) with no document/UI integration yet.
- Persistence spike: XRecord round-trip for the new table, following
sketch_persist.rs's exact precedent — prove save/load byte-fidelity before wiring anything else to it (this is the same order stage 2 of the constraint-solver plan used, and it caught real assumptions early there). - Wire
SketchConstraint::driving_paramto resolve through the table (Distance/Angle/Radius constraints only, the three existing call sites). - UI: a parameters panel (add/rename/delete a named value, edit its
formula, see its resolved number and any error) — likely the
src/ui/style/dimstyle.rs-pattern panel already used elsewhere in this codebase, and a way to pick "use a named parameter" instead of typing a literal wherever a Distance/Angle/Radius constraint's value is entered today (src/modules/draw/constrain/value.rs'sDistanceConstraintCommand/AngleConstraintCommand). - (If scoped in per §2/§3) extend to
solid_history.rs'sPROP_*values.
5. Open questions to resolve before/while coding (not resolved by this doc)
- Solid-history scope — does v1 include primitive solid dimensions (box length, cylinder radius, …), or sketch dimensional constraints only? Affects how much of §4's stage 5 is in scope.
- Expression language surface — arithmetic only, or also functions
(
min,sqrt, trig)? AutoCAD's Parameters Manager supports a fairly rich set; matching all of it is not required for v1 but the parser's grammar should be chosen so functions can be added later without a rewrite. - Undo granularity for a parameter edit — a named-parameter edit can
ripple through many constraints across many scopes in one edit, similar
to (bigger than) a single constraint's own resolve. Should probably reuse
the same
record_undo_before/one-history-entry-per-trigger modelrefresh_sketch_constraintsalready established, but confirm rather than assume. - Where does the parameter table live for multi-document workflows?
Per-document (confirmed in §2) — but does a block/xref carry its own
independent table, or only the top-level document?
sketch_persist.rs's per-BlockRecordXRecord scanning pattern suggests per-block-record is natural to copy, but sketch scopes and a shared parameter table are not the same kind of thing (scopes are naturally per-sketch; parameters are naturally document-wide) — don't copy the pattern mechanically without checking this distinction holds. - Naming collisions / reserved words — needs a validation rule (valid identifier characters, no collision with another parameter, sensible error on a malformed formula) before the UI stage.
6. Progress
Stage 1 done and tested. src/scene/named_parameters.rs, new module
(registered pub mod named_parameters; in src/scene/mod.rs, alongside
sketch_constraints/sketch_persist/sketch_solve). Pure data + parser,
no document/UI integration yet, per this stage's own scope.
Expr: a small AST (Number,Ref,Neg,Add/Sub/Mul/Div/Pow,Call) plus a hand-rolled tokenizer/recursive-descent parser (parse) — ordinary precedence (+/-<*//< unary-<^, right-associative^), parens, and scientific-notation numeric literals (1.5e-2). Deviation from this doc's own §2 suggestion to checkevalexpr/mevalfirst: hand-rolled anyway, because cycle detection needs the set of names an expression references before it's evaluated (Expr::refs), and owning the parse tree gives that for free — treating evaluation as a black box the way a generic crate is normally used would just mean re-deriving the same dependency information some other way. No new external dependency added.- §5 open question 2 (expression language surface): resolved for v1 as
arithmetic + a small, deliberately conservative builtin function set
(
sqrt,abs,pow,min,max—call_builtin), chosen as low-risk, commonly-needed CAD primitives rather than a considered product decision on the full surface. The grammar'sCall(String, Vec<Expr>)node means adding more later needs no parser rewrite, matching this doc's own call-out. Builtin names are reserved (is_reserved_name) so a parameter can never collide with one — resolves part of open question 5 (naming collisions) alongsideis_valid_name's identifier rule (leading letter/underscore, then alphanumeric/underscore). - Cycle detection happens at
ParameterTable::settime, not at resolve/evaluate time — the design doc's "must be rejected" read as real-time rejection (matching a spreadsheet's usual UX), not a resolve-time error.setwalks the dependency graph from the candidate formula's own references, through every already-stored formula, looking for a path back to the name being defined; a reference to a name that doesn't exist yet just terminates that branch rather than counting as a cycle (order of definition doesn't matter, same as a spreadsheet formula referencing an empty cell) — it only becomesParamError::UndefinedReferenceifresolve/resolve_allactually needs that value later and it's still missing. Aresolve-time cycle guard exists too (resolve_inner'sstackcheck) but is defense-in-depth only — unreachable through the public API, sincesetalready refuses to create one; the code comment is explicit that this is a safety net (e.g. against a foreign/hand-edited file deserialized straight into a table without going throughset), not a path the design expects to hit. resolve_allisolates a failure to just the parameters it actually affects, not the whole table — matching this codebase's established don't-let-one-bad-item-take-down-everything-else pattern from the constraint system's own dangling-reference handling.ParameterTable::setis a single upsert (define-or-redefine), not two separate "must not exist" / "must already exist" entry points — a later UI stage that wants add-vs-rename semantics can checkcontainsitself first; not a decision this data-layer stage needed to make.- 17 unit tests: precedence/associativity, scientific notation, malformed
formulas, builtin arity/unknown-function errors, division by zero, name
validation, literal and chained-reference resolution, undefined-reference
reporting, direct and indirect (closes-later) cycle rejection, redefining
a parameter to break a would-be cycle,
resolve_allisolation, remove-then-resolve leaving a dependent gracefully undefined (not panicking), upsert semantics, insertion-order stability across redefinitions, and abincoderound-trip (bothExprandParameterderiveSerialize/Deserializealready, ready for stage 2's XRecord persistence — not exercised through the real save/load path yet, just in-memorybincode::serialize/deserialize, the same distinction stage 4 of the sketch-constraint project drew between its own in-memory-agreement test and its real-save-path tests).
Full regression check: cargo test --workspace, 686 passed / 1 failed (the
same pre-existing, unrelated Turkish-glyph-fallback font test noted
throughout docs/parametric_system_design.md) / 13 ignored. cargo clippy --lib shows no new warnings in named_parameters.rs (pre-existing warnings
elsewhere untouched).
Still open after stage 1 (per §5): open question 1 (solid-history scope), 3 (undo granularity), and 4 (per-document vs per-block-record table scope) — none needed answering yet since nothing here touches the document model or undo. Open question 2 and part of 5 got a working v1 answer above, not a final product ruling.
Stage 2 done and tested. src/scene/named_parameters_persist.rs, new
module (registered mod named_parameters_persist; in src/scene/mod.rs),
following sketch_persist.rs's exact precedent: the "lazy" model, a
version-prefixed bincode blob in one Chunk-typed XRecordEntry,
materialized right before save and read back right after load. Also went
ahead with the "wire it into open/save" half of stage 2 (not just the
spike) since it's the same four call sites sketch_persist already touches
and there was no reason to leave it half-done:
OpenCADStudio::prepare_native_save and the wasm save path
(src/app/update/file.rs), OpenCADStudio::on_file_opened's native/web
open path (src/app/update/file.rs:1393), and the automation "open"/
"new" ops (src/app/automation.rs) — named_parameters lives on Scene
(Scene::named_parameters: named_parameters::ParameterTable, alongside
sketch_constraints, for the same co-location-with-document reason
stage 3 of the constraint-solver project gave), with Scene:: named_parameters/named_parameters_mut accessors.
- A real round-trip gap found and worked around, not patched: the
obvious owner handle for a document-wide (not per-scope) XRecord is
document.header.named_objects_dict_handle— the vendoredcadcodecsource's ownextension_dictionary_handleexplicitly resolves an extension dictionary for aDictionary-type owner, and that crate's own test suite (tests/issue51.rs) uses exactly that handle as an XRecord owner. In practice this round-tripped through DWG but silently dropped the XRecord through DXF — caught by this stage's own real-bytes save/ load tests, not assumed to work from reading the source alone (the same disciplinesketch_persist's stage 4 applied). Root cause, traced into the vendored source:CadDocument::ensure_xrecordonly writes the owner's ownxdictionary_handlefield whenget_entity_mut(owner)succeeds (i.e.owneris a real "entity"); aDictionaryowner like the root NOD isn't one, soensure_xrecordfalls back to a runtime-only side map (xdic_by_handle) instead. The DWG writer consults that side map; greppingio/dxf/writer/for it turns up nothing — the DXF writer only ever reads the owner's ownxdictionary_handlefield, never the side map, so a Dictionary-owned extension dictionary is simply invisible to a DXF save. This is a gap in the vendored crate for this specific owner-type combination. Per this project's own stated preference (§1.4 of the sibling design doc: "requires zero changes to the externalacadrust/cadkernelcrates") the fix taken was to pick a different owner rather than patch the dependency:document.header.model_space_block_handle— a realBlockRecordentity, the exact anchorsketch_persist's ownSketchScope::ModelSpacecase already proved clean through both formats. Not a perfect semantic fit (a parameter table has nothing to do with model space specifically) but it's guaranteed to exist in everyCadDocumentand is a real entity, which is the property that actually matters. Worth revisiting if a later need for a true document-root-anchored XRecord comes up — the fix would be incadcodecitself (teachensure_xrecordto also set aDictionary/Layout/XRecord/PlotSettings/VisualStyle/Material/ProxyObjectowner's ownxdictionary_handlefield, not just entities'), not something to route around a second time. - 5 new tests (
named_parameters_persist.rs): in-memory materialize+reload agreement, realsave_to_bytes/load_bytesround trips for both DXF and DWG (the two that caught the gap above), an empty table not persisting, and format-version rejection — same shape assketch_persist's own test suite.
Full regression check: cargo test --workspace, 691 passed / 1 failed (same
pre-existing, unrelated font test) / 13 ignored. cargo clippy --lib shows
no new warnings in either named_parameters.rs or
named_parameters_persist.rs.
Stage 3 done and tested. SketchConstraint::driving_param is now
Option<named_parameters::DrivingValue> (sketch_constraints.rs) instead
of Option<f64> — exactly this doc's own §2 suggested shape:
pub enum DrivingValue {
Literal(f64),
Named(String),
}
with DrivingValue::resolve(&self, table: &ParameterTable) -> Result<f64, ParamError> (named_parameters.rs) doing the lookup — a literal resolves
to itself, a named reference resolves through the table.
- Solve-side wiring:
sketch_solve.rs'sbuild_constraint/solve_scopenow take a&ParameterTable(threaded fromScene::named_parametersat both call sites,refresh_sketch_constraintsandsolve_sketch_constraints_preview) and resolvedriving_paramat the three existing call sites (Distance/Angle/Radius) viac.driving_param. as_ref()?.resolve(params).ok()?— a resolve failure (an undefined reference, a division by zero; a cycle should be unreachable sincesetalready refuses to create one) is treated exactly like every other unbuildable-constraint casebuild_constraint's own doc comment already documents: the constraint is skipped for this solve, not an error. - Judgment call, not dictated by this doc: how a resolve failure should surface wasn't an open question this doc raised explicitly, so the existing precedent decided it — matching a dangling-reference / unbuildable constraint rather than inventing a new "parameter error" state. A richer surfacing (e.g. a distinct glyph color, a status-bar warning naming the broken reference) is possible future work once stage 4's UI exists to make it discoverable, not something this data/solver-layer stage needed to add speculatively.
CmdResult::AddSketchConstraintintentionally left asOption<f64>(src/command.rs) rather than also switching it toDrivingValue— no UI can author aNamedreference yet (that's stage 4's picker), so every existing call site (DistanceConstraintCommand/AngleConstraintCommandinsrc/modules/draw/constrain/value.rs, the Horizontal/Vertical/etc. dispatch insrc/app/commands/draw.rs) needed no changes at all; the one conversion point iscommand_driver.rs'sCmdResult::AddSketchConstrainthandler, which now wraps the literal intoDrivingValue::Literalright before callingSketchConstraintSet::add. Keeps this stage's diff to the data/solver layer plus one narrow seam, not a ripple through every typed- value UI command.glyph_label(sketch_constraints.rs) now matches on&DrivingValueinstead of a baref64: a literal renders exactly as before, a named reference renders its name (e.g. "↔ hole_dia") rather than a resolved number — showing the resolved value too would need threadingParameterTableinto the glyph-render call site (src/ui/overlay.rs), which is UI-layer work for stage 4, not this one; this arm exists only so the match stays exhaustive ahead of that UI (no command can produce aNameddriving_param yet, so it isn't reachable today).sketch_persist::FORMAT_VERSIONbumped 1 → 2:SketchConstraint's serialized shape changed (a baref64became an enum with its own discriminant), so a version-1 blob's bytes no longer line up with the current struct — exactly the "detected and gracefully skipped" case that version byte exists for (§5.2's own call-out: no migration path exists, bumping the version is the correct response, not a substitute for one). A previously-saved file's sketch constraints will not reload after this change; acceptable for personal-use, pre-release local work, not something a real migration was owed here.- 2 new tests (
tests/sketch_constraints_solve.rs): aDistanceconstraint driven by a named reference solves correctly and re-solves when the parameter's own value is redefined; aDistanceconstraint referencing an undefined name is skipped without panicking, leaving the triggering edit untouched (the same contract the existing "unrelated edit doesn't trigger a resolve" test already established for dangling refs). Every existingdriving_param-touching test (insketch_constraints.rs,sketch_persist.rs,tests/sketch_constraints_xrecord_roundtrip.rs,tests/sketch_constraints_solve.rs) updated to constructDrivingValue::Literal(...)instead of a bare float; all still pass. - Testing-methodology note, not a code defect: discovered while
re-verifying this stage that plain
cargo test --workspacestops after the first crate's test failures and silently skips the remaining integration-test binaries in the workspace — the pre-existing, unrelated font-fallback failure was silently truncating every regression check this whole project has run with a barecargo test --workspace. Re-ran with--no-fail-fastfor this stage's check and confirmed every binary (not just the lib crate) passes except that one known-unrelated test. Worth using--no-fail-fastfor any future full-workspace check in this repo.
Full regression check: cargo test --workspace --no-fail-fast, every test
binary passing except the same pre-existing, unrelated font test noted
throughout this document and its sibling. cargo clippy --lib --tests
shows no new warnings on any line this stage touched (the two warnings that
do appear in sketch_solve.rs/scene/mod.rs predate this stage and are on
code this stage didn't modify).
Stage 4 done and tested (not live-verified in the running app — see the note below).
- The parameters panel (
src/ui/window/named_parameters.rs, new): an in-canvas modal opened by a newPARAMETERScommand (AutoCAD's own name for the equivalent feature —src/app/commands/view.rs, dispatched alongsideALIASEDIT; registered in the autocomplete/MCP command list from the start, unlike the six pre-existing constraint commandsdocs/backlog.mdflags as missing that registration). Built by mirroringalias_editor.rs's buffered-rows pattern as closely as possible rather than inventing new UI conventions: rows are(name, formula), edited in a working buffer (OpenCADStudio::named_parameter_editor_rows,ModalKind::NamedParameters) and only committed toScene:: named_parameterson Apply — a better fit here than for aliases, even:ParameterTable::setvalidates (and can reject) a formula immediately, so writing through to the live table on every keystroke would fight a user mid-type. - Live preview column, beyond what the alias editor has: each row also
shows its resolved value or its error, recomputed from a scratch
ParameterTablebuilt from the whole buffer's current text on every render (preview, private to the UI module, unit-tested directly) — not cached, matching the "cheap enough to rebuild from scratch" philosophyParameterTable/sketch_solve.rsalready use throughout. This means a cross-row problem (a forward reference, a cycle that would close between two draft rows) shows up immediately, before Apply, not just after a failed commit. - A real gap found while building the preview, not by design: two rows
typed with the same name would otherwise silently collapse —
setupserts by name, so the buffer's later row would just overwrite the earlier one with no indication anything was discarded. Addedduplicate_name_rows(checked first, before anysetcall, by both the preview and the real Apply) to flag every row sharing a duplicated name as its own error instead of silently picking one arbitrarily. - The other half of stage 4 — "a way to pick a named parameter instead of
a literal": resolved as letting the same value prompt accept either,
rather than building a separate picker UI.
DistanceConstraintCommand/AngleConstraintCommand(src/modules/draw/constrain/value.rs) now take a one-time snapshot of the active tab's parameter names at construction (known_param_names— the samedefault_value-is-a-one-time-snapshot precedent these commands already relied on, sinceon_text_inputhas no document access);parse_driving_valuetries a numeric literal first, falls back to an exact match against that snapshot, and returns neither if the typed token is unrecognized (the prompt's existing "invalid input" behavior, unchanged). This mirrors how AutoCAD's own dimensional- constraint prompts already accept an expression in place of a bare value, and needed no new interaction design.CmdResult::AddSketchConstraint'sdriving_param(left as a bareOption<f64>in stage 3, deliberately, exactly so this stage could revisit it) is nowOption<DrivingValue>. - Apply re-solves what it affects: because editing a named parameter is
the entire point of the feature ("editing the named value ripples to
every constraint that references it," §1), Apply doesn't just rebuild the
table — it also gathers every entity handle touched by an enabled
constraint whose
driving_paramisDrivingValue::Named(across every scope; the whole table changed, not one isolated value, so there's no cheaper "which handles actually changed" test worth doing) and runs them through the samebegin_undo/bump_entities/commit_undo_deltabracketresolve_one_sketch_conflictalready established (OpenCADStudio::apply_named_parameter_editor_rows,command_driver.rs). - Judgment call on undo granularity (§5 open question 3), not fully
resolved: the geometry Apply moves is one undo step (bracketed as
above), matching how every other solve-triggering action in this codebase
behaves. The parameter table edit itself has no undo entry of its own
yet — that would need a new
HistorySnapshotvariant mirroringSketchConstraints(the sibling project's own stage 9), which this pass didn't build. Documented, not silently dropped, exactly like stage 9's own "two presses, not one" scope-down: undoing after an Apply undoes the geometry it moved, not the parameter values. - 15 new tests: 3 for
parse_driving_value(literal-first, name match, unrecognized token), 6 for the panel'spreview(cross-row reference, forward reference regardless of buffer order, a cycle flagged on the row that closes it, a blank row left unflagged, a malformed formula, the duplicate-name fix), and 6 integration tests incommand_driver.rs(applying rows defines the parameter and re-solves a constraint that already referenced it by name — including re-applying after redefining the parameter; a malformed row is reported as an error and doesn't block a good row alongside it; the duplicate-name fix exercised end to end throughapply_named_parameter_editor_rows, not just the UI-layer preview). - Testing-methodology note carried over from stage 3: verified with
cargo test --workspace --no-fail-fastthroughout this stage too, for the same reason noted there.
Full regression check: cargo test --workspace --no-fail-fast, 703 tests
passing, only the same pre-existing, unrelated font-fallback failure noted
throughout this document and its sibling. cargo clippy --lib --tests shows
no new warnings on any line this stage touched.
Not live-verified in the running app this pass — a real gap, not a
formality skipped by habit: this is genuinely new UI surface (a whole modal
panel, plus a new branch in an existing text-input prompt), and static
review/unit tests can't catch every real-app integration issue (layout,
widget-API misuse that only manifests at runtime, focus/keyboard handling).
Per docs/backlog.md's own live-testing notes, exercising it for real needs
a release build swapped into a scratch copy of the installed app bundle
(open_application/request_access always resolve to the /Applications
copy, and the debug binary doesn't reliably present a controllable window in
this environment) — a real time investment this pass didn't spend, flagged
explicitly rather than silently assumed to work. First things to check in a
future live-testing pass: the panel actually opens via PARAMETERS, the
value column updates live while typing, the duplicate/cycle/malformed-
formula error text renders legibly, Apply visibly moves geometry for a
constraint driven by a named parameter, and typing a parameter's name at a
DCONSTRAINT/ACONSTRAINT prompt is actually accepted.
Follow-up work not yet done
- Solid-history
PROP_*values (§2's own flagged scope decision, open question 1) — named parameters currently only reachSketchConstraint:: driving_param(Distance/Angle/Radius), notsolid_history.rs's box length/cylinder radius/etc. Not started; a decision to make, not assumed. - Constraint-table-edit undo (open question 3, only partially resolved — see above).
- Reserved-word/name-collision UX polish:
ParameterTable::setalready rejects an invalid or builtin-colliding name (stage 1); the panel surfaces that rejection as the row's error text viapreview/Apply, but there's no live per-keystroke nudge beyond that (e.g. graying outApplywhile any row has an error) — a small, optional polish, not a functional gap. - Live UI verification, per the note above.