forked from 1b-agent/opencadstudio-upstream
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2d39b049d
commit
7cddf31bcb
47 files changed
+4136
-52
No files matched your search
@@ -19,9 +19,30 @@ use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, InputKind};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::named_parameters::DrivingValue;
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
use crate::scene::Scene;
|
||||
|
||||
/// Recognizes a typed prompt token as either a numeric literal or a
|
||||
/// reference to an existing named parameter (`docs/
|
||||
/// named_parameters_design.md` stage 4's "a way to pick a named parameter
|
||||
/// instead of typing a literal" — the same prompt takes either, mirroring
|
||||
/// how AutoCAD's own dimensional-constraint prompts accept an expression in
|
||||
/// place of a bare value). `known_names` is a one-time snapshot taken at
|
||||
/// command construction (`DistanceConstraintCommand`/`AngleConstraintCommand
|
||||
/// ::new`, which already has `&Scene`) since `on_text_input` itself has no
|
||||
/// document access — the same constraint `default_value` already works
|
||||
/// around. Only *existence* is checked here; the actual value is resolved
|
||||
/// fresh at solve time (`sketch_solve::build_constraint`), consistent with
|
||||
/// this project's "no incremental/cached resolution" approach throughout.
|
||||
fn parse_driving_value(text: &str, known_names: &[String]) -> Option<DrivingValue> {
|
||||
let text = text.trim();
|
||||
if let Ok(value) = text.parse::<f64>() {
|
||||
return Some(DrivingValue::Literal(value));
|
||||
}
|
||||
known_names.iter().find(|n| n.as_str() == text).map(|n| DrivingValue::Named(n.clone()))
|
||||
}
|
||||
|
||||
pub mod distance_tool {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
@@ -55,6 +76,9 @@ pub struct DistanceConstraintCommand {
|
||||
/// split between line-length (`Distance`) and circle-radius (`Radius`).
|
||||
is_circle: bool,
|
||||
default_value: f64,
|
||||
/// Snapshot of `Scene::named_parameters`' names at construction time —
|
||||
/// see `parse_driving_value`'s doc comment for why a snapshot.
|
||||
known_param_names: Vec<String>,
|
||||
}
|
||||
|
||||
impl DistanceConstraintCommand {
|
||||
@@ -70,12 +94,15 @@ impl DistanceConstraintCommand {
|
||||
EntityType::Circle(c) => (true, c.radius),
|
||||
_ => return None,
|
||||
};
|
||||
Some(Self { handle, is_circle, default_value })
|
||||
let known_param_names = scene.named_parameters().iter().map(|p| p.name.clone()).collect();
|
||||
Some(Self { handle, is_circle, default_value, known_param_names })
|
||||
}
|
||||
|
||||
fn build(&self, target: f64) -> Option<CmdResult> {
|
||||
if target <= 0.0 {
|
||||
return None;
|
||||
fn build(&self, target: DrivingValue) -> Option<CmdResult> {
|
||||
if let DrivingValue::Literal(v) = target {
|
||||
if v <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let (kind, refs, label) = if self.is_circle {
|
||||
(ConstraintKind::Radius, vec![SketchRef::whole(self.handle)], "Radius constraint")
|
||||
@@ -108,11 +135,11 @@ impl CadCommand for DistanceConstraintCommand {
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
self.build(self.default_value).unwrap_or(CmdResult::Cancel)
|
||||
self.build(DrivingValue::Literal(self.default_value)).unwrap_or(CmdResult::Cancel)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let value: f64 = text.trim().parse().ok()?;
|
||||
let value = parse_driving_value(text, &self.known_param_names)?;
|
||||
self.build(value)
|
||||
}
|
||||
|
||||
@@ -126,6 +153,9 @@ pub struct AngleConstraintCommand {
|
||||
fixed_handle: Handle,
|
||||
moving_handle: Handle,
|
||||
default_value: f64,
|
||||
/// Snapshot of `Scene::named_parameters`' names at construction time —
|
||||
/// see `parse_driving_value`'s doc comment for why a snapshot.
|
||||
known_param_names: Vec<String>,
|
||||
}
|
||||
|
||||
impl AngleConstraintCommand {
|
||||
@@ -139,14 +169,15 @@ impl AngleConstraintCommand {
|
||||
let a1 = (f.end.y - f.start.y).atan2(f.end.x - f.start.x);
|
||||
let a2 = (m.end.y - m.start.y).atan2(m.end.x - m.start.x);
|
||||
let default_value = (a2 - a1).to_degrees();
|
||||
Some(Self { fixed_handle: fixed, moving_handle: moving, default_value })
|
||||
let known_param_names = scene.named_parameters().iter().map(|p| p.name.clone()).collect();
|
||||
Some(Self { fixed_handle: fixed, moving_handle: moving, default_value, known_param_names })
|
||||
}
|
||||
|
||||
fn build(&self, target_degrees: f64) -> Option<CmdResult> {
|
||||
fn build(&self, target: DrivingValue) -> Option<CmdResult> {
|
||||
Some(CmdResult::AddSketchConstraint {
|
||||
kind: ConstraintKind::Angle,
|
||||
refs: vec![SketchRef::whole(self.fixed_handle), SketchRef::whole(self.moving_handle)],
|
||||
driving_param: Some(target_degrees),
|
||||
driving_param: Some(target),
|
||||
label: "Angle constraint",
|
||||
})
|
||||
}
|
||||
@@ -170,11 +201,11 @@ impl CadCommand for AngleConstraintCommand {
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
self.build(self.default_value).unwrap_or(CmdResult::Cancel)
|
||||
self.build(DrivingValue::Literal(self.default_value)).unwrap_or(CmdResult::Cancel)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let value: f64 = text.trim().parse().ok()?;
|
||||
let value = parse_driving_value(text, &self.known_param_names)?;
|
||||
self.build(value)
|
||||
}
|
||||
|
||||
@@ -182,3 +213,30 @@ impl CadCommand for AngleConstraintCommand {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_driving_value_prefers_a_numeric_literal() {
|
||||
let known = vec!["hole_dia".to_string()];
|
||||
assert_eq!(parse_driving_value("12.5", &known), Some(DrivingValue::Literal(12.5)));
|
||||
// Distance's own `build` rejects a non-positive target later; the
|
||||
// parse step itself accepts any number, negative included.
|
||||
assert_eq!(parse_driving_value("-3", &known), Some(DrivingValue::Literal(-3.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_driving_value_recognizes_a_known_parameter_name() {
|
||||
let known = vec!["hole_dia".to_string(), "plate_len".to_string()];
|
||||
assert_eq!(parse_driving_value("hole_dia", &known), Some(DrivingValue::Named("hole_dia".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_driving_value_rejects_an_unknown_token() {
|
||||
let known = vec!["hole_dia".to_string()];
|
||||
assert_eq!(parse_driving_value("bogus", &known), None);
|
||||
assert_eq!(parse_driving_value("", &known), None);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user