forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main'
The command driver was split into command_driver/ upstream; the PDF underlay and XCLIP additions to the old file move with it: their CmdResult arms into the router (mod.rs) and the block-reference selection filter with its clip lookup into utilities.rs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
56c04408d6
18 files changed
+13693
-11710
No files matched your search
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,521 @@
|
||||
use super::*;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn handle_edit_dimension_break(
|
||||
&mut self,
|
||||
dimensions: Vec<Handle>,
|
||||
operation: crate::command::DimensionBreakOperation,
|
||||
) {
|
||||
let i = self.active_tab;
|
||||
self.push_undo_snapshot(i, "DIMBREAK");
|
||||
let result = apply_dimbreak(&mut self.tabs[i].scene, &dimensions, operation);
|
||||
if result.changed.is_empty() {
|
||||
self.command_line.push_info(&result.message);
|
||||
} else {
|
||||
for handle in &result.changed {
|
||||
self.tabs[i].scene.invalidate_dim_block_recorded(*handle);
|
||||
}
|
||||
let changes = result
|
||||
.changed
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect::<Vec<_>>();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&result.message);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
}
|
||||
|
||||
pub(super) fn handle_edit_dimension_jog(&mut self, dimension: Handle, point: Option<glam::DVec3>) {
|
||||
let i = self.active_tab;
|
||||
let valid = matches!(
|
||||
self.tabs[i].scene.document.get_entity(dimension),
|
||||
Some(acadrust::EntityType::Dimension(
|
||||
acadrust::entities::Dimension::Linear(_)
|
||||
| acadrust::entities::Dimension::Aligned(_)
|
||||
))
|
||||
);
|
||||
if !valid {
|
||||
self.command_line.push_error(
|
||||
crate::t!("DIMJOGLINE: select a linear or aligned dimension.").as_ref(),
|
||||
);
|
||||
} else if !self.reject_locked_edit(i, dimension) {
|
||||
self.push_undo_snapshot(i, "DIMJOGLINE");
|
||||
let values = point.map(|point| {
|
||||
use acadrust::xdata::XDataValue;
|
||||
vec![
|
||||
XDataValue::Integer16(387),
|
||||
XDataValue::Integer16(3),
|
||||
XDataValue::Integer16(389),
|
||||
XDataValue::Point3D(acadrust::types::Vector3::new(
|
||||
point.x, point.y, point.z,
|
||||
)),
|
||||
]
|
||||
});
|
||||
crate::scene::view::dispatch::set_entity_xdata(
|
||||
&mut self.tabs[i].scene.document,
|
||||
dimension,
|
||||
"ACAD_DSTYLE_DIMJAG_POSITION",
|
||||
values,
|
||||
);
|
||||
self.tabs[i].scene.invalidate_dim_block_recorded(dimension);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(dimension, crate::scene::ChangeKind::Modified)]);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(
|
||||
if point.is_some() {
|
||||
crate::t!("DIMJOGLINE: jog added.")
|
||||
} else {
|
||||
crate::t!("DIMJOGLINE: jog removed.")
|
||||
}
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
}
|
||||
|
||||
pub(super) fn handle_space_dimensions(&mut self, base: Handle, others: Vec<Handle>, spacing: Option<f64>) {
|
||||
let i = self.active_tab;
|
||||
self.push_undo_snapshot(i, "DIMSPACE");
|
||||
if apply_dimspace(&mut self.tabs[i].scene, base, &others, spacing) {
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(crate::t!("DIMSPACE Spacing adjusted.").as_ref());
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
}
|
||||
}
|
||||
|
||||
struct DimBreakResult {
|
||||
changed: Vec<Handle>,
|
||||
message: String,
|
||||
}
|
||||
|
||||
fn wire_segments(
|
||||
models: &[crate::scene::model::wire_model::WireModel],
|
||||
) -> Vec<(glam::DVec3, glam::DVec3)> {
|
||||
let mut output = Vec::new();
|
||||
for model in models {
|
||||
for run in model.points.split(|point| !point[0].is_finite()) {
|
||||
for pair in run.windows(2) {
|
||||
let first =
|
||||
glam::DVec3::new(pair[0][0] as f64, pair[0][1] as f64, pair[0][2] as f64);
|
||||
let second =
|
||||
glam::DVec3::new(pair[1][0] as f64, pair[1][1] as f64, pair[1][2] as f64);
|
||||
if first.is_finite()
|
||||
&& second.is_finite()
|
||||
&& first.distance_squared(second) > 1.0e-18
|
||||
{
|
||||
output.push((first, second));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn segment_intersection_xy(
|
||||
first: (glam::DVec3, glam::DVec3),
|
||||
second: (glam::DVec3, glam::DVec3),
|
||||
) -> Option<(glam::DVec3, glam::DVec3)> {
|
||||
let gap = cadkernel::space::segment_break_gap_xy(
|
||||
[first.0.to_array(), first.1.to_array()],
|
||||
[second.0.to_array(), second.1.to_array()],
|
||||
0.25,
|
||||
)?;
|
||||
Some((
|
||||
glam::DVec3::from_array(gap[0]),
|
||||
glam::DVec3::from_array(gap[1]),
|
||||
))
|
||||
}
|
||||
|
||||
fn break_object_handle(document: &acadrust::CadDocument, dimension: Handle) -> Option<Handle> {
|
||||
document.objects.iter().find_map(|(handle, object)| {
|
||||
let acadrust::objects::ObjectType::DataObject(object) = object else {
|
||||
return None;
|
||||
};
|
||||
let acadrust::objects::DataObjectData::BreakData(data) = &object.data else {
|
||||
return None;
|
||||
};
|
||||
(data.dimension_reference == dimension).then_some(*handle)
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_dimension_break_data(document: &mut acadrust::CadDocument, dimension: Handle) -> bool {
|
||||
let handles: Vec<_> = document
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|(handle, object)| match object {
|
||||
acadrust::objects::ObjectType::DataObject(object) => match &object.data {
|
||||
acadrust::objects::DataObjectData::BreakData(data)
|
||||
if data.dimension_reference == dimension =>
|
||||
{
|
||||
Some(*handle)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
if handles.is_empty() {
|
||||
return false;
|
||||
}
|
||||
for handle in &handles {
|
||||
document.objects.remove(handle);
|
||||
}
|
||||
if let Some(dictionary_handle) = document.extension_dictionary_handle(dimension) {
|
||||
if let Some(acadrust::objects::ObjectType::Dictionary(dictionary)) =
|
||||
document.objects.get_mut(&dictionary_handle)
|
||||
{
|
||||
dictionary.entries.retain(|(name, handle)| {
|
||||
!name.eq_ignore_ascii_case("ACAD_BREAKDATA") && !handles.contains(handle)
|
||||
});
|
||||
dictionary
|
||||
.hard_owner_entries
|
||||
.retain(|name| !name.eq_ignore_ascii_case("ACAD_BREAKDATA"));
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn write_dimension_break_data(
|
||||
document: &mut acadrust::CadDocument,
|
||||
dimension: Handle,
|
||||
reserved_reference: Handle,
|
||||
references: Vec<acadrust::objects::BreakPointReference>,
|
||||
) -> bool {
|
||||
use acadrust::objects::{BreakData, DataObject, DataObjectData, Dictionary, ObjectType};
|
||||
|
||||
if references.is_empty() {
|
||||
return remove_dimension_break_data(document, dimension);
|
||||
}
|
||||
if let Some(handle) = break_object_handle(document, dimension) {
|
||||
if let Some(ObjectType::DataObject(object)) = document.objects.get_mut(&handle) {
|
||||
object.data = DataObjectData::BreakData(BreakData {
|
||||
version: 0,
|
||||
dimension_reference: dimension,
|
||||
reserved_reference,
|
||||
point_references: references,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
let dictionary_handle = match document.extension_dictionary_handle(dimension) {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
let handle = document.allocate_handle();
|
||||
let mut dictionary = Dictionary::new();
|
||||
dictionary.handle = handle;
|
||||
dictionary.owner = dimension;
|
||||
dictionary.hard_owner = true;
|
||||
document
|
||||
.objects
|
||||
.insert(handle, ObjectType::Dictionary(dictionary));
|
||||
if let Some(entity) = document.get_entity_mut(dimension) {
|
||||
entity.common_mut().xdictionary_handle = Some(handle);
|
||||
}
|
||||
handle
|
||||
}
|
||||
};
|
||||
|
||||
let object_handle = document.allocate_handle();
|
||||
let mut object = DataObject::new(DataObjectData::BreakData(BreakData {
|
||||
version: 0,
|
||||
dimension_reference: dimension,
|
||||
reserved_reference,
|
||||
point_references: references,
|
||||
}));
|
||||
object.handle = object_handle;
|
||||
object.owner = dictionary_handle;
|
||||
document
|
||||
.objects
|
||||
.insert(object_handle, ObjectType::DataObject(object));
|
||||
if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&dictionary_handle) {
|
||||
dictionary
|
||||
.entries
|
||||
.retain(|(name, _)| !name.eq_ignore_ascii_case("ACAD_BREAKDATA"));
|
||||
dictionary.add_entry("ACAD_BREAKDATA", object_handle);
|
||||
dictionary.set_entry_hard_owner("ACAD_BREAKDATA", true);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn break_reference(
|
||||
identifier: i32,
|
||||
reference_type: i32,
|
||||
first: glam::DVec3,
|
||||
second: glam::DVec3,
|
||||
) -> acadrust::objects::BreakPointReference {
|
||||
acadrust::objects::BreakPointReference {
|
||||
version: 0,
|
||||
reserved: 0,
|
||||
reference_type,
|
||||
flags: 0,
|
||||
identifier,
|
||||
first_point: acadrust::types::Vector3::new(first.x, first.y, first.z),
|
||||
second_point: acadrust::types::Vector3::new(second.x, second.y, second.z),
|
||||
trailing_version: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_dimbreak(
|
||||
scene: &mut crate::scene::Scene,
|
||||
requested: &[Handle],
|
||||
operation: crate::command::DimensionBreakOperation,
|
||||
) -> DimBreakResult {
|
||||
use crate::command::DimensionBreakOperation;
|
||||
|
||||
let dimensions: Vec<_> = requested
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|handle| {
|
||||
matches!(
|
||||
scene.document.get_entity(*handle),
|
||||
Some(acadrust::EntityType::Dimension(_))
|
||||
) && scene.locked_layer_name(*handle).is_none()
|
||||
})
|
||||
.collect();
|
||||
if dimensions.is_empty() {
|
||||
return DimBreakResult {
|
||||
changed: Vec::new(),
|
||||
message: crate::t!("DIMBREAK: no editable dimensions were selected.").into_owned(),
|
||||
};
|
||||
}
|
||||
|
||||
if matches!(operation, DimensionBreakOperation::Remove) {
|
||||
let changed: Vec<_> = dimensions
|
||||
.into_iter()
|
||||
.filter(|handle| remove_dimension_break_data(&mut scene.document, *handle))
|
||||
.collect();
|
||||
return DimBreakResult {
|
||||
message: crate::tf!(
|
||||
"DIMBREAK: removed breaks from {} dimension(s).",
|
||||
changed.len()
|
||||
)
|
||||
.into_owned(),
|
||||
changed,
|
||||
};
|
||||
}
|
||||
|
||||
let crossing_handles: Vec<Handle> = match operation {
|
||||
DimensionBreakOperation::Object(handle) => vec![handle],
|
||||
DimensionBreakOperation::Auto => scene
|
||||
.document
|
||||
.entities()
|
||||
.map(|entity| entity.common().handle)
|
||||
.filter(|handle| {
|
||||
!dimensions.contains(handle) && scene.entity_belongs_to_active_space(*handle)
|
||||
})
|
||||
.collect(),
|
||||
DimensionBreakOperation::Manual(_, _) | DimensionBreakOperation::Remove => Vec::new(),
|
||||
};
|
||||
let crossing_segments: Vec<_> = crossing_handles
|
||||
.iter()
|
||||
.flat_map(|handle| wire_segments(&scene.wire_models_for(&[*handle])))
|
||||
.collect();
|
||||
|
||||
let mut pending = Vec::new();
|
||||
for dimension in &dimensions {
|
||||
let existing: Vec<_> = break_object_handle(&scene.document, *dimension)
|
||||
.and_then(|handle| scene.document.objects.get(&handle))
|
||||
.and_then(|object| match object {
|
||||
acadrust::objects::ObjectType::DataObject(object) => match &object.data {
|
||||
acadrust::objects::DataObjectData::BreakData(data) => {
|
||||
Some(data.point_references.clone())
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut references = if matches!(operation, DimensionBreakOperation::Manual(_, _)) {
|
||||
existing
|
||||
} else {
|
||||
existing
|
||||
.into_iter()
|
||||
.filter(|reference| reference.reference_type == 2)
|
||||
.collect()
|
||||
};
|
||||
match operation {
|
||||
DimensionBreakOperation::Manual(first, second) => {
|
||||
references.push(break_reference(references.len() as i32, 2, first, second));
|
||||
}
|
||||
DimensionBreakOperation::Auto | DimensionBreakOperation::Object(_) => {
|
||||
let dimension_segments = wire_segments(&scene.wire_models_for(&[*dimension]));
|
||||
let mut intersections = Vec::new();
|
||||
for dim_segment in &dimension_segments {
|
||||
for crossing_segment in &crossing_segments {
|
||||
if let Some(points) =
|
||||
segment_intersection_xy(*dim_segment, *crossing_segment)
|
||||
{
|
||||
let center = (points.0 + points.1) * 0.5;
|
||||
if intersections.iter().all(
|
||||
|(first, second): &(glam::DVec3, glam::DVec3)| {
|
||||
center.distance_squared((*first + *second) * 0.5) > 1.0e-8
|
||||
},
|
||||
) {
|
||||
intersections.push(points);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let start = references.len() as i32;
|
||||
references.extend(
|
||||
intersections
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, points)| {
|
||||
break_reference(start + index as i32, 1, points.0, points.1)
|
||||
}),
|
||||
);
|
||||
}
|
||||
DimensionBreakOperation::Remove => {}
|
||||
}
|
||||
pending.push((*dimension, references));
|
||||
}
|
||||
|
||||
let reserved = match operation {
|
||||
DimensionBreakOperation::Object(handle) => handle,
|
||||
_ => Handle::NULL,
|
||||
};
|
||||
let changed: Vec<_> = pending
|
||||
.into_iter()
|
||||
.filter_map(|(dimension, references)| {
|
||||
write_dimension_break_data(&mut scene.document, dimension, reserved, references)
|
||||
.then_some(dimension)
|
||||
})
|
||||
.collect();
|
||||
DimBreakResult {
|
||||
message: if changed.is_empty() {
|
||||
crate::t!("DIMBREAK: no intersections were found.").into_owned()
|
||||
} else {
|
||||
crate::tf!("DIMBREAK: updated {} dimension(s).", changed.len()).into_owned()
|
||||
},
|
||||
changed,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_dimspace(
|
||||
scene: &mut crate::scene::Scene,
|
||||
base_h: Handle,
|
||||
others: &[Handle],
|
||||
requested_spacing: Option<f64>,
|
||||
) -> bool {
|
||||
use acadrust::entities::Dimension;
|
||||
let (axis, normal, definition, auto_spacing) = match scene.document.get_entity(base_h) {
|
||||
Some(acadrust::EntityType::Dimension(dimension @ Dimension::Linear(d))) => {
|
||||
let spacing = dimension_auto_spacing(
|
||||
&scene.document,
|
||||
dimension,
|
||||
scene.creation_annotation_multiplier(),
|
||||
);
|
||||
(
|
||||
[d.rotation.cos(), d.rotation.sin(), 0.0],
|
||||
[d.base.normal.x, d.base.normal.y, d.base.normal.z],
|
||||
[
|
||||
d.definition_point.x,
|
||||
d.definition_point.y,
|
||||
d.definition_point.z,
|
||||
],
|
||||
spacing,
|
||||
)
|
||||
}
|
||||
Some(acadrust::EntityType::Dimension(dimension @ Dimension::Aligned(d))) => {
|
||||
let spacing = dimension_auto_spacing(
|
||||
&scene.document,
|
||||
dimension,
|
||||
scene.creation_annotation_multiplier(),
|
||||
);
|
||||
(
|
||||
[
|
||||
d.second_point.x - d.first_point.x,
|
||||
d.second_point.y - d.first_point.y,
|
||||
d.second_point.z - d.first_point.z,
|
||||
],
|
||||
[d.base.normal.x, d.base.normal.y, d.base.normal.z],
|
||||
[
|
||||
d.definition_point.x,
|
||||
d.definition_point.y,
|
||||
d.definition_point.z,
|
||||
],
|
||||
spacing,
|
||||
)
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
let Some(frame) = cadkernel::space::dimension_spacing_frame(axis, normal, definition) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let effective_spacing = requested_spacing.unwrap_or(auto_spacing);
|
||||
let mut changes = Vec::new();
|
||||
for (idx, &h) in others.iter().enumerate() {
|
||||
let target = frame.coordinate + effective_spacing * (idx + 1) as f64;
|
||||
let mut changed = false;
|
||||
if let Some(acadrust::EntityType::Dimension(dim)) = scene.document.get_entity_mut(h) {
|
||||
let slide = |p: &mut acadrust::types::Vector3| {
|
||||
let point =
|
||||
cadkernel::space::move_to_dimension_spacing([p.x, p.y, p.z], frame, target);
|
||||
*p = acadrust::types::Vector3::new(point[0], point[1], point[2]);
|
||||
};
|
||||
match dim {
|
||||
Dimension::Linear(d) => {
|
||||
slide(&mut d.definition_point);
|
||||
d.base.definition_point = d.definition_point;
|
||||
changed = true;
|
||||
}
|
||||
Dimension::Aligned(d) => {
|
||||
slide(&mut d.definition_point);
|
||||
d.base.definition_point = d.definition_point;
|
||||
changed = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
// The dimension line moved, so its baked *D block is stale — drop it
|
||||
// so the next save re-bakes it. (#181)
|
||||
scene.invalidate_dim_block_recorded(h);
|
||||
changes.push((h, crate::scene::ChangeKind::Modified));
|
||||
}
|
||||
}
|
||||
if !changes.is_empty() {
|
||||
scene.bump_entities(&changes);
|
||||
}
|
||||
!changes.is_empty()
|
||||
}
|
||||
|
||||
fn dimension_auto_spacing(
|
||||
document: &acadrust::CadDocument,
|
||||
dimension: &acadrust::entities::Dimension,
|
||||
annotation_multiplier: f64,
|
||||
) -> f64 {
|
||||
let style = document
|
||||
.dim_styles
|
||||
.iter()
|
||||
.find(|style| {
|
||||
style
|
||||
.name
|
||||
.eq_ignore_ascii_case(&dimension.base().style_name)
|
||||
})
|
||||
.map(|style| {
|
||||
crate::entities::dimension::resolved_dimension_style(style, dimension, document)
|
||||
});
|
||||
style
|
||||
.map(|style| {
|
||||
let scale = if style.dimscale > 1.0e-9 {
|
||||
style.dimscale
|
||||
} else {
|
||||
annotation_multiplier.max(1.0e-9)
|
||||
};
|
||||
(style.dimdli.abs() * scale).max(1.0e-6)
|
||||
})
|
||||
.unwrap_or(3.75)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,951 @@
|
||||
use super::*;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn handle_lengthen_entity(&mut self, handle: Handle, pick_pt: glam::DVec3, mode: crate::modules::draw::modify::lengthen::LenMode) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
if self.reject_locked_edit(i, handle) {
|
||||
return Some(Task::none());
|
||||
}
|
||||
use crate::modules::draw::modify::lengthen::lengthen_entity;
|
||||
let result = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.and_then(|e| lengthen_entity(e, pick_pt.as_vec3(), &mode));
|
||||
match result {
|
||||
Some(new_entity) => {
|
||||
let label = self.history_label_from_active_cmd(i, "LENGTHEN");
|
||||
self.push_undo_snapshot(i, label);
|
||||
self.tabs[i].scene.erase_entities(&[handle]);
|
||||
self.tabs[i].scene.add_entity(new_entity);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(crate::t!("LENGTHEN: applied.").as_ref());
|
||||
self.refresh_properties();
|
||||
}
|
||||
None => {
|
||||
self.command_line
|
||||
.push_error(crate::t!("LENGTHEN: entity type not supported.").as_ref());
|
||||
}
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn handle_divide_entity(&mut self, handle: Handle, n: usize, marker: Option<crate::command::CurveMarker>) {
|
||||
let i = self.active_tab;
|
||||
use crate::modules::draw::inquiry::divide::divide_entity;
|
||||
let pts = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.map(|e| divide_entity(e, n, marker.as_ref()))
|
||||
.unwrap_or_default();
|
||||
let count = pts.len();
|
||||
if count > 0 {
|
||||
self.push_undo_snapshot(i, "DIVIDE");
|
||||
let layer = self.tabs[i].active_layer.clone();
|
||||
for mut p in pts {
|
||||
p.as_entity_mut().set_layer(layer.clone());
|
||||
self.tabs[i].scene.add_entity(p);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(crate::tf!("DIVIDE: {count} marker(s) placed.").as_ref());
|
||||
} else {
|
||||
self.command_line.push_error(
|
||||
crate::t!("DIVIDE: entity type not supported or N < 2.").as_ref(),
|
||||
);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
}
|
||||
|
||||
pub(super) fn handle_measure_entity(&mut self, result: CmdResult) {
|
||||
let i = self.active_tab;
|
||||
let CmdResult::MeasureEntity {
|
||||
handle,
|
||||
segment_length,
|
||||
pick_point,
|
||||
marker,
|
||||
} = result
|
||||
else {
|
||||
unreachable!("router only routes the matching variant");
|
||||
};
|
||||
use crate::modules::draw::inquiry::divide::measure_entity;
|
||||
let pts = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.map(|e| measure_entity(e, segment_length, pick_point, marker.as_ref()))
|
||||
.unwrap_or_default();
|
||||
let count = pts.len();
|
||||
if count > 0 {
|
||||
self.push_undo_snapshot(i, "MEASURE");
|
||||
let layer = self.tabs[i].active_layer.clone();
|
||||
for mut p in pts {
|
||||
p.as_entity_mut().set_layer(layer.clone());
|
||||
self.tabs[i].scene.add_entity(p);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(crate::tf!("MEASURE: {count} marker(s) placed.").as_ref());
|
||||
} else {
|
||||
self.command_line
|
||||
.push_output(crate::t!("MEASURE: 0 markers placed.").as_ref());
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
}
|
||||
|
||||
pub(super) fn handle_pedit_op(&mut self, handle: Handle, op: crate::modules::draw::modify::pedit::PeditOp) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.apply_pedit_result(i, handle, op)
|
||||
}
|
||||
|
||||
pub(super) fn handle_join_to_source(&mut self, source: Handle, handles: Vec<Handle>) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
if self.reject_locked_edit(i, source) {
|
||||
return Some(Task::none());
|
||||
}
|
||||
let joined = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(source)
|
||||
.and_then(|entity| {
|
||||
let candidates: Vec<_> = handles
|
||||
.iter()
|
||||
.filter(|handle| {
|
||||
**handle != source && !self.tabs[i].scene.is_layer_locked(**handle)
|
||||
})
|
||||
.filter_map(|handle| {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(*handle)
|
||||
.map(|entity| (*handle, entity))
|
||||
})
|
||||
.collect();
|
||||
crate::modules::draw::modify::join::join_to_source(entity, &candidates)
|
||||
});
|
||||
if let Some((replacement, consumed)) = joined {
|
||||
self.push_undo_snapshot(i, "JOIN");
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(source) {
|
||||
*entity = replacement;
|
||||
}
|
||||
self.tabs[i].scene.erase_entities(&consumed);
|
||||
self.tabs[i].scene.refresh_fill_model(source);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(source, crate::scene::ChangeKind::Modified)]);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!(
|
||||
"JOIN: {} objects joined to source.",
|
||||
consumed.len()
|
||||
));
|
||||
self.refresh_properties();
|
||||
} else {
|
||||
self.command_line
|
||||
.push_info("JOIN: no compatible objects joined to source.");
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn handle_join_entities(&mut self, handles: Vec<Handle>) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
if let Some(handle) = handles
|
||||
.iter()
|
||||
.find(|handle| self.tabs[i].scene.is_layer_locked(**handle))
|
||||
{
|
||||
self.reject_locked_edit(i, *handle);
|
||||
return Some(Task::none());
|
||||
}
|
||||
use crate::modules::draw::modify::join::join_entities;
|
||||
let pairs: Vec<_> = handles
|
||||
.iter()
|
||||
.filter_map(|&h| self.tabs[i].scene.document.get_entity(h).map(|e| (h, e)))
|
||||
.collect();
|
||||
match join_entities(&pairs) {
|
||||
Some((to_remove, merged)) => {
|
||||
let label = self.history_label_from_active_cmd(i, "JOIN");
|
||||
self.push_undo_snapshot(i, label);
|
||||
self.tabs[i].scene.erase_entities(&to_remove);
|
||||
let count_in = to_remove.len();
|
||||
let count_out = merged.len();
|
||||
for e in merged {
|
||||
self.tabs[i].scene.add_entity(e);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.command_line.push_output(
|
||||
crate::tf!("JOIN: {count_in} object(s) joined into {count_out}.")
|
||||
.as_ref(),
|
||||
);
|
||||
self.refresh_properties();
|
||||
}
|
||||
None => {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.command_line.push_error(
|
||||
crate::t!("JOIN: objects don't form a single connected chain, or contain an unsupported type / tilted arc.").as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn handle_break_entity(&mut self, handle: Handle, p1: glam::DVec3, p2: glam::DVec3) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
if self.reject_locked_edit(i, handle) {
|
||||
return Some(Task::none());
|
||||
}
|
||||
use crate::modules::draw::modify::break_cmd::break_entity;
|
||||
let replacement = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.and_then(|e| break_entity(e, p1, p2));
|
||||
match replacement {
|
||||
Some(frags) => {
|
||||
let unchanged = frags.len() == 1
|
||||
&& self.tabs[i].scene.document.get_entity(handle).is_some_and(
|
||||
|original| {
|
||||
let mut fragment = frags[0].clone();
|
||||
fragment.common_mut().handle = handle;
|
||||
&fragment == original
|
||||
},
|
||||
);
|
||||
if unchanged {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.command_line.push_output("BREAK: no geometry changed.");
|
||||
return Some(Task::none());
|
||||
}
|
||||
let label = self.history_label_from_active_cmd(i, "BREAK");
|
||||
self.push_undo_snapshot(i, label);
|
||||
let count = frags.len();
|
||||
let owner = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.map(|entity| entity.common().owner_handle);
|
||||
let mut fragments = frags.into_iter();
|
||||
if let Some(mut first) = fragments.next() {
|
||||
first.common_mut().handle = handle;
|
||||
self.tabs[i].scene.update_entity(first);
|
||||
for mut fragment in fragments {
|
||||
fragment.common_mut().handle = Handle::NULL;
|
||||
if let Some(owner) = owner {
|
||||
fragment.common_mut().owner_handle = owner;
|
||||
}
|
||||
self.tabs[i].scene.add_entity(fragment);
|
||||
}
|
||||
} else {
|
||||
self.tabs[i].scene.erase_entities(&[handle]);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.command_line
|
||||
.push_output(crate::tf!("BREAK: {} fragment(s).", count).as_ref());
|
||||
self.refresh_properties();
|
||||
}
|
||||
None => {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.command_line
|
||||
.push_error(crate::t!("BREAK: entity type not supported.").as_ref());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
pub(super) fn handle_hatchedit_apply(&mut self, result: CmdResult) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
let CmdResult::HatcheditApply {
|
||||
handle,
|
||||
name,
|
||||
scale,
|
||||
angle,
|
||||
operation,
|
||||
} = result
|
||||
else {
|
||||
unreachable!("router only routes the matching variant");
|
||||
};
|
||||
if self.reject_locked_edit(i, handle) {
|
||||
return Some(Task::none());
|
||||
}
|
||||
if !matches!(
|
||||
self.tabs[i].scene.document.get_entity(handle),
|
||||
Some(acadrust::EntityType::Hatch(_))
|
||||
) {
|
||||
self.command_line
|
||||
.push_error(crate::t!("HATCHEDIT: hatch entity not found.").as_ref());
|
||||
} else {
|
||||
use crate::command::{CadCommand, HatchEditOperation};
|
||||
if matches!(&operation, HatchEditOperation::BeginAssociate) {
|
||||
let associative = matches!(self.tabs[i].scene.document.get_entity(handle),Some(acadrust::EntityType::Hatch(h)) if h.is_associative);
|
||||
if associative {
|
||||
self.command_line
|
||||
.push_info("HATCHEDIT: hatch is already associative.");
|
||||
self.tabs[i].active_cmd = None;
|
||||
} else {
|
||||
let plane = match self.tabs[i].scene.document.get_entity(handle) {
|
||||
Some(acadrust::EntityType::Hatch(h)) => {
|
||||
let storage =
|
||||
crate::entities::curve::ocs_plane(h.normal, h.elevation);
|
||||
crate::command::WorkingPlane::new(
|
||||
glam::DVec3::from_array(storage.origin),
|
||||
glam::DVec3::from_array(storage.x_axis),
|
||||
glam::DVec3::from_array(storage.y_axis),
|
||||
)
|
||||
}
|
||||
_ => self.tabs[i].ucs_xform().working_plane(),
|
||||
};
|
||||
let mut sources =
|
||||
self.tabs[i].scene.boundary_sources_on_plane(plane, 1e-6);
|
||||
sources.remove(&handle);
|
||||
let command=crate::modules::draw::draw::hatchedit::HatcheditCommand::for_association(handle,name,scale,angle,plane,sources);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
return Some(Task::none());
|
||||
}
|
||||
if let HatchEditOperation::DrawOrderBoundary { above } = &operation {
|
||||
let references: Vec<_> =
|
||||
match self.tabs[i].scene.document.get_entity(handle) {
|
||||
Some(acadrust::EntityType::Hatch(h)) => h
|
||||
.paths
|
||||
.iter()
|
||||
.flat_map(|p| p.boundary_handles.iter())
|
||||
.filter(|h| {
|
||||
self.tabs[i].scene.document.get_entity(**h).is_some()
|
||||
})
|
||||
.map(|h| format!("{:X}", h.value()))
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
self.tabs[i].active_cmd = None;
|
||||
if references.is_empty() {
|
||||
self.command_line
|
||||
.push_info("HATCHEDIT: no associated boundary objects.");
|
||||
return Some(Task::none());
|
||||
}
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].scene.select_entity(handle, false);
|
||||
let command = format!(
|
||||
"DRAWORDER {} {}",
|
||||
if *above { "ABOVE" } else { "UNDER" },
|
||||
references.join(" ")
|
||||
);
|
||||
return Some(self.dispatch_view(&command, i).unwrap_or_else(Task::none));
|
||||
}
|
||||
if matches!(
|
||||
&operation,
|
||||
HatchEditOperation::DrawOrderFront | HatchEditOperation::DrawOrderBack
|
||||
) {
|
||||
let command = if matches!(&operation, HatchEditOperation::DrawOrderFront) {
|
||||
"DRAWORDER FRONT"
|
||||
} else {
|
||||
"DRAWORDER BACK"
|
||||
};
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].scene.select_entity(handle, false);
|
||||
self.tabs[i].active_cmd = None;
|
||||
return Some(self.dispatch_view(command, i).unwrap_or_else(Task::none));
|
||||
}
|
||||
self.push_undo_snapshot(i, "HATCHEDIT");
|
||||
match operation {
|
||||
HatchEditOperation::Appearance {
|
||||
color,
|
||||
layer,
|
||||
transparency,
|
||||
} => {
|
||||
let layer = layer.map(|name| {
|
||||
if name == "." {
|
||||
self.tabs[i].active_layer.clone()
|
||||
} else {
|
||||
name
|
||||
}
|
||||
});
|
||||
if layer.as_ref().is_some_and(|name| {
|
||||
self.tabs[i].scene.document.layers.get(name).is_none()
|
||||
}) {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error("HATCHEDIT: layer not found.");
|
||||
return Some(Task::none());
|
||||
}
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
let common = entity.common_mut();
|
||||
if let Some(value) = color {
|
||||
common.color = value;
|
||||
common.color_name = None;
|
||||
common.color_book_handle = None;
|
||||
}
|
||||
if let Some(value) = layer {
|
||||
common.layer = value;
|
||||
}
|
||||
if let Some(value) = transparency {
|
||||
common.transparency = value;
|
||||
}
|
||||
}
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||||
self.refresh_properties();
|
||||
}
|
||||
HatchEditOperation::Update {
|
||||
origin,
|
||||
store_origin,
|
||||
disassociate,
|
||||
style,
|
||||
annotative,
|
||||
} => {
|
||||
if store_origin {
|
||||
if let Some((x, y)) = origin {
|
||||
if !self.tabs[i].scene.document.set_hatch_origin([x, y]) {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line
|
||||
.push_error("HATCHEDIT: cannot store default origin.");
|
||||
return Some(Task::none());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(acadrust::EntityType::Hatch(hatch)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
if !name.is_empty() && name != hatch.pattern.name {
|
||||
if let Some(entry) =
|
||||
crate::scene::model::hatch_patterns::find(&name)
|
||||
{
|
||||
let mut pattern =
|
||||
crate::scene::model::hatch_patterns::build_dxf_pattern(
|
||||
entry,
|
||||
);
|
||||
crate::entities::hatch::scale_pattern_geometry(
|
||||
&mut pattern,
|
||||
scale.max(1.0e-6) as f64,
|
||||
);
|
||||
crate::entities::hatch::rotate_pattern_geometry(
|
||||
&mut pattern,
|
||||
(angle as f64).to_radians(),
|
||||
);
|
||||
let origin = hatch.pattern_origin();
|
||||
crate::entities::hatch::translate_pattern_geometry(
|
||||
&mut pattern,
|
||||
origin.x,
|
||||
origin.y,
|
||||
);
|
||||
hatch.pattern = pattern;
|
||||
hatch.is_solid = matches!(
|
||||
entry.gpu,
|
||||
crate::scene::model::hatch_model::HatchPattern::Solid
|
||||
);
|
||||
hatch.pattern_type =
|
||||
acadrust::entities::HatchPatternType::Predefined;
|
||||
hatch.gradient_color.enabled = false;
|
||||
}
|
||||
} else {
|
||||
let requested_scale = scale.max(1.0e-6) as f64;
|
||||
if hatch.pattern_scale > 1.0e-12 {
|
||||
let factor = requested_scale / hatch.pattern_scale;
|
||||
hatch.scale_pattern_about_origin(factor);
|
||||
}
|
||||
let requested_angle = (angle as f64).to_radians();
|
||||
let delta = requested_angle - hatch.pattern_angle;
|
||||
hatch.rotate_pattern_about_origin(delta);
|
||||
}
|
||||
hatch.pattern_scale = scale.max(1.0e-6) as f64;
|
||||
hatch.pattern_angle = (angle as f64).to_radians();
|
||||
if let Some((x, y)) = origin {
|
||||
hatch.set_pattern_origin(acadrust::types::Vector2::new(x, y));
|
||||
}
|
||||
if disassociate {
|
||||
for path in &mut hatch.paths {
|
||||
path.boundary_handles.clear();
|
||||
path.flags.set_external(false);
|
||||
}
|
||||
hatch.is_associative = false;
|
||||
}
|
||||
if let Some(style) = style {
|
||||
hatch.style = style;
|
||||
}
|
||||
}
|
||||
if let Some(value) = annotative {
|
||||
crate::scene::annotative::set_entity_annotative(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
value,
|
||||
);
|
||||
if value {
|
||||
if let Some(scale_handle) =
|
||||
self.tabs[i].scene.creation_annotation_scale_handle()
|
||||
{
|
||||
crate::scene::annotative::create_annotation_context(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
scale_handle,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||||
}
|
||||
HatchEditOperation::AddBoundaries(handles) => {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.edit_hatch_boundary_handles(handle, &handles, true);
|
||||
}
|
||||
HatchEditOperation::RemoveBoundaries(handles) => {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.edit_hatch_boundary_handles(handle, &handles, false);
|
||||
}
|
||||
HatchEditOperation::AssociatePaths(paths) => {
|
||||
if paths.is_empty()
|
||||
|| paths.iter().any(|path| {
|
||||
path.boundary_handles.is_empty()
|
||||
|| path.boundary_handles.iter().any(|source| {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(*source)
|
||||
.is_none()
|
||||
})
|
||||
})
|
||||
{
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error(
|
||||
"HATCHEDIT: associated boundary is no longer available.",
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
self.tabs[i].scene.replace_hatch_association(handle, paths);
|
||||
}
|
||||
HatchEditOperation::RecreateBoundary { associate, region } => {
|
||||
let source = self.tabs[i].scene.document.get_entity(handle).cloned();
|
||||
if let Some(acadrust::EntityType::Hatch(source)) = source {
|
||||
let storage = crate::entities::curve::ocs_plane(
|
||||
source.normal,
|
||||
source.elevation,
|
||||
);
|
||||
let plane = crate::command::WorkingPlane::new(
|
||||
glam::DVec3::from_array(storage.origin),
|
||||
glam::DVec3::from_array(storage.x_axis),
|
||||
glam::DVec3::from_array(storage.y_axis),
|
||||
);
|
||||
let (entities, path_groups) = if region {
|
||||
let loops = source
|
||||
.paths
|
||||
.iter()
|
||||
.map(|path| {
|
||||
path.edges
|
||||
.iter()
|
||||
.map(crate::entities::hatch::edge_curve)
|
||||
.collect::<Option<Vec<_>>>()
|
||||
})
|
||||
.collect::<Option<Vec<_>>>();
|
||||
let Some(loops) = loops.filter(|loops| {
|
||||
!loops.is_empty()
|
||||
&& loops.iter().all(|ring| !ring.is_empty())
|
||||
}) else {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error(
|
||||
"HATCHEDIT: boundary cannot form a region.",
|
||||
);
|
||||
return Some(Task::none());
|
||||
};
|
||||
let contained = loops
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(inner, ring)| {
|
||||
let seed = ring[0].point_at(0.0);
|
||||
loops
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(outer, boundary)| {
|
||||
inner != outer
|
||||
&& cadkernel::geom2d::contains(
|
||||
boundary,
|
||||
seed,
|
||||
cadkernel::geom2d::Tolerance::new(1e-6),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let depths = contained
|
||||
.iter()
|
||||
.map(|row| row.iter().filter(|inside| **inside).count())
|
||||
.collect::<Vec<_>>();
|
||||
let groups = depths
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, depth)| **depth % 2 == 0)
|
||||
.map(|(outer, depth)| {
|
||||
let mut indices = vec![outer];
|
||||
indices.extend((0..loops.len()).filter(|inner| {
|
||||
depths[*inner] == *depth + 1
|
||||
&& contained[*inner][outer]
|
||||
}));
|
||||
indices
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let entities = groups
|
||||
.iter()
|
||||
.map(|indices| {
|
||||
let profiles = indices
|
||||
.iter()
|
||||
.map(|index| loops[*index].clone())
|
||||
.collect::<Vec<_>>();
|
||||
crate::scene::model::presspull_model::region_from_loops(
|
||||
&profiles, plane,
|
||||
)
|
||||
})
|
||||
.collect::<Option<Vec<_>>>();
|
||||
let Some(entities) = entities else {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error(
|
||||
"HATCHEDIT: boundary cannot form a region.",
|
||||
);
|
||||
return Some(Task::none());
|
||||
};
|
||||
(entities, groups)
|
||||
} else {
|
||||
let rings = crate::scene::hatch_boundary_rings(&source);
|
||||
if rings.len() != source.paths.len() {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error(
|
||||
"HATCHEDIT: boundary cannot form a polyline.",
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
let entities = crate::scene::boundary_entities(&rings, plane);
|
||||
if entities.len() != source.paths.len() {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error(
|
||||
"HATCHEDIT: boundary cannot form a polyline.",
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
let groups =
|
||||
(0..entities.len()).map(|index| vec![index]).collect();
|
||||
(entities, groups)
|
||||
};
|
||||
let mut path_handles = vec![None; source.paths.len()];
|
||||
for (entity, paths) in entities.into_iter().zip(path_groups) {
|
||||
if let Some(boundary) = self.commit_entity_handle(entity) {
|
||||
for path in paths {
|
||||
if let Some(slot) = path_handles.get_mut(path) {
|
||||
*slot = Some(boundary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if associate {
|
||||
if let Some(acadrust::EntityType::Hatch(hatch)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
for (path, boundary) in
|
||||
hatch.paths.iter_mut().zip(path_handles.iter().copied())
|
||||
{
|
||||
if let Some(boundary) = boundary {
|
||||
path.boundary_handles = vec![boundary];
|
||||
path.flags.set_external(true);
|
||||
}
|
||||
}
|
||||
hatch.is_associative =
|
||||
path_handles.iter().all(Option::is_some);
|
||||
}
|
||||
}
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||||
}
|
||||
}
|
||||
HatchEditOperation::Separate => {
|
||||
let source = self.tabs[i].scene.document.get_entity(handle).cloned();
|
||||
if let Some(acadrust::EntityType::Hatch(hatch)) = source {
|
||||
let groups = crate::scene::separated_hatch_path_groups(&hatch);
|
||||
if groups.len() > 1 {
|
||||
for paths in groups {
|
||||
let mut separated = hatch.clone();
|
||||
separated.common.handle = acadrust::Handle::NULL;
|
||||
separated.paths = paths;
|
||||
separated.is_associative = separated
|
||||
.paths
|
||||
.iter()
|
||||
.any(|path| !path.boundary_handles.is_empty());
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.add_entity(acadrust::EntityType::Hatch(separated));
|
||||
}
|
||||
self.tabs[i].scene.erase_entities(&[handle]);
|
||||
}
|
||||
}
|
||||
}
|
||||
HatchEditOperation::DrawOrderFront
|
||||
| HatchEditOperation::DrawOrderBack
|
||||
| HatchEditOperation::DrawOrderBoundary { .. }
|
||||
| HatchEditOperation::BeginAssociate => unreachable!(),
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(crate::t!("HATCHEDIT: hatch updated.").as_ref());
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
None
|
||||
}
|
||||
|
||||
fn apply_pedit_result(
|
||||
&mut self,
|
||||
tab: usize,
|
||||
handle: Handle,
|
||||
op: crate::modules::draw::modify::pedit::PeditOp,
|
||||
) -> Task<Message> {
|
||||
use crate::modules::draw::modify::pedit::{apply_pedit, convert_to_polyline, PeditOp};
|
||||
|
||||
if !matches!(
|
||||
&op,
|
||||
PeditOp::Multiple(_, _) | PeditOp::JoinSelection(_, _, _)
|
||||
) && self.reject_locked_edit(tab, handle)
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
match &op {
|
||||
PeditOp::JoinSelection(handles, fuzz, kind) => {
|
||||
let mut available = handles
|
||||
.iter()
|
||||
.filter_map(|handle| {
|
||||
if self.tabs[tab].scene.is_layer_locked(*handle) {
|
||||
return None;
|
||||
}
|
||||
self.tabs[tab]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(*handle)
|
||||
.cloned()
|
||||
.map(|entity| (*handle, entity))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let connector_distance =
|
||||
crate::modules::draw::modify::pedit::selection_connector_distance(
|
||||
&available, *fuzz,
|
||||
);
|
||||
let mut changes = Vec::new();
|
||||
while !available.is_empty() {
|
||||
let (source_handle, source) = available.remove(0);
|
||||
let candidates = available
|
||||
.iter()
|
||||
.map(|(handle, entity)| (*handle, entity))
|
||||
.collect::<Vec<_>>();
|
||||
if let Some((mut result, consumed)) =
|
||||
crate::modules::draw::modify::pedit::join_selection_extend(
|
||||
&source,
|
||||
&candidates,
|
||||
*fuzz,
|
||||
*kind,
|
||||
connector_distance,
|
||||
)
|
||||
{
|
||||
*result.common_mut() = source.common().clone();
|
||||
available.retain(|(handle, _)| !consumed.contains(handle));
|
||||
changes.push((source_handle, result, consumed));
|
||||
}
|
||||
}
|
||||
if !changes.is_empty() {
|
||||
self.push_undo_snapshot(tab, "PEDIT");
|
||||
for (source, replacement, consumed) in changes {
|
||||
self.tabs[tab].scene.erase_entities(&consumed);
|
||||
self.tabs[tab].scene.update_entity(replacement.clone());
|
||||
if let Some(command) = self.tabs[tab].active_cmd.as_mut() {
|
||||
for old in consumed {
|
||||
command.on_entity_replaced(old, &[source]);
|
||||
}
|
||||
command.inject_picked_entity(replacement);
|
||||
}
|
||||
}
|
||||
if let Some(command) = self.tabs[tab].active_cmd.as_mut() {
|
||||
command.on_pedit_applied();
|
||||
}
|
||||
self.tabs[tab].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
}
|
||||
PeditOp::Multiple(handles, operation) => {
|
||||
let replacements: Vec<_> = handles
|
||||
.iter()
|
||||
.filter(|handle| !self.tabs[tab].scene.is_layer_locked(**handle))
|
||||
.filter_map(|handle| {
|
||||
let original = self.tabs[tab].scene.document.get_entity(*handle)?;
|
||||
if matches!(operation.as_ref(), PeditOp::ConvertToPolyline) {
|
||||
convert_to_polyline(original)
|
||||
.map(|replacement| (*handle, replacement, true))
|
||||
} else {
|
||||
let mut replacement = original.clone();
|
||||
apply_pedit(&mut replacement, operation).then_some((
|
||||
*handle,
|
||||
replacement,
|
||||
false,
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if replacements.is_empty() {
|
||||
return Task::none();
|
||||
}
|
||||
self.push_undo_snapshot(tab, "PEDIT");
|
||||
for (handle, replacement, converted) in replacements {
|
||||
let updated_handle = if converted {
|
||||
self.tabs[tab].scene.erase_entities(&[handle]);
|
||||
let new_handle = self.tabs[tab].scene.add_entity(replacement);
|
||||
if let Some(command) = self.tabs[tab].active_cmd.as_mut() {
|
||||
command.on_entity_replaced(handle, &[new_handle]);
|
||||
}
|
||||
new_handle
|
||||
} else {
|
||||
if let Some(entity) = self.tabs[tab].scene.document.get_entity_mut(handle) {
|
||||
*entity = replacement;
|
||||
}
|
||||
self.tabs[tab].scene.refresh_fill_model(handle);
|
||||
self.tabs[tab]
|
||||
.scene
|
||||
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||||
handle
|
||||
};
|
||||
let updated = self.tabs[tab]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(updated_handle)
|
||||
.cloned();
|
||||
if let (Some(command), Some(entity)) =
|
||||
(self.tabs[tab].active_cmd.as_mut(), updated)
|
||||
{
|
||||
command.inject_picked_entity(entity);
|
||||
}
|
||||
}
|
||||
if let Some(command) = self.tabs[tab].active_cmd.as_mut() {
|
||||
command.on_pedit_applied();
|
||||
}
|
||||
self.tabs[tab].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
PeditOp::VertexRange { first, last, split } => {
|
||||
let pieces = self.tabs[tab]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.and_then(|entity| {
|
||||
crate::modules::draw::modify::pedit::edit_vertex_range(
|
||||
entity, *first, *last, *split,
|
||||
)
|
||||
});
|
||||
if let Some(mut pieces) = pieces {
|
||||
self.push_undo_snapshot(tab, "PEDIT");
|
||||
let source = pieces.remove(0);
|
||||
self.tabs[tab].scene.update_entity(source);
|
||||
for mut piece in pieces {
|
||||
piece.common_mut().handle = Handle::NULL;
|
||||
self.tabs[tab].scene.add_entity(piece);
|
||||
}
|
||||
let updated = self.tabs[tab].scene.document.get_entity(handle).cloned();
|
||||
if let Some(command) = self.tabs[tab].active_cmd.as_mut() {
|
||||
if let Some(entity) = updated {
|
||||
command.inject_picked_entity(entity);
|
||||
}
|
||||
command.on_pedit_applied();
|
||||
}
|
||||
self.tabs[tab].dirty = true;
|
||||
self.refresh_properties();
|
||||
} else {
|
||||
self.command_line.push_error("PEDIT: invalid vertex range.");
|
||||
}
|
||||
}
|
||||
PeditOp::ConvertToPolyline => {
|
||||
let converted = self.tabs[tab]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.and_then(convert_to_polyline);
|
||||
match converted {
|
||||
Some(polyline) => {
|
||||
return self
|
||||
.apply_cmd_result(CmdResult::ReplaceEntity(handle, vec![polyline]));
|
||||
}
|
||||
None => self
|
||||
.command_line
|
||||
.push_error(crate::t!("PEDIT: cannot convert this entity.").as_ref()),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.push_undo_snapshot(tab, "PEDIT");
|
||||
let changed = self.tabs[tab]
|
||||
.scene
|
||||
.document
|
||||
.get_entity_mut(handle)
|
||||
.map(|entity| apply_pedit(entity, &op))
|
||||
.unwrap_or(false);
|
||||
if changed {
|
||||
let updated = self.tabs[tab].scene.document.get_entity(handle).cloned();
|
||||
if let Some(command) = self.tabs[tab].active_cmd.as_mut() {
|
||||
if let Some(entity) = updated {
|
||||
command.inject_picked_entity(entity);
|
||||
}
|
||||
command.on_pedit_applied();
|
||||
}
|
||||
self.tabs[tab].dirty = true;
|
||||
self.tabs[tab].scene.refresh_fill_model(handle);
|
||||
self.tabs[tab]
|
||||
.scene
|
||||
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||||
self.command_line
|
||||
.push_output(crate::t!("PEDIT: applied.").as_ref());
|
||||
self.refresh_properties();
|
||||
} else {
|
||||
self.discard_last_undo_entry(tab);
|
||||
self.command_line.push_error(
|
||||
crate::t!("PEDIT: operation not applicable to this entity.").as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(prompt) = self.tabs[tab]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|command| command.prompt())
|
||||
{
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,992 @@
|
||||
use super::*;
|
||||
|
||||
/// `old` as a whole identifier in `source` becomes `new`.
|
||||
fn replace_identifier(source: &str, old: &str, new: &str) -> String {
|
||||
let mut out = String::with_capacity(source.len());
|
||||
let mut token = String::new();
|
||||
let flush = |token: &mut String, out: &mut String| {
|
||||
if !token.is_empty() {
|
||||
out.push_str(if token == old { new } else { token.as_str() });
|
||||
token.clear();
|
||||
}
|
||||
};
|
||||
for c in source.chars() {
|
||||
if c.is_alphanumeric() || c == '_' {
|
||||
token.push(c);
|
||||
} else {
|
||||
flush(&mut token, &mut out);
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
flush(&mut token, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
/// Drops what removed dimensional constraints leave behind: their dynamic
|
||||
/// dimensions, and their parameters when no other constraint reads them.
|
||||
pub(crate) fn purge_dimensional_extras(
|
||||
&mut self,
|
||||
i: usize,
|
||||
dimensions: Vec<Handle>,
|
||||
parameters: Vec<String>,
|
||||
) {
|
||||
if !dimensions.is_empty() {
|
||||
self.tabs[i].scene.erase_entities(&dimensions);
|
||||
}
|
||||
let unused: Vec<String> = parameters
|
||||
.into_iter()
|
||||
.filter(|name| {
|
||||
!self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraints
|
||||
.iter()
|
||||
.flat_map(|set| set.constraints.iter())
|
||||
.any(|constraint| {
|
||||
matches!(
|
||||
&constraint.driving_param,
|
||||
Some(crate::scene::named_parameters::DrivingValue::Named(used))
|
||||
if used == name
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if !unused.is_empty() {
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
for name in &unused {
|
||||
self.tabs[i].scene.named_parameters_mut().remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `Dynamic` or `Annotational`, the DCFORM setting.
|
||||
pub(crate) fn constraint_form_name(&self) -> &'static str {
|
||||
if self.constraint_form_annotational {
|
||||
"Annotational"
|
||||
} else {
|
||||
"Dynamic"
|
||||
}
|
||||
}
|
||||
|
||||
/// The value prompt a double-clicked constraint dimension opens.
|
||||
pub(crate) fn dynamic_dimension_value_command(
|
||||
&self,
|
||||
i: usize,
|
||||
handle: Handle,
|
||||
) -> Option<crate::modules::parametric::DimensionValueCommand> {
|
||||
use crate::scene::named_parameters::DrivingValue;
|
||||
let scene = &self.tabs[i].scene;
|
||||
for set in &scene.parametric_constraints {
|
||||
let Some((id, _)) = set
|
||||
.dimensions
|
||||
.iter()
|
||||
.find(|(_, dimension)| **dimension == handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let constraint = set.get(*id)?;
|
||||
let Some(DrivingValue::Named(name)) = &constraint.driving_param else {
|
||||
return None;
|
||||
};
|
||||
let table = if set.local_parameters.is_empty() {
|
||||
scene.named_parameters()
|
||||
} else {
|
||||
&set.local_parameters
|
||||
};
|
||||
let current = table
|
||||
.get(name)
|
||||
.map(|parameter| parameter.source.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
return Some(crate::modules::parametric::DimensionValueCommand::new(
|
||||
name.clone(),
|
||||
current,
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Applies `expression` or `newname=expression` to parameter `name`:
|
||||
/// the dimension value prompt and -PARAMETERS Edit.
|
||||
pub(crate) fn apply_parameter_input(&mut self, i: usize, name: &str, input: &str) {
|
||||
let input = input.trim();
|
||||
let (target, expression) = match input.split_once('=') {
|
||||
Some((new_name, expression)) => {
|
||||
(new_name.trim().to_string(), expression.trim().to_string())
|
||||
}
|
||||
None => (name.to_string(), input.to_string()),
|
||||
};
|
||||
if expression.is_empty() {
|
||||
return;
|
||||
}
|
||||
if target != name {
|
||||
if let Err(error) = self.rename_parameter(i, name, &target) {
|
||||
self.command_line.push_error(&error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let mut table = self.tabs[i].scene.named_parameters().clone();
|
||||
if let Err(error) = table.set(&target, &expression) {
|
||||
self.command_line.push_error(&error.to_string());
|
||||
return;
|
||||
}
|
||||
if !table
|
||||
.resolve(&target)
|
||||
.is_ok_and(|value| value.is_finite())
|
||||
{
|
||||
self.command_line.push_error("Invalid expression.");
|
||||
return;
|
||||
}
|
||||
let pending = self.begin_undo(i, "Edit parameter", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters = table;
|
||||
self.resolve_named_parameter_edit(i, &target);
|
||||
self.tabs[i].scene.refresh_dynamic_dimension_texts();
|
||||
self.tabs[i].dirty = true;
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
|
||||
/// -PARAMETERS New.
|
||||
pub(crate) fn create_parameter(&mut self, i: usize, name: &str, expression: &str) {
|
||||
if self.tabs[i].scene.named_parameters().contains(name) {
|
||||
self.command_line
|
||||
.push_error(&format!("Parameter {name} already exists."));
|
||||
return;
|
||||
}
|
||||
let mut table = self.tabs[i].scene.named_parameters().clone();
|
||||
if let Err(error) = table.set(name, expression) {
|
||||
self.command_line.push_error(&error.to_string());
|
||||
return;
|
||||
}
|
||||
let pending = self.begin_undo(i, "New parameter", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters = table;
|
||||
self.tabs[i].scene.sync_native_parametric_graph();
|
||||
self.tabs[i].dirty = true;
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
|
||||
/// Renames a parameter everywhere: the table, the expressions that use
|
||||
/// it and the constraints it drives.
|
||||
pub(crate) fn rename_parameter(&mut self, i: usize, old: &str, new: &str) -> Result<(), String> {
|
||||
use crate::scene::named_parameters::{is_reserved_name, is_valid_name, DrivingValue};
|
||||
let table = self.tabs[i].scene.named_parameters().clone();
|
||||
let Some(source) = table.get(old).map(|parameter| parameter.source.clone()) else {
|
||||
return Err(format!("Parameter {old} not found."));
|
||||
};
|
||||
if !is_valid_name(new) || is_reserved_name(new) || table.contains(new) {
|
||||
return Err(format!("Invalid parameter name {new}."));
|
||||
}
|
||||
let mut renamed = table.clone();
|
||||
renamed.set(new, &source).map_err(|error| error.to_string())?;
|
||||
for parameter in table.iter() {
|
||||
if parameter.name == old {
|
||||
continue;
|
||||
}
|
||||
let rewritten = replace_identifier(¶meter.source, old, new);
|
||||
if rewritten != parameter.source {
|
||||
renamed
|
||||
.set(¶meter.name, &rewritten)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
}
|
||||
renamed.remove(old);
|
||||
let pending = self.begin_undo(i, "Rename parameter", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters = renamed;
|
||||
let scopes: Vec<_> = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraints
|
||||
.iter()
|
||||
.map(|set| set.scope)
|
||||
.collect();
|
||||
for scope in scopes {
|
||||
let before = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.filter(|set| {
|
||||
set.constraints.iter().any(|constraint| {
|
||||
matches!(&constraint.driving_param, Some(DrivingValue::Named(name)) if name == old)
|
||||
})
|
||||
})
|
||||
.cloned();
|
||||
let Some(before) = before else {
|
||||
continue;
|
||||
};
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, before);
|
||||
let set = self.tabs[i].scene.parametric_constraint_set_mut(scope);
|
||||
for constraint in &mut set.constraints {
|
||||
if matches!(&constraint.driving_param, Some(DrivingValue::Named(name)) if name == old) {
|
||||
constraint.driving_param = Some(DrivingValue::Named(new.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.refresh_dynamic_dimension_texts();
|
||||
self.tabs[i].scene.sync_native_parametric_graph();
|
||||
self.tabs[i].dirty = true;
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// -PARAMETERS Delete: a parameter no constraint drives with.
|
||||
pub(crate) fn delete_parameter(&mut self, i: usize, name: &str) {
|
||||
use crate::scene::named_parameters::DrivingValue;
|
||||
if !self.tabs[i].scene.named_parameters().contains(name) {
|
||||
self.command_line
|
||||
.push_error(&format!("Parameter {name} not found."));
|
||||
return;
|
||||
}
|
||||
let in_use = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraints
|
||||
.iter()
|
||||
.flat_map(|set| set.constraints.iter())
|
||||
.any(|constraint| {
|
||||
matches!(&constraint.driving_param, Some(DrivingValue::Named(used)) if used == name)
|
||||
});
|
||||
if in_use {
|
||||
self.command_line
|
||||
.push_error(&format!("Parameter {name} is used by a constraint."));
|
||||
return;
|
||||
}
|
||||
let pending = self.begin_undo(i, "Delete parameter", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters_mut().remove(name);
|
||||
self.tabs[i].scene.sync_native_parametric_graph();
|
||||
self.tabs[i].dirty = true;
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_parametric_constraint(
|
||||
&mut self,
|
||||
id: crate::scene::parametric_constraints::ConstraintId,
|
||||
label: &'static str,
|
||||
) -> bool {
|
||||
let i = self.active_tab;
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let Some(set) = self.tabs[i].scene.parametric_constraint_set(scope) else {
|
||||
return false;
|
||||
};
|
||||
let Some(constraint) = set.get(id) else {
|
||||
return false;
|
||||
};
|
||||
let touched: Vec<Handle> = constraint.refs.iter().map(|r| r.entity).collect();
|
||||
let dimension = set.dimensions.get(&id).copied();
|
||||
let parameter = match &constraint.driving_param {
|
||||
Some(crate::scene::named_parameters::DrivingValue::Named(name)) => Some(name.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let constraints_before = set.clone();
|
||||
let pending = self.begin_undo(i, label, touched.len(), dimension.is_none());
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, constraints_before);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set_mut(scope)
|
||||
.remove(id);
|
||||
self.purge_dimensional_extras(
|
||||
i,
|
||||
dimension.into_iter().collect(),
|
||||
parameter.into_iter().collect(),
|
||||
);
|
||||
self.tabs[i].scene.bump_constraints_epoch();
|
||||
if self.tabs[i].scene.selected_constraint == Some(id) {
|
||||
self.tabs[i].scene.selected_constraint = None;
|
||||
}
|
||||
let changes: Vec<(Handle, crate::scene::ChangeKind)> = touched
|
||||
.into_iter()
|
||||
.map(|h| (h, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
self.refresh_properties();
|
||||
true
|
||||
}
|
||||
|
||||
/// Removes and re-solves the first conflicting constraint in the active scope.
|
||||
pub(in crate::app) fn resolve_one_parametric_conflict(&mut self) {
|
||||
let i = self.active_tab;
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let Some(id) = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.and_then(|set| set.conflicts.first().map(|(id, _)| *id))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.remove_parametric_constraint(id, "Remove conflicting constraint");
|
||||
}
|
||||
|
||||
/// Removes one user-selected constraint from the active standard graph scope.
|
||||
pub(in crate::app) fn delete_parametric_constraint(
|
||||
&mut self,
|
||||
id: crate::scene::parametric_constraints::ConstraintId,
|
||||
) {
|
||||
self.remove_parametric_constraint(id, "Delete constraint");
|
||||
}
|
||||
|
||||
/// Rebuilds the active tab's `Scene::named_parameters` from the
|
||||
/// editor's working buffer and re-solves dependent constraints.
|
||||
pub(in crate::app) fn apply_named_parameter_editor_rows(&mut self) {
|
||||
let i = self.active_tab;
|
||||
// A name shared by more than one row can't be resolved by picking
|
||||
// one arbitrarily (`ParameterTable::set` would just let the last
|
||||
// one silently win) -- refuse every row sharing that name up front,
|
||||
// same as `named_parameters::preview`'s live check.
|
||||
let duplicates = crate::ui::window::named_parameters::duplicate_name_rows(
|
||||
&self.named_parameter_editor_rows,
|
||||
);
|
||||
let mut table = crate::scene::named_parameters::ParameterTable::new();
|
||||
let mut failed = 0usize;
|
||||
for (idx, row) in self.named_parameter_editor_rows.iter().enumerate() {
|
||||
let name = row.name.trim();
|
||||
if name.is_empty() || duplicates.contains(&idx) {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = table.set(name, row.formula.trim()) {
|
||||
self.command_line
|
||||
.push_error(crate::tf!("Parameter '{}': {}", name, e).as_ref());
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
if !duplicates.is_empty() {
|
||||
self.command_line.push_error(
|
||||
crate::tf!(
|
||||
"{} row(s) skipped: duplicate parameter name.",
|
||||
duplicates.len()
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
failed += duplicates.len();
|
||||
}
|
||||
let param_count = table.len();
|
||||
|
||||
// The whole table just changed, not one isolated value, so there is
|
||||
// no cheaper "which handles actually moved" test worth doing here —
|
||||
// re-solve every scope with a constraint driven by *any* named
|
||||
// parameter (mirrors `solve_scope`'s own "full rebuild, not
|
||||
// incremental" philosophy).
|
||||
let touched: Vec<Handle> = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraints
|
||||
.iter()
|
||||
.flat_map(|set| set.constraints.iter())
|
||||
.filter(|c| {
|
||||
c.enabled
|
||||
&& matches!(
|
||||
c.driving_param,
|
||||
Some(crate::scene::named_parameters::DrivingValue::Named(_))
|
||||
)
|
||||
})
|
||||
.flat_map(|c| c.refs.iter().map(|r| r.entity))
|
||||
.collect();
|
||||
|
||||
// A changed value moves a dimensional constraint's second point; its
|
||||
// first point stays, as in the reference.
|
||||
let anchors: Vec<crate::scene::parametric_constraints::ParametricRef> = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraints
|
||||
.iter()
|
||||
.flat_map(|set| set.constraints.iter())
|
||||
.filter(|c| {
|
||||
c.enabled
|
||||
&& matches!(
|
||||
c.driving_param,
|
||||
Some(crate::scene::named_parameters::DrivingValue::Named(_))
|
||||
)
|
||||
})
|
||||
.flat_map(|c| {
|
||||
crate::scene::parametric_constraints::dimensional_anchor_refs(
|
||||
&self.tabs[i].scene.document,
|
||||
&c.refs,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let pending = self.begin_undo(i, "Apply named parameters", touched.len(), true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters = table;
|
||||
self.tabs[i].dirty = true;
|
||||
if !touched.is_empty() {
|
||||
let changes: Vec<(Handle, crate::scene::ChangeKind)> = touched
|
||||
.into_iter()
|
||||
.map(|h| (h, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities_with_parametric_policy(
|
||||
&changes,
|
||||
&anchors,
|
||||
self.constraint_solve_mode,
|
||||
);
|
||||
} else {
|
||||
self.tabs[i].scene.sync_native_parametric_graph();
|
||||
}
|
||||
self.tabs[i].scene.refresh_dynamic_dimension_texts();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
if failed == 0 {
|
||||
self.command_line
|
||||
.push_info(crate::tf!("{} parameter(s) applied.", param_count).as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-solves every entity touched by an enabled constraint driven by
|
||||
/// `name` — the targeted, single-row counterpart to
|
||||
/// `apply_named_parameter_editor_rows`'s "re-solve what it affects": an
|
||||
/// inline Properties-panel edit changes one parameter, not the whole
|
||||
/// table, so unlike that whole-table Apply this can cheaply scope the
|
||||
/// re-solve to just the entities that actually reference it.
|
||||
fn resolve_named_parameter_edit(&mut self, i: usize, name: &str) {
|
||||
let readers: Vec<&crate::scene::parametric_constraints::ParametricConstraint> = self
|
||||
.tabs[i]
|
||||
.scene
|
||||
.parametric_constraints
|
||||
.iter()
|
||||
.flat_map(|set| set.constraints.iter())
|
||||
.filter(|c| {
|
||||
c.enabled
|
||||
&& matches!(&c.driving_param, Some(crate::scene::named_parameters::DrivingValue::Named(n)) if n == name)
|
||||
})
|
||||
.collect();
|
||||
let touched: Vec<Handle> = readers
|
||||
.iter()
|
||||
.flat_map(|c| c.refs.iter().map(|r| r.entity))
|
||||
.collect();
|
||||
// A changed value moves a dimensional constraint's second point; its
|
||||
// first point (and the line it measures perpendicular to) stays, as
|
||||
// in the reference.
|
||||
let anchors: Vec<crate::scene::parametric_constraints::ParametricRef> = readers
|
||||
.iter()
|
||||
.flat_map(|c| {
|
||||
crate::scene::parametric_constraints::dimensional_anchor_refs(
|
||||
&self.tabs[i].scene.document,
|
||||
&c.refs,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
self.tabs[i].dirty = true;
|
||||
if touched.is_empty() {
|
||||
self.tabs[i].scene.sync_native_parametric_graph();
|
||||
return;
|
||||
}
|
||||
let changes: Vec<(Handle, crate::scene::ChangeKind)> = touched
|
||||
.into_iter()
|
||||
.map(|h| (h, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities_with_parametric_policy(&changes, &anchors, false);
|
||||
}
|
||||
|
||||
/// Commits one field of one Parameters-section row (Properties panel) —
|
||||
/// the per-row, commit-on-submit counterpart to
|
||||
/// `apply_named_parameter_editor_rows`'s whole-table Apply. A Formula
|
||||
/// edit is a plain redefine (`ParameterTable::set` on the existing
|
||||
/// name); a Name edit is a remove-then-set (there's no rename primitive
|
||||
/// — `ParameterTable` upserts by name), restoring the old name if the
|
||||
/// new one fails to validate so nothing is silently lost. Either way
|
||||
/// this only re-solves what actually depends on this one parameter —
|
||||
/// no "duplicate name" scan across the whole table is needed the way
|
||||
/// the buffered modal Apply needs one, since every row here is always
|
||||
/// already a real, distinct table entry.
|
||||
pub(in crate::app) fn on_prop_param_commit(
|
||||
&mut self,
|
||||
index: usize,
|
||||
field: crate::ui::window::named_parameters::ParamField,
|
||||
) -> Task<Message> {
|
||||
use crate::ui::properties::FieldKey;
|
||||
use crate::ui::window::named_parameters::ParamField;
|
||||
let i = self.active_tab;
|
||||
let key = FieldKey::Param(index, field);
|
||||
self.tabs[i].properties.active_field = None;
|
||||
let Some(typed) = self.tabs[i].properties.edit_buf.remove(&key) else {
|
||||
return Task::none();
|
||||
};
|
||||
let typed = typed.trim().to_string();
|
||||
let Some(current) = self.tabs[i]
|
||||
.scene
|
||||
.named_parameters()
|
||||
.iter()
|
||||
.nth(index)
|
||||
.cloned()
|
||||
else {
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
match field {
|
||||
ParamField::Formula if typed.is_empty() || typed == current.source => {
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
ParamField::Name if typed.is_empty() || typed == current.name => {
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
ParamField::Name if self.tabs[i].scene.named_parameters().contains(&typed) => {
|
||||
self.command_line
|
||||
.push_error(crate::tf!("Parameter '{}' already exists.", typed).as_ref());
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let pending = self.begin_undo(i, "Edit named parameter", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
|
||||
let (result, resolve_name) = match field {
|
||||
ParamField::Formula => (
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.named_parameters_mut()
|
||||
.set(¤t.name, &typed),
|
||||
current.name.clone(),
|
||||
),
|
||||
ParamField::Name => {
|
||||
let description = self.tabs[i]
|
||||
.scene
|
||||
.named_parameters()
|
||||
.description(¤t.name)
|
||||
.to_string();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.named_parameters_mut()
|
||||
.remove(¤t.name);
|
||||
let outcome = self.tabs[i]
|
||||
.scene
|
||||
.named_parameters_mut()
|
||||
.set(&typed, ¤t.source);
|
||||
let kept = if outcome.is_err() {
|
||||
let _ = self.tabs[i]
|
||||
.scene
|
||||
.named_parameters_mut()
|
||||
.set(¤t.name, ¤t.source);
|
||||
current.name.as_str()
|
||||
} else {
|
||||
typed.as_str()
|
||||
};
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.named_parameters_mut()
|
||||
.set_description(kept, &description);
|
||||
(outcome, typed.clone())
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
self.tabs[i].scene.take_undo_recording();
|
||||
self.command_line
|
||||
.push_error(crate::tf!("Parameter '{}': {}", current.name, e).as_ref());
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
if field == ParamField::Name {
|
||||
let affected: Vec<usize> = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraints
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, set)| {
|
||||
set.constraints
|
||||
.iter()
|
||||
.any(|constraint| {
|
||||
matches!(&constraint.driving_param, Some(crate::scene::named_parameters::DrivingValue::Named(name)) if name == ¤t.name)
|
||||
})
|
||||
.then_some(index)
|
||||
})
|
||||
.collect();
|
||||
for set_index in affected {
|
||||
let scope = self.tabs[i].scene.parametric_constraints[set_index].scope;
|
||||
let before = self.tabs[i].scene.parametric_constraints[set_index].clone();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, before);
|
||||
// A parameter rename rewrites the `Named` driving references
|
||||
// pointing at it — the glyph labels change with them.
|
||||
let mut renamed = false;
|
||||
for constraint in
|
||||
&mut self.tabs[i].scene.parametric_constraints[set_index].constraints
|
||||
{
|
||||
if let Some(crate::scene::named_parameters::DrivingValue::Named(name)) =
|
||||
&mut constraint.driving_param
|
||||
{
|
||||
if name == ¤t.name {
|
||||
*name = typed.clone();
|
||||
renamed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if renamed {
|
||||
self.tabs[i].scene.bump_constraints_epoch();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.resolve_named_parameter_edit(i, &resolve_name);
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
self.refresh_properties();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// The dynamic dimension the Properties panel targets: its handle, scope,
|
||||
/// constraint id and parameter name.
|
||||
fn dynamic_dimension_target(
|
||||
&self,
|
||||
i: usize,
|
||||
) -> Option<(
|
||||
Handle,
|
||||
crate::scene::parametric_constraints::ParametricScope,
|
||||
crate::scene::parametric_constraints::ConstraintId,
|
||||
String,
|
||||
)> {
|
||||
use crate::scene::named_parameters::DrivingValue;
|
||||
use crate::scene::parametric_constraints::dynamic_dimension_constraint;
|
||||
let handle = *self.property_target_handles(i).first()?;
|
||||
let (set, constraint) =
|
||||
dynamic_dimension_constraint(&self.tabs[i].scene.parametric_constraints, handle)?;
|
||||
let Some(DrivingValue::Named(name)) = &constraint.driving_param else {
|
||||
return None;
|
||||
};
|
||||
Some((handle, set.scope, constraint.id, name.clone()))
|
||||
}
|
||||
|
||||
/// Commits a dynamic dimension's Description row into its parameter.
|
||||
pub(in crate::app) fn on_dynamic_dimension_description_commit(
|
||||
&mut self,
|
||||
field: &'static str,
|
||||
) -> Task<Message> {
|
||||
use crate::ui::properties::FieldKey;
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].properties.active_field = None;
|
||||
let Some(typed) = self.tabs[i]
|
||||
.properties
|
||||
.edit_buf
|
||||
.remove(&FieldKey::Geom(field))
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some((_, _, _, name)) = self.dynamic_dimension_target(i) else {
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
};
|
||||
let pending = self.begin_undo(i, "Constraint description", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.named_parameters_mut()
|
||||
.set_description(&name, &typed);
|
||||
self.tabs[i].dirty = true;
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
self.refresh_properties();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Applies a dynamic dimension's Constraint Form or Reference choice.
|
||||
pub(in crate::app) fn on_dynamic_dimension_choice(
|
||||
&mut self,
|
||||
field: &'static str,
|
||||
value: &str,
|
||||
) -> Task<Message> {
|
||||
use crate::scene::parametric_constraints::DYNAMIC_DIMENSION_LAYER;
|
||||
let i = self.active_tab;
|
||||
let Some((handle, scope, id, _)) = self.dynamic_dimension_target(i) else {
|
||||
return Task::none();
|
||||
};
|
||||
match field {
|
||||
"dyn_constraint_form" => {
|
||||
// Annotational: an ordinary dimension on the current layer
|
||||
// that plots; Dynamic: the gray one on the constraints layer.
|
||||
let annotational = value.eq_ignore_ascii_case("Annotational");
|
||||
let active_layer = self.tabs[i].active_layer.clone();
|
||||
if !annotational {
|
||||
self.tabs[i].scene.ensure_dynamic_dimension_layer();
|
||||
}
|
||||
self.apply_property_op(i, "Constraint form", &[handle], |app, handle| {
|
||||
use crate::entities::dim_override as dov;
|
||||
let Some(mut entity) = app.tabs[i].scene.document.get_entity(handle).cloned()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if annotational {
|
||||
entity.as_entity_mut().set_layer(active_layer.clone());
|
||||
entity.common_mut().color = acadrust::types::Color::ByLayer;
|
||||
// The dynamic form's screen-size and horizontal-text
|
||||
// overrides go; the style draws it.
|
||||
for code in [
|
||||
dov::DIMSCALE,
|
||||
dov::DIMGAP,
|
||||
dov::DIMEXO,
|
||||
dov::DIMEXE,
|
||||
dov::DIMASZ,
|
||||
dov::DIMTAD,
|
||||
dov::DIMTIH,
|
||||
dov::DIMTOH,
|
||||
] {
|
||||
dov::set_on_entity(&mut entity, code, None);
|
||||
}
|
||||
} else {
|
||||
entity
|
||||
.as_entity_mut()
|
||||
.set_layer(DYNAMIC_DIMENSION_LAYER.to_string());
|
||||
entity.common_mut().color = acadrust::types::Color::Rgb {
|
||||
r: 103,
|
||||
g: 109,
|
||||
b: 118,
|
||||
};
|
||||
}
|
||||
app.tabs[i].scene.update_entity(entity);
|
||||
});
|
||||
// The dynamic form is rescaled to the screen and shows the
|
||||
// trimmed value; the annotational one is never hidden.
|
||||
self.tabs[i].scene.refresh_dynamic_dimension_scales(true);
|
||||
self.tabs[i].scene.refresh_dynamic_dimension_texts();
|
||||
self.tabs[i].scene.refresh_hidden_dynamic_dimensions();
|
||||
}
|
||||
"dyn_constraint_reference" => {
|
||||
// A reference constraint reads the geometry instead of
|
||||
// driving it; its text sits in parentheses.
|
||||
let reference =
|
||||
value.eq_ignore_ascii_case("Yes") || value == crate::t!("Yes").as_ref();
|
||||
let Some(before) = self.tabs[i].scene.parametric_constraint_set(scope).cloned()
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let pending = self.begin_undo(i, "Constraint reference", 0, true);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, before);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
let set = self.tabs[i].scene.parametric_constraint_set_mut(scope);
|
||||
if let Some(constraint) = set.constraints.iter_mut().find(|c| c.id == id) {
|
||||
constraint.enabled = !reference;
|
||||
}
|
||||
// Toggling `enabled` changes the glyph placement set (and the
|
||||
// dynamic pills), so the memoised placements must miss next
|
||||
// frame — same guarantee `set_constraint_enabled` gives.
|
||||
self.tabs[i].scene.bump_constraints_epoch();
|
||||
self.tabs[i].scene.refresh_dynamic_dimension_texts();
|
||||
self.tabs[i].scene.sync_native_parametric_graph();
|
||||
self.tabs[i].dirty = true;
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
self.refresh_properties();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Commits a dynamic dimension's Name or Expression row: the typed text
|
||||
/// goes to the Parameters-section row of the constraint's parameter.
|
||||
pub(in crate::app) fn on_dynamic_dimension_field_commit(
|
||||
&mut self,
|
||||
field: &'static str,
|
||||
param_field: crate::ui::window::named_parameters::ParamField,
|
||||
) -> Task<Message> {
|
||||
use crate::scene::named_parameters::DrivingValue;
|
||||
use crate::scene::parametric_constraints::dynamic_dimension_constraint;
|
||||
use crate::ui::properties::FieldKey;
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].properties.active_field = None;
|
||||
let Some(typed) = self.tabs[i]
|
||||
.properties
|
||||
.edit_buf
|
||||
.remove(&FieldKey::Geom(field))
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let handles = self.property_target_handles(i);
|
||||
let name = handles.first().and_then(|handle| {
|
||||
let (_, constraint) =
|
||||
dynamic_dimension_constraint(&self.tabs[i].scene.parametric_constraints, *handle)?;
|
||||
match &constraint.driving_param {
|
||||
Some(DrivingValue::Named(name)) => Some(name.clone()),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
let index = name.and_then(|name| {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.named_parameters()
|
||||
.iter()
|
||||
.position(|parameter| parameter.name == name)
|
||||
});
|
||||
let Some(index) = index else {
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
};
|
||||
self.tabs[i]
|
||||
.properties
|
||||
.edit_buf
|
||||
.insert(FieldKey::Param(index, param_field), typed);
|
||||
self.on_prop_param_commit(index, param_field)
|
||||
}
|
||||
|
||||
/// Removes Parameters-section row `index` immediately. Any
|
||||
/// constraint that referenced it gets the same "undefined reference"
|
||||
/// resolve-failure treatment a formula's own bad reference already gets
|
||||
/// (`build_constraint`) — re-solving surfaces that rather than needing
|
||||
/// special-case handling here.
|
||||
pub(in crate::app) fn on_prop_param_delete(&mut self, index: usize) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let Some(name) = self.tabs[i]
|
||||
.scene
|
||||
.named_parameters()
|
||||
.iter()
|
||||
.nth(index)
|
||||
.map(|p| p.name.clone())
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let pending = self.begin_undo(i, "Delete named parameter", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters_mut().remove(&name);
|
||||
self.resolve_named_parameter_edit(i, &name);
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
self.refresh_properties();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// The Parameters section's "+ Add parameter" row: appends a fresh,
|
||||
/// uniquely-named parameter (`param1`, `param2`, …) the user then
|
||||
/// renames/redefines inline via `on_prop_param_commit`. Defaults to `1`,
|
||||
/// not `0`: renaming this onto a name an existing Distance/Radius
|
||||
/// constraint already references (unusual, but reachable — define
|
||||
/// first, wire up the constraint's reference later) would otherwise
|
||||
/// briefly collapse that geometry to a zero-length/zero-radius
|
||||
/// degenerate state, which is a genuine numerical singularity for a
|
||||
/// point-to-point distance constraint to grow back out of (no defined
|
||||
/// direction to separate two exactly-coincident points from). `1` never
|
||||
/// hits that.
|
||||
pub(in crate::app) fn on_prop_param_add_new(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let mut n = 1usize;
|
||||
let name = loop {
|
||||
let candidate = format!("param{n}");
|
||||
if !self.tabs[i].scene.named_parameters().contains(&candidate) {
|
||||
break candidate;
|
||||
}
|
||||
n += 1;
|
||||
};
|
||||
let pending = self.begin_undo(i, "Add named parameter", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
let _ = self.tabs[i].scene.named_parameters_mut().set(&name, "1");
|
||||
self.tabs[i].scene.sync_native_parametric_graph();
|
||||
self.tabs[i].dirty = true;
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
self.refresh_properties();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) 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));
|
||||
let id = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set_mut(scope)
|
||||
.add(kind, refs, None);
|
||||
self.tabs[i].scene.note_parametric_constraint_applied(
|
||||
scope,
|
||||
id,
|
||||
self.constraint_bar_display,
|
||||
);
|
||||
}
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
use super::*;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn handle_match_entity_layer(&mut self, dest: Vec<Handle>, src: Handle) {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
let src_layer = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(src)
|
||||
.map(|e| e.common().layer.clone());
|
||||
let dest: Vec<_> = dest
|
||||
.into_iter()
|
||||
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
||||
.collect();
|
||||
if dest.is_empty() {
|
||||
self.command_line
|
||||
.push_info(crate::t!("No editable objects selected.").as_ref());
|
||||
} else if let Some(layer) = src_layer {
|
||||
self.push_undo_snapshot(i, "LAYMATCH");
|
||||
// Pure mutation lives in the shared kernel; lock-filter (above)
|
||||
// plus undo, invalidate, dirty and echo stay caller-side.
|
||||
let _ = crate::entities::match_props::match_layer_kernel(
|
||||
&mut self.tabs[i].scene.document,
|
||||
&dest,
|
||||
src,
|
||||
);
|
||||
// New layer changes the baked by-layer colour/linetype/
|
||||
// lineweight — re-tessellate the moved entities so they
|
||||
// repaint immediately (issue #231 class).
|
||||
self.invalidate_property_targets(i, &dest);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_info(crate::tf!("Layer matched to \"{layer}\".").as_ref());
|
||||
self.sync_ribbon_layers();
|
||||
} else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("Source object not found.").as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub(super) fn handle_match_properties(
|
||||
&mut self,
|
||||
mut dest: Vec<Handle>,
|
||||
src: Handle,
|
||||
) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
dest.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
||||
if dest.is_empty() {
|
||||
return Some(Task::none());
|
||||
}
|
||||
// The command stays active after each apply so more targets
|
||||
// can keep being picked; Enter / Esc ends it (#362).
|
||||
let src_clone = self.tabs[i].scene.document.get_entity(src).cloned();
|
||||
// This application record is the hatch background colour.
|
||||
// Keep the outer Option to distinguish "source is not a
|
||||
// hatch" from "source hatch has no background".
|
||||
let hatch_background_xdata: Option<Option<Vec<acadrust::xdata::XDataValue>>> =
|
||||
match src_clone.as_ref() {
|
||||
Some(acadrust::EntityType::Hatch(h)) => Some(
|
||||
h.common
|
||||
.extended_data
|
||||
.get_record("HATCHBACKGROUNDCOLOR")
|
||||
.map(|r| r.values.clone()),
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
// Dimension-style overrides ride the ACAD record, identified
|
||||
// by a leading DSTYLE string. Matching replicates that payload
|
||||
// (or clears the destination when the source has none).
|
||||
let dstyle_xdata: Option<Vec<(i16, acadrust::xdata::XDataValue)>> = src_clone
|
||||
.as_ref()
|
||||
.filter(|e| {
|
||||
matches!(
|
||||
e,
|
||||
acadrust::EntityType::Dimension(_) | acadrust::EntityType::Leader(_)
|
||||
)
|
||||
})
|
||||
.map(|e| crate::entities::dim_override::pairs(&e.common().extended_data));
|
||||
|
||||
if src_clone.is_some() {
|
||||
// Entity-level transfer lives in the shared kernel so headless
|
||||
// callers share it; the `&mut Document` xdata paths, undo,
|
||||
// tessellation, selection and echo stay caller-side here.
|
||||
// `all()` preserves the interactive behaviour; the xdata paths
|
||||
// below honour the matching flags for future narrow callers.
|
||||
let match_opts = crate::entities::match_props::MatchOpts::all();
|
||||
self.apply_property_op(i, "MATCHPROP", &dest, |app, handle| {
|
||||
let mut is_dim = false;
|
||||
let mut is_hatch = false;
|
||||
if let Some(se) = &src_clone {
|
||||
if let Some(e) = app.tabs[i].scene.document.get_entity_mut(handle) {
|
||||
is_dim = matches!(e, acadrust::EntityType::Dimension(_));
|
||||
is_hatch = matches!(e, acadrust::EntityType::Hatch(_));
|
||||
crate::entities::match_props::match_properties_kernel(se, e, &match_opts);
|
||||
}
|
||||
}
|
||||
if is_hatch && match_opts.copy_hatch_background {
|
||||
if let Some(values) = &hatch_background_xdata {
|
||||
crate::scene::view::dispatch::set_entity_xdata(
|
||||
&mut app.tabs[i].scene.document,
|
||||
handle,
|
||||
"HATCHBACKGROUNDCOLOR",
|
||||
values.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Dim-style overrides follow the style for dimension /
|
||||
// leader destinations — through set_entity_xdata so no
|
||||
// stale raw record survives. Caller-side: needs `&mut Document`.
|
||||
// (Collapsed: the old outer `dstyle_xdata.is_some() || dst-is-dim`
|
||||
// gate is subsumed — the replace only ever ran when both dst
|
||||
// and src are dim/leader.)
|
||||
if match_opts.copy_dim_overrides
|
||||
&& matches!(
|
||||
app.tabs[i].scene.document.get_entity(handle),
|
||||
Some(acadrust::EntityType::Dimension(_) | acadrust::EntityType::Leader(_))
|
||||
)
|
||||
&& matches!(
|
||||
src_clone,
|
||||
Some(acadrust::EntityType::Dimension(_) | acadrust::EntityType::Leader(_))
|
||||
)
|
||||
{
|
||||
crate::entities::dim_override::replace(
|
||||
&mut app.tabs[i].scene.document,
|
||||
handle,
|
||||
dstyle_xdata.clone().unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
// A restyled dimension renders from its baked *D block —
|
||||
// drop the stale block so the new style shows (#398).
|
||||
if is_dim {
|
||||
app.tabs[i].scene.invalidate_dim_block_recorded(handle);
|
||||
}
|
||||
// Hatch fills render from a prebuilt model (#415).
|
||||
app.tabs[i].scene.refresh_fill_model(handle);
|
||||
});
|
||||
// Color / linetype / lineweight are baked into the cached
|
||||
// wires at tessellation time; re-tessellate only the matched
|
||||
// objects instead of rebuilding a large drawing.
|
||||
let changes: Vec<_> = dest
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
self.command_line
|
||||
.push_info(crate::tf!("Properties matched to {} object(s).", dest.len()).as_ref());
|
||||
// Clear the consumed target selection and keep prompting.
|
||||
self.tabs[i].scene.deselect_all();
|
||||
if let Some(cmd) = &self.tabs[i].active_cmd {
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
}
|
||||
} else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("Source object not found.").as_ref());
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,773 @@
|
||||
use super::*;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn handle_extrude_entities(&mut self, result: CmdResult) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
let CmdResult::ExtrudeEntities {
|
||||
handles,
|
||||
extent,
|
||||
mode,
|
||||
taper_angle,
|
||||
color: _,
|
||||
} = result
|
||||
else {
|
||||
unreachable!("router only routes the matching variant");
|
||||
};
|
||||
let delete_sources = self.delete_objects != 0;
|
||||
if handles.is_empty()
|
||||
|| handles
|
||||
.iter()
|
||||
.any(|handle| self.reject_locked_edit(i, *handle))
|
||||
{
|
||||
self.tabs[i].active_cmd = None;
|
||||
return Some(Task::none());
|
||||
}
|
||||
use crate::command::{ExtrudeExtent, ExtrudeMode};
|
||||
use crate::modules::insert::solid3d_cmds::{empty_extruded_surface, empty_solid3d};
|
||||
use crate::scene::model::sweep_model;
|
||||
let path = match extent {
|
||||
ExtrudeExtent::Path(handle) => self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.cloned()
|
||||
.map(|entity| (handle, entity)),
|
||||
_ => None,
|
||||
};
|
||||
if matches!(extent, ExtrudeExtent::Path(_)) && path.is_none() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("EXTRUDE: path entity not found.").as_ref());
|
||||
self.tabs[i].active_cmd = None;
|
||||
return Some(Task::none());
|
||||
}
|
||||
let pending = self.begin_undo(i, "EXTRUDE", handles.len(), true);
|
||||
let mut created_handles = Vec::new();
|
||||
let mut consumed = Vec::new();
|
||||
let mut failed = 0usize;
|
||||
for handle in &handles {
|
||||
let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned()
|
||||
else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let Some((profile, closed)) = sweep_model::extrusion_profile_of(&entity) else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let direction = match extent {
|
||||
ExtrudeExtent::Height(height) => profile.plane.normal().map(|normal| {
|
||||
glam::DVec3::new(
|
||||
normal[0] * height,
|
||||
normal[1] * height,
|
||||
normal[2] * height,
|
||||
)
|
||||
}),
|
||||
ExtrudeExtent::Direction(direction) => Some(direction),
|
||||
ExtrudeExtent::Path(_) => None,
|
||||
};
|
||||
let path_direction = path
|
||||
.as_ref()
|
||||
.and_then(|(_, path)| sweep_model::straight_path_direction(path))
|
||||
.map(glam::DVec3::from_array);
|
||||
// The creation mode controls closed profiles. Open curves
|
||||
// always create sheet surfaces.
|
||||
let creates_surface = matches!(mode, ExtrudeMode::Surface) || !closed;
|
||||
let body = match (creates_surface, &path, direction) {
|
||||
(false, Some((_, path)), _) => {
|
||||
sweep_model::extruded_along_path(&entity, path, taper_angle)
|
||||
}
|
||||
(false, None, Some(direction)) => sweep_model::extruded_direction(
|
||||
&entity,
|
||||
direction.to_array(),
|
||||
taper_angle,
|
||||
),
|
||||
(true, None, Some(direction)) => sweep_model::extruded_surface(
|
||||
&entity,
|
||||
direction.to_array(),
|
||||
taper_angle,
|
||||
),
|
||||
(true, Some(_), _) => path_direction.and_then(|direction| {
|
||||
sweep_model::extruded_surface(
|
||||
&entity,
|
||||
direction.to_array(),
|
||||
taper_angle,
|
||||
)
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
let Some(body) = body else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let created = if creates_surface {
|
||||
let direction = direction.or(path_direction).unwrap_or(glam::DVec3::ZERO);
|
||||
let history = path
|
||||
.is_none()
|
||||
.then(|| {
|
||||
sweep_model::extrusion_history(
|
||||
&entity,
|
||||
None,
|
||||
direction.to_array(),
|
||||
taper_angle,
|
||||
profile.plane.origin,
|
||||
)
|
||||
})
|
||||
.flatten();
|
||||
let mut surface = empty_extruded_surface(direction, taper_angle);
|
||||
if let (
|
||||
Some(acadrust::objects::SolidHistoryOperation::Extrusion(value)),
|
||||
acadrust::EntityType::Surface(entity),
|
||||
) = (&history, &mut surface)
|
||||
{
|
||||
if let Some(data) =
|
||||
crate::scene::model::solid_history::extrusion_surface_data(value)
|
||||
{
|
||||
entity.surface_data = data;
|
||||
}
|
||||
}
|
||||
match history {
|
||||
Some(history) => {
|
||||
self.add_surface_model_with_history(surface, body, history)
|
||||
}
|
||||
None => self.add_surface_model(surface, body),
|
||||
}
|
||||
} else {
|
||||
let direction = direction.unwrap_or(glam::DVec3::ZERO);
|
||||
let history = path
|
||||
.as_ref()
|
||||
.and_then(|(_, path)| {
|
||||
sweep_model::sweep_history(
|
||||
&entity,
|
||||
path,
|
||||
taper_angle,
|
||||
profile.plane.origin,
|
||||
)
|
||||
})
|
||||
.or_else(|| {
|
||||
sweep_model::extrusion_history(
|
||||
&entity,
|
||||
None,
|
||||
direction.to_array(),
|
||||
taper_angle,
|
||||
profile.plane.origin,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| crate::scene::model::solid_history::brep_op(&body));
|
||||
self.add_solid_model(empty_solid3d(), body, history)
|
||||
};
|
||||
if created.is_null() {
|
||||
failed += 1;
|
||||
} else {
|
||||
created_handles.push(created);
|
||||
if delete_sources {
|
||||
consumed.push(*handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
if delete_sources && !created_handles.is_empty() {
|
||||
consumed.sort_unstable_by_key(|handle| handle.value());
|
||||
consumed.dedup();
|
||||
self.tabs[i].scene.erase_entities(&consumed);
|
||||
}
|
||||
if !created_handles.is_empty() {
|
||||
self.tabs[i].scene.deselect_all();
|
||||
for handle in &created_handles {
|
||||
self.tabs[i].scene.select_entity(*handle, true);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"EXTRUDE: created %{created} object(s); %{failed} source(s) could not be extruded.",
|
||||
created = created_handles.len(),
|
||||
failed = failed
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
} else {
|
||||
self.command_line.push_error(crate::t!("EXTRUDE: no selected object could be extruded with the requested options.").as_ref());
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.refresh_properties();
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn handle_thicken_entities(&mut self, handles: Vec<Handle>, distance: f64) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
|
||||
if handles.is_empty() {
|
||||
self.command_line
|
||||
.push_output(crate::t!("No surfaces selected.").as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
if !distance.is_finite() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("Requires numeric distance or two points.").as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
if distance.abs() <= f64::EPSILON {
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||
let pending = self.begin_undo(i, "THICKEN", handles.len(), true);
|
||||
let mut created = 0usize;
|
||||
let mut failed = 0usize;
|
||||
let mut self_intersections = 0usize;
|
||||
for handle in handles {
|
||||
let Some(acadrust::EntityType::Surface(surface)) =
|
||||
self.tabs[i].scene.document.get_entity(handle)
|
||||
else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let kernel_distance =
|
||||
if surface.kind == acadrust::entities::SurfaceKind::Revolved {
|
||||
-distance
|
||||
} else {
|
||||
distance
|
||||
};
|
||||
let body = self.tabs[i]
|
||||
.scene
|
||||
.solid_models
|
||||
.get(&handle)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
crate::scene::convert::solid3d_tess::kernel_surface_body(surface)
|
||||
});
|
||||
let Some(body) = body else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let solid = match cadkernel::brep::thicken(&body, kernel_distance) {
|
||||
Ok(solid) => solid,
|
||||
Err(cadkernel::brep::ThickenError::SelfIntersection) => {
|
||||
failed += 1;
|
||||
self_intersections += 1;
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
failed += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let history = crate::scene::model::solid_history::brep_op(&solid);
|
||||
if self
|
||||
.add_solid_model(empty_solid3d(), solid, history)
|
||||
.is_null()
|
||||
{
|
||||
failed += 1;
|
||||
} else {
|
||||
created += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if created > 0 {
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
if self_intersections > 0 {
|
||||
self.command_line.push_error(&crate::tf!(
|
||||
"{} surface(s) cannot be thickened because the offset intersects itself.",
|
||||
self_intersections
|
||||
));
|
||||
}
|
||||
if failed > 0 {
|
||||
self.command_line.push_output(&crate::tf!(
|
||||
"THICKEN: created {} solid(s); {} surface(s) could not be thickened exactly.",
|
||||
created, failed
|
||||
));
|
||||
}
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
self.refresh_properties();
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn handle_presspull_pick(&mut self, result: CmdResult) {
|
||||
let CmdResult::PresspullPick {
|
||||
handle,
|
||||
point,
|
||||
offset,
|
||||
multiple: _,
|
||||
} = result
|
||||
else {
|
||||
unreachable!("router only routes the matching variant");
|
||||
};
|
||||
self.presspull_pick(handle, point, offset);
|
||||
}
|
||||
|
||||
pub(super) fn handle_presspull_apply(&mut self, targets: Vec<crate::scene::model::presspull_model::PresspullTarget>, distance: f64) {
|
||||
self.presspull_apply(targets, distance);
|
||||
}
|
||||
|
||||
pub(super) fn handle_revolve_entities(&mut self, result: CmdResult) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
let CmdResult::RevolveEntities {
|
||||
handles,
|
||||
axis_start,
|
||||
axis_end,
|
||||
angle,
|
||||
start_angle,
|
||||
mode,
|
||||
color: _,
|
||||
} = result
|
||||
else {
|
||||
unreachable!("router only routes the matching variant");
|
||||
};
|
||||
if handles.is_empty() {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
return Some(Task::none());
|
||||
}
|
||||
let delete_sources = self.delete_objects != 0;
|
||||
use crate::command::ExtrudeMode;
|
||||
use crate::modules::insert::solid3d_cmds::{empty_revolved_surface, empty_solid3d};
|
||||
use crate::scene::model::sweep_model;
|
||||
let from = axis_start.to_array();
|
||||
let to = axis_end.to_array();
|
||||
let mut editable_handles = Vec::with_capacity(handles.len());
|
||||
let mut failed = 0usize;
|
||||
for handle in handles {
|
||||
if self.reject_locked_edit(i, handle) {
|
||||
failed += 1;
|
||||
} else {
|
||||
editable_handles.push(handle);
|
||||
}
|
||||
}
|
||||
let pending = if editable_handles.is_empty() {
|
||||
None
|
||||
} else {
|
||||
self.begin_undo(i, "REVOLVE", editable_handles.len(), true)
|
||||
};
|
||||
let mut created_handles = Vec::new();
|
||||
let mut consumed = Vec::new();
|
||||
for handle in &editable_handles {
|
||||
let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned()
|
||||
else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let Some((_, closed)) = sweep_model::extrusion_profile_of(&entity) else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let creates_surface = mode == ExtrudeMode::Surface || !closed;
|
||||
let created = if creates_surface {
|
||||
let Some(body) =
|
||||
sweep_model::revolved_surface(&entity, from, to, angle, start_angle)
|
||||
else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
self.add_surface_model(
|
||||
empty_revolved_surface(
|
||||
&entity,
|
||||
axis_start,
|
||||
axis_end,
|
||||
angle,
|
||||
start_angle,
|
||||
),
|
||||
body,
|
||||
)
|
||||
} else {
|
||||
let result =
|
||||
sweep_model::revolve_history(&entity, from, to, angle, start_angle)
|
||||
.and_then(|history| {
|
||||
cadkernel::acis::rebuild_body(&history)
|
||||
.ok()
|
||||
.map(|solid| (solid, history))
|
||||
})
|
||||
.or_else(|| {
|
||||
sweep_model::revolved(&entity, from, to, angle, start_angle)
|
||||
.map(|solid| {
|
||||
let history =
|
||||
crate::scene::model::solid_history::brep_op(&solid);
|
||||
(solid, history)
|
||||
})
|
||||
});
|
||||
let Some((solid, history)) = result else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
self.add_solid_model(empty_solid3d(), solid, history)
|
||||
};
|
||||
if created.is_null() {
|
||||
failed += 1;
|
||||
} else {
|
||||
created_handles.push(created);
|
||||
if delete_sources {
|
||||
consumed.push(*handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
if delete_sources && !created_handles.is_empty() {
|
||||
consumed.sort_unstable_by_key(|handle| handle.value());
|
||||
consumed.dedup();
|
||||
self.tabs[i].scene.erase_entities(&consumed);
|
||||
}
|
||||
if !created_handles.is_empty() {
|
||||
self.tabs[i].scene.deselect_all();
|
||||
for handle in &created_handles {
|
||||
self.tabs[i].scene.select_entity(*handle, true);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"REVOLVE: created %{created} object(s); %{failed} source(s) could not be revolved.",
|
||||
created = created_handles.len(),
|
||||
failed = failed
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
} else {
|
||||
self.command_line.push_error(
|
||||
crate::t!("REVOLVE: no selected object could be revolved with the requested options.")
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.refresh_properties();
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn handle_sweep_entities(&mut self, result: CmdResult) {
|
||||
let i = self.active_tab;
|
||||
let CmdResult::SweepEntities {
|
||||
handles,
|
||||
path_handle,
|
||||
mode,
|
||||
options,
|
||||
color: _,
|
||||
} = result
|
||||
else {
|
||||
unreachable!("router only routes the matching variant");
|
||||
};
|
||||
use crate::command::ExtrudeMode;
|
||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||
use crate::scene::model::sweep_model;
|
||||
let delete_objects = self.delete_objects;
|
||||
let delete_profiles = delete_objects != 0;
|
||||
let path = self.tabs[i].scene.document.get_entity(path_handle).cloned();
|
||||
let mut profiles = Vec::new();
|
||||
let mut failed = 0usize;
|
||||
for handle in handles {
|
||||
if handle == path_handle || self.reject_locked_edit(i, handle) {
|
||||
failed += 1;
|
||||
continue;
|
||||
}
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() {
|
||||
profiles.push((handle, entity));
|
||||
} else {
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
let selection = profiles
|
||||
.iter()
|
||||
.map(|(_, entity)| entity.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let options = sweep_model::sweep_selection_options(&selection, options);
|
||||
let pending = if profiles.is_empty() {
|
||||
None
|
||||
} else {
|
||||
self.begin_undo(i, "SWEEP", profiles.len(), true)
|
||||
};
|
||||
let mut created_handles = Vec::new();
|
||||
let mut consumed = Vec::new();
|
||||
for (handle, profile) in profiles {
|
||||
let result = path.as_ref().zip(options).and_then(|(path, options)| {
|
||||
let record = sweep_model::sweep_record(&profile, path, options)?;
|
||||
let (_, _, closed) = cadkernel::acis::sweep_profile_geometry(
|
||||
record.sweep_entity.as_ref()?,
|
||||
record.sweep_entity_transform,
|
||||
)
|
||||
.ok()?;
|
||||
let surface = mode == ExtrudeMode::Surface || !closed;
|
||||
let body =
|
||||
cadkernel::acis::rebuild_sweep_with_mode(&record, surface).ok()?;
|
||||
Some((body, record, surface))
|
||||
});
|
||||
let Some((body, record, surface)) = result else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let deletes_path =
|
||||
crate::app::delobj_deletes_auxiliary(delete_objects, surface);
|
||||
if deletes_path && self.tabs[i].scene.is_layer_locked(path_handle) {
|
||||
failed += 1;
|
||||
continue;
|
||||
}
|
||||
let created = if surface {
|
||||
self.add_surface_model(sweep_model::swept_surface_entity(&record), body)
|
||||
} else {
|
||||
self.add_solid_model(
|
||||
empty_solid3d(),
|
||||
body,
|
||||
acadrust::objects::SolidHistoryOperation::Sweep(record),
|
||||
)
|
||||
};
|
||||
if created.is_null() {
|
||||
failed += 1;
|
||||
} else {
|
||||
created_handles.push(created);
|
||||
if delete_profiles {
|
||||
consumed.push(handle);
|
||||
}
|
||||
if deletes_path {
|
||||
consumed.push(path_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !created_handles.is_empty() {
|
||||
consumed.sort_unstable_by_key(|handle| handle.value());
|
||||
consumed.dedup();
|
||||
self.tabs[i].scene.erase_entities(&consumed);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
for handle in &created_handles {
|
||||
self.tabs[i].scene.select_entity(*handle, true);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"SWEEP: created {created} object(s); {failed} source(s) could not be swept.",
|
||||
created = created_handles.len(), failed = failed
|
||||
).as_ref());
|
||||
} else {
|
||||
self.command_line.push_error(
|
||||
crate::t!("SWEEP: could not sweep the profile along the path.").as_ref(),
|
||||
);
|
||||
}
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
pub(super) fn handle_loft_entities(&mut self, result: CmdResult) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
let CmdResult::LoftEntities {
|
||||
sections,
|
||||
guides,
|
||||
path,
|
||||
mode,
|
||||
options,
|
||||
color: _,
|
||||
} = result
|
||||
else {
|
||||
unreachable!("router only routes the matching variant");
|
||||
};
|
||||
use crate::command::LoftSectionSelection;
|
||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||
use crate::scene::model::loft_command_model;
|
||||
let mut sources = sections
|
||||
.iter()
|
||||
.flat_map(|section| match section {
|
||||
LoftSectionSelection::Entity(handle) => vec![*handle],
|
||||
LoftSectionSelection::Join(handles) => handles.clone(),
|
||||
LoftSectionSelection::Point(_) => Vec::new(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
sources.sort_unstable_by_key(|handle| handle.value());
|
||||
sources.dedup();
|
||||
let delete_objects = self.delete_objects;
|
||||
let delete_sections = delete_objects != 0;
|
||||
let section_locked = delete_sections
|
||||
&& sources
|
||||
.iter()
|
||||
.any(|handle| self.tabs[i].scene.is_layer_locked(*handle));
|
||||
let available = sources
|
||||
.iter()
|
||||
.chain(guides.iter())
|
||||
.copied()
|
||||
.chain(path)
|
||||
.filter_map(|handle| {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.cloned()
|
||||
.map(|entity| (handle, entity))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let result = if section_locked {
|
||||
Err("LOFT: a source is on a locked layer; disable source deletion or unlock it.".to_string())
|
||||
} else {
|
||||
loft_command_model::record(§ions, &guides, path, &available, mode, options)
|
||||
.and_then(|record| {
|
||||
cadkernel::acis::rebuild_loft_with_options(&record)
|
||||
.map(|body| (body, record))
|
||||
})
|
||||
};
|
||||
match result {
|
||||
Ok((body, record)) => {
|
||||
let surface = record
|
||||
.parameters
|
||||
.as_ref()
|
||||
.is_some_and(|settings| settings.surface);
|
||||
let delete_auxiliary =
|
||||
crate::app::delobj_deletes_auxiliary(delete_objects, surface);
|
||||
if delete_auxiliary
|
||||
&& guides
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(path)
|
||||
.any(|handle| self.tabs[i].scene.is_layer_locked(handle))
|
||||
{
|
||||
self.command_line.push_error(
|
||||
"LOFT: a source is on a locked layer; disable source deletion or unlock it.",
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
let dirty_before = self.tabs[i].dirty;
|
||||
let pending = self.begin_undo(i, "LOFT", 1, true);
|
||||
let created = if surface {
|
||||
let handle = self.add_surface_model(
|
||||
loft_command_model::surface_entity(&record),
|
||||
body,
|
||||
);
|
||||
if !handle.is_null() {
|
||||
self.tabs[i].scene.create_solid_history(
|
||||
handle,
|
||||
acadrust::objects::SolidHistoryOperation::Loft(record),
|
||||
);
|
||||
}
|
||||
handle
|
||||
} else {
|
||||
self.add_solid_model(
|
||||
empty_solid3d(),
|
||||
body,
|
||||
acadrust::objects::SolidHistoryOperation::Loft(record),
|
||||
)
|
||||
};
|
||||
if created.is_null() {
|
||||
// The creation helper already removed its provisional
|
||||
// result. Discard its empty undo recording as well.
|
||||
self.tabs[i].scene.take_undo_recording();
|
||||
self.tabs[i].dirty = dirty_before;
|
||||
self.command_line.push_error(crate::t!("LOFT could not create a complete display. The source sections were preserved.").as_ref());
|
||||
return Some(Task::none());
|
||||
} else {
|
||||
let mut consumed = if delete_sections {
|
||||
sources.clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
if delete_auxiliary {
|
||||
consumed.extend(guides.iter().copied());
|
||||
consumed.extend(path);
|
||||
}
|
||||
consumed.sort_unstable_by_key(|handle| handle.value());
|
||||
consumed.dedup();
|
||||
self.tabs[i].scene.erase_entities(&consumed);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].scene.select_entity(created, true);
|
||||
self.tabs[i].dirty = true;
|
||||
let message = if surface {
|
||||
crate::t!("LOFT: surface created.")
|
||||
} else {
|
||||
crate::t!("LOFT: solid created.")
|
||||
};
|
||||
self.command_line.push_output(message.as_ref());
|
||||
}
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
self.command_line.push_error(&error);
|
||||
return Some(Task::none());
|
||||
}
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.refresh_properties();
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn handle_solid_edge_blend(&mut self, result: CmdResult) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let CmdResult::SolidEdgeBlend {
|
||||
handle,
|
||||
edges,
|
||||
base_face,
|
||||
value,
|
||||
other_value,
|
||||
fillet,
|
||||
} = result
|
||||
else {
|
||||
unreachable!("router only routes the matching variant");
|
||||
};
|
||||
let task =
|
||||
self.solid_edge_blend(handle, &edges, base_face, value, other_value, fillet);
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
task
|
||||
}
|
||||
|
||||
pub(super) fn handle_solid_shell(&mut self, handle: Handle, actions: Vec<crate::command::ShellFaceAction>, distance: f64) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let task = self.solid_shell(handle, &actions, distance);
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
task
|
||||
}
|
||||
|
||||
pub(super) fn handle_solid_subtract(&mut self, bases: Vec<Handle>, cutters: Vec<Handle>, convert_meshes: bool) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let task = self.solid_subtract(&bases, &cutters, convert_meshes);
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
task
|
||||
}
|
||||
|
||||
pub(super) fn handle_slice_entities(&mut self, targets: Vec<Handle>, plane: cadkernel::space::Plane, keep_point: Option<glam::DVec3>) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let task = self.solid_slice(&targets, plane, keep_point);
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
task
|
||||
}
|
||||
|
||||
pub(super) fn handle_slice_surface_entities(&mut self, targets: Vec<Handle>, cutter: Box<cadkernel::brep::Body>, keep_point: Option<glam::DVec3>) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let task = self.solid_slice_surface(&targets, *cutter, keep_point);
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
task
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,245 @@
|
||||
use super::*;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn handle_open_mtext_editor(&mut self, result: CmdResult) {
|
||||
let i = self.active_tab;
|
||||
let CmdResult::OpenMTextEditor {
|
||||
pos,
|
||||
handle,
|
||||
initial,
|
||||
height,
|
||||
template,
|
||||
} = result
|
||||
else {
|
||||
unreachable!("router only routes the matching variant");
|
||||
};
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.open_mtext_editor(pos, handle, &initial, height, template.map(|m| *m));
|
||||
}
|
||||
|
||||
pub(super) fn handle_suspend_for_mtext_input(&mut self, pos: glam::DVec3, initial: String, height: f64) {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].suspended_cmd = self.tabs[i].active_cmd.take();
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.command_mtext_input = true;
|
||||
self.pending_command_editor_text = None;
|
||||
self.open_mtext_editor(pos, None, &initial, height, None);
|
||||
}
|
||||
|
||||
pub(super) fn handle_open_text_editor(&mut self, result: CmdResult) {
|
||||
let i = self.active_tab;
|
||||
let CmdResult::OpenTextEditor {
|
||||
pos,
|
||||
handle,
|
||||
initial,
|
||||
height,
|
||||
} = result
|
||||
else {
|
||||
unreachable!("router only routes the matching variant");
|
||||
};
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.open_text_inline(
|
||||
pos,
|
||||
handle,
|
||||
&initial,
|
||||
height,
|
||||
super::super::text_inline::TextEntityField::Text,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_suspend_for_text_input(
|
||||
&mut self,
|
||||
pos: glam::DVec3,
|
||||
entity: acadrust::entities::Text,
|
||||
) {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].suspended_cmd = self.tabs[i].active_cmd.take();
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
let height = entity.height;
|
||||
self.open_text_inline(
|
||||
pos,
|
||||
None,
|
||||
"",
|
||||
height,
|
||||
super::super::text_inline::TextEntityField::Text,
|
||||
Some(entity),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_edit_text_entity(&mut self, handle: Handle) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.ribbon.deactivate_tool();
|
||||
self.begin_text_edit(handle)
|
||||
}
|
||||
|
||||
pub(super) fn handle_suspend_for_text_edit(&mut self, handle: Handle) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let is_editable =
|
||||
crate::app::text_inline::can_edit_text(handle, &self.tabs[i].scene.document);
|
||||
if !is_editable {
|
||||
self.command_line
|
||||
.push_error(crate::t!("TEXTEDIT: selected entity is not text.").as_ref());
|
||||
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||
if let Some(p) = prompt {
|
||||
self.command_line.push_info(&p);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
let cmd = self.tabs[i].active_cmd.take();
|
||||
self.tabs[i].suspended_cmd = cmd;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.ribbon.deactivate_tool();
|
||||
self.begin_text_edit(handle)
|
||||
}
|
||||
|
||||
pub(super) fn handle_set_textedit_mode(&mut self, val: bool) {
|
||||
let i = self.active_tab;
|
||||
self.texteditmode = val;
|
||||
let display_val = if val { 1 } else { 0 };
|
||||
self.command_line
|
||||
.push_output(crate::tf!("TEXTEDITMODE set to {display_val}").as_ref());
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
}
|
||||
|
||||
pub(super) fn handle_edit_dimensions(
|
||||
&mut self,
|
||||
mut handles: Vec<Handle>,
|
||||
operation: crate::command::DimensionEditOperation,
|
||||
) {
|
||||
let i = self.active_tab;
|
||||
handles.retain(|handle| {
|
||||
!self.tabs[i].scene.is_layer_locked(*handle)
|
||||
&& matches!(
|
||||
self.tabs[i].scene.document.get_entity(*handle),
|
||||
Some(acadrust::EntityType::Dimension(_))
|
||||
)
|
||||
});
|
||||
if handles.is_empty() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("DIMEDIT: no editable dimensions were selected.").as_ref());
|
||||
} else {
|
||||
self.push_undo_snapshot(i, "DIMEDIT");
|
||||
for handle in &handles {
|
||||
if let Some(acadrust::EntityType::Dimension(dimension)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(*handle)
|
||||
{
|
||||
use crate::command::DimensionEditOperation as Operation;
|
||||
match &operation {
|
||||
Operation::Home => {
|
||||
let base = dimension.base_mut();
|
||||
base.text_middle_point = acadrust::types::Vector3::ZERO;
|
||||
base.insertion_point = acadrust::types::Vector3::ZERO;
|
||||
base.text_user_positioned = false;
|
||||
base.text_rotation = 0.0;
|
||||
}
|
||||
Operation::NewText(text) => {
|
||||
dimension
|
||||
.base_mut()
|
||||
.set_text_override((!text.is_empty()).then_some(text.clone()));
|
||||
}
|
||||
Operation::Rotate(degrees) => {
|
||||
dimension.base_mut().text_rotation = degrees.to_radians();
|
||||
}
|
||||
Operation::Oblique(degrees) => match dimension {
|
||||
acadrust::entities::Dimension::Linear(value) => {
|
||||
value.ext_line_rotation = degrees.to_radians();
|
||||
}
|
||||
acadrust::entities::Dimension::Aligned(value) => {
|
||||
value.ext_line_rotation = degrees.to_radians();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.invalidate_dim_block_recorded(*handle);
|
||||
}
|
||||
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.command_line.push_output(
|
||||
crate::tf!("DIMEDIT: updated {} dimension(s).", handles.len()).as_ref(),
|
||||
);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
}
|
||||
|
||||
pub(super) fn handle_ddedit_entity(&mut self, handle: Handle, new_text: String) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
if self.reject_locked_edit(i, handle) {
|
||||
self.tabs[i].active_cmd = None;
|
||||
return Some(Task::none());
|
||||
}
|
||||
self.push_undo_snapshot(i, "DDEDIT");
|
||||
let mut updated = false;
|
||||
let mut is_dim = false;
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
||||
match entity {
|
||||
acadrust::EntityType::Text(t) => {
|
||||
t.value = new_text;
|
||||
updated = true;
|
||||
}
|
||||
acadrust::EntityType::MText(t) => {
|
||||
t.value = new_text;
|
||||
updated = true;
|
||||
}
|
||||
acadrust::EntityType::AttributeDefinition(a) => {
|
||||
a.default_value = new_text;
|
||||
updated = true;
|
||||
}
|
||||
acadrust::EntityType::AttributeEntity(a) => {
|
||||
a.set_value(new_text);
|
||||
updated = true;
|
||||
}
|
||||
acadrust::EntityType::Dimension(d) => {
|
||||
// Empty string resets to auto-measured value; otherwise set override.
|
||||
let base = d.base_mut();
|
||||
base.text = new_text;
|
||||
updated = true;
|
||||
is_dim = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if is_dim {
|
||||
// The edited override changed the dimension text; drop its
|
||||
// stale *D block so save re-bakes it. (#181)
|
||||
self.tabs[i].scene.invalidate_dim_block_recorded(handle);
|
||||
}
|
||||
if updated {
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(crate::t!("DDEDIT: text updated.").as_ref());
|
||||
} else {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line
|
||||
.push_error(crate::t!("DDEDIT: entity type not supported.").as_ref());
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
use super::*;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn handle_transform_selected(&mut self, mut handles: Vec<Handle>, transform: crate::command::EntityTransform) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
||||
if handles.is_empty() {
|
||||
self.tabs[i].active_cmd = None;
|
||||
return Some(Task::none());
|
||||
}
|
||||
if matches!(&transform, crate::command::EntityTransform::Translate(_)) {
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
handles = self.tabs[i]
|
||||
.scene
|
||||
.parametric_connected_handles(scope, &handles, false);
|
||||
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
||||
}
|
||||
let label = self.history_label_from_active_cmd(i, "MOVE");
|
||||
// A translation carries its complete connected constraint
|
||||
// component so each member keeps its own dimensions. Other
|
||||
// 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;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.refresh_properties();
|
||||
if let Some(p) = pending {
|
||||
self.commit_undo_delta(i, p);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn handle_copy_selected(&mut self, mut handles: Vec<Handle>, transform: crate::command::EntityTransform) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
||||
if handles.is_empty() {
|
||||
return Some(Task::none());
|
||||
}
|
||||
let label = self.history_label_from_active_cmd(i, "COPY");
|
||||
// Copying a dimension clones a *D block record, so gate delta on
|
||||
// the selection being dimension-free.
|
||||
let delta_safe = self.delta_copy_safe(i, &handles);
|
||||
let pending = self.begin_undo(i, label, handles.len(), delta_safe);
|
||||
let new_handles = self.tabs[i].scene.copy_entities(&handles, &transform);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
for h in new_handles {
|
||||
self.tabs[i].scene.select_entity(h, false);
|
||||
}
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||
if let Some(p) = prompt {
|
||||
self.command_line.push_info(&p);
|
||||
}
|
||||
self.refresh_properties();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn handle_align_selected(&mut self, result: CmdResult) {
|
||||
let i = self.active_tab;
|
||||
let CmdResult::AlignSelected {
|
||||
mut handles,
|
||||
src1,
|
||||
dst1,
|
||||
angle_rad,
|
||||
scale,
|
||||
} = result
|
||||
else {
|
||||
unreachable!("router only routes the matching variant");
|
||||
};
|
||||
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
||||
if handles.is_empty() {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
} else {
|
||||
let label = self.history_label_from_active_cmd(i, "ALIGN");
|
||||
// A chain of transform_entities on the same handles — the
|
||||
// recording keeps each entity's first (pre-align) image, so
|
||||
// it's delta-safe like a single transform.
|
||||
let pending = self.begin_undo(i, label, handles.len(), true);
|
||||
// Step 1: translate so src1 is at origin
|
||||
self.tabs[i].scene.transform_entities(
|
||||
&handles,
|
||||
&crate::command::EntityTransform::Translate(-src1),
|
||||
);
|
||||
// Step 2: uniform scale (only when != 1)
|
||||
if (scale - 1.0).abs() > 1e-4 {
|
||||
self.tabs[i].scene.transform_entities(
|
||||
&handles,
|
||||
&crate::command::EntityTransform::Scale {
|
||||
center: glam::DVec3::ZERO,
|
||||
factor: scale,
|
||||
},
|
||||
);
|
||||
}
|
||||
// Step 3: rotate in the XY plane by angle_rad
|
||||
if angle_rad.abs() > 1e-4 {
|
||||
self.tabs[i].scene.transform_entities(
|
||||
&handles,
|
||||
&crate::command::EntityTransform::Rotate {
|
||||
center: glam::DVec3::ZERO,
|
||||
axis: glam::DVec3::Z,
|
||||
angle_rad,
|
||||
},
|
||||
);
|
||||
}
|
||||
// Step 4: translate to dst1
|
||||
self.tabs[i].scene.transform_entities(
|
||||
&handles,
|
||||
&crate::command::EntityTransform::Translate(dst1),
|
||||
);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
for h in &handles {
|
||||
self.tabs[i].scene.select_entity(*h, false);
|
||||
}
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.command_line
|
||||
.push_output(crate::t!("ALIGN: applied.").as_ref());
|
||||
self.refresh_properties();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_stretch_window(&mut self, mut handles: Vec<Handle>, windows: Vec<(glam::DVec3, glam::DVec3)>) {
|
||||
let i = self.active_tab;
|
||||
// Accumulate every entity touched by any crossing window. Keep STRETCH
|
||||
// in its selection stage; Enter is what advances to the base point.
|
||||
{
|
||||
let scene = &self.tabs[i].scene;
|
||||
|
||||
for (win_min, win_max) in &windows {
|
||||
handles.extend(
|
||||
scene
|
||||
.interaction_handles_in_world_aabb([
|
||||
win_min.x, win_min.y, win_max.x, win_max.y,
|
||||
])
|
||||
.into_iter()
|
||||
.filter(|&handle| !scene.is_layer_locked(handle)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handles.sort_unstable_by_key(|handle| handle.value());
|
||||
handles.dedup();
|
||||
|
||||
if handles.is_empty() {
|
||||
self.command_line
|
||||
.push_output(crate::t!("STRETCH: nothing crosses the window.").as_ref());
|
||||
} else {
|
||||
// Visual feedback must only highlight entities that actually have a
|
||||
// stretchable point inside one of the crossing windows. `handles` can
|
||||
// contain the whole candidate/preselection set, while `windows` decides
|
||||
// which vertices will really move.
|
||||
let point_inside = |point: glam::DVec3| {
|
||||
const EPS: f64 = 1.0e-9;
|
||||
|
||||
windows.iter().any(|(win_min, win_max)| {
|
||||
point.x >= win_min.x - EPS
|
||||
&& point.x <= win_max.x + EPS
|
||||
&& point.y >= win_min.y - EPS
|
||||
&& point.y <= win_max.y + EPS
|
||||
&& point.z >= win_min.z - EPS
|
||||
&& point.z <= win_max.z + EPS
|
||||
})
|
||||
};
|
||||
|
||||
let mut visual_handles = Vec::new();
|
||||
|
||||
for &handle in &handles {
|
||||
let wires = self.tabs[i].scene.wire_models_for(&[handle]);
|
||||
|
||||
let affected = wires.iter().any(|wire| {
|
||||
// LINE / LWPOLYLINE / other vertex-based entities.
|
||||
let vertex_hit = wire
|
||||
.key_vertices
|
||||
.iter()
|
||||
.any(|point| point_inside(glam::DVec3::from_array(*point)));
|
||||
|
||||
if vertex_hit {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Whole-object STRETCH cases such as circles/arcs/inserts/points:
|
||||
// use only their meaningful anchor snap, not quadrants or
|
||||
// tessellated display points.
|
||||
wire.snap_pts.iter().any(|(point, hint)| {
|
||||
matches!(
|
||||
hint,
|
||||
crate::scene::model::wire_model::SnapHint::Center
|
||||
| crate::scene::model::wire_model::SnapHint::Insertion
|
||||
| crate::scene::model::wire_model::SnapHint::Node
|
||||
) && point_inside(*point)
|
||||
})
|
||||
});
|
||||
|
||||
if affected {
|
||||
visual_handles.push(handle);
|
||||
}
|
||||
}
|
||||
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].scene.select_entities(&visual_handles);
|
||||
self.refresh_selected_grips();
|
||||
// STRETCH temporary grips: keep only the actual vertices/endpoints
|
||||
// that lie inside one of the accumulated crossing windows.
|
||||
//
|
||||
// Mid-segment grips are deliberately excluded: STRETCH moves vertices,
|
||||
// not the midpoint affordances used by normal grip editing.
|
||||
let mut stretch_grips = Vec::new();
|
||||
let mut stretch_grip_handles = Vec::new();
|
||||
|
||||
for (handle, grip) in std::mem::take(&mut self.tabs[i].selected_grip_handles)
|
||||
.into_iter()
|
||||
.zip(std::mem::take(&mut self.tabs[i].selected_grips))
|
||||
{
|
||||
if !grip.is_midpoint && point_inside(grip.world) {
|
||||
stretch_grip_handles.push(handle);
|
||||
stretch_grips.push(grip);
|
||||
}
|
||||
}
|
||||
|
||||
self.tabs[i].selected_grip_handles = stretch_grip_handles;
|
||||
self.tabs[i].selected_grips = stretch_grips;
|
||||
}
|
||||
|
||||
use crate::command::CadCommand;
|
||||
use crate::modules::draw::modify::stretch::StretchCommand;
|
||||
|
||||
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
||||
|
||||
let cmd = StretchCommand::with_windows(handles, wires, windows);
|
||||
|
||||
self.command_line.push_info(&CadCommand::prompt(&cmd));
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
pub(super) fn handle_stretch_entities(&mut self, mut handles: Vec<Handle>, windows: Vec<(glam::DVec3, glam::DVec3)>, delta: glam::DVec3) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
||||
if handles.is_empty() {
|
||||
self.tabs[i].active_cmd = None;
|
||||
return Some(Task::none());
|
||||
}
|
||||
let structural = handles.iter().any(|handle| {
|
||||
matches!(
|
||||
self.tabs[i].scene.document.get_entity(*handle),
|
||||
Some(acadrust::EntityType::Dimension(_))
|
||||
)
|
||||
});
|
||||
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).
|
||||
let in_win = |x: f64, y: f64| -> bool {
|
||||
windows.iter().any(|(win_min, win_max)| {
|
||||
x >= win_min.x && x <= win_max.x && y >= win_min.y && y <= win_max.y
|
||||
})
|
||||
};
|
||||
|
||||
let dx = delta.x as f64;
|
||||
let dy = delta.y as f64; // drawing plane is world XY
|
||||
let dz = delta.z as f64;
|
||||
|
||||
// Dimensions whose points moved — their baked *D block is
|
||||
// stale afterwards and must be dropped (see #398 / #372).
|
||||
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;
|
||||
};
|
||||
let mut stretched = false;
|
||||
match entity {
|
||||
acadrust::EntityType::Line(l) => {
|
||||
let s_in = in_win(l.start.x, l.start.y);
|
||||
let e_in = in_win(l.end.x, l.end.y);
|
||||
if s_in {
|
||||
l.start.x += dx;
|
||||
l.start.y += dy;
|
||||
l.start.z += dz;
|
||||
stretched = true;
|
||||
}
|
||||
if e_in {
|
||||
l.end.x += dx;
|
||||
l.end.y += dy;
|
||||
l.end.z += dz;
|
||||
stretched = true;
|
||||
}
|
||||
}
|
||||
acadrust::EntityType::LwPolyline(p) => {
|
||||
let Some(mut world) = crate::entities::curve::lwpolyline_world_xy(p)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for v in &mut world.vertices {
|
||||
if in_win(v.location.x, v.location.y) {
|
||||
v.location.x += dx;
|
||||
v.location.y += dy;
|
||||
stretched = true;
|
||||
}
|
||||
}
|
||||
if stretched {
|
||||
*p = world;
|
||||
}
|
||||
}
|
||||
acadrust::EntityType::Polyline2D(p) => {
|
||||
for v in &mut p.vertices {
|
||||
if in_win(v.location.x, v.location.y) {
|
||||
v.location.x += dx;
|
||||
v.location.y += dy;
|
||||
stretched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
acadrust::EntityType::Polyline(p) => {
|
||||
for v in &mut p.vertices {
|
||||
if in_win(v.location.x, v.location.z) {
|
||||
v.location.x += dx;
|
||||
v.location.z += dy;
|
||||
stretched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
acadrust::EntityType::Arc(a) => {
|
||||
if in_win(a.center.x, a.center.y) {
|
||||
a.center.x += dx;
|
||||
a.center.y += dy;
|
||||
a.center.z += dz;
|
||||
stretched = true;
|
||||
}
|
||||
}
|
||||
acadrust::EntityType::Circle(c) => {
|
||||
if in_win(c.center.x, c.center.y) {
|
||||
c.center.x += dx;
|
||||
c.center.y += dy;
|
||||
c.center.z += dz;
|
||||
stretched = true;
|
||||
}
|
||||
}
|
||||
acadrust::EntityType::Ellipse(e) => {
|
||||
if in_win(e.center.x, e.center.y) {
|
||||
e.center.x += dx;
|
||||
e.center.y += dy;
|
||||
e.center.z += dz;
|
||||
stretched = true;
|
||||
}
|
||||
}
|
||||
acadrust::EntityType::Insert(ins) => {
|
||||
if in_win(ins.insert_point.x, ins.insert_point.y) {
|
||||
ins.insert_point.x += dx;
|
||||
ins.insert_point.y += dy;
|
||||
ins.insert_point.z += dz;
|
||||
stretched = true;
|
||||
}
|
||||
}
|
||||
acadrust::EntityType::Text(t) => {
|
||||
if in_win(t.insertion_point.x, t.insertion_point.y) {
|
||||
t.insertion_point.x += dx;
|
||||
t.insertion_point.y += dy;
|
||||
t.insertion_point.z += dz;
|
||||
stretched = true;
|
||||
}
|
||||
}
|
||||
acadrust::EntityType::MText(t) => {
|
||||
if in_win(t.insertion_point.x, t.insertion_point.y) {
|
||||
t.insertion_point.x += dx;
|
||||
t.insertion_point.y += dy;
|
||||
t.insertion_point.z += dz;
|
||||
stretched = true;
|
||||
}
|
||||
}
|
||||
acadrust::EntityType::Viewport(vp) => {
|
||||
stretched = windows.iter().any(|(win_min, win_max)| {
|
||||
crate::entities::viewport::stretch(vp, *win_min, *win_max, delta)
|
||||
});
|
||||
}
|
||||
acadrust::EntityType::Dimension(dim) => {
|
||||
use acadrust::entities::Dimension;
|
||||
// Move every definition point that falls inside the
|
||||
// window — the same points the grips expose — then
|
||||
// refresh the stored measurement so the value tracks
|
||||
// the stretched geometry.
|
||||
let mv = |p: &mut acadrust::types::Vector3| {
|
||||
if in_win(p.x, p.y) {
|
||||
p.x += dx;
|
||||
p.y += dy;
|
||||
p.z += dz;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
match dim {
|
||||
Dimension::Linear(d) => {
|
||||
stretched |= mv(&mut d.first_point);
|
||||
stretched |= mv(&mut d.second_point);
|
||||
stretched |= mv(&mut d.definition_point);
|
||||
}
|
||||
Dimension::Aligned(d) => {
|
||||
stretched |= mv(&mut d.first_point);
|
||||
stretched |= mv(&mut d.second_point);
|
||||
stretched |= mv(&mut d.definition_point);
|
||||
}
|
||||
Dimension::Radius(d) => {
|
||||
stretched |= mv(&mut d.angle_vertex);
|
||||
stretched |= mv(&mut d.definition_point);
|
||||
}
|
||||
Dimension::Diameter(d) => {
|
||||
stretched |= mv(&mut d.angle_vertex);
|
||||
stretched |= mv(&mut d.definition_point);
|
||||
}
|
||||
Dimension::Angular2Ln(d) => {
|
||||
stretched |= mv(&mut d.angle_vertex);
|
||||
stretched |= mv(&mut d.first_point);
|
||||
stretched |= mv(&mut d.second_point);
|
||||
stretched |= mv(&mut d.definition_point);
|
||||
}
|
||||
Dimension::Angular3Pt(d) => {
|
||||
stretched |= mv(&mut d.angle_vertex);
|
||||
stretched |= mv(&mut d.first_point);
|
||||
stretched |= mv(&mut d.second_point);
|
||||
stretched |= mv(&mut d.definition_point);
|
||||
}
|
||||
Dimension::Ordinate(d) => {
|
||||
stretched |= mv(&mut d.feature_location);
|
||||
stretched |= mv(&mut d.leader_endpoint);
|
||||
}
|
||||
Dimension::Arc(d) => {
|
||||
stretched |= mv(&mut d.definition_point);
|
||||
stretched |= mv(&mut d.first_extension_point);
|
||||
stretched |= mv(&mut d.second_extension_point);
|
||||
stretched |= mv(&mut d.center_point);
|
||||
if d.has_leader {
|
||||
stretched |= mv(&mut d.first_leader_point);
|
||||
stretched |= mv(&mut d.second_leader_point);
|
||||
}
|
||||
}
|
||||
Dimension::LargeRadial(d) => {
|
||||
stretched |= mv(&mut d.definition_point);
|
||||
stretched |= mv(&mut d.chord_point);
|
||||
stretched |= mv(&mut d.override_center);
|
||||
stretched |= mv(&mut d.jog_point);
|
||||
}
|
||||
}
|
||||
// Pinned text follows too; the zero sentinel means
|
||||
// "auto placement" and must not be captured by a
|
||||
// window that happens to cover the origin.
|
||||
let t = dim.base().text_middle_point;
|
||||
if t.x * t.x + t.y * t.y + t.z * t.z > 1e-16 {
|
||||
let mut t = t;
|
||||
if mv(&mut t) {
|
||||
dim.base_mut().text_middle_point = t;
|
||||
stretched = true;
|
||||
}
|
||||
}
|
||||
if stretched {
|
||||
dim.base_mut().actual_measurement = dim.measurement();
|
||||
stretched_dims.push(*handle);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Generic: move entire entity (treat as block-level)
|
||||
stretched = false; // skip generic types
|
||||
}
|
||||
}
|
||||
if stretched {
|
||||
if let Some(before) = before {
|
||||
self.tabs[i].scene.record_undo_before(*handle, Some(before));
|
||||
}
|
||||
self.tabs[i].scene.mark_entity_dirty(*handle);
|
||||
changed_handles.push(*handle);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
// A stretched dimension renders through its baked *D block when
|
||||
// one exists (file roundtrip) — drop it so tessellation falls
|
||||
// back to the live points and the next save re-bakes. (#372)
|
||||
for h in stretched_dims {
|
||||
self.tabs[i].scene.invalidate_dim_block_recorded(h);
|
||||
}
|
||||
|
||||
// Geometry was edited in place via get_entity_mut, which the
|
||||
// scene's tessellation cache doesn't observe. Re-tessellate the
|
||||
// moved entities so the viewport reflects the stretch right away
|
||||
// instead of only on the next unrelated redraw. See #95.
|
||||
if count > 0 {
|
||||
let changes: Vec<_> = changed_handles
|
||||
.iter()
|
||||
.map(|&handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
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;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.command_line
|
||||
.push_output(crate::tf!("STRETCH: {count} entity(ies) stretched.").as_ref());
|
||||
self.refresh_properties();
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,414 @@
|
||||
use super::*;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn handle_mview_create(&mut self, viewport: acadrust::entities::Viewport, preserve_view: bool) {
|
||||
let i = self.active_tab;
|
||||
let saved_view = preserve_view.then(|| {
|
||||
(
|
||||
viewport.view_target.clone(),
|
||||
viewport.view_direction.clone(),
|
||||
viewport.view_center.clone(),
|
||||
viewport.view_height,
|
||||
viewport.custom_scale,
|
||||
viewport.lens_length,
|
||||
viewport.twist_angle,
|
||||
viewport.status.perspective,
|
||||
)
|
||||
});
|
||||
let label = self.history_label_from_active_cmd(i, "MVIEW");
|
||||
let pending = self.begin_undo(i, label, 1, true);
|
||||
let handle = self.commit_entity_handle(acadrust::EntityType::Viewport(viewport));
|
||||
if let (Some(handle), Some(saved)) = (handle, saved_view) {
|
||||
if let Some(acadrust::EntityType::Viewport(viewport)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
viewport.view_target = saved.0;
|
||||
viewport.view_direction = saved.1;
|
||||
viewport.view_center = saved.2;
|
||||
viewport.view_height = saved.3;
|
||||
viewport.custom_scale = saved.4;
|
||||
viewport.lens_length = saved.5;
|
||||
viewport.twist_angle = saved.6;
|
||||
viewport.status.perspective = saved.7;
|
||||
}
|
||||
self.tabs[i].scene.camera_generation += 1;
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_mview_create_clipped(
|
||||
&mut self,
|
||||
boundary: Option<acadrust::EntityType>,
|
||||
boundary_handle: Handle,
|
||||
) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
if boundary.is_none() {
|
||||
let scene = &self.tabs[i].scene;
|
||||
let valid = scene.entity_belongs_to_current_layout(boundary_handle)
|
||||
&& scene
|
||||
.document
|
||||
.get_entity(boundary_handle)
|
||||
.is_some_and(|entity| match entity {
|
||||
acadrust::EntityType::Circle(_) => true,
|
||||
acadrust::EntityType::Ellipse(ellipse) => ellipse.is_full(),
|
||||
acadrust::EntityType::LwPolyline(polyline) => polyline.is_closed,
|
||||
acadrust::EntityType::Polyline(polyline) => polyline.is_closed(),
|
||||
acadrust::EntityType::Polyline2D(polyline) => polyline.is_closed(),
|
||||
acadrust::EntityType::Polyline3D(polyline) => polyline.flags.closed,
|
||||
_ => false,
|
||||
});
|
||||
if !valid {
|
||||
self.command_line.push_error(
|
||||
crate::t!(
|
||||
"MVIEW Object: select a closed paper-space circle, ellipse, or polyline."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
if let Some(prompt) = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|command| command.prompt())
|
||||
{
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
return Some(Task::none());
|
||||
}
|
||||
}
|
||||
|
||||
let created_boundary = boundary.is_some();
|
||||
let touched = 2;
|
||||
let label = self.history_label_from_active_cmd(i, "MVIEW");
|
||||
let pending = self.begin_undo(i, label, touched, true);
|
||||
let clip_handle = match boundary {
|
||||
Some(mut boundary) => {
|
||||
// A non-rectangular viewport owns a helper boundary
|
||||
// entity through `clip_boundary_handle`. Keep that
|
||||
// helper in the document for DWG compatibility and
|
||||
// stencil clipping, but do not expose it as a separate
|
||||
// selectable polyline.
|
||||
boundary.common_mut().invisible = true;
|
||||
match self.commit_entity_handle(boundary) {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
self.tabs[i].active_cmd = None;
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
return Some(Task::none());
|
||||
}
|
||||
}
|
||||
}
|
||||
None => boundary_handle,
|
||||
};
|
||||
let polygon = self.tabs[i].scene.clip_boundary_polygon(clip_handle, 0.0);
|
||||
let bounds: Option<(f64, f64, f64, f64)> = polygon.iter().fold(None, |bounds, point| {
|
||||
if !point[0].is_finite() || !point[1].is_finite() {
|
||||
return bounds;
|
||||
}
|
||||
Some(match bounds {
|
||||
Some((min_x, min_y, max_x, max_y)) => (
|
||||
min_x.min(point[0] as f64),
|
||||
min_y.min(point[1] as f64),
|
||||
max_x.max(point[0] as f64),
|
||||
max_y.max(point[1] as f64),
|
||||
),
|
||||
None => (
|
||||
point[0] as f64,
|
||||
point[1] as f64,
|
||||
point[0] as f64,
|
||||
point[1] as f64,
|
||||
),
|
||||
})
|
||||
});
|
||||
let Some((min_x, min_y, max_x, max_y)) = bounds else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("MVIEW: the clipping boundary has no usable area.").as_ref());
|
||||
self.tabs[i].active_cmd = None;
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
return Some(Task::none());
|
||||
};
|
||||
if max_x - min_x < 1e-6 || max_y - min_y < 1e-6 {
|
||||
self.command_line
|
||||
.push_error(crate::t!("MVIEW: the clipping boundary has no usable area.").as_ref());
|
||||
self.tabs[i].active_cmd = None;
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
||||
let mut viewport = acadrust::entities::Viewport::new();
|
||||
viewport.center =
|
||||
acadrust::types::Vector3::new((min_x + max_x) / 2.0, (min_y + max_y) / 2.0, 0.0);
|
||||
viewport.width = max_x - min_x;
|
||||
viewport.height = max_y - min_y;
|
||||
viewport.id = 2;
|
||||
viewport.clip_boundary_handle = clip_handle;
|
||||
let viewport_handle = self.commit_entity_handle(acadrust::EntityType::Viewport(viewport));
|
||||
if let Some(viewport_handle) = viewport_handle {
|
||||
if !created_boundary {
|
||||
let before = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(clip_handle)
|
||||
.cloned()
|
||||
.map(std::sync::Arc::new);
|
||||
self.tabs[i].scene.record_undo_before(clip_handle, before);
|
||||
}
|
||||
if let Some(boundary) = self.tabs[i].scene.document.get_entity_mut(clip_handle) {
|
||||
let common = boundary.common_mut();
|
||||
common.invisible = true;
|
||||
if !common.reactors.contains(&viewport_handle) {
|
||||
common.reactors.push(viewport_handle);
|
||||
}
|
||||
}
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(clip_handle, crate::scene::ChangeKind::Modified)]);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn handle_wipeout_from_polyline(
|
||||
&mut self,
|
||||
handle: Handle,
|
||||
erase_source: bool,
|
||||
) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
let wipeout = {
|
||||
let scene = &self.tabs[i].scene;
|
||||
scene
|
||||
.entity_belongs_to_active_space(handle)
|
||||
.then(|| scene.document.get_entity(handle))
|
||||
.flatten()
|
||||
.and_then(crate::modules::draw::draw::wipeout::wipeout_from_polyline)
|
||||
};
|
||||
if let Some(wipeout) = wipeout {
|
||||
if erase_source {
|
||||
return Some(self.apply_cmd_result(CmdResult::ReplaceMany(
|
||||
vec![(handle, Vec::new())],
|
||||
vec![wipeout],
|
||||
)));
|
||||
}
|
||||
return Some(self.apply_cmd_result(CmdResult::CommitAndExit(wipeout)));
|
||||
}
|
||||
self.command_line.push_error(
|
||||
crate::t!("WIPEOUT Polyline: select a straight, closed, planar 2D polyline with at least 3 non-intersecting vertices.").as_ref(),
|
||||
);
|
||||
let command = crate::modules::draw::draw::wipeout::WipeoutCommand::new_polyline();
|
||||
self.command_line
|
||||
.push_info(&crate::command::CadCommand::prompt(&command));
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn handle_mview_switch_layout(&mut self, layout: String) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let task = self.on_layout_switch_preserving_command(layout);
|
||||
if let Some(prompt) = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|command| command.prompt())
|
||||
{
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
task
|
||||
}
|
||||
|
||||
pub(super) fn handle_mview_cancel_to_layout(&mut self, layout: String) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.on_layout_switch(layout)
|
||||
}
|
||||
|
||||
pub(super) fn handle_vp_layer_update(
|
||||
&mut self,
|
||||
vp_handle: Handle,
|
||||
freeze: Vec<String>,
|
||||
thaw: Vec<String>,
|
||||
) {
|
||||
let i = self.active_tab;
|
||||
// Resolve layer names → handles, then update frozen_layers on the viewport(s).
|
||||
// vp_handle == Handle::NULL means "apply to all viewports in current layout".
|
||||
let freeze_handles: Vec<Handle> = freeze
|
||||
.iter()
|
||||
.filter_map(|name| {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.find(|l| l.name.eq_ignore_ascii_case(name))
|
||||
.map(|l| l.handle)
|
||||
})
|
||||
.collect();
|
||||
let thaw_handles: Vec<Handle> = thaw
|
||||
.iter()
|
||||
.filter_map(|name| {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.find(|l| l.name.eq_ignore_ascii_case(name))
|
||||
.map(|l| l.handle)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut frozen_count = 0usize;
|
||||
let mut thawed_count = 0usize;
|
||||
|
||||
// Collect target viewport handles
|
||||
let target_handles: Vec<Handle> = if vp_handle == acadrust::Handle::NULL {
|
||||
// All viewports in current layout block
|
||||
let block_handle = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter(|e| {
|
||||
e.common().owner_handle == block_handle
|
||||
&& matches!(e, acadrust::EntityType::Viewport(_))
|
||||
})
|
||||
.map(|e| e.common().handle)
|
||||
.collect()
|
||||
} else {
|
||||
vec![vp_handle]
|
||||
};
|
||||
|
||||
for &target_handle in &target_handles {
|
||||
if let Some(acadrust::EntityType::Viewport(vp)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(target_handle)
|
||||
{
|
||||
for h in &freeze_handles {
|
||||
if !vp.frozen_layers.contains(h) {
|
||||
vp.frozen_layers.push(*h);
|
||||
frozen_count += 1;
|
||||
}
|
||||
}
|
||||
for h in &thaw_handles {
|
||||
let before = vp.frozen_layers.len();
|
||||
vp.frozen_layers.retain(|fh| fh != h);
|
||||
if vp.frozen_layers.len() < before {
|
||||
thawed_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if frozen_count > 0 || thawed_count > 0 {
|
||||
self.push_undo_snapshot(i, "VPLAYER");
|
||||
self.tabs[i].dirty = true;
|
||||
if frozen_count > 0 {
|
||||
self.command_line.push_info(
|
||||
crate::tf!("VPLAYER: {frozen_count} layer(s) frozen in viewport.").as_ref(),
|
||||
);
|
||||
}
|
||||
if thawed_count > 0 {
|
||||
self.command_line.push_info(
|
||||
crate::tf!("VPLAYER: {thawed_count} layer(s) thawed in viewport.").as_ref(),
|
||||
);
|
||||
}
|
||||
// Sync layer panel so VP freeze columns update immediately.
|
||||
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
||||
let vp_info = self.tabs[i].scene.viewport_list();
|
||||
self.tabs[i]
|
||||
.layers
|
||||
.sync_with_viewports(&doc_layers, vp_info);
|
||||
}
|
||||
|
||||
// Show updated prompt (command stays active for more operations).
|
||||
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||
if let Some(p) = prompt {
|
||||
self.command_line.push_info(&p);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_zoom_to_window(&mut self, p1: glam::DVec3, p2: glam::DVec3) {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].scene.remember_current_view();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.zoom_to_window(p1.as_vec3(), p2.as_vec3());
|
||||
self.command_line
|
||||
.push_output(crate::t!("Zoom Window").as_ref());
|
||||
}
|
||||
|
||||
pub(super) fn handle_set_plot_window(&mut self, p1: glam::DVec3, p2: glam::DVec3) {
|
||||
let i = self.active_tab;
|
||||
let layout_name = self.tabs[i].scene.current_layout.clone();
|
||||
if layout_name == "Model" {
|
||||
// Model space: remember the window (world X/Y) for the plot dialog.
|
||||
let x0 = p1.x.min(p2.x);
|
||||
let y0 = p1.y.min(p2.y);
|
||||
let x1 = p1.x.max(p2.x);
|
||||
let y1 = p1.y.max(p2.y);
|
||||
self.plot_window = Some((x0, y0, x1, y1));
|
||||
self.plot_dialog.window = self.plot_window;
|
||||
self.command_line.push_output(
|
||||
crate::tf!("Plot window: {x0:.2},{y0:.2} to {x1:.2},{y1:.2}").as_ref(),
|
||||
);
|
||||
// Pick window closed the plot dialog so the viewport could
|
||||
// receive the two clicks — bring the dialog back with the
|
||||
// window now active.
|
||||
self.plot_dialog.area = "Window".to_string();
|
||||
// Remember the pick immediately, like the printer choice.
|
||||
self.save_config();
|
||||
self.active_modal = Some(super::super::ModalKind::Plot);
|
||||
} else {
|
||||
// PLOTWINDOW always describes the plotted layout. In MSPACE
|
||||
// the command points are model coordinates, so map them back
|
||||
// through the active floating viewport first.
|
||||
let p1 = self.tabs[i].scene.model_to_paper(p1);
|
||||
let p2 = self.tabs[i].scene.model_to_paper(p2);
|
||||
let x1 = p1.x.min(p2.x);
|
||||
let y1 = p1.y.min(p2.y);
|
||||
let x2 = p1.x.max(p2.x);
|
||||
let y2 = p1.y.max(p2.y);
|
||||
self.plot_window = Some((x1, y1, x2, y2));
|
||||
self.plot_dialog.window = self.plot_window;
|
||||
self.command_line.push_output(
|
||||
crate::tf!("Plot window: {x1:.2},{y1:.2} to {x2:.2},{y2:.2}").as_ref(),
|
||||
);
|
||||
self.plot_dialog.area = "Window".to_string();
|
||||
self.save_config();
|
||||
self.active_modal = Some(super::super::ModalKind::Plot);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
}
|
||||
|
||||
pub(super) fn handle_quick_print(&mut self, handles: Vec<Handle>) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.on_quick_print_handles(handles)
|
||||
}
|
||||
}
|
||||
@@ -37,15 +37,9 @@ impl OpenCADStudio {
|
||||
handles: &[acadrust::Handle],
|
||||
base: glam::DVec3,
|
||||
) -> usize {
|
||||
let (entities, deps) = {
|
||||
let document = &self.tabs[i].scene.document;
|
||||
let entities: Vec<_> = handles
|
||||
.iter()
|
||||
.filter_map(|&handle| document.get_entity(handle).cloned())
|
||||
.collect();
|
||||
let deps = super::super::ClipboardDeps::capture(document, &entities);
|
||||
(entities, deps)
|
||||
};
|
||||
// Clone + dep-capture live in the shared kernel; storage stays here.
|
||||
let (entities, deps) =
|
||||
super::super::command_driver::copy_to_clipboard_kernel(&self.tabs[i].scene.document, handles);
|
||||
let count = entities.len();
|
||||
self.clipboard_base = base;
|
||||
self.clipboard = entities;
|
||||
|
||||
@@ -0,0 +1,688 @@
|
||||
//! MATCHPROP special-property kernels (#281).
|
||||
//!
|
||||
//! Free functions shared by the MATCHPROP handler and headless callers
|
||||
//! (buttons, MCP, plugins). No `OpenCADStudio` state required.
|
||||
|
||||
/// MATCHPROP special properties (#281): copy every STYLE-affecting field from
|
||||
/// `src` to `dst` when the destination supports it — never content (text
|
||||
/// strings, block names) or placement (positions, rotations of the object
|
||||
/// itself). Text formatting crosses TEXT ↔ MTEXT; the dimension style crosses
|
||||
/// Dimension / Leader / Tolerance.
|
||||
pub fn match_special_props(src: &acadrust::EntityType, dst: &mut acadrust::EntityType) {
|
||||
use acadrust::EntityType as E;
|
||||
|
||||
// Text-ish source formatting.
|
||||
let text_fmt = match src {
|
||||
E::Text(t) => Some((
|
||||
t.style.clone(),
|
||||
t.height,
|
||||
Some(t.width_factor),
|
||||
Some(t.oblique_angle),
|
||||
)),
|
||||
E::MText(m) => Some((m.style.clone(), m.height, None, None)),
|
||||
E::AttributeDefinition(a) => Some((
|
||||
a.text_style.clone(),
|
||||
a.height,
|
||||
Some(a.width_factor),
|
||||
Some(a.oblique_angle),
|
||||
)),
|
||||
E::AttributeEntity(a) => Some((
|
||||
a.text_style.clone(),
|
||||
a.height,
|
||||
Some(a.width_factor),
|
||||
Some(a.oblique_angle),
|
||||
)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some((style, height, wf, ob)) = text_fmt {
|
||||
match dst {
|
||||
E::Text(t) => {
|
||||
t.style = style.clone();
|
||||
t.height = height;
|
||||
if let Some(value) = wf {
|
||||
t.width_factor = value;
|
||||
}
|
||||
if let Some(value) = ob {
|
||||
t.oblique_angle = value;
|
||||
}
|
||||
}
|
||||
E::MText(m) => {
|
||||
m.style = style.clone();
|
||||
m.height = height;
|
||||
}
|
||||
E::AttributeDefinition(a) => {
|
||||
a.text_style = style.clone();
|
||||
a.height = height;
|
||||
if let Some(value) = wf {
|
||||
a.width_factor = value;
|
||||
}
|
||||
if let Some(value) = ob {
|
||||
a.oblique_angle = value;
|
||||
}
|
||||
}
|
||||
E::AttributeEntity(a) => {
|
||||
a.text_style = style;
|
||||
a.height = height;
|
||||
if let Some(value) = wf {
|
||||
a.width_factor = value;
|
||||
}
|
||||
if let Some(value) = ob {
|
||||
a.oblique_angle = value;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// MText-only extras (line spacing, background fill).
|
||||
if let (E::MText(sm), E::MText(dm)) = (src, dst as &mut E) {
|
||||
dm.drawing_direction = sm.drawing_direction;
|
||||
dm.line_spacing_factor = sm.line_spacing_factor;
|
||||
dm.line_spacing_style = sm.line_spacing_style;
|
||||
dm.background_fill_flags = sm.background_fill_flags;
|
||||
dm.background_scale = sm.background_scale;
|
||||
dm.background_color = sm.background_color;
|
||||
dm.background_transparency = sm.background_transparency;
|
||||
}
|
||||
|
||||
// Dimension style name crosses the three dim-styled families.
|
||||
let dim_style = match src {
|
||||
E::Dimension(d) => Some(d.base().style_name.clone()),
|
||||
E::Leader(l) => Some(l.dimension_style.clone()),
|
||||
E::Tolerance(t) => Some(t.dimension_style_name.clone()),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(ds) = dim_style {
|
||||
match dst {
|
||||
E::Dimension(d) => d.base_mut().style_name = ds,
|
||||
E::Leader(l) => l.dimension_style = ds,
|
||||
E::Tolerance(t) => t.dimension_style_name = ds,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Hatch pattern / gradient — everything but the boundary.
|
||||
if let (E::Hatch(sh), E::Hatch(dh)) = (src, dst as &mut E) {
|
||||
dh.pattern = sh.pattern.clone();
|
||||
dh.pattern_type = sh.pattern_type;
|
||||
dh.pattern_angle = sh.pattern_angle;
|
||||
dh.pattern_scale = sh.pattern_scale;
|
||||
dh.is_solid = sh.is_solid;
|
||||
dh.is_double = sh.is_double;
|
||||
dh.style = sh.style;
|
||||
dh.gradient_color = sh.gradient_color.clone();
|
||||
}
|
||||
|
||||
// Polyline display style crosses lightweight and legacy 2D polylines.
|
||||
// Per-vertex/tapered widths are resampled over the destination vertices;
|
||||
// they must not be flattened into the source's constant-width field.
|
||||
if let Some(style) = PolylineMatchStyle::from_entity(src) {
|
||||
style.apply_to(dst);
|
||||
}
|
||||
|
||||
if let (E::Leader(sl), E::Leader(dl)) = (src, dst as &mut E) {
|
||||
dl.arrow_enabled = sl.arrow_enabled;
|
||||
dl.path_type = sl.path_type;
|
||||
dl.hookline_direction = sl.hookline_direction;
|
||||
dl.hookline_enabled = sl.hookline_enabled;
|
||||
dl.override_color = sl.override_color;
|
||||
dl.dimension_gap = sl.dimension_gap;
|
||||
dl.arrowhead_type = sl.arrowhead_type;
|
||||
dl.arrow_size = sl.arrow_size;
|
||||
dl.byblock_color = sl.byblock_color;
|
||||
}
|
||||
if let (E::Tolerance(st), E::Tolerance(dt)) = (src, dst as &mut E) {
|
||||
dt.dimension_style_handle = st.dimension_style_handle;
|
||||
dt.text_height = st.text_height;
|
||||
dt.dimension_gap = st.dimension_gap;
|
||||
}
|
||||
|
||||
// Paper-space geometry and view position stay; viewport display/plot
|
||||
// styling and effective scale follow the source.
|
||||
if let (E::Viewport(sv), E::Viewport(dv)) = (src, dst as &mut E) {
|
||||
let scale = crate::scene::vp_effective_scale(sv.custom_scale, sv.view_height, sv.height);
|
||||
dv.custom_scale = scale;
|
||||
if scale.abs() > 1e-9 {
|
||||
dv.view_height = dv.height / scale;
|
||||
}
|
||||
dv.status.locked = sv.status.locked;
|
||||
dv.status.hide_plot = sv.status.hide_plot;
|
||||
dv.render_mode = sv.render_mode;
|
||||
dv.style_sheet = sv.style_sheet.clone();
|
||||
dv.shade_plot_mode = sv.shade_plot_mode;
|
||||
dv.background_handle = sv.background_handle;
|
||||
dv.shade_plot_handle = sv.shade_plot_handle;
|
||||
dv.visual_style_handle = sv.visual_style_handle;
|
||||
dv.default_lighting = sv.default_lighting;
|
||||
dv.default_lighting_type = sv.default_lighting_type;
|
||||
dv.brightness = sv.brightness;
|
||||
dv.contrast = sv.contrast;
|
||||
dv.ambient_color = sv.ambient_color;
|
||||
}
|
||||
|
||||
if let (E::Table(st), E::Table(dt)) = (src, dst as &mut E) {
|
||||
dt.table_style_handle = st.table_style_handle;
|
||||
dt.base_style = st.base_style.clone();
|
||||
dt.override_flag = st.override_flag;
|
||||
dt.override_border_color = st.override_border_color;
|
||||
dt.override_border_line_weight = st.override_border_line_weight;
|
||||
dt.override_border_visibility = st.override_border_visibility;
|
||||
dt.legacy_style_override = st.legacy_style_override.clone();
|
||||
dt.legacy_border_colors = st.legacy_border_colors.clone();
|
||||
dt.legacy_border_line_weights = st.legacy_border_line_weights.clone();
|
||||
dt.legacy_border_visibility = st.legacy_border_visibility.clone();
|
||||
}
|
||||
|
||||
if let (E::MLine(sm), E::MLine(dm)) = (src, dst as &mut E) {
|
||||
dm.style_handle = sm.style_handle;
|
||||
dm.style_name = sm.style_name.clone();
|
||||
dm.style_element_count = sm.style_element_count;
|
||||
dm.justification = sm.justification;
|
||||
dm.scale_factor = sm.scale_factor;
|
||||
// Stored segment offsets bake the old style into each vertex. Empty
|
||||
// data deliberately selects the renderer's style-derived fallback.
|
||||
for vertex in &mut dm.vertices {
|
||||
vertex.segments.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// MultiLeader style + every style-affecting override; content and
|
||||
// geometry (leader points, text, block handle) stay.
|
||||
if let (E::MultiLeader(sm), E::MultiLeader(dm)) = (src, dst as &mut E) {
|
||||
dm.style_handle = sm.style_handle;
|
||||
dm.path_type = sm.path_type;
|
||||
dm.line_color = sm.line_color;
|
||||
dm.line_type_handle = sm.line_type_handle;
|
||||
dm.line_weight = sm.line_weight;
|
||||
dm.enable_landing = sm.enable_landing;
|
||||
dm.enable_dogleg = sm.enable_dogleg;
|
||||
dm.dogleg_length = sm.dogleg_length;
|
||||
dm.arrowhead_handle = sm.arrowhead_handle;
|
||||
dm.arrowhead_size = sm.arrowhead_size;
|
||||
dm.text_style_handle = sm.text_style_handle;
|
||||
dm.text_color = sm.text_color;
|
||||
dm.text_frame = sm.text_frame;
|
||||
dm.text_height = sm.text_height;
|
||||
dm.text_left_attachment = sm.text_left_attachment;
|
||||
dm.text_right_attachment = sm.text_right_attachment;
|
||||
dm.text_top_attachment = sm.text_top_attachment;
|
||||
dm.text_bottom_attachment = sm.text_bottom_attachment;
|
||||
dm.text_attachment_direction = sm.text_attachment_direction;
|
||||
dm.text_attachment_point = sm.text_attachment_point;
|
||||
dm.text_alignment = sm.text_alignment;
|
||||
dm.text_angle_type = sm.text_angle_type;
|
||||
dm.text_direction_negative = sm.text_direction_negative;
|
||||
dm.text_align_in_ipe = sm.text_align_in_ipe;
|
||||
dm.block_content_color = sm.block_content_color;
|
||||
dm.block_connection_type = sm.block_connection_type;
|
||||
dm.block_scale = sm.block_scale;
|
||||
dm.scale_factor = sm.scale_factor;
|
||||
dm.property_override_flags = sm.property_override_flags;
|
||||
dm.enable_annotation_scale = sm.enable_annotation_scale;
|
||||
dm.extend_leader_to_text = sm.extend_leader_to_text;
|
||||
dm.arrowhead_overrides = sm.arrowhead_overrides.clone();
|
||||
|
||||
let sc = &sm.context;
|
||||
let dc = &mut dm.context;
|
||||
dc.scale_factor = sc.scale_factor;
|
||||
dc.text_height = sc.text_height;
|
||||
dc.text_width = sc.text_width;
|
||||
dc.text_boundary_height = sc.text_boundary_height;
|
||||
dc.line_spacing_factor = sc.line_spacing_factor;
|
||||
dc.line_spacing_style = sc.line_spacing_style;
|
||||
dc.text_color = sc.text_color;
|
||||
dc.text_attachment_point = sc.text_attachment_point;
|
||||
dc.text_flow_direction = sc.text_flow_direction;
|
||||
dc.text_alignment = sc.text_alignment;
|
||||
dc.text_left_attachment = sc.text_left_attachment;
|
||||
dc.text_right_attachment = sc.text_right_attachment;
|
||||
dc.text_top_attachment = sc.text_top_attachment;
|
||||
dc.text_bottom_attachment = sc.text_bottom_attachment;
|
||||
dc.text_height_automatic = sc.text_height_automatic;
|
||||
dc.word_break = sc.word_break;
|
||||
dc.text_style_handle = sc.text_style_handle;
|
||||
dc.block_content_scale = sc.block_content_scale;
|
||||
dc.block_content_color = sc.block_content_color;
|
||||
dc.block_connection_type = sc.block_connection_type;
|
||||
dc.column_type = sc.column_type;
|
||||
dc.column_width = sc.column_width;
|
||||
dc.column_gutter = sc.column_gutter;
|
||||
dc.column_flow_reversed = sc.column_flow_reversed;
|
||||
dc.column_sizes = sc.column_sizes.clone();
|
||||
dc.background_fill_enabled = sc.background_fill_enabled;
|
||||
dc.background_mask_fill_on = sc.background_mask_fill_on;
|
||||
dc.background_fill_color = sc.background_fill_color;
|
||||
dc.background_scale_factor = sc.background_scale_factor;
|
||||
dc.background_transparency = sc.background_transparency;
|
||||
dc.arrowhead_size = sc.arrowhead_size;
|
||||
dc.landing_gap = sc.landing_gap;
|
||||
dc.scale_handle = sc.scale_handle;
|
||||
}
|
||||
|
||||
// External-reference identity, placement and clip geometry stay. Only
|
||||
// display controls/appearance are matched.
|
||||
if let (E::RasterImage(si), E::RasterImage(di)) = (src, dst as &mut E) {
|
||||
di.flags = si.flags;
|
||||
di.clipping_enabled = si.clipping_enabled;
|
||||
di.brightness = si.brightness;
|
||||
di.contrast = si.contrast;
|
||||
di.fade = si.fade;
|
||||
di.clip_boundary.clip_mode = si.clip_boundary.clip_mode;
|
||||
}
|
||||
if let (E::Wipeout(sw), E::Wipeout(dw)) = (src, dst as &mut E) {
|
||||
dw.flags = sw.flags;
|
||||
dw.clipping_enabled = sw.clipping_enabled;
|
||||
dw.brightness = sw.brightness;
|
||||
dw.contrast = sw.contrast;
|
||||
dw.fade = sw.fade;
|
||||
dw.clip_mode = sw.clip_mode;
|
||||
}
|
||||
if let (E::Underlay(su), E::Underlay(du)) = (src, dst as &mut E) {
|
||||
du.flags = su.flags;
|
||||
du.contrast = su.contrast;
|
||||
du.fade = su.fade;
|
||||
du.clip_inverted = su.clip_inverted;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PolylineMatchStyle {
|
||||
pub plinegen: bool,
|
||||
pub widths: Vec<(f64, f64)>,
|
||||
}
|
||||
|
||||
impl PolylineMatchStyle {
|
||||
pub fn from_entity(entity: &acadrust::EntityType) -> Option<Self> {
|
||||
use acadrust::EntityType as E;
|
||||
|
||||
match entity {
|
||||
E::LwPolyline(poly) => {
|
||||
let widths = if poly.vertices.is_empty() {
|
||||
vec![(poly.constant_width, poly.constant_width)]
|
||||
} else {
|
||||
poly.vertices
|
||||
.iter()
|
||||
.map(|v| {
|
||||
(
|
||||
width_or_default(v.start_width, poly.constant_width),
|
||||
width_or_default(v.end_width, poly.constant_width),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
Some(Self {
|
||||
plinegen: poly.plinegen,
|
||||
widths,
|
||||
})
|
||||
}
|
||||
E::Polyline2D(poly) => {
|
||||
let widths = if poly.vertices.is_empty() {
|
||||
vec![(poly.start_width, poly.end_width)]
|
||||
} else {
|
||||
poly.vertices
|
||||
.iter()
|
||||
.map(|v| {
|
||||
(
|
||||
width_or_default(v.start_width, poly.start_width),
|
||||
width_or_default(v.end_width, poly.end_width),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
Some(Self {
|
||||
plinegen: poly.flags.bits()
|
||||
& acadrust::entities::PolylineFlags::LINETYPE_CONTINUOUS.bits()
|
||||
!= 0,
|
||||
widths,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, entity: &mut acadrust::EntityType) {
|
||||
use acadrust::EntityType as E;
|
||||
|
||||
match entity {
|
||||
E::LwPolyline(poly) => {
|
||||
poly.plinegen = self.plinegen;
|
||||
let sampled = resample_widths(&self.widths, poly.vertices.len());
|
||||
let first = sampled.first().copied().unwrap_or((0.0, 0.0));
|
||||
let can_use_constant = widths_are_same(&sampled) && nearly_equal(first.0, first.1);
|
||||
poly.constant_width = if can_use_constant { first.0 } else { 0.0 };
|
||||
for (vertex, (start, end)) in poly.vertices.iter_mut().zip(sampled) {
|
||||
if can_use_constant {
|
||||
vertex.start_width = 0.0;
|
||||
vertex.end_width = 0.0;
|
||||
} else {
|
||||
vertex.start_width = start;
|
||||
vertex.end_width = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
E::Polyline2D(poly) => {
|
||||
let mut bits = poly.flags.bits();
|
||||
let flag = acadrust::entities::PolylineFlags::LINETYPE_CONTINUOUS.bits();
|
||||
if self.plinegen {
|
||||
bits |= flag;
|
||||
} else {
|
||||
bits &= !flag;
|
||||
}
|
||||
poly.flags = acadrust::entities::PolylineFlags::from_bits(bits);
|
||||
let sampled = resample_widths(&self.widths, poly.vertices.len());
|
||||
let first = sampled.first().copied().unwrap_or((0.0, 0.0));
|
||||
let can_use_defaults = widths_are_same(&sampled);
|
||||
if can_use_defaults {
|
||||
poly.start_width = first.0;
|
||||
poly.end_width = first.1;
|
||||
} else {
|
||||
poly.start_width = 0.0;
|
||||
poly.end_width = 0.0;
|
||||
}
|
||||
for (vertex, (start, end)) in poly.vertices.iter_mut().zip(sampled) {
|
||||
if can_use_defaults {
|
||||
vertex.start_width = 0.0;
|
||||
vertex.end_width = 0.0;
|
||||
} else {
|
||||
vertex.start_width = start;
|
||||
vertex.end_width = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width_or_default(value: f64, default: f64) -> f64 {
|
||||
if value.abs() <= 1e-12 {
|
||||
default
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nearly_equal(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() <= 1e-9
|
||||
}
|
||||
|
||||
pub fn widths_are_same(widths: &[(f64, f64)]) -> bool {
|
||||
let Some(first) = widths.first() else {
|
||||
return true;
|
||||
};
|
||||
widths
|
||||
.iter()
|
||||
.all(|value| nearly_equal(value.0, first.0) && nearly_equal(value.1, first.1))
|
||||
}
|
||||
|
||||
pub fn resample_widths(source: &[(f64, f64)], count: usize) -> Vec<(f64, f64)> {
|
||||
if count == 0 || source.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
if count == 1 || source.len() == 1 {
|
||||
return vec![source[0]; count];
|
||||
}
|
||||
(0..count)
|
||||
.map(|index| {
|
||||
let source_index = index.saturating_mul(source.len() - 1) / count.saturating_sub(1);
|
||||
source[source_index]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Why [`match_layer_kernel`] could not run.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum MatchLayerError {
|
||||
/// The source handle names no entity in the document.
|
||||
SourceNotFound,
|
||||
}
|
||||
|
||||
/// LAYMATCH kernel: copy the source entity's layer onto every destination
|
||||
/// entity, returning how many destinations were updated. Destination handles
|
||||
/// that resolve to nothing are skipped. Lock-filtering, undo, dirty-state and
|
||||
/// command-line echo stay caller-side in `handle_match_entity_layer`.
|
||||
pub fn match_layer_kernel(
|
||||
doc: &mut acadrust::CadDocument,
|
||||
dest: &[acadrust::Handle],
|
||||
src: acadrust::Handle,
|
||||
) -> Result<usize, MatchLayerError> {
|
||||
let layer = doc
|
||||
.get_entity(src)
|
||||
.map(|e| e.common().layer.clone())
|
||||
.ok_or(MatchLayerError::SourceNotFound)?;
|
||||
let mut count = 0;
|
||||
for handle in dest {
|
||||
if let Some(e) = doc.get_entity_mut(*handle) {
|
||||
e.as_entity_mut().set_layer(layer.clone());
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// MATCHPROP per-pair transfer switches. The entity-level transfers
|
||||
/// (`copy_common`, `copy_thickness`, `copy_special`) run inside
|
||||
/// [`match_properties_kernel`]; the document-level transfers
|
||||
/// (`copy_hatch_background`, `copy_dim_overrides`) stay caller-side in
|
||||
/// `handle_match_properties` (they need `&mut CadDocument` for
|
||||
/// `set_entity_xdata` / `dim_override::replace`) but are gated by the same
|
||||
/// flags so headless callers and the handler agree on what "match" means.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct MatchOpts {
|
||||
/// Layer, color, lineweight, linetype (+handle/scale), transparency,
|
||||
/// color name/book, visual styles, material, shadow, plotstyle.
|
||||
pub copy_common: bool,
|
||||
/// Extrusion thickness (DXF 39) where the destination carries one.
|
||||
pub copy_thickness: bool,
|
||||
/// Style-affecting fields via [`match_special_props`] (text formatting,
|
||||
/// dim style, hatch pattern/gradient, polyline widths, …).
|
||||
pub copy_special: bool,
|
||||
/// Hatch background colour (`HATCHBACKGROUNDCOLOR` xdata). Caller-side.
|
||||
pub copy_hatch_background: bool,
|
||||
/// Dimension-style overrides (`DSTYLE` xdata payload). Caller-side.
|
||||
pub copy_dim_overrides: bool,
|
||||
}
|
||||
|
||||
impl MatchOpts {
|
||||
/// Every transfer enabled — the interactive MATCHPROP behaviour.
|
||||
pub fn all() -> Self {
|
||||
Self {
|
||||
copy_common: true,
|
||||
copy_thickness: true,
|
||||
copy_special: true,
|
||||
copy_hatch_background: true,
|
||||
copy_dim_overrides: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MatchOpts {
|
||||
fn default() -> Self {
|
||||
Self::all()
|
||||
}
|
||||
}
|
||||
|
||||
/// MATCHPROP kernel: transfer the source entity's properties onto one
|
||||
/// destination entity, gated by `opts`. Covers the `&EntityType`-level work
|
||||
/// from `handle_match_properties` — common fields, thickness and
|
||||
/// [`match_special_props`]. The `&mut CadDocument` paths (hatch-background
|
||||
/// xdata, dim-override replace, dim-block invalidation, fill-model refresh)
|
||||
/// stay caller-side; lock-filtering, undo, dirty-state, selection and echo
|
||||
/// stay caller-side too.
|
||||
pub fn match_properties_kernel(
|
||||
src: &acadrust::EntityType,
|
||||
dst: &mut acadrust::EntityType,
|
||||
opts: &MatchOpts,
|
||||
) {
|
||||
if opts.copy_common {
|
||||
let common = src.common().clone();
|
||||
dst.as_entity_mut().set_layer(common.layer.clone());
|
||||
crate::scene::view::dispatch::apply_color(dst, common.color);
|
||||
crate::scene::view::dispatch::apply_line_weight(dst, common.line_weight);
|
||||
{
|
||||
let dst_common = dst.common_mut();
|
||||
dst_common.linetype = common.linetype.clone();
|
||||
dst_common.linetype_handle = common.linetype_handle;
|
||||
dst_common.linetype_scale = common.linetype_scale;
|
||||
dst_common.transparency = common.transparency;
|
||||
dst_common.color_name = common.color_name.clone();
|
||||
dst_common.color_book_handle = common.color_book_handle;
|
||||
dst_common.full_visual_style_handle = common.full_visual_style_handle;
|
||||
dst_common.face_visual_style_handle = common.face_visual_style_handle;
|
||||
dst_common.edge_visual_style_handle = common.edge_visual_style_handle;
|
||||
dst_common.material_flags = common.material_flags;
|
||||
dst_common.material_handle = common.material_handle;
|
||||
dst_common.shadow_flags = common.shadow_flags;
|
||||
dst_common.plotstyle_flags = common.plotstyle_flags;
|
||||
dst_common.plotstyle_handle = common.plotstyle_handle;
|
||||
}
|
||||
}
|
||||
if opts.copy_thickness {
|
||||
if let Some(value) = crate::scene::view::dispatch::entity_thickness(src) {
|
||||
crate::scene::view::dispatch::set_entity_thickness(dst, value);
|
||||
}
|
||||
}
|
||||
if opts.copy_special {
|
||||
match_special_props(src, dst);
|
||||
}
|
||||
// `copy_hatch_background` / `copy_dim_overrides` are intentionally not
|
||||
// applied here: both ride xdata records through `&mut CadDocument`
|
||||
// (`set_entity_xdata`, `dim_override::replace`). The handler checks the
|
||||
// same flags before running those paths.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod match_layer_kernel_tests {
|
||||
use super::{MatchLayerError, match_layer_kernel};
|
||||
|
||||
fn line_on(layer: &str) -> acadrust::EntityType {
|
||||
let mut line = acadrust::entities::Line::new();
|
||||
line.common.layer = layer.to_string();
|
||||
acadrust::EntityType::Line(line)
|
||||
}
|
||||
|
||||
fn layer_of(doc: &acadrust::CadDocument, handle: acadrust::Handle) -> String {
|
||||
doc.get_entity(handle).unwrap().common().layer.clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copies_src_layer_onto_every_dest_and_returns_count() {
|
||||
let mut doc = acadrust::CadDocument::new();
|
||||
let src = doc.add_entity(line_on("WALLS")).unwrap();
|
||||
let a = doc.add_entity(line_on("0")).unwrap();
|
||||
let b = doc.add_entity(line_on("0")).unwrap();
|
||||
|
||||
let count = match_layer_kernel(&mut doc, &[a, b], src).unwrap();
|
||||
|
||||
assert_eq!(count, 2);
|
||||
assert_eq!(layer_of(&doc, a), "WALLS");
|
||||
assert_eq!(layer_of(&doc, b), "WALLS");
|
||||
assert_eq!(layer_of(&doc, src), "WALLS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_src_errors_and_leaves_dest_untouched() {
|
||||
let mut doc = acadrust::CadDocument::new();
|
||||
let dest = doc.add_entity(line_on("0")).unwrap();
|
||||
let missing = doc.allocate_handle();
|
||||
|
||||
let err = match_layer_kernel(&mut doc, &[dest], missing).unwrap_err();
|
||||
|
||||
assert_eq!(err, MatchLayerError::SourceNotFound);
|
||||
assert_eq!(layer_of(&doc, dest), "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_missing_dest_handles_but_counts_the_rest() {
|
||||
let mut doc = acadrust::CadDocument::new();
|
||||
let src = doc.add_entity(line_on("WALLS")).unwrap();
|
||||
let dest = doc.add_entity(line_on("0")).unwrap();
|
||||
let missing = doc.allocate_handle();
|
||||
|
||||
let count = match_layer_kernel(&mut doc, &[dest, missing], src).unwrap();
|
||||
|
||||
assert_eq!(count, 1);
|
||||
assert_eq!(layer_of(&doc, dest), "WALLS");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod match_properties_kernel_tests {
|
||||
use super::{MatchOpts, match_properties_kernel};
|
||||
use acadrust::EntityType;
|
||||
|
||||
fn line_src() -> EntityType {
|
||||
let mut line = acadrust::entities::Line::new();
|
||||
line.common.layer = "WALLS".to_string();
|
||||
line.common.linetype_scale = 2.5;
|
||||
line.thickness = 1.25;
|
||||
EntityType::Line(line)
|
||||
}
|
||||
|
||||
fn line_dst() -> EntityType {
|
||||
let mut line = acadrust::entities::Line::new();
|
||||
line.common.layer = "0".to_string();
|
||||
line.common.linetype_scale = 1.0;
|
||||
line.thickness = 0.0;
|
||||
EntityType::Line(line)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfers_common_fields_and_thickness_when_enabled() {
|
||||
let src = line_src();
|
||||
let mut dst = line_dst();
|
||||
|
||||
match_properties_kernel(&src, &mut dst, &MatchOpts::all());
|
||||
|
||||
assert_eq!(dst.common().layer, "WALLS");
|
||||
assert!((dst.common().linetype_scale - 2.5).abs() < 1e-9);
|
||||
assert!(
|
||||
crate::scene::view::dispatch::entity_thickness(&dst) == Some(1.25),
|
||||
"thickness must follow the source, got {:?}",
|
||||
crate::scene::view::dispatch::entity_thickness(&dst)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfers_text_style_via_special_props_flag() {
|
||||
let mut src_text = acadrust::entities::Text::new();
|
||||
src_text.value = "SRC".into();
|
||||
src_text.style = "BIG".into();
|
||||
src_text.height = 5.0;
|
||||
let src = EntityType::Text(src_text);
|
||||
let mut dst_text = acadrust::entities::Text::new();
|
||||
dst_text.value = "DST".into();
|
||||
let mut dst = EntityType::Text(dst_text);
|
||||
|
||||
match_properties_kernel(&src, &mut dst, &MatchOpts::all());
|
||||
|
||||
match &dst {
|
||||
EntityType::Text(t) => {
|
||||
assert_eq!(t.style, "BIG");
|
||||
assert!((t.height - 5.0).abs() < 1e-9);
|
||||
// Content never crosses.
|
||||
assert_eq!(t.value, "DST");
|
||||
}
|
||||
other => panic!("expected Text, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respects_disabled_common_and_special_flags() {
|
||||
let src = line_src();
|
||||
let mut dst = line_dst();
|
||||
let opts = MatchOpts {
|
||||
copy_common: false,
|
||||
copy_thickness: true,
|
||||
copy_special: false,
|
||||
copy_hatch_background: false,
|
||||
copy_dim_overrides: false,
|
||||
};
|
||||
|
||||
match_properties_kernel(&src, &mut dst, &opts);
|
||||
|
||||
assert_eq!(dst.common().layer, "0", "disabled common must not copy");
|
||||
assert!(
|
||||
crate::scene::view::dispatch::entity_thickness(&dst) == Some(1.25),
|
||||
"enabled thickness must still copy"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ pub mod leader;
|
||||
pub mod light;
|
||||
pub mod line;
|
||||
pub mod lwpolyline;
|
||||
pub mod match_props;
|
||||
pub mod mesh;
|
||||
pub mod mline;
|
||||
pub mod mtext;
|
||||
|
||||
Reference in new issue
Block a user