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:
Hakan SevenandClaude Opus 5.5 committed 2026-09-25 00:05:11 +03:00
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
+521
View File
@@ -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
+951
View File
@@ -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()
}
}
+992
View File
@@ -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(&parameter.source, old, new);
if rewritten != parameter.source {
renamed
.set(&parameter.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(&current.name, &typed),
current.name.clone(),
),
ParamField::Name => {
let description = self.tabs[i]
.scene
.named_parameters()
.description(&current.name)
.to_string();
self.tabs[i]
.scene
.named_parameters_mut()
.remove(&current.name);
let outcome = self.tabs[i]
.scene
.named_parameters_mut()
.set(&typed, &current.source);
let kept = if outcome.is_err() {
let _ = self.tabs[i]
.scene
.named_parameters_mut()
.set(&current.name, &current.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 == &current.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 == &current.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,
);
}
}
+167
View File
@@ -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
}
}
+773
View File
@@ -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(&sections, &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
+245
View File
@@ -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
}
}
+577
View File
@@ -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
+414
View File
@@ -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)
}
}