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
@@ -1057,7 +1057,6 @@ mod tests {
|
||||
"CHANGELOG",
|
||||
"CUI",
|
||||
"ALIASEDIT",
|
||||
"OPTIONS",
|
||||
];
|
||||
for cmd in standalone {
|
||||
assert!(
|
||||
@@ -1613,4 +1612,3 @@ mod tests {
|
||||
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("MTP"));
|
||||
}
|
||||
}
|
||||
|
||||
+135
-156
@@ -915,16 +915,8 @@ impl OpenCADStudio {
|
||||
TableCellEditStart::Started
|
||||
}
|
||||
|
||||
/// Design doc §6.2 (stage 8, Phase B live inference): after a `LINE`
|
||||
/// command commits `new_handle`, auto-adds a Coincident constraint for
|
||||
/// every endpoint that already coincides with an existing entity's
|
||||
/// point in the current scope (`sketch_constraints::infer_coincident_refs`).
|
||||
/// Suppressible by holding Shift while placing the point — reuses the
|
||||
/// existing `shift_down` tracking rather than the design doc's proposed
|
||||
/// new `CadCommand::set_modifier` mechanism, since this dispatch site
|
||||
/// already reads `self.shift_down` directly (`apply_cmd_result_inner`'s
|
||||
/// Ctrl/Shift push at :392-393) and needs no per-command plumbing.
|
||||
/// No-op (and no undo entry) when nothing was inferred.
|
||||
/// Adds inferred Coincident constraints after a line is committed.
|
||||
/// Holding Shift suppresses inference. No match means no undo entry.
|
||||
fn infer_and_add_coincident_constraints(&mut self, i: usize, new_handle: Handle, entity: &acadrust::EntityType) {
|
||||
if self.shift_down {
|
||||
return;
|
||||
@@ -939,7 +931,12 @@ impl OpenCADStudio {
|
||||
if pairs.is_empty() {
|
||||
return;
|
||||
}
|
||||
let constraints_before = self.tabs[i].scene.sketch_constraint_set(scope).cloned();
|
||||
let constraints_before = self
|
||||
.tabs[i]
|
||||
.scene
|
||||
.sketch_constraint_set(scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| crate::scene::sketch_constraints::SketchConstraintSet::new(scope));
|
||||
let mut touched: Vec<Handle> = Vec::new();
|
||||
for (new_ref, existing_ref) in &pairs {
|
||||
touched.push(new_ref.entity);
|
||||
@@ -948,13 +945,10 @@ impl OpenCADStudio {
|
||||
touched.sort();
|
||||
touched.dedup();
|
||||
let label = "Coincident (inferred)";
|
||||
// Mirrors `CmdResult::AddSketchConstraint`'s handler: brackets the
|
||||
// add-and-solve in its own `begin_undo`/`commit_undo_delta` so any
|
||||
// geometry the solve moves (the two points may be within epsilon but
|
||||
// not bit-identical) is captured as an undoable `Delta`, then pushes
|
||||
// the constraint-set edit itself as the separate, adjacent
|
||||
// `SketchConstraints` entry stage 9 established.
|
||||
let pending = self.begin_undo(i, label, touched.len(), true);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_sketch_constraints_before(scope, constraints_before);
|
||||
for (new_ref, existing_ref) in pairs {
|
||||
self.tabs[i].scene.sketch_constraint_set_mut(scope).add(
|
||||
crate::scene::sketch_constraints::ConstraintKind::Coincident,
|
||||
@@ -968,31 +962,9 @@ impl OpenCADStudio {
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
self.push_sketch_constraints_history(i, label, scope, constraints_before);
|
||||
}
|
||||
|
||||
/// Design doc §6.4 (stage 11, Phase C): removes the first
|
||||
/// redundant/conflicting constraint `SketchConstraintSet::conflicts`
|
||||
/// (populated by `solve_scope`'s `ocs_gcs::diagnosis::classify_redundant`
|
||||
/// call, stage 10) flags in the active tab's current sketch scope, and
|
||||
/// re-solves.
|
||||
///
|
||||
/// **Scope-down from this doc**: a full `ConflictResolverPanel` — a
|
||||
/// window listing every flagged constraint by name with a cyclable live
|
||||
/// preview of each candidate fix before committing — is real, standalone
|
||||
/// UI work (new window state, `Message` plumbing for open/close/select/
|
||||
/// preview/commit) this pass didn't have live-UI-testing access to
|
||||
/// verify (the desktop session's app focus was blocked by an unrelated
|
||||
/// stuck System Settings window for the whole of this stage). What's
|
||||
/// built instead is bounded but real: a status-bar pill, visible only
|
||||
/// when the current scope has at least one flagged conflict, that
|
||||
/// removes exactly one flagged constraint per click — "guided" in that
|
||||
/// the flagged set already comes from `classify_redundant`, just without
|
||||
/// per-candidate naming/preview/choice. A user with several conflicts
|
||||
/// clicks it repeatedly, watching the DOF/conflict count drop, same
|
||||
/// outcome as the panel's "remove and re-solve" action minus the
|
||||
/// browsing UI. The full panel remains open follow-up work, not silently
|
||||
/// dropped.
|
||||
/// Removes and re-solves the first conflicting constraint in the active scope.
|
||||
pub(super) fn resolve_one_sketch_conflict(&mut self) {
|
||||
let i = self.active_tab;
|
||||
let scope = self.tabs[i].current_sketch_scope();
|
||||
@@ -1002,8 +974,11 @@ impl OpenCADStudio {
|
||||
let touched: Vec<Handle> = constraint.refs.iter().map(|r| r.entity).collect();
|
||||
let label = "Remove conflicting constraint";
|
||||
|
||||
let constraints_before = Some(set.clone());
|
||||
let constraints_before = set.clone();
|
||||
let pending = self.begin_undo(i, label, touched.len(), true);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_sketch_constraints_before(scope, constraints_before);
|
||||
self.tabs[i].scene.sketch_constraint_set_mut(scope).remove(id);
|
||||
let changes: Vec<(Handle, crate::scene::ChangeKind)> =
|
||||
touched.into_iter().map(|h| (h, crate::scene::ChangeKind::Modified)).collect();
|
||||
@@ -1011,28 +986,10 @@ impl OpenCADStudio {
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
self.push_sketch_constraints_history(i, label, scope, constraints_before);
|
||||
}
|
||||
|
||||
/// PARAMETERS' Apply button (`docs/named_parameters_design.md` stage 4):
|
||||
/// rebuilds the active tab's `Scene::named_parameters` from the
|
||||
/// editor's working buffer, reports any row that failed to validate via
|
||||
/// the command line, and re-solves every scope with a constraint driven
|
||||
/// by a named parameter — the entire point of a *named* parameter is
|
||||
/// that editing it ripples to whatever references it, the same way
|
||||
/// editing a constraint's literal value already does.
|
||||
///
|
||||
/// **Deliberate scope-down, matching this project's established
|
||||
/// treatment of undo-granularity questions** (design doc §5 open
|
||||
/// question 3, and the sibling constraint-system project's stage 9): the
|
||||
/// geometry this ripple moves is bracketed into one undo step below
|
||||
/// (same `begin_undo`/`bump_entities`/`commit_undo_delta` pattern
|
||||
/// `resolve_one_sketch_conflict` above uses), but the parameter *table*
|
||||
/// edit itself has no undo entry of its own yet — that would need a new
|
||||
/// `HistorySnapshot` variant mirroring `SketchConstraints`
|
||||
/// (`push_sketch_constraints_history`). Left for a follow-up, not
|
||||
/// silently dropped: undoing after an Apply undoes the geometry it
|
||||
/// moved, not the parameter values themselves.
|
||||
/// Rebuilds the active tab's `Scene::named_parameters` from the
|
||||
/// editor's working buffer and re-solves dependent constraints.
|
||||
pub(super) fn apply_named_parameter_editor_rows(&mut self) {
|
||||
let i = self.active_tab;
|
||||
// A name shared by more than one row can't be resolved by picking
|
||||
@@ -1057,7 +1014,6 @@ impl OpenCADStudio {
|
||||
failed += duplicates.len();
|
||||
}
|
||||
let param_count = table.len();
|
||||
self.tabs[i].scene.named_parameters = table;
|
||||
|
||||
// The whole table just changed, not one isolated value, so there is
|
||||
// no cheaper "which handles actually moved" test worth doing here —
|
||||
@@ -1073,17 +1029,18 @@ impl OpenCADStudio {
|
||||
.flat_map(|c| c.refs.iter().map(|r| r.entity))
|
||||
.collect();
|
||||
|
||||
let pending = self.begin_undo(i, "Apply named parameters", touched.len(), true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters = table;
|
||||
self.tabs[i].dirty = true;
|
||||
if !touched.is_empty() {
|
||||
let pending = self.begin_undo(i, "Apply named parameters", touched.len(), true);
|
||||
let changes: Vec<(Handle, crate::scene::ChangeKind)> =
|
||||
touched.into_iter().map(|h| (h, crate::scene::ChangeKind::Modified)).collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
|
||||
self.tabs[i].dirty = true;
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
if failed == 0 {
|
||||
self.command_line.push_info(crate::tf!("{} parameter(s) applied.", param_count).as_ref());
|
||||
}
|
||||
@@ -1111,13 +1068,9 @@ impl OpenCADStudio {
|
||||
if touched.is_empty() {
|
||||
return;
|
||||
}
|
||||
let pending = self.begin_undo(i, "Edit named parameter", touched.len(), true);
|
||||
let changes: Vec<(Handle, crate::scene::ChangeKind)> =
|
||||
touched.into_iter().map(|h| (h, crate::scene::ChangeKind::Modified)).collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
|
||||
/// Commits one field of one Parameters-section row (Properties panel) —
|
||||
@@ -1150,24 +1103,30 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
let (result, resolve_name) = match field {
|
||||
ParamField::Formula => {
|
||||
if typed.is_empty() || typed == current.source {
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
(self.tabs[i].scene.named_parameters_mut().set(¤t.name, &typed), current.name.clone())
|
||||
match field {
|
||||
ParamField::Formula if typed.is_empty() || typed == current.source => {
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
ParamField::Name if typed.is_empty() || typed == current.name => {
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
ParamField::Name if self.tabs[i].scene.named_parameters().contains(&typed) => {
|
||||
self.command_line.push_error(crate::tf!("Parameter '{}' already exists.", typed).as_ref());
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let pending = self.begin_undo(i, "Edit named parameter", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
|
||||
let (result, resolve_name) = match field {
|
||||
ParamField::Formula =>
|
||||
(self.tabs[i].scene.named_parameters_mut().set(¤t.name, &typed), current.name.clone()),
|
||||
ParamField::Name => {
|
||||
if typed.is_empty() || typed == current.name {
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
if self.tabs[i].scene.named_parameters().contains(&typed) {
|
||||
self.command_line.push_error(crate::tf!("Parameter '{}' already exists.", typed).as_ref());
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
self.tabs[i].scene.named_parameters_mut().remove(¤t.name);
|
||||
let outcome = self.tabs[i].scene.named_parameters_mut().set(&typed, ¤t.source);
|
||||
if outcome.is_err() {
|
||||
@@ -1178,17 +1137,49 @@ impl OpenCADStudio {
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
self.tabs[i].scene.take_undo_recording();
|
||||
self.command_line.push_error(crate::tf!("Parameter '{}': {}", current.name, e).as_ref());
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
if field == ParamField::Name {
|
||||
let affected: Vec<usize> = self.tabs[i]
|
||||
.scene
|
||||
.sketch_constraints
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, set)| {
|
||||
set.constraints
|
||||
.iter()
|
||||
.any(|constraint| {
|
||||
matches!(&constraint.driving_param, Some(crate::scene::named_parameters::DrivingValue::Named(name)) if name == ¤t.name)
|
||||
})
|
||||
.then_some(index)
|
||||
})
|
||||
.collect();
|
||||
for set_index in affected {
|
||||
let scope = self.tabs[i].scene.sketch_constraints[set_index].scope;
|
||||
let before = self.tabs[i].scene.sketch_constraints[set_index].clone();
|
||||
self.tabs[i].scene.record_undo_sketch_constraints_before(scope, before);
|
||||
for constraint in &mut self.tabs[i].scene.sketch_constraints[set_index].constraints {
|
||||
if let Some(crate::scene::named_parameters::DrivingValue::Named(name)) = &mut constraint.driving_param {
|
||||
if name == ¤t.name {
|
||||
*name = typed.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.resolve_named_parameter_edit(i, &resolve_name);
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
self.refresh_properties();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Removes Parameters-section row `index` immediately — no confirmation,
|
||||
/// matching AutoCAD's own Parameters Manager delete button. Any
|
||||
/// Removes Parameters-section row `index` immediately. Any
|
||||
/// constraint that referenced it gets the same "undefined reference"
|
||||
/// resolve-failure treatment a formula's own bad reference already gets
|
||||
/// (`build_constraint`) — re-solving surfaces that rather than needing
|
||||
@@ -1198,8 +1189,13 @@ impl OpenCADStudio {
|
||||
let Some(name) = self.tabs[i].scene.named_parameters().iter().nth(index).map(|p| p.name.clone()) else {
|
||||
return Task::none();
|
||||
};
|
||||
let pending = self.begin_undo(i, "Delete named parameter", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters_mut().remove(&name);
|
||||
self.resolve_named_parameter_edit(i, &name);
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
self.refresh_properties();
|
||||
Task::none()
|
||||
}
|
||||
@@ -1225,7 +1221,13 @@ impl OpenCADStudio {
|
||||
}
|
||||
n += 1;
|
||||
};
|
||||
let pending = self.begin_undo(i, "Add named parameter", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
let _ = self.tabs[i].scene.named_parameters_mut().set(&name, "1");
|
||||
self.tabs[i].dirty = true;
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
self.refresh_properties();
|
||||
Task::none()
|
||||
}
|
||||
@@ -2540,14 +2542,29 @@ impl OpenCADStudio {
|
||||
self.refresh_properties();
|
||||
}
|
||||
CmdResult::AddSketchConstraint { kind, refs, driving_param, label } => {
|
||||
if let Err(message) = self.tabs[i]
|
||||
.scene
|
||||
.validate_sketch_constraint(kind, &refs, driving_param.as_ref())
|
||||
{
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.command_line.push_error(message);
|
||||
return Task::none();
|
||||
}
|
||||
let scope = self.tabs[i].current_sketch_scope();
|
||||
// Design doc §5.1/§9: captured before the add so the
|
||||
// constraint-set edit itself (not just the geometry it
|
||||
// triggers) is undoable — pushed as its own history entry
|
||||
// below, adjacent to (not merged into) the entity delta.
|
||||
let constraints_before = self.tabs[i].scene.sketch_constraint_set(scope).cloned();
|
||||
let constraints_before = self
|
||||
.tabs[i]
|
||||
.scene
|
||||
.sketch_constraint_set(scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
crate::scene::sketch_constraints::SketchConstraintSet::new(scope)
|
||||
});
|
||||
let touched: Vec<Handle> = refs.iter().map(|r| r.entity).collect();
|
||||
let pending = self.begin_undo(i, label, touched.len(), true);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_sketch_constraints_before(scope, constraints_before);
|
||||
self.tabs[i].scene.sketch_constraint_set_mut(scope).add(kind, refs, driving_param);
|
||||
let changes: Vec<(Handle, crate::scene::ChangeKind)> = touched
|
||||
.into_iter()
|
||||
@@ -2562,7 +2579,6 @@ impl OpenCADStudio {
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
self.push_sketch_constraints_history(i, label, scope, constraints_before);
|
||||
}
|
||||
CmdResult::AddCoincidentConstraint { point_a, point_b, label } => {
|
||||
let scope = self.tabs[i].current_sketch_scope();
|
||||
@@ -5466,8 +5482,7 @@ impl OpenCADStudio {
|
||||
let groups = self.clipboard_deps.groups.clone();
|
||||
self.tabs[i].scene.recreate_groups(groups, &handle_map);
|
||||
}
|
||||
// Design doc §12: a constraint entirely between pasted entities
|
||||
// follows them, matching in-drawing COPY (`Scene::copy_entities`).
|
||||
// Constraints wholly within the pasted selection follow it.
|
||||
self.tabs[i].scene.duplicate_sketch_constraints_for(&handle_map);
|
||||
// Incremental: `add_entity` already tessellated every pasted top-level
|
||||
// solid, and existing document solids are still cached — so only newly
|
||||
@@ -6432,18 +6447,9 @@ mod sketch_constraint_undo_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Design doc §5.1/§9: adding a persistent constraint must itself be
|
||||
/// undoable/redoable — not just the geometry it happens to move. This is
|
||||
/// the scoped-down §5.1(a) alternative (not the §5.1(b) "live XRecord"
|
||||
/// approach, which needs stage 4/save-load): the constraint-set edit is
|
||||
/// pushed as its own `HistorySnapshot::SketchConstraints` entry, adjacent
|
||||
/// to but not merged into the entity `Delta` the solve produced. So a
|
||||
/// compound add-and-solve costs two undo/redo presses, not one — the
|
||||
/// first press (topmost entry, pushed last) removes the constraint
|
||||
/// record while leaving the solved geometry in place; the second
|
||||
/// (the earlier `Delta` entry) reverts the geometry itself.
|
||||
/// Constraint state and solved geometry share one undo/redo transaction.
|
||||
#[test]
|
||||
fn adding_a_constraint_is_undoable_and_redoable_in_two_steps() {
|
||||
fn adding_a_constraint_is_undoable_and_redoable_atomically() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let _ = app.automation_op(r#"{"op":"new"}"#);
|
||||
let line = add_line(&mut app, 0.0, 0.0, 10.0, 3.0);
|
||||
@@ -6462,9 +6468,6 @@ mod sketch_constraint_undo_tests {
|
||||
1
|
||||
);
|
||||
|
||||
// First undo: pops the (later-pushed) SketchConstraints entry —
|
||||
// removes the constraint record but leaves the already-solved
|
||||
// geometry alone.
|
||||
app.undo_steps(1);
|
||||
assert_eq!(
|
||||
app.tabs[app.active_tab]
|
||||
@@ -6473,41 +6476,24 @@ mod sketch_constraint_undo_tests {
|
||||
.map(|s| s.constraints.len())
|
||||
.unwrap_or(0),
|
||||
0,
|
||||
"first undo should remove the constraint record"
|
||||
"undo should remove the constraint record"
|
||||
);
|
||||
assert!(line_angle_deg(&app, line).abs() < 1e-6, "first undo should not yet touch the solved geometry");
|
||||
|
||||
// Second undo: pops the Delta entry — reverts the geometry.
|
||||
app.undo_steps(1);
|
||||
assert!(
|
||||
(line_angle_deg(&app, line) - original_angle).abs() < 1e-3,
|
||||
"second undo should restore the pre-constraint angle"
|
||||
);
|
||||
|
||||
// Redo replays in the same two-step order, symmetric with undo.
|
||||
app.redo_steps(1);
|
||||
assert!(line_angle_deg(&app, line).abs() < 1e-6, "first redo should re-level the line");
|
||||
assert_eq!(
|
||||
app.tabs[app.active_tab]
|
||||
.scene
|
||||
.sketch_constraint_set(SketchScope::ModelSpace)
|
||||
.map(|s| s.constraints.len())
|
||||
.unwrap_or(0),
|
||||
0,
|
||||
"first redo should not yet restore the constraint record"
|
||||
"undo should restore the pre-constraint angle"
|
||||
);
|
||||
|
||||
app.redo_steps(1);
|
||||
assert!(line_angle_deg(&app, line).abs() < 1e-6, "redo should re-level the line");
|
||||
assert_eq!(
|
||||
app.tabs[app.active_tab].scene.sketch_constraint_set(SketchScope::ModelSpace).unwrap().constraints.len(),
|
||||
1,
|
||||
"second redo should restore the constraint record"
|
||||
"redo should restore the constraint record"
|
||||
);
|
||||
}
|
||||
|
||||
/// Design doc §6.2 (stage 8): drawing a `LINE` whose endpoint already
|
||||
/// coincides with an existing entity's point should auto-add a
|
||||
/// Coincident constraint — suppressible by holding Shift.
|
||||
/// A line endpoint landing on an existing point infers Coincident unless
|
||||
/// Shift is held.
|
||||
#[test]
|
||||
fn drawing_a_line_onto_an_existing_endpoint_infers_a_coincident_constraint() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
@@ -6556,9 +6542,7 @@ mod sketch_constraint_undo_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Design doc §6.4 (stage 11): the bounded v1 conflict resolver removes
|
||||
/// one flagged constraint per call and re-solves, and the removal itself
|
||||
/// is undoable/redoable via the same stage-9 mechanism.
|
||||
/// Conflict resolution removes one flagged constraint and is undoable.
|
||||
#[test]
|
||||
fn resolve_one_sketch_conflict_removes_a_flagged_constraint_and_is_undoable() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
@@ -6587,19 +6571,7 @@ mod sketch_constraint_undo_tests {
|
||||
let set = app.tabs[app.active_tab].scene.sketch_constraint_set(SketchScope::ModelSpace).unwrap();
|
||||
assert_eq!(set.constraints.len(), 1, "one of the two duplicates should have been removed");
|
||||
|
||||
// Undo restores it. The removal pushes at least its own
|
||||
// `SketchConstraints` entry (stage 9's mechanism); it may or may not
|
||||
// also push a geometry `Delta`, depending on whether the resolve's
|
||||
// own re-solve actually moved anything (here it shouldn't — removing
|
||||
// a duplicate Horizontal changes nothing already-horizontal) — so
|
||||
// undo one step at a time until the constraint reappears, rather
|
||||
// than asserting a fixed step count.
|
||||
for _ in 0..5 {
|
||||
if app.tabs[app.active_tab].scene.sketch_constraint_set(SketchScope::ModelSpace).unwrap().constraints.len() == 2 {
|
||||
break;
|
||||
}
|
||||
app.undo_steps(1);
|
||||
}
|
||||
app.undo_steps(1);
|
||||
let set = app.tabs[app.active_tab].scene.sketch_constraint_set(SketchScope::ModelSpace).unwrap();
|
||||
assert_eq!(set.constraints.len(), 2, "undo should restore the removed constraint");
|
||||
}
|
||||
@@ -6645,6 +6617,17 @@ mod sketch_constraint_undo_tests {
|
||||
};
|
||||
let len = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
|
||||
assert!((len - 3.0).abs() < 1e-6, "length should track the redefined parameter, got {len}");
|
||||
|
||||
app.undo_steps(1);
|
||||
assert_eq!(app.tabs[app.active_tab].scene.named_parameters().resolve("target_len"), Ok(8.0));
|
||||
let length_after_undo = match app.tabs[app.active_tab].scene.document.get_entity(line) {
|
||||
Some(acadrust::EntityType::Line(line)) => line.length(),
|
||||
other => panic!("expected a Line, got {other:?}"),
|
||||
};
|
||||
assert!((length_after_undo - 8.0).abs() < 1e-6);
|
||||
|
||||
app.redo_steps(1);
|
||||
assert_eq!(app.tabs[app.active_tab].scene.named_parameters().resolve("target_len"), Ok(3.0));
|
||||
}
|
||||
|
||||
/// End-to-end through the actual `Message` handlers a Properties-panel
|
||||
@@ -6765,11 +6748,7 @@ mod sketch_constraint_undo_tests {
|
||||
assert!(app.command_line.history.iter().any(|e| e.kind == crate::ui::command_line::EntryKind::Error));
|
||||
}
|
||||
|
||||
/// Design doc §6.1's manual Coincident UI: two picks landing on two
|
||||
/// different lines' endpoints should resolve to a real `SketchRef` pair
|
||||
/// and add a Coincident constraint — the same `AddCoincidentConstraint`
|
||||
/// → `nearest_sketch_point` → `AddSketchConstraint` path
|
||||
/// `CoincidentConstraintCommand` drives via its two `on_point` calls.
|
||||
/// Two endpoint picks resolve to a Coincident constraint.
|
||||
#[test]
|
||||
fn coincident_command_resolves_two_picked_points_into_a_constraint() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
|
||||
@@ -963,9 +963,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Constraints (persistent: added to the scope's SketchConstraintSet,
|
||||
// solved via Scene::bump_entities, re-solved on every later edit —
|
||||
// see docs/parametric_system_design.md §6.1) ──
|
||||
// ── Persistent constraints ────────────────────────────────────
|
||||
"HCONSTRAINT" | "VCONSTRAINT" | "FXCONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
|
||||
@@ -298,10 +298,12 @@ pub fn start_allowed(cmd: &str) -> bool {
|
||||
| "PERF"
|
||||
| "CUI"
|
||||
| "ALIASEDIT"
|
||||
| "OPTIONS"
|
||||
| "OP"
|
||||
| "CUILOAD"
|
||||
| "CUIIMPORT"
|
||||
// The start page already offers Options as a button, so the
|
||||
// command that opens the same dialog belongs here too.
|
||||
| "OPTIONS"
|
||||
| "OP"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -240,30 +240,23 @@ impl OpenCADStudio {
|
||||
return Some(Task::done(Message::AliasEditorOpen));
|
||||
}
|
||||
|
||||
// ── OPTIONS — app preferences dialog ────────────────────────────
|
||||
// Registered for autocomplete (`commands/mod.rs`'s "one-shot
|
||||
// commands" list) but had no actual dispatch arm anywhere — the
|
||||
// Start-tab welcome screen's "Options" button emits
|
||||
// `Message::OptionsOpen` directly and was the only way to reach
|
||||
// it. Found while live-testing the new UI-scale setting (nothing
|
||||
// to do with that feature itself, just discovered along the
|
||||
// way). "OP" is AutoCAD's own short alias for this command.
|
||||
// ── CUI — keyboard shortcut / key-binding editor ───────────────
|
||||
"CUI" => {
|
||||
return Some(Task::done(Message::ShortcutsPanelOpen));
|
||||
}
|
||||
|
||||
// ── OPTIONS / OP — the preferences dialog ──────────────────────
|
||||
// Both names were registered for autocomplete but never dispatched,
|
||||
// so typing either one answered "Unknown command" and the dialog
|
||||
// could only be reached from the button on the start page.
|
||||
"OPTIONS" | "OP" => {
|
||||
return Some(Task::done(Message::OptionsOpen));
|
||||
}
|
||||
|
||||
// ── PARAMETERS — named-parameter/expression table editor ───────
|
||||
// `docs/named_parameters_design.md` stage 4. AutoCAD's own
|
||||
// command name for the equivalent feature (Parameters Manager).
|
||||
"PARAMETERS" => {
|
||||
return Some(Task::done(Message::NamedParametersOpen));
|
||||
}
|
||||
|
||||
// ── CUI — keyboard shortcut / key-binding editor ───────────────
|
||||
"CUI" => {
|
||||
return Some(Task::done(Message::ShortcutsPanelOpen));
|
||||
}
|
||||
|
||||
// CUIEXPORT <path> — write the keyboard-shortcut customizations
|
||||
// (the drawing-independent CUI data) to a plain "KEY COMMAND" file.
|
||||
"CUIEXPORT" => {
|
||||
|
||||
+1
-16
@@ -435,25 +435,10 @@ pub struct StartConfig {
|
||||
pub section: super::StartSection,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct RibbonConfig {
|
||||
pub collapse: CollapseMode,
|
||||
/// Font size (px) of the caption under a large ribbon button's icon.
|
||||
pub label_font_size: i32,
|
||||
/// Font size (px) of a panel's own title ("Draw", "Modify", "Layers", …)
|
||||
/// shown beneath its tools.
|
||||
pub group_title_font_size: i32,
|
||||
}
|
||||
|
||||
impl Default for RibbonConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
collapse: CollapseMode::default(),
|
||||
label_font_size: 10,
|
||||
group_title_font_size: 9,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
|
||||
+13
-43
@@ -661,7 +661,6 @@ pub(super) enum HistorySnapshot {
|
||||
// which one it actually holds.
|
||||
Delta(Box<DeltaSnapshot>),
|
||||
ObjectVisibility(ObjectVisibilitySnapshot),
|
||||
SketchConstraints(SketchConstraintsSnapshot),
|
||||
}
|
||||
|
||||
impl HistorySnapshot {
|
||||
@@ -669,7 +668,6 @@ impl HistorySnapshot {
|
||||
match self {
|
||||
HistorySnapshot::Delta(d) => &d.label,
|
||||
HistorySnapshot::ObjectVisibility(v) => &v.label,
|
||||
HistorySnapshot::SketchConstraints(s) => &s.label,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,7 +697,12 @@ impl HistorySnapshot {
|
||||
.iter()
|
||||
.map(|entry| entry.before.constraints.len().saturating_add(entry.after.constraints.len()))
|
||||
.sum::<usize>()
|
||||
.saturating_mul(96), // matches HistorySnapshot::SketchConstraints's own rough per-constraint estimate
|
||||
.saturating_mul(96),
|
||||
)
|
||||
.saturating_add(
|
||||
d.named_parameters
|
||||
.as_ref()
|
||||
.map_or(0, |(before, after)| before.len().saturating_add(after.len()).saturating_mul(128)),
|
||||
)
|
||||
.saturating_add(d.label.len()),
|
||||
HistorySnapshot::ObjectVisibility(v) => v
|
||||
@@ -723,40 +726,10 @@ impl HistorySnapshot {
|
||||
.saturating_add(v.selected_after.len())
|
||||
.saturating_mul(16)
|
||||
.saturating_add(v.label.len()),
|
||||
HistorySnapshot::SketchConstraints(s) => s
|
||||
.before
|
||||
.as_ref()
|
||||
.map_or(0, |set| set.constraints.len())
|
||||
.saturating_add(s.after.as_ref().map_or(0, |set| set.constraints.len()))
|
||||
.saturating_mul(96) // rough per-constraint size (a handful of refs + an id/kind/f64)
|
||||
.saturating_add(s.label.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Undo/redo image for one persistent-constraint edit (design doc §5.1/§9):
|
||||
/// a `SketchConstraintSet` add/remove, captured as a whole-set before/after
|
||||
/// pair rather than a per-constraint diff — sketches are small, so cloning
|
||||
/// the whole set is cheap, and it sidesteps needing a separate "undo one
|
||||
/// constraint add" vs. "undo one constraint remove" representation. Kept as
|
||||
/// its own `HistorySnapshot` variant (not folded into `DeltaSnapshot`) since
|
||||
/// `SketchConstraintSet` lives on `Scene`, not in the entity/object store
|
||||
/// `DeltaSnapshot` already covers — see the design doc's §5.1 discussion of
|
||||
/// why this needs its own path. A geometry-changing constraint add (the
|
||||
/// common case) pushes this *alongside* its own `DeltaSnapshot` entry rather
|
||||
/// than merged into one atomic step — two adjacent undo presses instead of
|
||||
/// one, a deliberate scope-down from the design doc's recommended "live
|
||||
/// XRecord" approach (§5.1(b)), which needs stage 4 (save/load) wired up
|
||||
/// first; this variant needs nothing beyond what already exists.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct SketchConstraintsSnapshot {
|
||||
pub(super) scope: crate::scene::sketch_constraints::SketchScope,
|
||||
/// `None` when the scope had no constraint set at all yet.
|
||||
pub(super) before: Option<crate::scene::sketch_constraints::SketchConstraintSet>,
|
||||
pub(super) after: Option<crate::scene::sketch_constraints::SketchConstraintSet>,
|
||||
pub(super) label: String,
|
||||
}
|
||||
|
||||
/// Symmetric undo/redo image for session-only object visibility. It contains
|
||||
/// no document entity data and therefore can never make isolation serializable.
|
||||
#[derive(Clone)]
|
||||
@@ -793,6 +766,11 @@ pub(super) struct DeltaSnapshot {
|
||||
/// `document.objects`, so it needs its own channel here rather than
|
||||
/// riding along with `entities`/`structure`. Almost always empty.
|
||||
pub(super) sketch_constraints: Vec<SketchConstraintsEntryDelta>,
|
||||
/// Drawing-wide parameter table changed by this transaction.
|
||||
pub(super) named_parameters: Option<(
|
||||
crate::scene::named_parameters::ParameterTable,
|
||||
crate::scene::named_parameters::ParameterTable,
|
||||
)>,
|
||||
pub(super) label: String,
|
||||
}
|
||||
|
||||
@@ -853,16 +831,8 @@ pub(super) struct ObjectEntryDelta {
|
||||
pub(super) after: Option<acadrust::objects::ObjectType>,
|
||||
}
|
||||
|
||||
/// One sketch-constraint scope's before/after image within an entity
|
||||
/// `DeltaSnapshot` — e.g. ERASE removing an entity's constraints along with
|
||||
/// it (`Scene::refresh_sketch_constraints`'s deletion policy). Distinct from
|
||||
/// [`SketchConstraintsSnapshot`]: that type is its own separate
|
||||
/// `HistorySnapshot` entry for a dedicated constraint add/remove command
|
||||
/// (still a deliberate two-step undo, design doc's own scope-down); this one
|
||||
/// rides inside the *same* delta as the entity change that caused it, so one
|
||||
/// undo restores both together. `before`/`after` are always `Some` scope
|
||||
/// state (never absent) — a scope's `Vec` entry, once created, is never
|
||||
/// removed, so there is no "scope didn't exist" case to represent here.
|
||||
/// One sketch-constraint scope's before/after image within an entity delta.
|
||||
/// This keeps constraint cleanup atomic with edits such as entity erasure.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct SketchConstraintsEntryDelta {
|
||||
pub(super) scope: crate::scene::sketch_constraints::SketchScope,
|
||||
|
||||
+19
-52
@@ -1,12 +1,11 @@
|
||||
use super::{
|
||||
document::{
|
||||
DeltaSnapshot, HistorySnapshot, ObjectEntryDelta, ObjectVisibilitySnapshot,
|
||||
PendingHistorySnapshot, SketchConstraintsEntryDelta, SketchConstraintsSnapshot,
|
||||
StructureSnapshot, TableEntryDelta,
|
||||
PendingHistorySnapshot, SketchConstraintsEntryDelta, StructureSnapshot, TableEntryDelta,
|
||||
},
|
||||
OpenCADStudio,
|
||||
};
|
||||
use crate::scene::{ChangeKind, ObjectIsolationState};
|
||||
use crate::scene::ObjectIsolationState;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use rustc_hash::{FxHashMap, FxHashSet as HashSet};
|
||||
use std::sync::Arc;
|
||||
@@ -185,40 +184,6 @@ impl OpenCADStudio {
|
||||
);
|
||||
}
|
||||
|
||||
/// Design doc §5.1/§9: makes a persistent-constraint add/remove
|
||||
/// undoable. `before` is the scope's constraint set captured *before*
|
||||
/// the caller's edit (by the caller, since this fn only sees "now");
|
||||
/// `after` is read fresh here, so call this once the edit is done.
|
||||
pub(super) fn push_sketch_constraints_history(
|
||||
&mut self,
|
||||
i: usize,
|
||||
label: impl Into<String>,
|
||||
scope: crate::scene::sketch_constraints::SketchScope,
|
||||
before: Option<crate::scene::sketch_constraints::SketchConstraintSet>,
|
||||
) {
|
||||
self.finish_pending_history(i);
|
||||
let after = self.tabs[i].scene.sketch_constraint_set(scope).cloned();
|
||||
self.push_undo_entry(
|
||||
i,
|
||||
HistorySnapshot::SketchConstraints(SketchConstraintsSnapshot { scope, before, after, label: label.into() }),
|
||||
);
|
||||
}
|
||||
|
||||
fn apply_sketch_constraints_state(&mut self, i: usize, snapshot: &SketchConstraintsSnapshot, undo: bool) {
|
||||
let state = if undo { snapshot.before.clone() } else { snapshot.after.clone() };
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
let set = state.unwrap_or_else(|| crate::scene::sketch_constraints::SketchConstraintSet::new(snapshot.scope));
|
||||
// The restored set's geometry may be stale (e.g. redo re-adds a
|
||||
// constraint whose entities moved via other edits since) — resolve
|
||||
// every entity it references, same as a fresh add would.
|
||||
let touched: Vec<Handle> = set.constraints.iter().flat_map(|c| c.refs.iter().map(|r| r.entity)).collect();
|
||||
*scene.sketch_constraint_set_mut(snapshot.scope) = set;
|
||||
if !touched.is_empty() {
|
||||
let changes: Vec<(Handle, ChangeKind)> = touched.into_iter().map(|h| (h, ChangeKind::Modified)).collect();
|
||||
scene.bump_entities(&changes);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn push_entity_group_history(
|
||||
&mut self,
|
||||
i: usize,
|
||||
@@ -261,6 +226,7 @@ impl OpenCADStudio {
|
||||
active_layer: None,
|
||||
structure: (!objects.is_empty()).then_some(StructureSnapshot::Objects(objects)),
|
||||
sketch_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: label.into(),
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
|
||||
@@ -371,6 +337,7 @@ impl OpenCADStudio {
|
||||
.then_some((pending.active_layer, active_layer_after)),
|
||||
structure: structure_changed.then_some(StructureSnapshot::Full(pending.structure_before)),
|
||||
sketch_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
|
||||
@@ -479,6 +446,7 @@ impl OpenCADStudio {
|
||||
active_layer: None,
|
||||
structure: Some(StructureSnapshot::Layers(entries)),
|
||||
sketch_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
|
||||
@@ -535,6 +503,7 @@ impl OpenCADStudio {
|
||||
active_layer: None,
|
||||
structure: Some(StructureSnapshot::TextStyles(entries)),
|
||||
sketch_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
|
||||
@@ -591,6 +560,7 @@ impl OpenCADStudio {
|
||||
active_layer: None,
|
||||
structure: Some(StructureSnapshot::DimStyles(entries)),
|
||||
sketch_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
|
||||
@@ -655,6 +625,7 @@ impl OpenCADStudio {
|
||||
active_layer: None,
|
||||
structure: Some(StructureSnapshot::Objects(entries)),
|
||||
sketch_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
|
||||
@@ -689,6 +660,7 @@ impl OpenCADStudio {
|
||||
object_handles,
|
||||
}),
|
||||
sketch_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: "STYLE".to_string(),
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
|
||||
@@ -769,7 +741,8 @@ impl OpenCADStudio {
|
||||
pending.label
|
||||
);
|
||||
}
|
||||
let (entity_before, object_before, sketch_constraints_before) = rec.into_recorded_images();
|
||||
let (entity_before, object_before, sketch_constraints_before, named_parameters_before) =
|
||||
rec.into_recorded_images();
|
||||
let entities: Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)> = entity_before
|
||||
.into_iter()
|
||||
.map(|(h, before)| {
|
||||
@@ -803,6 +776,10 @@ impl OpenCADStudio {
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let named_parameters = named_parameters_before.and_then(|before| {
|
||||
let after = self.tabs[i].scene.named_parameters().clone();
|
||||
(before != after).then_some((before, after))
|
||||
});
|
||||
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let dirty_after = self.tabs[i].dirty;
|
||||
let mut structure = pending.structure_before.map(StructureSnapshot::Full);
|
||||
@@ -839,6 +816,7 @@ impl OpenCADStudio {
|
||||
active_layer: None,
|
||||
structure,
|
||||
sketch_constraints,
|
||||
named_parameters,
|
||||
label: pending.label,
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
|
||||
@@ -971,6 +949,9 @@ impl OpenCADStudio {
|
||||
let value = if undo { &entry.before } else { &entry.after };
|
||||
*scene.sketch_constraint_set_mut(entry.scope) = value.clone();
|
||||
}
|
||||
if let Some((before, after)) = &d.named_parameters {
|
||||
scene.named_parameters = if undo { before } else { after }.clone();
|
||||
}
|
||||
let (sel, dirty) = if undo {
|
||||
(&d.selected_before, d.dirty_before)
|
||||
} else {
|
||||
@@ -1168,13 +1149,6 @@ impl OpenCADStudio {
|
||||
.redo_stack
|
||||
.push(HistorySnapshot::ObjectVisibility(v));
|
||||
}
|
||||
HistorySnapshot::SketchConstraints(s) => {
|
||||
self.apply_sketch_constraints_state(i, &s, true);
|
||||
self.tabs[i]
|
||||
.history
|
||||
.redo_stack
|
||||
.push(HistorySnapshot::SketchConstraints(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.finish_history_apply(i, had_full, layer_panel_changed, &changes);
|
||||
@@ -1230,13 +1204,6 @@ impl OpenCADStudio {
|
||||
.undo_stack
|
||||
.push(HistorySnapshot::ObjectVisibility(v));
|
||||
}
|
||||
HistorySnapshot::SketchConstraints(s) => {
|
||||
self.apply_sketch_constraints_state(i, &s, false);
|
||||
self.tabs[i]
|
||||
.history
|
||||
.undo_stack
|
||||
.push(HistorySnapshot::SketchConstraints(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.finish_history_apply(i, had_full, layer_panel_changed, &changes);
|
||||
|
||||
+11
-46
@@ -524,12 +524,6 @@ pub(super) struct OpenCADStudio {
|
||||
snap_angle_input: String,
|
||||
/// Model-space lineweight preview scale, in percent (25..=200).
|
||||
lineweight_display_scale: i32,
|
||||
/// Whole-interface scale, in percent (50..=200) — drives iced's own
|
||||
/// per-window `scale_factor` (`run`'s `.scale_factor(...)` hook), so it
|
||||
/// resizes every panel, dialog, the ribbon, and their text uniformly
|
||||
/// rather than needing a separate font-size knob threaded through each
|
||||
/// panel's own view code.
|
||||
ui_scale: i32,
|
||||
/// Isometric drafting state and active axis pair.
|
||||
isometric_drafting: bool,
|
||||
iso_plane: settings::IsoPlane,
|
||||
@@ -553,25 +547,13 @@ pub(super) struct OpenCADStudio {
|
||||
/// When true (default), the app registers itself as a .dwg/.dxf/.bak file
|
||||
/// handler on each launch. Toggle with the FILEASSOC command.
|
||||
pub file_assoc_enabled: bool,
|
||||
/// When true, saving a drawing with sketch constraints also materializes
|
||||
/// them as AutoCAD's own native `AssocNetwork`/`Assoc2dConstraintGroup`
|
||||
/// object graph (`scene::dwg_native_constraints`), so AutoCAD/BricsCAD
|
||||
/// recognize them as real constraints rather than an opaque OCS-only
|
||||
/// XRecord. Off by default (#opt-in — adds objects/file weight every
|
||||
/// user pays for save-after-save, whether or not they need AutoCAD
|
||||
/// interop). A drawing that already carries this native graph (from a
|
||||
/// prior save with this on, or from real AutoCAD) keeps it synced on
|
||||
/// every save regardless of this setting — see
|
||||
/// `Scene::materialize_dwg_native_constraints_for_save`'s doc comment;
|
||||
/// this only governs whether the graph is *created* in a scope that
|
||||
/// doesn't have one yet.
|
||||
/// When true, saving creates native constraint objects alongside the
|
||||
/// application's own persistence record. Existing native objects remain
|
||||
/// synchronized regardless of this setting.
|
||||
pub write_dwg_native_constraints: bool,
|
||||
/// When true (default), a sketch constraint's viewport pill shows its
|
||||
/// glyph plus a driven value or named-parameter name (e.g. "⌀ 5.000" or
|
||||
/// "⌀ hole_dia"). When false, every pill shows just the bare glyph — the
|
||||
/// value/name text can cover important canvas detail on a dense sketch,
|
||||
/// and the glyph alone is already enough to see *that* a constraint is
|
||||
/// present (`docs/parametric_system_design.md` §6.3/§7).
|
||||
/// glyph plus a driven value or named-parameter name. When false, every
|
||||
/// pill shows only the glyph.
|
||||
pub show_constraint_values: bool,
|
||||
/// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command);
|
||||
/// 0 disables autosave.
|
||||
@@ -1066,7 +1048,7 @@ pub(super) struct OpenCADStudio {
|
||||
/// edited. Seeded from `command_aliases` on open, committed back on close.
|
||||
alias_editor_rows: Vec<(String, String)>,
|
||||
|
||||
// ── Named Parameters (docs/named_parameters_design.md, stage 4) ────────
|
||||
// ── Named Parameters ──────────────────────────────────────────────────
|
||||
/// Working buffer for the PARAMETERS modal. Unlike `alias_editor_rows`,
|
||||
/// this isn't a copy of a separate app-level store — the real state
|
||||
/// lives per-document at `Scene::named_parameters`; this buffer is
|
||||
@@ -1999,12 +1981,6 @@ pub enum Message {
|
||||
CursorTypeChanged(settings::CursorType),
|
||||
/// Set the model-space lineweight preview scale from Options.
|
||||
LineweightDisplayScaleChanged(i32),
|
||||
/// Set the whole-interface scale (`ui_scale`) from Options.
|
||||
UiScaleChanged(i32),
|
||||
/// Set the ribbon's large-button icon-label font size from Options.
|
||||
RibbonLabelFontSizeChanged(i32),
|
||||
/// Set the ribbon's panel-title ("Draw", "Modify", …) font size from Options.
|
||||
RibbonGroupTitleFontSizeChanged(i32),
|
||||
/// Edit the optional crosshair RGB value; blank restores automatic contrast.
|
||||
CrosshairColorChanged(String),
|
||||
/// Set the default type/version used when first saving a new drawing.
|
||||
@@ -2113,8 +2089,7 @@ pub enum Message {
|
||||
/// Register or unregister as the .dwg/.dxf handler, from Options. Same
|
||||
/// setting the FILEASSOC command carries.
|
||||
FileAssocChanged(bool),
|
||||
/// Toggle writing AutoCAD-native constraint objects on save, from
|
||||
/// Options. See `write_dwg_native_constraints`'s doc comment.
|
||||
/// Toggle writing native constraint objects on save.
|
||||
WriteDwgNativeConstraintsChanged(bool),
|
||||
/// Toggle showing driven values/named-parameter names on constraint
|
||||
/// pills, from Options. See `show_constraint_values`'s doc comment.
|
||||
@@ -2542,12 +2517,8 @@ pub enum Message {
|
||||
CloseLayoutList,
|
||||
/// Cycle the coordinate readout mode ($COORDS): static → live → polar.
|
||||
CycleCoordsMode,
|
||||
/// Design doc §6.4, stage 11: removes one flagged redundant/conflicting
|
||||
/// constraint from the current sketch scope (a bounded v1 of the
|
||||
/// "SketchXpert-style" resolver — one guided removal per click rather
|
||||
/// than a candidate-list panel with a cyclable preview; see the status
|
||||
/// bar's `ResolveOneSketchConflict` pill and
|
||||
/// `OpenCADStudio::resolve_one_sketch_conflict` for the full rationale).
|
||||
/// Removes one flagged redundant or conflicting constraint from the
|
||||
/// current sketch scope.
|
||||
/// No-op if the scope currently has no flagged conflict.
|
||||
ResolveOneSketchConflict,
|
||||
/// Toggle the status-bar customization menu open/closed.
|
||||
@@ -2863,9 +2834,7 @@ pub enum Message {
|
||||
/// and re-solve every constraint that reads a named parameter; stays
|
||||
/// open.
|
||||
NamedParametersApply,
|
||||
// ── Parameters / Constraints sections embedded in the Properties panel
|
||||
// (AutoCAD-style: a live section instead of the modal above, per-row
|
||||
// commit-on-submit instead of a whole-table Apply) ─────────────────────
|
||||
// ── Parameters / Constraints sections embedded in Properties ──────────
|
||||
/// Live text of one column of parameter row `index`, keyed by its
|
||||
/// `ParameterTable::iter()` position — see `PropValue::ParamRow`.
|
||||
PropParamInput {
|
||||
@@ -2880,8 +2849,7 @@ pub enum Message {
|
||||
index: usize,
|
||||
field: crate::ui::window::named_parameters::ParamField,
|
||||
},
|
||||
/// Remove parameter row `index` immediately (no confirmation, matching
|
||||
/// AutoCAD's own Parameters Manager delete button).
|
||||
/// Remove parameter row `index` immediately.
|
||||
PropParamDelete(usize),
|
||||
/// Append a fresh, uniquely-named parameter to `Scene::named_parameters`.
|
||||
PropParamAddNew,
|
||||
@@ -3566,7 +3534,6 @@ impl OpenCADStudio {
|
||||
isolines_awaiting_regen: false,
|
||||
snap_angle_input: "0".to_string(),
|
||||
lineweight_display_scale: 100,
|
||||
ui_scale: 100,
|
||||
isometric_drafting: false,
|
||||
iso_plane: settings::IsoPlane::Left,
|
||||
snap_angle_deg: 0.0,
|
||||
@@ -4180,7 +4147,6 @@ pub fn run() -> iced::Result {
|
||||
..iced::Settings::default()
|
||||
})
|
||||
.subscription(OpenCADStudio::subscription)
|
||||
.scale_factor(|state: &OpenCADStudio, _window: window::Id| state.ui_scale as f32 / 100.0)
|
||||
.title(|state: &OpenCADStudio, window_id: window::Id| {
|
||||
let _ = window_id; // all dialogs are in-canvas modals now
|
||||
if let Some(tab) = state.tabs.get(state.active_tab) {
|
||||
@@ -4225,7 +4191,6 @@ pub fn run_web() -> iced::Result {
|
||||
OpenCADStudio::view_main,
|
||||
)
|
||||
.subscription(OpenCADStudio::subscription)
|
||||
.scale_factor(|state: &OpenCADStudio| state.ui_scale as f32 / 100.0)
|
||||
.title(|_state: &OpenCADStudio| {
|
||||
concat!("Open CAD Studio ", env!("OCS_APP_VERSION")).to_string()
|
||||
})
|
||||
|
||||
@@ -406,8 +406,7 @@ impl OpenCADStudio {
|
||||
read_only(t!("Visual Style").as_ref(), tab.visual_style.clone()),
|
||||
],
|
||||
},
|
||||
// Document-wide named-parameter table (AutoCAD
|
||||
// Parameters Manager equivalent), embedded here
|
||||
// Document-wide named-parameter table embedded here
|
||||
// instead of a separate modal — see `PropValue::
|
||||
// ParamRow`'s doc comment. Belongs on the no-
|
||||
// selection (drawing-level) page, not a per-entity
|
||||
|
||||
+2
-16
@@ -221,14 +221,6 @@ pub struct UserSettings {
|
||||
pub crosshair_color: Option<[u8; 3]>,
|
||||
/// Model-space lineweight preview scale as a percentage.
|
||||
pub lineweight_display_scale: i32,
|
||||
/// Whole-interface scale, as a percentage — every panel, dialog, the
|
||||
/// ribbon, and their text, not just one widget's font size. Wired
|
||||
/// straight into iced's own per-window `scale_factor` hook
|
||||
/// (`OpenCADStudio::run`), so a change takes effect immediately with no
|
||||
/// per-panel plumbing needed. Relies on the struct-level `#[serde(default)]`
|
||||
/// (via `impl Default` below) to backfill this field for a config file
|
||||
/// saved before this setting existed, same as every other field here.
|
||||
pub ui_scale: i32,
|
||||
/// Isometric drafting changes the grid and crosshair to the active axis pair.
|
||||
pub isometric_drafting: bool,
|
||||
pub iso_plane: IsoPlane,
|
||||
@@ -284,13 +276,8 @@ pub struct UserSettings {
|
||||
/// When true (default), the app (re)registers itself as a .dwg/.dxf/.bak
|
||||
/// handler on every launch. Toggle with the FILEASSOC command.
|
||||
pub file_assoc_enabled: bool,
|
||||
/// When true, saving a drawing also writes its sketch constraints as
|
||||
/// AutoCAD's own native object graph (`scene::dwg_native_constraints`),
|
||||
/// not just the app's own XRecord format. Off by default — opt-in, since
|
||||
/// it adds file weight every save whether or not AutoCAD/BricsCAD
|
||||
/// interop is needed. See `write_dwg_native_constraints` on the app
|
||||
/// struct for the full contract (a drawing that already has this native
|
||||
/// graph keeps it synced regardless of this setting).
|
||||
/// When true, saving also writes sketch constraints as native drawing
|
||||
/// objects alongside the application's own persistence record.
|
||||
#[serde(default)]
|
||||
pub write_dwg_native_constraints: bool,
|
||||
/// When true (default), a sketch constraint's viewport pill shows its
|
||||
@@ -402,7 +389,6 @@ impl Default for UserSettings {
|
||||
cursor_type: CursorType::Crosshair,
|
||||
crosshair_color: None,
|
||||
lineweight_display_scale: 100,
|
||||
ui_scale: 100,
|
||||
isometric_drafting: false,
|
||||
iso_plane: IsoPlane::Left,
|
||||
snap_angle_deg: 0.0,
|
||||
|
||||
+3
-14
@@ -407,7 +407,6 @@ impl OpenCADStudio {
|
||||
cursor_type: self.cursor_type,
|
||||
crosshair_color: self.crosshair_color,
|
||||
lineweight_display_scale: self.lineweight_display_scale,
|
||||
ui_scale: self.ui_scale,
|
||||
isometric_drafting: self.isometric_drafting,
|
||||
iso_plane: self.iso_plane,
|
||||
snap_angle_deg: self.snap_angle_deg,
|
||||
@@ -477,7 +476,6 @@ impl OpenCADStudio {
|
||||
.map(crate::app::config::rgb_to_hex)
|
||||
.unwrap_or_default();
|
||||
self.lineweight_display_scale = s.lineweight_display_scale.clamp(25, 200);
|
||||
self.ui_scale = s.ui_scale.clamp(50, 200);
|
||||
self.isometric_drafting = s.isometric_drafting;
|
||||
self.iso_plane = s.iso_plane;
|
||||
self.snap_angle_deg = if s.snap_angle_deg.is_finite() {
|
||||
@@ -836,8 +834,6 @@ impl OpenCADStudio {
|
||||
annotation_auto_scale: self.annotation_auto_scale,
|
||||
ribbon: crate::app::config::RibbonConfig {
|
||||
collapse: self.ribbon.collapse_mode(),
|
||||
label_font_size: self.ribbon.label_font_size(),
|
||||
group_title_font_size: self.ribbon.group_title_font_size(),
|
||||
},
|
||||
plot: self.plot_dialog.clone(),
|
||||
shortcuts: crate::app::config::ShortcutConfig {
|
||||
@@ -895,9 +891,6 @@ impl OpenCADStudio {
|
||||
self.dock = dock;
|
||||
self.annotation_auto_scale = cfg.annotation_auto_scale.clamp(-4, 4);
|
||||
self.ribbon.set_collapse_mode(cfg.ribbon.collapse);
|
||||
self.ribbon.set_label_font_size(cfg.ribbon.label_font_size);
|
||||
self.ribbon
|
||||
.set_group_title_font_size(cfg.ribbon.group_title_font_size);
|
||||
self.plot_dialog = cfg.plot;
|
||||
self.shortcut_bindings = cfg.shortcuts.bindings.into_iter().collect();
|
||||
self.shortcut_bindings
|
||||
@@ -1449,8 +1442,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
self.tabs[i].scene.material_base_dir =
|
||||
path.parent().map(std::path::Path::to_path_buf);
|
||||
self.tabs[i].scene.document = doc;
|
||||
// Design doc §8 stage 4: read back any persisted sketch
|
||||
// constraint sets right after the document is installed.
|
||||
// Load persisted constraints after installing the document.
|
||||
self.tabs[i].scene.load_sketch_constraints_from_document();
|
||||
// named_parameters_design.md stage 2: same load-time hook,
|
||||
// for the document-wide parameter table.
|
||||
@@ -1718,15 +1710,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
self.tabs[i].scene.document.header.user_real1 =
|
||||
self.tabs[i].scene.annotation_scale as f64;
|
||||
self.sync_solid_models_for_save(i);
|
||||
// Design doc §8 stage 4: write any sketch constraint sets into the
|
||||
// document right before it's serialized.
|
||||
// Materialize constraints immediately before serialization.
|
||||
self.tabs[i].scene.materialize_sketch_constraints_for_save();
|
||||
// named_parameters_design.md stage 2: same save-time hook, for the
|
||||
// document-wide parameter table.
|
||||
self.tabs[i].scene.materialize_named_parameters_for_save();
|
||||
// docs/dwg_constraint_compatibility_design.md: additive DWG/DXF-
|
||||
// native constraint graph, alongside (not instead of) the XRecord
|
||||
// above — same save-time hook.
|
||||
// Keep the optional native constraint graph synchronized on save.
|
||||
self.tabs[i].scene.materialize_dwg_native_constraints_for_save(self.write_dwg_native_constraints);
|
||||
}
|
||||
|
||||
|
||||
@@ -6040,24 +6040,6 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::UiScaleChanged(value) => {
|
||||
self.ui_scale = value.clamp(50, 200);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::RibbonLabelFontSizeChanged(value) => {
|
||||
self.ribbon.set_label_font_size(value);
|
||||
self.save_config();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::RibbonGroupTitleFontSizeChanged(value) => {
|
||||
self.ribbon.set_group_title_font_size(value);
|
||||
self.save_config();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::CrosshairColorChanged(value) => {
|
||||
self.crosshair_color_input = value.clone();
|
||||
if value.trim().is_empty() {
|
||||
|
||||
@@ -1706,16 +1706,8 @@ impl OpenCADStudio {
|
||||
);
|
||||
}
|
||||
}
|
||||
// Live sketch-constraint re-solve (design doc §7 open question
|
||||
// 6): previously constrained neighbors only snapped into place
|
||||
// on grip release — this runs the same rebuild-and-solve every
|
||||
// frame so they track the drag live. First touch this gesture
|
||||
// captures the neighbor's original state into the grip's own
|
||||
// undo tracking (mirroring how `edited_handles` itself is
|
||||
// captured above) and hides it from the resident tessellation;
|
||||
// every frame folds it into this frame's `edited_handles` so the
|
||||
// existing mesh/hatch/wire-preview refresh below already covers
|
||||
// it for free.
|
||||
// Re-solve constrained neighbors on each drag frame and include
|
||||
// their original state in the gesture's undo record.
|
||||
let solved_by_constraints = self.tabs[i].scene.solve_sketch_constraints_preview(&edited_handles);
|
||||
for (handle, _) in &solved_by_constraints {
|
||||
let handle = *handle;
|
||||
|
||||
+1
-3
@@ -805,9 +805,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
let point_cursor = tab.active_cmd.as_ref().is_some_and(|cmd| {
|
||||
!cmd.needs_entity_pick() && !cmd.is_selection_gathering()
|
||||
});
|
||||
// Persistent sketch-constraint glyphs (design doc §6.3/§7) — model
|
||||
// space only, like the constraints themselves; paper space shows
|
||||
// none (out of scope per the design doc §3.1).
|
||||
// Constraint glyphs are displayed in model space only.
|
||||
let constraint_glyphs: Vec<(iced::Point, String, bool)> = if is_paper {
|
||||
Vec::new()
|
||||
} else {
|
||||
|
||||
@@ -265,9 +265,6 @@ impl OpenCADStudio {
|
||||
self.crosshair_color,
|
||||
&self.crosshair_color_input,
|
||||
self.lineweight_display_scale,
|
||||
self.ui_scale,
|
||||
self.ribbon.label_font_size(),
|
||||
self.ribbon.group_title_font_size(),
|
||||
&self.model_space,
|
||||
&self.model_bg_input,
|
||||
&self.paper_bg_input,
|
||||
|
||||
Reference in new issue
Block a user