Merge pull request #1246

This commit is contained in:
Hakan Seven committed 2026-09-14 19:58:35 +03:00
commit 293d0796fb
38 files changed
+4399 -487

No files matched your search

File diff suppressed because it is too large. Load diff
+600 -25
View File
@@ -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
View File
@@ -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
View File
@@ -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('"', "\"\""))
+1 -78
View File
@@ -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).
+2 -1
View File
@@ -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",
+43
View File
@@ -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(),
+53
View File
@@ -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(
+21
View File
@@ -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,