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
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user