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
Generated
+99
-1
@@ -13,6 +13,7 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cadkernel",
|
"cadkernel",
|
||||||
"cadkernel-constraints",
|
"cadkernel-constraints",
|
||||||
|
"calamine",
|
||||||
"clap",
|
"clap",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"cosmic-text 0.15.0",
|
"cosmic-text 0.15.0",
|
||||||
@@ -588,6 +589,16 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atoi_simd"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3cdb3708a128e559a30fb830e8a77a5022ee6902806925c216658652b452a44"
|
||||||
|
dependencies = [
|
||||||
|
"debug_unsafe",
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -924,6 +935,23 @@ dependencies = [
|
|||||||
"nalgebra",
|
"nalgebra",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "calamine"
|
||||||
|
version = "0.36.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5fa68281b1a76b54a62156474adb06bb380a67e07dd60656e3217152b42183f3"
|
||||||
|
dependencies = [
|
||||||
|
"atoi_simd",
|
||||||
|
"byteorder",
|
||||||
|
"codepage",
|
||||||
|
"encoding_rs",
|
||||||
|
"fast-float2",
|
||||||
|
"log",
|
||||||
|
"quick-xml 0.41.0",
|
||||||
|
"serde",
|
||||||
|
"zip",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "calloop"
|
name = "calloop"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@@ -1070,6 +1098,15 @@ dependencies = [
|
|||||||
"error-code",
|
"error-code",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codepage"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bdff162541cd8b79de82e2edcc7eff3a8c2a6dc3d75152636028f96d93de3b26"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codespan-reporting"
|
name = "codespan-reporting"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@@ -1363,6 +1400,12 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "debug_unsafe"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7eed2c4702fa172d1ce21078faa7c5203e69f5394d48cc436d25928394a867a2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "defmt"
|
name = "defmt"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -1692,6 +1735,12 @@ dependencies = [
|
|||||||
"zune-inflate",
|
"zune-inflate",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fast-float2"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6e8948ce679d00a02a94739ea185595dca7118ed04feb991127e443bd3d761f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
@@ -1748,6 +1797,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
|
"zlib-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4736,6 +4786,16 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.41.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.46"
|
version = "1.0.46"
|
||||||
@@ -6166,6 +6226,12 @@ dependencies = [
|
|||||||
"rustc-hash 2.1.3",
|
"rustc-hash 2.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typed-path"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -6643,7 +6709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a"
|
checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quick-xml",
|
"quick-xml 0.39.4",
|
||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -7608,12 +7674,44 @@ dependencies = [
|
|||||||
"syn 3.0.3",
|
"syn 3.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "8.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"flate2",
|
||||||
|
"indexmap",
|
||||||
|
"memchr",
|
||||||
|
"typed-path",
|
||||||
|
"zopfli",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zlib-rs"
|
||||||
|
version = "0.6.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zopfli"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"crc32fast",
|
||||||
|
"log",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zune-core"
|
name = "zune-core"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ winresource = "0.1"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
calamine = "0.36.1"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
semver = "1"
|
semver = "1"
|
||||||
url = "2"
|
url = "2"
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
+600
-25
@@ -2474,6 +2474,7 @@ impl OpenCADStudio {
|
|||||||
CmdResult::CommitManyAndEditText {
|
CmdResult::CommitManyAndEditText {
|
||||||
entities,
|
entities,
|
||||||
edit_index,
|
edit_index,
|
||||||
|
open_editor,
|
||||||
} => {
|
} => {
|
||||||
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
||||||
let delta_safe = entities
|
let delta_safe = entities
|
||||||
@@ -2527,8 +2528,10 @@ impl OpenCADStudio {
|
|||||||
if let Some(pd) = pending {
|
if let Some(pd) = pending {
|
||||||
self.commit_undo_delta(i, pd);
|
self.commit_undo_delta(i, pd);
|
||||||
}
|
}
|
||||||
if let Some(h) = edit_handle {
|
if open_editor {
|
||||||
return self.begin_text_edit(h);
|
if let Some(h) = edit_handle {
|
||||||
|
return self.begin_text_edit(h);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CmdResult::CreateBlock {
|
CmdResult::CreateBlock {
|
||||||
@@ -2777,6 +2780,33 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
self.refresh_properties();
|
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 } => {
|
CmdResult::AddSketchConstraint { kind, refs, driving_param, label } => {
|
||||||
if let Err(message) = self.tabs[i]
|
if let Err(message) = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
@@ -2948,36 +2978,111 @@ impl OpenCADStudio {
|
|||||||
if new_entities.len() == 1 {
|
if new_entities.len() == 1 {
|
||||||
if let acadrust::EntityType::XLine(ref xl) = new_entities[0] {
|
if let acadrust::EntityType::XLine(ref xl) = new_entities[0] {
|
||||||
let layer = xl.common.layer.clone();
|
let layer = xl.common.layer.clone();
|
||||||
if layer.starts_with("__DIMBREAK__")
|
if layer.starts_with("__DIMBREAK_") {
|
||||||
|| layer.starts_with("__DIMBREAK_AUTO__")
|
self.push_undo_snapshot(i, "DIMBREAK");
|
||||||
{
|
let result = apply_dimbreak(&mut self.tabs[i].scene, &layer);
|
||||||
// DIMBREAK needs a break-gap field on the dimension
|
if result.changed.is_empty() {
|
||||||
// model (not yet present) to store and render the gap.
|
self.command_line.push_info(&result.message);
|
||||||
// Report honestly rather than claiming success while
|
self.tabs[i].active_cmd = None;
|
||||||
// changing nothing. (#181 / DIM-020)
|
self.tabs[i].snap_result = None;
|
||||||
self.command_line
|
return Task::none();
|
||||||
.push_info(crate::t!("DIMBREAK: not yet implemented — nothing changed.").as_ref());
|
}
|
||||||
|
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].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
if layer.starts_with("__DIMSPACE__") {
|
if layer.starts_with("__DIMSPACE__") {
|
||||||
|
self.push_undo_snapshot(i, "DIMSPACE");
|
||||||
if let Some(encoded) = layer.strip_prefix("__DIMSPACE__") {
|
if let Some(encoded) = layer.strip_prefix("__DIMSPACE__") {
|
||||||
apply_dimspace(&mut self.tabs[i].scene, encoded);
|
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.command_line.push_output(crate::t!("DIMSPACE Spacing adjusted.").as_ref());
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
if layer.starts_with("__DIMJOG__") {
|
if layer.starts_with("__DIMJOG__")
|
||||||
// DIMJOGLINE needs a jog-point field on the dimension
|
|| layer.starts_with("__DIMJOG_REMOVE__")
|
||||||
// model (not yet present) to store and render the jog.
|
{
|
||||||
// Report honestly rather than faking success. (DIM-019)
|
let remove = layer.starts_with("__DIMJOG_REMOVE__");
|
||||||
self.command_line
|
let point = layer
|
||||||
.push_info(crate::t!("DIMJOGLINE: not yet implemented — nothing changed.").as_ref());
|
.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].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -5621,6 +5726,72 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
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 } => {
|
CmdResult::DdeditEntity { handle, new_text } => {
|
||||||
if self.reject_locked_edit(i, handle) {
|
if self.reject_locked_edit(i, handle) {
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
@@ -6176,6 +6347,382 @@ fn remap_object(
|
|||||||
|
|
||||||
// ── DIMSPACE helper ───────────────────────────────────────────────────────────
|
// ── 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.
|
/// Parse `base_val,h1;h2;...;hN,spacing` and adjust parallel dimension positions.
|
||||||
fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
|
fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
|
||||||
// Format: "<base_handle>,<h1>;<h2>;...;<hN>,<spacing>"
|
// Format: "<base_handle>,<h1>;<h2>;...;<hN>,<spacing>"
|
||||||
@@ -6188,7 +6735,11 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
|
|||||||
.split(';')
|
.split(';')
|
||||||
.filter_map(|s| s.parse::<u64>().ok())
|
.filter_map(|s| s.parse::<u64>().ok())
|
||||||
.collect();
|
.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;
|
use acadrust::entities::Dimension;
|
||||||
let base_h = acadrust::Handle::from(base_val);
|
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
|
// 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
|
// 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)
|
// 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) {
|
let (perp, base_coord, auto_spacing) = match scene.document.get_entity(base_h) {
|
||||||
Some(acadrust::EntityType::Dimension(Dimension::Linear(d))) => {
|
Some(acadrust::EntityType::Dimension(dimension @ Dimension::Linear(d))) => {
|
||||||
let (s, c) = d.rotation.sin_cos();
|
let (s, c) = d.rotation.sin_cos();
|
||||||
let perp = (-s, c);
|
let perp = (-s, c);
|
||||||
let dp = d.definition_point;
|
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 dx = d.second_point.x - d.first_point.x;
|
||||||
let dy = d.second_point.y - d.first_point.y;
|
let dy = d.second_point.y - d.first_point.y;
|
||||||
let len = (dx * dx + dy * dy).sqrt().max(1e-12);
|
let len = (dx * dx + dy * dy).sqrt().max(1e-12);
|
||||||
let perp = (-dy / len, dx / len);
|
let perp = (-dy / len, dx / len);
|
||||||
let dp = d.definition_point;
|
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,
|
_ => 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();
|
let mut changes = Vec::new();
|
||||||
for (idx, &hv) in other_vals.iter().enumerate() {
|
for (idx, &hv) in other_vals.iter().enumerate() {
|
||||||
let h = acadrust::Handle::from(hv);
|
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(
|
fn apply_mleader_align(
|
||||||
scene: &mut crate::scene::Scene,
|
scene: &mut crate::scene::Scene,
|
||||||
handles: &[acadrust::Handle],
|
handles: &[acadrust::Handle],
|
||||||
|
|||||||
+1
-26
@@ -668,32 +668,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
"TABLE" => {
|
"TABLE" => {
|
||||||
use crate::modules::annotate::table_cmd::TableCommand;
|
self.open_table_insert();
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"TABLEDIT" => {
|
"TABLEDIT" => {
|
||||||
|
|||||||
+180
-57
@@ -1041,55 +1041,110 @@ impl OpenCADStudio {
|
|||||||
return Some(Task::done(Message::PlotDialogOpen));
|
return Some(Task::done(Message::PlotDialogOpen));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Recognized commands whose full implementation is pending ─────────
|
// Add or remove the current representation on the selected
|
||||||
// These verbs are surfaced by the ribbon / menus but their feature is
|
// annotative objects. With no preselection, gather one through the
|
||||||
// still being built. Acknowledge them with an honest status so the
|
// regular selection engine before applying the action.
|
||||||
// button responds instead of reporting an unknown command; each is
|
action @ ("OBJECTSCALE ADD" | "OBJECTSCALE DELETE") => {
|
||||||
// replaced by its real handler as the feature lands.
|
let selected: Vec<acadrust::Handle> = self.tabs[i]
|
||||||
// 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]
|
|
||||||
.scene
|
.scene
|
||||||
.selected_entities()
|
.selected_entities()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(h, _)| *h)
|
.map(|(h, _)| *h)
|
||||||
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
||||||
.collect();
|
.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
|
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());
|
return Some(Task::none());
|
||||||
}
|
}
|
||||||
self.push_undo_snapshot(i, "OBJECTSCALE");
|
|
||||||
let Some(scale) = self.tabs[i].scene.creation_annotation_scale_handle() else {
|
let Some(scale) = self.tabs[i].scene.creation_annotation_scale_handle() else {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error(crate::t!("OBJECTSCALE: the active annotation scale is unavailable.").as_ref());
|
.push_error(crate::t!("OBJECTSCALE: the active annotation scale is unavailable.").as_ref());
|
||||||
return Some(Task::none());
|
return Some(Task::none());
|
||||||
};
|
};
|
||||||
let mut n = 0usize;
|
|
||||||
for h in &handles {
|
let adding = action.ends_with("ADD");
|
||||||
if crate::scene::annotative::create_annotation_context(
|
let handles: Vec<_> = selected
|
||||||
&mut self.tabs[i].scene.document,
|
.into_iter()
|
||||||
*h,
|
.filter(|handle| {
|
||||||
scale,
|
let memberships = crate::scene::annotative::object_scale_memberships(
|
||||||
) {
|
&self.tabs[i].scene.document,
|
||||||
crate::scene::annotative::set_entity_annotative(
|
*handle,
|
||||||
&mut self.tabs[i].scene.document,
|
|
||||||
*h,
|
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
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
|
let changes: Vec<_> = changed
|
||||||
.into_iter()
|
.iter()
|
||||||
|
.copied()
|
||||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
.collect();
|
.collect();
|
||||||
self.tabs[i].scene.bump_entities(&changes);
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.command_line.push_output(crate::tf!(
|
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());
|
).as_ref());
|
||||||
return Some(Task::none());
|
return Some(Task::none());
|
||||||
}
|
}
|
||||||
@@ -1459,12 +1514,78 @@ impl OpenCADStudio {
|
|||||||
return Some(Task::done(Message::AnnoObjectScaleOpen));
|
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 <path.csv> — create a persistent linked table.
|
||||||
"DATALINK" => {
|
"DATALINK" => {
|
||||||
use crate::command::ValuePromptCommand;
|
self.open_data_link_manager(false);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
cmd if cmd.starts_with("DATALINK ") => {
|
cmd if cmd.starts_with("DATALINK ") => {
|
||||||
let path = cmd.trim_start_matches("DATALINK").trim();
|
let path = cmd.trim_start_matches("DATALINK").trim();
|
||||||
@@ -1566,20 +1687,7 @@ impl OpenCADStudio {
|
|||||||
let Some(link_handle) = link_handle else {
|
let Some(link_handle) = link_handle else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let path = match self.tabs[i].scene.document.objects.get(&link_handle) {
|
jobs.push((*handle, 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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if jobs.is_empty() {
|
if jobs.is_empty() {
|
||||||
self.command_line
|
self.command_line
|
||||||
@@ -1588,28 +1696,43 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
if write_back {
|
if write_back {
|
||||||
let mut written = 0usize;
|
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)) =
|
let Some(acadrust::EntityType::Table(table)) =
|
||||||
self.tabs[i].scene.document.get_entity(*table_handle)
|
self.tabs[i].scene.document.get_entity(*table_handle)
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let csv = table_to_csv(table);
|
let csv = table_to_csv(table);
|
||||||
if std::fs::write(path, csv).is_ok() {
|
match crate::app::annotation_data::data_link_write_path(
|
||||||
written += 1;
|
&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(
|
if written > 0 {
|
||||||
crate::tf!("DATALINKUPDATE: wrote {} linked source(s).", written).as_ref(),
|
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());
|
return Some(Task::none());
|
||||||
}
|
}
|
||||||
let updates: Vec<_> = jobs
|
let updates: Vec<_> = jobs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(table_handle, link_handle, path)| {
|
.filter_map(|(table_handle, link_handle)| {
|
||||||
std::fs::read_to_string(&path)
|
crate::app::annotation_data::read_data_link(
|
||||||
.ok()
|
&self.tabs[i].scene.document,
|
||||||
.map(|text| (table_handle, link_handle, parse_csv_table(&text)))
|
link_handle,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.map(|rows| (table_handle, link_handle, rows))
|
||||||
})
|
})
|
||||||
.filter(|(_, _, rows)| !rows.is_empty())
|
.filter(|(_, _, rows)| !rows.is_empty())
|
||||||
.collect();
|
.collect();
|
||||||
@@ -1764,7 +1887,7 @@ fn parse_landxml_cgpoints(xml: &str) -> Vec<[f64; 3]> {
|
|||||||
out
|
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 rows = Vec::new();
|
||||||
let mut row = Vec::new();
|
let mut row = Vec::new();
|
||||||
let mut field = String::new();
|
let mut field = String::new();
|
||||||
@@ -1804,7 +1927,7 @@ fn parse_csv_table(text: &str) -> Vec<Vec<String>> {
|
|||||||
rows
|
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 {
|
fn escape(value: &str) -> String {
|
||||||
if value.contains([',', '"', '\r', '\n']) {
|
if value.contains([',', '"', '\r', '\n']) {
|
||||||
format!("\"{}\"", value.replace('"', "\"\""))
|
format!("\"{}\"", value.replace('"', "\"\""))
|
||||||
|
|||||||
@@ -1198,8 +1198,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
"DATAEXTRACTION" | "EATTEXT" | "ATTEXT" => {
|
"DATAEXTRACTION" | "EATTEXT" | "ATTEXT" => {
|
||||||
let csv = build_data_extraction_csv(&self.tabs[i].scene.document);
|
self.open_data_extraction();
|
||||||
return Some(Task::done(Message::DataExtractionSave(csv)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Find / Replace ────────────────────────────────────────────────
|
// ── 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
|
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 ──────────────────────────────────
|
// ── CAL — arithmetic expression evaluator ──────────────────────────────────
|
||||||
// A small recursive-descent parser for `+ - * /`, parentheses, unary signs and
|
// A small recursive-descent parser for `+ - * /`, parentheses, unary signs and
|
||||||
// decimal numbers. Self-contained (no external dependency).
|
// decimal numbers. Self-contained (no external dependency).
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
mod blocks;
|
mod blocks;
|
||||||
mod dim;
|
mod dim;
|
||||||
mod display;
|
pub(crate) mod display;
|
||||||
mod draw;
|
mod draw;
|
||||||
mod fileops;
|
mod fileops;
|
||||||
mod inquiry;
|
mod inquiry;
|
||||||
@@ -400,6 +400,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
|||||||
"ANNOUPDATE",
|
"ANNOUPDATE",
|
||||||
"SCALELISTEDIT",
|
"SCALELISTEDIT",
|
||||||
"OBJECTSCALE",
|
"OBJECTSCALE",
|
||||||
|
"ANNORESET",
|
||||||
// Import CSV into a table + LandXML survey points.
|
// Import CSV into a table + LandXML survey points.
|
||||||
"DATALINK",
|
"DATALINK",
|
||||||
"DATALINKUPDATE",
|
"DATALINKUPDATE",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ mod startup;
|
|||||||
mod style_ops;
|
mod style_ops;
|
||||||
mod text_inline;
|
mod text_inline;
|
||||||
mod tolerance_dialog;
|
mod tolerance_dialog;
|
||||||
|
mod annotation_data;
|
||||||
mod update;
|
mod update;
|
||||||
mod view;
|
mod view;
|
||||||
mod visibility;
|
mod visibility;
|
||||||
@@ -1092,6 +1093,12 @@ pub(super) struct OpenCADStudio {
|
|||||||
/// without Apply, mirroring the style managers' staging.
|
/// without Apply, mirroring the style managers' staging.
|
||||||
scale_stage: Option<crate::app::style_ops::ScaleStage>,
|
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 ──────────────────────────────────────────────────
|
// ── Plot Style Panel ──────────────────────────────────────────────────
|
||||||
/// Selected ACI index in the panel (1-255).
|
/// Selected ACI index in the panel (1-255).
|
||||||
plotstyle_panel_aci: u8,
|
plotstyle_panel_aci: u8,
|
||||||
@@ -1747,6 +1754,9 @@ pub enum ModalKind {
|
|||||||
/// Add / remove the annotation scales a single selected object has a
|
/// Add / remove the annotation scales a single selected object has a
|
||||||
/// per-object representation for.
|
/// per-object representation for.
|
||||||
AnnoObjectScale,
|
AnnoObjectScale,
|
||||||
|
InsertTable,
|
||||||
|
DataLinkManager,
|
||||||
|
DataExtraction,
|
||||||
/// The scene is drawn by a software rasterizer, or not at all: what that
|
/// 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
|
/// means and what usually fixes it. Queued once per verdict; the status
|
||||||
/// bar's ⚠ pill reopens it.
|
/// bar's ⚠ pill reopens it.
|
||||||
@@ -3412,6 +3422,35 @@ pub enum Message {
|
|||||||
/// Background extraction/write completion.
|
/// Background extraction/write completion.
|
||||||
WblockWriteFinished(String, std::path::PathBuf, Result<(), String>),
|
WblockWriteFinished(String, std::path::PathBuf, Result<(), String>),
|
||||||
// ── DATAEXTRACTION ────────────────────────────────────────────────────
|
// ── 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.
|
/// Save the pre-built CSV string to a file chosen by the user.
|
||||||
DataExtractionSave(String),
|
DataExtractionSave(String),
|
||||||
/// Path chosen (or None = cancelled).
|
/// Path chosen (or None = cancelled).
|
||||||
@@ -3780,6 +3819,10 @@ impl OpenCADStudio {
|
|||||||
anno_object_scale_target: None,
|
anno_object_scale_target: None,
|
||||||
scale_rename_buf: String::new(),
|
scale_rename_buf: String::new(),
|
||||||
scale_stage: None,
|
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(),
|
layout_manager_rename_buf: String::new(),
|
||||||
plotstyle_panel_aci: 1,
|
plotstyle_panel_aci: 1,
|
||||||
ps_color_buf: String::new(),
|
ps_color_buf: String::new(),
|
||||||
|
|||||||
@@ -1075,6 +1075,59 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
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) => {
|
Message::DataExtractionSave(csv) => {
|
||||||
let csv_clone = csv.clone();
|
let csv_clone = csv.clone();
|
||||||
Task::perform(
|
Task::perform(
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ impl OpenCADStudio {
|
|||||||
Some(K::LayoutManager) => crate::tr!("modal", "layout-manager"),
|
Some(K::LayoutManager) => crate::tr!("modal", "layout-manager"),
|
||||||
Some(K::ScaleManager) => crate::tr!("modal", "scale-manager"),
|
Some(K::ScaleManager) => crate::tr!("modal", "scale-manager"),
|
||||||
Some(K::AnnoObjectScale) => crate::tr!("modal", "annotation-object-scale"),
|
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::Plotstyle) => crate::tr!("modal", "plot-style-editor"),
|
||||||
Some(K::TextStyle) => crate::tr!("modal", "text-style-manager"),
|
Some(K::TextStyle) => crate::tr!("modal", "text-style-manager"),
|
||||||
Some(K::MlStyle) => crate::tr!("modal", "multiline-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(
|
super::super::ModalKind::Plotstyle => sized_flow(
|
||||||
ex,
|
ex,
|
||||||
780,
|
780,
|
||||||
|
|||||||
@@ -1314,6 +1314,14 @@ impl Default for LoftOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum DimensionEditOperation {
|
||||||
|
Home,
|
||||||
|
NewText(String),
|
||||||
|
Rotate(f64),
|
||||||
|
Oblique(f64),
|
||||||
|
}
|
||||||
|
|
||||||
/// Returned by every `CadCommand` method to tell main.rs what to do.
|
/// Returned by every `CadCommand` method to tell main.rs what to do.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum CmdResult {
|
pub enum CmdResult {
|
||||||
@@ -1376,6 +1384,7 @@ pub enum CmdResult {
|
|||||||
CommitManyAndEditText {
|
CommitManyAndEditText {
|
||||||
entities: Vec<EntityType>,
|
entities: Vec<EntityType>,
|
||||||
edit_index: usize,
|
edit_index: usize,
|
||||||
|
open_editor: bool,
|
||||||
},
|
},
|
||||||
/// Create a block definition from existing entities and insert one reference.
|
/// Create a block definition from existing entities and insert one reference.
|
||||||
CreateBlock {
|
CreateBlock {
|
||||||
@@ -1412,6 +1421,8 @@ pub enum CmdResult {
|
|||||||
BatchCopy(Vec<Handle>, Vec<EntityTransform>),
|
BatchCopy(Vec<Handle>, Vec<EntityTransform>),
|
||||||
/// Erase `handle` and replace with new entities; command stays active.
|
/// Erase `handle` and replace with new entities; command stays active.
|
||||||
ReplaceEntity(Handle, Vec<EntityType>),
|
ReplaceEntity(Handle, Vec<EntityType>),
|
||||||
|
/// Update one entity in place, preserve its handle, and end the command.
|
||||||
|
UpdateEntityAndFinish { handle: Handle, entity: EntityType },
|
||||||
/// Replace / delete multiple entities and add new ones; command ends.
|
/// Replace / delete multiple entities and add new ones; command ends.
|
||||||
/// Each pair: (handle_to_erase, replacement_entities) — empty vec = delete only.
|
/// Each pair: (handle_to_erase, replacement_entities) — empty vec = delete only.
|
||||||
ReplaceMany(Vec<(Handle, Vec<EntityType>)>, Vec<EntityType>),
|
ReplaceMany(Vec<(Handle, Vec<EntityType>)>, Vec<EntityType>),
|
||||||
@@ -1592,6 +1603,11 @@ pub enum CmdResult {
|
|||||||
QuickPrint(Vec<Handle>),
|
QuickPrint(Vec<Handle>),
|
||||||
/// Replace the text content of a Text/MText entity in-place.
|
/// Replace the text content of a Text/MText entity in-place.
|
||||||
DdeditEntity { handle: Handle, new_text: String },
|
DdeditEntity { handle: Handle, new_text: String },
|
||||||
|
/// Apply one DIMEDIT operation to every selected dimension.
|
||||||
|
EditDimensions {
|
||||||
|
handles: Vec<Handle>,
|
||||||
|
operation: DimensionEditOperation,
|
||||||
|
},
|
||||||
/// Open the in-place editor (plain box or rich MText editor, per type) for
|
/// Open the in-place editor (plain box or rich MText editor, per type) for
|
||||||
/// a text-bearing entity picked by a command such as DDEDIT.
|
/// a text-bearing entity picked by a command such as DDEDIT.
|
||||||
EditTextEntity { handle: Handle },
|
EditTextEntity { handle: Handle },
|
||||||
|
|||||||
+100
-1
@@ -3235,6 +3235,93 @@ fn apply_dimension_breaks(
|
|||||||
*lines = output;
|
*lines = output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dimension_jog_point(dimension: &Dimension) -> Option<Vec3> {
|
||||||
|
if !matches!(dimension, Dimension::Linear(_) | Dimension::Aligned(_)) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
dimension
|
||||||
|
.base()
|
||||||
|
.common
|
||||||
|
.extended_data
|
||||||
|
.get_record("ACAD_DSTYLE_DIMJAG_POSITION")
|
||||||
|
.and_then(|record| {
|
||||||
|
record.values.iter().rev().find_map(|value| match value {
|
||||||
|
acadrust::xdata::XDataValue::Point3D(point) => Some(vec3_local(*point)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_dimension_jog(lines: &mut Vec<[f32; 3]>, requested: Vec3, size: f32, angle: f32) {
|
||||||
|
if lines.len() < 2 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut best: Option<(usize, Vec3, f32)> = None;
|
||||||
|
for (index, segment) in lines.windows(2).enumerate() {
|
||||||
|
if segment[0][0].is_nan() || segment[1][0].is_nan() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let first = Vec3::from_array(segment[0]);
|
||||||
|
let second = Vec3::from_array(segment[1]);
|
||||||
|
let delta = second - first;
|
||||||
|
let length_squared = delta.length_squared();
|
||||||
|
if length_squared <= 1.0e-12 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let parameter = ((requested - first).dot(delta) / length_squared).clamp(0.0, 1.0);
|
||||||
|
let point = first + delta * parameter;
|
||||||
|
let distance = point.distance_squared(requested);
|
||||||
|
if best.is_none_or(|(_, _, current)| distance < current) {
|
||||||
|
best = Some((index, point, distance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some((target, center, _)) = best else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let first = Vec3::from_array(lines[target]);
|
||||||
|
let second = Vec3::from_array(lines[target + 1]);
|
||||||
|
let Some(direction) = (second - first).try_normalize() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let perpendicular = Vec3::new(-direction.y, direction.x, 0.0);
|
||||||
|
let half = size.max(1.0e-3).min(first.distance(second) * 0.2);
|
||||||
|
let amplitude = (half * (angle * 0.5).tan().abs()).clamp(half * 0.35, half * 1.5);
|
||||||
|
let jog_start = center - direction * half;
|
||||||
|
let jog_end = center + direction * half;
|
||||||
|
let jog = [
|
||||||
|
jog_start,
|
||||||
|
center - direction * half * 0.25 + perpendicular * amplitude,
|
||||||
|
center + direction * half * 0.25 - perpendicular * amplitude,
|
||||||
|
jog_end,
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut output = Vec::with_capacity(lines.len() + 4);
|
||||||
|
output.extend_from_slice(&lines[..target]);
|
||||||
|
if !output.is_empty() && !output.last().is_some_and(|point| point[0].is_nan()) {
|
||||||
|
output.push([f32::NAN; 3]);
|
||||||
|
}
|
||||||
|
if first.distance_squared(jog_start) > 1.0e-12 {
|
||||||
|
output.push(first.to_array());
|
||||||
|
output.push(jog_start.to_array());
|
||||||
|
output.push([f32::NAN; 3]);
|
||||||
|
}
|
||||||
|
output.extend(jog.into_iter().map(|point| point.to_array()));
|
||||||
|
if jog_end.distance_squared(second) > 1.0e-12 {
|
||||||
|
output.push([f32::NAN; 3]);
|
||||||
|
output.push(jog_end.to_array());
|
||||||
|
output.push(second.to_array());
|
||||||
|
}
|
||||||
|
if target + 2 < lines.len() {
|
||||||
|
if !output.last().is_some_and(|point| point[0].is_nan())
|
||||||
|
&& !lines[target + 2][0].is_nan()
|
||||||
|
{
|
||||||
|
output.push([f32::NAN; 3]);
|
||||||
|
}
|
||||||
|
output.extend_from_slice(&lines[target + 2..]);
|
||||||
|
}
|
||||||
|
*lines = output;
|
||||||
|
}
|
||||||
|
|
||||||
pub trait DimensionTess {
|
pub trait DimensionTess {
|
||||||
fn tessellate(
|
fn tessellate(
|
||||||
&self,
|
&self,
|
||||||
@@ -3568,12 +3655,19 @@ fn tessellate_dimension_inner(
|
|||||||
// DIMUPT governs interactive creation-time text placement; saved
|
// DIMUPT governs interactive creation-time text placement; saved
|
||||||
// geometry already carries the resulting position.
|
// geometry already carries the resulting position.
|
||||||
let _ = s.dimupt;
|
let _ = s.dimupt;
|
||||||
let _ = (s.dimarcsym, s.dimjogang);
|
let _ = s.dimarcsym;
|
||||||
// DIMUNIT is the obsolete pre-R2000 linear unit format; DIMLUNIT
|
// DIMUNIT is the obsolete pre-R2000 linear unit format; DIMLUNIT
|
||||||
// supersedes it. Read but not honoured.
|
// supersedes it. Read but not honoured.
|
||||||
let _ = s.dimunit;
|
let _ = s.dimunit;
|
||||||
}
|
}
|
||||||
|
if let Some(point) = dimension_jog_point(dim) {
|
||||||
|
let jog_angle = style
|
||||||
|
.map(|style| style.dimjogang as f32)
|
||||||
|
.unwrap_or(std::f32::consts::FRAC_PI_4);
|
||||||
|
apply_dimension_jog(&mut geom.dim_lines, point, dim_txt as f32 * 0.6, jog_angle);
|
||||||
|
}
|
||||||
apply_dimension_breaks(document, handle, &mut geom.dim_lines);
|
apply_dimension_breaks(document, handle, &mut geom.dim_lines);
|
||||||
|
apply_dimension_breaks(document, handle, &mut geom.ext_lines);
|
||||||
// Dimension entity fields that the render path doesn't yet use but are
|
// Dimension entity fields that the render path doesn't yet use but are
|
||||||
// preserved on save:
|
// preserved on save:
|
||||||
// - base.insertion_point: legacy anchor reference; render uses
|
// - base.insertion_point: legacy anchor reference; render uses
|
||||||
@@ -7014,6 +7108,11 @@ pub(crate) fn baked_large_radial_geometry(
|
|||||||
dimension.base().common.handle,
|
dimension.base().common.handle,
|
||||||
&mut geometry.dim_lines,
|
&mut geometry.dim_lines,
|
||||||
);
|
);
|
||||||
|
apply_dimension_breaks(
|
||||||
|
document,
|
||||||
|
dimension.base().common.handle,
|
||||||
|
&mut geometry.ext_lines,
|
||||||
|
);
|
||||||
Some(geometry)
|
Some(geometry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use acadrust::EntityType;
|
|||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
|
|
||||||
use crate::command::{
|
use crate::command::{
|
||||||
CadCommand, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
|
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
|
||||||
};
|
};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
@@ -164,11 +164,24 @@ impl CadCommand for AlignedDimensionCommand {
|
|||||||
fn input_kind(&self) -> InputKind {
|
fn input_kind(&self) -> InputKind {
|
||||||
if self.awaiting_text {
|
if self.awaiting_text {
|
||||||
InputKind::FreeText
|
InputKind::FreeText
|
||||||
} else {
|
} else if self.awaiting_angle {
|
||||||
InputKind::SingleToken
|
InputKind::SingleToken
|
||||||
|
} else {
|
||||||
|
InputKind::Point
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
if !matches!(self.step, Step::DimLine { .. }) || self.awaiting_text || self.awaiting_angle {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
vec![
|
||||||
|
CmdOption::new("MText", "MTEXT"),
|
||||||
|
CmdOption::new("Text", "TEXT"),
|
||||||
|
CmdOption::new("Angle", "ANGLE"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
// While entering the override text or angle it is a value, not a point
|
// While entering the override text or angle it is a value, not a point
|
||||||
// step.
|
// step.
|
||||||
|
|||||||
@@ -489,8 +489,10 @@ impl CadCommand for AngularDimensionCommand {
|
|||||||
fn input_kind(&self) -> InputKind {
|
fn input_kind(&self) -> InputKind {
|
||||||
if self.awaiting_text {
|
if self.awaiting_text {
|
||||||
InputKind::FreeText
|
InputKind::FreeText
|
||||||
} else {
|
} else if self.awaiting_angle || self.awaiting_quadrant {
|
||||||
InputKind::SingleToken
|
InputKind::SingleToken
|
||||||
|
} else {
|
||||||
|
InputKind::Point
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use acadrust::Handle;
|
||||||
|
use glam::DVec3;
|
||||||
|
|
||||||
|
use crate::command::{CadCommand, CmdResult};
|
||||||
|
|
||||||
|
/// Selection front-end used by the annotation-scale ribbon actions. The
|
||||||
|
/// action itself runs only after the normal selection engine completes, so
|
||||||
|
/// window/crossing selection, removal, Escape and Previous all keep their
|
||||||
|
/// usual behavior.
|
||||||
|
pub struct AnnotationScaleSelectionCommand {
|
||||||
|
name: &'static str,
|
||||||
|
action: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnnotationScaleSelectionCommand {
|
||||||
|
pub fn new(name: &'static str, action: &'static str) -> Self {
|
||||||
|
Self { name, action }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for AnnotationScaleSelectionCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
crate::tf!("{} Select annotative objects:", self.name).into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_selection_gathering(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||||
|
if handles.is_empty() {
|
||||||
|
CmdResult::Cancel
|
||||||
|
} else {
|
||||||
|
CmdResult::Relaunch(self.action.to_string(), handles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -353,8 +353,10 @@ impl CadCommand for ArcLengthDimensionCommand {
|
|||||||
fn input_kind(&self) -> InputKind {
|
fn input_kind(&self) -> InputKind {
|
||||||
if self.awaiting_text {
|
if self.awaiting_text {
|
||||||
InputKind::FreeText
|
InputKind::FreeText
|
||||||
} else {
|
} else if self.awaiting_angle {
|
||||||
InputKind::SingleToken
|
InputKind::SingleToken
|
||||||
|
} else {
|
||||||
|
InputKind::Point
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub fn tool() -> ToolDef {
|
|||||||
pub struct DataLinkPlaceCommand {
|
pub struct DataLinkPlaceCommand {
|
||||||
table: Table,
|
table: Table,
|
||||||
plane: WorkingPlane,
|
plane: WorkingPlane,
|
||||||
|
command_name: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DataLinkPlaceCommand {
|
impl DataLinkPlaceCommand {
|
||||||
@@ -30,6 +31,43 @@ impl DataLinkPlaceCommand {
|
|||||||
Self {
|
Self {
|
||||||
table,
|
table,
|
||||||
plane: WorkingPlane::default(),
|
plane: WorkingPlane::default(),
|
||||||
|
command_name: "DATALINK",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Place a table that references a data-link object already stored in the
|
||||||
|
/// drawing. Every populated cell is locked and points at the same link so
|
||||||
|
/// update and save/reopen keep one stable relationship.
|
||||||
|
pub fn existing(mut table: Table, link: acadrust::Handle) -> Self {
|
||||||
|
use acadrust::entities::table::CellStateFlags;
|
||||||
|
let rows = table.row_count() as i32;
|
||||||
|
let columns = table.column_count() as i32;
|
||||||
|
for row in &mut table.rows {
|
||||||
|
for cell in &mut row.cells {
|
||||||
|
cell.has_linked_data = true;
|
||||||
|
cell.data_link_handle = Some(link);
|
||||||
|
cell.data_link_rows = rows;
|
||||||
|
cell.data_link_columns = columns;
|
||||||
|
cell.state.insert(
|
||||||
|
CellStateFlags::LINKED
|
||||||
|
| CellStateFlags::CONTENT_LOCKED
|
||||||
|
| CellStateFlags::FORMAT_LOCKED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
table,
|
||||||
|
plane: WorkingPlane::default(),
|
||||||
|
command_name: "DATALINK",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Place a prepared, unlinked table (used by the extraction wizard).
|
||||||
|
pub fn unlinked(table: Table) -> Self {
|
||||||
|
Self {
|
||||||
|
table,
|
||||||
|
plane: WorkingPlane::default(),
|
||||||
|
command_name: "DATAEXTRACTION",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,11 +143,15 @@ impl CadCommand for DataLinkPlaceCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"DATALINK"
|
self.command_name
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
t!("DATALINK Specify insertion point:").into_owned()
|
if self.command_name == "DATALINK" {
|
||||||
|
t!("DATALINK Specify insertion point:").into_owned()
|
||||||
|
} else {
|
||||||
|
t!("DATAEXTRACTION Specify insertion point:").into_owned()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||||
|
|||||||
@@ -139,8 +139,10 @@ impl CadCommand for DiameterDimensionCommand {
|
|||||||
fn input_kind(&self) -> InputKind {
|
fn input_kind(&self) -> InputKind {
|
||||||
if self.awaiting_text {
|
if self.awaiting_text {
|
||||||
InputKind::FreeText
|
InputKind::FreeText
|
||||||
} else {
|
} else if self.awaiting_angle {
|
||||||
InputKind::SingleToken
|
InputKind::SingleToken
|
||||||
|
} else {
|
||||||
|
InputKind::Point
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,9 @@
|
|||||||
// DIMBREAK command — insert a break in a dimension/extension line where
|
// DIMBREAK — create automatic, object-driven or manual gaps in dimensions.
|
||||||
// another object crosses it.
|
|
||||||
//
|
|
||||||
// Simplified workflow:
|
|
||||||
// 1. Select the dimension to break
|
|
||||||
// 2. Select the object that crosses it (or Enter for AUTO mode on all crossings)
|
|
||||||
//
|
|
||||||
// Because breaking a dimension line requires deep geometric intersection logic
|
|
||||||
// that depends on the renderer, this implementation records the target handles
|
|
||||||
// and emits a ReplaceEntity sentinel that commands.rs intercepts to apply the break.
|
|
||||||
|
|
||||||
use acadrust::Handle;
|
use acadrust::{EntityType, Handle};
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
@@ -28,20 +19,54 @@ pub fn tool() -> ToolDef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Step {
|
enum Step {
|
||||||
PickDim,
|
PickDimension,
|
||||||
PickCrossing { dim_handle: Handle },
|
PickDimensions(Vec<Handle>),
|
||||||
|
PickCrossing(Vec<Handle>),
|
||||||
|
ManualFirst(Vec<Handle>),
|
||||||
|
ManualSecond(Vec<Handle>, DVec3),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DimBreakCommand {
|
pub struct DimBreakCommand {
|
||||||
step: Step,
|
step: Step,
|
||||||
|
picked_entity: Option<EntityType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DimBreakCommand {
|
impl DimBreakCommand {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
step: Step::PickDim,
|
step: Step::PickDimension,
|
||||||
|
picked_entity: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn marker(layer: String, primary: Handle) -> CmdResult {
|
||||||
|
use acadrust::entities::XLine;
|
||||||
|
let mut marker = XLine::default();
|
||||||
|
marker.common.layer = layer;
|
||||||
|
CmdResult::ReplaceEntity(primary, vec![EntityType::XLine(marker)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handles(handles: &[Handle]) -> String {
|
||||||
|
handles
|
||||||
|
.iter()
|
||||||
|
.map(|handle| handle.value().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(";")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn automatic(handles: &[Handle]) -> CmdResult {
|
||||||
|
Self::marker(
|
||||||
|
format!("__DIMBREAK_AUTO__{}", Self::handles(handles)),
|
||||||
|
handles[0],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(handles: &[Handle]) -> CmdResult {
|
||||||
|
Self::marker(
|
||||||
|
format!("__DIMBREAK_REMOVE__{}", Self::handles(handles)),
|
||||||
|
handles[0],
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CadCommand for DimBreakCommand {
|
impl CadCommand for DimBreakCommand {
|
||||||
@@ -51,54 +76,151 @@ impl CadCommand for DimBreakCommand {
|
|||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match &self.step {
|
match &self.step {
|
||||||
Step::PickDim => t!("DIMBREAK Select dimension to break:").into_owned(),
|
Step::PickDimension => {
|
||||||
Step::PickCrossing { .. } => {
|
t!("DIMBREAK Select dimension to add/remove break or [Multiple]:").into_owned()
|
||||||
t!("DIMBREAK Select object to break at, or Enter for Auto:").into_owned()
|
}
|
||||||
|
Step::PickDimensions(handles) => crate::tf!(
|
||||||
|
"DIMBREAK Select dimensions ({} selected, press Enter when done):",
|
||||||
|
handles.len()
|
||||||
|
)
|
||||||
|
.into_owned(),
|
||||||
|
Step::PickCrossing(_) => t!(
|
||||||
|
"DIMBREAK Select object to break dimension or [Auto/Manual/Remove] <Auto>:"
|
||||||
|
)
|
||||||
|
.into_owned(),
|
||||||
|
Step::ManualFirst(_) => t!("DIMBREAK Specify first break point:").into_owned(),
|
||||||
|
Step::ManualSecond(_, _) => {
|
||||||
|
t!("DIMBREAK Specify second break point:").into_owned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
match self.step {
|
||||||
|
Step::PickDimension => vec![CmdOption::new("Multiple", "MULTIPLE")],
|
||||||
|
Step::PickCrossing(_) => vec![
|
||||||
|
CmdOption::new("Auto", "AUTO"),
|
||||||
|
CmdOption::new("Manual", "MANUAL"),
|
||||||
|
CmdOption::new("Remove", "REMOVE"),
|
||||||
|
],
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_kind(&self) -> InputKind {
|
||||||
|
InputKind::Point
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
|
matches!(self.step, Step::PickDimension | Step::PickCrossing(_))
|
||||||
|
}
|
||||||
|
|
||||||
fn needs_entity_pick(&self) -> bool {
|
fn needs_entity_pick(&self) -> bool {
|
||||||
true
|
matches!(
|
||||||
|
self.step,
|
||||||
|
Step::PickDimension | Step::PickDimensions(_) | Step::PickCrossing(_)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
fn inject_before_entity_pick(&self) -> bool {
|
||||||
|
matches!(self.step, Step::PickDimension | Step::PickDimensions(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||||
|
self.picked_entity = Some(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
|
||||||
if handle.is_null() {
|
if handle.is_null() {
|
||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
match &self.step {
|
match &mut self.step {
|
||||||
Step::PickDim => {
|
Step::PickDimension => {
|
||||||
self.step = Step::PickCrossing { dim_handle: handle };
|
if !matches!(self.picked_entity.take(), Some(EntityType::Dimension(_))) {
|
||||||
|
return CmdResult::ReportError(
|
||||||
|
t!("DIMBREAK: select a dimension.").into_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.step = Step::PickCrossing(vec![handle]);
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
Step::PickCrossing { dim_handle } => {
|
Step::PickDimensions(handles) => {
|
||||||
let dim_h = *dim_handle;
|
if !matches!(self.picked_entity.take(), Some(EntityType::Dimension(_))) {
|
||||||
// Emit sentinel: the actual break logic is in commands.rs
|
return CmdResult::ReportError(
|
||||||
use acadrust::entities::XLine;
|
t!("DIMBREAK: select a dimension.").into_owned(),
|
||||||
let mut xl = XLine::default();
|
);
|
||||||
xl.common.layer = format!("__DIMBREAK__{},{}", dim_h.value(), handle.value());
|
}
|
||||||
CmdResult::ReplaceEntity(dim_h, vec![acadrust::EntityType::XLine(xl)])
|
if !handles.contains(&handle) {
|
||||||
|
handles.push(handle);
|
||||||
|
}
|
||||||
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
|
Step::PickCrossing(handles) => Self::marker(
|
||||||
|
format!(
|
||||||
|
"__DIMBREAK_OBJECT__{}|{}",
|
||||||
|
Self::handles(handles),
|
||||||
|
handle.value()
|
||||||
|
),
|
||||||
|
handles[0],
|
||||||
|
),
|
||||||
|
Step::ManualFirst(_) | Step::ManualSecond(_, _) => CmdResult::NeedPoint,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
CmdResult::NeedPoint
|
let keyword = text.trim().to_ascii_uppercase();
|
||||||
|
match &mut self.step {
|
||||||
|
Step::PickDimension if matches!(keyword.as_str(), "M" | "MULTIPLE") => {
|
||||||
|
self.step = Step::PickDimensions(Vec::new());
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
Step::PickCrossing(handles) if matches!(keyword.as_str(), "A" | "AUTO") => {
|
||||||
|
Some(Self::automatic(handles))
|
||||||
|
}
|
||||||
|
Step::PickCrossing(handles) if matches!(keyword.as_str(), "M" | "MANUAL") => {
|
||||||
|
self.step = Step::ManualFirst(handles.clone());
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
Step::PickCrossing(handles) if matches!(keyword.as_str(), "R" | "REMOVE") => {
|
||||||
|
Some(Self::remove(handles))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||||
|
match &self.step {
|
||||||
|
Step::ManualFirst(handles) => {
|
||||||
|
self.step = Step::ManualSecond(handles.clone(), point);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::ManualSecond(handles, first) => Self::marker(
|
||||||
|
format!(
|
||||||
|
"__DIMBREAK_MANUAL__{}|{:.15},{:.15},{:.15}|{:.15},{:.15},{:.15}",
|
||||||
|
Self::handles(handles),
|
||||||
|
first.x,
|
||||||
|
first.y,
|
||||||
|
first.z,
|
||||||
|
point.x,
|
||||||
|
point.y,
|
||||||
|
point.z
|
||||||
|
),
|
||||||
|
handles[0],
|
||||||
|
),
|
||||||
|
_ => CmdResult::NeedPoint,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
// Auto mode: apply to all crossings
|
match &self.step {
|
||||||
if let Step::PickCrossing { dim_handle } = &self.step {
|
Step::PickDimensions(handles) if !handles.is_empty() => {
|
||||||
let dim_h = *dim_handle;
|
self.step = Step::PickCrossing(handles.clone());
|
||||||
use acadrust::entities::XLine;
|
CmdResult::NeedPoint
|
||||||
let mut xl = XLine::default();
|
}
|
||||||
xl.common.layer = format!("__DIMBREAK_AUTO__{}", dim_h.value());
|
Step::PickCrossing(handles) => Self::automatic(handles),
|
||||||
return CmdResult::ReplaceEntity(dim_h, vec![acadrust::EntityType::XLine(xl)]);
|
_ => CmdResult::Cancel,
|
||||||
}
|
}
|
||||||
CmdResult::Cancel
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["DIMBREAK"] });
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMBREAK"] }); // DimBreakCommand
|
|
||||||
+105
-43
@@ -1,13 +1,11 @@
|
|||||||
// DIMEDIT command — edit dimension text or reset it to the measured value.
|
// DIMEDIT — edit dimension text, rotation, extension-line obliquing or home position.
|
||||||
//
|
|
||||||
// Workflow:
|
|
||||||
// 1. Pick a dimension entity
|
|
||||||
// 2. Enter new text (blank = reset to auto-measured value, "<>" = measured value placeholder)
|
|
||||||
|
|
||||||
use acadrust::Handle;
|
use acadrust::Handle;
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{
|
||||||
|
CadCommand, CmdOption, CmdResult, DimensionEditOperation, InputKind,
|
||||||
|
};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
@@ -23,8 +21,11 @@ pub fn tool() -> ToolDef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Step {
|
enum Step {
|
||||||
PickDim,
|
Choose,
|
||||||
EnterText { handle: Handle },
|
NewText,
|
||||||
|
Rotate,
|
||||||
|
Oblique,
|
||||||
|
Select(DimensionEditOperation),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DimEditCommand {
|
pub struct DimEditCommand {
|
||||||
@@ -33,9 +34,12 @@ pub struct DimEditCommand {
|
|||||||
|
|
||||||
impl DimEditCommand {
|
impl DimEditCommand {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self { step: Step::Choose }
|
||||||
step: Step::PickDim,
|
}
|
||||||
}
|
|
||||||
|
fn select(&mut self, operation: DimensionEditOperation) -> CmdResult {
|
||||||
|
self.step = Step::Select(operation);
|
||||||
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,48 +49,106 @@ impl CadCommand for DimEditCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match &self.step {
|
match self.step {
|
||||||
Step::PickDim => t!("DIMEDIT Select dimension:").into_owned(),
|
Step::Choose => {
|
||||||
Step::EnterText { .. } => {
|
t!("DIMEDIT Enter type of dimension editing [Home/New/Rotate/Oblique] <Home>:")
|
||||||
t!("DIMEDIT Enter text override (blank = reset to measured):").into_owned()
|
.into_owned()
|
||||||
|
}
|
||||||
|
Step::NewText => t!("DIMEDIT Enter new dimension text <measured value>:").into_owned(),
|
||||||
|
Step::Rotate => t!("DIMEDIT Specify angle for dimension text:").into_owned(),
|
||||||
|
Step::Oblique => {
|
||||||
|
t!("DIMEDIT Enter obliquing angle (press Enter for none):").into_owned()
|
||||||
|
}
|
||||||
|
Step::Select(_) => t!("DIMEDIT Select objects:").into_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
if !matches!(self.step, Step::Choose) {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
vec![
|
||||||
|
CmdOption::new("Home", "HOME"),
|
||||||
|
CmdOption::new("New", "NEW"),
|
||||||
|
CmdOption::new("Rotate", "ROTATE"),
|
||||||
|
CmdOption::new("Oblique", "OBLIQUE"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_kind(&self) -> InputKind {
|
||||||
|
match self.step {
|
||||||
|
Step::NewText => InputKind::FreeText,
|
||||||
|
Step::Select(_) => InputKind::Point,
|
||||||
|
Step::Choose | Step::Rotate | Step::Oblique => InputKind::SingleToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_selection_gathering(&self) -> bool {
|
||||||
|
matches!(self.step, Step::Select(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||||
|
let Step::Select(operation) = &self.step else {
|
||||||
|
return CmdResult::Cancel;
|
||||||
|
};
|
||||||
|
if handles.is_empty() {
|
||||||
|
CmdResult::Cancel
|
||||||
|
} else {
|
||||||
|
CmdResult::EditDimensions {
|
||||||
|
handles,
|
||||||
|
operation: operation.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_entity_pick(&self) -> bool {
|
|
||||||
matches!(self.step, Step::PickDim)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
|
||||||
if handle.is_null() {
|
|
||||||
return CmdResult::NeedPoint;
|
|
||||||
}
|
|
||||||
self.step = Step::EnterText { handle };
|
|
||||||
CmdResult::NeedPoint
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
|
||||||
matches!(self.step, Step::EnterText { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
let handle = match &self.step {
|
let value = text.trim();
|
||||||
Step::EnterText { handle } => *handle,
|
let keyword = value.to_ascii_uppercase();
|
||||||
_ => return None,
|
match self.step {
|
||||||
};
|
Step::Choose => match keyword.as_str() {
|
||||||
// Empty = reset to auto-measured, "<>" = keep measured, else override
|
"H" | "HOME" => Some(self.select(DimensionEditOperation::Home)),
|
||||||
let new_text = text.trim().to_string();
|
"N" | "NEW" => {
|
||||||
Some(CmdResult::DdeditEntity { handle, new_text })
|
self.step = Step::NewText;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
"R" | "ROTATE" => {
|
||||||
|
self.step = Step::Rotate;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
"O" | "OBLIQUE" => {
|
||||||
|
self.step = Step::Oblique;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
Step::NewText => Some(self.select(DimensionEditOperation::NewText(
|
||||||
|
if value == "<>" { String::new() } else { text.to_string() },
|
||||||
|
))),
|
||||||
|
Step::Rotate => value
|
||||||
|
.parse::<f64>()
|
||||||
|
.ok()
|
||||||
|
.map(|degrees| self.select(DimensionEditOperation::Rotate(degrees))),
|
||||||
|
Step::Oblique => value
|
||||||
|
.parse::<f64>()
|
||||||
|
.ok()
|
||||||
|
.filter(|degrees| (-85.0..=85.0).contains(degrees))
|
||||||
|
.map(|degrees| self.select(DimensionEditOperation::Oblique(degrees))),
|
||||||
|
Step::Select(_) => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
match self.step {
|
||||||
|
Step::Choose => self.select(DimensionEditOperation::Home),
|
||||||
|
Step::NewText => self.select(DimensionEditOperation::NewText(String::new())),
|
||||||
|
Step::Oblique => self.select(DimensionEditOperation::Oblique(0.0)),
|
||||||
|
Step::Rotate | Step::Select(_) => CmdResult::Cancel,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["DIMEDIT"] });
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMEDIT"] }); // DimEditCommand
|
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
// DIMJOGLINE command — add a jog (zigzag) symbol to a linear or aligned dimension.
|
// DIMJOGLINE — add or remove a jog on a linear/aligned dimension.
|
||||||
//
|
|
||||||
// Workflow:
|
|
||||||
// 1. Pick the dimension
|
|
||||||
// 2. Click the position on the dimension line where the jog should appear
|
|
||||||
|
|
||||||
use acadrust::Handle;
|
use acadrust::entities::Dimension;
|
||||||
use glam::DVec3;
|
use acadrust::{EntityType, Handle};
|
||||||
|
use glam::{DVec3, Vec3};
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
@@ -30,12 +27,76 @@ enum Step {
|
|||||||
|
|
||||||
pub struct DimJogLineCommand {
|
pub struct DimJogLineCommand {
|
||||||
step: Step,
|
step: Step,
|
||||||
|
remove: bool,
|
||||||
|
picked_entity: Option<EntityType>,
|
||||||
|
dimension: Option<Dimension>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DimJogLineCommand {
|
impl DimJogLineCommand {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
step: Step::PickDim,
|
step: Step::PickDim,
|
||||||
|
remove: false,
|
||||||
|
picked_entity: None,
|
||||||
|
dimension: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn result(handle: Handle, point: Option<DVec3>) -> CmdResult {
|
||||||
|
use acadrust::entities::XLine;
|
||||||
|
let mut marker = XLine::default();
|
||||||
|
marker.common.layer = match point {
|
||||||
|
Some(point) => format!(
|
||||||
|
"__DIMJOG__{}|{:.15},{:.15},{:.15}",
|
||||||
|
handle.value(), point.x, point.y, point.z
|
||||||
|
),
|
||||||
|
None => format!("__DIMJOG_REMOVE__{}", handle.value()),
|
||||||
|
};
|
||||||
|
CmdResult::ReplaceEntity(handle, vec![EntityType::XLine(marker)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_position(dimension: &Dimension) -> Option<DVec3> {
|
||||||
|
let (first, second, definition, axis) = match dimension {
|
||||||
|
Dimension::Linear(value) => (
|
||||||
|
value.first_point,
|
||||||
|
value.second_point,
|
||||||
|
value.definition_point,
|
||||||
|
DVec3::new(value.rotation.cos(), value.rotation.sin(), 0.0),
|
||||||
|
),
|
||||||
|
Dimension::Aligned(value) => {
|
||||||
|
let first = DVec3::new(value.first_point.x, value.first_point.y, value.first_point.z);
|
||||||
|
let second = DVec3::new(value.second_point.x, value.second_point.y, value.second_point.z);
|
||||||
|
(
|
||||||
|
value.first_point,
|
||||||
|
value.second_point,
|
||||||
|
value.definition_point,
|
||||||
|
(second - first).try_normalize().unwrap_or(DVec3::X),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let first = DVec3::new(first.x, first.y, first.z);
|
||||||
|
let second = DVec3::new(second.x, second.y, second.z);
|
||||||
|
let definition = DVec3::new(definition.x, definition.y, definition.z);
|
||||||
|
let axis = axis.try_normalize().unwrap_or(DVec3::X);
|
||||||
|
let perpendicular = DVec3::new(-axis.y, axis.x, 0.0);
|
||||||
|
let offset = definition.dot(perpendicular);
|
||||||
|
let first_on_line = first + perpendicular * (offset - first.dot(perpendicular));
|
||||||
|
let second_on_line = second + perpendicular * (offset - second.dot(perpendicular));
|
||||||
|
let midpoint = (first_on_line + second_on_line) * 0.5;
|
||||||
|
|
||||||
|
let text = dimension.base().text_middle_point;
|
||||||
|
let text = DVec3::new(text.x, text.y, text.z);
|
||||||
|
let line = second_on_line - first_on_line;
|
||||||
|
let length_squared = line.length_squared();
|
||||||
|
if length_squared <= 1.0e-18 {
|
||||||
|
return Some(midpoint);
|
||||||
|
}
|
||||||
|
let parameter = (text - first_on_line).dot(line) / length_squared;
|
||||||
|
if text.is_finite() && (0.0..=1.0).contains(¶meter) {
|
||||||
|
Some((first_on_line + text) * 0.5)
|
||||||
|
} else {
|
||||||
|
Some(midpoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,90 +107,129 @@ impl CadCommand for DimJogLineCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match &self.step {
|
match self.step {
|
||||||
Step::PickDim => t!("DIMJOGLINE Select linear or aligned dimension:").into_owned(),
|
Step::PickDim if self.remove => {
|
||||||
Step::PickJogPos { .. } => t!("DIMJOGLINE Specify jog location:").into_owned(),
|
t!("DIMJOGLINE Select dimension to remove jog:").into_owned()
|
||||||
|
}
|
||||||
|
Step::PickDim => {
|
||||||
|
t!("DIMJOGLINE Select dimension to add jog or [Remove]:").into_owned()
|
||||||
|
}
|
||||||
|
Step::PickJogPos { .. } => {
|
||||||
|
t!("DIMJOGLINE Specify jog location (or press Enter):").into_owned()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
if matches!(self.step, Step::PickDim) && !self.remove {
|
||||||
|
vec![CmdOption::new("Remove", "REMOVE")]
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_kind(&self) -> InputKind {
|
||||||
|
InputKind::Point
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
|
matches!(self.step, Step::PickDim) && !self.remove
|
||||||
|
}
|
||||||
|
|
||||||
fn needs_entity_pick(&self) -> bool {
|
fn needs_entity_pick(&self) -> bool {
|
||||||
matches!(self.step, Step::PickDim)
|
matches!(self.step, Step::PickDim)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
fn inject_before_entity_pick(&self) -> bool {
|
||||||
|
matches!(self.step, Step::PickDim)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||||
|
self.picked_entity = Some(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
|
||||||
|
let Some(EntityType::Dimension(dimension @ (Dimension::Linear(_) | Dimension::Aligned(_)))) =
|
||||||
|
self.picked_entity.take()
|
||||||
|
else {
|
||||||
|
return CmdResult::ReportError(
|
||||||
|
t!("DIMJOGLINE: select a linear or aligned dimension.").into_owned(),
|
||||||
|
);
|
||||||
|
};
|
||||||
if handle.is_null() {
|
if handle.is_null() {
|
||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
|
if self.remove {
|
||||||
|
return Self::result(handle, None);
|
||||||
|
}
|
||||||
|
self.dimension = Some(dimension);
|
||||||
self.step = Step::PickJogPos { handle };
|
self.step = Step::PickJogPos { handle };
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
if let Step::PickJogPos { handle } = &self.step {
|
if matches!(self.step, Step::PickDim)
|
||||||
let h = *handle;
|
&& matches!(text.trim().to_ascii_uppercase().as_str(), "R" | "REMOVE")
|
||||||
// Emit sentinel for commands.rs to store the jog position
|
{
|
||||||
use acadrust::entities::XLine;
|
self.remove = true;
|
||||||
let mut xl = XLine::default();
|
Some(CmdResult::NeedPoint)
|
||||||
xl.common.layer = format!("__DIMJOG__{},{:.6},{:.6}", h.value(), pt.x, pt.z);
|
} else {
|
||||||
return CmdResult::ReplaceEntity(h, vec![acadrust::EntityType::XLine(xl)]);
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||||
|
match self.step {
|
||||||
|
Step::PickJogPos { handle } => Self::result(handle, Some(point)),
|
||||||
|
Step::PickDim => CmdResult::NeedPoint,
|
||||||
}
|
}
|
||||||
CmdResult::NeedPoint
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
match (&self.step, &self.dimension) {
|
||||||
|
(Step::PickJogPos { handle }, Some(dimension)) => Self::default_position(dimension)
|
||||||
|
.map(|point| Self::result(*handle, Some(point)))
|
||||||
|
.unwrap_or(CmdResult::Cancel),
|
||||||
|
_ => CmdResult::Cancel,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
|
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||||
if !matches!(self.step, Step::PickJogPos { .. }) {
|
if !matches!(self.step, Step::PickJogPos { .. }) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let d = 0.3_f32;
|
let axis = self
|
||||||
Some(WireModel {
|
.dimension
|
||||||
bg_adapt: None,
|
.as_ref()
|
||||||
point_marker: None,
|
.and_then(|dimension| match dimension {
|
||||||
taper_widths: Vec::new(),
|
Dimension::Linear(value) => {
|
||||||
pattern_stations: Vec::new(),
|
Some(Vec3::new(value.rotation.cos() as f32, value.rotation.sin() as f32, 0.0))
|
||||||
world_width: 0.0,
|
}
|
||||||
depth_override: None,
|
Dimension::Aligned(value) => Some(Vec3::new(
|
||||||
display_visible: true,
|
(value.second_point.x - value.first_point.x) as f32,
|
||||||
plot_visible: true,
|
(value.second_point.y - value.first_point.y) as f32,
|
||||||
fill_is_3d: false,
|
0.0,
|
||||||
fill_is_2d_solid: false,
|
)),
|
||||||
render_instance: None,
|
_ => None,
|
||||||
pick_tris: Vec::new(),
|
})
|
||||||
pick_tris_low: Vec::new(),
|
.and_then(Vec3::try_normalize)
|
||||||
dash_from_start: false,
|
.unwrap_or(Vec3::X);
|
||||||
dash_align_end: None,
|
let perpendicular = Vec3::new(-axis.y, axis.x, 0.0);
|
||||||
text_verts: Vec::new(),
|
let center = point.as_vec3();
|
||||||
name: "dimjog_preview".into(),
|
let size = 0.3;
|
||||||
// Jog zigzag in the XY drawing plane (Z is elevation, ~0). The old
|
let points = vec![
|
||||||
// marker varied Z, so in the top-down view it collapsed to a flat
|
center - axis * size,
|
||||||
// line instead of a zigzag. (#150)
|
center - axis * size * 0.25 + perpendicular * size,
|
||||||
points: vec![
|
center + axis * size * 0.25 - perpendicular * size,
|
||||||
[pt.x - d, pt.y, pt.z],
|
center + axis * size,
|
||||||
[pt.x - d * 0.3, pt.y + d, pt.z],
|
];
|
||||||
[pt.x + d * 0.3, pt.y - d, pt.z],
|
let mut preview = WireModel::default();
|
||||||
[pt.x + d, pt.y, pt.z],
|
preview.name = "dimjog_preview".into();
|
||||||
],
|
preview.points = points.into_iter().map(|point| point.to_array()).collect();
|
||||||
points_low: Vec::new(),
|
preview.color = WireModel::CYAN;
|
||||||
color: WireModel::CYAN,
|
preview.line_weight_px = 1.2;
|
||||||
selected: false,
|
Some(preview)
|
||||||
pattern_length: 0.0,
|
|
||||||
pattern: [0.0; 8],
|
|
||||||
line_weight_px: 1.2,
|
|
||||||
snap_pts: vec![],
|
|
||||||
tangent_geoms: vec![],
|
|
||||||
aci: 0,
|
|
||||||
key_vertices: vec![],
|
|
||||||
aabb: WireModel::UNBOUNDED_AABB,
|
|
||||||
plinegen: true,
|
|
||||||
fill_tris: vec![],
|
|
||||||
fill_tris_low: Vec::new(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["DIMJOGLINE"] });
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMJOGLINE"] }); // DimJogLineCommand
|
|
||||||
@@ -3,12 +3,12 @@
|
|||||||
// Workflow:
|
// Workflow:
|
||||||
// 1. Select the base dimension
|
// 1. Select the base dimension
|
||||||
// 2. Select the other dimensions to space (click each, Enter to finish)
|
// 2. Select the other dimensions to space (click each, Enter to finish)
|
||||||
// 3. Enter spacing value (or 0 for automatic equal spacing)
|
// 3. Enter a non-negative spacing value, or choose Auto
|
||||||
|
|
||||||
use acadrust::Handle;
|
use acadrust::{EntityType, Handle};
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, SelectionEntity};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
@@ -31,12 +31,14 @@ enum Step {
|
|||||||
|
|
||||||
pub struct DimSpaceCommand {
|
pub struct DimSpaceCommand {
|
||||||
step: Step,
|
step: Step,
|
||||||
|
picked_entity: Option<EntityType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DimSpaceCommand {
|
impl DimSpaceCommand {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
step: Step::PickBase,
|
step: Step::PickBase,
|
||||||
|
picked_entity: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,12 +56,66 @@ impl CadCommand for DimSpaceCommand {
|
|||||||
count = others.len()
|
count = others.len()
|
||||||
)
|
)
|
||||||
.into_owned(),
|
.into_owned(),
|
||||||
Step::EnterSpacing { .. } => t!("DIMSPACE Enter value (0 = auto):").into_owned(),
|
Step::EnterSpacing { .. } => {
|
||||||
|
t!("DIMSPACE Specify distance between dimensions or [Auto] <Auto>:")
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
matches!(self.step, Step::EnterSpacing { .. })
|
||||||
|
.then(|| vec![CmdOption::new("Auto", "AUTO")])
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_kind(&self) -> InputKind {
|
||||||
|
if matches!(self.step, Step::EnterSpacing { .. }) {
|
||||||
|
InputKind::SingleToken
|
||||||
|
} else {
|
||||||
|
InputKind::Point
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_entity_pick(&self) -> bool {
|
fn needs_entity_pick(&self) -> bool {
|
||||||
matches!(self.step, Step::PickBase | Step::PickOthers { .. })
|
matches!(self.step, Step::PickBase)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_before_entity_pick(&self) -> bool {
|
||||||
|
matches!(self.step, Step::PickBase)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||||
|
self.picked_entity = Some(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_selection_gathering(&self) -> bool {
|
||||||
|
matches!(self.step, Step::PickOthers { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selection_entities_exclude_locked(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
|
||||||
|
let Step::PickOthers { base, others } = &mut self.step else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
others.clear();
|
||||||
|
for selected in entities {
|
||||||
|
if selected.handle != *base
|
||||||
|
&& matches!(
|
||||||
|
selected.entity,
|
||||||
|
EntityType::Dimension(
|
||||||
|
acadrust::entities::Dimension::Linear(_)
|
||||||
|
| acadrust::entities::Dimension::Aligned(_)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
&& !others.contains(&selected.handle)
|
||||||
|
{
|
||||||
|
others.push(selected.handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||||
@@ -68,29 +124,60 @@ impl CadCommand for DimSpaceCommand {
|
|||||||
}
|
}
|
||||||
match &mut self.step {
|
match &mut self.step {
|
||||||
Step::PickBase => {
|
Step::PickBase => {
|
||||||
|
if !matches!(
|
||||||
|
self.picked_entity.take(),
|
||||||
|
Some(EntityType::Dimension(
|
||||||
|
acadrust::entities::Dimension::Linear(_)
|
||||||
|
| acadrust::entities::Dimension::Aligned(_)
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return CmdResult::ReportError(
|
||||||
|
t!("DIMSPACE: select a linear or aligned dimension.").into_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
self.step = Step::PickOthers {
|
self.step = Step::PickOthers {
|
||||||
base: handle,
|
base: handle,
|
||||||
others: vec![],
|
others: vec![],
|
||||||
};
|
};
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
Step::PickOthers { others, .. } => {
|
|
||||||
if !others.contains(&handle) {
|
|
||||||
others.push(handle);
|
|
||||||
}
|
|
||||||
CmdResult::NeedPoint
|
|
||||||
}
|
|
||||||
_ => CmdResult::NeedPoint,
|
_ => CmdResult::NeedPoint,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
|
||||||
matches!(self.step, Step::EnterSpacing { .. })
|
let Step::PickOthers { base, others } = &self.step else {
|
||||||
|
return CmdResult::Cancel;
|
||||||
|
};
|
||||||
|
if others.is_empty() {
|
||||||
|
return CmdResult::ReportError(
|
||||||
|
t!("DIMSPACE: select one or more linear or aligned dimensions.").into_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let base = *base;
|
||||||
|
let others = others.clone();
|
||||||
|
self.step = Step::EnterSpacing { base, others };
|
||||||
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
if let Step::EnterSpacing { base, others } = &self.step {
|
if let Step::EnterSpacing { base, others } = &self.step {
|
||||||
let spacing: f64 = text.trim().parse().unwrap_or(0.0);
|
let token = text.trim();
|
||||||
|
let spacing = if matches!(token.to_ascii_uppercase().as_str(), "A" | "AUTO") {
|
||||||
|
"AUTO".to_string()
|
||||||
|
} else {
|
||||||
|
let Ok(value) = token.parse::<f64>() else {
|
||||||
|
return Some(CmdResult::ReportError(
|
||||||
|
t!("DIMSPACE: enter a non-negative spacing value or Auto.").into_owned(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if value < 0.0 {
|
||||||
|
return Some(CmdResult::ReportError(
|
||||||
|
t!("DIMSPACE: spacing value cannot be negative.").into_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
value.to_string()
|
||||||
|
};
|
||||||
let b = *base;
|
let b = *base;
|
||||||
let o = others.clone();
|
let o = others.clone();
|
||||||
// Emit sentinel for commands.rs to handle
|
// Emit sentinel for commands.rs to handle
|
||||||
@@ -116,15 +203,12 @@ impl CadCommand for DimSpaceCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
match &self.step {
|
if matches!(self.step, Step::EnterSpacing { .. }) {
|
||||||
Step::PickOthers { base, others } if !others.is_empty() => {
|
return self
|
||||||
let b = *base;
|
.on_text_input("AUTO")
|
||||||
let o = others.clone();
|
.unwrap_or(CmdResult::Cancel);
|
||||||
self.step = Step::EnterSpacing { base: b, others: o };
|
|
||||||
CmdResult::NeedPoint
|
|
||||||
}
|
|
||||||
_ => CmdResult::Cancel,
|
|
||||||
}
|
}
|
||||||
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
// DIMTEDIT command — reposition the text of an existing dimension.
|
// DIMTEDIT — reposition, justify, home or rotate existing dimension text.
|
||||||
//
|
|
||||||
// Workflow:
|
|
||||||
// 1. Pick a dimension entity
|
|
||||||
// 2. Click the new text position (entity data is injected by update.rs after pick)
|
|
||||||
|
|
||||||
|
use acadrust::entities::{AttachmentPointType, Dimension};
|
||||||
use acadrust::{EntityType, Handle};
|
use acadrust::{EntityType, Handle};
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
@@ -29,18 +26,47 @@ enum Step {
|
|||||||
handle: Handle,
|
handle: Handle,
|
||||||
entity: Option<EntityType>,
|
entity: Option<EntityType>,
|
||||||
},
|
},
|
||||||
|
EnterAngle {
|
||||||
|
handle: Handle,
|
||||||
|
entity: Option<EntityType>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DimTeditCommand {
|
pub struct DimTeditCommand {
|
||||||
step: Step,
|
step: Step,
|
||||||
|
picked_entity: Option<EntityType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum Placement {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Center,
|
||||||
|
Home,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DimTeditCommand {
|
impl DimTeditCommand {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
step: Step::PickDim,
|
step: Step::PickDim,
|
||||||
|
picked_entity: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn finish_placement(
|
||||||
|
handle: Handle,
|
||||||
|
entity: &mut Option<EntityType>,
|
||||||
|
placement: Placement,
|
||||||
|
) -> CmdResult {
|
||||||
|
let Some(mut entity) = entity.take() else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
let EntityType::Dimension(dimension) = &mut entity else {
|
||||||
|
return CmdResult::Cancel;
|
||||||
|
};
|
||||||
|
apply_placement(dimension, placement);
|
||||||
|
CmdResult::UpdateEntityAndFinish { handle, entity }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CadCommand for DimTeditCommand {
|
impl CadCommand for DimTeditCommand {
|
||||||
@@ -49,47 +75,123 @@ impl CadCommand for DimTeditCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match &self.step {
|
match self.step {
|
||||||
Step::PickDim => t!("DIMTEDIT Select dimension:").into_owned(),
|
Step::PickDim => t!("DIMTEDIT Select dimension:").into_owned(),
|
||||||
Step::PickTextPos { .. } => {
|
Step::PickTextPos { .. } => t!(
|
||||||
t!("DIMTEDIT Specify new location for dimension text:").into_owned()
|
"DIMTEDIT Specify new location for dimension text or [Left/Right/Center/Home/Angle]:"
|
||||||
|
)
|
||||||
|
.into_owned(),
|
||||||
|
Step::EnterAngle { .. } => {
|
||||||
|
t!("DIMTEDIT Specify angle for dimension text:").into_owned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
if !matches!(self.step, Step::PickTextPos { .. }) {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
vec![
|
||||||
|
CmdOption::new("Left", "LEFT"),
|
||||||
|
CmdOption::new("Right", "RIGHT"),
|
||||||
|
CmdOption::new("Center", "CENTER"),
|
||||||
|
CmdOption::new("Home", "HOME"),
|
||||||
|
CmdOption::new("Angle", "ANGLE"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_kind(&self) -> InputKind {
|
||||||
|
match self.step {
|
||||||
|
Step::EnterAngle { .. } => InputKind::SingleToken,
|
||||||
|
Step::PickDim | Step::PickTextPos { .. } => InputKind::Point,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
|
matches!(self.step, Step::PickTextPos { .. })
|
||||||
|
}
|
||||||
|
|
||||||
fn needs_entity_pick(&self) -> bool {
|
fn needs_entity_pick(&self) -> bool {
|
||||||
matches!(self.step, Step::PickDim)
|
matches!(self.step, Step::PickDim)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
fn inject_before_entity_pick(&self) -> bool {
|
||||||
|
matches!(self.step, Step::PickDim)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
|
||||||
if handle.is_null() {
|
if handle.is_null() {
|
||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
|
if !matches!(self.picked_entity.as_ref(), Some(EntityType::Dimension(_))) {
|
||||||
|
self.picked_entity = None;
|
||||||
|
return CmdResult::ReportError(
|
||||||
|
t!("DIMTEDIT: select a dimension.").into_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
self.step = Step::PickTextPos {
|
self.step = Step::PickTextPos {
|
||||||
handle,
|
handle,
|
||||||
entity: None,
|
entity: self.picked_entity.take(),
|
||||||
};
|
};
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
if let Step::PickTextPos { handle, entity } = &mut self.step {
|
let keyword = text.trim().to_ascii_uppercase();
|
||||||
let h = *handle;
|
match &mut self.step {
|
||||||
if let Some(mut ent) = entity.take() {
|
Step::PickTextPos { handle, entity } => match keyword.as_str() {
|
||||||
if let EntityType::Dimension(ref mut d) = ent {
|
"L" | "LEFT" => Some(Self::finish_placement(*handle, entity, Placement::Left)),
|
||||||
let new_pt = acadrust::types::Vector3::new(pt.x, pt.y, pt.z);
|
"R" | "RIGHT" => {
|
||||||
d.base_mut().text_middle_point = new_pt;
|
Some(Self::finish_placement(*handle, entity, Placement::Right))
|
||||||
d.base_mut().insertion_point = new_pt;
|
|
||||||
// Pin the text to this location, else the renderer recomputes
|
|
||||||
// the style-default placement and the move is discarded.
|
|
||||||
d.base_mut().text_user_positioned = true;
|
|
||||||
}
|
}
|
||||||
return CmdResult::ReplaceEntity(h, vec![ent]);
|
"C" | "CENTER" => {
|
||||||
|
Some(Self::finish_placement(*handle, entity, Placement::Center))
|
||||||
|
}
|
||||||
|
"H" | "HOME" => Some(Self::finish_placement(*handle, entity, Placement::Home)),
|
||||||
|
"A" | "ANGLE" => {
|
||||||
|
let handle = *handle;
|
||||||
|
let entity = entity.take();
|
||||||
|
self.step = Step::EnterAngle { handle, entity };
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
Step::EnterAngle { handle, entity } => {
|
||||||
|
let degrees = text.trim().parse::<f64>().ok()?;
|
||||||
|
let Some(mut entity) = entity.take() else {
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
};
|
||||||
|
let EntityType::Dimension(dimension) = &mut entity else {
|
||||||
|
return Some(CmdResult::Cancel);
|
||||||
|
};
|
||||||
|
dimension.base_mut().text_rotation = degrees.to_radians();
|
||||||
|
Some(CmdResult::UpdateEntityAndFinish {
|
||||||
|
handle: *handle,
|
||||||
|
entity,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
// No entity yet — wait for inject
|
Step::PickDim => None,
|
||||||
CmdResult::NeedPoint
|
}
|
||||||
} else {
|
}
|
||||||
CmdResult::NeedPoint
|
|
||||||
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||||
|
let Step::PickTextPos { handle, entity } = &mut self.step else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
let Some(mut entity) = entity.take() else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
let EntityType::Dimension(dimension) = &mut entity else {
|
||||||
|
return CmdResult::Cancel;
|
||||||
|
};
|
||||||
|
let point = acadrust::types::Vector3::new(point.x, point.y, point.z);
|
||||||
|
let base = dimension.base_mut();
|
||||||
|
base.text_middle_point = point;
|
||||||
|
base.insertion_point = point;
|
||||||
|
base.text_user_positioned = true;
|
||||||
|
CmdResult::UpdateEntityAndFinish {
|
||||||
|
handle: *handle,
|
||||||
|
entity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,63 +199,102 @@ impl CadCommand for DimTeditCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
|
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||||
if !matches!(self.step, Step::PickTextPos { .. }) {
|
if !matches!(self.step, Step::PickTextPos { .. }) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
let point = point.as_vec3();
|
||||||
let d = 0.2_f32;
|
let d = 0.2_f32;
|
||||||
Some(WireModel {
|
Some(WireModel {
|
||||||
bg_adapt: None,
|
|
||||||
point_marker: None,
|
|
||||||
taper_widths: Vec::new(),
|
|
||||||
pattern_stations: Vec::new(),
|
|
||||||
world_width: 0.0,
|
|
||||||
depth_override: None,
|
|
||||||
display_visible: true,
|
|
||||||
plot_visible: true,
|
|
||||||
fill_is_3d: false,
|
|
||||||
fill_is_2d_solid: false,
|
|
||||||
render_instance: None,
|
|
||||||
pick_tris: Vec::new(),
|
|
||||||
pick_tris_low: Vec::new(),
|
|
||||||
dash_from_start: false,
|
|
||||||
dash_align_end: None,
|
|
||||||
text_verts: Vec::new(),
|
|
||||||
name: "dimtedit_preview".into(),
|
name: "dimtedit_preview".into(),
|
||||||
// Marker box in the XY drawing plane (Z is elevation, ~0). The old
|
|
||||||
// box varied Z, so in the top-down view it collapsed to a flat line
|
|
||||||
// instead of a square. (#150)
|
|
||||||
points: vec![
|
points: vec![
|
||||||
[pt.x - d, pt.y - d, pt.z],
|
[point.x - d, point.y - d, point.z],
|
||||||
[pt.x + d, pt.y - d, pt.z],
|
[point.x + d, point.y - d, point.z],
|
||||||
[pt.x + d, pt.y + d, pt.z],
|
[point.x + d, point.y + d, point.z],
|
||||||
[pt.x - d, pt.y + d, pt.z],
|
[point.x - d, point.y + d, point.z],
|
||||||
[pt.x - d, pt.y - d, pt.z],
|
[point.x - d, point.y - d, point.z],
|
||||||
],
|
],
|
||||||
points_low: Vec::new(),
|
|
||||||
color: WireModel::CYAN,
|
color: WireModel::CYAN,
|
||||||
selected: false,
|
..WireModel::default()
|
||||||
pattern_length: 0.0,
|
|
||||||
pattern: [0.0; 8],
|
|
||||||
line_weight_px: 1.0,
|
|
||||||
snap_pts: vec![],
|
|
||||||
tangent_geoms: vec![],
|
|
||||||
aci: 0,
|
|
||||||
key_vertices: vec![],
|
|
||||||
aabb: WireModel::UNBOUNDED_AABB,
|
|
||||||
plinegen: true,
|
|
||||||
fill_tris: vec![],
|
|
||||||
fill_tris_low: Vec::new(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||||
if let Step::PickTextPos { entity: slot, .. } = &mut self.step {
|
match &mut self.step {
|
||||||
*slot = Some(entity);
|
Step::PickTextPos { entity: slot, .. } | Step::EnterAngle { entity: slot, .. } => {
|
||||||
|
*slot = Some(entity);
|
||||||
|
}
|
||||||
|
Step::PickDim => self.picked_entity = Some(entity),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dimension_line_endpoints(dimension: &Dimension) -> Option<(DVec3, DVec3)> {
|
||||||
|
let (first, second, definition, axis) = match dimension {
|
||||||
|
Dimension::Linear(value) => (
|
||||||
|
value.first_point,
|
||||||
|
value.second_point,
|
||||||
|
value.definition_point,
|
||||||
|
DVec3::new(value.rotation.cos(), value.rotation.sin(), 0.0),
|
||||||
|
),
|
||||||
|
Dimension::Aligned(value) => {
|
||||||
|
let delta = DVec3::new(
|
||||||
|
value.second_point.x - value.first_point.x,
|
||||||
|
value.second_point.y - value.first_point.y,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
if delta.length_squared() <= 1.0e-18 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
(
|
||||||
|
value.first_point,
|
||||||
|
value.second_point,
|
||||||
|
value.definition_point,
|
||||||
|
delta.normalize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let first = DVec3::new(first.x, first.y, first.z);
|
||||||
|
let second = DVec3::new(second.x, second.y, second.z);
|
||||||
|
let definition = DVec3::new(definition.x, definition.y, definition.z);
|
||||||
|
let perpendicular = DVec3::new(-axis.y, axis.x, 0.0);
|
||||||
|
let project = |point: DVec3| {
|
||||||
|
point + perpendicular * (definition - point).dot(perpendicular)
|
||||||
|
};
|
||||||
|
let first = project(first);
|
||||||
|
let second = project(second);
|
||||||
|
if first.dot(axis) <= second.dot(axis) {
|
||||||
|
Some((first, second))
|
||||||
|
} else {
|
||||||
|
Some((second, first))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
fn apply_placement(dimension: &mut Dimension, placement: Placement) {
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMTED", "DIMTEDIT"] }); // DimTeditCommand
|
if matches!(placement, Placement::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.attachment_point = AttachmentPointType::MiddleCenter;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some((left, right)) = dimension_line_endpoints(dimension) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (point, attachment) = match placement {
|
||||||
|
Placement::Left => (left, AttachmentPointType::MiddleLeft),
|
||||||
|
Placement::Right => (right, AttachmentPointType::MiddleRight),
|
||||||
|
Placement::Center => ((left + right) * 0.5, AttachmentPointType::MiddleCenter),
|
||||||
|
Placement::Home => unreachable!(),
|
||||||
|
};
|
||||||
|
let point = acadrust::types::Vector3::new(point.x, point.y, point.z);
|
||||||
|
let base = dimension.base_mut();
|
||||||
|
base.text_middle_point = point;
|
||||||
|
base.insertion_point = point;
|
||||||
|
base.text_user_positioned = true;
|
||||||
|
base.attachment_point = attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["DIMTED", "DIMTEDIT"] });
|
||||||
@@ -230,8 +230,10 @@ impl CadCommand for JoggedRadiusDimensionCommand {
|
|||||||
fn input_kind(&self) -> InputKind {
|
fn input_kind(&self) -> InputKind {
|
||||||
if self.awaiting_text {
|
if self.awaiting_text {
|
||||||
InputKind::FreeText
|
InputKind::FreeText
|
||||||
} else {
|
} else if self.awaiting_angle {
|
||||||
InputKind::SingleToken
|
InputKind::SingleToken
|
||||||
|
} else {
|
||||||
|
InputKind::Point
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
// (DXF group 340); editing/erasing them stays in sync via that link.
|
// (DXF group 340); editing/erasing them stays in sync via that link.
|
||||||
|
|
||||||
use acadrust::entities::mtext::AttachmentPoint;
|
use acadrust::entities::mtext::AttachmentPoint;
|
||||||
use acadrust::entities::{Leader, LeaderCreationType, MText};
|
use acadrust::entities::{Leader, LeaderCreationType, LeaderPathType, MText};
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
use acadrust::EntityType;
|
use acadrust::EntityType;
|
||||||
use glam::{DVec3, Mat4, Vec3};
|
use glam::{DVec3, Mat4, Vec3};
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, WorkingPlane};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
@@ -31,8 +31,19 @@ pub fn tool() -> ToolDef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Step {
|
||||||
|
Points,
|
||||||
|
Format,
|
||||||
|
Annotation,
|
||||||
|
AnnotationOptions,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct LeaderCommand {
|
pub struct LeaderCommand {
|
||||||
verts: Vec<DVec3>,
|
verts: Vec<DVec3>,
|
||||||
|
step: Step,
|
||||||
|
path_type: LeaderPathType,
|
||||||
|
arrow_enabled: bool,
|
||||||
plane: WorkingPlane,
|
plane: WorkingPlane,
|
||||||
dimension_style: String,
|
dimension_style: String,
|
||||||
text_style: String,
|
text_style: String,
|
||||||
@@ -41,6 +52,7 @@ pub struct LeaderCommand {
|
|||||||
gap: f64,
|
gap: f64,
|
||||||
arrow_size: f64,
|
arrow_size: f64,
|
||||||
annotative: bool,
|
annotative: bool,
|
||||||
|
annotation_lines: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LeaderCommand {
|
impl LeaderCommand {
|
||||||
@@ -56,6 +68,9 @@ impl LeaderCommand {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
verts: Vec::new(),
|
verts: Vec::new(),
|
||||||
|
step: Step::Points,
|
||||||
|
path_type: LeaderPathType::StraightLine,
|
||||||
|
arrow_enabled: true,
|
||||||
plane: WorkingPlane::default(),
|
plane: WorkingPlane::default(),
|
||||||
dimension_style: defaults.style_name,
|
dimension_style: defaults.style_name,
|
||||||
text_style: defaults.text_style_name,
|
text_style: defaults.text_style_name,
|
||||||
@@ -64,10 +79,11 @@ impl LeaderCommand {
|
|||||||
gap: defaults.gap,
|
gap: defaults.gap,
|
||||||
arrow_size: defaults.arrow_size,
|
arrow_size: defaults.arrow_size,
|
||||||
annotative: defaults.annotative,
|
annotative: defaults.annotative,
|
||||||
|
annotation_lines: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(&self) -> CmdResult {
|
fn finish(&self, annotation: Option<&str>, open_editor: bool) -> CmdResult {
|
||||||
if self.verts.len() < 2 {
|
if self.verts.len() < 2 {
|
||||||
return CmdResult::Cancel;
|
return CmdResult::Cancel;
|
||||||
}
|
}
|
||||||
@@ -91,7 +107,7 @@ impl LeaderCommand {
|
|||||||
let mut leader_points = local.clone();
|
let mut leader_points = local.clone();
|
||||||
|
|
||||||
let first = local[0];
|
let first = local[0];
|
||||||
let elbow = local[1];
|
let elbow = *local.last().unwrap_or(&first);
|
||||||
let sign = if elbow.x >= first.x { 1.0 } else { -1.0 };
|
let sign = if elbow.x >= first.x { 1.0 } else { -1.0 };
|
||||||
|
|
||||||
let landing_end = DVec3::new(
|
let landing_end = DVec3::new(
|
||||||
@@ -102,7 +118,7 @@ impl LeaderCommand {
|
|||||||
|
|
||||||
leader_points.push(landing_end);
|
leader_points.push(landing_end);
|
||||||
|
|
||||||
let leader = build_leader(
|
let mut leader = build_leader(
|
||||||
&leader_points,
|
&leader_points,
|
||||||
Mat4::IDENTITY,
|
Mat4::IDENTITY,
|
||||||
&self.dimension_style,
|
&self.dimension_style,
|
||||||
@@ -110,6 +126,8 @@ impl LeaderCommand {
|
|||||||
self.gap,
|
self.gap,
|
||||||
self.arrow_size,
|
self.arrow_size,
|
||||||
);
|
);
|
||||||
|
leader.path_type = self.path_type;
|
||||||
|
leader.arrow_enabled = self.arrow_enabled;
|
||||||
|
|
||||||
// The MTEXT starts at the real end of the landing.
|
// The MTEXT starts at the real end of the landing.
|
||||||
let (anchor, attach) =
|
let (anchor, attach) =
|
||||||
@@ -120,8 +138,13 @@ impl LeaderCommand {
|
|||||||
// when another representation becomes active.
|
// when another representation becomes active.
|
||||||
let mtext_height = displayed_height;
|
let mtext_height = displayed_height;
|
||||||
|
|
||||||
|
if annotation.is_none() {
|
||||||
|
leader.creation_type = LeaderCreationType::NoAnnotation;
|
||||||
|
return CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Leader(leader)));
|
||||||
|
}
|
||||||
|
|
||||||
let mtext = build_mtext(
|
let mtext = build_mtext(
|
||||||
"",
|
annotation.unwrap_or_default(),
|
||||||
anchor,
|
anchor,
|
||||||
mtext_height,
|
mtext_height,
|
||||||
attach,
|
attach,
|
||||||
@@ -136,6 +159,7 @@ impl LeaderCommand {
|
|||||||
self.plane.place_entity(EntityType::MText(mtext)),
|
self.plane.place_entity(EntityType::MText(mtext)),
|
||||||
],
|
],
|
||||||
edit_index: 1,
|
edit_index: 1,
|
||||||
|
open_editor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,25 +174,138 @@ impl CadCommand for LeaderCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
if self.verts.is_empty() {
|
if self.step == Step::Format {
|
||||||
t!("LEADER Specify arrowhead point:").into_owned()
|
return t!("LEADER Enter leader formatting option [Spline/Straight/Arrow/None] <exit>:")
|
||||||
|
.into_owned();
|
||||||
|
}
|
||||||
|
if self.step == Step::AnnotationOptions {
|
||||||
|
return t!("LEADER Enter an annotation option [None/Mtext]:").into_owned();
|
||||||
|
}
|
||||||
|
if self.step == Step::Annotation {
|
||||||
|
return if self.annotation_lines.is_empty() {
|
||||||
|
t!("LEADER Enter first line of annotation text or <options>:").into_owned()
|
||||||
|
} else {
|
||||||
|
t!("LEADER Enter next line of annotation text:").into_owned()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
match self.verts.len() {
|
||||||
|
0 => t!("LEADER Specify leader start point:").into_owned(),
|
||||||
|
1 => t!("LEADER Specify next point:").into_owned(),
|
||||||
|
_ => t!("LEADER Specify next point or [Annotation/Format/Undo] <Annotation>:")
|
||||||
|
.into_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
if self.step == Step::Format {
|
||||||
|
return vec![
|
||||||
|
CmdOption::new("Spline", "SPLINE"),
|
||||||
|
CmdOption::new("Straight", "STRAIGHT"),
|
||||||
|
CmdOption::new("Arrow", "ARROW"),
|
||||||
|
CmdOption::new("None", "NONE"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if self.step == Step::AnnotationOptions {
|
||||||
|
return vec![
|
||||||
|
CmdOption::new("None", "NONE"),
|
||||||
|
CmdOption::new("Mtext", "MTEXT"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if self.verts.len() >= 2 {
|
||||||
|
vec![
|
||||||
|
CmdOption::new("Annotation", "ANNOTATION"),
|
||||||
|
CmdOption::new("Format", "FORMAT"),
|
||||||
|
CmdOption::new("Undo", "UNDO"),
|
||||||
|
]
|
||||||
} else {
|
} else {
|
||||||
t!("LEADER Specify landing point:").into_owned()
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_kind(&self) -> InputKind {
|
||||||
|
if matches!(self.step, Step::Format | Step::AnnotationOptions) {
|
||||||
|
InputKind::SingleToken
|
||||||
|
} else if self.step == Step::Annotation {
|
||||||
|
InputKind::FreeText
|
||||||
|
} else {
|
||||||
|
InputKind::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
|
self.step == Step::Points && self.verts.len() >= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
|
let keyword = text.trim().to_ascii_uppercase();
|
||||||
|
if self.step == Step::Annotation {
|
||||||
|
self.annotation_lines.push(text.to_string());
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
if self.step == Step::AnnotationOptions {
|
||||||
|
return match keyword.as_str() {
|
||||||
|
"N" | "NONE" => Some(self.finish(None, false)),
|
||||||
|
"M" | "MTEXT" => Some(self.finish(Some(""), true)),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if self.step == Step::Format {
|
||||||
|
match keyword.as_str() {
|
||||||
|
"S" | "SPLINE" => self.path_type = LeaderPathType::Spline,
|
||||||
|
"ST" | "STRAIGHT" => self.path_type = LeaderPathType::StraightLine,
|
||||||
|
"A" | "ARROW" => self.arrow_enabled = true,
|
||||||
|
"N" | "NONE" => self.arrow_enabled = false,
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
self.step = Step::Points;
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
if self.verts.len() < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match keyword.as_str() {
|
||||||
|
"A" | "ANNOTATION" => {
|
||||||
|
self.step = Step::Annotation;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
"F" | "FORMAT" => {
|
||||||
|
self.step = Step::Format;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
"U" | "UNDO" => {
|
||||||
|
self.verts.pop();
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
self.verts.push(pt);
|
if self.step != Step::Points {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
if self.verts.len() >= 2 {
|
|
||||||
self.finish()
|
|
||||||
} else {
|
|
||||||
CmdResult::NeedPoint
|
|
||||||
}
|
}
|
||||||
|
self.verts.push(pt);
|
||||||
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
self.finish()
|
if self.step == Step::Format {
|
||||||
|
self.step = Step::Points;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
} else if self.step == Step::Annotation {
|
||||||
|
if self.annotation_lines.is_empty() {
|
||||||
|
self.step = Step::AnnotationOptions;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
} else {
|
||||||
|
let text = self.annotation_lines.join("\n");
|
||||||
|
self.finish(Some(&text), false)
|
||||||
|
}
|
||||||
|
} else if self.step == Step::AnnotationOptions {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
} else {
|
||||||
|
self.step = Step::Annotation;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_escape(&mut self) -> CmdResult {
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
@@ -176,7 +313,7 @@ impl CadCommand for LeaderCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||||
if self.verts.is_empty() {
|
if self.step != Step::Points || self.verts.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut pts: Vec<Vec3> = self
|
let mut pts: Vec<Vec3> = self
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use cadkernel::geom2d::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::command::{
|
use crate::command::{
|
||||||
CadCommand, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
|
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
|
||||||
};
|
};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
@@ -224,11 +224,31 @@ impl CadCommand for LinearDimensionCommand {
|
|||||||
fn input_kind(&self) -> InputKind {
|
fn input_kind(&self) -> InputKind {
|
||||||
if self.awaiting_text {
|
if self.awaiting_text {
|
||||||
InputKind::FreeText
|
InputKind::FreeText
|
||||||
} else {
|
} else if self.awaiting_angle || self.awaiting_rotation {
|
||||||
InputKind::SingleToken
|
InputKind::SingleToken
|
||||||
|
} else {
|
||||||
|
InputKind::Point
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
if !matches!(self.step, Step::DimensionLine { .. })
|
||||||
|
|| self.awaiting_text
|
||||||
|
|| self.awaiting_angle
|
||||||
|
|| self.awaiting_rotation
|
||||||
|
{
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
vec![
|
||||||
|
CmdOption::new("MText", "MTEXT"),
|
||||||
|
CmdOption::new("Text", "TEXT"),
|
||||||
|
CmdOption::new("Angle", "ANGLE"),
|
||||||
|
CmdOption::new("Horizontal", "HORIZONTAL"),
|
||||||
|
CmdOption::new("Vertical", "VERTICAL"),
|
||||||
|
CmdOption::new("Rotated", "ROTATED"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
// While typing the override text or angle, route input as a value, not
|
// While typing the override text or angle, route input as a value, not
|
||||||
// a point pick / keyword.
|
// a point pick / keyword.
|
||||||
|
|||||||
@@ -472,6 +472,8 @@ impl CadCommand for MLeaderCommand {
|
|||||||
fn input_kind(&self) -> InputKind {
|
fn input_kind(&self) -> InputKind {
|
||||||
if matches!(self.step, Step::Layer | Step::PreEnterText) {
|
if matches!(self.step, Step::Layer | Step::PreEnterText) {
|
||||||
InputKind::FreeText
|
InputKind::FreeText
|
||||||
|
} else if matches!(self.step, Step::PickPoints | Step::SelectMText) {
|
||||||
|
InputKind::Point
|
||||||
} else {
|
} else {
|
||||||
InputKind::SingleToken
|
InputKind::SingleToken
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-21
@@ -1,6 +1,7 @@
|
|||||||
// Annotate module — dimension, text, leader, table, and markup tools.
|
// Annotate module — dimension, text, leader, table, and markup tools.
|
||||||
|
|
||||||
pub mod aligned_dim;
|
pub mod aligned_dim;
|
||||||
|
pub mod annotation_scale;
|
||||||
pub mod arc_length_dim;
|
pub mod arc_length_dim;
|
||||||
pub mod angular_dim;
|
pub mod angular_dim;
|
||||||
pub mod data_extract;
|
pub mod data_extract;
|
||||||
@@ -205,29 +206,33 @@ impl CadModule for AnnotateModule {
|
|||||||
RibbonGroup {
|
RibbonGroup {
|
||||||
title: "Annotation Scaling",
|
title: "Annotation Scaling",
|
||||||
tools: vec![
|
tools: vec![
|
||||||
RibbonItem::Tool(crate::modules::ToolDef {
|
RibbonItem::LabeledDropdown {
|
||||||
id: "ANNOSCALE",
|
id: "ANNOTATION_CURRENT_SCALE",
|
||||||
label: "Scale List",
|
label: "Add Current Scale",
|
||||||
icon: crate::modules::IconKind::Svg(include_bytes!(
|
|
||||||
"../../../assets/icons/scale_list.svg"
|
|
||||||
)),
|
|
||||||
event: crate::modules::ModuleEvent::Command("ANNOSCALE".to_string()),
|
|
||||||
}),
|
|
||||||
RibbonItem::Tool(crate::modules::ToolDef {
|
|
||||||
id: "OBJECTSCALE",
|
|
||||||
label: "Add Scale",
|
|
||||||
icon: crate::modules::IconKind::Svg(include_bytes!(
|
icon: crate::modules::IconKind::Svg(include_bytes!(
|
||||||
"../../../assets/icons/add_scale.svg"
|
"../../../assets/icons/add_scale.svg"
|
||||||
)),
|
)),
|
||||||
// The quick "mark annotative at the current scale"
|
items: vec![
|
||||||
// action; bare OBJECTSCALE opens the manage dialog.
|
(
|
||||||
event: crate::modules::ModuleEvent::Command(
|
"OBJECTSCALE ADD",
|
||||||
"OBJECTSCALE ADD".to_string(),
|
"Add Current Scale",
|
||||||
),
|
crate::modules::IconKind::Svg(include_bytes!(
|
||||||
}),
|
"../../../assets/icons/add_scale.svg"
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"OBJECTSCALE DELETE",
|
||||||
|
"Delete Current Scale",
|
||||||
|
crate::modules::IconKind::Svg(include_bytes!(
|
||||||
|
"../../../assets/icons/add_scale.svg"
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
default: "OBJECTSCALE ADD",
|
||||||
|
},
|
||||||
RibbonItem::Tool(crate::modules::ToolDef {
|
RibbonItem::Tool(crate::modules::ToolDef {
|
||||||
id: "SCALELISTEDIT",
|
id: "SCALELISTEDIT",
|
||||||
label: "Scale Edit",
|
label: "Scale List",
|
||||||
icon: crate::modules::IconKind::Svg(include_bytes!(
|
icon: crate::modules::IconKind::Svg(include_bytes!(
|
||||||
"../../../assets/icons/scale_list.svg"
|
"../../../assets/icons/scale_list.svg"
|
||||||
)),
|
)),
|
||||||
@@ -236,13 +241,23 @@ impl CadModule for AnnotateModule {
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
RibbonItem::Tool(crate::modules::ToolDef {
|
RibbonItem::Tool(crate::modules::ToolDef {
|
||||||
id: "SYNCPVIEWPORTS",
|
id: "OBJECTSCALE",
|
||||||
label: "Sync Scales",
|
label: "Add/Delete Scales",
|
||||||
|
icon: crate::modules::IconKind::Svg(include_bytes!(
|
||||||
|
"../../../assets/icons/add_scale.svg"
|
||||||
|
)),
|
||||||
|
event: crate::modules::ModuleEvent::Command(
|
||||||
|
"OBJECTSCALE".to_string(),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
RibbonItem::Tool(crate::modules::ToolDef {
|
||||||
|
id: "ANNORESET",
|
||||||
|
label: "Sync Scale Positions",
|
||||||
icon: crate::modules::IconKind::Svg(include_bytes!(
|
icon: crate::modules::IconKind::Svg(include_bytes!(
|
||||||
"../../../assets/icons/sync.svg"
|
"../../../assets/icons/sync.svg"
|
||||||
)),
|
)),
|
||||||
event: crate::modules::ModuleEvent::Command(
|
event: crate::modules::ModuleEvent::Command(
|
||||||
"SYNCPVIEWPORTS".to_string(),
|
"ANNORESET".to_string(),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -246,6 +246,8 @@ impl CadCommand for MTextCommand {
|
|||||||
fn input_kind(&self) -> InputKind {
|
fn input_kind(&self) -> InputKind {
|
||||||
if matches!(self.step, Step::Style) {
|
if matches!(self.step, Step::Style) {
|
||||||
InputKind::FreeText
|
InputKind::FreeText
|
||||||
|
} else if matches!(self.step, Step::FirstCorner | Step::OppositeCorner) {
|
||||||
|
InputKind::Point
|
||||||
} else {
|
} else {
|
||||||
InputKind::SingleToken
|
InputKind::SingleToken
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,8 +142,10 @@ impl CadCommand for OrdinateDimCommand {
|
|||||||
fn input_kind(&self) -> InputKind {
|
fn input_kind(&self) -> InputKind {
|
||||||
if self.awaiting_text {
|
if self.awaiting_text {
|
||||||
InputKind::FreeText
|
InputKind::FreeText
|
||||||
} else {
|
} else if self.awaiting_angle {
|
||||||
InputKind::SingleToken
|
InputKind::SingleToken
|
||||||
|
} else {
|
||||||
|
InputKind::Point
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,8 +137,10 @@ impl CadCommand for RadiusDimensionCommand {
|
|||||||
fn input_kind(&self) -> InputKind {
|
fn input_kind(&self) -> InputKind {
|
||||||
if self.awaiting_text {
|
if self.awaiting_text {
|
||||||
InputKind::FreeText
|
InputKind::FreeText
|
||||||
} else {
|
} else if self.awaiting_angle {
|
||||||
InputKind::SingleToken
|
InputKind::SingleToken
|
||||||
|
} else {
|
||||||
|
InputKind::Point
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
// data rows. The command remembers its previous settings and supports either a
|
// data rows. The command remembers its previous settings and supports either a
|
||||||
// fixed insertion point or a two-corner sizing window.
|
// fixed insertion point or a two-corner sizing window.
|
||||||
|
|
||||||
|
use acadrust::entities::table::{CellStyle, CellStylePropertyFlags};
|
||||||
use acadrust::entities::TableBuilder;
|
use acadrust::entities::TableBuilder;
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
use acadrust::EntityType;
|
use acadrust::EntityType;
|
||||||
@@ -101,9 +102,47 @@ pub struct TableCommand {
|
|||||||
preview_scale: f64,
|
preview_scale: f64,
|
||||||
title_row: bool,
|
title_row: bool,
|
||||||
header_row: bool,
|
header_row: bool,
|
||||||
|
row_style_overrides: Option<[CellStyle; 3]>,
|
||||||
|
preview_enabled: bool,
|
||||||
plane: WorkingPlane,
|
plane: WorkingPlane,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cell_style_from_row(style: &acadrust::objects::RowCellStyle) -> CellStyle {
|
||||||
|
let mut result = CellStyle::new();
|
||||||
|
result.property_flags = CellStylePropertyFlags::DATA_TYPE
|
||||||
|
| CellStylePropertyFlags::DATA_FORMAT
|
||||||
|
| CellStylePropertyFlags::ALIGNMENT
|
||||||
|
| CellStylePropertyFlags::CONTENT_COLOR
|
||||||
|
| CellStylePropertyFlags::TEXT_STYLE
|
||||||
|
| CellStylePropertyFlags::TEXT_HEIGHT
|
||||||
|
| CellStylePropertyFlags::BACKGROUND_COLOR;
|
||||||
|
result.content_color = style.text_color;
|
||||||
|
result.text_style_handle = style.text_style_handle;
|
||||||
|
result.text_style_name = style.text_style_name.clone();
|
||||||
|
result.text_height = style.text_height;
|
||||||
|
result.alignment = style.alignment as i32;
|
||||||
|
result.background_color = style.fill_color;
|
||||||
|
result.fill_enabled = style.fill_enabled;
|
||||||
|
result.value_data_type = style.data_type;
|
||||||
|
result.value_unit_type = style.unit_type;
|
||||||
|
result.value_format = style.format_string.clone();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_row_style(
|
||||||
|
table_style: &acadrust::objects::TableStyle,
|
||||||
|
name: &str,
|
||||||
|
) -> CellStyle {
|
||||||
|
let style = if name.eq_ignore_ascii_case("Title") {
|
||||||
|
&table_style.title_row_style
|
||||||
|
} else if name.eq_ignore_ascii_case("Header") {
|
||||||
|
&table_style.header_row_style
|
||||||
|
} else {
|
||||||
|
&table_style.data_row_style
|
||||||
|
};
|
||||||
|
cell_style_from_row(style)
|
||||||
|
}
|
||||||
|
|
||||||
impl TableCommand {
|
impl TableCommand {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -114,6 +153,8 @@ impl TableCommand {
|
|||||||
preview_scale: 1.0,
|
preview_scale: 1.0,
|
||||||
title_row: true,
|
title_row: true,
|
||||||
header_row: true,
|
header_row: true,
|
||||||
|
row_style_overrides: None,
|
||||||
|
preview_enabled: true,
|
||||||
plane: WorkingPlane::default(),
|
plane: WorkingPlane::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,10 +182,60 @@ impl TableCommand {
|
|||||||
},
|
},
|
||||||
title_row: !style.title_suppressed,
|
title_row: !style.title_suppressed,
|
||||||
header_row: !style.header_suppressed,
|
header_row: !style.header_suppressed,
|
||||||
|
row_style_overrides: None,
|
||||||
|
preview_enabled: true,
|
||||||
plane: WorkingPlane::default(),
|
plane: WorkingPlane::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the insertion command from the values accepted by the Insert
|
||||||
|
/// Table dialog. The dialog owns validation, so the command starts at the
|
||||||
|
/// point/window placement step instead of asking for the same values again.
|
||||||
|
pub fn configured(
|
||||||
|
style: Option<(acadrust::Handle, &acadrust::objects::TableStyle)>,
|
||||||
|
annotation_multiplier: f64,
|
||||||
|
columns: usize,
|
||||||
|
data_rows: usize,
|
||||||
|
column_width: f64,
|
||||||
|
row_height: f64,
|
||||||
|
use_window: bool,
|
||||||
|
preview_enabled: bool,
|
||||||
|
row_style_names: [&str; 3],
|
||||||
|
) -> Self {
|
||||||
|
let mut command = style.map_or_else(Self::new, |(handle, style)| {
|
||||||
|
Self::with_style(handle, style, annotation_multiplier)
|
||||||
|
});
|
||||||
|
let insertion_mode = if use_window {
|
||||||
|
InsertionMode::Window
|
||||||
|
} else {
|
||||||
|
InsertionMode::Point
|
||||||
|
};
|
||||||
|
if let Ok(mut saved) = saved_defaults().lock() {
|
||||||
|
*saved = TableDefaults {
|
||||||
|
columns,
|
||||||
|
data_rows,
|
||||||
|
column_width,
|
||||||
|
row_height,
|
||||||
|
insertion_mode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
command.step = if use_window {
|
||||||
|
Step::WindowFirst { columns, data_rows }
|
||||||
|
} else {
|
||||||
|
Step::Insertion {
|
||||||
|
columns,
|
||||||
|
data_rows,
|
||||||
|
column_width,
|
||||||
|
row_height,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
command.preview_enabled = preview_enabled;
|
||||||
|
command.row_style_overrides = style.map(|(_, table_style)| {
|
||||||
|
row_style_names.map(|name| selected_row_style(table_style, name))
|
||||||
|
});
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
fn defaults(&self) -> TableDefaults {
|
fn defaults(&self) -> TableDefaults {
|
||||||
let mut defaults = saved_defaults().lock().map(|value| *value).unwrap_or_default();
|
let mut defaults = saved_defaults().lock().map(|value| *value).unwrap_or_default();
|
||||||
if (defaults.column_width - DEFAULT_COL_WIDTH).abs() < 1.0e-9 {
|
if (defaults.column_width - DEFAULT_COL_WIDTH).abs() < 1.0e-9 {
|
||||||
@@ -175,6 +266,11 @@ impl TableCommand {
|
|||||||
.column_width(column_width)
|
.column_width(column_width)
|
||||||
.build();
|
.build();
|
||||||
table.table_style_handle = self.style_handle;
|
table.table_style_handle = self.style_handle;
|
||||||
|
if let Some(styles) = &self.row_style_overrides {
|
||||||
|
for (row_index, row) in table.rows.iter_mut().enumerate() {
|
||||||
|
row.style = Some(styles[row_index.min(2)].clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
self.plane.place_entity(EntityType::Table(table))
|
self.plane.place_entity(EntityType::Table(table))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,6 +543,9 @@ impl CadCommand for TableCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||||
|
if !self.preview_enabled {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::Insertion {
|
Step::Insertion {
|
||||||
columns,
|
columns,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use cadkernel::space::{PlanarCurve, Plane, Vec3};
|
|||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
|
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, WorkingPlane};
|
||||||
use crate::entities::curve::{curve_points, entity_curve, ocs_plane};
|
use crate::entities::curve::{curve_points, entity_curve, ocs_plane};
|
||||||
use crate::entities::lwpolyline::{is_revision_cloud, revision_cloud_from_curve};
|
use crate::entities::lwpolyline::{is_revision_cloud, revision_cloud_from_curve};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
@@ -666,6 +666,17 @@ impl CadCommand for RevCloudCommand {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn input_kind(&self) -> InputKind {
|
||||||
|
match self.stage {
|
||||||
|
Stage::ArcLength | Stage::Style | Stage::Reverse(_) => InputKind::SingleToken,
|
||||||
|
Stage::Create
|
||||||
|
| Stage::Object
|
||||||
|
| Stage::ModifySelect
|
||||||
|
| Stage::ModifyDraw(_)
|
||||||
|
| Stage::ModifyErase(_) => InputKind::Point,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
matches!(self.stage, Stage::Create | Stage::ModifyDraw(_))
|
matches!(self.stage, Stage::Create | Stage::ModifyDraw(_))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use cadkernel::space::Plane;
|
|||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
|
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, WorkingPlane};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
|
|
||||||
@@ -270,6 +270,15 @@ impl CadCommand for WipeoutCommand {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn input_kind(&self) -> InputKind {
|
||||||
|
match self.mode {
|
||||||
|
WipeoutMode::Frames | WipeoutMode::ErasePolyline => InputKind::SingleToken,
|
||||||
|
WipeoutMode::Draw | WipeoutMode::Polyline | WipeoutMode::Rectangular => {
|
||||||
|
InputKind::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.mode,
|
self.mode,
|
||||||
|
|||||||
@@ -644,6 +644,44 @@ pub fn active_object_context_for_scale(
|
|||||||
default.or(first)
|
default.or(first)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reset every alternate annotation-scale representation to the placement of
|
||||||
|
/// the representation displayed at `scale_handle`. The leaf identity, scale
|
||||||
|
/// handle and default marker stay intact; only the context payload is copied.
|
||||||
|
pub fn reset_annotation_context_positions(
|
||||||
|
doc: &mut CadDocument,
|
||||||
|
entity: Handle,
|
||||||
|
scale_handle: Option<Handle>,
|
||||||
|
) -> bool {
|
||||||
|
let Some(source) = active_object_context_for_scale(doc, entity, scale_handle) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let source_handle = source.handle;
|
||||||
|
let source_kind = source.kind.clone();
|
||||||
|
let targets: Vec<Handle> = annotation_scales_dict(doc, entity)
|
||||||
|
.and_then(|collection| as_dict(doc, collection))
|
||||||
|
.map(|collection| {
|
||||||
|
collection
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|(_, handle)| *handle)
|
||||||
|
.filter(|handle| *handle != source_handle)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut changed = false;
|
||||||
|
for handle in targets {
|
||||||
|
let Some(ObjectType::ObjectContextData(context)) = doc.objects.get_mut(&handle) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if context.kind != source_kind {
|
||||||
|
context.kind = source_kind.clone();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
pub fn effective_annotation_scale_for(
|
pub fn effective_annotation_scale_for(
|
||||||
doc: &CadDocument,
|
doc: &CadDocument,
|
||||||
entity: &EntityType,
|
entity: &EntityType,
|
||||||
|
|||||||
@@ -0,0 +1,626 @@
|
|||||||
|
//! Dialogs used by the Annotate tab's table and drawing-data tools.
|
||||||
|
//!
|
||||||
|
//! The working state lives outside the drawing until the user presses OK or
|
||||||
|
//! Finish. That keeps Cancel lossless and lets the wizard validate one page
|
||||||
|
//! before advancing to the next.
|
||||||
|
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use acadrust::types::Handle;
|
||||||
|
use iced::widget::{button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space};
|
||||||
|
use iced::{Border, Element, Fill, Length, Theme};
|
||||||
|
|
||||||
|
use crate::app::Message;
|
||||||
|
use crate::t;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum TableSource {
|
||||||
|
#[default]
|
||||||
|
Empty,
|
||||||
|
DataLink,
|
||||||
|
DataExtraction,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TableSource {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let label = match self {
|
||||||
|
Self::Empty => t!("Empty table").into_owned(),
|
||||||
|
Self::DataLink => t!("Data link").into_owned(),
|
||||||
|
Self::DataExtraction => t!("Data extraction").into_owned(),
|
||||||
|
};
|
||||||
|
f.write_str(&label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum TableInsertion {
|
||||||
|
#[default]
|
||||||
|
Point,
|
||||||
|
Window,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TableInsertion {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let label = match self {
|
||||||
|
Self::Point => t!("Specify insertion point").into_owned(),
|
||||||
|
Self::Window => t!("Specify window").into_owned(),
|
||||||
|
};
|
||||||
|
f.write_str(&label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DataLinkChoice {
|
||||||
|
pub handle: Handle,
|
||||||
|
pub name: String,
|
||||||
|
pub path: String,
|
||||||
|
pub valid: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DataLinkChoice {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
if self.valid {
|
||||||
|
write!(f, "🔗 {}", self.name)
|
||||||
|
} else {
|
||||||
|
write!(f, "⚠ {}", self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TableInsertState {
|
||||||
|
pub styles: Vec<String>,
|
||||||
|
pub style: String,
|
||||||
|
pub source: TableSource,
|
||||||
|
pub links: Vec<DataLinkChoice>,
|
||||||
|
pub link: Option<DataLinkChoice>,
|
||||||
|
pub insertion: TableInsertion,
|
||||||
|
pub columns: String,
|
||||||
|
pub data_rows: String,
|
||||||
|
pub column_width: String,
|
||||||
|
pub row_height: String,
|
||||||
|
pub preview: bool,
|
||||||
|
pub first_row_style: String,
|
||||||
|
pub second_row_style: String,
|
||||||
|
pub other_row_style: String,
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TableInsertState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
styles: vec!["Standard".into()],
|
||||||
|
style: "Standard".into(),
|
||||||
|
source: TableSource::Empty,
|
||||||
|
links: Vec::new(),
|
||||||
|
link: None,
|
||||||
|
insertion: TableInsertion::Point,
|
||||||
|
columns: "3".into(),
|
||||||
|
data_rows: "4".into(),
|
||||||
|
column_width: "2.0".into(),
|
||||||
|
row_height: "0.5".into(),
|
||||||
|
preview: true,
|
||||||
|
first_row_style: "Title".into(),
|
||||||
|
second_row_style: "Header".into(),
|
||||||
|
other_row_style: "Data".into(),
|
||||||
|
error: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TableInsertField {
|
||||||
|
Style(String),
|
||||||
|
Source(TableSource),
|
||||||
|
Link(DataLinkChoice),
|
||||||
|
Insertion(TableInsertion),
|
||||||
|
Columns(String),
|
||||||
|
DataRows(String),
|
||||||
|
ColumnWidth(String),
|
||||||
|
RowHeight(String),
|
||||||
|
Preview(bool),
|
||||||
|
FirstRowStyle(String),
|
||||||
|
SecondRowStyle(String),
|
||||||
|
OtherRowStyle(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum LinkPathType {
|
||||||
|
#[default]
|
||||||
|
Full,
|
||||||
|
Relative,
|
||||||
|
FileName,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for LinkPathType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let label = match self {
|
||||||
|
Self::Full => t!("Full path").into_owned(),
|
||||||
|
Self::Relative => t!("Relative path").into_owned(),
|
||||||
|
Self::FileName => t!("No path").into_owned(),
|
||||||
|
};
|
||||||
|
f.write_str(&label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum LinkRange {
|
||||||
|
#[default]
|
||||||
|
EntireSheet,
|
||||||
|
NamedRange,
|
||||||
|
CellRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for LinkRange {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let label = match self {
|
||||||
|
Self::EntireSheet => t!("Entire sheet").into_owned(),
|
||||||
|
Self::NamedRange => t!("Named range").into_owned(),
|
||||||
|
Self::CellRange => t!("Cell range").into_owned(),
|
||||||
|
};
|
||||||
|
f.write_str(&label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DataLinkManagerState {
|
||||||
|
pub links: Vec<DataLinkChoice>,
|
||||||
|
pub selected: Option<DataLinkChoice>,
|
||||||
|
pub editing: bool,
|
||||||
|
pub editing_handle: Option<Handle>,
|
||||||
|
pub name: String,
|
||||||
|
pub path: String,
|
||||||
|
pub path_type: LinkPathType,
|
||||||
|
pub range_kind: LinkRange,
|
||||||
|
pub range: String,
|
||||||
|
pub sheets: Vec<String>,
|
||||||
|
pub sheet: String,
|
||||||
|
pub named_ranges: Vec<String>,
|
||||||
|
pub allow_write: bool,
|
||||||
|
pub use_source_formatting: bool,
|
||||||
|
pub update_source_formatting: bool,
|
||||||
|
pub insert_table: bool,
|
||||||
|
pub more_options: bool,
|
||||||
|
pub preview: Vec<Vec<String>>,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DataLinkManagerState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
links: Vec::new(),
|
||||||
|
selected: None,
|
||||||
|
editing: false,
|
||||||
|
editing_handle: None,
|
||||||
|
name: String::new(),
|
||||||
|
path: String::new(),
|
||||||
|
path_type: LinkPathType::Full,
|
||||||
|
range_kind: LinkRange::EntireSheet,
|
||||||
|
range: String::new(),
|
||||||
|
sheets: Vec::new(),
|
||||||
|
sheet: String::new(),
|
||||||
|
named_ranges: Vec::new(),
|
||||||
|
allow_write: false,
|
||||||
|
use_source_formatting: true,
|
||||||
|
update_source_formatting: true,
|
||||||
|
insert_table: false,
|
||||||
|
more_options: false,
|
||||||
|
preview: Vec::new(),
|
||||||
|
status: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum DataLinkField {
|
||||||
|
Name(String),
|
||||||
|
Path(String),
|
||||||
|
PathType(LinkPathType),
|
||||||
|
RangeKind(LinkRange),
|
||||||
|
Range(String),
|
||||||
|
Sheet(String),
|
||||||
|
AllowWrite(bool),
|
||||||
|
UseSourceFormatting(bool),
|
||||||
|
UpdateSourceFormatting(bool),
|
||||||
|
InsertTable(bool),
|
||||||
|
MoreOptions(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum ExtractionPage {
|
||||||
|
#[default]
|
||||||
|
Begin,
|
||||||
|
Source,
|
||||||
|
Objects,
|
||||||
|
Properties,
|
||||||
|
Refine,
|
||||||
|
Output,
|
||||||
|
TableStyle,
|
||||||
|
Finish,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtractionPage {
|
||||||
|
pub const fn number(self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Begin => 1,
|
||||||
|
Self::Source => 2,
|
||||||
|
Self::Objects => 3,
|
||||||
|
Self::Properties => 4,
|
||||||
|
Self::Refine => 5,
|
||||||
|
Self::Output => 6,
|
||||||
|
Self::TableStyle => 7,
|
||||||
|
Self::Finish => 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum ExtractionBegin {
|
||||||
|
#[default]
|
||||||
|
New,
|
||||||
|
Template,
|
||||||
|
Edit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtractionBegin {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let label = match self {
|
||||||
|
Self::New => t!("Create a new data extraction").into_owned(),
|
||||||
|
Self::Template => t!("Use a previous extraction as a template").into_owned(),
|
||||||
|
Self::Edit => t!("Edit an existing data extraction").into_owned(),
|
||||||
|
};
|
||||||
|
f.write_str(&label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum ExtractionSource {
|
||||||
|
#[default]
|
||||||
|
CurrentDrawing,
|
||||||
|
CurrentSelection,
|
||||||
|
DrawingsAndFolders,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtractionSource {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let label = match self {
|
||||||
|
Self::CurrentDrawing => t!("Current drawing").into_owned(),
|
||||||
|
Self::CurrentSelection => t!("Selected objects in current drawing").into_owned(),
|
||||||
|
Self::DrawingsAndFolders => t!("Drawings / folders").into_owned(),
|
||||||
|
};
|
||||||
|
f.write_str(&label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ExtractionObject {
|
||||||
|
pub name: String,
|
||||||
|
pub checked: bool,
|
||||||
|
pub count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ExtractionProperty {
|
||||||
|
pub key: String,
|
||||||
|
pub name: String,
|
||||||
|
pub category: String,
|
||||||
|
pub checked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DataExtractionState {
|
||||||
|
pub page: ExtractionPage,
|
||||||
|
pub begin: ExtractionBegin,
|
||||||
|
pub settings_path: String,
|
||||||
|
pub source: ExtractionSource,
|
||||||
|
pub include_current: bool,
|
||||||
|
pub include_subfolders: bool,
|
||||||
|
pub source_files: Vec<String>,
|
||||||
|
pub selection_handles: Vec<Handle>,
|
||||||
|
pub objects: Vec<ExtractionObject>,
|
||||||
|
pub properties: Vec<ExtractionProperty>,
|
||||||
|
pub categories: BTreeSet<String>,
|
||||||
|
pub combine_identical: bool,
|
||||||
|
pub show_count: bool,
|
||||||
|
pub show_name: bool,
|
||||||
|
pub preview: Vec<Vec<String>>,
|
||||||
|
pub output_table: bool,
|
||||||
|
pub output_file: bool,
|
||||||
|
pub output_path: String,
|
||||||
|
pub table_styles: Vec<String>,
|
||||||
|
pub table_style: String,
|
||||||
|
pub table_title: String,
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DataExtractionState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
page: ExtractionPage::Begin,
|
||||||
|
begin: ExtractionBegin::New,
|
||||||
|
settings_path: String::new(),
|
||||||
|
source: ExtractionSource::CurrentDrawing,
|
||||||
|
include_current: true,
|
||||||
|
include_subfolders: true,
|
||||||
|
source_files: Vec::new(),
|
||||||
|
selection_handles: Vec::new(),
|
||||||
|
objects: Vec::new(),
|
||||||
|
properties: vec![
|
||||||
|
ExtractionProperty { key: "type".into(), name: "Type".into(), category: "General".into(), checked: true },
|
||||||
|
ExtractionProperty { key: "handle".into(), name: "Handle".into(), category: "General".into(), checked: true },
|
||||||
|
ExtractionProperty { key: "layer".into(), name: "Layer".into(), category: "General".into(), checked: true },
|
||||||
|
ExtractionProperty { key: "color".into(), name: "Color".into(), category: "General".into(), checked: true },
|
||||||
|
ExtractionProperty { key: "linetype".into(), name: "Linetype".into(), category: "General".into(), checked: true },
|
||||||
|
ExtractionProperty { key: "details".into(), name: "Details".into(), category: "Geometry".into(), checked: true },
|
||||||
|
],
|
||||||
|
categories: ["General".to_string(), "Geometry".to_string()].into_iter().collect(),
|
||||||
|
combine_identical: false,
|
||||||
|
show_count: true,
|
||||||
|
show_name: true,
|
||||||
|
preview: Vec::new(),
|
||||||
|
output_table: true,
|
||||||
|
output_file: false,
|
||||||
|
output_path: String::new(),
|
||||||
|
table_styles: vec!["Standard".into()],
|
||||||
|
table_style: "Standard".into(),
|
||||||
|
table_title: "Data Extraction".into(),
|
||||||
|
error: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum DataExtractionField {
|
||||||
|
Begin(ExtractionBegin),
|
||||||
|
SettingsPath(String),
|
||||||
|
Source(ExtractionSource),
|
||||||
|
IncludeCurrent(bool),
|
||||||
|
IncludeSubfolders(bool),
|
||||||
|
Object(usize, bool),
|
||||||
|
Property(usize, bool),
|
||||||
|
Category(String, bool),
|
||||||
|
CombineIdentical(bool),
|
||||||
|
ShowCount(bool),
|
||||||
|
ShowName(bool),
|
||||||
|
OutputTable(bool),
|
||||||
|
OutputFile(bool),
|
||||||
|
OutputPath(String),
|
||||||
|
TableStyle(String),
|
||||||
|
TableTitle(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn muted(theme: &Theme) -> iced::widget::text::Style {
|
||||||
|
iced::widget::text::Style {
|
||||||
|
color: Some(theme.palette().background.base.text.scale_alpha(0.62)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn group<'a>(title: impl Into<String>, body: Element<'a, Message>) -> Element<'a, Message> {
|
||||||
|
container(column![text(title.into()).size(11).style(muted), body].spacing(7))
|
||||||
|
.padding(9)
|
||||||
|
.width(Fill)
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
border: Border { width: 1.0, radius: 4.0.into(), color: theme.palette().background.strong.color },
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn labeled_input<'a>(label: impl Into<String>, value: &'a str, msg: fn(String) -> Message) -> Element<'a, Message> {
|
||||||
|
row![text(label.into()).size(11).style(muted).width(118), text_input("", value).on_input(msg).padding([4, 7]).size(12).width(Fill)]
|
||||||
|
.spacing(8).align_y(iced::Center).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_style_picker<'a>(
|
||||||
|
label: impl Into<String>,
|
||||||
|
selected: &'a str,
|
||||||
|
msg: fn(String) -> Message,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
row![
|
||||||
|
text(label.into()).size(11).style(muted).width(118),
|
||||||
|
pick_list(
|
||||||
|
Some(selected.to_string()),
|
||||||
|
vec!["Title".to_string(), "Header".to_string(), "Data".to_string()],
|
||||||
|
|value| value.clone(),
|
||||||
|
)
|
||||||
|
.on_select(msg)
|
||||||
|
.padding([3, 6])
|
||||||
|
.text_size(12)
|
||||||
|
.width(Fill),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn radio_button<'a>(selected: bool, label: impl Into<String>, msg: Message) -> Element<'a, Message> {
|
||||||
|
let mark = if selected { "◉" } else { "○" };
|
||||||
|
button(row![text(mark).size(13), text(label.into()).size(12)].spacing(6))
|
||||||
|
.on_press(msg).padding([4, 7]).style(button::text).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview_grid<'a>(rows: &'a [Vec<String>]) -> Element<'a, Message> {
|
||||||
|
if rows.is_empty() {
|
||||||
|
return text(t!("No preview available.")).size(11).style(muted).into();
|
||||||
|
}
|
||||||
|
let lines = rows.iter().take(8).map(|row| row.join(" | ")).collect::<Vec<_>>().join("\n");
|
||||||
|
container(text(lines).size(11)).padding(6).width(Fill).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn table_insert_view(state: &TableInsertState, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
|
||||||
|
let style = pick_list(Some(state.style.clone()), state.styles.clone(), |value| value.clone()).on_select(Message::TableInsertStyle).padding([3, 6]).text_size(12).width(Fill);
|
||||||
|
let source = column![
|
||||||
|
radio_button(state.source == TableSource::Empty, t!("Start from empty table"), Message::TableInsertField(TableInsertField::Source(TableSource::Empty))),
|
||||||
|
radio_button(state.source == TableSource::DataLink, t!("Start from data link"), Message::TableInsertField(TableInsertField::Source(TableSource::DataLink))),
|
||||||
|
radio_button(state.source == TableSource::DataExtraction, t!("Start from data extraction"), Message::TableInsertField(TableInsertField::Source(TableSource::DataExtraction))),
|
||||||
|
].spacing(1);
|
||||||
|
let link_picker: Element<'_, Message> = if state.source == TableSource::DataLink {
|
||||||
|
row![
|
||||||
|
text(t!("Data link")).size(11).style(muted).width(118),
|
||||||
|
pick_list(state.link.clone(), state.links.clone(), |value| value.to_string()).on_select(|value| Message::TableInsertField(TableInsertField::Link(value))).padding([3, 6]).text_size(12).width(Fill),
|
||||||
|
button(text(t!("Manage…")).size(11)).on_press(Message::DataLinkManagerOpen).padding([4, 8]),
|
||||||
|
].spacing(8).align_y(iced::Center).into()
|
||||||
|
} else { Space::new().height(0).into() };
|
||||||
|
let insertion = column![
|
||||||
|
radio_button(state.insertion == TableInsertion::Point, t!("Specify insertion point"), Message::TableInsertField(TableInsertField::Insertion(TableInsertion::Point))),
|
||||||
|
radio_button(state.insertion == TableInsertion::Window, t!("Specify window"), Message::TableInsertField(TableInsertField::Insertion(TableInsertion::Window))),
|
||||||
|
];
|
||||||
|
let settings = column![
|
||||||
|
labeled_input(t!("Columns"), &state.columns, |v| Message::TableInsertField(TableInsertField::Columns(v))),
|
||||||
|
labeled_input(t!("Column width"), &state.column_width, |v| Message::TableInsertField(TableInsertField::ColumnWidth(v))),
|
||||||
|
labeled_input(t!("Data rows"), &state.data_rows, |v| Message::TableInsertField(TableInsertField::DataRows(v))),
|
||||||
|
labeled_input(t!("Row height"), &state.row_height, |v| Message::TableInsertField(TableInsertField::RowHeight(v))),
|
||||||
|
].spacing(5);
|
||||||
|
let cell_styles = column![
|
||||||
|
cell_style_picker(t!("First row"), &state.first_row_style, |v| Message::TableInsertField(TableInsertField::FirstRowStyle(v))),
|
||||||
|
cell_style_picker(t!("Second row"), &state.second_row_style, |v| Message::TableInsertField(TableInsertField::SecondRowStyle(v))),
|
||||||
|
cell_style_picker(t!("All other rows"), &state.other_row_style, |v| Message::TableInsertField(TableInsertField::OtherRowStyle(v))),
|
||||||
|
].spacing(5);
|
||||||
|
column![
|
||||||
|
group(t!("Table style"), row![style].into()),
|
||||||
|
group(t!("Insert options"), column![source, link_picker].spacing(4).into()),
|
||||||
|
row![group(t!("Insertion behavior"), insertion.into()), group(t!("Column & row settings"), settings.into())].spacing(8),
|
||||||
|
group(t!("Set cell styles"), cell_styles.into()),
|
||||||
|
checkbox(state.preview).label(t!("Preview")).on_toggle(|v| Message::TableInsertField(TableInsertField::Preview(v))).size(14),
|
||||||
|
text(&state.error).size(11),
|
||||||
|
row![Space::new().width(Fill), button(text(t!("Cancel")).size(12)).on_press(Message::CloseModal).padding([5, 12]), button(text(t!("OK")).size(12)).on_press(Message::TableInsertApply).style(button::primary).padding([5, 12])].spacing(7),
|
||||||
|
].spacing(8).padding(10).width(sizing.width).height(sizing.height).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data_link_view(state: &DataLinkManagerState, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
|
||||||
|
let mut links = column![text(t!("Spreadsheet Links")).size(11).style(muted)].spacing(2);
|
||||||
|
links = links.push(button(text(t!("Create a New Data Link")).size(12)).on_press(Message::DataLinkNew).padding([5, 7]).style(button::text).width(Fill));
|
||||||
|
for link in &state.links {
|
||||||
|
let selected = state.selected.as_ref().is_some_and(|v| v.handle == link.handle);
|
||||||
|
let label = if selected { format!("● {link}") } else { format!(" {link}") };
|
||||||
|
links = links.push(button(text(label).size(12)).on_press(Message::DataLinkSelect(link.handle)).padding([5, 7]).style(button::text).width(Fill));
|
||||||
|
}
|
||||||
|
let tree = container(scrollable(links).height(Fill)).padding(6).width(Length::Fixed(220.0)).height(Fill);
|
||||||
|
let right: Element<'_, Message> = if state.editing {
|
||||||
|
let mut editor = column![
|
||||||
|
labeled_input(t!("Data link name"), &state.name, |v| Message::DataLinkField(DataLinkField::Name(v))),
|
||||||
|
row![text(t!("File")).size(11).style(muted).width(118), text_input("", &state.path).on_input(|v| Message::DataLinkField(DataLinkField::Path(v))).padding([4, 7]).size(12).width(Fill), button(text("…")).on_press(Message::DataLinkBrowse).padding([4, 9])].spacing(6).align_y(iced::Center),
|
||||||
|
row![text(t!("Path type")).size(11).style(muted).width(118), pick_list(Some(state.path_type), vec![LinkPathType::Full, LinkPathType::Relative, LinkPathType::FileName], |value| value.to_string()).on_select(|value| Message::DataLinkField(DataLinkField::PathType(value))).width(Fill).padding([3, 6])].spacing(8),
|
||||||
|
row![text(t!("Link option")).size(11).style(muted).width(118), pick_list(Some(state.range_kind), vec![LinkRange::EntireSheet, LinkRange::NamedRange, LinkRange::CellRange], |value| value.to_string()).on_select(|value| Message::DataLinkField(DataLinkField::RangeKind(value))).width(Fill).padding([3, 6])].spacing(8),
|
||||||
|
].spacing(6);
|
||||||
|
if !state.sheets.is_empty() && state.range_kind != LinkRange::NamedRange {
|
||||||
|
editor = editor.push(row![
|
||||||
|
text(t!("Sheet")).size(11).style(muted).width(118),
|
||||||
|
pick_list(Some(state.sheet.clone()), state.sheets.clone(), |value| value.clone())
|
||||||
|
.on_select(|value| Message::DataLinkField(DataLinkField::Sheet(value)))
|
||||||
|
.width(Fill)
|
||||||
|
.padding([3, 6]),
|
||||||
|
].spacing(8));
|
||||||
|
}
|
||||||
|
if state.range_kind == LinkRange::NamedRange && !state.named_ranges.is_empty() {
|
||||||
|
editor = editor.push(row![
|
||||||
|
text(t!("Named range")).size(11).style(muted).width(118),
|
||||||
|
pick_list(Some(state.range.clone()), state.named_ranges.clone(), |value| value.clone())
|
||||||
|
.on_select(|value| Message::DataLinkField(DataLinkField::Range(value)))
|
||||||
|
.width(Fill)
|
||||||
|
.padding([3, 6]),
|
||||||
|
].spacing(8));
|
||||||
|
} else if state.range_kind != LinkRange::EntireSheet {
|
||||||
|
editor = editor.push(labeled_input(t!("Range"), &state.range, |v| Message::DataLinkField(DataLinkField::Range(v))));
|
||||||
|
}
|
||||||
|
editor = editor.push(checkbox(state.more_options).label(t!("More options")).on_toggle(|v| Message::DataLinkField(DataLinkField::MoreOptions(v))).size(14));
|
||||||
|
if state.more_options {
|
||||||
|
editor = editor
|
||||||
|
.push(checkbox(state.allow_write).label(t!("Allow writing to source file")).on_toggle(|v| Message::DataLinkField(DataLinkField::AllowWrite(v))).size(14))
|
||||||
|
.push(checkbox(state.use_source_formatting).label(t!("Use source formatting")).on_toggle(|v| Message::DataLinkField(DataLinkField::UseSourceFormatting(v))).size(14))
|
||||||
|
.push(checkbox(state.update_source_formatting).label(t!("Keep table updated to source formatting")).on_toggle(|v| Message::DataLinkField(DataLinkField::UpdateSourceFormatting(v))).size(14));
|
||||||
|
}
|
||||||
|
editor = editor.push(checkbox(state.insert_table).label(t!("Insert table after saving link")).on_toggle(|v| Message::DataLinkField(DataLinkField::InsertTable(v))).size(14));
|
||||||
|
group(t!("Link settings"), editor.into())
|
||||||
|
} else if let Some(link) = &state.selected {
|
||||||
|
column![
|
||||||
|
group(t!("Details"), column![text(format!("{}: {}", t!("Name"), link.name)).size(11), text(format!("{}: {}", t!("File"), link.path)).size(11), text(if link.valid { t!("Status: Linked") } else { t!("Status: Source not found") }).size(11)].spacing(5).into()),
|
||||||
|
group(t!("Preview"), preview_grid(&state.preview)),
|
||||||
|
].spacing(8).into()
|
||||||
|
} else {
|
||||||
|
group(t!("Details"), text(t!("Select a data link or create a new one.")).size(11).style(muted).into())
|
||||||
|
};
|
||||||
|
let actions = if state.editing {
|
||||||
|
row![Space::new().width(Fill), button(text(t!("Cancel")).size(12)).on_press(Message::DataLinkEditCancel).padding([5, 11]), button(text(t!("OK")).size(12)).on_press(Message::DataLinkSave).style(button::primary).padding([5, 11])].spacing(7)
|
||||||
|
} else {
|
||||||
|
let enabled = state.selected.is_some();
|
||||||
|
let edit = button(text(t!("Edit")).size(12)).padding([5, 11]);
|
||||||
|
let delete = button(text(t!("Delete")).size(12)).padding([5, 11]);
|
||||||
|
let insert = button(text(t!("Insert Table")).size(12)).padding([5, 11]);
|
||||||
|
row![if enabled { edit.on_press(Message::DataLinkEdit) } else { edit }, if enabled { delete.on_press(Message::DataLinkDelete) } else { delete }, Space::new().width(Fill), if enabled { insert.on_press(Message::DataLinkInsert) } else { insert }, button(text(t!("Close")).size(12)).on_press(Message::DataLinkClose).padding([5, 11])].spacing(7)
|
||||||
|
};
|
||||||
|
column![row![tree, container(right).width(Fill).height(Fill)].spacing(8).height(Fill), text(&state.status).size(11), actions]
|
||||||
|
.spacing(7).padding(10).width(sizing.width).height(sizing.height).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wizard_nav(state: &DataExtractionState) -> iced::widget::Row<'_, Message> {
|
||||||
|
let back = button(text(t!("Back")).size(12)).padding([5, 12]);
|
||||||
|
let next = button(text(if state.page == ExtractionPage::Finish { t!("Finish") } else { t!("Next") }).size(12)).padding([5, 12]).style(button::primary);
|
||||||
|
row![Space::new().width(Fill), if state.page == ExtractionPage::Begin { back } else { back.on_press(Message::DataExtractionBack) }, button(text(t!("Cancel")).size(12)).on_press(Message::CloseModal).padding([5, 12]), if state.error.is_empty() { next.on_press(if state.page == ExtractionPage::Finish { Message::DataExtractionFinish } else { Message::DataExtractionNext }) } else { next }].spacing(7)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data_extraction_view(state: &DataExtractionState, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
|
||||||
|
let body: Element<'_, Message> = match state.page {
|
||||||
|
ExtractionPage::Begin => group(t!("Begin"), column![
|
||||||
|
radio_button(state.begin == ExtractionBegin::New, t!("Create a new data extraction"), Message::DataExtractionField(DataExtractionField::Begin(ExtractionBegin::New))),
|
||||||
|
radio_button(state.begin == ExtractionBegin::Template, t!("Use a previous extraction as a template"), Message::DataExtractionField(DataExtractionField::Begin(ExtractionBegin::Template))),
|
||||||
|
radio_button(state.begin == ExtractionBegin::Edit, t!("Edit an existing data extraction"), Message::DataExtractionField(DataExtractionField::Begin(ExtractionBegin::Edit))),
|
||||||
|
row![text(t!("Extraction settings")).size(11).style(muted).width(118), text_input("", &state.settings_path).on_input(|v| Message::DataExtractionField(DataExtractionField::SettingsPath(v))).padding([4, 7]).size(12).width(Fill), button(text("…")).on_press(Message::DataExtractionBrowseSettings).padding([4, 9])].spacing(6).align_y(iced::Center),
|
||||||
|
].spacing(5).into()),
|
||||||
|
ExtractionPage::Source => group(t!("Define data source"), column![
|
||||||
|
radio_button(state.source == ExtractionSource::CurrentDrawing, t!("Current drawing"), Message::DataExtractionField(DataExtractionField::Source(ExtractionSource::CurrentDrawing))),
|
||||||
|
radio_button(state.source == ExtractionSource::CurrentSelection, t!("Select objects in the current drawing"), Message::DataExtractionField(DataExtractionField::Source(ExtractionSource::CurrentSelection))),
|
||||||
|
radio_button(state.source == ExtractionSource::DrawingsAndFolders, t!("Drawings / folders"), Message::DataExtractionField(DataExtractionField::Source(ExtractionSource::DrawingsAndFolders))),
|
||||||
|
checkbox(state.include_current).label(t!("Include current drawing")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::IncludeCurrent(v))).size(14),
|
||||||
|
checkbox(state.include_subfolders).label(t!("Include subfolders")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::IncludeSubfolders(v))).size(14),
|
||||||
|
row![
|
||||||
|
button(text(t!("Add drawings…")).size(12)).on_press(Message::DataExtractionAddDrawings).padding([5, 10]),
|
||||||
|
button(text(t!("Add folder…")).size(12)).on_press(Message::DataExtractionAddFolder).padding([5, 10]),
|
||||||
|
button(text(t!("Clear")).size(12)).on_press(Message::DataExtractionClearSources).padding([5, 10]),
|
||||||
|
].spacing(6),
|
||||||
|
text(state.source_files.join("\n")).size(10),
|
||||||
|
].spacing(6).into()),
|
||||||
|
ExtractionPage::Objects => {
|
||||||
|
let mut list = column![].spacing(3);
|
||||||
|
for (i, item) in state.objects.iter().enumerate() {
|
||||||
|
list = list.push(checkbox(item.checked).label(format!("{} ({})", item.name, item.count)).on_toggle(move |v| Message::DataExtractionField(DataExtractionField::Object(i, v))).size(14));
|
||||||
|
}
|
||||||
|
group(t!("Select objects"), scrollable(list).height(Fill).into())
|
||||||
|
}
|
||||||
|
ExtractionPage::Properties => {
|
||||||
|
let mut categories = row![].spacing(10);
|
||||||
|
for category in ["General", "Geometry"] {
|
||||||
|
let checked = state.categories.contains(category);
|
||||||
|
let key = category.to_string();
|
||||||
|
categories = categories.push(checkbox(checked).label(category).on_toggle(move |v| Message::DataExtractionField(DataExtractionField::Category(key.clone(), v))).size(14));
|
||||||
|
}
|
||||||
|
let mut list = column![categories].spacing(4);
|
||||||
|
for (i, item) in state.properties.iter().enumerate().filter(|(_, p)| state.categories.contains(&p.category)) {
|
||||||
|
list = list.push(checkbox(item.checked).label(format!("{} · {}", item.name, item.category)).on_toggle(move |v| Message::DataExtractionField(DataExtractionField::Property(i, v))).size(14));
|
||||||
|
}
|
||||||
|
group(t!("Select properties"), scrollable(list).height(Fill).into())
|
||||||
|
}
|
||||||
|
ExtractionPage::Refine => group(t!("Refine data"), column![
|
||||||
|
row![checkbox(state.combine_identical).label(t!("Combine identical rows")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::CombineIdentical(v))).size(14), checkbox(state.show_count).label(t!("Show count column")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::ShowCount(v))).size(14), checkbox(state.show_name).label(t!("Show name column")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::ShowName(v))).size(14)].spacing(12),
|
||||||
|
scrollable(preview_grid(&state.preview)).height(Fill),
|
||||||
|
].spacing(8).into()),
|
||||||
|
ExtractionPage::Output => group(t!("Choose output"), column![
|
||||||
|
checkbox(state.output_table).label(t!("Insert data extraction table into drawing")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::OutputTable(v))).size(14),
|
||||||
|
checkbox(state.output_file).label(t!("Output data to external file")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::OutputFile(v))).size(14),
|
||||||
|
row![text(t!("Output file")).size(11).style(muted).width(118), text_input("", &state.output_path).on_input(|v| Message::DataExtractionField(DataExtractionField::OutputPath(v))).padding([4, 7]).size(12).width(Fill), button(text("…")).on_press(Message::DataExtractionBrowseOutput).padding([4, 9])].spacing(6).align_y(iced::Center),
|
||||||
|
].spacing(7).into()),
|
||||||
|
ExtractionPage::TableStyle => group(t!("Table style"), column![
|
||||||
|
row![text(t!("Style")).size(11).style(muted).width(118), pick_list(Some(state.table_style.clone()), state.table_styles.clone(), |value| value.clone()).on_select(|value| Message::DataExtractionField(DataExtractionField::TableStyle(value))).padding([3, 6]).width(Fill)].spacing(8),
|
||||||
|
labeled_input(t!("Title"), &state.table_title, |v| Message::DataExtractionField(DataExtractionField::TableTitle(v))),
|
||||||
|
preview_grid(&state.preview),
|
||||||
|
].spacing(7).into()),
|
||||||
|
ExtractionPage::Finish => group(t!("Finish"), column![text(t!("The selected drawing data is ready to be extracted.")).size(12), text(format!("{}: {}", t!("Rows"), state.preview.len().saturating_sub(1))).size(11), text(format!("{}: {}{}", t!("Output"), if state.output_table { t!("Drawing table") } else { "".into() }, if state.output_file { format!(" {}", t!("External file")) } else { String::new() })).size(11)].spacing(7).into()),
|
||||||
|
};
|
||||||
|
column![
|
||||||
|
text(format!("{} {}/8", t!("Data Extraction"), state.page.number())).size(13),
|
||||||
|
container(body).height(Fill).width(Fill),
|
||||||
|
text(&state.error).size(11),
|
||||||
|
wizard_nav(state),
|
||||||
|
].spacing(8).padding(10).width(sizing.width).height(sizing.height).into()
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ pub mod open_progress;
|
|||||||
pub mod recovery;
|
pub mod recovery;
|
||||||
pub mod options;
|
pub mod options;
|
||||||
pub mod attribute_editor;
|
pub mod attribute_editor;
|
||||||
|
pub mod annotation_data;
|
||||||
pub mod alias_editor;
|
pub mod alias_editor;
|
||||||
pub mod find_replace;
|
pub mod find_replace;
|
||||||
pub mod named_parameters;
|
pub mod named_parameters;
|
||||||
Reference in new issue
Block a user