forked from 1b-agent/opencadstudio-upstream
Harden parametric sketch constraints
This commit is contained in:
1 parent
41c07e9858
commit
9b58ca8a93
101 files changed
+3128
-13767
No files matched your search
-148
@@ -1,148 +0,0 @@
|
||||
# Backlog — deferred items, known gaps, and scope-downs
|
||||
|
||||
A consolidated list of things noticed but deliberately not fixed/built, so
|
||||
they aren't lost between sessions. Pulled from `docs/
|
||||
parametric_system_design.md`'s own "open questions" log, the `ocs_gcs` port
|
||||
plan, and things found while doing unrelated work this session. Not a bug
|
||||
tracker for the whole app — scoped to what surfaced during the parametric
|
||||
constraint system project and its surrounding investigation.
|
||||
|
||||
## Parametric constraint system — deliberate scope-downs (documented, not accidental)
|
||||
|
||||
1. **Stage 9 undo is two presses, not one.** Adding a persistent constraint
|
||||
and the geometry move it triggers are two separate undo/redo entries
|
||||
(`HistorySnapshot::SketchConstraints` pushed adjacent to, not merged into,
|
||||
the geometry `Delta`). The design doc's recommended §5.1(b) "live
|
||||
XRecord" approach would fix this but depends on stage 4 (save/load),
|
||||
which itself was scoped down (next item) — revisit both together if this
|
||||
is ever worth fixing.
|
||||
2. **Stage 4 does not resolve on load.** Opening a file with saved
|
||||
constraints restores them but does not immediately re-solve — the DOF
|
||||
badge and any conflict state only appear after the next edit that touches
|
||||
an affected scope. Deliberate: resolving on load risked desyncing from
|
||||
the `DerivedCaches` bundle `on_file_opened` installs right after, without
|
||||
budget this session to verify that's safe.
|
||||
3. **Stage 11's conflict resolver is a one-at-a-time pill, not the full
|
||||
SketchXpert-style panel.** Clicking "⚠ n conflicting" removes one flagged
|
||||
constraint and re-solves; there's no named-candidate list or cyclable live
|
||||
preview before committing (design doc §6.4 Phase C). This is the single
|
||||
largest UX gap remaining in the constraint system relative to the
|
||||
original plan.
|
||||
|
||||
## `ocs_gcs` solver — not ported
|
||||
|
||||
- `SlopeAtBSplineKnot` (tangent continuity across a spline knot) — needs
|
||||
knot-multiplicity reconstruction for a narrower use case than
|
||||
`PointOnBSpline`, which is ported. Deferred, not silently dropped.
|
||||
- `InternalAlignmentPoint2Ellipse` / `InternalAlignmentPoint2Hyperbola` —
|
||||
FreeCAD-Sketcher-internal bookkeeping for *creating* conic geometry
|
||||
interactively, not for constraining existing geometry. Lower priority.
|
||||
- `EllipticalArcRangeToEndPoints` — dead code upstream (a `ConstraintType`
|
||||
enum value with no implementing class anywhere in current planegcs
|
||||
source), not a gap in this port.
|
||||
- **Golden-fixture testing against the locally-installed FreeCAD.app**
|
||||
(testing-strategy "Oracle A") was never wired up. Everything in `ocs_gcs`
|
||||
is verified only against Oracle B (closed-form + finite-difference). Still
|
||||
a real asset sitting unused if higher-confidence validation is ever
|
||||
wanted.
|
||||
- **Performance at scale is untested.** The "only touched scopes rebuild"
|
||||
locality argument (design doc §4.2) is architecturally sound but has no
|
||||
profiling behind it on a large, heavily-constrained drawing.
|
||||
|
||||
## Constraint types not exposed in the app's UI (even though some primitives exist)
|
||||
|
||||
`ConstraintKind` (`src/scene/sketch_constraints.rs`) currently has:
|
||||
Coincident, Horizontal, Vertical, Parallel, Perpendicular, Equal, Distance,
|
||||
Angle, Radius, Tangent. AutoCAD's own Geometric Constraints panel additionally
|
||||
has **Fix**, **Concentric**, **Collinear**, **Symmetric**, **Smooth** — none
|
||||
of these have a `ConstraintKind` variant here. Some are buildable from
|
||||
already-ported `ocs_gcs` primitives (e.g. Concentric ≈ `C2CDistance` at zero
|
||||
distance) without new solver math; `Fix` would need a different mechanism
|
||||
entirely (pinning a point's params non-free rather than adding a constraint
|
||||
row) and doesn't fit the current `ConstraintKind` model at all.
|
||||
|
||||
## Newly discovered this session (not previously tracked anywhere)
|
||||
|
||||
- **`CCONSTRAINT`, `TCONSTRAINT`, and their four siblings
|
||||
(`HCONSTRAINT`/`VCONSTRAINT`/`PCONSTRAINT`/`QCONSTRAINT`/`ECONSTRAINT`) have
|
||||
no `inventory::submit!(CommandRegistration)` registration.** Found while
|
||||
generating `docs/command_reference.md`. All seven work correctly when
|
||||
invoked (ribbon button or exact typed name) but don't appear in the
|
||||
command-line's autocomplete or in the MCP server's `commands` listing,
|
||||
unlike almost every other command in the app. Pre-existing across the
|
||||
whole constraint-command family, not something this session's Tangent/
|
||||
Coincident additions broke uniquely — just never noticed before. Cheap fix
|
||||
(one `inventory::submit!` block) whenever someone wants to spend five
|
||||
minutes on it.
|
||||
- **No live link from a sketch to 3D solid features.**
|
||||
`SolidHistorySweep`/`Revolve`/etc. (`src/scene/model/solid_history.rs`)
|
||||
hold `sweep_entity: Option<EmbeddedEntity>` — a geometry copy snapshotted
|
||||
at creation time, not a `Handle` back into the sketch. Dragging or
|
||||
re-solving a sketch that was previously extruded/revolved/swept does not
|
||||
update the resulting solid. This is the real "feature-history tree" gap
|
||||
discussed when scoping FreeCAD/AutoCAD parity — a substantially bigger
|
||||
project than the named-parameters work (see `docs/
|
||||
named_parameters_design.md` §3), not something to fold into it by
|
||||
accident.
|
||||
- **No named-parameter/expression sheet** (AutoCAD's Parameters Manager /
|
||||
FreeCAD's Spreadsheet equivalent). This is the next feature queued up —
|
||||
see `docs/named_parameters_design.md` for the handoff context.
|
||||
- **No end-user manual exists anywhere in the repo.** Checked `docs/`
|
||||
thoroughly — everything there is developer-facing (plugin architecture,
|
||||
this parametric design doc, tessellation notes, native-vs-web diffing,
|
||||
release notes, automation scripts) plus localized top-level README
|
||||
translations. No in-app Help link points anywhere either. Not started;
|
||||
would be a real, separate writing effort if wanted.
|
||||
- **No theme install/import/export mechanism.** Theming is entirely in-app:
|
||||
pick one of iced's built-in themes, or hand-tune one "Custom" slot via six
|
||||
hex-color inputs (`src/ui/window/options.rs`, `UiThemePalette` in
|
||||
`src/app/config.rs`). No theme file format, no way to author and share a
|
||||
theme, only one custom palette remembered at a time.
|
||||
- **No touch/multi-touch input support**, relevant to the wasm build running
|
||||
on a tablet (e.g. iPad). Grepped the whole codebase for touch/gesture
|
||||
handling — none exists; all input assumes mouse+keyboard, relying on the
|
||||
browser's synthetic mouse-event emulation for taps. No pinch-zoom, no
|
||||
two-finger pan, and OSNAP/grip-drag precision (already tight even with a
|
||||
mouse — see the Coincident epsilon note below) would be materially worse
|
||||
with a fingertip.
|
||||
|
||||
## Testing/tooling notes for future live-verification sessions
|
||||
|
||||
- **Coincident's success path could not be reliably live-verified via
|
||||
synthetic mouse clicks** — its resolver epsilon (`COINCIDENT_EPSILON_SQ =
|
||||
1.0e-12`, ~1e-6 world units, reused from stage 8's `infer_coincident_refs`)
|
||||
is tighter than this session's `left_click`/OSNAP automation could
|
||||
reliably hit, even with Endpoint snap enabled. Concluded to be an
|
||||
environment limitation, not a code defect — automated tests cover the
|
||||
resolver logic directly — but worth knowing before assuming a future
|
||||
automated click-through will succeed where this session's didn't.
|
||||
- **`open_application`/`request_access` always resolve to the
|
||||
`/Applications`-installed app bundle**, regardless of which binary was
|
||||
most recently built. To live-test a code change, build a release binary
|
||||
and swap it into a **scratch copy** of the installed bundle (e.g.
|
||||
`/tmp/ocs_livetest/OpenCADStudio.app`), keeping the original small
|
||||
launcher (`CFBundleExecutable = "OpenCADStudio"`) intact, then
|
||||
`codesign --force --deep -s -` to re-sign ad-hoc before `open`-ing it.
|
||||
This reuses the real bundle identity (`io.github.HakanSeven12.OpenCadStudio`)
|
||||
so access requests correctly target it. A standalone bundle with a fresh
|
||||
bundle id gets silently `user_denied` — not a viable shortcut. The debug
|
||||
binary run bare via `nohup` never presented a controllable window either
|
||||
— use a release build.
|
||||
- **`target/debug` can balloon fast from repeated live-testing rebuilds** —
|
||||
hit 35GB and caused a real "No space left on device" build failure this
|
||||
session. `cargo clean` reclaimed 64GB. Worth a proactive `cargo clean` (or
|
||||
checking `df`) before a long live-testing stretch, not just after it
|
||||
breaks.
|
||||
- Synthetic input reliability, generally: Escape frequently failed to cancel
|
||||
an active command (use Return instead); LINE clicks sometimes produced
|
||||
stray zero-length duplicate entities; selection clicks often missed thin
|
||||
line bodies or landed on a midpoint-edit-box zone (use a crossing-window
|
||||
drag-select instead of precise body clicks).
|
||||
|
||||
## Legal / process
|
||||
|
||||
- **LGPL-2.1-or-later review for `ocs_gcs` is still open** (original plan
|
||||
§6). Non-blocking for continued development, but must happen before any
|
||||
public release containing it — separate-relinkable-module posture vs.
|
||||
broader relicensing, notice/attribution obligations under GPLv3
|
||||
distribution.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,414 +0,0 @@
|
||||
# DWG-Native Constraint Compatibility — Design
|
||||
|
||||
## 1. What this is, and why
|
||||
|
||||
Hakan (upstream maintainer) reviewed the sketch-constraint system
|
||||
(`docs/parametric_system_design.md`) and asked for it to persist using the
|
||||
constraint object model already exposed in `acadrust`/`cadcodec` (the pinned
|
||||
CAD kernel), so a file saved by OpenCADStudio carries constraints AutoCAD or
|
||||
BricsCAD would recognize as their own native objects — not just an opaque
|
||||
custom blob only this app understands.
|
||||
|
||||
This is purely a **persistence-format** question, not a solver question.
|
||||
`ocs_gcs` (the FreeCAD `planegcs` port: `crates/ocs_gcs/src/solvers/{bfgs,dogleg,lm}.rs`)
|
||||
is the numerical engine that computes geometry from a constraint system —
|
||||
that stays exactly as it is regardless of what this document decides. What
|
||||
changes is only how `SketchConstraintSet` gets written to and read from a
|
||||
`CadDocument`, in `src/scene/sketch_persist.rs`, currently a custom
|
||||
`bincode`-in-XRecord blob keyed `OCS_SKETCH_CONSTRAINTS`.
|
||||
|
||||
## 2. What `acadrust` already provides (confirmed from source)
|
||||
|
||||
All paths below are relative to the pinned checkout
|
||||
(`~/.cargo/git/checkouts/cadcodec-*/*/src/`, rev pinned in `Cargo.toml`).
|
||||
|
||||
- `objects/associative.rs` defines AutoCAD's real 2D geometric-constraint
|
||||
object graph as plain Rust types, reachable generically through
|
||||
`ObjectType::Associative(AssociativeObject { data: AssociativeData::ConstraintGroup(Assoc2dConstraintGroup), .. })`
|
||||
— the same `document.objects: HashMap<Handle, ObjectType>` map
|
||||
`sketch_persist.rs` already inserts `XRecord`/`Dictionary` entries into.
|
||||
- Both DWG (`io/dwg/dwg_stream_{readers,writers}/object_{reader,writer}/associative.rs`,
|
||||
~1240/~870 lines) and DXF (`io/dxf/{reader,writer}/section_{reader,writer}/associative.rs`)
|
||||
have **complete read and write** implementations for every type below —
|
||||
confirmed by reading the actual field-by-field (de)serialization code, not
|
||||
inferred from the type definitions alone.
|
||||
- `classes/mod.rs` registers the DXF class names every one of these needs
|
||||
when written (`(DXF_NAME, "AcDbClassName", ...)` tuples):
|
||||
`AcDbAssocDependency`, `AcDbAssocGeomDependency`, `AcDbAssocAction`,
|
||||
`AcDbAssocNetwork`, `AcDbAssoc2dConstraintGroup`, `AcDbAssocVariable`,
|
||||
`AcDbAssocPersSubentManager`.
|
||||
|
||||
### The graph shape
|
||||
|
||||
Not a flat list — a small object graph, matching AutoCAD's real associative
|
||||
framework:
|
||||
|
||||
```
|
||||
AssocNetwork (per block/space that owns constrained geometry)
|
||||
└─ owned_actions: Vec<Handle> ─→ AssocAction
|
||||
├─ action_body: Handle ─→ Assoc2dConstraintGroup
|
||||
│ ├─ work_plane: [Vector3; 3]
|
||||
│ └─ nodes: Vec<AssocConstraintNode>
|
||||
│ (both the *constraint* nodes
|
||||
│ and the *geometry* nodes live
|
||||
│ in this one Vec — see §3/§4)
|
||||
└─ dependencies: Vec<AssocActionDependency>
|
||||
─→ AssocGeomDependency
|
||||
├─ dependency: AssocDependency
|
||||
│ └─ dependent_on: Handle
|
||||
│ (the real Line/Circle/... entity)
|
||||
└─ persistent_subent: AssocPersistentSubentId
|
||||
```
|
||||
|
||||
`AssocConstraintNode { node_id: i32, status: u8, connections: Vec<i32>,
|
||||
class_name: String, registry_flag: bool, data: AssocConstraintNodeData }` —
|
||||
`node_id`/`connections` link nodes to each other *within* the group (a
|
||||
constraint node's `owner_id` fields point at a geometry node's `node_id`);
|
||||
`class_name` picks which `AssocConstraintNodeData` variant applies on
|
||||
read (`read_constraint_node_data(reader, class_name)`,
|
||||
`dwg_stream_readers/object_reader/associative.rs:663`).
|
||||
|
||||
### 2b. Primary sources beyond `cadcodec`'s own code
|
||||
|
||||
`cadcodec` ships no constraint-bearing test fixture and its own source only
|
||||
shows *what bytes get read*, not *what they mean* semantically — §7 (now
|
||||
mostly resolved) needed Autodesk's own interface documentation to answer.
|
||||
Two real, first-party sources turned out to be publicly available with no
|
||||
sample DWG file needed at all:
|
||||
|
||||
- **The ObjectARX 2012 SDK's own C++ headers**, mirrored on GitHub at
|
||||
[Stalso/CadPlugin](https://github.com/Stalso/CadPlugin/tree/master/ObjectARX%202012/inc)
|
||||
— Autodesk's own copyright header on each file explicitly grants "permission
|
||||
to use, copy, modify, and distribute this software... for any purpose and
|
||||
without fee", and this is the SDK Autodesk itself publishes specifically so
|
||||
third parties can build interoperable tools — not leaked or decompiled
|
||||
source. `AcDbAssoc2dConstraintGroup.h`, `AcDbAssocNetwork.h`,
|
||||
`AcDbAssocManager.h`, `AcDbAssocVariable.h`, `AcConstrainedGeometry.h` were
|
||||
all read directly; their class-level doc comments describe the exact
|
||||
same object graph `cadcodec` implements, in Autodesk's own words.
|
||||
- **Autodesk's live public ObjectARX reference**
|
||||
(`help.autodesk.com/view/OARXMAC/2025/...`) — confirmed the
|
||||
`AcDb::ImplicitPointType` enum's exact values and per-member descriptions
|
||||
(used for §4's `point_type` mapping) and `AcDbAssocNetwork`'s
|
||||
`getInstanceFromObject`/`getInstanceFromDatabase` dictionary-key default
|
||||
(used for §7 question 1). No login or SDK download needed — the reference
|
||||
guide is indexed and publicly browsable.
|
||||
|
||||
## 3. Constraint-kind mapping
|
||||
|
||||
Our `ConstraintKind` (`src/scene/sketch_constraints.rs`) against
|
||||
`AssocConstraintNodeData` (`objects/associative.rs:885`). The "refs shape"
|
||||
column is copied from `sketch_solve::build_constraint`'s actual `match`
|
||||
(`src/scene/sketch_solve.rs:151-208`) — the ground truth for what each kind
|
||||
expects, not the doc comment.
|
||||
|
||||
| `ConstraintKind` | refs shape (from `build_constraint`) | `AssocConstraintNodeData` | Notes |
|
||||
|---|---|---|---|
|
||||
| `Coincident` | `[point, point]` | `Geometrical` + `class_name = "ACPOINTCOINCIDENCECONSTRAINT"` | |
|
||||
| `Horizontal` | `[whole_line]` | `Geometrical` + `"ACHORIZONTALCONSTRAINT"` | |
|
||||
| `Vertical` | `[whole_line]` | `Geometrical` + `"ACVERTICALCONSTRAINT"` | |
|
||||
| `Parallel` | `[whole_line, whole_line]` | `Parallel { datum_line_index: None, .. }` | dedicated variant, not `Geometrical` |
|
||||
| `Perpendicular` | `[whole_line, whole_line]` | `Geometrical` + `"ACPERPENDICULARCONSTRAINT"` | |
|
||||
| `Equal` (line case) | `[whole_line, whole_line]` | `Geometrical` + `"ACEQUALLENGTHCONSTRAINT"` | |
|
||||
| `Equal` (circle/arc case) | `[whole_circle, whole_circle]` | `Geometrical` + `"ACEQUALRADIUSCONSTRAINT"` | **our `Equal` doesn't distinguish these — see §5** |
|
||||
| `Distance` | `[point, point]` | `Distance { direction_type, distance: Option<Vector3>, value_dependency, dimension_dependency, .. }` | needs an `AssocVariable`/value dependency for the driving value — see §5 |
|
||||
| `Angle` | `[whole_line, whole_line]` | `Angle { sector_type, value_dependency, dimension_dependency, .. }` | same value-dependency need |
|
||||
| `Radius` | `[whole_circle]` | `RadiusDiameter { mode, value_dependency, dimension_dependency, .. }` | `mode` presumably distinguishes radius vs. diameter — we're always radius |
|
||||
| `Tangent` | `[circle, circle]` or `[circle, line]`/`[line, circle]` — Line-Line and anything involving an `Arc` are explicitly unsupported today (`sketch_solve.rs:239-241`, `Arc` isn't a supported `EntityGeom` variant yet) | `Geometrical` + `"ACTANGENTCONSTRAINT"` | our own `Arc`-tangent gap predates this document, unrelated to DWG compatibility |
|
||||
|
||||
AutoCAD's `is_plain_geometrical_constraint` list also includes several kinds
|
||||
we have no `ConstraintKind` for at all: `ACCENTERPOINTCONSTRAINT`,
|
||||
`ACCOLINEARCONSTRAINT`, `ACCONCENTRICCONSTRAINT`, `ACEQUALCURVATURECONSTRAINT`,
|
||||
`ACEQUALDISTANCECONSTRAINT`, `ACEQUALHELPPARAMETERCONSTRAINT`,
|
||||
`ACFIXEDCONSTRAINT`, `ACMIDPOINTCONSTRAINT`, `ACNORMALCONSTRAINT`,
|
||||
`ACPOINTCURVECONSTRAINT`, `ACSYMMETRICCONSTRAINT` — genuinely nothing to map
|
||||
to yet, not a mapping gap (§5).
|
||||
|
||||
## 4. Geometry-node mapping
|
||||
|
||||
Every entity a constraint references also needs its own node in the same
|
||||
`nodes: Vec<AssocConstraintNode>`, so constraint nodes have something to
|
||||
point `owner_id` at, *and* a paired `AssocGeomDependency` (in the owning
|
||||
`AssocAction`'s `dependencies`) so the graph — not just the constraint data
|
||||
— actually references the real `Line`/`Circle`/... entity by `Handle`.
|
||||
|
||||
| Our reference | `AssocConstraintNodeData` geometry variant |
|
||||
|---|---|
|
||||
| `SketchRef::whole(line)` | `BoundedLine { point, direction, is_ray: false, start_point, end_point, .. }` — confirmed a *bounded* line is the right choice: `Line`/`BoundedLine`'s split exists because AutoCAD's own constrained-geometry set includes true infinite construction lines (`ACCONSTRAINEDCONSTRUCTIONLINE`/`ACCONSTRAINEDDATUMLINE`, `read_constraint_node_data`, `associative.rs:711-714`) as a *separate* case from an ordinary bounded segment; nothing we draw is an infinite line |
|
||||
| `SketchRef::point(line, 0\|1)` | `ImplicitPoint { point_type: kStartImplicit(0)\|kEndImplicit(1), point_index: -1, curve_id: <the Line/BoundedLine node's node_id>, .. }` — **confirmed**, `AcDb::ImplicitPointType` enum, `kStartImplicit`/`kEndImplicit`/`kMidImplicit`/`kCenterImplicit`/`kDefineImplicit` (0-4), Autodesk's own live ObjectARX reference (§2b). `point_index` is `-1` for every case except `kDefineImplicit` (a spline control point, which we don't reference); `curve_id` is presumably the owning geometry node's own `node_id` (not yet cross-checked against a real file — the *values* are confirmed, this one wiring detail isn't) |
|
||||
| `SketchRef::whole(circle)` | `Circle { center, normal, direction, radius, start_parameter: 0.0, end_parameter: TAU, .. }` |
|
||||
| `SketchRef::whole(arc)` | `Arc { .. same fields .. , start_point, end_point }` |
|
||||
| `SketchRef::center(circle\|arc)` | `ImplicitPoint { point_type: kCenterImplicit(3), point_index: -1, curve_id: <the Circle/Arc node's node_id>, .. }` — confirmed by the same enum (`kCenterImplicit`: "Center point of a circle, arc, ellipse or bounded ellipse") — **not** the separate `Point`/`ACCONSTRAINEDPOINT` variant, which the ObjectARX docs' own class hierarchy (§2b) show is for a *free-standing* constrained point, not one implicitly defined by another curve |
|
||||
| `SketchRef::point(_, -2)` (reserved, unused today) | would be `kMidImplicit` if ever used — "Mid point of a bounded line ... or arc" is exactly what marker `-2`'s doc comment already reserved this for |
|
||||
|
||||
`SketchRef` and this graph already agree on one thing directly: our own doc
|
||||
comment (`src/scene/sketch_constraints.rs:20-22`) says the `marker`
|
||||
convention was deliberately borrowed from
|
||||
`AssocDimensionReference::main_gs_marker` (`src/scene/dimension_assoc.rs`,
|
||||
already shipping, already reading/writing real `Assoc*` objects for
|
||||
associative dimensions). That's a different, lighter mechanism than the
|
||||
constraint graph (an `AcDbOsnapPointRef` chain attached directly to a
|
||||
`Dimension` entity, not the `AssocNetwork`/`AssocAction` framework — see
|
||||
`AssocDimensionAssociation`, `objects/associative.rs:1200`), so it's a
|
||||
precedent for *how this codebase already talks to `Assoc*` types*, not a
|
||||
template for the constraint graph's own wiring.
|
||||
|
||||
## 5. Known gaps (not addressed by this document)
|
||||
|
||||
- **`Equal` is one `ConstraintKind`, AutoCAD has (at least) two**:
|
||||
`ACEQUALLENGTHCONSTRAINT` (lines) vs. `ACEQUALRADIUSCONSTRAINT`
|
||||
(circles/arcs), plus `ACEQUALCURVATURECONSTRAINT`/
|
||||
`ACEQUALDISTANCECONSTRAINT` we don't model at all. `build_constraint`
|
||||
already branches on `refs`' resolved shape at solve time
|
||||
(`sketch_solve.rs:181-188`), so the same branch can pick the write-side
|
||||
`class_name` — no data-model change needed for the length/radius split;
|
||||
the other two AutoCAD sub-kinds are a real, separate gap.
|
||||
- **`DrivingValue::Named` — confirmed, has a real native equivalent, and the
|
||||
full wiring is now traced end to end.**
|
||||
`AcDbAssocVariable` (`objects/associative.rs:1024`: `name`, `expression`,
|
||||
`evaluator`, `description`, cached `value`) is fully read *and* written on
|
||||
both DWG and DXF (confirmed in
|
||||
`dwg_stream_readers/object_reader/associative.rs:1031-1063` and the DXF
|
||||
reader/writer equivalents) — it's the same associative-action graph a
|
||||
constraint group lives in (`AssocVariable` wraps an `AssocAction`, exactly
|
||||
like `Assoc2dConstraintGroup` does), not a separate table.
|
||||
|
||||
The two handles on `Distance`/`Angle`/`RadiusDiameter`
|
||||
(`read_explicit_constraint`, `associative.rs:652-661` — reads them in this
|
||||
order: `value_dependency` first, `dimension_dependency` second) resolve
|
||||
to two different, unrelated things:
|
||||
|
||||
- **`value_dependency`** → an `AssocValueDependency` object
|
||||
(`{ dependency: AssocDependency, name, value: AssocEvalVariant }`,
|
||||
`objects/associative.rs:554`). Its own `value: AssocEvalVariant`
|
||||
(`{ code: i16 /* DXF resbuf type */, value: AssocEvalValue::Real(f64) }`,
|
||||
confirmed `associative.rs:579-596`) is the **cached, already-resolved
|
||||
number** — always present, read fast without evaluating anything. The
|
||||
embedded `AssocDependency.dependent_on: Handle`
|
||||
(`read_dependency`, `associative.rs:83-109`) is what actually links to a
|
||||
named parameter: `Handle::NULL` when the driving value is a bare
|
||||
literal, or an `AssocVariable`'s handle when it's driven by one. This
|
||||
gives a direct, confirmed mapping:
|
||||
- `DrivingValue::Literal(v)` → `AssocValueDependency` with
|
||||
`value = Real(v)`, `dependency.dependent_on = Handle::NULL`.
|
||||
- `DrivingValue::Named(name)` → the same, but `dependency.dependent_on`
|
||||
points at an `AssocVariable{ name, expression: <our formula>, .. }`
|
||||
object (create once per distinct name, reused by every constraint
|
||||
referencing it — `value` still carries the current resolved number,
|
||||
kept in sync with `ParameterTable`'s last evaluation, same duty our
|
||||
own `dof`/`conflicts` caches already have).
|
||||
- **`dimension_dependency`** → a *separate* dependency chain
|
||||
(`AssocAnnotationDependency`/`AssocDimDependencyBody`,
|
||||
`associative.rs:744-751` and `:1077-1082`) linking to the actual
|
||||
`Dimension` entity AutoCAD draws next to a constraint to show its
|
||||
current value on screen — nothing to do with named parameters at all.
|
||||
Not required for our purposes unless we also want the constraint to
|
||||
carry a visible on-canvas dimension the way AutoCAD's does (open
|
||||
question, not yet decided — §7).
|
||||
|
||||
Remaining gap before mapping directly: AutoCAD's `expression` is
|
||||
evaluated by a named, extensible `evaluator` (not fixed) — real-file
|
||||
survey needed to know what evaluator name(s) actually appear in practice
|
||||
(§7 open question 4), ours has one hardcoded evaluator.
|
||||
- **Missing `ConstraintKind`s**: Concentric, Colinear, Symmetric, Fixed,
|
||||
Midpoint, Normal, CenterPoint, PointOnCurve — real AutoCAD constraint
|
||||
types with no `ConstraintKind` variant to map from at all. Out of scope
|
||||
until/unless the sketch-constraint system itself grows them.
|
||||
- **`Tangent` involving an `Arc`** has no native mapping to design, since
|
||||
`build_constraint` doesn't support it at all yet (`sketch_solve.rs:239-241`
|
||||
— `Arc` isn't a supported `EntityGeom` variant in that function). A
|
||||
pre-existing gap in the constraint system itself, unrelated to and not
|
||||
blocking this document.
|
||||
|
||||
## 6. Persistence architecture: additive, not a replacement
|
||||
|
||||
**Proposal: write both.** Keep `OCS_SKETCH_CONSTRAINTS` as the authoritative
|
||||
round-trip format OpenCADStudio itself reads back
|
||||
(`sketch_persist::load_sketch_constraints_from_document`), and *additionally*
|
||||
materialize the `AssocNetwork`/.../`Assoc2dConstraintGroup` graph alongside
|
||||
it on save, for other applications to read.
|
||||
|
||||
Reasoning:
|
||||
- Our own model carries information the native graph can't represent
|
||||
losslessly today — `DrivingValue::Named` chief among them (§5), also
|
||||
`enabled` (a suppressed-but-kept constraint) which may or may not have a
|
||||
clean native equivalent (`AssocConstraintNodeData`'s `is_active` field on
|
||||
several variants looks like the right fit, but unconfirmed, §7).
|
||||
Round-tripping through the native format only and reconstructing our
|
||||
model from it risks silent data loss on every save/reopen cycle.
|
||||
- The native graph's own geometry-node fields (`center`, `radius`, `point`,
|
||||
...) are *snapshots* of the current solved geometry, not derived at load
|
||||
time — so they need to be kept in sync with whatever `ocs_gcs` last
|
||||
solved, same as we already do for our own tessellated wire geometry. Two
|
||||
representations of "the same" constraint, kept consistent by one save
|
||||
path, is a smaller invariant to maintain than trying to make the native
|
||||
graph the *only* source of truth for a system it can't fully express.
|
||||
- This is reversible: if the native graph turns out to work well enough
|
||||
(once tested against real AutoCAD/BricsCAD opens, §7) to *become* the
|
||||
primary format later, dropping the redundant `OCS_SKETCH_CONSTRAINTS`
|
||||
write is a small follow-up, not a rewrite.
|
||||
|
||||
## 7. Open questions
|
||||
|
||||
Three of the original five are now resolved from Autodesk's own primary
|
||||
sources (§2b), not just inferred from `cadcodec`'s struct shapes — kept
|
||||
here with their answers so the reasoning stays visible, not deleted.
|
||||
`cadcodec` itself still ships no constraint-bearing DWG/DXF fixture and no
|
||||
constraint-specific test (checked: no hits for `Assoc2dConstraintGroup`/
|
||||
`AssocConstraintNode` under any `tests/` directory or fixture path in the
|
||||
dependency), so even the resolved ones haven't been checked against a real
|
||||
written-and-reopened file yet — that step still matters (§8 stage 1) for
|
||||
confirming implementation correctness, just not for the *design* questions
|
||||
below anymore.
|
||||
|
||||
1. **Resolved.** Where the top-level `AssocNetwork` attaches:
|
||||
`AcDbAssocNetwork::getInstanceFromDatabase`/`getInstanceFromObject`'s own
|
||||
doc comments (§2b) state the default sub-dictionary key directly —
|
||||
`"ACAD_ASSOCNETWORK"` — used both for the one top-level network (owned by
|
||||
the database's Named Object Dictionary) and for each per-block-record
|
||||
network (owned by that `AcDbBlockTableRecord`'s own extension
|
||||
dictionary — confirming the parallel to `OCS_SKETCH_CONSTRAINTS`'s own
|
||||
attachment was right). The two are linked: creating a per-block network
|
||||
with `addToTopLevelNetwork = true` (the default) also registers it as an
|
||||
action inside the top-level network, so evaluating the top-level network
|
||||
cascades through every block's sub-network.
|
||||
2. **Resolved.** Sub-entity addressing for `ImplicitPoint`: confirmed the
|
||||
real addressing lives entirely in the constraint-node's own
|
||||
`point_type`/`point_index`/`curve_id` fields, not in
|
||||
`AssocPersistentSubentId` (that type turned out to be an abstract C++
|
||||
base class for a *different*, more general subentity-tracking need
|
||||
elsewhere in the associative framework — irrelevant to 2D constraint
|
||||
geometry). `AcDb::ImplicitPointType`'s exact enum (§4) settles the
|
||||
mapping.
|
||||
3. **Still open.** Whether a partially-populated graph (e.g. omitting
|
||||
`AssocPersSubentManager`, or leaving some `AssocAction` bookkeeping
|
||||
fields at their `Default`) still opens cleanly in real AutoCAD, or
|
||||
whether it requires every field correctly populated to avoid a repair
|
||||
prompt or silent constraint loss. Genuinely needs a real write-then-open
|
||||
test, not documentation — this is a robustness/tolerance question, not a
|
||||
structural one.
|
||||
4. **Narrowed to almost nothing.** `AssocVariable`'s `evaluator` field:
|
||||
`AcDbAssocVariable.h`'s own doc comment (§2b) says plainly — "If empty,
|
||||
the default evaluator for current acad version will be used." Leaving it
|
||||
as an empty string is officially-supported behavior per the SDK itself,
|
||||
not a guess. No further investigation needed unless a real file later
|
||||
shows this doesn't round-trip as expected.
|
||||
5. **Still open.** DWG version gating — `AssocConstraintNode`'s own reader
|
||||
already branches on `version.r2013_plus(dxf_version)` for one field
|
||||
(`dwg_stream_readers/object_reader/associative.rs:604`); unclear whether
|
||||
other fields need per-version handling this document hasn't looked for.
|
||||
Likely answerable the same way as questions 1/2/4 were (Autodesk's own
|
||||
docs usually note version-specific members) — just not done yet.
|
||||
6. **Still open, low priority.** Whether to bother wiring
|
||||
`dimension_dependency` at all (§5) — it drives the small on-canvas
|
||||
dimension label AutoCAD shows next to a constrained value, cosmetic
|
||||
rather than structural. Leaving it `Handle::NULL` is presumably safe (the
|
||||
constraint itself is still fully defined without it) but unconfirmed;
|
||||
deferring this to a later stage either way (§8).
|
||||
|
||||
## 8. Suggested staged approach
|
||||
|
||||
1. **Proof of concept, narrowest possible slice**: `Horizontal`/`Vertical`
|
||||
on a single `Line`, no dimensional constraints (sidesteps the
|
||||
`AssocVariable`/value-dependency wiring entirely, though that's now fully
|
||||
designed too — §5). Write the graph — `AssocNetwork` at
|
||||
`"ACAD_ASSOCNETWORK"` on the scope owner's extension dictionary (§7.1),
|
||||
one `Assoc2dConstraintGroup` action, one `BoundedLine` geometry node, one
|
||||
`Geometrical`/`"ACHORIZONTALCONSTRAINT"` (or vertical) constraint node —
|
||||
and open the file in whatever real-DWG-reading tool is available, to
|
||||
confirm AutoCAD/BricsCAD actually shows a constraint icon on the line and
|
||||
doesn't flag the file as corrupt. This is what answers open question 3
|
||||
(partial-graph tolerance) — the one remaining question documentation
|
||||
alone can't settle.
|
||||
2. **Geometric constraints, still no driving value**: add `Coincident`
|
||||
(needs `ImplicitPoint`/`kStartImplicit`/`kEndImplicit`, §4, now
|
||||
confirmed), `Parallel`, `Perpendicular`, `Equal` (both sub-kinds) —
|
||||
verify against the same real-file test from stage 1.
|
||||
3. **Dimensional constraints**: `Distance`, `Angle`, `Radius`, using the now
|
||||
fully-traced `value_dependency → AssocValueDependency → AssocVariable`
|
||||
chain (§5) — including deciding whether `DrivingValue::Named` gets the
|
||||
real native mapping designed there, or stays a resolved-literal degrade
|
||||
for a first pass.
|
||||
4. **Everything else**: `Tangent`'s non-circle-circle cases, the missing
|
||||
`ConstraintKind`s (§5) if/when the sketch-constraint system grows them,
|
||||
`dimension_dependency` (§7.6) if a visible on-canvas dimension label
|
||||
turns out to matter.
|
||||
|
||||
Each stage's file should be hand-inspected (or opened in real software)
|
||||
before moving to the next — the graph's correctness genuinely can't be
|
||||
verified by this codebase's own round-trip tests alone, since a bug that's
|
||||
symmetric (write it wrong, read the same wrongness back) would pass here and
|
||||
still fail to open correctly elsewhere.
|
||||
|
||||
## 9. Progress
|
||||
|
||||
**Implemented** (`src/scene/dwg_native_constraints.rs`, wired into both save
|
||||
paths in `src/app/update/file.rs` alongside the existing
|
||||
`materialize_sketch_constraints_for_save`/`materialize_named_parameters_for_save`
|
||||
calls). All 4 stages from §8 landed together rather than incrementally, since
|
||||
`ConstraintKind` only has the 10 variants already covered — there was no
|
||||
narrower slice left once Horizontal/Vertical's plumbing (network/dictionary
|
||||
creation, the two-pass builder, class-table registration) was in place.
|
||||
Covers: every `ConstraintKind` (`Coincident`, `Horizontal`, `Vertical`,
|
||||
`Parallel`, `Perpendicular`, `Equal`'s line/circle split, `Distance`,
|
||||
`Angle`, `Radius`, `Tangent`), `AssocGeomDependency` for whole-geometry nodes,
|
||||
`AssocValueDependency`/`AssocVariable` for dimensional constraints including
|
||||
`DrivingValue::Named`, and a resave-clears-and-rebuilds cleanup pass
|
||||
(`remove_owned_recursive`) so materializing twice doesn't accumulate orphans.
|
||||
Verified via real `save_to_bytes`/`load_bytes` round trips through
|
||||
`acadrust`'s own reader/writer (5 tests in that module) — this confirms
|
||||
self-consistency with `acadrust`'s implementation of the documented format,
|
||||
**not** real-world AutoCAD/BricsCAD compatibility (§7.3/§7.5 below remain
|
||||
unverified; no such software is available in this environment).
|
||||
|
||||
Two implementation-time findings not anticipated by the design above:
|
||||
|
||||
- **The top-level NOD-anchored network was dropped from scope.**
|
||||
`named_parameters_persist.rs`'s own doc comment already documents a real
|
||||
`acadrust` bug: a `Dictionary`-type object (like the database's Named
|
||||
Object Dictionary) used as an XRecord/dictionary owner loses its extension
|
||||
data through a DXF save specifically (`CadDocument::ensure_xrecord` only
|
||||
patches the owner's `xdictionary_handle` when `get_entity_mut(owner)`
|
||||
succeeds — a Dictionary isn't an entity, so it silently falls back to a
|
||||
DWG-only side map). Rather than hit that bug, this module only creates a
|
||||
network per `SketchScope::owner_handle()` (a real `BlockRecord` entity) —
|
||||
the same already-proven-safe attachment point `sketch_persist.rs` uses. No
|
||||
top-level network is created at all.
|
||||
- **DXF cannot carry the dependency chain — DWG is the only fully-faithful
|
||||
format.** Confirmed by reading `acadrust`'s DXF writer
|
||||
(`io/dxf/writer/section_writer.rs`'s `is_unrestorable_assoc_object`): it
|
||||
deliberately skips writing `AssocDependency`/`AssocValueDependency`/
|
||||
`AssocGeomDependency`/`AssocVariable` objects to DXF at all
|
||||
("Unrestorable associative-framework objects are not written") — DWG has
|
||||
no equivalent filter. A DXF save keeps the `Assoc2dConstraintGroup`/
|
||||
`AssocNetwork` shell and every node's own embedded geometry, but loses live
|
||||
geometry/value linkage and every `AssocVariable`. This module still
|
||||
materializes the same graph for both formats (the shell that survives DXF
|
||||
is strictly more than nothing, and dangling handles are an ordinary,
|
||||
tolerated pattern in both formats) rather than special-casing DXF to write
|
||||
less. Demonstrated directly by
|
||||
`a_dwg_save_keeps_the_full_dependency_chain_including_the_named_variable`
|
||||
vs. `a_dxf_save_keeps_only_the_constraint_group_shell` in that module's
|
||||
test suite.
|
||||
|
||||
Also confirmed while implementing (not previously documented): the DWG
|
||||
writer's `Assoc2dConstraintGroup` arm only emits node data at all when
|
||||
`nodes.first()` is `Some` — an empty `nodes` `Vec` skips node serialization
|
||||
entirely — so `nodes[0]` must always be a synthetic root pseudo-node (empty
|
||||
`class_name`, connected to every real node) even for a single-constraint
|
||||
graph; and a multi-geometry constraint (e.g. `Parallel`) references its
|
||||
second geometry via the generic `connections: Vec<i32>` field on
|
||||
`AssocConstraintNode`, not a second dedicated field on
|
||||
`AssocConstraintNodeData::Parallel` — confirmed from `AcConstraintGroupNode`'s
|
||||
own doc comment ("the connection between two nodes is not directed").
|
||||
|
||||
Of the original five open questions, three were resolved from Autodesk's own
|
||||
primary documentation (§7.1, 7.2, 7.4) before implementation started — the
|
||||
network's dictionary key, `ImplicitPoint`'s addressing enum, and the
|
||||
`AssocVariable` evaluator default were confirmed, not guessed. Two remain
|
||||
open (§7.3 partial-graph tolerance, §7.5 version gating) and still genuinely
|
||||
need a real AutoCAD/BricsCAD open, not just more reading — self-consistency
|
||||
through `acadrust`'s own round trip can't settle either one. Next action is
|
||||
opening a saved DWG in real software (or at minimum a third-party DWG
|
||||
inspector) to make progress on those two, and posting the GitHub Discussion
|
||||
Hakan asked for once that's done.
|
||||
@@ -1,477 +0,0 @@
|
||||
# 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: mirror
|
||||
`src/scene/sketch_persist.rs`'s pattern exactly — an `XRecord`/`Dictionary`
|
||||
blob on a well-known owner (that file's `XRECORD_KEY` /
|
||||
`materialize_*_for_save` / `load_*_from_document` shape is the direct
|
||||
precedent to copy, including the version-prefixed bincode envelope and the
|
||||
hook points into `on_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_param` is currently
|
||||
`Option<f64>` (`src/scene/sketch_constraints.rs:97`), consumed directly in
|
||||
`sketch_solve.rs` (`build_constraint`, lines ~184/190/195, one per
|
||||
Distance/Angle/Radius) via `sys.add_param(c.driving_param?, ...)`. This
|
||||
would become something like `DrivingValue::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 with
|
||||
`refresh_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`'s `PROP_LENGTH`/`PROP_HEIGHT`/etc. are
|
||||
plain `f64` fields on structs like `SolidHistoryBox` (e.g. `value.length`,
|
||||
set via the generic property-editing dispatch at `solid_history.rs:2239`).
|
||||
Whether named parameters should reach these too (so a box's height can
|
||||
reference the same `plate_thickness` a 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 more `PROP_*`
|
||||
constants than there are `ConstraintKind`s).
|
||||
|
||||
## 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. hold `sweep_entity: Option<EmbeddedEntity>`
|
||||
— a **snapshotted copy** of the profile geometry, not a live `Handle` back
|
||||
into the sketch (`src/scene/model/solid_history.rs`, grep `sweep_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.
|
||||
|
||||
1. Data model: the parameter table type + expression parser, unit-tested in
|
||||
isolation (parse, evaluate, cycle detection) with no document/UI
|
||||
integration yet.
|
||||
2. 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).
|
||||
3. Wire `SketchConstraint::driving_param` to resolve through the table
|
||||
(Distance/Angle/Radius constraints only, the three existing call sites).
|
||||
4. 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`'s `DistanceConstraintCommand`
|
||||
/ `AngleConstraintCommand`).
|
||||
5. (If scoped in per §2/§3) extend to `solid_history.rs`'s `PROP_*` values.
|
||||
|
||||
## 5. Open questions to resolve before/while coding (not resolved by this doc)
|
||||
|
||||
1. **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.
|
||||
2. **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.
|
||||
3. **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 model
|
||||
`refresh_sketch_constraints` already established, but confirm rather than
|
||||
assume.
|
||||
4. **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-`BlockRecord` XRecord 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.
|
||||
5. **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 check
|
||||
`evalexpr`/`meval` first: 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's `Call(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) alongside `is_valid_name`'s identifier rule (leading
|
||||
letter/underscore, then alphanumeric/underscore).
|
||||
- **Cycle detection happens at `ParameterTable::set` time, 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. `set` walks 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 becomes
|
||||
`ParamError::UndefinedReference` if `resolve`/`resolve_all` actually needs
|
||||
that value later and it's still missing. A `resolve`-time cycle guard
|
||||
exists too (`resolve_inner`'s `stack` check) but is defense-in-depth only
|
||||
— unreachable through the public API, since `set` already 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 through `set`), not a path the design expects to hit.
|
||||
- **`resolve_all` isolates 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::set` is 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 check `contains` itself
|
||||
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_all` isolation,
|
||||
remove-then-resolve leaving a dependent gracefully undefined (not
|
||||
panicking), upsert semantics, insertion-order stability across
|
||||
redefinitions, and a `bincode` round-trip (both `Expr` and `Parameter`
|
||||
derive `Serialize`/`Deserialize` already, ready for stage 2's XRecord
|
||||
persistence — not exercised through the real save/load path yet, just
|
||||
in-memory `bincode::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 vendored `cadcodec`
|
||||
source's own `extension_dictionary_handle` explicitly resolves an
|
||||
extension dictionary for a `Dictionary`-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
|
||||
discipline `sketch_persist`'s stage 4 applied). Root cause, traced into
|
||||
the vendored source: `CadDocument::ensure_xrecord` only writes the
|
||||
*owner's own* `xdictionary_handle` field when `get_entity_mut(owner)`
|
||||
succeeds (i.e. `owner` is a real "entity"); a `Dictionary` owner like the
|
||||
root NOD isn't one, so `ensure_xrecord` falls back to a runtime-only side
|
||||
map (`xdic_by_handle`) instead. The DWG writer consults that side map;
|
||||
grepping `io/dxf/writer/` for it turns up nothing — the DXF writer only
|
||||
ever reads the owner's own `xdictionary_handle` field, 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 external `acadrust`/`cadkernel`
|
||||
crates") the fix taken was to pick a different owner rather than patch
|
||||
the dependency: **`document.header.model_space_block_handle`** — a real
|
||||
`BlockRecord` entity, the exact anchor `sketch_persist`'s own
|
||||
`SketchScope::ModelSpace` case 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 every `CadDocument`
|
||||
and 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 in `cadcodec` itself (teach `ensure_xrecord` to also
|
||||
set a `Dictionary`/`Layout`/`XRecord`/`PlotSettings`/`VisualStyle`/
|
||||
`Material`/`ProxyObject` owner's own `xdictionary_handle` field, not just
|
||||
entities'), not something to route around a second time.
|
||||
- 5 new tests (`named_parameters_persist.rs`): in-memory materialize+reload
|
||||
agreement, real `save_to_bytes`/`load_bytes` round trips for both DXF and
|
||||
DWG (the two that caught the gap above), an empty table not persisting,
|
||||
and format-version rejection — same shape as `sketch_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:
|
||||
|
||||
```rust
|
||||
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`'s `build_constraint`/`solve_scope`
|
||||
now take a `&ParameterTable` (threaded from `Scene::named_parameters` at
|
||||
both call sites, `refresh_sketch_constraints` and
|
||||
`solve_sketch_constraints_preview`) and resolve `driving_param` at the
|
||||
three existing call sites (Distance/Angle/Radius) via `c.driving_param.
|
||||
as_ref()?.resolve(params).ok()?` — a resolve failure (an undefined
|
||||
reference, a division by zero; a cycle should be unreachable since `set`
|
||||
already refuses to create one) is treated exactly like every other
|
||||
unbuildable-constraint case `build_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::AddSketchConstraint` intentionally left as `Option<f64>`**
|
||||
(`src/command.rs`) rather than also switching it to `DrivingValue` — no UI
|
||||
can author a `Named` reference yet (that's stage 4's picker), so every
|
||||
existing call site (`DistanceConstraintCommand`/`AngleConstraintCommand`
|
||||
in `src/modules/draw/constrain/value.rs`, the Horizontal/Vertical/etc.
|
||||
dispatch in `src/app/commands/draw.rs`) needed no changes at all; the one
|
||||
conversion point is `command_driver.rs`'s `CmdResult::AddSketchConstraint`
|
||||
handler, which now wraps the literal into `DrivingValue::Literal` right
|
||||
before calling `SketchConstraintSet::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 `&DrivingValue`
|
||||
instead of a bare `f64`: 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 threading
|
||||
`ParameterTable` into 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 a `Named` driving_param yet, so it isn't reachable
|
||||
today).
|
||||
- **`sketch_persist::FORMAT_VERSION` bumped 1 → 2**: `SketchConstraint`'s
|
||||
serialized shape changed (a bare `f64` became 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`): a `Distance`
|
||||
constraint driven by a named reference solves correctly and re-solves
|
||||
when the parameter's own value is redefined; a `Distance` constraint
|
||||
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 existing `driving_param`-touching test (in
|
||||
`sketch_constraints.rs`, `sketch_persist.rs`,
|
||||
`tests/sketch_constraints_xrecord_roundtrip.rs`,
|
||||
`tests/sketch_constraints_solve.rs`) updated to construct
|
||||
`DrivingValue::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 --workspace` stops 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 bare `cargo test --workspace`. Re-ran with
|
||||
`--no-fail-fast` for this stage's check and confirmed every binary (not
|
||||
just the lib crate) passes except that one known-unrelated test. Worth
|
||||
using `--no-fail-fast` for 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 new `PARAMETERS` command (AutoCAD's own name
|
||||
for the equivalent feature — `src/app/commands/view.rs`, dispatched
|
||||
alongside `ALIASEDIT`; registered in the autocomplete/MCP command list
|
||||
from the start, unlike the six pre-existing constraint commands
|
||||
`docs/backlog.md` flags as missing that registration). Built by mirroring
|
||||
`alias_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 to `Scene::
|
||||
named_parameters` on Apply — a better fit here than for aliases, even:
|
||||
`ParameterTable::set` validates (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
|
||||
`ParameterTable` built 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" philosophy
|
||||
`ParameterTable`/`sketch_solve.rs` already 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 — `set`
|
||||
upserts by name, so the buffer's later row would just overwrite the
|
||||
earlier one with no indication anything was discarded. Added
|
||||
`duplicate_name_rows` (checked first, before any `set` call, 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 same `default_value`-is-a-one-time-snapshot
|
||||
precedent these commands already relied on, since `on_text_input` has no
|
||||
document access); `parse_driving_value` tries 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`'s
|
||||
`driving_param` (left as a bare `Option<f64>` in stage 3, deliberately,
|
||||
exactly so this stage could revisit it) is now `Option<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_param` is `DrivingValue::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 same `begin_undo`/`bump_entities`/`commit_undo_delta` bracket
|
||||
`resolve_one_sketch_conflict` already 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 `HistorySnapshot` variant mirroring
|
||||
`SketchConstraints` (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's `preview` (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 in `command_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 through
|
||||
`apply_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-fast` throughout 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 reach `SketchConstraint::
|
||||
driving_param` (Distance/Angle/Radius), not `solid_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::set` already
|
||||
rejects an invalid or builtin-colliding name (stage 1); the panel surfaces
|
||||
that rejection as the row's error text via `preview`/Apply, but there's no
|
||||
live per-keystroke nudge beyond that (e.g. graying out `Apply` while any
|
||||
row has an error) — a small, optional polish, not a functional gap.
|
||||
- **Live UI verification**, per the note above.
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user