forked from 1b-agent/opencadstudio-upstream
Merge pull request #1246
This commit is contained in:
commit
293d0796fb
38 files changed
+4399
-487
No files matched your search
File diff suppressed because it is too large.
Load diff
+600
-25
@@ -2474,6 +2474,7 @@ impl OpenCADStudio {
|
||||
CmdResult::CommitManyAndEditText {
|
||||
entities,
|
||||
edit_index,
|
||||
open_editor,
|
||||
} => {
|
||||
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
||||
let delta_safe = entities
|
||||
@@ -2527,8 +2528,10 @@ impl OpenCADStudio {
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
if let Some(h) = edit_handle {
|
||||
return self.begin_text_edit(h);
|
||||
if open_editor {
|
||||
if let Some(h) = edit_handle {
|
||||
return self.begin_text_edit(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
CmdResult::CreateBlock {
|
||||
@@ -2777,6 +2780,33 @@ impl OpenCADStudio {
|
||||
}
|
||||
self.refresh_properties();
|
||||
}
|
||||
CmdResult::UpdateEntityAndFinish { handle, entity } => {
|
||||
if self.reject_locked_edit(i, handle) {
|
||||
self.tabs[i].active_cmd = None;
|
||||
return Task::none();
|
||||
}
|
||||
let label = self.history_label_from_active_cmd(i, "EDIT");
|
||||
self.push_undo_snapshot(i, label);
|
||||
let is_dimension = matches!(entity, acadrust::EntityType::Dimension(_));
|
||||
if let Some(current) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
||||
*current = entity;
|
||||
if is_dimension {
|
||||
self.tabs[i].scene.invalidate_dim_block_recorded(handle);
|
||||
}
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||||
self.tabs[i].dirty = true;
|
||||
} else {
|
||||
self.command_line.push_error(
|
||||
crate::t!("The selected object is no longer available.").as_ref(),
|
||||
);
|
||||
}
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.refresh_properties();
|
||||
}
|
||||
CmdResult::AddSketchConstraint { kind, refs, driving_param, label } => {
|
||||
if let Err(message) = self.tabs[i]
|
||||
.scene
|
||||
@@ -2948,36 +2978,111 @@ impl OpenCADStudio {
|
||||
if new_entities.len() == 1 {
|
||||
if let acadrust::EntityType::XLine(ref xl) = new_entities[0] {
|
||||
let layer = xl.common.layer.clone();
|
||||
if layer.starts_with("__DIMBREAK__")
|
||||
|| layer.starts_with("__DIMBREAK_AUTO__")
|
||||
{
|
||||
// DIMBREAK needs a break-gap field on the dimension
|
||||
// model (not yet present) to store and render the gap.
|
||||
// Report honestly rather than claiming success while
|
||||
// changing nothing. (#181 / DIM-020)
|
||||
self.command_line
|
||||
.push_info(crate::t!("DIMBREAK: not yet implemented — nothing changed.").as_ref());
|
||||
if layer.starts_with("__DIMBREAK_") {
|
||||
self.push_undo_snapshot(i, "DIMBREAK");
|
||||
let result = apply_dimbreak(&mut self.tabs[i].scene, &layer);
|
||||
if result.changed.is_empty() {
|
||||
self.command_line.push_info(&result.message);
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
return Task::none();
|
||||
}
|
||||
for handle in &result.changed {
|
||||
self.tabs[i].scene.invalidate_dim_block_recorded(*handle);
|
||||
}
|
||||
let changes: Vec<_> = result
|
||||
.changed
|
||||
.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(&result.message);
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
return Task::none();
|
||||
}
|
||||
if layer.starts_with("__DIMSPACE__") {
|
||||
self.push_undo_snapshot(i, "DIMSPACE");
|
||||
if let Some(encoded) = layer.strip_prefix("__DIMSPACE__") {
|
||||
apply_dimspace(&mut self.tabs[i].scene, encoded);
|
||||
}
|
||||
self.push_undo_snapshot(i, "DIMSPACE");
|
||||
self.command_line.push_output(crate::t!("DIMSPACE Spacing adjusted.").as_ref());
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
return Task::none();
|
||||
}
|
||||
if layer.starts_with("__DIMJOG__") {
|
||||
// DIMJOGLINE needs a jog-point field on the dimension
|
||||
// model (not yet present) to store and render the jog.
|
||||
// Report honestly rather than faking success. (DIM-019)
|
||||
self.command_line
|
||||
.push_info(crate::t!("DIMJOGLINE: not yet implemented — nothing changed.").as_ref());
|
||||
if layer.starts_with("__DIMJOG__")
|
||||
|| layer.starts_with("__DIMJOG_REMOVE__")
|
||||
{
|
||||
let remove = layer.starts_with("__DIMJOG_REMOVE__");
|
||||
let point = layer
|
||||
.strip_prefix("__DIMJOG__")
|
||||
.and_then(|encoded| encoded.split_once('|'))
|
||||
.and_then(|(_, coordinates)| {
|
||||
let mut values = coordinates
|
||||
.split(',')
|
||||
.filter_map(|value| value.parse::<f64>().ok());
|
||||
Some(acadrust::types::Vector3::new(
|
||||
values.next()?,
|
||||
values.next()?,
|
||||
values.next()?,
|
||||
))
|
||||
});
|
||||
let valid = matches!(
|
||||
self.tabs[i].scene.document.get_entity(handle),
|
||||
Some(acadrust::EntityType::Dimension(
|
||||
acadrust::entities::Dimension::Linear(_)
|
||||
| acadrust::entities::Dimension::Aligned(_)
|
||||
))
|
||||
);
|
||||
if !valid || (!remove && point.is_none()) {
|
||||
self.command_line.push_error(
|
||||
crate::t!("DIMJOGLINE: select a linear or aligned dimension.")
|
||||
.as_ref(),
|
||||
);
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
return Task::none();
|
||||
}
|
||||
self.push_undo_snapshot(i, "DIMJOGLINE");
|
||||
if remove {
|
||||
crate::scene::view::dispatch::set_entity_xdata(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
"ACAD_DSTYLE_DIMJAG_POSITION",
|
||||
None,
|
||||
);
|
||||
} else if let Some(point) = point {
|
||||
use acadrust::xdata::XDataValue;
|
||||
crate::scene::view::dispatch::set_entity_xdata(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
"ACAD_DSTYLE_DIMJAG_POSITION",
|
||||
Some(vec![
|
||||
XDataValue::Integer16(387),
|
||||
XDataValue::Integer16(3),
|
||||
XDataValue::Integer16(389),
|
||||
XDataValue::Point3D(point),
|
||||
]),
|
||||
);
|
||||
}
|
||||
self.tabs[i].scene.invalidate_dim_block_recorded(handle);
|
||||
self.tabs[i].scene.bump_entities(&[(
|
||||
handle,
|
||||
crate::scene::ChangeKind::Modified,
|
||||
)]);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(
|
||||
if remove {
|
||||
crate::t!("DIMJOGLINE: jog removed.")
|
||||
} else {
|
||||
crate::t!("DIMJOGLINE: jog added.")
|
||||
}
|
||||
.as_ref(),
|
||||
);
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
return Task::none();
|
||||
@@ -5621,6 +5726,72 @@ impl OpenCADStudio {
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
}
|
||||
CmdResult::EditDimensions {
|
||||
mut handles,
|
||||
operation,
|
||||
} => {
|
||||
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();
|
||||
}
|
||||
CmdResult::DdeditEntity { handle, new_text } => {
|
||||
if self.reject_locked_edit(i, handle) {
|
||||
self.tabs[i].active_cmd = None;
|
||||
@@ -6176,6 +6347,382 @@ fn remap_object(
|
||||
|
||||
// ── DIMSPACE helper ───────────────────────────────────────────────────────────
|
||||
|
||||
struct DimBreakResult {
|
||||
changed: Vec<Handle>,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum DimBreakOperation {
|
||||
Auto,
|
||||
Object(Handle),
|
||||
Manual(glam::DVec3, glam::DVec3),
|
||||
Remove,
|
||||
}
|
||||
|
||||
fn parse_dim_break_handles(encoded: &str) -> Vec<Handle> {
|
||||
encoded
|
||||
.split(';')
|
||||
.filter_map(|value| value.parse::<u64>().ok())
|
||||
.map(Handle::from)
|
||||
.filter(|handle| !handle.is_null())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_dim_break_point(encoded: &str) -> Option<glam::DVec3> {
|
||||
let mut values = encoded.split(',').map(str::parse::<f64>);
|
||||
Some(glam::DVec3::new(
|
||||
values.next()?.ok()?,
|
||||
values.next()?.ok()?,
|
||||
values.next()?.ok()?,
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_dim_break(encoded: &str) -> Option<(Vec<Handle>, DimBreakOperation)> {
|
||||
if let Some(handles) = encoded.strip_prefix("__DIMBREAK_AUTO__") {
|
||||
return Some((parse_dim_break_handles(handles), DimBreakOperation::Auto));
|
||||
}
|
||||
if let Some(handles) = encoded.strip_prefix("__DIMBREAK_REMOVE__") {
|
||||
return Some((parse_dim_break_handles(handles), DimBreakOperation::Remove));
|
||||
}
|
||||
if let Some(rest) = encoded.strip_prefix("__DIMBREAK_OBJECT__") {
|
||||
let (handles, crossing) = rest.split_once('|')?;
|
||||
return Some((
|
||||
parse_dim_break_handles(handles),
|
||||
DimBreakOperation::Object(Handle::from(crossing.parse::<u64>().ok()?)),
|
||||
));
|
||||
}
|
||||
if let Some(rest) = encoded.strip_prefix("__DIMBREAK_MANUAL__") {
|
||||
let mut parts = rest.split('|');
|
||||
let handles = parse_dim_break_handles(parts.next()?);
|
||||
let first = parse_dim_break_point(parts.next()?)?;
|
||||
let second = parse_dim_break_point(parts.next()?)?;
|
||||
return Some((handles, DimBreakOperation::Manual(first, second)));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
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 p = first.0;
|
||||
let r = first.1 - first.0;
|
||||
let q = second.0;
|
||||
let s = second.1 - second.0;
|
||||
let cross = |a: glam::DVec3, b: glam::DVec3| a.x * b.y - a.y * b.x;
|
||||
let denominator = cross(r, s);
|
||||
if denominator.abs() <= 1.0e-12 {
|
||||
return None;
|
||||
}
|
||||
let qp = q - p;
|
||||
let t = cross(qp, s) / denominator;
|
||||
let u = cross(qp, r) / denominator;
|
||||
if !(-1.0e-9..=1.0 + 1.0e-9).contains(&t)
|
||||
|| !(-1.0e-9..=1.0 + 1.0e-9).contains(&u)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let direction = r.normalize_or_zero();
|
||||
if direction.length_squared() <= 1.0e-18 {
|
||||
return None;
|
||||
}
|
||||
let center = p + r * t.clamp(0.0, 1.0);
|
||||
let half_gap = 0.25_f64.min(first.0.distance(first.1) * 0.2);
|
||||
Some((
|
||||
center - direction * half_gap,
|
||||
center + direction * half_gap,
|
||||
))
|
||||
}
|
||||
|
||||
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, encoded: &str) -> DimBreakResult {
|
||||
let Some((requested, operation)) = parse_dim_break(encoded) else {
|
||||
return DimBreakResult {
|
||||
changed: Vec::new(),
|
||||
message: crate::t!("DIMBREAK: invalid operation data.").into_owned(),
|
||||
};
|
||||
};
|
||||
let dimensions: Vec<_> = requested
|
||||
.into_iter()
|
||||
.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, DimBreakOperation::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 {
|
||||
DimBreakOperation::Object(handle) => vec![handle],
|
||||
DimBreakOperation::Auto => scene
|
||||
.document
|
||||
.entities()
|
||||
.map(|entity| entity.common().handle)
|
||||
.filter(|handle| {
|
||||
!dimensions.contains(handle) && scene.entity_belongs_to_active_space(*handle)
|
||||
})
|
||||
.collect(),
|
||||
DimBreakOperation::Manual(_, _) | DimBreakOperation::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, DimBreakOperation::Manual(_, _)) {
|
||||
existing
|
||||
} else {
|
||||
existing
|
||||
.into_iter()
|
||||
.filter(|reference| reference.reference_type == 2)
|
||||
.collect()
|
||||
};
|
||||
match operation {
|
||||
DimBreakOperation::Manual(first, second) => {
|
||||
references.push(break_reference(references.len() as i32, 2, first, second));
|
||||
}
|
||||
DimBreakOperation::Auto | DimBreakOperation::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)
|
||||
},
|
||||
));
|
||||
}
|
||||
DimBreakOperation::Remove => {}
|
||||
}
|
||||
pending.push((*dimension, references));
|
||||
}
|
||||
|
||||
let reserved = match operation {
|
||||
DimBreakOperation::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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse `base_val,h1;h2;...;hN,spacing` and adjust parallel dimension positions.
|
||||
fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
|
||||
// Format: "<base_handle>,<h1>;<h2>;...;<hN>,<spacing>"
|
||||
@@ -6188,7 +6735,11 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
|
||||
.split(';')
|
||||
.filter_map(|s| s.parse::<u64>().ok())
|
||||
.collect();
|
||||
let spacing: f64 = parts[2].parse().unwrap_or(0.0);
|
||||
let requested_spacing = if parts[2].eq_ignore_ascii_case("AUTO") {
|
||||
None
|
||||
} else {
|
||||
parts[2].parse::<f64>().ok()
|
||||
};
|
||||
|
||||
use acadrust::entities::Dimension;
|
||||
let base_h = acadrust::Handle::from(base_val);
|
||||
@@ -6196,25 +6747,27 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
|
||||
// dim line's perpendicular coordinate. Spacing steps each parallel dim along
|
||||
// this perp IN THE DRAWING PLANE — offsetting Z had no effect on the dim
|
||||
// line, which is computed from def·perp with perp.z = 0. (#181 / DIM-021)
|
||||
let (perp, base_coord) = match scene.document.get_entity(base_h) {
|
||||
Some(acadrust::EntityType::Dimension(Dimension::Linear(d))) => {
|
||||
let (perp, base_coord, auto_spacing) = match scene.document.get_entity(base_h) {
|
||||
Some(acadrust::EntityType::Dimension(dimension @ Dimension::Linear(d))) => {
|
||||
let (s, c) = d.rotation.sin_cos();
|
||||
let perp = (-s, c);
|
||||
let dp = d.definition_point;
|
||||
(perp, dp.x * perp.0 + dp.y * perp.1)
|
||||
let spacing = dimension_auto_spacing(&scene.document, dimension, scene.creation_annotation_multiplier());
|
||||
(perp, dp.x * perp.0 + dp.y * perp.1, spacing)
|
||||
}
|
||||
Some(acadrust::EntityType::Dimension(Dimension::Aligned(d))) => {
|
||||
Some(acadrust::EntityType::Dimension(dimension @ Dimension::Aligned(d))) => {
|
||||
let dx = d.second_point.x - d.first_point.x;
|
||||
let dy = d.second_point.y - d.first_point.y;
|
||||
let len = (dx * dx + dy * dy).sqrt().max(1e-12);
|
||||
let perp = (-dy / len, dx / len);
|
||||
let dp = d.definition_point;
|
||||
(perp, dp.x * perp.0 + dp.y * perp.1)
|
||||
let spacing = dimension_auto_spacing(&scene.document, dimension, scene.creation_annotation_multiplier());
|
||||
(perp, dp.x * perp.0 + dp.y * perp.1, spacing)
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let effective_spacing = if spacing <= 0.0 { 10.0 } else { spacing };
|
||||
let effective_spacing = requested_spacing.unwrap_or(auto_spacing);
|
||||
let mut changes = Vec::new();
|
||||
for (idx, &hv) in other_vals.iter().enumerate() {
|
||||
let h = acadrust::Handle::from(hv);
|
||||
@@ -6256,6 +6809,28 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn apply_mleader_align(
|
||||
scene: &mut crate::scene::Scene,
|
||||
handles: &[acadrust::Handle],
|
||||
|
||||
+1
-26
@@ -668,32 +668,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
"TABLE" => {
|
||||
use crate::modules::annotate::table_cmd::TableCommand;
|
||||
let name = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.current_table_style_name
|
||||
.clone();
|
||||
let style = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.iter()
|
||||
.find_map(|(handle, object)| match object {
|
||||
acadrust::objects::ObjectType::TableStyle(style)
|
||||
if style.name.eq_ignore_ascii_case(&name) =>
|
||||
{
|
||||
Some((*handle, style.clone()))
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
let multiplier = self.tabs[i].scene.creation_annotation_multiplier();
|
||||
let cmd = style.as_ref().map_or_else(TableCommand::new, |(handle, style)| {
|
||||
TableCommand::with_style(*handle, style, multiplier)
|
||||
});
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
self.open_table_insert();
|
||||
}
|
||||
|
||||
"TABLEDIT" => {
|
||||
|
||||
+180
-57
@@ -1041,55 +1041,110 @@ impl OpenCADStudio {
|
||||
return Some(Task::done(Message::PlotDialogOpen));
|
||||
}
|
||||
|
||||
// ── Recognized commands whose full implementation is pending ─────────
|
||||
// These verbs are surfaced by the ribbon / menus but their feature is
|
||||
// still being built. Acknowledge them with an honest status so the
|
||||
// button responds instead of reporting an unknown command; each is
|
||||
// replaced by its real handler as the feature lands.
|
||||
// OBJECTSCALE ADD — add the active scale representation to every
|
||||
// selected object that supports per-scale context data.
|
||||
"OBJECTSCALE ADD" => {
|
||||
let handles: Vec<acadrust::Handle> = self.tabs[i]
|
||||
// Add or remove the current representation on the selected
|
||||
// annotative objects. With no preselection, gather one through the
|
||||
// regular selection engine before applying the action.
|
||||
action @ ("OBJECTSCALE ADD" | "OBJECTSCALE DELETE") => {
|
||||
let selected: Vec<acadrust::Handle> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.iter()
|
||||
.map(|(h, _)| *h)
|
||||
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
||||
.collect();
|
||||
if handles.is_empty() {
|
||||
if selected.is_empty() {
|
||||
let action = if action.ends_with("ADD") {
|
||||
"OBJECTSCALE ADD"
|
||||
} else {
|
||||
"OBJECTSCALE DELETE"
|
||||
};
|
||||
let command = crate::modules::annotate::annotation_scale::AnnotationScaleSelectionCommand::new(
|
||||
"OBJECTSCALE",
|
||||
action,
|
||||
);
|
||||
self.command_line
|
||||
.push_error(crate::t!("OBJECTSCALE: select objects first.").as_ref());
|
||||
.push_info(&crate::command::CadCommand::prompt(&command));
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
return Some(Task::none());
|
||||
}
|
||||
self.push_undo_snapshot(i, "OBJECTSCALE");
|
||||
let Some(scale) = self.tabs[i].scene.creation_annotation_scale_handle() else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("OBJECTSCALE: the active annotation scale is unavailable.").as_ref());
|
||||
return Some(Task::none());
|
||||
};
|
||||
let mut n = 0usize;
|
||||
for h in &handles {
|
||||
if crate::scene::annotative::create_annotation_context(
|
||||
&mut self.tabs[i].scene.document,
|
||||
*h,
|
||||
scale,
|
||||
) {
|
||||
crate::scene::annotative::set_entity_annotative(
|
||||
&mut self.tabs[i].scene.document,
|
||||
*h,
|
||||
true,
|
||||
|
||||
let adding = action.ends_with("ADD");
|
||||
let handles: Vec<_> = selected
|
||||
.into_iter()
|
||||
.filter(|handle| {
|
||||
let memberships = crate::scene::annotative::object_scale_memberships(
|
||||
&self.tabs[i].scene.document,
|
||||
*handle,
|
||||
);
|
||||
n += 1;
|
||||
let member = memberships.iter().any(|(_, current)| *current == scale);
|
||||
if adding {
|
||||
!member
|
||||
&& self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(*handle)
|
||||
.is_some_and(crate::scene::annotative::supports_annotation_context)
|
||||
} else {
|
||||
member
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if handles.is_empty() {
|
||||
self.command_line.push_info(
|
||||
if adding {
|
||||
crate::t!("OBJECTSCALE: the selected objects already have the current scale, or do not support annotation scales.")
|
||||
} else {
|
||||
crate::t!("OBJECTSCALE: the selected objects do not have the current scale.")
|
||||
}
|
||||
.as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
||||
self.push_undo_snapshot(i, "OBJECTSCALE");
|
||||
let mut changed = Vec::new();
|
||||
for handle in handles {
|
||||
let ok = if adding {
|
||||
let created = crate::scene::annotative::create_annotation_context(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
scale,
|
||||
);
|
||||
if created {
|
||||
crate::scene::annotative::set_entity_annotative(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
true,
|
||||
);
|
||||
}
|
||||
created
|
||||
} else {
|
||||
crate::scene::annotative::remove_annotation_context_for_scale(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
scale,
|
||||
)
|
||||
};
|
||||
if ok {
|
||||
changed.push(handle);
|
||||
}
|
||||
}
|
||||
let changes: Vec<_> = handles
|
||||
.into_iter()
|
||||
let changes: Vec<_> = changed
|
||||
.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!(
|
||||
"OBJECTSCALE: added the active scale to {n} object(s)."
|
||||
"OBJECTSCALE: {} the current scale on {} object(s).",
|
||||
if adding { "added" } else { "removed" },
|
||||
changed.len()
|
||||
).as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
@@ -1459,12 +1514,78 @@ impl OpenCADStudio {
|
||||
return Some(Task::done(Message::AnnoObjectScaleOpen));
|
||||
}
|
||||
|
||||
// Reset every alternate scale representation to the position of
|
||||
// the representation visible at the current annotation scale.
|
||||
"ANNORESET" => {
|
||||
let handles: Vec<acadrust::Handle> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.iter()
|
||||
.map(|(handle, _)| *handle)
|
||||
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
||||
.collect();
|
||||
if handles.is_empty() {
|
||||
let command = crate::modules::annotate::annotation_scale::AnnotationScaleSelectionCommand::new(
|
||||
"ANNORESET",
|
||||
"ANNORESET",
|
||||
);
|
||||
self.command_line
|
||||
.push_info(&crate::command::CadCommand::prompt(&command));
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
return Some(Task::none());
|
||||
}
|
||||
let scale = self.tabs[i].scene.creation_annotation_scale_handle();
|
||||
let candidates: Vec<_> = handles
|
||||
.into_iter()
|
||||
.filter(|handle| {
|
||||
crate::scene::annotative::object_scale_memberships(
|
||||
&self.tabs[i].scene.document,
|
||||
*handle,
|
||||
)
|
||||
.len()
|
||||
> 1
|
||||
})
|
||||
.collect();
|
||||
if candidates.is_empty() {
|
||||
self.command_line.push_info(
|
||||
crate::t!("ANNORESET: no selected object has alternate scale representations.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
self.push_undo_snapshot(i, "ANNORESET");
|
||||
let mut changed = Vec::new();
|
||||
for handle in candidates {
|
||||
if crate::scene::annotative::reset_annotation_context_positions(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
scale,
|
||||
) {
|
||||
changed.push(handle);
|
||||
}
|
||||
}
|
||||
if !changed.is_empty() {
|
||||
let changes: Vec<_> = changed
|
||||
.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!(
|
||||
"ANNORESET: synchronized {} object(s) from the current scale.",
|
||||
changed.len()
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
||||
// DATALINK <path.csv> — create a persistent linked table.
|
||||
"DATALINK" => {
|
||||
use crate::command::ValuePromptCommand;
|
||||
let c = ValuePromptCommand::new("DATALINK", "DATALINK path to the .csv file:");
|
||||
self.command_line.push_info(&c.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||
self.open_data_link_manager(false);
|
||||
}
|
||||
cmd if cmd.starts_with("DATALINK ") => {
|
||||
let path = cmd.trim_start_matches("DATALINK").trim();
|
||||
@@ -1566,20 +1687,7 @@ impl OpenCADStudio {
|
||||
let Some(link_handle) = link_handle else {
|
||||
continue;
|
||||
};
|
||||
let path = match self.tabs[i].scene.document.objects.get(&link_handle) {
|
||||
Some(acadrust::objects::ObjectType::ClassObject(object)) => {
|
||||
match &object.data {
|
||||
acadrust::objects::ClassObjectData::DataLink(link) => {
|
||||
Some(link.connection_string.clone())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(path) = path {
|
||||
jobs.push((*handle, link_handle, path));
|
||||
}
|
||||
jobs.push((*handle, link_handle));
|
||||
}
|
||||
if jobs.is_empty() {
|
||||
self.command_line
|
||||
@@ -1588,28 +1696,43 @@ impl OpenCADStudio {
|
||||
}
|
||||
if write_back {
|
||||
let mut written = 0usize;
|
||||
for (table_handle, _, path) in &jobs {
|
||||
let mut errors = Vec::new();
|
||||
for (table_handle, link_handle) in &jobs {
|
||||
let Some(acadrust::EntityType::Table(table)) =
|
||||
self.tabs[i].scene.document.get_entity(*table_handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let csv = table_to_csv(table);
|
||||
if std::fs::write(path, csv).is_ok() {
|
||||
written += 1;
|
||||
match crate::app::annotation_data::data_link_write_path(
|
||||
&self.tabs[i].scene.document,
|
||||
*link_handle,
|
||||
)
|
||||
.and_then(|path| std::fs::write(path, csv).map_err(|error| error.to_string()))
|
||||
{
|
||||
Ok(()) => written += 1,
|
||||
Err(error) => errors.push(error),
|
||||
}
|
||||
}
|
||||
self.command_line.push_output(
|
||||
crate::tf!("DATALINKUPDATE: wrote {} linked source(s).", written).as_ref(),
|
||||
);
|
||||
if written > 0 {
|
||||
self.command_line.push_output(
|
||||
crate::tf!("DATALINKUPDATE: wrote {} linked source(s).", written).as_ref(),
|
||||
);
|
||||
}
|
||||
for error in errors {
|
||||
self.command_line.push_error(&error);
|
||||
}
|
||||
return Some(Task::none());
|
||||
}
|
||||
let updates: Vec<_> = jobs
|
||||
.into_iter()
|
||||
.filter_map(|(table_handle, link_handle, path)| {
|
||||
std::fs::read_to_string(&path)
|
||||
.ok()
|
||||
.map(|text| (table_handle, link_handle, parse_csv_table(&text)))
|
||||
.filter_map(|(table_handle, link_handle)| {
|
||||
crate::app::annotation_data::read_data_link(
|
||||
&self.tabs[i].scene.document,
|
||||
link_handle,
|
||||
)
|
||||
.ok()
|
||||
.map(|rows| (table_handle, link_handle, rows))
|
||||
})
|
||||
.filter(|(_, _, rows)| !rows.is_empty())
|
||||
.collect();
|
||||
@@ -1764,7 +1887,7 @@ fn parse_landxml_cgpoints(xml: &str) -> Vec<[f64; 3]> {
|
||||
out
|
||||
}
|
||||
|
||||
fn parse_csv_table(text: &str) -> Vec<Vec<String>> {
|
||||
pub(crate) fn parse_csv_table(text: &str) -> Vec<Vec<String>> {
|
||||
let mut rows = Vec::new();
|
||||
let mut row = Vec::new();
|
||||
let mut field = String::new();
|
||||
@@ -1804,7 +1927,7 @@ fn parse_csv_table(text: &str) -> Vec<Vec<String>> {
|
||||
rows
|
||||
}
|
||||
|
||||
fn table_to_csv(table: &acadrust::entities::Table) -> String {
|
||||
pub(crate) fn table_to_csv(table: &acadrust::entities::Table) -> String {
|
||||
fn escape(value: &str) -> String {
|
||||
if value.contains([',', '"', '\r', '\n']) {
|
||||
format!("\"{}\"", value.replace('"', "\"\""))
|
||||
|
||||
@@ -1198,8 +1198,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
"DATAEXTRACTION" | "EATTEXT" | "ATTEXT" => {
|
||||
let csv = build_data_extraction_csv(&self.tabs[i].scene.document);
|
||||
return Some(Task::done(Message::DataExtractionSave(csv)));
|
||||
self.open_data_extraction();
|
||||
}
|
||||
|
||||
// ── Find / Replace ────────────────────────────────────────────────
|
||||
@@ -1935,82 +1934,6 @@ fn z_axis_normal(normal: acadrust::types::Vector3) -> bool {
|
||||
normal.x == 0.0 && normal.y == 0.0 && normal.z != 0.0
|
||||
}
|
||||
|
||||
// ── DATAEXTRACTION ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Build a CSV string with one row per entity in model space.
|
||||
/// Columns: Type, Handle, Layer, Color, Linetype, ExtraInfo
|
||||
fn build_data_extraction_csv(doc: &acadrust::CadDocument) -> String {
|
||||
use acadrust::EntityType;
|
||||
|
||||
let mut out = String::from("Type,Handle,Layer,Color,Linetype,ExtraInfo\n");
|
||||
|
||||
let ms_handle = doc.header.model_space_block_handle;
|
||||
for e in doc.entities() {
|
||||
// Skip Block/EndBlock sentinels and paper-space entities.
|
||||
if matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)) {
|
||||
continue;
|
||||
}
|
||||
if !ms_handle.is_null() && e.common().owner_handle != ms_handle {
|
||||
continue;
|
||||
}
|
||||
let type_name = crate::entities::names::dxf_name(e);
|
||||
let handle = format!("{:X}", e.common().handle.value());
|
||||
let layer = csv_escape(&e.common().layer);
|
||||
let color = format!("{}", e.common().color);
|
||||
let lt = csv_escape(&e.common().linetype);
|
||||
let extra = csv_escape(&entity_extra_info(e));
|
||||
out.push_str(&format!(
|
||||
"{type_name},{handle},{layer},{color},{lt},{extra}\n"
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Return a short geometry summary for CSV ExtraInfo column.
|
||||
fn entity_extra_info(entity: &acadrust::EntityType) -> String {
|
||||
use acadrust::EntityType;
|
||||
match entity {
|
||||
EntityType::Line(e) => format!(
|
||||
"({:.3},{:.3})-({:.3},{:.3})",
|
||||
e.start.x, e.start.y, e.end.x, e.end.y
|
||||
),
|
||||
EntityType::Circle(e) => {
|
||||
format!("C({:.3},{:.3}) R={:.3}", e.center.x, e.center.y, e.radius)
|
||||
}
|
||||
EntityType::Arc(e) => format!(
|
||||
"C({:.3},{:.3}) R={:.3} {:.1}°-{:.1}°",
|
||||
e.center.x,
|
||||
e.center.y,
|
||||
e.radius,
|
||||
e.start_angle.to_degrees(),
|
||||
e.end_angle.to_degrees()
|
||||
),
|
||||
EntityType::Text(e) => e.value.clone(),
|
||||
EntityType::MText(e) => e.value.chars().take(60).collect(),
|
||||
EntityType::Insert(e) => format!(
|
||||
"BLK={} @({:.3},{:.3})",
|
||||
e.block_name, e.insert_point.x, e.insert_point.y
|
||||
),
|
||||
EntityType::LwPolyline(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
|
||||
EntityType::Polyline(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
|
||||
EntityType::Polyline2D(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
|
||||
EntityType::Polyline3D(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
|
||||
EntityType::Hatch(e) => format!("PAT={}", e.pattern.name),
|
||||
EntityType::Dimension(e) => format!("{:.3}", e.base().actual_measurement),
|
||||
EntityType::Spline(e) => crate::tf!("{} ctrl pts", e.control_points.len()).into_owned(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape a string for a CSV field (wrap in quotes if it contains comma/quote/newline).
|
||||
fn csv_escape(s: &str) -> String {
|
||||
if s.contains(',') || s.contains('"') || s.contains('\n') {
|
||||
format!("\"{}\"", s.replace('"', "\"\""))
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// ── CAL — arithmetic expression evaluator ──────────────────────────────────
|
||||
// A small recursive-descent parser for `+ - * /`, parentheses, unary signs and
|
||||
// decimal numbers. Self-contained (no external dependency).
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::path::PathBuf;
|
||||
|
||||
mod blocks;
|
||||
mod dim;
|
||||
mod display;
|
||||
pub(crate) mod display;
|
||||
mod draw;
|
||||
mod fileops;
|
||||
mod inquiry;
|
||||
@@ -400,6 +400,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"ANNOUPDATE",
|
||||
"SCALELISTEDIT",
|
||||
"OBJECTSCALE",
|
||||
"ANNORESET",
|
||||
// Import CSV into a table + LandXML survey points.
|
||||
"DATALINK",
|
||||
"DATALINKUPDATE",
|
||||
|
||||
@@ -29,6 +29,7 @@ mod startup;
|
||||
mod style_ops;
|
||||
mod text_inline;
|
||||
mod tolerance_dialog;
|
||||
mod annotation_data;
|
||||
mod update;
|
||||
mod view;
|
||||
mod visibility;
|
||||
@@ -1092,6 +1093,12 @@ pub(super) struct OpenCADStudio {
|
||||
/// without Apply, mirroring the style managers' staging.
|
||||
scale_stage: Option<crate::app::style_ops::ScaleStage>,
|
||||
|
||||
// ── Annotation table/data dialogs ────────────────────────────────────
|
||||
table_insert: crate::ui::window::annotation_data::TableInsertState,
|
||||
data_link_manager: crate::ui::window::annotation_data::DataLinkManagerState,
|
||||
data_link_parent_table: bool,
|
||||
data_extraction: crate::ui::window::annotation_data::DataExtractionState,
|
||||
|
||||
// ── Plot Style Panel ──────────────────────────────────────────────────
|
||||
/// Selected ACI index in the panel (1-255).
|
||||
plotstyle_panel_aci: u8,
|
||||
@@ -1747,6 +1754,9 @@ pub enum ModalKind {
|
||||
/// Add / remove the annotation scales a single selected object has a
|
||||
/// per-object representation for.
|
||||
AnnoObjectScale,
|
||||
InsertTable,
|
||||
DataLinkManager,
|
||||
DataExtraction,
|
||||
/// The scene is drawn by a software rasterizer, or not at all: what that
|
||||
/// means and what usually fixes it. Queued once per verdict; the status
|
||||
/// bar's ⚠ pill reopens it.
|
||||
@@ -3412,6 +3422,35 @@ pub enum Message {
|
||||
/// Background extraction/write completion.
|
||||
WblockWriteFinished(String, std::path::PathBuf, Result<(), String>),
|
||||
// ── DATAEXTRACTION ────────────────────────────────────────────────────
|
||||
TableInsertStyle(String),
|
||||
TableInsertField(crate::ui::window::annotation_data::TableInsertField),
|
||||
TableInsertApply,
|
||||
DataLinkManagerOpen,
|
||||
DataLinkNew,
|
||||
DataLinkSelect(acadrust::types::Handle),
|
||||
DataLinkEdit,
|
||||
DataLinkEditCancel,
|
||||
DataLinkField(crate::ui::window::annotation_data::DataLinkField),
|
||||
DataLinkBrowse,
|
||||
DataLinkBrowseResult(Option<std::path::PathBuf>),
|
||||
DataLinkSave,
|
||||
DataLinkDelete,
|
||||
DataLinkInsert,
|
||||
DataLinkClose,
|
||||
DataExtractionOpen,
|
||||
DataExtractionField(crate::ui::window::annotation_data::DataExtractionField),
|
||||
DataExtractionBack,
|
||||
DataExtractionNext,
|
||||
DataExtractionBrowseSettings,
|
||||
DataExtractionBrowseSettingsResult(Option<std::path::PathBuf>),
|
||||
DataExtractionAddDrawings,
|
||||
DataExtractionAddDrawingsResult(Vec<std::path::PathBuf>),
|
||||
DataExtractionAddFolder,
|
||||
DataExtractionAddFolderResult(Option<std::path::PathBuf>),
|
||||
DataExtractionClearSources,
|
||||
DataExtractionBrowseOutput,
|
||||
DataExtractionBrowseOutputResult(Option<std::path::PathBuf>),
|
||||
DataExtractionFinish,
|
||||
/// Save the pre-built CSV string to a file chosen by the user.
|
||||
DataExtractionSave(String),
|
||||
/// Path chosen (or None = cancelled).
|
||||
@@ -3780,6 +3819,10 @@ impl OpenCADStudio {
|
||||
anno_object_scale_target: None,
|
||||
scale_rename_buf: String::new(),
|
||||
scale_stage: None,
|
||||
table_insert: Default::default(),
|
||||
data_link_manager: Default::default(),
|
||||
data_link_parent_table: false,
|
||||
data_extraction: Default::default(),
|
||||
layout_manager_rename_buf: String::new(),
|
||||
plotstyle_panel_aci: 1,
|
||||
ps_color_buf: String::new(),
|
||||
|
||||
@@ -1075,6 +1075,59 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::TableInsertStyle(value) => self.on_table_insert_style(value),
|
||||
Message::TableInsertField(field) => self.on_table_insert_field(field),
|
||||
Message::TableInsertApply => self.on_table_insert_apply(),
|
||||
Message::DataLinkManagerOpen => {
|
||||
let from_table = self.active_modal == Some(super::ModalKind::InsertTable);
|
||||
self.open_data_link_manager(from_table);
|
||||
Task::none()
|
||||
}
|
||||
Message::DataLinkNew => self.on_data_link_new(),
|
||||
Message::DataLinkSelect(handle) => self.on_data_link_select(handle),
|
||||
Message::DataLinkEdit => self.on_data_link_edit(),
|
||||
Message::DataLinkEditCancel => {
|
||||
self.data_link_manager.editing = false;
|
||||
self.data_link_manager.editing_handle = None;
|
||||
self.data_link_manager.status.clear();
|
||||
Task::none()
|
||||
}
|
||||
Message::DataLinkField(field) => self.on_data_link_field(field),
|
||||
Message::DataLinkBrowse => self.on_data_link_browse(),
|
||||
Message::DataLinkBrowseResult(path) => self.on_data_link_browse_result(path),
|
||||
Message::DataLinkSave => self.on_data_link_save(),
|
||||
Message::DataLinkDelete => self.on_data_link_delete(),
|
||||
Message::DataLinkInsert => self.on_data_link_insert(),
|
||||
Message::DataLinkClose => self.on_data_link_close(),
|
||||
Message::DataExtractionOpen => {
|
||||
self.open_data_extraction();
|
||||
Task::none()
|
||||
}
|
||||
Message::DataExtractionField(field) => self.on_data_extraction_field(field),
|
||||
Message::DataExtractionBack => self.on_data_extraction_back(),
|
||||
Message::DataExtractionNext => self.on_data_extraction_next(),
|
||||
Message::DataExtractionBrowseSettings => self.on_data_extraction_browse_settings(),
|
||||
Message::DataExtractionBrowseSettingsResult(path) => {
|
||||
self.on_data_extraction_browse_settings_result(path)
|
||||
}
|
||||
Message::DataExtractionAddDrawings => self.on_data_extraction_add_drawings(),
|
||||
Message::DataExtractionAddDrawingsResult(paths) => {
|
||||
self.on_data_extraction_add_drawings_result(paths)
|
||||
}
|
||||
Message::DataExtractionAddFolder => self.on_data_extraction_add_folder(),
|
||||
Message::DataExtractionAddFolderResult(path) => {
|
||||
self.on_data_extraction_add_folder_result(path)
|
||||
}
|
||||
Message::DataExtractionClearSources => {
|
||||
self.data_extraction.source_files.clear();
|
||||
Task::none()
|
||||
}
|
||||
Message::DataExtractionBrowseOutput => self.on_data_extraction_browse_output(),
|
||||
Message::DataExtractionBrowseOutputResult(path) => {
|
||||
self.on_data_extraction_browse_output_result(path)
|
||||
}
|
||||
Message::DataExtractionFinish => self.on_data_extraction_finish(),
|
||||
|
||||
Message::DataExtractionSave(csv) => {
|
||||
let csv_clone = csv.clone();
|
||||
Task::perform(
|
||||
|
||||
@@ -31,6 +31,9 @@ impl OpenCADStudio {
|
||||
Some(K::LayoutManager) => crate::tr!("modal", "layout-manager"),
|
||||
Some(K::ScaleManager) => crate::tr!("modal", "scale-manager"),
|
||||
Some(K::AnnoObjectScale) => crate::tr!("modal", "annotation-object-scale"),
|
||||
Some(K::InsertTable) => crate::t!("Insert Table").into_owned(),
|
||||
Some(K::DataLinkManager) => crate::t!("Data Link Manager").into_owned(),
|
||||
Some(K::DataExtraction) => crate::t!("Data Extraction Wizard").into_owned(),
|
||||
Some(K::Plotstyle) => crate::tr!("modal", "plot-style-editor"),
|
||||
Some(K::TextStyle) => crate::tr!("modal", "text-style-manager"),
|
||||
Some(K::MlStyle) => crate::tr!("modal", "multiline-style-manager"),
|
||||
@@ -581,6 +584,24 @@ impl OpenCADStudio {
|
||||
},
|
||||
)
|
||||
}
|
||||
super::super::ModalKind::InsertTable => sized_flow(
|
||||
ex,
|
||||
620,
|
||||
650,
|
||||
|flow| crate::ui::window::annotation_data::table_insert_view(&self.table_insert, flow),
|
||||
),
|
||||
super::super::ModalKind::DataLinkManager => sized_flow(
|
||||
ex,
|
||||
760,
|
||||
560,
|
||||
|flow| crate::ui::window::annotation_data::data_link_view(&self.data_link_manager, flow),
|
||||
),
|
||||
super::super::ModalKind::DataExtraction => sized_flow(
|
||||
ex,
|
||||
780,
|
||||
570,
|
||||
|flow| crate::ui::window::annotation_data::data_extraction_view(&self.data_extraction, flow),
|
||||
),
|
||||
super::super::ModalKind::Plotstyle => sized_flow(
|
||||
ex,
|
||||
780,
|
||||
|
||||
Reference in new issue
Block a user