Merge pull request #1284 from ramox81/codex/autoconstrain-parity

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/app/view/mod.rs
#	src/ui/overlay.rs
This commit is contained in:
Hakan Seven committed 2026-09-15 12:38:41 +03:00
commit c9eeb6f66e
23 files changed
+1461 -99

No files matched your search

+179 -12
View File
@@ -1617,6 +1617,92 @@ impl OpenCADStudio {
Task::none() Task::none()
} }
fn apply_continuous_constraints(&mut self, i: usize, new_handles: &[acadrust::Handle]) {
let supported_creation = self.tabs[i]
.active_cmd
.as_ref()
.is_some_and(|command| {
matches!(
command.name(),
"LINE"
| "PLINE"
| "POLYLINE"
| "RECTANG"
| "RECTANGLE"
| "POLYGON"
| "CIRCLE"
| "ARC"
)
});
if supported_creation {
self.apply_inferred_constraints(i, new_handles);
}
}
pub(in crate::app) fn apply_inferred_constraints(
&mut self,
i: usize,
changed_handles: &[acadrust::Handle],
) {
if !self.constraint_infer || changed_handles.is_empty() {
return;
}
let scope = self.tabs[i].current_parametric_scope();
let owner = scope.owner_handle(&self.tabs[i].scene.document);
let candidates = self.tabs[i]
.scene
.document
.block_records
.iter()
.find(|record| record.handle == owner)
.map(|record| record.entity_handles.clone())
.unwrap_or_default();
let inferred = self.tabs[i].scene.inferred_parametric_constraints(
scope,
&candidates,
&self.auto_constrain_settings,
);
let inferred: Vec<_> = inferred
.into_iter()
.filter(|(_, refs)| {
refs.iter()
.any(|reference| changed_handles.contains(&reference.entity))
})
.collect();
if inferred.is_empty() {
return;
}
let before = self.tabs[i]
.scene
.parametric_constraint_set(scope)
.cloned()
.unwrap_or_else(|| {
crate::scene::parametric_constraints::ParametricConstraintSet::new(scope)
});
self.tabs[i]
.scene
.record_undo_parametric_constraints_before(scope, before);
let mut touched = Vec::new();
for (kind, refs) in inferred {
touched.extend(refs.iter().map(|reference| reference.entity));
self.tabs[i]
.scene
.parametric_constraint_set_mut(scope)
.add(kind, refs, None);
}
touched.sort_unstable_by_key(|handle| handle.value());
touched.dedup();
let changes: Vec<_> = touched
.into_iter()
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities_with_parametric_policy(
&changes,
&[],
self.constraint_solve_mode,
);
}
fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task<Message> { fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task<Message> {
let i = self.active_tab; let i = self.active_tab;
if let Some(bind) = self.tabs[i] if let Some(bind) = self.tabs[i]
@@ -1638,6 +1724,15 @@ impl OpenCADStudio {
.as_ref() .as_ref()
.is_some_and(|command| command.preserve_commit_layer()); .is_some_and(|command| command.preserve_commit_layer());
match result { match result {
CmdResult::OpenAutoConstrainSettings => {
self.auto_constrain_saved = Some(self.auto_constrain_settings.clone());
self.auto_constrain_selected_row = 0;
self.auto_constrain_distance_input =
format!("{}", self.auto_constrain_settings.distance_tolerance);
self.auto_constrain_angle_input =
format!("{}", self.auto_constrain_settings.angle_tolerance_deg);
self.active_modal = Some(super::ModalKind::AutoConstrainSettings);
}
CmdResult::NeedPoint => { CmdResult::NeedPoint => {
// ATTEDIT finished its entity pick: hand the chosen block off to // ATTEDIT finished its entity pick: hand the chosen block off to
// the attribute editor dialog and end the command (open_attribute // the attribute editor dialog and end the command (open_attribute
@@ -1745,6 +1840,9 @@ impl OpenCADStudio {
.attach_dimension_association(handle, sources); .attach_dimension_association(handle, sources);
} }
} }
if let Some(handle) = committed {
self.apply_continuous_constraints(i, &[handle]);
}
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
if let Some(p) = prompt { if let Some(p) = prompt {
@@ -1785,15 +1883,18 @@ impl OpenCADStudio {
} }
self.refresh_layer_panel(); self.refresh_layer_panel();
} }
let mut committed = Vec::new();
for entity in entities { for entity in entities {
if preserve_commit_style { let handle = if preserve_commit_style {
let _ = self.commit_entity_handle_preserve_style(entity); self.commit_entity_handle_preserve_style(entity)
} else if preserve_commit_layer { } else if preserve_commit_layer {
let _ = self.commit_entity_handle_preserve_layer(entity); self.commit_entity_handle_preserve_layer(entity)
} else { } else {
self.commit_entity(entity); self.commit_entity_handle(entity)
} };
committed.extend(handle);
} }
self.apply_continuous_constraints(i, &committed);
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire(); self.tabs[i].scene.clear_preview_wire();
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
@@ -1824,15 +1925,18 @@ impl OpenCADStudio {
} }
self.refresh_layer_panel(); self.refresh_layer_panel();
} }
let mut committed = Vec::new();
for entity in entities { for entity in entities {
if preserve_commit_style { let handle = if preserve_commit_style {
let _ = self.commit_entity_handle_preserve_style(entity); self.commit_entity_handle_preserve_style(entity)
} else if preserve_commit_layer { } else if preserve_commit_layer {
let _ = self.commit_entity_handle_preserve_layer(entity); self.commit_entity_handle_preserve_layer(entity)
} else { } else {
self.commit_entity(entity); self.commit_entity_handle(entity)
} };
committed.extend(handle);
} }
self.apply_continuous_constraints(i, &committed);
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire(); self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
@@ -2088,6 +2192,7 @@ impl OpenCADStudio {
// transform_entities — always delta-safe. // transform_entities — always delta-safe.
let pending = self.begin_undo(i, label, handles.len(), true); let pending = self.begin_undo(i, label, handles.len(), true);
self.tabs[i].scene.transform_entities(&handles, &transform); self.tabs[i].scene.transform_entities(&handles, &transform);
self.apply_inferred_constraints(i, &handles);
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire(); self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
@@ -2206,6 +2311,9 @@ impl OpenCADStudio {
.attach_dimension_association(handle, sources); .attach_dimension_association(handle, sources);
} }
} }
if let Some(handle) = committed {
self.apply_continuous_constraints(i, &[handle]);
}
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire(); self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
@@ -2935,6 +3043,7 @@ impl OpenCADStudio {
self.tabs[i] self.tabs[i]
.scene .scene
.record_undo_parametric_constraints_before(scope, constraints_before); .record_undo_parametric_constraints_before(scope, constraints_before);
let retain_size = self.constraint_solve_mode && driving_param.is_none();
self.tabs[i].scene.parametric_constraint_set_mut(scope).add( self.tabs[i].scene.parametric_constraint_set_mut(scope).add(
kind, kind,
refs, refs,
@@ -2944,7 +3053,11 @@ impl OpenCADStudio {
.into_iter() .into_iter()
.map(|h| (h, crate::scene::ChangeKind::Modified)) .map(|h| (h, crate::scene::ChangeKind::Modified))
.collect(); .collect();
self.tabs[i].scene.bump_entities(&changes); self.tabs[i].scene.bump_entities_with_parametric_policy(
&changes,
&[],
retain_size,
);
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None; self.tabs[i].snap_result = None;
@@ -4492,6 +4605,7 @@ impl OpenCADStudio {
let pending = self.begin_undo(i, "STRETCH", handles.len(), !structural); let pending = self.begin_undo(i, "STRETCH", handles.len(), !structural);
let mut count = 0usize; let mut count = 0usize;
let mut changed_handles = Vec::new(); let mut changed_handles = Vec::new();
let mut driven_refs = Vec::new();
// Helper: is DXF point (x, y) inside the world-space window? // Helper: is DXF point (x, y) inside the world-space window?
// Drawing plane is world XY (= DXF XY). // Drawing plane is world XY (= DXF XY).
@@ -4510,6 +4624,54 @@ impl OpenCADStudio {
let mut stretched_dims: Vec<acadrust::Handle> = Vec::new(); let mut stretched_dims: Vec<acadrust::Handle> = Vec::new();
for handle in &handles { for handle in &handles {
let before = self.tabs[i].scene.document.get_entity_arc(*handle); let before = self.tabs[i].scene.document.get_entity_arc(*handle);
if let Some(entity) = before.as_deref() {
use crate::scene::parametric_constraints::ParametricRef;
match entity {
acadrust::EntityType::Line(line) => {
if in_win(line.start.x, line.start.y) {
driven_refs.push(ParametricRef::point(*handle, 0));
}
if in_win(line.end.x, line.end.y) {
driven_refs.push(ParametricRef::point(*handle, 1));
}
}
acadrust::EntityType::LwPolyline(polyline) => {
if let Some(world) =
crate::entities::curve::lwpolyline_world_xy(polyline)
{
for (index, vertex) in world.vertices.iter().enumerate() {
if in_win(vertex.location.x, vertex.location.y) {
driven_refs.push(ParametricRef::point(
*handle,
index as i32,
));
}
}
}
}
acadrust::EntityType::Polyline2D(polyline) => {
for (index, vertex) in polyline.vertices.iter().enumerate() {
if in_win(vertex.location.x, vertex.location.y) {
driven_refs.push(ParametricRef::point(
*handle,
index as i32,
));
}
}
}
acadrust::EntityType::Circle(circle)
if in_win(circle.center.x, circle.center.y) =>
{
driven_refs.push(ParametricRef::center(*handle));
}
acadrust::EntityType::Arc(arc)
if in_win(arc.center.x, arc.center.y) =>
{
driven_refs.push(ParametricRef::center(*handle));
}
_ => {}
}
}
let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) else { let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) else {
continue; continue;
}; };
@@ -4732,7 +4894,12 @@ impl OpenCADStudio {
.iter() .iter()
.map(|&handle| (handle, crate::scene::ChangeKind::Modified)) .map(|&handle| (handle, crate::scene::ChangeKind::Modified))
.collect(); .collect();
self.tabs[i].scene.bump_entities(&changes); self.tabs[i].scene.bump_entities_with_parametric_policy(
&changes,
&driven_refs,
self.constraint_solve_mode && !driven_refs.is_empty(),
);
self.apply_inferred_constraints(i, &changed_handles);
} }
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
+66 -35
View File
@@ -1113,55 +1113,86 @@ impl OpenCADStudio {
} }
} }
"CONSTRAINTSETTINGS" => {
self.auto_constrain_saved = Some(self.auto_constrain_settings.clone());
self.auto_constrain_selected_row = 0;
self.auto_constrain_distance_input =
format!("{}", self.auto_constrain_settings.distance_tolerance);
self.auto_constrain_angle_input =
format!("{}", self.auto_constrain_settings.angle_tolerance_deg);
self.active_modal = Some(crate::app::ModalKind::AutoConstrainSettings);
}
"AUTOCONSTRAIN" => { "AUTOCONSTRAIN" => {
let handles = self.tabs[i].scene.selected_handles_in_order(); let handles = self.tabs[i].scene.selected_handles_in_order();
if handles.is_empty() { if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand; use crate::modules::draw::select::SelectObjectsCommand;
let sel = SelectObjectsCommand::new(cmd); let sel = SelectObjectsCommand::auto_constrain(cmd);
self.command_line.push_info(&sel.prompt()); self.command_line.push_info(&sel.prompt());
self.tabs[i].active_cmd = Some(Box::new(sel)); self.tabs[i].active_cmd = Some(Box::new(sel));
} else { } else {
let scope = self.tabs[i].current_parametric_scope(); let scope = self.tabs[i].current_parametric_scope();
let inferred = self.tabs[i] let inferred = self.tabs[i]
.scene .scene
.inferred_parametric_constraints(scope, &handles); .inferred_parametric_constraints(
scope,
&handles,
&self.auto_constrain_settings,
);
if inferred.is_empty() { if inferred.is_empty() {
self.command_line self.command_line.push_output(
.push_output("No supported geometric relations were found."); format!(
return None; "0 constraint(s) applied to {} object(s).",
} handles.len()
let before = self.tabs[i]
.scene
.parametric_constraint_set(scope)
.cloned()
.unwrap_or_else(|| {
crate::scene::parametric_constraints::ParametricConstraintSet::new(
scope,
) )
}); .as_str(),
let pending = self.begin_undo(i, "Auto constrain", handles.len(), true); );
self.tabs[i] self.tabs[i].scene.deselect_all();
.scene self.refresh_selected_grips();
.record_undo_parametric_constraints_before(scope, before); } else {
let count = inferred.len(); let before = self.tabs[i]
for (kind, refs) in inferred { .scene
.parametric_constraint_set(scope)
.cloned()
.unwrap_or_else(|| {
crate::scene::parametric_constraints::ParametricConstraintSet::new(
scope,
)
});
let pending = self.begin_undo(i, "Auto constrain", handles.len(), true);
self.tabs[i] self.tabs[i]
.scene .scene
.parametric_constraint_set_mut(scope) .record_undo_parametric_constraints_before(scope, before);
.add(kind, refs, None); let count = inferred.len();
} for (kind, refs) in inferred {
let changes: Vec<_> = handles self.tabs[i]
.iter() .scene
.copied() .parametric_constraint_set_mut(scope)
.map(|handle| (handle, crate::scene::ChangeKind::Modified)) .add(kind, refs, None);
.collect(); }
self.tabs[i].scene.bump_entities(&changes); let changes: Vec<_> = handles
self.tabs[i].dirty = true; .iter()
self.refresh_properties(); .copied()
self.command_line .map(|handle| (handle, crate::scene::ChangeKind::Modified))
.push_output(format!("{} constraint(s) applied.", count).as_str()); .collect();
if let Some(pd) = pending { self.tabs[i].scene.bump_entities_with_parametric_policy(
self.commit_undo_delta(i, pd); &changes,
&[],
self.constraint_solve_mode,
);
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(
format!(
"{} constraint(s) applied to {} object(s).",
count,
handles.len()
)
.as_str(),
);
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
} }
} }
} }
+48 -1
View File
@@ -1046,6 +1046,9 @@ impl OpenCADStudio {
| "GRIPHOT" | "GRIPHOT"
| "GRIPHOVER" | "GRIPHOVER"
| "GRIPOBJLIMIT" | "GRIPOBJLIMIT"
| "CONSTRAINTSOLVEMODE"
| "CONSTRAINTINFER"
| "CONSTRAINTBARDISPLAY"
) => ) =>
{ {
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i); return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
@@ -1068,7 +1071,7 @@ impl OpenCADStudio {
let value = it.next().map(|s| s.trim().to_string()); let value = it.next().map(|s| s.trim().to_string());
if name.is_empty() || name == "?" { if name.is_empty() || name == "?" {
self.command_line.push_info( self.command_line.push_info(
crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(), crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(),
); );
} else { } else {
if name == "CETRANSPARENCY" { if name == "CETRANSPARENCY" {
@@ -1262,6 +1265,50 @@ impl OpenCADStudio {
} }
return Some(self.finish_dispatch(cmd)); return Some(self.finish_dispatch(cmd));
} }
if matches!(
name.as_str(),
"CONSTRAINTSOLVEMODE" | "CONSTRAINTINFER" | "CONSTRAINTBARDISPLAY"
) {
let current = match name.as_str() {
"CONSTRAINTSOLVEMODE" => i16::from(self.constraint_solve_mode),
"CONSTRAINTINFER" => i16::from(self.constraint_infer),
"CONSTRAINTBARDISPLAY" => self.constraint_bar_display,
_ => unreachable!(),
};
let maximum = if name == "CONSTRAINTBARDISPLAY" { 3 } else { 1 };
if let Some(value) = &value {
match value
.parse::<i16>()
.ok()
.filter(|value| (0..=maximum).contains(value))
{
Some(mode) => {
match name.as_str() {
"CONSTRAINTSOLVEMODE" => {
self.constraint_solve_mode = mode != 0
}
"CONSTRAINTINFER" => self.constraint_infer = mode != 0,
"CONSTRAINTBARDISPLAY" => {
self.constraint_bar_display = mode
}
_ => unreachable!(),
}
self.persist_settings_if_changed();
self.command_line
.push_output(&crate::tf!("{name} = {mode}"));
}
None => self.command_line.push_error(&crate::tf!(
"{name}: expected an integer from 0 to {maximum}."
)),
}
} else {
self.command_line.push_output(crate::tf!(
"Enter new value for {name} <{current}>:"
).as_ref());
self.pending_setvar = Some(name.clone());
}
return Some(self.finish_dispatch(cmd));
}
if matches!(name.as_str(), "DONUTID" | "DONUTOD") { if matches!(name.as_str(), "DONUTID" | "DONUTOD") {
let current = if name == "DONUTID" { let current = if name == "DONUTID" {
crate::modules::draw::defaults::get_donut_inner_diameter() crate::modules::draw::defaults::get_donut_inner_diameter()
+31
View File
@@ -556,6 +556,14 @@ pub(super) struct OpenCADStudio {
/// glyph plus a driven value or named-parameter name. When false, every /// glyph plus a driven value or named-parameter name. When false, every
/// pill shows only the glyph. /// pill shows only the glyph.
pub show_constraint_values: bool, pub show_constraint_values: bool,
pub auto_constrain_settings: settings::AutoConstrainSettings,
auto_constrain_saved: Option<settings::AutoConstrainSettings>,
auto_constrain_selected_row: usize,
auto_constrain_distance_input: String,
auto_constrain_angle_input: String,
pub constraint_solve_mode: bool,
pub constraint_infer: bool,
pub constraint_bar_display: i16,
/// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command); /// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command);
/// 0 disables autosave. /// 0 disables autosave.
pub savetime_min: i32, pub savetime_min: i32,
@@ -1722,6 +1730,7 @@ pub enum ModalKind {
DrawingUnits, DrawingUnits,
GeometricTolerance, GeometricTolerance,
DraftingSettings, DraftingSettings,
AutoConstrainSettings,
LayerStateEditor, LayerStateEditor,
Plot, Plot,
PrintAll, PrintAll,
@@ -2640,6 +2649,20 @@ pub enum Message {
DraftingSettingsClose, DraftingSettingsClose,
DraftingSettingsCloseDiscard, DraftingSettingsCloseDiscard,
DraftingSettingsCloseKeep, DraftingSettingsCloseKeep,
AutoConstrainSelectRow(usize),
AutoConstrainToggleKind(settings::AutoConstraintKind),
AutoConstrainMoveUp,
AutoConstrainMoveDown,
AutoConstrainSelectAll,
AutoConstrainClearAll,
AutoConstrainReset,
AutoConstrainToggleTangentPoint,
AutoConstrainTogglePerpendicularIntersection,
AutoConstrainDistanceChanged(String),
AutoConstrainAngleChanged(String),
AutoConstrainApply,
AutoConstrainOk,
AutoConstrainCancel,
/// Toggle a ribbon dropdown open/closed. /// Toggle a ribbon dropdown open/closed.
ToggleRibbonDropdown(String), ToggleRibbonDropdown(String),
/// Toggle a collapsed ribbon panel's flyout open/closed (by panel title). /// Toggle a collapsed ribbon panel's flyout open/closed (by panel title).
@@ -3633,6 +3656,14 @@ impl OpenCADStudio {
backup_on_save: true, backup_on_save: true,
file_assoc_enabled: true, file_assoc_enabled: true,
show_constraint_values: true, show_constraint_values: true,
auto_constrain_settings: settings::AutoConstrainSettings::default(),
auto_constrain_saved: None,
auto_constrain_selected_row: 0,
auto_constrain_distance_input: "0.05".to_string(),
auto_constrain_angle_input: "1".to_string(),
constraint_solve_mode: true,
constraint_infer: false,
constraint_bar_display: 3,
savetime_min: 10, savetime_min: 10,
default_bg_color: None, default_bg_color: None,
default_paper_bg_color: None, default_paper_bg_color: None,
+27 -1
View File
@@ -2813,6 +2813,25 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
} }
pub(super) fn invalidate_property_targets(&mut self, i: usize, handles: &[Handle]) { pub(super) fn invalidate_property_targets(&mut self, i: usize, handles: &[Handle]) {
self.invalidate_property_targets_with_driven(i, handles, &[]);
}
pub(super) fn invalidate_property_targets_with_driven(
&mut self,
i: usize,
handles: &[Handle],
driven_refs: &[crate::scene::parametric_constraints::ParametricRef],
) {
self.invalidate_property_targets_with_originals(i, handles, driven_refs, &[]);
}
pub(super) fn invalidate_property_targets_with_originals(
&mut self,
i: usize,
handles: &[Handle],
driven_refs: &[crate::scene::parametric_constraints::ParametricRef],
retained_originals: &[(Handle, acadrust::EntityType)],
) {
let mut context_object_changed = false; let mut context_object_changed = false;
for &handle in handles { for &handle in handles {
// A dimension is drawn from the block holding its picture, and that // A dimension is drawn from the block holding its picture, and that
@@ -2842,7 +2861,14 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
.iter() .iter()
.map(|&handle| (handle, crate::scene::ChangeKind::Modified)) .map(|&handle| (handle, crate::scene::ChangeKind::Modified))
.collect(); .collect();
self.tabs[i].scene.bump_entities(&changes); self.tabs[i]
.scene
.bump_entities_with_parametric_originals(
&changes,
driven_refs,
self.constraint_solve_mode && !driven_refs.is_empty(),
retained_originals,
);
} }
/// Apply a single-property edit to every handle in `handles`, recording the /// Apply a single-property edit to every handle in `handles`, recording the
+129
View File
@@ -11,6 +11,110 @@
use crate::snap::SnapType; use crate::snap::SnapType;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AutoConstraintKind {
Coincident,
Collinear,
Parallel,
Perpendicular,
Tangent,
Concentric,
Horizontal,
Vertical,
Equal,
}
impl AutoConstraintKind {
pub const ALL: [Self; 9] = [
Self::Coincident,
Self::Collinear,
Self::Parallel,
Self::Perpendicular,
Self::Tangent,
Self::Concentric,
Self::Horizontal,
Self::Vertical,
Self::Equal,
];
pub const fn label(self) -> &'static str {
match self {
Self::Coincident => "Coincident",
Self::Collinear => "Collinear",
Self::Parallel => "Parallel",
Self::Perpendicular => "Perpendicular",
Self::Tangent => "Tangent",
Self::Concentric => "Concentric",
Self::Horizontal => "Horizontal",
Self::Vertical => "Vertical",
Self::Equal => "Equal",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct AutoConstrainSettings {
pub priority: Vec<AutoConstraintKind>,
pub enabled: Vec<AutoConstraintKind>,
pub tangent_must_share_point: bool,
pub perpendicular_must_intersect: bool,
pub distance_tolerance: f64,
pub angle_tolerance_deg: f64,
}
impl Default for AutoConstrainSettings {
fn default() -> Self {
Self {
priority: AutoConstraintKind::ALL.to_vec(),
// Equal remains available in the priority list, but the reference
// application does not enable it in a fresh profile. Enabling it
// by default adds a redundant relation between equal-length line
// segments (including adjacent polyline segments).
enabled: AutoConstraintKind::ALL
.into_iter()
.filter(|kind| *kind != AutoConstraintKind::Equal)
.collect(),
tangent_must_share_point: true,
perpendicular_must_intersect: true,
distance_tolerance: 0.05,
angle_tolerance_deg: 1.0,
}
}
}
impl AutoConstrainSettings {
pub fn sanitize(&mut self) {
let mut priority = Vec::with_capacity(AutoConstraintKind::ALL.len());
for kind in self
.priority
.iter()
.copied()
.chain(AutoConstraintKind::ALL)
{
if !priority.contains(&kind) {
priority.push(kind);
}
}
self.priority = priority;
self.enabled
.retain(|kind| AutoConstraintKind::ALL.contains(kind));
self.enabled.sort_by_key(|kind| {
self.priority
.iter()
.position(|candidate| candidate == kind)
.unwrap_or(usize::MAX)
});
self.enabled.dedup();
if !self.distance_tolerance.is_finite() || self.distance_tolerance < 0.0 {
self.distance_tolerance = 0.05;
}
if !self.angle_tolerance_deg.is_finite() || self.angle_tolerance_deg < 0.0 {
self.angle_tolerance_deg = 1.0;
}
}
}
/// Cursor shown over the drawing viewport. /// Cursor shown over the drawing viewport.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum CursorType { pub enum CursorType {
@@ -289,6 +393,19 @@ pub struct UserSettings {
/// canvas detail on a dense sketch. /// canvas detail on a dense sketch.
#[serde(default = "default_show_constraint_values")] #[serde(default = "default_show_constraint_values")]
pub show_constraint_values: bool, pub show_constraint_values: bool,
/// Inference types, priority, intersection rules, and tolerances used by
/// the Auto Constrain command.
#[serde(default)]
pub auto_constrain: AutoConstrainSettings,
/// Keep existing geometry size while solving after a constraint edit.
#[serde(default = "default_constraint_solve_mode")]
pub constraint_solve_mode: bool,
/// Apply eligible geometric constraints while creating geometry.
#[serde(default)]
pub constraint_infer: bool,
/// Constraint bar display bit mask: 1 after applying, 2 on selection.
#[serde(default = "default_constraint_bar_display")]
pub constraint_bar_display: i16,
/// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command). /// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command).
/// 0 disables autosave. /// 0 disables autosave.
pub savetime_min: i32, pub savetime_min: i32,
@@ -359,6 +476,14 @@ fn default_show_constraint_values() -> bool {
true true
} }
fn default_constraint_solve_mode() -> bool {
true
}
fn default_constraint_bar_display() -> i16 {
3
}
fn deserialize_clipromptlines<'de, D>(de: D) -> Result<i32, D::Error> fn deserialize_clipromptlines<'de, D>(de: D) -> Result<i32, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
@@ -415,6 +540,10 @@ impl Default for UserSettings {
backup_on_save: true, backup_on_save: true,
file_assoc_enabled: true, file_assoc_enabled: true,
show_constraint_values: true, show_constraint_values: true,
auto_constrain: AutoConstrainSettings::default(),
constraint_solve_mode: true,
constraint_infer: false,
constraint_bar_display: 3,
savetime_min: 10, savetime_min: 10,
default_save_format: crate::io::DEFAULT_SAVE_FORMAT.to_string(), default_save_format: crate::io::DEFAULT_SAVE_FORMAT.to_string(),
pick_add: true, pick_add: true,
+39 -1
View File
@@ -3247,6 +3247,29 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.tabs[i].properties.active_field = None; self.tabs[i].properties.active_field = None;
let handles = self.property_target_handles(i); let handles = self.property_target_handles(i);
if !handles.is_empty() { if !handles.is_empty() {
let driven_marker = match field {
"start_x" | "start_y" | "start_z" => Some(0),
"end_x" | "end_y" | "end_z" => Some(1),
"center_x" | "center_y" | "center_z" => Some(-3),
_ => None,
};
let retained_originals: Vec<_> = if self.constraint_solve_mode
&& driven_marker.is_some()
{
handles
.iter()
.filter_map(|handle| {
self.tabs[i]
.scene
.document
.get_entity(*handle)
.cloned()
.map(|entity| (*handle, entity))
})
.collect()
} else {
Vec::new()
};
let evaluates_expression = self.tabs[i] let evaluates_expression = self.tabs[i]
.properties .properties
.sections .sections
@@ -3563,7 +3586,22 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
} }
} }
} }
self.invalidate_property_targets(i, &handles); let driven_refs: Vec<_> = driven_marker
.into_iter()
.flat_map(|marker| {
handles.iter().copied().map(move |handle| {
crate::scene::parametric_constraints::ParametricRef::point(
handle, marker,
)
})
})
.collect();
self.invalidate_property_targets_with_originals(
i,
&handles,
&driven_refs,
&retained_originals,
);
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.refresh_properties(); self.refresh_properties();
} }
+13
View File
@@ -428,6 +428,10 @@ impl OpenCADStudio {
backup_on_save: self.backup_on_save, backup_on_save: self.backup_on_save,
file_assoc_enabled: self.file_assoc_enabled, file_assoc_enabled: self.file_assoc_enabled,
show_constraint_values: self.show_constraint_values, show_constraint_values: self.show_constraint_values,
auto_constrain: self.auto_constrain_settings.clone(),
constraint_solve_mode: self.constraint_solve_mode,
constraint_infer: self.constraint_infer,
constraint_bar_display: self.constraint_bar_display,
savetime_min: self.savetime_min, savetime_min: self.savetime_min,
default_save_format: self.default_save_format.clone(), default_save_format: self.default_save_format.clone(),
pick_add: self.pick_add, pick_add: self.pick_add,
@@ -509,6 +513,15 @@ impl OpenCADStudio {
self.backup_on_save = s.backup_on_save; self.backup_on_save = s.backup_on_save;
self.file_assoc_enabled = s.file_assoc_enabled; self.file_assoc_enabled = s.file_assoc_enabled;
self.show_constraint_values = s.show_constraint_values; self.show_constraint_values = s.show_constraint_values;
self.auto_constrain_settings = s.auto_constrain.clone();
self.auto_constrain_settings.sanitize();
self.auto_constrain_distance_input =
format!("{}", self.auto_constrain_settings.distance_tolerance);
self.auto_constrain_angle_input =
format!("{}", self.auto_constrain_settings.angle_tolerance_deg);
self.constraint_solve_mode = s.constraint_solve_mode;
self.constraint_infer = s.constraint_infer;
self.constraint_bar_display = s.constraint_bar_display.clamp(0, 3);
self.savetime_min = s.savetime_min; self.savetime_min = s.savetime_min;
self.default_save_format = self.default_save_format =
crate::io::canonical_save_format(&s.default_save_format).to_string(); crate::io::canonical_save_format(&s.default_save_format).to_string();
+111
View File
@@ -4289,6 +4289,112 @@ impl OpenCADStudio {
self.drafting_settings_close_confirm = false; self.drafting_settings_close_confirm = false;
Task::none() Task::none()
} }
Message::AutoConstrainSelectRow(index) => {
if index < self.auto_constrain_settings.priority.len() {
self.auto_constrain_selected_row = index;
}
Task::none()
}
Message::AutoConstrainToggleKind(kind) => {
if let Some(index) = self
.auto_constrain_settings
.enabled
.iter()
.position(|candidate| *candidate == kind)
{
self.auto_constrain_settings.enabled.remove(index);
} else {
self.auto_constrain_settings.enabled.push(kind);
}
Task::none()
}
Message::AutoConstrainMoveUp => {
let index = self.auto_constrain_selected_row;
if index > 0 && index < self.auto_constrain_settings.priority.len() {
self.auto_constrain_settings.priority.swap(index, index - 1);
self.auto_constrain_selected_row -= 1;
}
Task::none()
}
Message::AutoConstrainMoveDown => {
let index = self.auto_constrain_selected_row;
if index + 1 < self.auto_constrain_settings.priority.len() {
self.auto_constrain_settings.priority.swap(index, index + 1);
self.auto_constrain_selected_row += 1;
}
Task::none()
}
Message::AutoConstrainSelectAll => {
self.auto_constrain_settings.enabled =
super::settings::AutoConstraintKind::ALL.to_vec();
Task::none()
}
Message::AutoConstrainClearAll => {
self.auto_constrain_settings.enabled.clear();
Task::none()
}
Message::AutoConstrainReset => {
self.auto_constrain_settings =
super::settings::AutoConstrainSettings::default();
self.auto_constrain_selected_row = 0;
self.auto_constrain_distance_input =
format!("{}", self.auto_constrain_settings.distance_tolerance);
self.auto_constrain_angle_input =
format!("{}", self.auto_constrain_settings.angle_tolerance_deg);
Task::none()
}
Message::AutoConstrainToggleTangentPoint => {
self.auto_constrain_settings.tangent_must_share_point =
!self.auto_constrain_settings.tangent_must_share_point;
Task::none()
}
Message::AutoConstrainTogglePerpendicularIntersection => {
self.auto_constrain_settings.perpendicular_must_intersect =
!self.auto_constrain_settings.perpendicular_must_intersect;
Task::none()
}
Message::AutoConstrainDistanceChanged(value) => {
self.auto_constrain_distance_input = value;
Task::none()
}
Message::AutoConstrainAngleChanged(value) => {
self.auto_constrain_angle_input = value;
Task::none()
}
action @ (Message::AutoConstrainApply | Message::AutoConstrainOk) => {
let distance = self.auto_constrain_distance_input.trim().parse::<f64>();
let angle = self.auto_constrain_angle_input.trim().parse::<f64>();
match (distance, angle) {
(Ok(distance), Ok(angle))
if distance.is_finite()
&& distance >= 0.0
&& angle.is_finite()
&& angle >= 0.0 =>
{
self.auto_constrain_settings.distance_tolerance = distance;
self.auto_constrain_settings.angle_tolerance_deg = angle;
self.auto_constrain_settings.sanitize();
self.auto_constrain_saved =
Some(self.auto_constrain_settings.clone());
self.persist_settings_if_changed();
if matches!(action, Message::AutoConstrainOk) {
self.close_active_modal();
}
}
_ => self.command_line.push_error(
crate::t!("Distance and angle tolerances must be non-negative numbers.")
.as_ref(),
),
}
Task::none()
}
Message::AutoConstrainCancel => {
if let Some(saved) = self.auto_constrain_saved.take() {
self.auto_constrain_settings = saved;
}
self.close_active_modal();
Task::none()
}
// ── Ribbon dropdowns ────────────────────────────────────────── // ── Ribbon dropdowns ──────────────────────────────────────────
Message::ToggleRibbonDropdown(id) => { Message::ToggleRibbonDropdown(id) => {
@@ -6857,6 +6963,11 @@ impl OpenCADStudio {
return Task::none(); return Task::none();
} }
self.drafting_settings_close_confirm = false; self.drafting_settings_close_confirm = false;
if self.active_modal == Some(super::ModalKind::AutoConstrainSettings) {
if let Some(saved) = self.auto_constrain_saved.take() {
self.auto_constrain_settings = saved;
}
}
let resume_open_queue = self.active_modal == Some(super::ModalKind::Recovery); let resume_open_queue = self.active_modal == Some(super::ModalKind::Recovery);
self.close_active_modal(); self.close_active_modal();
if resume_open_queue { if resume_open_queue {
+75 -1
View File
@@ -933,6 +933,13 @@ impl OpenCADStudio {
return Task::none(); return Task::none();
} }
let i = self.active_tab; let i = self.active_tab;
let constraint_hover = self
.constraint_glyph_under(i, p)
.map(|(_, handles)| handles)
.unwrap_or_default();
self.tabs[i]
.scene
.set_constraint_hover_highlights(&constraint_hover);
// Modifier-driven selection must be known before cursor_plane/axis and // Modifier-driven selection must be known before cursor_plane/axis and
// drafting constraints are read, not merely before the final callback. // drafting constraints are read, not merely before the final callback.
if let Some(command) = self.tabs[i].active_cmd.as_mut() { if let Some(command) = self.tabs[i].active_cmd.as_mut() {
@@ -1737,11 +1744,31 @@ impl OpenCADStudio {
); );
} }
} }
let driven_refs: Vec<_> = grip
.targets
.iter()
.filter_map(|target| {
let entity = self.tabs[i]
.scene
.document
.get_entity(target.handle)?;
crate::scene::parametric_constraints::driven_ref_for_grip(
entity,
target.handle,
target.grip_id,
)
})
.collect();
// Re-solve constrained neighbors on each drag frame and include // Re-solve constrained neighbors on each drag frame and include
// their original state in the gesture's undo record. // their original state in the gesture's undo record.
let solved_by_constraints = self.tabs[i] let solved_by_constraints = self.tabs[i]
.scene .scene
.solve_parametric_constraints_preview(&edited_handles); .solve_parametric_constraints_preview(
&edited_handles,
&driven_refs,
self.constraint_solve_mode && !driven_refs.is_empty(),
&self.grip_originals,
);
for (handle, _) in &solved_by_constraints { for (handle, _) in &solved_by_constraints {
let handle = *handle; let handle = *handle;
if !self.grip_preview_handles.contains(&handle) { if !self.grip_preview_handles.contains(&handle) {
@@ -2637,6 +2664,7 @@ impl OpenCADStudio {
// viewport so it doesn't stick while the mouse is over the // viewport so it doesn't stick while the mouse is over the
// ribbon / panels. // ribbon / panels.
self.tabs[i].scene.set_hover_highlight(None); self.tabs[i].scene.set_hover_highlight(None);
self.tabs[i].scene.set_constraint_hover_highlights(&[]);
// Don't touch `context_menu` here. ViewportExit also fires // Don't touch `context_menu` here. ViewportExit also fires
// when an upper overlay (the right-click menu panel) takes // when an upper overlay (the right-click menu panel) takes
// the cursor, so clearing the menu state on every exit // the cursor, so clearing the menu state on every exit
@@ -2949,6 +2977,34 @@ impl OpenCADStudio {
Task::none() Task::none()
} }
fn constraint_glyph_under(
&self,
i: usize,
cursor: iced::Point,
) -> Option<(
crate::scene::parametric_constraints::ConstraintKind,
Vec<acadrust::Handle>,
)> {
if self.tabs[i].scene.current_layout != "Model" {
return None;
}
let vp_size = self.tabs[i].scene.selection.borrow().vp_size;
let scope = self.tabs[i].current_parametric_scope();
let id = self.tabs[i].scene.constraint_glyph_hit(
scope,
vp_size,
self.show_constraint_values,
self.constraint_bar_display,
cursor,
)?;
let set = self.tabs[i].scene.parametric_constraint_set(scope)?;
let constraint = set.get(id)?;
let mut handles: Vec<_> = constraint.refs.iter().map(|r| r.entity).collect();
handles.sort_unstable_by_key(|handle| handle.value());
handles.dedup();
Some((constraint.kind, handles))
}
pub(super) fn on_viewport_left_press(&mut self) -> Task<Message> { pub(super) fn on_viewport_left_press(&mut self) -> Task<Message> {
let i = self.active_tab; let i = self.active_tab;
if let Some(command) = self.tabs[i].active_cmd.as_mut() { if let Some(command) = self.tabs[i].active_cmd.as_mut() {
@@ -3004,6 +3060,23 @@ impl OpenCADStudio {
}; };
let (vw, vh) = vp_size; let (vw, vh) = vp_size;
if self.tabs[i].active_cmd.is_none() {
if let Some((kind, handles)) = self.constraint_glyph_under(i, p) {
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.select_entities(&handles);
self.refresh_selected_grips();
self.refresh_properties();
self.command_line
.push_info(&format!("{kind:?} constraint selected."));
self.tabs[i]
.scene
.selection
.borrow_mut()
.clear_left_selection_gesture();
return Task::none();
}
}
// An engaged grip owns the next left press (click-move-click placement // An engaged grip owns the next left press (click-move-click placement
// or the release of a press-drag). Do not let the same press arm the // or the release of a press-drag). Do not let the same press arm the
// normal box/lasso state or trigger another viewport control; the // normal box/lasso state or trigger another viewport control; the
@@ -4432,6 +4505,7 @@ properties={:.1}ms picked={}",
scope, scope,
canvas_sz, canvas_sz,
self.show_constraint_values, self.show_constraint_values,
self.constraint_bar_display,
p_full, p_full,
) )
}) })
+1
View File
@@ -822,6 +822,7 @@ bg={bg_ms:.1}ms n={view_count}"
scope, scope,
sel_ref.vp_size, sel_ref.vp_size,
self.show_constraint_values, self.show_constraint_values,
self.constraint_bar_display,
) )
.into_iter() .into_iter()
.map(|(id, point, direction, label, is_conflicting)| { .map(|(id, point, direction, label, is_conflicting)| {
+15
View File
@@ -25,6 +25,7 @@ impl OpenCADStudio {
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(), Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(), Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(), Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(),
Some(K::AutoConstrainSettings) => crate::t!("Constraint Settings").into_owned(),
Some(K::LayerStateEditor) => crate::tr!("modal", "edit-layer-state"), Some(K::LayerStateEditor) => crate::tr!("modal", "edit-layer-state"),
Some(K::Plot) => crate::tr!("modal", "plot"), Some(K::Plot) => crate::tr!("modal", "plot"),
Some(K::PrintAll) => t!("Print All").into_owned(), Some(K::PrintAll) => t!("Print All").into_owned(),
@@ -307,6 +308,20 @@ impl OpenCADStudio {
}, },
) )
} }
super::super::ModalKind::AutoConstrainSettings => sized_flow(
ex,
620,
610,
|flow| {
crate::ui::window::auto_constrain_settings::view_window(
&self.auto_constrain_settings,
self.auto_constrain_selected_row,
&self.auto_constrain_distance_input,
&self.auto_constrain_angle_input,
flow,
)
},
),
super::super::ModalKind::FindReplace => automatic_flow(ex, |flow| { super::super::ModalKind::FindReplace => automatic_flow(ex, |flow| {
crate::ui::window::find_replace::view_window( crate::ui::window::find_replace::view_window(
&self.find_replace.search, &self.find_replace.search,
+2
View File
@@ -1466,6 +1466,8 @@ pub enum CmdResult {
/// Undo-history label, e.g. `"Horizontal constraint"`. /// Undo-history label, e.g. `"Horizontal constraint"`.
label: &'static str, label: &'static str,
}, },
/// Opens the Auto Constrain settings dialog from the selection prompt.
OpenAutoConstrainSettings,
/// Adds a Coincident constraint between the sub-entity points nearest /// Adds a Coincident constraint between the sub-entity points nearest
/// the two picked positions. The host resolves the raw points because /// the two picked positions. The host resolves the raw points because
/// `CadCommand` has no document access. /// `CadCommand` has no document access.
+35 -2
View File
@@ -34,6 +34,8 @@ pub struct SelectObjectsCommand {
/// Whether the polygon being picked takes what it merely touches. Ignored /// Whether the polygon being picked takes what it merely touches. Ignored
/// by a fence, which has no inside to speak of. /// by a fence, which has no inside to speak of.
pick_crossing: bool, pick_crossing: bool,
/// Add the command-specific Settings keyword used by Auto Constrain.
auto_constrain_settings: bool,
} }
impl SelectObjectsCommand { impl SelectObjectsCommand {
@@ -47,6 +49,7 @@ impl SelectObjectsCommand {
show_options: true, show_options: true,
pick: None, pick: None,
pick_crossing: true, pick_crossing: true,
auto_constrain_settings: false,
} }
} }
@@ -61,6 +64,7 @@ impl SelectObjectsCommand {
show_options: false, show_options: false,
pick: None, pick: None,
pick_crossing: true, pick_crossing: true,
auto_constrain_settings: false,
} }
} }
@@ -75,8 +79,15 @@ impl SelectObjectsCommand {
show_options: false, show_options: false,
pick: None, pick: None,
pick_crossing: true, pick_crossing: true,
auto_constrain_settings: false,
} }
} }
pub fn auto_constrain(pending_cmd: &str) -> Self {
let mut command = Self::new(pending_cmd);
command.auto_constrain_settings = true;
command
}
} }
impl CadCommand for SelectObjectsCommand { impl CadCommand for SelectObjectsCommand {
@@ -101,6 +112,14 @@ impl CadCommand for SelectObjectsCommand {
.into_owned(); .into_owned();
} }
if self.commit_on_enter && !self.handles.is_empty() { if self.commit_on_enter && !self.handles.is_empty() {
if self.auto_constrain_settings {
return t!(
"%{cmd} Select objects or [Settings] (%{count} selected, Enter to apply):",
cmd = self.prompt_cmd,
count = self.handles.len()
)
.into_owned();
}
t!( t!(
"%{cmd} Select objects (%{count} selected, Enter to apply):", "%{cmd} Select objects (%{count} selected, Enter to apply):",
cmd = self.prompt_cmd, cmd = self.prompt_cmd,
@@ -108,6 +127,13 @@ impl CadCommand for SelectObjectsCommand {
) )
.into_owned() .into_owned()
} else { } else {
if self.auto_constrain_settings {
return t!(
"%{cmd} Select objects or [Settings]:",
cmd = self.prompt_cmd
)
.into_owned();
}
t!("%{cmd} Select objects:", cmd = self.prompt_cmd).into_owned() t!("%{cmd} Select objects:", cmd = self.prompt_cmd).into_owned()
} }
} }
@@ -130,7 +156,10 @@ impl CadCommand for SelectObjectsCommand {
// the on-screen keyboard-less case needs. // the on-screen keyboard-less case needs.
fn options(&self) -> Vec<CmdOption> { fn options(&self) -> Vec<CmdOption> {
if self.commit_on_enter && self.show_options { if self.commit_on_enter && self.show_options {
vec![ if self.auto_constrain_settings {
return vec![CmdOption::new(t!("Settings").as_ref(), "S")];
}
let options = vec![
CmdOption::new(t!("Window").as_ref(), "W"), CmdOption::new(t!("Window").as_ref(), "W"),
CmdOption::new(t!("Crossing").as_ref(), "C"), CmdOption::new(t!("Crossing").as_ref(), "C"),
CmdOption::new(t!("Fence").as_ref(), "F"), CmdOption::new(t!("Fence").as_ref(), "F"),
@@ -141,7 +170,8 @@ impl CadCommand for SelectObjectsCommand {
CmdOption::new(t!("Remove").as_ref(), "R"), CmdOption::new(t!("Remove").as_ref(), "R"),
CmdOption::new(t!("Previous").as_ref(), "P"), CmdOption::new(t!("Previous").as_ref(), "P"),
CmdOption::new(t!("Last").as_ref(), "L"), CmdOption::new(t!("Last").as_ref(), "L"),
] ];
options
} else { } else {
Vec::new() Vec::new()
} }
@@ -181,6 +211,9 @@ impl CadCommand for SelectObjectsCommand {
return None; return None;
} }
match text.trim().to_uppercase().as_str() { match text.trim().to_uppercase().as_str() {
"S" | "SETTINGS" if self.auto_constrain_settings => {
Some(CmdResult::OpenAutoConstrainSettings)
}
"F" | "FENCE" => { "F" | "FENCE" => {
self.pick = Some(FencePick::fence()); self.pick = Some(FencePick::fence());
Some(CmdResult::NeedPoint) Some(CmdResult::NeedPoint)
+1 -1
View File
@@ -128,7 +128,7 @@ impl CadModule for ParametricModule {
inventory::submit!(crate::command::CommandRegistration { inventory::submit!(crate::command::CommandRegistration {
names: &[ names: &[
"AUTOCONSTRAIN", "SMOOTHCONSTRAINT", "GCSHOW", "GCHIDE", "GCRESET", "AUTOCONSTRAIN", "CONSTRAINTSETTINGS", "SMOOTHCONSTRAINT", "GCSHOW", "GCHIDE", "GCRESET",
"GCSHOWALL", "GCHIDEALL", "DCSHOW", "DCHIDE", "DCSHOWALL", "DCHIDEALL", "GCSHOWALL", "GCHIDEALL", "DCSHOW", "DCHIDE", "DCSHOWALL", "DCHIDEALL",
"DCCONVERT", "DELCONSTRAINT", "DCCONVERT", "DELCONSTRAINT",
] ]
+55 -3
View File
@@ -1681,6 +1681,8 @@ pub struct Scene {
/// Entity drawn with the selection-highlight colour without being part /// Entity drawn with the selection-highlight colour without being part
/// of the real selection — used to preview a row in the cycling list box. /// of the real selection — used to preview a row in the cycling list box.
pub hover_highlight: Option<Handle>, pub hover_highlight: Option<Handle>,
/// All entities related to the constraint indicator currently under the cursor.
constraint_hover_highlights: HashSet<Handle>,
/// Color used to draw the selection highlight overlay wires (SELECTIONEFFECTCOLOR). /// Color used to draw the selection highlight overlay wires (SELECTIONEFFECTCOLOR).
pub selection_color: [f32; 4], pub selection_color: [f32; 4],
/// Whether selection visual effect (glow/highlight) is enabled (SELECTIONEFFECT). /// Whether selection visual effect (glow/highlight) is enabled (SELECTIONEFFECT).
@@ -1961,6 +1963,11 @@ pub struct Scene {
parametric_constraints::ParametricScope, parametric_constraints::ParametricScope,
parametric_constraints::ConstraintId, parametric_constraints::ConstraintId,
)>, )>,
/// Constraint glyphs explicitly shown through the constraint-bar commands.
shown_parametric_constraints: HashSet<(
parametric_constraints::ParametricScope,
parametric_constraints::ConstraintId,
)>,
/// Document-wide named-parameter and expression table decoded from /// Document-wide named-parameter and expression table decoded from
/// standard associative variables. /// standard associative variables.
pub(crate) named_parameters: named_parameters::ParameterTable, pub(crate) named_parameters: named_parameters::ParameterTable,
@@ -2205,6 +2212,7 @@ impl Scene {
command_preview_hidden: HashSet::default(), command_preview_hidden: HashSet::default(),
refedit_keep: None, refedit_keep: None,
hover_highlight: None, hover_highlight: None,
constraint_hover_highlights: HashSet::default(),
selection_color: crate::scene::model::wire_model::WireModel::SELECTED, selection_color: crate::scene::model::wire_model::WireModel::SELECTED,
selection_effect: true, selection_effect: true,
transparency_display: true, transparency_display: true,
@@ -2279,6 +2287,7 @@ impl Scene {
associative_hatch_source_cache: RefCell::new(None), associative_hatch_source_cache: RefCell::new(None),
parametric_constraints: Vec::new(), parametric_constraints: Vec::new(),
hidden_parametric_constraints: HashSet::default(), hidden_parametric_constraints: HashSet::default(),
shown_parametric_constraints: HashSet::default(),
named_parameters: named_parameters::ParameterTable::new(), named_parameters: named_parameters::ParameterTable::new(),
has_associative_centers: std::cell::Cell::new(None), has_associative_centers: std::cell::Cell::new(None),
block_defn_cache: RefCell::new(HashMap::default()), block_defn_cache: RefCell::new(HashMap::default()),
@@ -2838,6 +2847,33 @@ impl Scene {
} }
pub fn bump_entities(&mut self, changes: &[(Handle, ChangeKind)]) { pub fn bump_entities(&mut self, changes: &[(Handle, ChangeKind)]) {
self.bump_entities_with_parametric_policy(changes, &[], false);
}
pub fn bump_entities_with_parametric_driven(
&mut self,
changes: &[(Handle, ChangeKind)],
driven_refs: &[parametric_constraints::ParametricRef],
) {
self.bump_entities_with_parametric_policy(changes, driven_refs, false);
}
pub fn bump_entities_with_parametric_policy(
&mut self,
changes: &[(Handle, ChangeKind)],
driven_refs: &[parametric_constraints::ParametricRef],
retain_size: bool,
) {
self.bump_entities_with_parametric_originals(changes, driven_refs, retain_size, &[]);
}
pub fn bump_entities_with_parametric_originals(
&mut self,
changes: &[(Handle, ChangeKind)],
driven_refs: &[parametric_constraints::ParametricRef],
retain_size: bool,
retained_originals: &[(Handle, EntityType)],
) {
if changes.iter().any(|(handle, kind)| { if changes.iter().any(|(handle, kind)| {
matches!(kind, ChangeKind::Removed) matches!(kind, ChangeKind::Removed)
|| self || self
@@ -2871,7 +2907,12 @@ impl Scene {
} }
} }
if !self.parametric_constraints.is_empty() { if !self.parametric_constraints.is_empty() {
for change in self.refresh_parametric_constraints(&changes) { for change in self.refresh_parametric_constraints_with_originals(
&changes,
driven_refs,
retain_size,
retained_originals,
) {
if !changes.iter().any(|(handle, _)| *handle == change.0) { if !changes.iter().any(|(handle, _)| *handle == change.0) {
changes.push(change); changes.push(change);
} }
@@ -5075,9 +5116,20 @@ impl Scene {
/// remains the picked member for hit-test/UI bookkeeping; rendering expands /// remains the picked member for hit-test/UI bookkeeping; rendering expands
/// it to the same selectable group that a click would select. /// it to the same selectable group that a click would select.
pub fn hover_highlight_handles(&self) -> HashSet<Handle> { pub fn hover_highlight_handles(&self) -> HashSet<Handle> {
self.hover_highlight let mut handles = self
.hover_highlight
.map(|handle| self.handles_expanded_for_selectable_groups(&[handle])) .map(|handle| self.handles_expanded_for_selectable_groups(&[handle]))
.unwrap_or_default() .unwrap_or_default();
handles.extend(self.constraint_hover_highlights.iter().copied());
handles
}
pub fn set_constraint_hover_highlights(&mut self, handles: &[Handle]) {
let desired: HashSet<_> = handles.iter().copied().collect();
if desired != self.constraint_hover_highlights {
self.constraint_hover_highlights = desired;
self.bump_selection();
}
} }
/// Keep the current selection visible and temporarily filter every other /// Keep the current selection visible and temporarily filter every other
+258 -38
View File
@@ -83,6 +83,45 @@ impl ParametricRef {
} }
} }
/// Point reference driven by a native entity grip. Segment midpoint and
/// scalar radius grips deliberately return None because they do not identify
/// one stored point parameter.
pub(crate) fn driven_ref_for_grip(
entity: &acadrust::EntityType,
handle: Handle,
grip_id: usize,
) -> Option<ParametricRef> {
match entity {
acadrust::EntityType::Line(_) if grip_id <= 1 => {
Some(ParametricRef::point(handle, grip_id as i32))
}
acadrust::EntityType::LwPolyline(polyline) if grip_id < polyline.vertices.len() => {
Some(ParametricRef::point(handle, grip_id as i32))
}
acadrust::EntityType::Polyline2D(polyline) if grip_id < polyline.vertices.len() => {
Some(ParametricRef::point(handle, grip_id as i32))
}
acadrust::EntityType::Arc(_) => match grip_id {
0 => Some(ParametricRef::center(handle)),
1 => Some(ParametricRef::point(handle, 0)),
2 => Some(ParametricRef::point(handle, 1)),
_ => None,
},
acadrust::EntityType::Circle(_) if grip_id == 0 => {
Some(ParametricRef::center(handle))
}
acadrust::EntityType::Point(_)
| acadrust::EntityType::Insert(_)
| acadrust::EntityType::Text(_)
| acadrust::EntityType::MText(_)
if grip_id == 0 =>
{
Some(ParametricRef::point(handle, 0))
}
_ => None,
}
}
/// The friendly, user-facing constraint types — the "what button did they /// The friendly, user-facing constraint types — the "what button did they
/// click" vocabulary, one layer above the `cadkernel_constraints` primitives each maps /// click" vocabulary, one layer above the `cadkernel_constraints` primitives each maps
/// onto (that mapping is `constraint_map`, a later stage; see the design /// onto (that mapping is `constraint_map`, a later stage; see the design
@@ -576,6 +615,19 @@ impl super::Scene {
!self.hidden_parametric_constraints.contains(&(scope, id)) !self.hidden_parametric_constraints.contains(&(scope, id))
} }
pub fn should_display_parametric_constraint(
&self,
scope: ParametricScope,
id: ConstraintId,
related_entity_selected: bool,
display_mode: i16,
) -> bool {
self.is_parametric_constraint_visible(scope, id)
&& (self.shown_parametric_constraints.contains(&(scope, id))
|| display_mode & 1 != 0
|| (display_mode & 2 != 0 && related_entity_selected))
}
pub fn set_parametric_constraint_visibility( pub fn set_parametric_constraint_visibility(
&mut self, &mut self,
scope: ParametricScope, scope: ParametricScope,
@@ -601,8 +653,10 @@ impl super::Scene {
for id in &ids { for id in &ids {
if visible { if visible {
self.hidden_parametric_constraints.remove(&(scope, *id)); self.hidden_parametric_constraints.remove(&(scope, *id));
self.shown_parametric_constraints.insert((scope, *id));
} else { } else {
self.hidden_parametric_constraints.insert((scope, *id)); self.hidden_parametric_constraints.insert((scope, *id));
self.shown_parametric_constraints.remove(&(scope, *id));
} }
} }
ids.len() ids.len()
@@ -613,40 +667,155 @@ impl super::Scene {
&self, &self,
scope: ParametricScope, scope: ParametricScope,
handles: &[Handle], handles: &[Handle],
settings: &crate::app::settings::AutoConstrainSettings,
) -> Vec<(ConstraintKind, Vec<ParametricRef>)> { ) -> Vec<(ConstraintKind, Vec<ParametricRef>)> {
use cadkernel::geom2d::{ use cadkernel::geom2d::{
infer_constraints, Arc, Circle, ConstraintEndpoint, InferredConstraint, Line, infer_constraints_with_settings, Arc, Circle, ConstraintEndpoint, InferenceKind,
ParametricPrimitive, Tolerance, InferenceSettings, InferredConstraint, Line, ParametricPrimitive,
}; };
struct Source {
handle: Handle,
primitive: ParametricPrimitive,
whole: ParametricRef,
endpoints: [ParametricRef; 2],
}
let mut sources = Vec::new(); let mut sources = Vec::new();
for handle in handles { for handle in handles {
let primitive = match self.document.get_entity(*handle) { match self.document.get_entity(*handle) {
Some(acadrust::EntityType::Line(line)) => ParametricPrimitive::Line(Line { Some(acadrust::EntityType::Line(line)) => sources.push(Source {
start: [line.start.x, line.start.y], handle: *handle,
end: [line.end.x, line.end.y], primitive: ParametricPrimitive::Line(Line {
start: [line.start.x, line.start.y],
end: [line.end.x, line.end.y],
}),
whole: ParametricRef::whole(*handle),
endpoints: [
ParametricRef::point(*handle, 0),
ParametricRef::point(*handle, 1),
],
}), }),
Some(acadrust::EntityType::Circle(circle)) => ParametricPrimitive::Circle(Circle { Some(acadrust::EntityType::Circle(circle)) => sources.push(Source {
centre: [circle.center.x, circle.center.y], handle: *handle,
radius: circle.radius, primitive: ParametricPrimitive::Circle(Circle {
centre: [circle.center.x, circle.center.y],
radius: circle.radius,
}),
whole: ParametricRef::whole(*handle),
endpoints: [
ParametricRef::center(*handle),
ParametricRef::center(*handle),
],
}), }),
Some(acadrust::EntityType::Arc(arc)) => ParametricPrimitive::Arc(Arc { Some(acadrust::EntityType::Arc(arc)) => sources.push(Source {
centre: [arc.center.x, arc.center.y], handle: *handle,
radius: arc.radius, primitive: ParametricPrimitive::Arc(Arc {
start_angle: arc.start_angle, centre: [arc.center.x, arc.center.y],
end_angle: arc.end_angle, radius: arc.radius,
start_angle: arc.start_angle,
end_angle: arc.end_angle,
}),
whole: ParametricRef::whole(*handle),
endpoints: [
ParametricRef::point(*handle, 0),
ParametricRef::point(*handle, 1),
],
}), }),
Some(acadrust::EntityType::LwPolyline(polyline)) => {
let Some(world) = crate::entities::curve::lwpolyline_world_xy(polyline) else {
continue;
};
let count = world.vertices.len();
for index in 0..count {
let next = index + 1;
if next >= count && !world.is_closed {
break;
}
if world.vertices[index].bulge.abs() > 1e-9 {
continue;
}
let a = world.vertices[index].location;
let b = world.vertices[next % count].location;
sources.push(Source {
handle: *handle,
primitive: ParametricPrimitive::Line(Line {
start: [a.x, a.y],
end: [b.x, b.y],
}),
whole: ParametricRef::segment(*handle, index),
endpoints: [
ParametricRef::point(*handle, index as i32),
ParametricRef::point(*handle, (next % count) as i32),
],
});
}
}
Some(acadrust::EntityType::Polyline2D(polyline)) => {
let count = polyline.vertices.len();
for index in 0..count {
let next = index + 1;
if next >= count && !polyline.is_closed() {
break;
}
if polyline.vertices[index].bulge.abs() > 1e-9 {
continue;
}
let a = polyline.vertices[index].location;
let b = polyline.vertices[next % count].location;
sources.push(Source {
handle: *handle,
primitive: ParametricPrimitive::Line(Line {
start: [a.x, a.y],
end: [b.x, b.y],
}),
whole: ParametricRef::segment(*handle, index),
endpoints: [
ParametricRef::point(*handle, index as i32),
ParametricRef::point(*handle, (next % count) as i32),
],
});
}
}
_ => continue, _ => continue,
}; }
sources.push((*handle, primitive));
} }
let primitives: Vec<_> = sources.iter().map(|(_, primitive)| *primitive).collect(); let primitives: Vec<_> = sources.iter().map(|source| source.primitive).collect();
let marker = |endpoint| match endpoint { let marker = |endpoint| match endpoint {
ConstraintEndpoint::Start => 0, ConstraintEndpoint::Start => 0usize,
ConstraintEndpoint::End => 1, ConstraintEndpoint::End => 1usize,
};
let kind = |kind| match kind {
crate::app::settings::AutoConstraintKind::Coincident => InferenceKind::Coincident,
crate::app::settings::AutoConstraintKind::Collinear => InferenceKind::Collinear,
crate::app::settings::AutoConstraintKind::Parallel => InferenceKind::Parallel,
crate::app::settings::AutoConstraintKind::Perpendicular => {
InferenceKind::Perpendicular
}
crate::app::settings::AutoConstraintKind::Tangent => InferenceKind::Tangent,
crate::app::settings::AutoConstraintKind::Concentric => InferenceKind::Concentric,
crate::app::settings::AutoConstraintKind::Horizontal => InferenceKind::Horizontal,
crate::app::settings::AutoConstraintKind::Vertical => InferenceKind::Vertical,
crate::app::settings::AutoConstraintKind::Equal => InferenceKind::Equal,
};
let inference_settings = InferenceSettings {
priority: settings
.priority
.iter()
.copied()
.filter(|candidate| settings.enabled.contains(candidate))
.map(kind)
.collect(),
distance_tolerance: settings.distance_tolerance,
angle_tolerance_radians: settings.angle_tolerance_deg.to_radians(),
tangent_must_share_point: settings.tangent_must_share_point,
perpendicular_must_intersect: settings.perpendicular_must_intersect,
}; };
let mut mapped: Vec<_> = let mut mapped: Vec<_> =
infer_constraints(&primitives, Tolerance::new(1e-6), 0.5_f64.to_radians()) infer_constraints_with_settings(&primitives, &inference_settings)
.into_iter() .into_iter()
.filter(|relation| {
!matches!(relation, InferredConstraint::Coincident { first, second, .. }
if sources[*first].handle == sources[*second].handle)
})
.map(|relation| match relation { .map(|relation| match relation {
InferredConstraint::Coincident { InferredConstraint::Coincident {
first, first,
@@ -656,53 +825,57 @@ impl super::Scene {
} => ( } => (
ConstraintKind::Coincident, ConstraintKind::Coincident,
vec![ vec![
ParametricRef::point(sources[first].0, marker(first_endpoint)), sources[first].endpoints[marker(first_endpoint)],
ParametricRef::point(sources[second].0, marker(second_endpoint)), sources[second].endpoints[marker(second_endpoint)],
], ],
), ),
InferredConstraint::Collinear { first, second } => ( InferredConstraint::Collinear { first, second } => (
ConstraintKind::Colinear, ConstraintKind::Colinear,
vec![ vec![
ParametricRef::whole(sources[first].0), sources[first].whole,
ParametricRef::whole(sources[second].0), sources[second].whole,
], ],
), ),
InferredConstraint::Concentric { first, second } => ( InferredConstraint::Concentric { first, second } => (
ConstraintKind::Concentric, ConstraintKind::Concentric,
vec![ vec![
ParametricRef::center(sources[first].0), ParametricRef::center(sources[first].handle),
ParametricRef::center(sources[second].0), ParametricRef::center(sources[second].handle),
], ],
), ),
InferredConstraint::Parallel { first, second } => ( InferredConstraint::Parallel { first, second } => (
ConstraintKind::Parallel, ConstraintKind::Parallel,
vec![ vec![
ParametricRef::whole(sources[first].0), sources[first].whole,
ParametricRef::whole(sources[second].0), sources[second].whole,
], ],
), ),
InferredConstraint::Perpendicular { first, second } => ( InferredConstraint::Perpendicular { first, second } => (
ConstraintKind::Perpendicular, ConstraintKind::Perpendicular,
vec![ vec![
ParametricRef::whole(sources[first].0), sources[first].whole,
ParametricRef::whole(sources[second].0), sources[second].whole,
], ],
), ),
InferredConstraint::Horizontal { entity } => ( InferredConstraint::Horizontal { entity } => (
ConstraintKind::Horizontal, ConstraintKind::Horizontal,
vec![ParametricRef::whole(sources[entity].0)], vec![sources[entity].whole],
), ),
InferredConstraint::Vertical { entity } => ( InferredConstraint::Vertical { entity } => (
ConstraintKind::Vertical, ConstraintKind::Vertical,
vec![ParametricRef::whole(sources[entity].0)], vec![sources[entity].whole],
), ),
InferredConstraint::Tangent { first, second } => ( InferredConstraint::Tangent { first, second } => (
ConstraintKind::Tangent, ConstraintKind::Tangent,
vec![ vec![
ParametricRef::whole(sources[first].0), sources[first].whole,
ParametricRef::whole(sources[second].0), sources[second].whole,
], ],
), ),
InferredConstraint::Equal { first, second } => (
ConstraintKind::Equal,
vec![sources[first].whole, sources[second].whole],
),
}) })
.collect(); .collect();
if let Some(existing) = self.parametric_constraint_set(scope) { if let Some(existing) = self.parametric_constraint_set(scope) {
@@ -806,6 +979,7 @@ impl super::Scene {
scope: ParametricScope, scope: ParametricScope,
vp_size: (f32, f32), vp_size: (f32, f32),
show_values: bool, show_values: bool,
display_mode: i16,
) -> Vec<(ConstraintId, iced::Point, [f32; 2], String, bool)> { ) -> Vec<(ConstraintId, iced::Point, [f32; 2], String, bool)> {
let Some(set) = self.parametric_constraint_set(scope) else { let Some(set) = self.parametric_constraint_set(scope) else {
return Vec::new(); return Vec::new();
@@ -827,7 +1001,13 @@ impl super::Scene {
set.constraints set.constraints
.iter() .iter()
.filter(|c| c.enabled) .filter(|c| c.enabled)
.filter(|c| self.is_parametric_constraint_visible(scope, c.id)) .filter(|c| {
let selected = c
.refs
.iter()
.any(|reference| self.selected.contains(&reference.entity));
self.should_display_parametric_constraint(scope, c.id, selected, display_mode)
})
.filter_map(|c| { .filter_map(|c| {
let (anchor, outward) = glyph_placement(&self.document, c)?; let (anchor, outward) = glyph_placement(&self.document, c)?;
let screen = crate::scene::pick::grip::project_rte( let screen = crate::scene::pick::grip::project_rte(
@@ -869,9 +1049,15 @@ impl super::Scene {
scope: ParametricScope, scope: ParametricScope,
vp_size: (f32, f32), vp_size: (f32, f32),
show_values: bool, show_values: bool,
display_mode: i16,
p: iced::Point, p: iced::Point,
) -> Option<ConstraintId> { ) -> Option<ConstraintId> {
let placements = self.constraint_glyph_placements_screen(scope, vp_size, show_values); let placements = self.constraint_glyph_placements_screen(
scope,
vp_size,
show_values,
display_mode,
);
let glyphs: Vec<(iced::Point, [f32; 2], String, bool)> = placements let glyphs: Vec<(iced::Point, [f32; 2], String, bool)> = placements
.iter() .iter()
.map(|(_, point, direction, label, is_conflicting)| { .map(|(_, point, direction, label, is_conflicting)| {
@@ -1158,6 +1344,24 @@ mod tests {
); );
} }
#[test]
fn arc_grips_drive_center_start_and_end_but_not_midpoint() {
let handle = h(8);
let arc = acadrust::EntityType::Arc(acadrust::entities::Arc::from_coords(
0.0,
0.0,
0.0,
5.0,
0.0,
std::f64::consts::PI,
));
assert_eq!(driven_ref_for_grip(&arc, handle, 0), Some(ParametricRef::center(handle)));
assert_eq!(driven_ref_for_grip(&arc, handle, 1), Some(ParametricRef::point(handle, 0)));
assert_eq!(driven_ref_for_grip(&arc, handle, 2), Some(ParametricRef::point(handle, 1)));
assert_eq!(driven_ref_for_grip(&arc, handle, 3), None);
}
#[test] #[test]
fn parameter_usage_finds_every_constraint_driven_by_the_named_parameter() { fn parameter_usage_finds_every_constraint_driven_by_the_named_parameter() {
let mut scene = super::super::Scene::new(); let mut scene = super::super::Scene::new();
@@ -1266,7 +1470,11 @@ mod tests {
); );
let inferred = let inferred =
scene.inferred_parametric_constraints(ParametricScope::ModelSpace, &[first, second]); scene.inferred_parametric_constraints(
ParametricScope::ModelSpace,
&[first, second],
&crate::app::settings::AutoConstrainSettings::default(),
);
assert!(!inferred.iter().any(|(kind, refs)| { assert!(!inferred.iter().any(|(kind, refs)| {
*kind == ConstraintKind::Horizontal && *refs == [ParametricRef::whole(first)] *kind == ConstraintKind::Horizontal && *refs == [ParametricRef::whole(first)]
@@ -1277,7 +1485,7 @@ mod tests {
assert!(inferred assert!(inferred
.iter() .iter()
.any(|(kind, _)| *kind == ConstraintKind::Coincident)); .any(|(kind, _)| *kind == ConstraintKind::Coincident));
assert!(inferred assert!(!inferred
.iter() .iter()
.any(|(kind, _)| *kind == ConstraintKind::Colinear)); .any(|(kind, _)| *kind == ConstraintKind::Colinear));
} }
@@ -1315,4 +1523,16 @@ mod tests {
assert!(!scene.is_parametric_constraint_visible(ParametricScope::ModelSpace, geometric)); assert!(!scene.is_parametric_constraint_visible(ParametricScope::ModelSpace, geometric));
assert!(scene.is_parametric_constraint_visible(ParametricScope::ModelSpace, dimensional)); assert!(scene.is_parametric_constraint_visible(ParametricScope::ModelSpace, dimensional));
} }
#[test]
fn hidden_constraint_stays_hidden_when_related_entity_is_selected() {
let mut scene = super::super::Scene::new();
let scope = ParametricScope::ModelSpace;
let id = scene
.parametric_constraint_set_mut(scope)
.add(ConstraintKind::Horizontal, vec![ParametricRef::whole(h(1))], None);
scene.hidden_parametric_constraints.insert((scope, id));
assert!(!scene.should_display_parametric_constraint(scope, id, true, 2));
}
} }
+215 -3
View File
@@ -1549,6 +1549,44 @@ type SolveResult = (
)>, )>,
); );
fn retained_line_length(entity: &EntityType, segment: Option<usize>) -> Option<f64> {
match (entity, segment) {
(EntityType::Line(line), None) => Some((line.end - line.start).length()),
(EntityType::LwPolyline(polyline), Some(index)) => {
let world = crate::entities::curve::lwpolyline_world_xy(polyline)?;
let a = world.vertices.get(index)?.location;
let b = if let Some(vertex) = world.vertices.get(index + 1) {
vertex.location
} else if world.is_closed {
world.vertices.first()?.location
} else {
return None;
};
Some((b - a).length())
}
(EntityType::Polyline2D(polyline), Some(index)) => {
let a = polyline.vertices.get(index)?.location;
let b = if let Some(vertex) = polyline.vertices.get(index + 1) {
vertex.location
} else if polyline.is_closed() {
polyline.vertices.first()?.location
} else {
return None;
};
Some((b - a).length())
}
_ => None,
}
}
fn retained_radius(entity: &EntityType) -> Option<f64> {
match entity {
EntityType::Circle(circle) => Some(circle.radius),
EntityType::Arc(arc) => Some(arc.radius),
_ => None,
}
}
/// Rebuilds `set`'s entire `cadkernel_constraints::System` from current document /// Rebuilds `set`'s entire `cadkernel_constraints::System` from current document
/// geometry, solves it, and returns the resulting entity states for every /// geometry, solves it, and returns the resulting entity states for every
/// handle whose registered coordinates actually moved beyond floating-point /// handle whose registered coordinates actually moved beyond floating-point
@@ -1564,6 +1602,9 @@ fn solve_scope(
document: &acadrust::CadDocument, document: &acadrust::CadDocument,
drawing_params: &ParameterTable, drawing_params: &ParameterTable,
set: &ParametricConstraintSet, set: &ParametricConstraintSet,
driven_refs: &[ParametricRef],
retain_size: bool,
retained_before: &HashMap<Handle, std::sync::Arc<EntityType>>,
) -> Option<SolveResult> { ) -> Option<SolveResult> {
let params = if set.local_parameters.is_empty() { let params = if set.local_parameters.is_empty() {
drawing_params drawing_params
@@ -1597,6 +1638,81 @@ fn solve_scope(
return None; return None;
} }
// Preserve current line lengths and radii for this solve when
// CONSTRAINTSOLVEMODE requests it. These temporary equations guide the
// solution without becoming persistent dimensional constraints.
if retain_size {
for (handle, geom) in &cache {
match geom {
EntityGeom::Line(line) => {
let current_length = {
let store = sys.store();
let dx = store.get(line.p2.x) - store.get(line.p1.x);
let dy = store.get(line.p2.y) - store.get(line.p1.y);
(dx * dx + dy * dy).sqrt()
};
let length = retained_before
.get(handle)
.and_then(|entity| retained_line_length(entity, None))
.unwrap_or(current_length);
let target = sys.add_param(length, true);
sys.add_constraint(Rc::new(P2PDistance::new(line.p1, line.p2, target)));
}
EntityGeom::Polyline {
points,
straight,
closed,
} => {
for index in 0..straight.len() {
if !straight[index] {
continue;
}
let Some(a) = points.get(index).copied() else {
continue;
};
let b = if let Some(point) = points.get(index + 1).copied() {
Some(point)
} else if *closed {
points.first().copied()
} else {
None
};
let Some(b) = b else { continue };
let current_length = {
let store = sys.store();
let dx = store.get(b.x) - store.get(a.x);
let dy = store.get(b.y) - store.get(a.y);
(dx * dx + dy * dy).sqrt()
};
let length = retained_before
.get(handle)
.and_then(|entity| retained_line_length(entity, Some(index)))
.unwrap_or(current_length);
let target = sys.add_param(length, true);
sys.add_constraint(Rc::new(P2PDistance::new(a, b, target)));
}
}
EntityGeom::Circle(circle) => {
let radius = retained_before
.get(handle)
.and_then(|entity| retained_radius(entity))
.unwrap_or_else(|| sys.store().get(circle.rad));
let target = sys.add_param(radius, true);
sys.add_constraint(Rc::new(Equal::new(circle.rad, target, 1.0)));
}
EntityGeom::Arc(arc) => {
let radius = retained_before
.get(handle)
.and_then(|entity| retained_radius(entity))
.unwrap_or_else(|| sys.store().get(arc.circle.rad));
let target = sys.add_param(radius, true);
sys.add_constraint(Rc::new(Equal::new(arc.circle.rad, target, 1.0)));
}
_ => {}
}
}
}
// Arc rules (this module's doc comment): keep every registered arc's // Arc rules (this module's doc comment): keep every registered arc's
// `start`/`end` points consistent with its center/radius/angle, // `start`/`end` points consistent with its center/radius/angle,
// unconditionally — the kernel adds these the moment an arc // unconditionally — the kernel adds these the moment an arc
@@ -1633,6 +1749,33 @@ fn solve_scope(
))); )));
} }
// A point explicitly edited by a grip, STRETCH, or Properties is the
// driver's target for this solve. Pin its current coordinates for this
// solve only so constrained neighbours respond while the chosen point
// stays at the exact cursor or typed coordinate.
for reference in driven_refs {
if !set
.constraints
.iter()
.any(|constraint| constraint.refs.iter().any(|r| r.entity == reference.entity))
{
continue;
}
let Some(point) =
resolve_constraint_point(document, &mut sys, &mut cache, *reference)
else {
continue;
};
let (x, y) = {
let store = sys.store();
(store.get(point.x), store.get(point.y))
};
let target_x = sys.add_param(x, true);
let target_y = sys.add_param(y, true);
sys.add_constraint(Rc::new(Equal::new(point.x, target_x, 1.0)));
sys.add_constraint(Rc::new(Equal::new(point.y, target_y, 1.0)));
}
// Ellipse rules (same motivation as arc rules, different mechanism): // Ellipse rules (same motivation as arc rules, different mechanism):
// `cadkernel_constraints::geo::Ellipse` stores `focus1` as an absolute point, not an // `cadkernel_constraints::geo::Ellipse` stores `focus1` as an absolute point, not an
// offset from `center`. If some constraint (e.g. Concentric) pulls only // offset from `center`. If some constraint (e.g. Concentric) pulls only
@@ -2151,9 +2294,43 @@ impl Scene {
/// the resulting `(Handle, ChangeKind::Modified)` entries the same way /// the resulting `(Handle, ChangeKind::Modified)` entries the same way
/// `refresh_associative_dimensions`/`_hatches` do, for `bump_entities` /// `refresh_associative_dimensions`/`_hatches` do, for `bump_entities`
/// to fold into its own `changes` vec. /// to fold into its own `changes` vec.
#[allow(dead_code)]
pub(crate) fn refresh_parametric_constraints( pub(crate) fn refresh_parametric_constraints(
&mut self, &mut self,
changes: &[(Handle, ChangeKind)], changes: &[(Handle, ChangeKind)],
) -> Vec<(Handle, ChangeKind)> {
self.refresh_parametric_constraints_with_policy(changes, &[], false)
}
#[allow(dead_code)]
pub(crate) fn refresh_parametric_constraints_with_driven(
&mut self,
changes: &[(Handle, ChangeKind)],
driven_refs: &[ParametricRef],
) -> Vec<(Handle, ChangeKind)> {
self.refresh_parametric_constraints_with_policy(changes, driven_refs, false)
}
pub(crate) fn refresh_parametric_constraints_with_policy(
&mut self,
changes: &[(Handle, ChangeKind)],
driven_refs: &[ParametricRef],
retain_size: bool,
) -> Vec<(Handle, ChangeKind)> {
self.refresh_parametric_constraints_with_originals(
changes,
driven_refs,
retain_size,
&[],
)
}
pub(crate) fn refresh_parametric_constraints_with_originals(
&mut self,
changes: &[(Handle, ChangeKind)],
driven_refs: &[ParametricRef],
retain_size: bool,
retained_originals: &[(Handle, EntityType)],
) -> Vec<(Handle, ChangeKind)> { ) -> Vec<(Handle, ChangeKind)> {
// An erased entity takes its constraints with it instead of leaving // An erased entity takes its constraints with it instead of leaving
// dangling references. Done first, // dangling references. Done first,
@@ -2185,6 +2362,25 @@ impl Scene {
} }
} }
let retained_before: HashMap<Handle, std::sync::Arc<EntityType>> = if retain_size {
let mut retained: HashMap<Handle, std::sync::Arc<EntityType>> = self
.undo_recording
.as_ref()
.into_iter()
.flat_map(|recording| recording.before.iter())
.filter_map(|(handle, entity)| {
entity
.as_ref()
.map(|entity| (*handle, std::sync::Arc::clone(entity)))
})
.collect();
for (handle, entity) in retained_originals {
retained.insert(*handle, std::sync::Arc::new(entity.clone()));
}
retained
} else {
HashMap::new()
};
let mut result = Vec::new(); let mut result = Vec::new();
for i in 0..self.parametric_constraints.len() { for i in 0..self.parametric_constraints.len() {
let touched = changes.iter().any(|(handle, _)| { let touched = changes.iter().any(|(handle, _)| {
@@ -2200,6 +2396,9 @@ impl Scene {
&self.document, &self.document,
&self.named_parameters, &self.named_parameters,
&self.parametric_constraints[i], &self.parametric_constraints[i],
driven_refs,
retain_size,
&retained_before,
) else { ) else {
continue; continue;
}; };
@@ -2224,10 +2423,17 @@ impl Scene {
pub(crate) fn solve_parametric_constraints_preview( pub(crate) fn solve_parametric_constraints_preview(
&self, &self,
touched: &[Handle], touched: &[Handle],
driven_refs: &[ParametricRef],
retain_size: bool,
retained_originals: &[(Handle, EntityType)],
) -> Vec<(Handle, EntityType)> { ) -> Vec<(Handle, EntityType)> {
if self.parametric_constraints.is_empty() || touched.is_empty() { if self.parametric_constraints.is_empty() || touched.is_empty() {
return Vec::new(); return Vec::new();
} }
let retained_before: HashMap<Handle, std::sync::Arc<EntityType>> = retained_originals
.iter()
.map(|(handle, entity)| (*handle, std::sync::Arc::new(entity.clone())))
.collect();
let mut result = Vec::new(); let mut result = Vec::new();
for set in &self.parametric_constraints { for set in &self.parametric_constraints {
let is_touched = touched let is_touched = touched
@@ -2236,8 +2442,14 @@ impl Scene {
if !is_touched { if !is_touched {
continue; continue;
} }
let Some((solved, _dof, _conflicts)) = let Some((solved, _dof, _conflicts)) = solve_scope(
solve_scope(&self.document, &self.named_parameters, set) &self.document,
&self.named_parameters,
set,
driven_refs,
retain_size,
&retained_before,
)
else { else {
continue; continue;
}; };
@@ -2515,7 +2727,7 @@ mod tests {
} }
let b_before = scene.document.get_entity(b).cloned(); let b_before = scene.document.get_entity(b).cloned();
let solved = scene.solve_parametric_constraints_preview(&[a]); let solved = scene.solve_parametric_constraints_preview(&[a], &[], false, &[]);
assert_eq!( assert_eq!(
scene.document.get_entity(b), scene.document.get_entity(b),
+1
View File
@@ -1007,6 +1007,7 @@ impl Scene {
self.hover_highlight = None; self.hover_highlight = None;
hover_changed = true; hover_changed = true;
} }
hover_changed |= self.constraint_hover_highlights.remove(&h);
self.hatches.remove(&h); self.hatches.remove(&h);
self.images.remove(&h); self.images.remove(&h);
self.meshes.remove(&h); self.meshes.remove(&h);
+4 -1
View File
@@ -3692,7 +3692,10 @@ impl Scene {
&self, &self,
inst: &ViewportInstance, inst: &ViewportInstance,
) -> Arc<Vec<WireModel>> { ) -> Arc<Vec<WireModel>> {
if self.selected.is_empty() && self.hover_highlight.is_none() { if self.selected.is_empty()
&& self.hover_highlight.is_none()
&& self.constraint_hover_highlights.is_empty()
{
return Arc::new(Vec::new()); return Arc::new(Vec::new());
} }
+12
View File
@@ -1017,6 +1017,18 @@ impl canvas::Program<Message> for SelectionCanvas {
} }
} }
} }
if let Some(pos) = cursor.position_in(bounds) {
let glyphs: Vec<_> = self
.constraint_glyphs
.iter()
.map(|(anchor, outward, label, conflict, _)| {
(*anchor, *outward, label.clone(), *conflict)
})
.collect();
if constraint_glyph_hit_test(&glyphs, pos).is_some() {
return mouse::Interaction::Pointer;
}
}
// The resize cursor over a divider is supplied by the input pane_grid // The resize cursor over a divider is supplied by the input pane_grid
// layered above this overlay; `draw` only suppresses the CAD crosshair // layered above this overlay; `draw` only suppresses the CAD crosshair
// there (see `divider_under`). // there (see `divider_under`).
+143
View File
@@ -0,0 +1,143 @@
use crate::app::settings::AutoConstrainSettings;
use crate::app::Message;
use iced::widget::{button, checkbox, column, container, row, scrollable, text, text_input, Space};
use iced::{Element, Fill};
pub fn view_window<'a>(
settings: &'a AutoConstrainSettings,
selected_row: usize,
distance_input: &'a str,
angle_input: &'a str,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let heading = row![
text(crate::t!("Priority")).width(70),
text(crate::t!("Constraint Type")).width(Fill),
text(crate::t!("Apply")).width(60),
]
.spacing(8);
let mut list = column![heading].spacing(3);
for (index, kind) in settings.priority.iter().copied().enumerate() {
let enabled = settings.enabled.contains(&kind);
let label = row![
text(format!("{}", index + 1)).width(70),
text(crate::t!(kind.label())).width(Fill),
checkbox(enabled)
.on_toggle(move |_| Message::AutoConstrainToggleKind(kind))
.width(60),
]
.spacing(8)
.align_y(iced::Center);
list = list.push(
button(label)
.on_press(Message::AutoConstrainSelectRow(index))
.style(if selected_row == index {
button::primary
} else {
button::text
})
.width(Fill)
.padding([5, 8]),
);
}
let reorder = column![
button(text(crate::t!("Move Up")))
.on_press_maybe((selected_row > 0).then_some(Message::AutoConstrainMoveUp))
.width(Fill),
button(text(crate::t!("Move Down")))
.on_press_maybe(
(selected_row + 1 < settings.priority.len())
.then_some(Message::AutoConstrainMoveDown)
)
.width(Fill),
Space::new().height(8),
button(text(crate::t!("Select All")))
.on_press(Message::AutoConstrainSelectAll)
.width(Fill),
button(text(crate::t!("Clear All")))
.on_press(Message::AutoConstrainClearAll)
.width(Fill),
button(text(crate::t!("Reset")))
.on_press(Message::AutoConstrainReset)
.width(Fill),
]
.spacing(6)
.width(125);
let types = row![
container(scrollable(list).height(260))
.padding(6)
.width(Fill),
reorder
]
.spacing(12);
let conditions = column![
checkbox(settings.tangent_must_share_point)
.label(crate::t!("Tangent objects must share an intersection point"))
.on_toggle(|_| Message::AutoConstrainToggleTangentPoint),
checkbox(settings.perpendicular_must_intersect)
.label(crate::t!("Perpendicular objects must share an intersection point"))
.on_toggle(|_| Message::AutoConstrainTogglePerpendicularIntersection),
]
.spacing(8);
let tolerances = column![
text(crate::t!("Tolerances")).size(14),
row![
text(crate::t!("Distance")).width(130),
text_input("0.05", distance_input)
.on_input(Message::AutoConstrainDistanceChanged)
.width(150),
]
.align_y(iced::Center),
row![
text(crate::t!("Angle")).width(130),
text_input("1", angle_input)
.on_input(Message::AutoConstrainAngleChanged)
.width(150),
text("°"),
]
.align_y(iced::Center),
]
.spacing(7);
let actions = row![
Space::new().width(Fill),
button(text(crate::t!("OK")))
.on_press(Message::AutoConstrainOk)
.style(button::primary)
.padding([6, 18]),
button(text(crate::t!("Apply")))
.on_press(Message::AutoConstrainApply)
.style(button::secondary)
.padding([6, 18]),
button(text(crate::t!("Cancel")))
.on_press(Message::AutoConstrainCancel)
.style(button::secondary)
.padding([6, 18]),
]
.spacing(8)
.align_y(iced::Center);
container(
column![
text(crate::t!("Auto Constrain")).size(16),
types,
conditions,
tolerances,
Space::new().height(Fill),
actions,
]
.spacing(14)
.width(sizing.width)
.height(sizing.height),
)
.padding([14, 16])
.width(sizing.width)
.height(sizing.height)
.style(container::rounded_box)
.into()
}
+1
View File
@@ -5,6 +5,7 @@ pub mod layer_state_manager;
pub mod drawing_units; pub mod drawing_units;
pub mod geometric_tolerance; pub mod geometric_tolerance;
pub mod drafting_settings; pub mod drafting_settings;
pub mod auto_constrain_settings;
pub mod layer_translator; pub mod layer_translator;
pub mod plot; pub mod plot;
pub mod print_all; pub mod print_all;