forked from 1b-agent/opencadstudio-upstream
Complete automatic constraint behavior
This commit is contained in:
1 parent
d738ebfcc6
commit
66be530655
25 files changed
+1494
-102
No files matched your search
+179
-12
@@ -1592,6 +1592,92 @@ impl OpenCADStudio {
|
||||
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> {
|
||||
let i = self.active_tab;
|
||||
if let Some(bind) = self.tabs[i]
|
||||
@@ -1613,6 +1699,15 @@ impl OpenCADStudio {
|
||||
.as_ref()
|
||||
.is_some_and(|command| command.preserve_commit_layer());
|
||||
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 => {
|
||||
// ATTEDIT finished its entity pick: hand the chosen block off to
|
||||
// the attribute editor dialog and end the command (open_attribute
|
||||
@@ -1720,6 +1815,9 @@ impl OpenCADStudio {
|
||||
.attach_dimension_association(handle, sources);
|
||||
}
|
||||
}
|
||||
if let Some(handle) = committed {
|
||||
self.apply_continuous_constraints(i, &[handle]);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||
if let Some(p) = prompt {
|
||||
@@ -1760,15 +1858,18 @@ impl OpenCADStudio {
|
||||
}
|
||||
self.refresh_layer_panel();
|
||||
}
|
||||
let mut committed = Vec::new();
|
||||
for entity in entities {
|
||||
if preserve_commit_style {
|
||||
let _ = self.commit_entity_handle_preserve_style(entity);
|
||||
let handle = if preserve_commit_style {
|
||||
self.commit_entity_handle_preserve_style(entity)
|
||||
} else if preserve_commit_layer {
|
||||
let _ = self.commit_entity_handle_preserve_layer(entity);
|
||||
self.commit_entity_handle_preserve_layer(entity)
|
||||
} 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].scene.clear_preview_wire();
|
||||
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||
@@ -1799,15 +1900,18 @@ impl OpenCADStudio {
|
||||
}
|
||||
self.refresh_layer_panel();
|
||||
}
|
||||
let mut committed = Vec::new();
|
||||
for entity in entities {
|
||||
if preserve_commit_style {
|
||||
let _ = self.commit_entity_handle_preserve_style(entity);
|
||||
let handle = if preserve_commit_style {
|
||||
self.commit_entity_handle_preserve_style(entity)
|
||||
} else if preserve_commit_layer {
|
||||
let _ = self.commit_entity_handle_preserve_layer(entity);
|
||||
self.commit_entity_handle_preserve_layer(entity)
|
||||
} 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].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
@@ -2063,6 +2167,7 @@ impl OpenCADStudio {
|
||||
// transform_entities — always delta-safe.
|
||||
let pending = self.begin_undo(i, label, handles.len(), true);
|
||||
self.tabs[i].scene.transform_entities(&handles, &transform);
|
||||
self.apply_inferred_constraints(i, &handles);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
@@ -2181,6 +2286,9 @@ impl OpenCADStudio {
|
||||
.attach_dimension_association(handle, sources);
|
||||
}
|
||||
}
|
||||
if let Some(handle) = committed {
|
||||
self.apply_continuous_constraints(i, &[handle]);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
@@ -2910,6 +3018,7 @@ impl OpenCADStudio {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.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(
|
||||
kind,
|
||||
refs,
|
||||
@@ -2919,7 +3028,11 @@ impl OpenCADStudio {
|
||||
.into_iter()
|
||||
.map(|h| (h, crate::scene::ChangeKind::Modified))
|
||||
.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].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
@@ -4467,6 +4580,7 @@ impl OpenCADStudio {
|
||||
let pending = self.begin_undo(i, "STRETCH", handles.len(), !structural);
|
||||
let mut count = 0usize;
|
||||
let mut changed_handles = Vec::new();
|
||||
let mut driven_refs = Vec::new();
|
||||
|
||||
// Helper: is DXF point (x, y) inside the world-space window?
|
||||
// Drawing plane is world XY (= DXF XY).
|
||||
@@ -4485,6 +4599,54 @@ impl OpenCADStudio {
|
||||
let mut stretched_dims: Vec<acadrust::Handle> = Vec::new();
|
||||
for handle in &handles {
|
||||
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 {
|
||||
continue;
|
||||
};
|
||||
@@ -4707,7 +4869,12 @@ impl OpenCADStudio {
|
||||
.iter()
|
||||
.map(|&handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.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].active_cmd = None;
|
||||
|
||||
+66
-35
@@ -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" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
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.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else {
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let inferred = self.tabs[i]
|
||||
.scene
|
||||
.inferred_parametric_constraints(scope, &handles);
|
||||
.inferred_parametric_constraints(
|
||||
scope,
|
||||
&handles,
|
||||
&self.auto_constrain_settings,
|
||||
);
|
||||
if inferred.is_empty() {
|
||||
self.command_line
|
||||
.push_output("No supported geometric relations were found.");
|
||||
return None;
|
||||
}
|
||||
let before = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
crate::scene::parametric_constraints::ParametricConstraintSet::new(
|
||||
scope,
|
||||
self.command_line.push_output(
|
||||
format!(
|
||||
"0 constraint(s) applied to {} object(s).",
|
||||
handles.len()
|
||||
)
|
||||
});
|
||||
let pending = self.begin_undo(i, "Auto constrain", handles.len(), true);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, before);
|
||||
let count = inferred.len();
|
||||
for (kind, refs) in inferred {
|
||||
.as_str(),
|
||||
);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.refresh_selected_grips();
|
||||
} else {
|
||||
let before = self.tabs[i]
|
||||
.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]
|
||||
.scene
|
||||
.parametric_constraint_set_mut(scope)
|
||||
.add(kind, refs, None);
|
||||
}
|
||||
let changes: Vec<_> = handles
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
self.command_line
|
||||
.push_output(format!("{} constraint(s) applied.", count).as_str());
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
.record_undo_parametric_constraints_before(scope, before);
|
||||
let count = inferred.len();
|
||||
for (kind, refs) in inferred {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set_mut(scope)
|
||||
.add(kind, refs, None);
|
||||
}
|
||||
let changes: Vec<_> = handles
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities_with_parametric_policy(
|
||||
&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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1046,6 +1046,9 @@ impl OpenCADStudio {
|
||||
| "GRIPHOT"
|
||||
| "GRIPHOVER"
|
||||
| "GRIPOBJLIMIT"
|
||||
| "CONSTRAINTSOLVEMODE"
|
||||
| "CONSTRAINTINFER"
|
||||
| "CONSTRAINTBARDISPLAY"
|
||||
) =>
|
||||
{
|
||||
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
||||
@@ -1068,7 +1071,7 @@ impl OpenCADStudio {
|
||||
let value = it.next().map(|s| s.trim().to_string());
|
||||
if name.is_empty() || name == "?" {
|
||||
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 {
|
||||
if name == "CETRANSPARENCY" {
|
||||
@@ -1262,6 +1265,50 @@ impl OpenCADStudio {
|
||||
}
|
||||
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") {
|
||||
let current = if name == "DONUTID" {
|
||||
crate::modules::draw::defaults::get_donut_inner_diameter()
|
||||
|
||||
@@ -556,6 +556,14 @@ pub(super) struct OpenCADStudio {
|
||||
/// glyph plus a driven value or named-parameter name. When false, every
|
||||
/// pill shows only the glyph.
|
||||
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);
|
||||
/// 0 disables autosave.
|
||||
pub savetime_min: i32,
|
||||
@@ -1722,6 +1730,7 @@ pub enum ModalKind {
|
||||
DrawingUnits,
|
||||
GeometricTolerance,
|
||||
DraftingSettings,
|
||||
AutoConstrainSettings,
|
||||
LayerStateEditor,
|
||||
Plot,
|
||||
PrintAll,
|
||||
@@ -2640,6 +2649,20 @@ pub enum Message {
|
||||
DraftingSettingsClose,
|
||||
DraftingSettingsCloseDiscard,
|
||||
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.
|
||||
ToggleRibbonDropdown(String),
|
||||
/// Toggle a collapsed ribbon panel's flyout open/closed (by panel title).
|
||||
@@ -3631,6 +3654,14 @@ impl OpenCADStudio {
|
||||
backup_on_save: true,
|
||||
file_assoc_enabled: 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,
|
||||
default_bg_color: None,
|
||||
default_paper_bg_color: None,
|
||||
|
||||
+27
-1
@@ -2772,6 +2772,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]) {
|
||||
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;
|
||||
for &handle in handles {
|
||||
// A dimension is drawn from the block holding its picture, and that
|
||||
@@ -2801,7 +2820,14 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
|
||||
.iter()
|
||||
.map(|&handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.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
|
||||
|
||||
@@ -11,6 +11,103 @@
|
||||
use crate::snap::SnapType;
|
||||
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(),
|
||||
enabled: AutoConstraintKind::ALL.to_vec(),
|
||||
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.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CursorType {
|
||||
@@ -289,6 +386,19 @@ pub struct UserSettings {
|
||||
/// canvas detail on a dense sketch.
|
||||
#[serde(default = "default_show_constraint_values")]
|
||||
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).
|
||||
/// 0 disables autosave.
|
||||
pub savetime_min: i32,
|
||||
@@ -359,6 +469,14 @@ fn default_show_constraint_values() -> bool {
|
||||
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>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
@@ -415,6 +533,10 @@ impl Default for UserSettings {
|
||||
backup_on_save: true,
|
||||
file_assoc_enabled: true,
|
||||
show_constraint_values: true,
|
||||
auto_constrain: AutoConstrainSettings::default(),
|
||||
constraint_solve_mode: true,
|
||||
constraint_infer: false,
|
||||
constraint_bar_display: 3,
|
||||
savetime_min: 10,
|
||||
default_save_format: crate::io::DEFAULT_SAVE_FORMAT.to_string(),
|
||||
pick_add: true,
|
||||
|
||||
@@ -3247,6 +3247,29 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
self.tabs[i].properties.active_field = None;
|
||||
let handles = self.property_target_handles(i);
|
||||
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]
|
||||
.properties
|
||||
.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.refresh_properties();
|
||||
}
|
||||
|
||||
@@ -429,6 +429,10 @@ impl OpenCADStudio {
|
||||
backup_on_save: self.backup_on_save,
|
||||
file_assoc_enabled: self.file_assoc_enabled,
|
||||
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,
|
||||
default_save_format: self.default_save_format.clone(),
|
||||
pick_add: self.pick_add,
|
||||
@@ -510,6 +514,15 @@ impl OpenCADStudio {
|
||||
self.backup_on_save = s.backup_on_save;
|
||||
self.file_assoc_enabled = s.file_assoc_enabled;
|
||||
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.default_save_format =
|
||||
crate::io::canonical_save_format(&s.default_save_format).to_string();
|
||||
|
||||
@@ -4289,6 +4289,112 @@ impl OpenCADStudio {
|
||||
self.drafting_settings_close_confirm = false;
|
||||
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 ──────────────────────────────────────────
|
||||
Message::ToggleRibbonDropdown(id) => {
|
||||
@@ -6847,6 +6953,11 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
}
|
||||
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);
|
||||
self.close_active_modal();
|
||||
if resume_open_queue {
|
||||
|
||||
+134
-1
@@ -933,6 +933,13 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
}
|
||||
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
|
||||
// drafting constraints are read, not merely before the final callback.
|
||||
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
|
||||
// their original state in the gesture's undo record.
|
||||
let solved_by_constraints = self.tabs[i]
|
||||
.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 {
|
||||
let handle = *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
|
||||
// ribbon / panels.
|
||||
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
|
||||
// when an upper overlay (the right-click menu panel) takes
|
||||
// the cursor, so clearing the menu state on every exit
|
||||
@@ -2949,6 +2977,94 @@ impl OpenCADStudio {
|
||||
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 (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let set = self.tabs[i].scene.parametric_constraint_set(scope)?;
|
||||
let edit_frame = self.tabs[i].scene.viewport_edit_frame((vw, vh));
|
||||
let bounds = match &edit_frame {
|
||||
Some((_, full)) => *full,
|
||||
None => self.tabs[i].scene.active_model_tile_bounds(vw, vh),
|
||||
};
|
||||
let (view_rot, eye) = if let Some((camera, _)) = &edit_frame {
|
||||
(camera.view_proj_rte(bounds), camera.eye())
|
||||
} else {
|
||||
let camera = self.tabs[i].scene.camera.borrow();
|
||||
(camera.view_proj_rte(bounds), camera.eye())
|
||||
};
|
||||
let mut ids = Vec::new();
|
||||
let mut glyphs = Vec::new();
|
||||
for constraint in set.constraints.iter().filter(|constraint| {
|
||||
let selected = constraint.refs.iter().any(|reference| {
|
||||
self.tabs[i].scene.selected.contains(&reference.entity)
|
||||
});
|
||||
constraint.enabled
|
||||
&& self.tabs[i].scene.should_display_parametric_constraint(
|
||||
scope,
|
||||
constraint.id,
|
||||
selected,
|
||||
self.constraint_bar_display,
|
||||
)
|
||||
}) {
|
||||
let Some((anchor, outward)) =
|
||||
crate::scene::parametric_constraints::glyph_placement(
|
||||
&self.tabs[i].scene.document,
|
||||
constraint,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let Some(screen) = crate::scene::pick::grip::project_rte(
|
||||
glam::DVec3::new(anchor.x, anchor.y, anchor.z),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let Some(outward_screen) = crate::scene::pick::grip::project_rte(
|
||||
glam::DVec3::new(
|
||||
anchor.x + outward.x,
|
||||
anchor.y + outward.y,
|
||||
anchor.z + outward.z,
|
||||
),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let direction = (outward_screen - screen).normalize_or(glam::Vec2::NEG_Y);
|
||||
let point = iced::Point::new(bounds.x + screen.x, bounds.y + screen.y);
|
||||
let label = if self.show_constraint_values {
|
||||
crate::scene::parametric_constraints::glyph_label(constraint)
|
||||
} else {
|
||||
constraint.kind.glyph_symbol().to_string()
|
||||
};
|
||||
let conflict = set
|
||||
.conflicts
|
||||
.iter()
|
||||
.any(|(id, _)| *id == constraint.id);
|
||||
ids.push(constraint.id);
|
||||
glyphs.push((point, direction.to_array(), label, conflict));
|
||||
}
|
||||
let index = crate::ui::overlay::constraint_glyph_hit(&glyphs, cursor)?;
|
||||
let constraint = set.get(ids[index])?;
|
||||
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> {
|
||||
let i = self.active_tab;
|
||||
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
|
||||
@@ -3004,6 +3120,23 @@ impl OpenCADStudio {
|
||||
};
|
||||
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
|
||||
// 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
|
||||
|
||||
+11
-1
@@ -835,7 +835,17 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
set.constraints
|
||||
.iter()
|
||||
.filter(|c| c.enabled)
|
||||
.filter(|c| tab.scene.is_parametric_constraint_visible(scope, c.id))
|
||||
.filter(|c| {
|
||||
let selected = c.refs.iter().any(|reference| {
|
||||
tab.scene.selected.contains(&reference.entity)
|
||||
});
|
||||
tab.scene.should_display_parametric_constraint(
|
||||
scope,
|
||||
c.id,
|
||||
selected,
|
||||
self.constraint_bar_display,
|
||||
)
|
||||
})
|
||||
.filter_map(|c| {
|
||||
let (anchor, outward) =
|
||||
crate::scene::parametric_constraints::glyph_placement(
|
||||
|
||||
@@ -25,6 +25,7 @@ impl OpenCADStudio {
|
||||
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
|
||||
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").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::Plot) => crate::tr!("modal", "plot"),
|
||||
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| {
|
||||
crate::ui::window::find_replace::view_window(
|
||||
&self.find_replace.search,
|
||||
|
||||
Reference in new issue
Block a user