Merge upstream main into Coincident parity

This commit is contained in:
ramox81 committed 2026-09-15 14:39:17 +03:00
commit f6d8209139
44 files changed
+11089 -884

No files matched your search

+179 -12
View File
@@ -1617,6 +1617,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]
@@ -1638,6 +1724,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
@@ -1745,6 +1840,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 {
@@ -1785,15 +1883,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());
@@ -1824,15 +1925,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;
@@ -2095,6 +2199,7 @@ impl OpenCADStudio {
// transforms retain their selected-object behavior.
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;
@@ -2213,6 +2318,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;
@@ -2942,6 +3050,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,
@@ -2951,7 +3060,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;
@@ -4620,6 +4733,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).
@@ -4638,6 +4752,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;
};
@@ -4860,7 +5022,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
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" => {
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);
}
}
}
}
+48 -1
View File
@@ -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()
+31
View File
@@ -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).
@@ -3633,6 +3656,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
View File
@@ -2753,6 +2753,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
@@ -2782,7 +2801,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
+127
View File
@@ -11,6 +11,108 @@
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(),
// Keep Equal available without creating redundant relations
// between equal-length segments by default.
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.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum CursorType {
@@ -289,6 +391,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 +474,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 +538,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,
+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;
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();
}
+177 -384
View File
@@ -86,33 +86,7 @@ fn native_paths_match(left: &std::path::Path, right: &std::path::Path) -> bool {
}
}
type LayoutPlotParams = (
std::sync::Arc<Vec<crate::io::pdf_export::PlotWire>>,
Vec<crate::scene::model::hatch_model::HatchModel>,
Vec<crate::scene::model::hatch_model::HatchModel>,
crate::io::pdf_export::PlotGroupSplits,
f64,
f64,
f64,
f64,
i32,
f32,
Option<(f32, f32, f32, f32)>,
);
type ClippedPlotParams = (
Vec<crate::io::pdf_export::PlotWire>,
Vec<crate::scene::model::hatch_model::HatchModel>,
Vec<crate::scene::model::hatch_model::HatchModel>,
crate::io::pdf_export::PlotGroupSplits,
f64,
f64,
f64,
f64,
i32,
f32,
Option<(f32, f32, f32, f32)>,
);
use crate::io::pdf_export::{PdfPageInput, PlotContent};
fn plot_dialog_sheet_mm(d: &crate::ui::window::plot::PlotDialogState) -> (f64, f64) {
use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize};
@@ -143,11 +117,8 @@ fn plot_dialog_sheet_mm(d: &crate::ui::window::plot::PlotDialogState) -> (f64, f
}
}
fn plot_content_extents(
wires: &[crate::io::pdf_export::PlotWire],
hatches: &[crate::scene::model::hatch_model::HatchModel],
wipeouts: &[crate::scene::model::hatch_model::HatchModel],
) -> Option<(f64, f64, f64, f64)> {
fn plot_content_extents(content: &PlotContent) -> Option<(f64, f64, f64, f64)> {
let PlotContent { wires, hatches, wipeouts, images, .. } = content;
let mut bounds = (
f64::INFINITY,
f64::INFINITY,
@@ -162,7 +133,7 @@ fn plot_content_extents(
bounds.3 = bounds.3.max(y);
}
};
for wire in wires {
for wire in wires.iter().filter(|wire| wire.name != "paper_printable_area") {
let [x0, y0, x1, y1] = wire.aabb;
include(x0 as f64, y0 as f64);
include(x1 as f64, y1 as f64);
@@ -175,6 +146,29 @@ fn plot_content_extents(
);
}
}
for plot in images {
let mut image_bounds = [f64::INFINITY, f64::INFINITY,
f64::NEG_INFINITY, f64::NEG_INFINITY];
for vertex in &plot.image.verts {
for axis in 0..2 {
let value = vertex.pos[axis] as f64 + vertex.pos_low[axis] as f64;
image_bounds[axis] = image_bounds[axis].min(value);
image_bounds[axis + 2] = image_bounds[axis + 2].max(value);
}
}
for clip in &plot.clips {
for axis in 0..2 {
let min = clip.iter().map(|p| p[axis]).fold(f64::INFINITY, f64::min);
let max = clip.iter().map(|p| p[axis]).fold(f64::NEG_INFINITY, f64::max);
image_bounds[axis] = image_bounds[axis].max(min);
image_bounds[axis + 2] = image_bounds[axis + 2].min(max);
}
}
if image_bounds[0] < image_bounds[2] && image_bounds[1] < image_bounds[3] {
include(image_bounds[0], image_bounds[1]);
include(image_bounds[2], image_bounds[3]);
}
}
(bounds.0.is_finite()
&& bounds.1.is_finite()
&& bounds.2.is_finite()
@@ -188,12 +182,7 @@ fn plot_scene_content(
scene: &crate::scene::Scene,
paper_space_last: bool,
render_mode_override: Option<acadrust::entities::ViewportRenderMode>,
) -> (
std::sync::Arc<Vec<crate::io::pdf_export::PlotWire>>,
Vec<crate::scene::model::hatch_model::HatchModel>,
Vec<crate::scene::model::hatch_model::HatchModel>,
crate::io::pdf_export::PlotGroupSplits,
) {
) -> PlotContent {
let (mut paper_wires, mut model_wires) = scene.plot_wire_groups(render_mode_override);
let plot_viewport_borders = scene
.effective_plot_settings()
@@ -227,56 +216,66 @@ fn plot_scene_content(
let model_wires = with_depth(model_wires);
let paper_hatches = scene.paper_plot_hatches().as_ref().clone();
let paper_wipeouts = scene.paper_plot_wipeouts().as_ref().clone();
let paper_images = scene.paper_plot_images();
if scene.current_layout == "Model" {
let splits = crate::io::pdf_export::PlotGroupSplits {
wires: paper_wires.len(),
hatches: paper_hatches.len(),
wipeouts: paper_wipeouts.len(),
images: paper_images.len(),
};
return PlotContent {
wires: std::sync::Arc::new(paper_wires),
hatches: paper_hatches,
wipeouts: paper_wipeouts,
images: paper_images,
group_splits: splits,
};
return (
std::sync::Arc::new(paper_wires),
paper_hatches,
paper_wipeouts,
splits,
);
}
let (mut model_pattern_wires, model_hatches, model_wipeouts) = scene.viewport_plot_fills();
let (mut model_pattern_wires, model_hatches, model_wipeouts, model_images) =
scene.viewport_plot_fills();
model_pattern_wires.retain(|(wire, _)| wire.plot_visible);
let model_pattern_wires = model_pattern_wires
.into_iter()
.map(|(wire, draw_depth)| crate::io::pdf_export::PlotWire { wire, draw_depth })
.collect::<Vec<_>>();
let (wires, hatches, wipeouts, splits) = if paper_space_last {
let (wires, hatches, wipeouts, images, splits) = if paper_space_last {
let splits = crate::io::pdf_export::PlotGroupSplits {
wires: model_wires.len() + model_pattern_wires.len(),
hatches: model_hatches.len(),
wipeouts: model_wipeouts.len(),
images: model_images.len(),
};
let mut wires = model_wires;
let mut hatches = model_hatches;
let mut wipeouts = model_wipeouts;
let mut images = model_images;
wires.extend(model_pattern_wires);
wires.extend(paper_wires);
hatches.extend(paper_hatches);
wipeouts.extend(paper_wipeouts);
(wires, hatches, wipeouts, splits)
images.extend(paper_images);
(wires, hatches, wipeouts, images, splits)
} else {
let splits = crate::io::pdf_export::PlotGroupSplits {
wires: paper_wires.len(),
hatches: paper_hatches.len(),
wipeouts: paper_wipeouts.len(),
images: paper_images.len(),
};
let mut wires = paper_wires;
let mut hatches = paper_hatches;
let mut wipeouts = paper_wipeouts;
let mut images = paper_images;
wires.extend(model_wires);
wires.extend(model_pattern_wires);
hatches.extend(model_hatches);
wipeouts.extend(model_wipeouts);
(wires, hatches, wipeouts, splits)
images.extend(model_images);
(wires, hatches, wipeouts, images, splits)
};
(std::sync::Arc::new(wires), hatches, wipeouts, splits)
PlotContent { wires: std::sync::Arc::new(wires), hatches, wipeouts, images, group_splits: splits }
}
impl OpenCADStudio {
@@ -429,6 +428,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 +513,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();
@@ -2951,46 +2963,18 @@ impl OpenCADStudio {
self.load_plotsettings_into_dialog(&settings);
}
}
let Some((
wires,
hatches,
wipeouts,
group_splits,
page_w,
page_h,
ox,
oy,
rotation,
scale,
clip,
)) = self.direct_plot_params()
let Some(page) = self.direct_plot_page()
else {
self.command_line.push_error(
crate::t!("Nothing to plot: model space contains no printable geometry.").as_ref(),
);
return Task::none();
};
let plot_style = self.dialog_plot_style(&self.plot_dialog);
let render_options = Self::pdf_plot_options(&self.plot_dialog, group_splits);
let worker_path = path.clone();
let work = move || {
crate::io::pdf_export::export_pdf(
&wires,
&hatches,
&wipeouts,
page_w,
page_h,
ox,
oy,
rotation,
scale,
clip,
&worker_path,
plot_style.as_ref(),
render_options,
)
.map(|_| crate::tf!("Exported: {}", worker_path.display()).into_owned())
.map_err(|e| crate::tf!("Export failed: {e}").into_owned())
crate::io::pdf_export::export_pdf(&page, &worker_path)
.map(|_| crate::tf!("Exported: {}", worker_path.display()).into_owned())
.map_err(|e| crate::tf!("Export failed: {e}").into_owned())
};
self.run_plot_work(self.plot_dialog.background, false, work)
}
@@ -3006,55 +2990,27 @@ impl OpenCADStudio {
"Extents" => self.extents_plot_job(),
_ => self.window_plot_job(),
};
let Some((
wires,
hatches,
wipeouts,
group_splits,
page_w,
page_h,
ox,
oy,
rotation,
scale,
clip,
)) = job
let Some(page) = job
else {
self.command_line
.push_error(crate::t!("Plot area is empty. Pick a larger window.").as_ref());
return Task::none();
};
let plot_style = self.dialog_plot_style(&self.plot_dialog);
let render_options = Self::pdf_plot_options(&self.plot_dialog, group_splits);
let worker_path = path.clone();
self.close_active_modal();
let work = move || {
crate::io::pdf_export::export_pdf(
&wires,
&hatches,
&wipeouts,
page_w,
page_h,
ox,
oy,
rotation,
scale,
clip,
&worker_path,
plot_style.as_ref(),
render_options,
)
.map(|_| {
crate::tf!(
"Plotted window to {}",
worker_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
)
.into_owned()
})
.map_err(|e| crate::tf!("Plot failed: {e}").into_owned())
crate::io::pdf_export::export_pdf(&page, &worker_path)
.map(|_| {
crate::tf!(
"Plotted window to {}",
worker_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
)
.into_owned()
})
.map_err(|e| crate::tf!("Plot failed: {e}").into_owned())
};
self.run_plot_work(self.plot_dialog.background, false, work)
}
@@ -3182,8 +3138,7 @@ impl OpenCADStudio {
)
.into_owned());
}
let plot_style = self.dialog_plot_style(&dialog);
let params = match dialog.area.as_str() {
let area_page = match dialog.area.as_str() {
"Display" => self.display_plot_job(),
"Extents" => self.extents_plot_job(),
"Limits" => self.limits_plot_job(),
@@ -3193,64 +3148,14 @@ impl OpenCADStudio {
}
_ => None,
};
let (
wires,
hatches,
wipeouts,
group_splits,
paper_w,
paper_h,
offset_x,
offset_y,
rotation_deg,
scale,
clip,
) = if dialog.area == "Layout" {
self.layout_plot_params_for("Layout")
let page = if dialog.area == "Layout" {
self.layout_plot_page_for("Layout")
} else {
let (
wires,
hatches,
wipeouts,
group_splits,
paper_w,
paper_h,
offset_x,
offset_y,
rotation_deg,
scale,
clip,
) = params.ok_or_else(|| {
area_page.ok_or_else(|| {
crate::tf!("Layout '{name}' plot area is empty.").into_owned()
})?;
(
std::sync::Arc::new(wires),
hatches,
wipeouts,
group_splits,
paper_w,
paper_h,
offset_x,
offset_y,
rotation_deg,
scale,
clip,
)
})?
};
pages.push(crate::io::pdf_export::PdfPageInput {
wires,
hatches,
wipeouts,
paper_w,
paper_h,
offset_x,
offset_y,
rotation_deg,
scale,
clip,
options: Self::pdf_plot_options(&dialog, group_splits),
plot_style,
});
pages.push(page);
}
Ok(pages)
})();
@@ -3328,7 +3233,7 @@ impl OpenCADStudio {
return Task::none();
}
};
let options = self.plot_print_options(&dialog, Default::default());
let options = self.plot_print_options(&dialog);
let temp_path = crate::io::print_to_printer::temp_pdf_path("print_all");
self.save_config();
self.close_active_modal();
@@ -3375,41 +3280,31 @@ impl OpenCADStudio {
}
}
/// Build the render inputs and page geometry for a full-layout plot: wires
/// plus hatch / wipeout fills, the effective (rotation-swapped) sheet size,
/// draw-origin offset, rotation, unit scale, and optional clip. Shared by
/// PDF export, preview, and printer output so all three render identically.
pub(super) fn layout_plot_params(&self) -> LayoutPlotParams {
self.layout_plot_params_for(&self.plot_dialog.area)
/// Assemble a layout page for PDF export, preview, or printing.
pub(super) fn layout_plot_page(&self) -> PdfPageInput {
self.layout_plot_page_for(&self.plot_dialog.area)
}
fn layout_plot_params_for(&self, plot_area: &str) -> LayoutPlotParams {
fn layout_plot_page_for(&self, plot_area: &str) -> PdfPageInput {
let i = self.active_tab;
let scene = &self.tabs[i].scene;
let paper_space = scene.current_layout != "Model";
let selected_sheet = plot_dialog_sheet_mm(&self.plot_dialog);
let (source_wires, hatches, wipeouts, mut group_splits) = plot_scene_content(
let mut content = plot_scene_content(
scene,
self.plot_dialog.paperspace_last,
plot_render_mode_override(&self.plot_dialog),
);
// The printable-area rectangle is an on-screen guide, not drawing
// content. It used to leak into every paper-space PDF/preview/print.
let wires = if paper_space {
group_splits.wires = source_wires[..group_splits.wires.min(source_wires.len())]
if paper_space {
content.group_splits.wires = content.wires[..content.group_splits.wires.min(content.wires.len())]
.iter()
.filter(|wire| wire.name != "paper_printable_area")
.count();
std::sync::Arc::new(
source_wires
.iter()
.filter(|wire| wire.name != "paper_printable_area")
.cloned()
.collect(),
)
} else {
source_wires
};
std::sync::Arc::make_mut(&mut content.wires)
.retain(|wire| wire.name != "paper_printable_area");
}
if let Some(((x0, y0), (x1, y1))) = scene.paper_limits() {
// Paper geometry is stored in layout units; PDF pages use mm.
// Scale both geometry and offsets by the same unit conversion.
@@ -3434,7 +3329,7 @@ impl OpenCADStudio {
let (scale, offset_x, offset_y) = if plot_extents {
let (min_x, min_y, max_x, max_y) =
plot_content_extents(&wires, &hatches, &wipeouts).unwrap_or((x0, y0, x1, y1));
plot_content_extents(&content).unwrap_or((x0, y0, x1, y1));
let content_w = (max_x - min_x).max(1e-9);
let content_h = (max_y - min_y).max(1e-9);
let scale = if self.plot_dialog.fit_to_paper {
@@ -3513,19 +3408,18 @@ impl OpenCADStudio {
} else {
None
};
return (
wires,
hatches,
wipeouts,
group_splits,
page_w,
page_h,
return PdfPageInput {
content,
paper_w: page_w,
paper_h: page_h,
offset_x,
offset_y,
rotation,
scale as f32,
rotation_deg: rotation,
scale: scale as f32,
clip,
);
options: Self::pdf_plot_options(&self.plot_dialog),
plot_style: self.dialog_plot_style(&self.plot_dialog),
};
}
// Model space keeps its established extents + 5% margin behaviour.
@@ -3545,77 +3439,52 @@ impl OpenCADStudio {
} else {
(297.0, 210.0, 0.0, 0.0)
};
(
wires,
hatches,
wipeouts,
group_splits,
page_w,
page_h,
PdfPageInput {
content,
paper_w: page_w,
paper_h: page_h,
offset_x,
offset_y,
0,
1.0,
None,
)
rotation_deg: 0,
scale: 1.0,
clip: None,
options: Self::pdf_plot_options(&self.plot_dialog),
plot_style: self.dialog_plot_style(&self.plot_dialog),
}
}
/// Build direct PDF/printer output using the physical layout sheet in
/// paper space and the selected ISO sheet/scale around Model extents.
fn direct_plot_params(&self) -> Option<LayoutPlotParams> {
fn direct_plot_page(&self) -> Option<PdfPageInput> {
if self.tabs[self.active_tab].scene.current_layout != "Model" {
return Some(self.layout_plot_params_for("Layout"));
return Some(self.layout_plot_page_for("Layout"));
}
let (wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip) =
self.extents_plot_job()?;
if wires.is_empty() && hatches.is_empty() && wipeouts.is_empty() {
let page = self.extents_plot_job()?;
let content = &page.content;
if content.wires.is_empty() && content.hatches.is_empty()
&& content.wipeouts.is_empty() && content.images.is_empty()
{
return None;
}
Some((
std::sync::Arc::new(wires),
hatches,
wipeouts,
group_splits,
page_w,
page_h,
ox,
oy,
rotation,
scale,
clip,
))
Some(page)
}
pub(super) fn on_print_to_printer(&mut self) -> Task<Message> {
let Some((
wires,
hatches,
wipeouts,
group_splits,
page_w,
page_h,
ox,
oy,
rotation,
scale,
clip,
)) = self.direct_plot_params()
let Some(page) = self.direct_plot_page()
else {
self.command_line.push_error(
crate::t!("Nothing to plot: model space contains no printable geometry.").as_ref(),
);
return Task::none();
};
let plot_style = self.dialog_plot_style(&self.plot_dialog);
let options = self.plot_print_options(&self.plot_dialog, group_splits);
let options = self.plot_print_options(&self.plot_dialog);
self.command_line
.push_info(crate::t!("Sending to system printer…").as_ref());
background_task(
move || {
iced::futures::executor::block_on(crate::io::print_to_printer::print_wires_with(
wires, hatches, wipeouts, page_w, page_h, ox, oy, rotation, scale, clip,
plot_style, options,
))
iced::futures::executor::block_on(
crate::io::print_to_printer::print_wires_with(page, options),
)
},
Message::PrintResult,
)
@@ -4499,7 +4368,6 @@ impl OpenCADStudio {
self.active_modal = None;
self.reset_modal_geometry();
let plot_style = self.dialog_plot_style(&d);
// Extents, Window and Display use one plot path in both spaces. Only
// Paper-space Layout is special: it uses the physical sheet bounds.
if d.area != "Layout" {
@@ -4513,19 +4381,7 @@ impl OpenCADStudio {
}
_ => None,
};
let Some((
w_wires,
w_hatches,
w_wipeouts,
wgroup_splits,
sw,
sh,
wox,
woy,
wrotation,
wscale,
wclip,
)) = job
let Some(page) = job
else {
self.command_line
.push_error(crate::t!("Plot area is empty. Pick a larger window.").as_ref());
@@ -4536,26 +4392,11 @@ impl OpenCADStudio {
// whatever the output target is.
if preview {
let tmp = crate::io::print_to_printer::temp_pdf_path("preview");
let render_options = Self::pdf_plot_options(&d, wgroup_splits);
let work = move || {
crate::io::pdf_export::export_pdf(
&w_wires,
&w_hatches,
&w_wipeouts,
sw,
sh,
wox,
woy,
wrotation,
wscale,
wclip,
&tmp,
plot_style.as_ref(),
render_options,
)
.and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp))
.map(|_| crate::t!("Opened plot preview.").into_owned())
.map_err(|e| crate::tf!("Preview failed: {e}").into_owned())
crate::io::pdf_export::export_pdf(&page, &tmp)
.and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp))
.map(|_| crate::t!("Opened plot preview.").into_owned())
.map_err(|e| crate::tf!("Preview failed: {e}").into_owned())
};
return self.run_plot_work(d.background, true, work);
}
@@ -4564,56 +4405,27 @@ impl OpenCADStudio {
return Task::done(Message::PlotWindowExport);
}
let tmp = crate::io::print_to_printer::temp_pdf_path("print");
let render_options = Self::pdf_plot_options(&d, wgroup_splits);
let opts = self.plot_print_options(&d, wgroup_splits);
let opts = self.plot_print_options(&d);
let work = move || {
crate::io::pdf_export::export_pdf(
&w_wires,
&w_hatches,
&w_wipeouts,
sw,
sh,
wox,
woy,
wrotation,
wscale,
wclip,
&tmp,
plot_style.as_ref(),
render_options,
)
.and_then(|_| crate::io::print_to_printer::print_existing_pdf(&tmp, &opts))
.map(|printer| crate::tf!("Sent to printer: {printer}").into_owned())
.map_err(|e| crate::tf!("Print failed: {e}").into_owned())
crate::io::pdf_export::export_pdf(&page, &tmp)
.and_then(|_| {
crate::io::print_to_printer::print_existing_pdf(&tmp, &opts)
})
.map(|printer| crate::tf!("Sent to printer: {printer}").into_owned())
.map_err(|e| crate::tf!("Print failed: {e}").into_owned())
};
return self.run_plot_work(true, false, work);
}
let (wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip) =
self.layout_plot_params();
let page = self.layout_plot_page();
if preview {
let tmp = crate::io::print_to_printer::temp_pdf_path("preview");
let render_options = Self::pdf_plot_options(&d, group_splits);
let work = move || {
crate::io::pdf_export::export_pdf(
&wires,
&hatches,
&wipeouts,
page_w,
page_h,
ox,
oy,
rotation,
scale,
clip,
&tmp,
plot_style.as_ref(),
render_options,
)
.and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp))
.map(|_| crate::t!("Opened plot preview.").into_owned())
.map_err(|e| crate::tf!("Preview failed: {e}").into_owned())
crate::io::pdf_export::export_pdf(&page, &tmp)
.and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp))
.map(|_| crate::t!("Opened plot preview.").into_owned())
.map_err(|e| crate::tf!("Preview failed: {e}").into_owned())
};
return self.run_plot_work(d.background, true, work);
}
@@ -4623,16 +4435,15 @@ impl OpenCADStudio {
return Task::done(Message::PlotExport);
}
let opts = self.plot_print_options(&d, group_splits);
let opts = self.plot_print_options(&d);
self.command_line
.push_info(crate::t!("Sending to system printer…").as_ref());
let work = move || {
iced::futures::executor::block_on(crate::io::print_to_printer::print_wires_with(
wires, hatches, wipeouts, page_w, page_h, ox, oy, rotation, scale, clip,
plot_style, opts,
page, opts,
))
.map(|printer| crate::tf!("Sent to printer: {printer}").into_owned())
.map_err(|error| crate::tf!("Print failed: {error}").into_owned())
.map(|printer| crate::tf!("Sent to printer: {printer}").into_owned())
.map_err(|error| crate::tf!("Print failed: {error}").into_owned())
};
self.run_plot_work(true, false, work)
}
@@ -4642,19 +4453,16 @@ impl OpenCADStudio {
fn plot_print_options(
&self,
d: &crate::ui::window::plot::PlotDialogState,
group_splits: crate::io::pdf_export::PlotGroupSplits,
) -> crate::io::print_to_printer::PrintOptions {
crate::io::print_to_printer::PrintOptions {
printer: d.printer.clone(),
copies: d.copies.trim().parse::<u32>().unwrap_or(1).max(1),
quality: Some(d.quality.clone()),
render: Self::pdf_plot_options(d, group_splits),
}
}
fn pdf_plot_options(
d: &crate::ui::window::plot::PlotDialogState,
group_splits: crate::io::pdf_export::PlotGroupSplits,
) -> crate::io::pdf_export::PdfPlotOptions {
crate::io::pdf_export::PdfPlotOptions {
object_lineweights: d.lineweights,
@@ -4662,7 +4470,6 @@ impl OpenCADStudio {
transparency: d.transparency,
stamp: d.stamp,
merge_lines: d.merge_lines,
group_splits,
}
}
@@ -4679,20 +4486,20 @@ impl OpenCADStudio {
.cloned()
}
fn window_plot_job(&self) -> Option<ClippedPlotParams> {
fn window_plot_job(&self) -> Option<PdfPageInput> {
self.area_plot_job(self.plot_window?)
}
fn display_plot_job(&self) -> Option<ClippedPlotParams> {
fn display_plot_job(&self) -> Option<PdfPageInput> {
self.area_plot_job(self.display_plot_window()?)
}
fn limits_plot_job(&self) -> Option<ClippedPlotParams> {
fn limits_plot_job(&self) -> Option<PdfPageInput> {
let (min, max) = self.tabs[self.active_tab].scene.current_drawing_limits()?;
self.area_plot_job((min.x, min.y, max.x, max.y))
}
fn named_view_plot_job(&self, name: &str) -> Option<ClippedPlotParams> {
fn named_view_plot_job(&self, name: &str) -> Option<PdfPageInput> {
let view = self.tabs[self.active_tab]
.scene
.document
@@ -4714,26 +4521,18 @@ impl OpenCADStudio {
))
}
fn extents_plot_job(&self) -> Option<ClippedPlotParams> {
fn extents_plot_job(&self) -> Option<PdfPageInput> {
let scene = &self.tabs[self.active_tab].scene;
if scene.current_layout == "Model" {
let (min, max) = scene.model_space_extents()?;
return self.area_plot_job((min.x as f64, min.y as f64, max.x as f64, max.y as f64));
}
let (wires, hatches, wipeouts, _) = plot_scene_content(
let content = plot_scene_content(
scene,
self.plot_dialog.paperspace_last,
plot_render_mode_override(&self.plot_dialog),
);
let extents = plot_content_extents(
&wires
.iter()
.filter(|wire| wire.name != "paper_printable_area")
.cloned()
.collect::<Vec<_>>(),
&hatches,
&wipeouts,
)?;
let extents = plot_content_extents(&content)?;
self.area_plot_job(extents)
}
@@ -4790,7 +4589,7 @@ impl OpenCADStudio {
/// Render a selected rectangle through one shared Model/Paper path. The
/// window may lie partly or wholly outside a paper sheet.
fn area_plot_job(&self, window: (f64, f64, f64, f64)) -> Option<ClippedPlotParams> {
fn area_plot_job(&self, window: (f64, f64, f64, f64)) -> Option<PdfPageInput> {
use crate::io::paper_sizes::{window_to_sheet, PlotScale};
let i = self.active_tab;
let (x0, y0, x1, y1) = window;
@@ -4829,27 +4628,22 @@ impl OpenCADStudio {
};
let scene = &self.tabs[i].scene;
let (wx0, wy0, wx1, wy1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32);
let (all_wires, hatches, wipeouts, mut group_splits) = plot_scene_content(
let mut content = plot_scene_content(
scene,
self.plot_dialog.paperspace_last,
plot_render_mode_override(&self.plot_dialog),
);
let first_wire_count = all_wires[..group_splits.wires.min(all_wires.len())]
let in_window = |wire: &crate::io::pdf_export::PlotWire| {
wire.name != "paper_printable_area"
&& wire.aabb[0] <= wx1 && wire.aabb[2] >= wx0
&& wire.aabb[1] <= wy1 && wire.aabb[3] >= wy0
};
content.group_splits.wires = content.wires
[..content.group_splits.wires.min(content.wires.len())]
.iter()
.filter(|w| w.name != "paper_printable_area")
.filter(|w| {
w.aabb[0] <= wx1 && w.aabb[2] >= wx0 && w.aabb[1] <= wy1 && w.aabb[3] >= wy0
})
.filter(|wire| in_window(wire))
.count();
let wires: Vec<_> = all_wires
.iter()
.filter(|w| w.name != "paper_printable_area")
.filter(|w| {
w.aabb[0] <= wx1 && w.aabb[2] >= wx0 && w.aabb[1] <= wy1 && w.aabb[3] >= wy0
})
.cloned()
.collect();
group_splits.wires = first_wire_count;
std::sync::Arc::make_mut(&mut content.wires).retain(in_window);
let offset_x = (target_x / scale) - x0;
let offset_y = (target_y / scale) - y0;
let clip = Some((
@@ -4863,19 +4657,18 @@ impl OpenCADStudio {
90 | 270 => (sheet_h, sheet_w),
_ => (sheet_w, sheet_h),
};
Some((
wires,
hatches,
wipeouts,
group_splits,
page_w,
page_h,
Some(PdfPageInput {
content,
paper_w: page_w,
paper_h: page_h,
offset_x,
offset_y,
rotation,
scale as f32,
rotation_deg: rotation,
scale: scale as f32,
clip,
))
options: Self::pdf_plot_options(&self.plot_dialog),
plot_style: self.dialog_plot_style(&self.plot_dialog),
})
}
pub(super) fn on_plot_style_panel_apply(&mut self) -> Task<Message> {
+111
View File
@@ -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) => {
@@ -6857,6 +6963,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 {
+75 -1
View File
@@ -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,34 @@ 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 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> {
let i = self.active_tab;
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
@@ -3004,6 +3060,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
@@ -4432,6 +4505,7 @@ properties={:.1}ms picked={}",
scope,
canvas_sz,
self.show_constraint_values,
self.constraint_bar_display,
p_full,
)
})
+1
View File
@@ -822,6 +822,7 @@ bg={bg_ms:.1}ms n={view_count}"
scope,
sel_ref.vp_size,
self.show_constraint_values,
self.constraint_bar_display,
)
.into_iter()
.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::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,