Add named parameters, UI scale setting, OPTIONS/OP dispatch, and help manual

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]>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-09 13:50:08 +02:00
1 parent e2d39b049d
commit 7cddf31bcb
47 files changed
+4136 -52

No files matched your search

+61 -3
View File
@@ -5,6 +5,7 @@
// constrained neighbors, produces the expected change list, and an
// unrelated edit doesn't touch anything (§4.3's "what does NOT trigger").
use OpenCADStudio::scene::named_parameters::DrivingValue;
use OpenCADStudio::scene::sketch_constraints::{ConstraintKind, SketchRef, SketchScope};
use OpenCADStudio::scene::{ChangeKind, Scene};
use acadrust::entities::EntityType;
@@ -96,7 +97,7 @@ fn distance_constraint_holds_the_target_length_after_an_unrelated_endpoint_edit(
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
ConstraintKind::Distance,
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
Some(20.0),
Some(DrivingValue::Literal(20.0)),
);
set_line_end(&mut scene, line, Vector3::new(5.0, 5.0, 0.0)); // arbitrary edit, not length=20
@@ -107,6 +108,63 @@ fn distance_constraint_holds_the_target_length_after_an_unrelated_endpoint_edit(
assert!((len - 20.0).abs() < 1e-6, "length should have been solved to the driving value, got {len}");
}
/// `named_parameters_design.md` stage 3: a `Distance` constraint's driving
/// value can be a named-parameter reference instead of a literal, resolved
/// through `Scene::named_parameters` at solve time.
#[test]
fn distance_constraint_resolves_a_named_parameter_reference() {
let mut scene = Scene::new();
let line = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
scene.named_parameters_mut().set("target_len", "20").unwrap();
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
ConstraintKind::Distance,
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
Some(DrivingValue::Named("target_len".to_string())),
);
set_line_end(&mut scene, line, Vector3::new(5.0, 5.0, 0.0)); // arbitrary edit, not length=20
scene.bump_entities(&[(line, ChangeKind::Modified)]);
let (start, end) = line_endpoints(&scene, line);
let len = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
assert!((len - 20.0).abs() < 1e-6, "length should have resolved the named parameter, got {len}");
// Editing the parameter itself and touching the scope again should
// ripple through, same as editing a literal driving value would.
scene.named_parameters_mut().set("target_len", "8").unwrap();
scene.bump_entities(&[(line, ChangeKind::Modified)]);
let (start, end) = line_endpoints(&scene, line);
let len = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
assert!((len - 8.0).abs() < 1e-6, "length should track the redefined parameter value, got {len}");
}
/// A `driving_param` referencing a named parameter that doesn't (or no
/// longer) exists must not panic the solve — same "skip, don't panic"
/// contract `build_constraint`'s doc comment gives every other unbuildable
/// constraint. The referenced line's two endpoint coordinates are still
/// registered (via `point_ref`) even though the constraint itself can't be
/// built, so they're just left free — the edit that triggered this solve is
/// not undone or altered.
#[test]
fn distance_constraint_with_an_undefined_named_reference_is_skipped_not_panicked() {
let mut scene = Scene::new();
let line = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
ConstraintKind::Distance,
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
Some(DrivingValue::Named("does_not_exist".to_string())),
);
let edited_end = Vector3::new(5.0, 5.0, 0.0);
set_line_end(&mut scene, line, edited_end);
scene.bump_entities(&[(line, ChangeKind::Modified)]); // must not panic
let (_, end) = line_endpoints(&scene, line);
assert_eq!(end, edited_end, "an unresolvable driving reference must leave the edit alone, not move it toward a phantom target");
}
#[test]
fn tangent_constraint_between_a_line_and_a_circle_solves_to_touching() {
let mut scene = Scene::new();
@@ -285,8 +343,8 @@ fn two_conflicting_distance_targets_are_reported_as_conflicting() {
let mut scene = Scene::new();
let a = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
let first = set.add(ConstraintKind::Distance, vec![SketchRef::point(a, 0), SketchRef::point(a, 1)], Some(20.0));
let second = set.add(ConstraintKind::Distance, vec![SketchRef::point(a, 0), SketchRef::point(a, 1)], Some(50.0));
let first = set.add(ConstraintKind::Distance, vec![SketchRef::point(a, 0), SketchRef::point(a, 1)], Some(DrivingValue::Literal(20.0)));
let second = set.add(ConstraintKind::Distance, vec![SketchRef::point(a, 0), SketchRef::point(a, 1)], Some(DrivingValue::Literal(50.0)));
scene.bump_entities(&[(a, ChangeKind::Modified)]);
@@ -5,6 +5,7 @@
// (save/load integration, the "lazy" persistence model) is gated on this
// test passing — see the design doc §8 stage 2 and §7 open question 1.
use OpenCADStudio::scene::named_parameters::DrivingValue;
use OpenCADStudio::scene::sketch_constraints::{ConstraintKind, SketchConstraintSet, SketchRef, SketchScope};
use OpenCADStudio::scene::Scene;
use acadrust::entities::EntityType;
@@ -20,9 +21,13 @@ fn sample_set(scope: SketchScope) -> SketchConstraintSet {
vec![SketchRef::point(Handle::new(101), 0), SketchRef::point(Handle::new(102), 1)],
None,
);
set.add(ConstraintKind::Distance, vec![SketchRef::whole(Handle::new(103))], Some(25.0));
set.add(ConstraintKind::Angle, vec![SketchRef::whole(Handle::new(104)), SketchRef::whole(Handle::new(105))], Some(45.0));
set.add(ConstraintKind::Radius, vec![SketchRef::whole(Handle::new(106))], Some(3.5));
set.add(ConstraintKind::Distance, vec![SketchRef::whole(Handle::new(103))], Some(DrivingValue::Literal(25.0)));
set.add(
ConstraintKind::Angle,
vec![SketchRef::whole(Handle::new(104)), SketchRef::whole(Handle::new(105))],
Some(DrivingValue::Literal(45.0)),
);
set.add(ConstraintKind::Radius, vec![SketchRef::whole(Handle::new(106))], Some(DrivingValue::Literal(3.5)));
set
}