forked from 1b-agent/opencadstudio-upstream
fix(mesh): complete polygon mesh support
This commit is contained in:
commit
e8cb752fab
48 files changed
+5962
-1701
No files matched your search
Generated
+2
-2
@@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=788eea0161ba9f3eb7ee6569fd8bb52a8207a2ed#788eea0161ba9f3eb7ee6569fd8bb52a8207a2ed"
|
source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=9a332bb3c48eca90c3eefaa17aed514da0b8694a#9a332bb3c48eca90c3eefaa17aed514da0b8694a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=b2b1d4b#b2b1d4b0cd7def2d26a0d519991fc89a89f916d3"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=da915bff09ba254e8bb5164e8627a6a24b37951e#da915bff09ba254e8bb5164e8627a6a24b37951e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|||||||
+3
-3
@@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
|||||||
rfd = "0.17"
|
rfd = "0.17"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "788eea0161ba9f3eb7ee6569fd8bb52a8207a2ed", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "9a332bb3c48eca90c3eefaa17aed514da0b8694a", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "b2b1d4b", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "da915bff09ba254e8bb5164e8627a6a24b37951e", features = ["acis", "offset"] }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
@@ -61,7 +61,7 @@ iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aa
|
|||||||
iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
||||||
|
|
||||||
[patch."https://github.com/HakanSeven12/cadcodec.git"]
|
[patch."https://github.com/HakanSeven12/cadcodec.git"]
|
||||||
acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "788eea0161ba9f3eb7ee6569fd8bb52a8207a2ed" }
|
acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "9a332bb3c48eca90c3eefaa17aed514da0b8694a" }
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] }
|
ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] }
|
||||||
|
|||||||
@@ -2447,6 +2447,8 @@ properties =
|
|||||||
.associative = Associative
|
.associative = Associative
|
||||||
.at-least = At least
|
.at-least = At least
|
||||||
.attachment-type = Attachment type
|
.attachment-type = Attachment type
|
||||||
|
.attachment = Attachment
|
||||||
|
.arrowhead-size = Arrowhead Size
|
||||||
.attenuation = Attenuation
|
.attenuation = Attenuation
|
||||||
.attribute-definition = Attribute Definition
|
.attribute-definition = Attribute Definition
|
||||||
.attributedefinition = AttributeDefinition
|
.attributedefinition = AttributeDefinition
|
||||||
@@ -2829,7 +2831,7 @@ properties =
|
|||||||
.polyfacemesh = PolyfaceMesh
|
.polyfacemesh = PolyfaceMesh
|
||||||
.polygonmesh = PolygonMesh
|
.polygonmesh = PolygonMesh
|
||||||
.polyline2d = Polyline2D
|
.polyline2d = Polyline2D
|
||||||
.polyline3d = Polyline3D
|
.polyline3d = 3D Polyline
|
||||||
.predefined = Predefined
|
.predefined = Predefined
|
||||||
.property-bag = Property Bag
|
.property-bag = Property Bag
|
||||||
.proxy-entity = Proxy Entity
|
.proxy-entity = Proxy Entity
|
||||||
|
|||||||
@@ -2387,6 +2387,8 @@ properties =
|
|||||||
.associative = İlişkisel
|
.associative = İlişkisel
|
||||||
.at-least = En az
|
.at-least = En az
|
||||||
.attachment-type = Bağlantı türü
|
.attachment-type = Bağlantı türü
|
||||||
|
.attachment = Bağlantı
|
||||||
|
.arrowhead-size = Ok Ucu Boyutu
|
||||||
.attenuation = Zayıflama
|
.attenuation = Zayıflama
|
||||||
.attribute-definition = Öznitelik Tanımı
|
.attribute-definition = Öznitelik Tanımı
|
||||||
.attributedefinition = Öznitelik Tanımı
|
.attributedefinition = Öznitelik Tanımı
|
||||||
|
|||||||
+346
-102
@@ -856,7 +856,7 @@ impl OpenCADStudio {
|
|||||||
.infer_dimension_sources(handle);
|
.infer_dimension_sources(handle);
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.attach_dimension_association(handle, sources.to_vec());
|
.attach_dimension_association(handle, sources);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
@@ -1269,7 +1269,7 @@ impl OpenCADStudio {
|
|||||||
.infer_dimension_sources(handle);
|
.infer_dimension_sources(handle);
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.attach_dimension_association(handle, sources.to_vec());
|
.attach_dimension_association(handle, sources);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
@@ -1287,6 +1287,8 @@ impl OpenCADStudio {
|
|||||||
CmdResult::CommitDimension {
|
CmdResult::CommitDimension {
|
||||||
mut entity,
|
mut entity,
|
||||||
association,
|
association,
|
||||||
|
preserve_base_style,
|
||||||
|
continue_command,
|
||||||
} => {
|
} => {
|
||||||
let label = self.history_label_from_active_cmd(i, "DIMENSION");
|
let label = self.history_label_from_active_cmd(i, "DIMENSION");
|
||||||
let association_mode = self.tabs[i]
|
let association_mode = self.tabs[i]
|
||||||
@@ -1300,6 +1302,17 @@ impl OpenCADStudio {
|
|||||||
acadrust::entities::Dimension::Ordinate(_)
|
acadrust::entities::Dimension::Ordinate(_)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
let inherited_dimension = if preserve_base_style {
|
||||||
|
match &entity {
|
||||||
|
acadrust::EntityType::Dimension(dimension) => Some((
|
||||||
|
dimension.base().common.layer.clone(),
|
||||||
|
dimension.base().style_name.clone(),
|
||||||
|
)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let pending = if association_mode == 0 {
|
let pending = if association_mode == 0 {
|
||||||
let layer = self.tabs[i].active_layer.clone();
|
let layer = self.tabs[i].active_layer.clone();
|
||||||
if layer != "0" || entity.as_entity().layer().is_empty() {
|
if layer != "0" || entity.as_entity().layer().is_empty() {
|
||||||
@@ -1330,6 +1343,12 @@ impl OpenCADStudio {
|
|||||||
&self.tabs[i].scene.document,
|
&self.tabs[i].scene.document,
|
||||||
&mut entity,
|
&mut entity,
|
||||||
);
|
);
|
||||||
|
if let (Some((layer, style_name)), acadrust::EntityType::Dimension(dimension)) =
|
||||||
|
(inherited_dimension, &mut entity)
|
||||||
|
{
|
||||||
|
dimension.base_mut().common.layer = layer;
|
||||||
|
dimension.base_mut().style_name = style_name;
|
||||||
|
}
|
||||||
let pieces = crate::modules::draw::modify::explode::explode_entity(
|
let pieces = crate::modules::draw::modify::explode::explode_entity(
|
||||||
&entity,
|
&entity,
|
||||||
&self.tabs[i].scene.document,
|
&self.tabs[i].scene.document,
|
||||||
@@ -1345,7 +1364,10 @@ impl OpenCADStudio {
|
|||||||
} else {
|
} else {
|
||||||
let delta_safe = self.delta_add_safe(i, &entity);
|
let delta_safe = self.delta_add_safe(i, &entity);
|
||||||
let pending = self.begin_undo(i, label, 1, delta_safe);
|
let pending = self.begin_undo(i, label, 1, delta_safe);
|
||||||
if let Some(handle) = self.commit_entity_handle(entity) {
|
if let Some(handle) = self.commit_entity_handle_with_dimension_policy(
|
||||||
|
entity,
|
||||||
|
preserve_base_style,
|
||||||
|
) {
|
||||||
if association_mode == 2 {
|
if association_mode == 2 {
|
||||||
let mut changes = vec![
|
let mut changes = vec![
|
||||||
(handle, crate::scene::ChangeKind::Modified),
|
(handle, crate::scene::ChangeKind::Modified),
|
||||||
@@ -1357,8 +1379,6 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.infer_dimension_sources(handle)
|
.infer_dimension_sources(handle)
|
||||||
.into_iter()
|
|
||||||
.collect()
|
|
||||||
},
|
},
|
||||||
|source| {
|
|source| {
|
||||||
if single_source_dimension {
|
if single_source_dimension {
|
||||||
@@ -1393,12 +1413,195 @@ impl OpenCADStudio {
|
|||||||
};
|
};
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
self.tabs[i].active_cmd = None;
|
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.restore_pre_cmd_tangent();
|
|
||||||
if let Some(pd) = pending {
|
if let Some(pd) = pending {
|
||||||
self.commit_undo_delta(i, pd);
|
self.commit_undo_delta(i, pd);
|
||||||
}
|
}
|
||||||
|
if continue_command {
|
||||||
|
if let Some(prompt) = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|cmd| cmd.prompt())
|
||||||
|
{
|
||||||
|
self.command_line.push_info(&prompt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CmdResult::CommitDimensionsAndExit(dimensions) => {
|
||||||
|
let association_mode = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.header
|
||||||
|
.dimension_associativity;
|
||||||
|
let mut made = 0;
|
||||||
|
let pending = if association_mode == 0 {
|
||||||
|
let mut pieces = Vec::new();
|
||||||
|
for (mut entity, _) in dimensions {
|
||||||
|
let layer = self.tabs[i].active_layer.clone();
|
||||||
|
if layer != "0" || entity.as_entity().layer().is_empty() {
|
||||||
|
entity.as_entity_mut().set_layer(layer);
|
||||||
|
}
|
||||||
|
crate::scene::view::dispatch::apply_color(
|
||||||
|
&mut entity,
|
||||||
|
self.ribbon.active_color,
|
||||||
|
);
|
||||||
|
crate::scene::view::dispatch::apply_common_prop(
|
||||||
|
&mut entity,
|
||||||
|
"linetype",
|
||||||
|
&self.ribbon.active_linetype.clone(),
|
||||||
|
);
|
||||||
|
crate::scene::view::dispatch::apply_line_weight(
|
||||||
|
&mut entity,
|
||||||
|
self.ribbon.active_lineweight,
|
||||||
|
);
|
||||||
|
let celtscale = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.header
|
||||||
|
.current_entity_linetype_scale;
|
||||||
|
if (celtscale - 1.0).abs() > 1e-9 && celtscale.abs() > 1e-9 {
|
||||||
|
entity.common_mut().linetype_scale = celtscale;
|
||||||
|
}
|
||||||
|
crate::scene::creation_style::apply_current_creation_styles(
|
||||||
|
&self.tabs[i].scene.document,
|
||||||
|
&mut entity,
|
||||||
|
);
|
||||||
|
let exploded = crate::modules::draw::modify::explode::explode_entity(
|
||||||
|
&entity,
|
||||||
|
&self.tabs[i].scene.document,
|
||||||
|
);
|
||||||
|
if !exploded.is_empty() {
|
||||||
|
made += 1;
|
||||||
|
}
|
||||||
|
pieces.extend(exploded);
|
||||||
|
}
|
||||||
|
let delta_safe = pieces
|
||||||
|
.iter()
|
||||||
|
.all(|piece| self.delta_add_safe(i, piece));
|
||||||
|
let pending = self.begin_undo(i, "QDIM", pieces.len(), delta_safe);
|
||||||
|
for piece in pieces {
|
||||||
|
self.tabs[i].scene.add_entity(piece);
|
||||||
|
}
|
||||||
|
pending
|
||||||
|
} else {
|
||||||
|
let delta_safe = dimensions
|
||||||
|
.iter()
|
||||||
|
.all(|(entity, _)| self.delta_add_safe(i, entity));
|
||||||
|
let pending = self.begin_undo(i, "QDIM", dimensions.len(), delta_safe);
|
||||||
|
for (entity, association) in dimensions {
|
||||||
|
let Some(handle) = self.commit_entity_handle(entity) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
made += 1;
|
||||||
|
if association_mode != 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let sources = match association {
|
||||||
|
crate::command::DimensionAssociationInput::Infer(source) => {
|
||||||
|
source.map_or_else(
|
||||||
|
|| {
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.infer_dimension_sources(handle)
|
||||||
|
.into_iter()
|
||||||
|
.map(|source| {
|
||||||
|
source.map(
|
||||||
|
crate::command::DimensionAssociationSource::inferred,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
|source| {
|
||||||
|
vec![Some(
|
||||||
|
crate::command::DimensionAssociationSource::inferred(
|
||||||
|
source,
|
||||||
|
),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
crate::command::DimensionAssociationInput::Explicit(sources) => {
|
||||||
|
sources
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.attach_dimension_association_sources(handle, sources.clone());
|
||||||
|
let mut changes = vec![(handle, crate::scene::ChangeKind::Modified)];
|
||||||
|
changes.extend(sources.into_iter().flatten().map(|source| {
|
||||||
|
(source.handle, crate::scene::ChangeKind::Modified)
|
||||||
|
}));
|
||||||
|
changes.sort_by_key(|(handle, _)| handle.value());
|
||||||
|
changes.dedup_by_key(|(handle, _)| handle.value());
|
||||||
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
|
}
|
||||||
|
pending
|
||||||
|
};
|
||||||
|
if made > 0 {
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
}
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
self.command_line
|
||||||
|
.push_output(crate::tf!("QDIM {made} dimensions created.").as_ref());
|
||||||
|
if let Some(pd) = pending {
|
||||||
|
self.commit_undo_delta(i, pd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CmdResult::SetQuickDimensionSnapPriority(priority) => {
|
||||||
|
self.quick_dimension_snap_priority = priority.min(1);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|command| command.prompt());
|
||||||
|
if let Some(prompt) = prompt {
|
||||||
|
self.command_line.push_info(&prompt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CmdResult::AlignMLeaders {
|
||||||
|
mut handles,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
} => {
|
||||||
|
handles.retain(|handle| {
|
||||||
|
!self.tabs[i].scene.is_layer_locked(*handle)
|
||||||
|
&& matches!(
|
||||||
|
self.tabs[i].scene.document.get_entity(*handle),
|
||||||
|
Some(acadrust::EntityType::MultiLeader(_))
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if !handles.is_empty() {
|
||||||
|
self.push_undo_snapshot(i, "MLEADERALIGN");
|
||||||
|
if apply_mleader_align(&mut self.tabs[i].scene, &handles, from, to) {
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line.push_output(
|
||||||
|
crate::t!("MLEADERALIGN Leaders aligned.").as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
}
|
||||||
|
CmdResult::CollectMLeaders { handles, point } => {
|
||||||
|
let compatible = compatible_mleader_collect_handles(&self.tabs[i].scene, &handles);
|
||||||
|
if compatible.len() >= 2 {
|
||||||
|
self.push_undo_snapshot(i, "MLEADERCOLLECT");
|
||||||
|
if apply_mleader_collect(&mut self.tabs[i].scene, &compatible, point) {
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line.push_output(
|
||||||
|
crate::t!("MLEADERCOLLECT Leaders collected.").as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
}
|
}
|
||||||
CmdResult::CommitSolid {
|
CmdResult::CommitSolid {
|
||||||
entity,
|
entity,
|
||||||
@@ -1859,30 +2062,6 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
if layer.starts_with("__MLEADERALIGN__") {
|
|
||||||
if let Some(encoded) = layer.strip_prefix("__MLEADERALIGN__") {
|
|
||||||
apply_mleader_align(&mut self.tabs[i].scene, encoded);
|
|
||||||
}
|
|
||||||
self.push_undo_snapshot(i, "MLEADERALIGN");
|
|
||||||
self.command_line
|
|
||||||
.push_output(crate::t!("MLEADERALIGN Leaders aligned.").as_ref());
|
|
||||||
self.tabs[i].dirty = true;
|
|
||||||
self.tabs[i].active_cmd = None;
|
|
||||||
self.tabs[i].snap_result = None;
|
|
||||||
return Task::none();
|
|
||||||
}
|
|
||||||
if layer.starts_with("__MLEADERCOLLECT__") {
|
|
||||||
if let Some(encoded) = layer.strip_prefix("__MLEADERCOLLECT__") {
|
|
||||||
apply_mleader_collect(&mut self.tabs[i].scene, encoded);
|
|
||||||
}
|
|
||||||
self.push_undo_snapshot(i, "MLEADERCOLLECT");
|
|
||||||
self.command_line
|
|
||||||
.push_output(crate::t!("MLEADERCOLLECT Leaders collected.").as_ref());
|
|
||||||
self.tabs[i].dirty = true;
|
|
||||||
self.tabs[i].active_cmd = None;
|
|
||||||
self.tabs[i].snap_result = None;
|
|
||||||
return Task::none();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2752,6 +2931,9 @@ impl OpenCADStudio {
|
|||||||
.map(|e| apply_pedit(e, &op))
|
.map(|e| apply_pedit(e, &op))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if changed {
|
if changed {
|
||||||
|
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
|
||||||
|
command.on_pedit_applied();
|
||||||
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
// Repaint immediately (wide-band fill included).
|
// Repaint immediately (wide-band fill included).
|
||||||
self.tabs[i].scene.refresh_fill_model(handle);
|
self.tabs[i].scene.refresh_fill_model(handle);
|
||||||
@@ -4437,92 +4619,154 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── MLEADERALIGN helper ───────────────────────────────────────────────────────
|
fn apply_mleader_align(
|
||||||
|
scene: &mut crate::scene::Scene,
|
||||||
|
handles: &[acadrust::Handle],
|
||||||
|
from: glam::DVec3,
|
||||||
|
to: glam::DVec3,
|
||||||
|
) -> bool {
|
||||||
|
use cadkernel::geom2d::{closest_point, Curve, Vec2, XLine};
|
||||||
|
|
||||||
/// Parse `h1,h2,...;fx,fz;tx,tz` and align multileader content points along the direction.
|
let Some(direction) = Vec2::new(to.x - from.x, to.y - from.y).normalize() else {
|
||||||
fn apply_mleader_align(scene: &mut crate::scene::Scene, encoded: &str) {
|
return false;
|
||||||
// Format: "<h1>,<h2>,...;<fx>,<fz>;<tx>,<tz>"
|
};
|
||||||
let parts: Vec<&str> = encoded.splitn(3, ';').collect();
|
let line = Curve::XLine(XLine {
|
||||||
if parts.len() < 3 {
|
base: [from.x, from.y],
|
||||||
return;
|
direction: direction.into(),
|
||||||
}
|
});
|
||||||
let handles: Vec<acadrust::Handle> = parts[0]
|
let mut changed = Vec::new();
|
||||||
.split(',')
|
for &handle in handles {
|
||||||
.filter_map(|s| s.parse::<u64>().ok().map(acadrust::Handle::from))
|
if let Some(acadrust::EntityType::MultiLeader(ml)) =
|
||||||
.collect();
|
scene.document.get_entity_mut(handle)
|
||||||
let from_parts: Vec<f64> = parts[1].split(',').filter_map(|s| s.parse().ok()).collect();
|
{
|
||||||
let to_parts: Vec<f64> = parts[2].split(',').filter_map(|s| s.parse().ok()).collect();
|
let old = ml.context.content_base_point;
|
||||||
if from_parts.len() < 2 || to_parts.len() < 2 || handles.is_empty() {
|
let projected = closest_point(&line, [old.x, old.y]).point;
|
||||||
return;
|
let new_x = projected[0];
|
||||||
}
|
let new_y = projected[1];
|
||||||
|
let shift_x = new_x - old.x;
|
||||||
let fx = from_parts[0];
|
let shift_y = new_y - old.y;
|
||||||
let fz = from_parts[1];
|
if shift_x.abs() <= 1.0e-12 && shift_y.abs() <= 1.0e-12 {
|
||||||
let tx = to_parts[0];
|
continue;
|
||||||
let tz = to_parts[1];
|
}
|
||||||
let dx = tx - fx;
|
ml.context.content_base_point.x = new_x;
|
||||||
let dz = tz - fz;
|
ml.context.content_base_point.y = new_y;
|
||||||
let len = (dx * dx + dz * dz).sqrt();
|
ml.context.text_location.x += shift_x;
|
||||||
if len < 1e-9 {
|
ml.context.text_location.y += shift_y;
|
||||||
return;
|
ml.context.block_content_location.x += shift_x;
|
||||||
}
|
ml.context.block_content_location.y += shift_y;
|
||||||
|
for root in &mut ml.context.leader_roots {
|
||||||
// Project each multileader's content point onto the alignment line, then
|
root.connection_point.x += shift_x;
|
||||||
// snap it to the line (preserve perpendicular offset from line is discarded;
|
root.connection_point.y += shift_y;
|
||||||
// align along direction through `from`).
|
}
|
||||||
for h in handles {
|
changed.push((handle, crate::scene::ChangeKind::Modified));
|
||||||
if let Some(acadrust::EntityType::MultiLeader(ml)) = scene.document.get_entity_mut(h) {
|
|
||||||
let cp = &mut ml.context.content_base_point;
|
|
||||||
// Project onto line from_pt + t * dir: keep t component, set perpendicular = 0
|
|
||||||
let rel_x = cp.x - fx;
|
|
||||||
let rel_z = cp.z - fz;
|
|
||||||
let t = (rel_x * (dx / len) + rel_z * (dz / len)) / len;
|
|
||||||
let t = t.clamp(0.0, 1.0);
|
|
||||||
cp.x = fx + t * dx;
|
|
||||||
cp.z = fz + t * dz;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !changed.is_empty() {
|
||||||
|
scene.bump_entities(&changed);
|
||||||
|
}
|
||||||
|
!changed.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── MLEADERCOLLECT helper ─────────────────────────────────────────────────────
|
fn compatible_mleader_collect_handles(
|
||||||
|
scene: &crate::scene::Scene,
|
||||||
/// Parse `h1,h2,...;px,pz` — merge all selected multileaders into the first one at position.
|
handles: &[acadrust::Handle],
|
||||||
fn apply_mleader_collect(scene: &mut crate::scene::Scene, encoded: &str) {
|
) -> Vec<acadrust::Handle> {
|
||||||
let parts: Vec<&str> = encoded.splitn(2, ';').collect();
|
let Some((base_block, base_style)) = handles.iter().find_map(|handle| {
|
||||||
if parts.len() < 2 {
|
if scene.is_layer_locked(*handle) {
|
||||||
return;
|
return None;
|
||||||
}
|
}
|
||||||
let handles: Vec<acadrust::Handle> = parts[0]
|
let acadrust::EntityType::MultiLeader(leader) =
|
||||||
.split(',')
|
scene.document.get_entity(*handle)?
|
||||||
.filter_map(|s| s.parse::<u64>().ok().map(acadrust::Handle::from))
|
else {
|
||||||
.collect();
|
return None;
|
||||||
let pos_parts: Vec<f64> = parts[1].split(',').filter_map(|s| s.parse().ok()).collect();
|
};
|
||||||
if handles.len() < 2 || pos_parts.len() < 2 {
|
(leader.content_type == acadrust::entities::LeaderContentType::Block)
|
||||||
return;
|
.then_some((leader.block_content_handle, leader.style_handle))
|
||||||
}
|
}) else {
|
||||||
|
return Vec::new();
|
||||||
let px = pos_parts[0];
|
};
|
||||||
let pz = pos_parts[1];
|
let mut compatible = Vec::new();
|
||||||
|
for handle in handles.iter().copied() {
|
||||||
// Collect all leader roots from the secondary multileaders.
|
if compatible.contains(&handle) {
|
||||||
let mut extra_roots: Vec<acadrust::entities::LeaderRoot> = Vec::new();
|
continue;
|
||||||
for &h in &handles[1..] {
|
}
|
||||||
if let Some(acadrust::EntityType::MultiLeader(ml)) = scene.document.get_entity(h) {
|
if !scene.is_layer_locked(handle)
|
||||||
extra_roots.extend(ml.context.leader_roots.iter().cloned());
|
&& matches!(
|
||||||
|
scene.document.get_entity(handle),
|
||||||
|
Some(acadrust::EntityType::MultiLeader(leader))
|
||||||
|
if leader.content_type == acadrust::entities::LeaderContentType::Block
|
||||||
|
&& leader.block_content_handle == base_block
|
||||||
|
&& leader.style_handle == base_style
|
||||||
|
)
|
||||||
|
{
|
||||||
|
compatible.push(handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
compatible
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_mleader_collect(
|
||||||
|
scene: &mut crate::scene::Scene,
|
||||||
|
handles: &[acadrust::Handle],
|
||||||
|
point: glam::DVec3,
|
||||||
|
) -> bool {
|
||||||
|
if handles.len() < 2 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let px = point.x;
|
||||||
|
let py = point.y;
|
||||||
|
let Some(acadrust::EntityType::MultiLeader(base)) =
|
||||||
|
scene.document.get_entity(handles[0])
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let base_block = base.block_content_handle;
|
||||||
|
let base_style = base.style_handle;
|
||||||
|
|
||||||
|
let mut extra_roots: Vec<acadrust::entities::LeaderRoot> = Vec::new();
|
||||||
|
let mut merged_handles = Vec::new();
|
||||||
|
for &h in &handles[1..] {
|
||||||
|
if let Some(acadrust::EntityType::MultiLeader(ml)) = scene.document.get_entity(h) {
|
||||||
|
if ml.content_type == acadrust::entities::LeaderContentType::Block
|
||||||
|
&& ml.block_content_handle == base_block
|
||||||
|
&& ml.style_handle == base_style
|
||||||
|
{
|
||||||
|
let shift_x = px - ml.context.content_base_point.x;
|
||||||
|
let shift_y = py - ml.context.content_base_point.y;
|
||||||
|
extra_roots.extend(ml.context.leader_roots.iter().cloned().map(|mut root| {
|
||||||
|
root.connection_point.x += shift_x;
|
||||||
|
root.connection_point.y += shift_y;
|
||||||
|
root
|
||||||
|
}));
|
||||||
|
merged_handles.push(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if merged_handles.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Add collected roots to the first multileader and move its content point.
|
|
||||||
if let Some(acadrust::EntityType::MultiLeader(ml)) = scene.document.get_entity_mut(handles[0]) {
|
if let Some(acadrust::EntityType::MultiLeader(ml)) = scene.document.get_entity_mut(handles[0]) {
|
||||||
|
let shift_x = px - ml.context.content_base_point.x;
|
||||||
|
let shift_y = py - ml.context.content_base_point.y;
|
||||||
|
for root in &mut ml.context.leader_roots {
|
||||||
|
root.connection_point.x += shift_x;
|
||||||
|
root.connection_point.y += shift_y;
|
||||||
|
}
|
||||||
ml.context.content_base_point.x = px;
|
ml.context.content_base_point.x = px;
|
||||||
ml.context.content_base_point.z = pz;
|
ml.context.content_base_point.y = py;
|
||||||
|
ml.context.block_content_location.x = px;
|
||||||
|
ml.context.block_content_location.y = py;
|
||||||
|
ml.context.text_location.x = px;
|
||||||
|
ml.context.text_location.y = py;
|
||||||
for root in extra_roots {
|
for root in extra_roots {
|
||||||
ml.context.leader_roots.push(root);
|
ml.context.leader_roots.push(root);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erase the secondary multileaders.
|
scene.erase_entities(&merged_handles);
|
||||||
scene.erase_entities(&handles[1..]);
|
scene.bump_entities(&[(handles[0], crate::scene::ChangeKind::Modified)]);
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// MATCHPROP special properties (#281): copy every STYLE-affecting field from
|
/// MATCHPROP special properties (#281): copy every STYLE-affecting field from
|
||||||
|
|||||||
+87
-230
@@ -604,9 +604,31 @@ impl OpenCADStudio {
|
|||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
let multiplier = self.tabs[i].scene.creation_annotation_multiplier();
|
let multiplier = self.tabs[i].scene.creation_annotation_multiplier();
|
||||||
|
let block_sources = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.filter(|block| !block.name.is_empty() && !block.name.starts_with('*'))
|
||||||
|
.map(|block| (block.name.clone(), block.handle))
|
||||||
|
.collect();
|
||||||
|
let layers = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.map(|layer| layer.name.clone())
|
||||||
|
.collect();
|
||||||
|
let text_styles = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.text_styles
|
||||||
|
.iter()
|
||||||
|
.map(|style| (style.name.clone(), style.handle))
|
||||||
|
.collect();
|
||||||
let new_cmd = style.map_or_else(MLeaderCommand::new, |style| {
|
let new_cmd = style.map_or_else(MLeaderCommand::new, |style| {
|
||||||
MLeaderCommand::with_style(style, multiplier)
|
MLeaderCommand::with_style(style, multiplier)
|
||||||
});
|
}).with_drawing_resources(block_sources, layers, text_styles);
|
||||||
self.command_line.push_info(&new_cmd.prompt());
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
@@ -646,138 +668,87 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
"DIMCONTINUE" => {
|
"DIMCONTINUE" => {
|
||||||
use crate::modules::annotate::dim_continue::DimContinueCommand;
|
use crate::modules::annotate::dim_continue::DimContinueCommand;
|
||||||
let cmd = if let Some((p1, p2, dp, rot, trot)) =
|
let scene = &self.tabs[i].scene;
|
||||||
find_last_linear_dim(&self.tabs[i].scene)
|
let recent = scene
|
||||||
{
|
.last_created_dimension
|
||||||
DimContinueCommand::from_base(p1, p2, dp, rot, trot)
|
.filter(|handle| scene.entity_belongs_to_active_space(*handle))
|
||||||
} else {
|
.and_then(|handle| scene.document.get_entity(handle))
|
||||||
DimContinueCommand::new()
|
.cloned();
|
||||||
};
|
let cmd = DimContinueCommand::new(
|
||||||
|
recent,
|
||||||
|
self.dimension_continue_mode == 1,
|
||||||
|
);
|
||||||
self.command_line.push_info(&cmd.prompt());
|
self.command_line.push_info(&cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
"DIMBASELINE" => {
|
"DIMBASELINE" => {
|
||||||
use crate::modules::annotate::dim_baseline::DimBaselineCommand;
|
use crate::modules::annotate::dim_baseline::DimBaselineCommand;
|
||||||
let cmd = if let Some((p1, p2, dp, rot, trot)) =
|
let scene = &self.tabs[i].scene;
|
||||||
find_last_linear_dim(&self.tabs[i].scene)
|
let recent = scene
|
||||||
{
|
.last_created_dimension
|
||||||
let doc = &self.tabs[i].scene.document;
|
.filter(|handle| scene.entity_belongs_to_active_space(*handle))
|
||||||
let dimdli = doc
|
.and_then(|handle| scene.document.get_entity(handle))
|
||||||
.dim_styles
|
.cloned();
|
||||||
.iter()
|
let dimdli_by_style = scene
|
||||||
.find(|s| {
|
.document
|
||||||
s.name
|
.dim_styles
|
||||||
.eq_ignore_ascii_case(&doc.header.current_dimstyle_name)
|
.iter()
|
||||||
})
|
.map(|style| (style.name.to_ascii_lowercase(), style.dimdli))
|
||||||
.map(|s| s.dimdli as f32)
|
.collect();
|
||||||
.unwrap_or(1.5);
|
let fallback_dimdli = if scene.document.header.measurement == 1 {
|
||||||
DimBaselineCommand::from_base(p1, p2, dp, rot, trot, dimdli)
|
3.75
|
||||||
} else {
|
} else {
|
||||||
DimBaselineCommand::new()
|
0.38
|
||||||
};
|
};
|
||||||
|
let current_style_name = scene.document.header.current_dimstyle_name.clone();
|
||||||
|
let cmd = DimBaselineCommand::new(
|
||||||
|
recent,
|
||||||
|
dimdli_by_style,
|
||||||
|
current_style_name,
|
||||||
|
fallback_dimdli,
|
||||||
|
self.dimension_continue_mode == 1,
|
||||||
|
);
|
||||||
self.command_line.push_info(&cmd.prompt());
|
self.command_line.push_info(&cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
"QDIM" => {
|
"QDIM" => {
|
||||||
use crate::modules::annotate::qdim::QdimCommand;
|
use crate::modules::annotate::qdim::QdimCommand;
|
||||||
let cmd = QdimCommand::new();
|
let document = &self.tabs[i].scene.document;
|
||||||
self.command_line.push_info(&cmd.prompt());
|
let dim_spacing = document
|
||||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
.dim_styles
|
||||||
}
|
.iter()
|
||||||
|
.find(|style| {
|
||||||
// QDIM second stage: the front-end relaunched with the picked
|
style
|
||||||
// dimension-line point and the gathered geometry now selected. Build
|
.name
|
||||||
// a continuous chain of linear dimensions across the endpoints.
|
.eq_ignore_ascii_case(&document.header.current_dimstyle_name)
|
||||||
cmd if cmd.starts_with("QDIM_PLACE ") => {
|
})
|
||||||
let nums: Vec<f64> = cmd
|
.map(|style| style.dimdli)
|
||||||
.split_whitespace()
|
.unwrap_or_else(|| {
|
||||||
.skip(1)
|
if document.header.measurement == 1 {
|
||||||
.filter_map(|s| s.parse().ok())
|
3.75
|
||||||
.collect();
|
} else {
|
||||||
if nums.len() < 3 {
|
0.38
|
||||||
return Some(Task::none());
|
}
|
||||||
}
|
});
|
||||||
let plane = if self.tabs[i].editing_model_space() {
|
let selection = self.tabs[i]
|
||||||
self.tabs[i].ucs_xform().working_plane()
|
|
||||||
} else {
|
|
||||||
crate::command::WorkingPlane::default()
|
|
||||||
};
|
|
||||||
let place = plane.to_local(glam::DVec3::new(nums[0], nums[1], nums[2]));
|
|
||||||
let handles: Vec<acadrust::Handle> = self.tabs[i]
|
|
||||||
.scene
|
.scene
|
||||||
.selected_entities()
|
.selected_entities()
|
||||||
.iter()
|
.into_iter()
|
||||||
.map(|(h, _)| *h)
|
.map(|(handle, entity)| crate::command::SelectionEntity {
|
||||||
|
handle,
|
||||||
|
entity: entity.clone(),
|
||||||
|
surface_area: None,
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let mut pts: Vec<glam::DVec3> = Vec::new();
|
let cmd = QdimCommand::new(
|
||||||
for h in &handles {
|
selection,
|
||||||
if let Some(e) = self.tabs[i].scene.document.get_entity(*h) {
|
dim_spacing,
|
||||||
qdim_collect_points(e, &mut pts);
|
self.quick_dimension_snap_priority,
|
||||||
}
|
);
|
||||||
}
|
self.command_line.push_info(&cmd.prompt());
|
||||||
for point in &mut pts {
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
*point = plane.to_local(*point);
|
|
||||||
}
|
|
||||||
if pts.len() < 2 {
|
|
||||||
self.command_line
|
|
||||||
.push_error(crate::t!("QDIM: no dimensionable endpoints in the selection.").as_ref());
|
|
||||||
return Some(Task::none());
|
|
||||||
}
|
|
||||||
// Choose the dimension axis from the points' spread: a wider X
|
|
||||||
// span dimensions horizontally (ordered by X), else vertically.
|
|
||||||
let (minx, maxx) = pts
|
|
||||||
.iter()
|
|
||||||
.fold((f64::MAX, f64::MIN), |(a, b), p| (a.min(p.x), b.max(p.x)));
|
|
||||||
let (miny, maxy) = pts
|
|
||||||
.iter()
|
|
||||||
.fold((f64::MAX, f64::MIN), |(a, b), p| (a.min(p.y), b.max(p.y)));
|
|
||||||
let horizontal = (maxx - minx) >= (maxy - miny);
|
|
||||||
if horizontal {
|
|
||||||
pts.sort_by(|a, b| a.x.total_cmp(&b.x));
|
|
||||||
pts.dedup_by(|a, b| (a.x - b.x).abs() < 1e-4);
|
|
||||||
} else {
|
|
||||||
pts.sort_by(|a, b| a.y.total_cmp(&b.y));
|
|
||||||
pts.dedup_by(|a, b| (a.y - b.y).abs() < 1e-4);
|
|
||||||
}
|
|
||||||
if pts.len() < 2 {
|
|
||||||
self.command_line
|
|
||||||
.push_error(crate::t!("QDIM: endpoints collapse to a single position.").as_ref());
|
|
||||||
return Some(Task::none());
|
|
||||||
}
|
|
||||||
self.push_undo_snapshot(i, "QDIM");
|
|
||||||
let v = |p: glam::DVec3| {
|
|
||||||
acadrust::types::Vector3::new(p.x, p.y, p.z)
|
|
||||||
};
|
|
||||||
let mut made = 0usize;
|
|
||||||
for w in pts.windows(2) {
|
|
||||||
let (p1, p2) = (w[0], w[1]);
|
|
||||||
let mut dim = acadrust::entities::DimensionLinear::new(v(p1), v(p2));
|
|
||||||
dim.rotation = if horizontal {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
std::f64::consts::FRAC_PI_2
|
|
||||||
};
|
|
||||||
// Dim line passes through the picked perpendicular position.
|
|
||||||
let def = if horizontal {
|
|
||||||
glam::DVec3::new((p1.x + p2.x) * 0.5, place.y, 0.0)
|
|
||||||
} else {
|
|
||||||
glam::DVec3::new(place.x, (p1.y + p2.y) * 0.5, 0.0)
|
|
||||||
};
|
|
||||||
dim.definition_point = v(def);
|
|
||||||
dim.base.definition_point = v(def);
|
|
||||||
dim.base.actual_measurement = dim.measurement();
|
|
||||||
let entity = acadrust::EntityType::Dimension(
|
|
||||||
acadrust::entities::Dimension::Linear(dim),
|
|
||||||
);
|
|
||||||
self.commit_entity(plane.place_entity(entity));
|
|
||||||
made += 1;
|
|
||||||
}
|
|
||||||
self.tabs[i].dirty = true;
|
|
||||||
self.command_line
|
|
||||||
.push_output(crate::tf!("QDIM {made} dimensions created.").as_ref());
|
|
||||||
return Some(Task::none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"DIMEDIT" => {
|
"DIMEDIT" => {
|
||||||
@@ -1376,120 +1347,6 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the last placed linear or aligned dimension in the document.
|
|
||||||
/// Returns `(first_point, second_point, definition_point, rotation_rad)` in world-space.
|
|
||||||
fn find_last_linear_dim(
|
|
||||||
scene: &crate::scene::Scene,
|
|
||||||
) -> Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64, f64)> {
|
|
||||||
use acadrust::entities::Dimension;
|
|
||||||
let mut best_handle: u64 = 0;
|
|
||||||
let mut result: Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64, f64)> = None;
|
|
||||||
|
|
||||||
for entity in scene.document.entities() {
|
|
||||||
if let acadrust::EntityType::Dimension(dim) = entity {
|
|
||||||
let h = entity.common().handle.value();
|
|
||||||
if h <= best_handle {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let item = match dim {
|
|
||||||
Dimension::Linear(d) => {
|
|
||||||
let p1 = glam::Vec3::new(
|
|
||||||
d.first_point.x as f32,
|
|
||||||
d.first_point.y as f32,
|
|
||||||
d.first_point.z as f32,
|
|
||||||
);
|
|
||||||
let p2 = glam::Vec3::new(
|
|
||||||
d.second_point.x as f32,
|
|
||||||
d.second_point.y as f32,
|
|
||||||
d.second_point.z as f32,
|
|
||||||
);
|
|
||||||
let dp = glam::Vec3::new(
|
|
||||||
d.base.definition_point.x as f32,
|
|
||||||
d.base.definition_point.y as f32,
|
|
||||||
d.base.definition_point.z as f32,
|
|
||||||
);
|
|
||||||
Some((p1, p2, dp, d.rotation, d.base.text_rotation))
|
|
||||||
}
|
|
||||||
Dimension::Aligned(d) => {
|
|
||||||
let p1 = glam::Vec3::new(
|
|
||||||
d.first_point.x as f32,
|
|
||||||
d.first_point.y as f32,
|
|
||||||
d.first_point.z as f32,
|
|
||||||
);
|
|
||||||
let p2 = glam::Vec3::new(
|
|
||||||
d.second_point.x as f32,
|
|
||||||
d.second_point.y as f32,
|
|
||||||
d.second_point.z as f32,
|
|
||||||
);
|
|
||||||
let dp = glam::Vec3::new(
|
|
||||||
d.base.definition_point.x as f32,
|
|
||||||
d.base.definition_point.y as f32,
|
|
||||||
d.base.definition_point.z as f32,
|
|
||||||
);
|
|
||||||
let dx = (d.second_point.x - d.first_point.x) as f32;
|
|
||||||
let dy = (d.second_point.y - d.first_point.y) as f32;
|
|
||||||
let rot = dy.atan2(dx) as f64;
|
|
||||||
Some((p1, p2, dp, rot, d.base.text_rotation))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
if let Some(data) = item {
|
|
||||||
best_handle = h;
|
|
||||||
result = Some(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collect candidate dimension endpoints from an entity for QDIM — line ends,
|
|
||||||
/// polyline vertices, arc endpoints — in world space.
|
|
||||||
fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec<glam::DVec3>) {
|
|
||||||
use acadrust::EntityType as ET;
|
|
||||||
let p = |v: &acadrust::types::Vector3| glam::DVec3::new(v.x, v.y, v.z);
|
|
||||||
let ocs = |point: (f64, f64, f64), normal: acadrust::types::Vector3| {
|
|
||||||
let world = crate::scene::view::transform::ocs_point_to_wcs(
|
|
||||||
point,
|
|
||||||
(normal.x, normal.y, normal.z),
|
|
||||||
);
|
|
||||||
glam::DVec3::new(world.0, world.1, world.2)
|
|
||||||
};
|
|
||||||
match e {
|
|
||||||
ET::Line(l) => {
|
|
||||||
out.push(p(&l.start));
|
|
||||||
out.push(p(&l.end));
|
|
||||||
}
|
|
||||||
ET::LwPolyline(pl) => {
|
|
||||||
for v in &pl.vertices {
|
|
||||||
out.push(ocs((v.location.x, v.location.y, pl.elevation), pl.normal));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ET::Polyline2D(pl) => {
|
|
||||||
for v in &pl.vertices {
|
|
||||||
out.push(ocs((v.location.x, v.location.y, pl.elevation), pl.normal));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ET::Polyline(pl) => {
|
|
||||||
for v in &pl.vertices {
|
|
||||||
out.push(p(&v.location));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ET::Arc(a) => {
|
|
||||||
for &ang in &[a.start_angle, a.end_angle] {
|
|
||||||
out.push(ocs(
|
|
||||||
(
|
|
||||||
a.center.x + a.radius * ang.cos(),
|
|
||||||
a.center.y + a.radius * ang.sin(),
|
|
||||||
a.center.z,
|
|
||||||
),
|
|
||||||
a.normal,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── TCASE helpers ──────────────────────────────────────────────────────────
|
// ── TCASE helpers ──────────────────────────────────────────────────────────
|
||||||
fn sentence_case(s: &str) -> String {
|
fn sentence_case(s: &str) -> String {
|
||||||
let mut c = s.chars();
|
let mut c = s.chars();
|
||||||
|
|||||||
@@ -461,6 +461,31 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"3DFACE" => {
|
||||||
|
use crate::modules::draw::draw::face3d::Face3dCommand;
|
||||||
|
let new_cmd = Face3dCommand::new();
|
||||||
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
"EDGE" => {
|
||||||
|
use crate::modules::draw::draw::face3d::FaceEdgeCommand;
|
||||||
|
let faces = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.filter_map(|entity| match entity {
|
||||||
|
acadrust::EntityType::Face3D(face) => {
|
||||||
|
Some((face.common.handle, face.clone()))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let new_cmd = FaceEdgeCommand::new(faces);
|
||||||
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
// 2D filled solid. Reached via SO / SOLID2D — the bare SOLID verb is
|
// 2D filled solid. Reached via SO / SOLID2D — the bare SOLID verb is
|
||||||
// currently the shaded-display toggle (token collision tracked).
|
// currently the shaded-display toggle (token collision tracked).
|
||||||
"SOLID" | "SOLID2D" => {
|
"SOLID" | "SOLID2D" => {
|
||||||
|
|||||||
@@ -931,6 +931,7 @@ impl OpenCADStudio {
|
|||||||
| "CENTERCROSSSIZE"
|
| "CENTERCROSSSIZE"
|
||||||
| "CENTERCROSSGAP"
|
| "CENTERCROSSGAP"
|
||||||
| "CENTERMARKEXE"
|
| "CENTERMARKEXE"
|
||||||
|
| "DIMCONTINUEMODE"
|
||||||
) =>
|
) =>
|
||||||
{
|
{
|
||||||
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
||||||
@@ -953,7 +954,7 @@ impl OpenCADStudio {
|
|||||||
let value = it.next().map(|s| s.trim().to_string());
|
let value = it.next().map(|s| s.trim().to_string());
|
||||||
if name.is_empty() || name == "?" {
|
if name.is_empty() || name == "?" {
|
||||||
self.command_line.push_info(
|
self.command_line.push_info(
|
||||||
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let frame_kind = crate::scene::frame::kind_for_name(&name);
|
let frame_kind = crate::scene::frame::kind_for_name(&name);
|
||||||
@@ -1069,6 +1070,28 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
return Some(self.finish_dispatch(cmd));
|
return Some(self.finish_dispatch(cmd));
|
||||||
}
|
}
|
||||||
|
if name == "DIMCONTINUEMODE" {
|
||||||
|
let current = self.dimension_continue_mode;
|
||||||
|
if let Some(value) = &value {
|
||||||
|
match value.parse::<i16>() {
|
||||||
|
Ok(mode @ 0..=1) => {
|
||||||
|
self.dimension_continue_mode = mode;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
self.command_line
|
||||||
|
.push_output(&format!("DIMCONTINUEMODE = {mode}"));
|
||||||
|
}
|
||||||
|
_ => self.command_line.push_error(
|
||||||
|
"SETVAR: DIMCONTINUEMODE requires 0 or 1.",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.command_line.push_output(crate::tf!(
|
||||||
|
"Enter new value for {name} <{current}>:"
|
||||||
|
).as_ref());
|
||||||
|
self.pending_setvar = Some(name.clone());
|
||||||
|
}
|
||||||
|
return Some(self.finish_dispatch(cmd));
|
||||||
|
}
|
||||||
if matches!(name.as_str(), "DONUTID" | "DONUTOD") {
|
if matches!(name.as_str(), "DONUTID" | "DONUTOD") {
|
||||||
let current = if name == "DONUTID" {
|
let current = if name == "DONUTID" {
|
||||||
crate::modules::draw::defaults::get_donut_inner_diameter()
|
crate::modules::draw::defaults::get_donut_inner_diameter()
|
||||||
|
|||||||
@@ -465,6 +465,10 @@ pub(super) struct OpenCADStudio {
|
|||||||
options_tab: crate::ui::window::options::OptionsTab,
|
options_tab: crate::ui::window::options::OptionsTab,
|
||||||
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
||||||
pub texteditmode: bool,
|
pub texteditmode: bool,
|
||||||
|
/// QDIM extension-origin priority: 0 = endpoints, 1 = intersections.
|
||||||
|
pub quick_dimension_snap_priority: u8,
|
||||||
|
/// Creation-style policy used by continuing and baseline dimensions.
|
||||||
|
pub dimension_continue_mode: i16,
|
||||||
/// When true (default), saving over an existing file first writes a `.bak`
|
/// When true (default), saving over an existing file first writes a `.bak`
|
||||||
/// copy of it for recovery (#205). Toggle with the ISAVEBAK command.
|
/// copy of it for recovery (#205). Toggle with the ISAVEBAK command.
|
||||||
pub backup_on_save: bool,
|
pub backup_on_save: bool,
|
||||||
@@ -2398,6 +2402,11 @@ pub enum Message {
|
|||||||
PropColorChanged(AcadColor),
|
PropColorChanged(AcadColor),
|
||||||
/// User selected a lineweight from the Properties pick_list.
|
/// User selected a lineweight from the Properties pick_list.
|
||||||
PropLwChanged(LineWeight),
|
PropLwChanged(LineWeight),
|
||||||
|
/// User selected an object-specific lineweight override.
|
||||||
|
PropFieldLwChanged {
|
||||||
|
field: &'static str,
|
||||||
|
value: LineWeight,
|
||||||
|
},
|
||||||
/// User selected a linetype from the linetype pick_list.
|
/// User selected a linetype from the linetype pick_list.
|
||||||
PropLinetypeChanged(String),
|
PropLinetypeChanged(String),
|
||||||
/// User toggled a boolean property (e.g. Invisible).
|
/// User toggled a boolean property (e.g. Invisible).
|
||||||
@@ -3210,6 +3219,8 @@ impl OpenCADStudio {
|
|||||||
dyn_input: true,
|
dyn_input: true,
|
||||||
options_tab: crate::ui::window::options::OptionsTab::General,
|
options_tab: crate::ui::window::options::OptionsTab::General,
|
||||||
texteditmode: false,
|
texteditmode: false,
|
||||||
|
quick_dimension_snap_priority: 0,
|
||||||
|
dimension_continue_mode: 1,
|
||||||
backup_on_save: true,
|
backup_on_save: true,
|
||||||
file_assoc_enabled: true,
|
file_assoc_enabled: true,
|
||||||
savetime_min: 10,
|
savetime_min: 10,
|
||||||
|
|||||||
+150
-20
@@ -122,7 +122,13 @@ impl OpenCADStudio {
|
|||||||
acadrust::EntityType::LwPolyline(polyline) => Some(polyline.vertices.len()),
|
acadrust::EntityType::LwPolyline(polyline) => Some(polyline.vertices.len()),
|
||||||
acadrust::EntityType::Polyline2D(polyline) => Some(polyline.vertices.len()),
|
acadrust::EntityType::Polyline2D(polyline) => Some(polyline.vertices.len()),
|
||||||
acadrust::EntityType::PolygonMesh(mesh) => Some(mesh.vertices.len()),
|
acadrust::EntityType::PolygonMesh(mesh) => Some(mesh.vertices.len()),
|
||||||
|
acadrust::EntityType::Polyline3D(polyline) => Some(
|
||||||
|
crate::entities::polyline::polyline3d_control_vertex_count(polyline),
|
||||||
|
),
|
||||||
acadrust::EntityType::Leader(leader) => Some(leader.vertices.len()),
|
acadrust::EntityType::Leader(leader) => Some(leader.vertices.len()),
|
||||||
|
acadrust::EntityType::Face3D(face) => {
|
||||||
|
Some(if face.is_triangle() { 3 } else { 4 })
|
||||||
|
}
|
||||||
acadrust::EntityType::Spline(spline) => {
|
acadrust::EntityType::Spline(spline) => {
|
||||||
Some(if crate::entities::spline::shows_fit_points(spline) {
|
Some(if crate::entities::spline::shows_fit_points(spline) {
|
||||||
spline.fit_points.len()
|
spline.fit_points.len()
|
||||||
@@ -846,6 +852,12 @@ impl OpenCADStudio {
|
|||||||
.map(|b| b.name.clone()),
|
.map(|b| b.name.clone()),
|
||||||
)
|
)
|
||||||
.collect();
|
.collect();
|
||||||
|
let block_names: Vec<String> = doc
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.map(|block| block.name.clone())
|
||||||
|
.filter(|name| !name.is_empty() && !name.starts_with('*'))
|
||||||
|
.collect();
|
||||||
let tstyle_names = text_style_names.clone();
|
let tstyle_names = text_style_names.clone();
|
||||||
// Currently selected names.
|
// Currently selected names.
|
||||||
let cur_style = ml
|
let cur_style = ml
|
||||||
@@ -879,6 +891,15 @@ impl OpenCADStudio {
|
|||||||
doc.line_types.iter().find(|l| l.handle == h).map(|l| l.name.clone())
|
doc.line_types.iter().find(|l| l.handle == h).map(|l| l.name.clone())
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| "ByBlock".to_string());
|
.unwrap_or_else(|| "ByBlock".to_string());
|
||||||
|
let cur_block = ml
|
||||||
|
.block_content_handle
|
||||||
|
.and_then(|handle| {
|
||||||
|
doc.block_records
|
||||||
|
.iter()
|
||||||
|
.find(|block| block.handle == handle)
|
||||||
|
.map(|block| block.name.clone())
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "(none)".to_string());
|
||||||
let mut set_choice =
|
let mut set_choice =
|
||||||
|field: &str, selected: String, options: Vec<String>| {
|
|field: &str, selected: String, options: Vec<String>| {
|
||||||
for section in sections.iter_mut() {
|
for section in sections.iter_mut() {
|
||||||
@@ -897,6 +918,9 @@ impl OpenCADStudio {
|
|||||||
set_choice("text_style_handle", cur_tstyle, tstyle_names);
|
set_choice("text_style_handle", cur_tstyle, tstyle_names);
|
||||||
set_choice("arrowhead_handle", cur_arrow, arrow_names);
|
set_choice("arrowhead_handle", cur_arrow, arrow_names);
|
||||||
set_choice("line_type_handle", cur_ltype, ltype_names);
|
set_choice("line_type_handle", cur_ltype, ltype_names);
|
||||||
|
if !block_names.is_empty() {
|
||||||
|
set_choice("block_content_handle", cur_block, block_names);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject viewport-only properties that require doc access.
|
// Inject viewport-only properties that require doc access.
|
||||||
@@ -1004,6 +1028,26 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy leaders omit Handle and report annotation association.
|
||||||
|
if let acadrust::EntityType::Leader(leader) = entity {
|
||||||
|
if let Some(general) = sections.first_mut() {
|
||||||
|
general.props.retain(|property| property.field != "handle");
|
||||||
|
let associative = !leader.annotation_handle.is_null()
|
||||||
|
&& self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity(leader.annotation_handle)
|
||||||
|
.is_some();
|
||||||
|
general.props.push(crate::scene::model::object::Property {
|
||||||
|
label: t!("Associative").into_owned(),
|
||||||
|
field: "associative",
|
||||||
|
value: crate::scene::model::object::PropValue::ReadOnly(
|
||||||
|
if associative { "Yes" } else { "No" }.to_string(),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Inject DimStyle picker + style-derived groups for Dimensions.
|
// Inject DimStyle picker + style-derived groups for Dimensions.
|
||||||
if let acadrust::EntityType::Dimension(d) = entity {
|
if let acadrust::EntityType::Dimension(d) = entity {
|
||||||
if let Some(general) = sections.first_mut() {
|
if let Some(general) = sections.first_mut() {
|
||||||
@@ -1198,14 +1242,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Lines & Arrows / Text / Fit default to the assigned
|
// Resolve editable overrides from the assigned dimension style.
|
||||||
// dimension style (the same source the tessellator
|
|
||||||
// uses); most rows are editable per-object overrides
|
|
||||||
// stored in ACAD_DSTYLE. Arrow size / block / overall
|
|
||||||
// scale and dim-line lineweight drive the render; text
|
|
||||||
// offset / vertical position round-trip to file but
|
|
||||||
// don't change the leader glyph here (its annotation
|
|
||||||
// is a separate entity). Dim-line colour is read-only.
|
|
||||||
if let Some(ds) = find_dim_style(doc, &ld.dimension_style) {
|
if let Some(ds) = find_dim_style(doc, &ld.dimension_style) {
|
||||||
use crate::entities::dim_override as dov;
|
use crate::entities::dim_override as dov;
|
||||||
use crate::scene::model::object::PropValue;
|
use crate::scene::model::object::PropValue;
|
||||||
@@ -1251,7 +1288,7 @@ impl OpenCADStudio {
|
|||||||
set_row_value(
|
set_row_value(
|
||||||
&mut sections,
|
&mut sections,
|
||||||
"arrow_size",
|
"arrow_size",
|
||||||
PropValue::EditText(format!("{asz:.4}")),
|
PropValue::EditText(asz.to_string()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let lwd = dov::int(xd, dov::DIMLWD).unwrap_or(ds.dimlwd);
|
let lwd = dov::int(xd, dov::DIMLWD).unwrap_or(ds.dimlwd);
|
||||||
@@ -1269,10 +1306,7 @@ impl OpenCADStudio {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Dim-line colour: a per-object ACAD_DSTYLE
|
// A per-object color override wins over the style.
|
||||||
// override (code 176, an ACI index) wins over the
|
|
||||||
// style's DIMCLRD. Editable — the picked colour is
|
|
||||||
// written back as that override so it round-trips.
|
|
||||||
let dim_c = dov::color(xd, dov::DIMCLRD)
|
let dim_c = dov::color(xd, dov::DIMCLRD)
|
||||||
.unwrap_or_else(|| acadrust::types::Color::from_index(ds.dimclrd));
|
.unwrap_or_else(|| acadrust::types::Color::from_index(ds.dimclrd));
|
||||||
set_row_value(
|
set_row_value(
|
||||||
@@ -1285,7 +1319,7 @@ impl OpenCADStudio {
|
|||||||
set_row_value(
|
set_row_value(
|
||||||
&mut sections,
|
&mut sections,
|
||||||
"text_offset",
|
"text_offset",
|
||||||
PropValue::EditText(format!("{gap:.4}")),
|
PropValue::EditText(gap.to_string()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let tad = dov::int(xd, dov::DIMTAD).unwrap_or(ds.dimtad);
|
let tad = dov::int(xd, dov::DIMTAD).unwrap_or(ds.dimtad);
|
||||||
@@ -1302,7 +1336,7 @@ impl OpenCADStudio {
|
|||||||
set_row_value(
|
set_row_value(
|
||||||
&mut sections,
|
&mut sections,
|
||||||
"dim_scale_overall",
|
"dim_scale_overall",
|
||||||
PropValue::EditText(format!("{scl:.4}")),
|
PropValue::EditText(scl.to_string()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1395,6 +1429,21 @@ impl OpenCADStudio {
|
|||||||
// MultiLeader: max points + segment-angle constraints
|
// MultiLeader: max points + segment-angle constraints
|
||||||
// are MLeaderStyle settings, not stored on the entity.
|
// are MLeaderStyle settings, not stored on the entity.
|
||||||
acadrust::EntityType::MultiLeader(ml) => {
|
acadrust::EntityType::MultiLeader(ml) => {
|
||||||
|
if ml
|
||||||
|
.text_style_handle
|
||||||
|
.and_then(|handle| {
|
||||||
|
doc.text_styles.iter().find(|style| style.handle == handle)
|
||||||
|
})
|
||||||
|
.is_some_and(|style| style.has_fixed_height())
|
||||||
|
{
|
||||||
|
set_row_value(
|
||||||
|
&mut sections,
|
||||||
|
"text_height",
|
||||||
|
PropValue::ReadOnly(
|
||||||
|
crate::entities::common::format_length(ml.text_height),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
if let Some(sh) = ml.style_handle {
|
if let Some(sh) = ml.style_handle {
|
||||||
if let Some((mx, a1, a2)) =
|
if let Some((mx, a1, a2)) =
|
||||||
doc.objects.iter().find_map(|(h, o)| match o {
|
doc.objects.iter().find_map(|(h, o)| match o {
|
||||||
@@ -1819,6 +1868,11 @@ impl OpenCADStudio {
|
|||||||
doc, entity,
|
doc, entity,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
acadrust::EntityType::Leader(_) => {
|
||||||
|
crate::scene::annotative::annotation_style_is_annotative(
|
||||||
|
doc, entity,
|
||||||
|
)
|
||||||
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
// Dimensions/tables/tolerances carry no Annotative row yet — add one
|
// Dimensions/tables/tolerances carry no Annotative row yet — add one
|
||||||
@@ -1856,8 +1910,17 @@ impl OpenCADStudio {
|
|||||||
"annotative",
|
"annotative",
|
||||||
if is_anno { "Yes" } else { "No" }.to_string(),
|
if is_anno { "Yes" } else { "No" }.to_string(),
|
||||||
),
|
),
|
||||||
|
acadrust::EntityType::Leader(_)
|
||||||
|
if crate::scene::annotative::annotation_style_is_annotative(
|
||||||
|
doc, entity,
|
||||||
|
) => set_row(
|
||||||
|
&mut sections,
|
||||||
|
"annotative",
|
||||||
|
"Yes".to_string(),
|
||||||
|
),
|
||||||
acadrust::EntityType::Text(_)
|
acadrust::EntityType::Text(_)
|
||||||
| acadrust::EntityType::Insert(_)
|
| acadrust::EntityType::Insert(_)
|
||||||
|
| acadrust::EntityType::Leader(_)
|
||||||
| acadrust::EntityType::Hatch(_)
|
| acadrust::EntityType::Hatch(_)
|
||||||
| acadrust::EntityType::Dimension(_) => set_row_value(
|
| acadrust::EntityType::Dimension(_) => set_row_value(
|
||||||
&mut sections,
|
&mut sections,
|
||||||
@@ -2474,10 +2537,40 @@ impl OpenCADStudio {
|
|||||||
/// (or `None` if it could not be added). Lets callers follow up — e.g.
|
/// (or `None` if it could not be added). Lets callers follow up — e.g.
|
||||||
/// open the in-place text editor on a freshly created MultiLeader.
|
/// open the in-place text editor on a freshly created MultiLeader.
|
||||||
pub(super) fn commit_entity_handle(
|
pub(super) fn commit_entity_handle(
|
||||||
|
&mut self,
|
||||||
|
entity: acadrust::EntityType,
|
||||||
|
) -> Option<Handle> {
|
||||||
|
self.commit_entity_handle_with_dimension_policy(entity, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commit an entity, optionally preserving its source dimension style.
|
||||||
|
pub(super) fn commit_entity_handle_with_dimension_policy(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut entity: acadrust::EntityType,
|
mut entity: acadrust::EntityType,
|
||||||
|
preserve_dimension_layer_and_style: bool,
|
||||||
) -> Option<Handle> {
|
) -> Option<Handle> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
let tracks_dimension_chain = matches!(
|
||||||
|
&entity,
|
||||||
|
acadrust::EntityType::Dimension(
|
||||||
|
acadrust::entities::Dimension::Linear(_)
|
||||||
|
| acadrust::entities::Dimension::Aligned(_)
|
||||||
|
| acadrust::entities::Dimension::Angular2Ln(_)
|
||||||
|
| acadrust::entities::Dimension::Angular3Pt(_)
|
||||||
|
| acadrust::entities::Dimension::Ordinate(_)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let inherited_dimension = if preserve_dimension_layer_and_style {
|
||||||
|
match &entity {
|
||||||
|
acadrust::EntityType::Dimension(dimension) => Some((
|
||||||
|
dimension.base().common.layer.clone(),
|
||||||
|
dimension.base().style_name.clone(),
|
||||||
|
)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
if let acadrust::EntityType::Table(table) = &mut entity {
|
if let acadrust::EntityType::Table(table) = &mut entity {
|
||||||
const PREFIX: &str = "__OPENCAD_LINK_PENDING__";
|
const PREFIX: &str = "__OPENCAD_LINK_PENDING__";
|
||||||
if let Some(path) = table.name.strip_prefix(PREFIX).map(str::to_string) {
|
if let Some(path) = table.name.strip_prefix(PREFIX).map(str::to_string) {
|
||||||
@@ -2533,9 +2626,21 @@ impl OpenCADStudio {
|
|||||||
| acadrust::EntityType::Polyline2D(_)
|
| acadrust::EntityType::Polyline2D(_)
|
||||||
| acadrust::EntityType::Polyline3D(_)
|
| acadrust::EntityType::Polyline3D(_)
|
||||||
);
|
);
|
||||||
let layer = &self.tabs[i].active_layer;
|
let explicit_mleader_layer = match &entity {
|
||||||
if layer != "0" || entity.as_entity().layer().is_empty() {
|
acadrust::EntityType::MultiLeader(leader) => leader
|
||||||
entity.as_entity_mut().set_layer(layer.clone());
|
.common
|
||||||
|
.layer
|
||||||
|
.strip_prefix("__MLEADER_LAYER__")
|
||||||
|
.map(str::to_owned),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(layer) = explicit_mleader_layer {
|
||||||
|
entity.as_entity_mut().set_layer(layer);
|
||||||
|
} else {
|
||||||
|
let layer = &self.tabs[i].active_layer;
|
||||||
|
if layer != "0" || entity.as_entity().layer().is_empty() {
|
||||||
|
entity.as_entity_mut().set_layer(layer.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// INSUNITS: when inserting a block whose BlockRecord.units differ
|
// INSUNITS: when inserting a block whose BlockRecord.units differ
|
||||||
@@ -2579,6 +2684,12 @@ impl OpenCADStudio {
|
|||||||
&self.tabs[i].scene.document,
|
&self.tabs[i].scene.document,
|
||||||
&mut entity,
|
&mut entity,
|
||||||
);
|
);
|
||||||
|
if let (Some((layer, style_name)), acadrust::EntityType::Dimension(dimension)) =
|
||||||
|
(inherited_dimension, &mut entity)
|
||||||
|
{
|
||||||
|
dimension.base_mut().common.layer = layer;
|
||||||
|
dimension.base_mut().style_name = style_name;
|
||||||
|
}
|
||||||
|
|
||||||
// Smart centre objects carry their own drawing-level creation style.
|
// Smart centre objects carry their own drawing-level creation style.
|
||||||
// Apply it after the generic ribbon style so ordinary LINE entities
|
// Apply it after the generic ribbon style so ordinary LINE entities
|
||||||
@@ -2738,6 +2849,9 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].last_draw_anchor = Some(handle);
|
self.tabs[i].last_draw_anchor = Some(handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if tracks_dimension_chain {
|
||||||
|
self.tabs[i].scene.last_created_dimension = new_handle;
|
||||||
|
}
|
||||||
new_handle
|
new_handle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2768,8 +2882,13 @@ fn make_sections_read_only(
|
|||||||
acadrust::types::Color::Index(index) => index.to_string(),
|
acadrust::types::Color::Index(index) => index.to_string(),
|
||||||
acadrust::types::Color::Rgb { r, g, b } => format!("{r},{g},{b}"),
|
acadrust::types::Color::Rgb { r, g, b } => format!("{r},{g},{b}"),
|
||||||
},
|
},
|
||||||
PropValue::ColorVaries | PropValue::LwVaries => VARIES_LABEL.to_string(),
|
PropValue::ColorVaries
|
||||||
PropValue::LwChoice(lineweight) => {
|
| PropValue::LwVaries
|
||||||
|
| PropValue::FieldLwVaries { .. } => VARIES_LABEL.to_string(),
|
||||||
|
PropValue::LwChoice(lineweight)
|
||||||
|
| PropValue::FieldLwChoice {
|
||||||
|
value: lineweight, ..
|
||||||
|
} => {
|
||||||
ui::properties::LwItem(*lineweight).to_string()
|
ui::properties::LwItem(*lineweight).to_string()
|
||||||
}
|
}
|
||||||
PropValue::BoolToggle { value, .. } => {
|
PropValue::BoolToggle { value, .. } => {
|
||||||
@@ -2927,6 +3046,17 @@ fn merge_prop_value(
|
|||||||
(PropValue::LwChoice(_), PropValue::LwChoice(_))
|
(PropValue::LwChoice(_), PropValue::LwChoice(_))
|
||||||
| (PropValue::LwVaries, _)
|
| (PropValue::LwVaries, _)
|
||||||
| (_, PropValue::LwVaries) => PropValue::LwVaries,
|
| (_, PropValue::LwVaries) => PropValue::LwVaries,
|
||||||
|
(
|
||||||
|
PropValue::FieldLwChoice { field, .. },
|
||||||
|
PropValue::FieldLwChoice {
|
||||||
|
field: other_field,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) if field == other_field => PropValue::FieldLwVaries { field },
|
||||||
|
(PropValue::FieldLwVaries { field }, _)
|
||||||
|
| (_, PropValue::FieldLwVaries { field }) => {
|
||||||
|
PropValue::FieldLwVaries { field }
|
||||||
|
}
|
||||||
(PropValue::LinetypeChoice(_), PropValue::LinetypeChoice(_)) => {
|
(PropValue::LinetypeChoice(_), PropValue::LinetypeChoice(_)) => {
|
||||||
PropValue::LinetypeChoice(VARIES_LABEL.into())
|
PropValue::LinetypeChoice(VARIES_LABEL.into())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,6 +198,13 @@ pub struct UserSettings {
|
|||||||
pub osmode: i32,
|
pub osmode: i32,
|
||||||
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
||||||
pub texteditmode: bool,
|
pub texteditmode: bool,
|
||||||
|
/// QDIM extension-origin priority: 0 = endpoints, 1 = intersections.
|
||||||
|
#[serde(default)]
|
||||||
|
pub quick_dimension_snap_priority: u8,
|
||||||
|
/// DIMCONTINUEMODE: 1 inherits the base dimension's layer/style; 0 uses
|
||||||
|
/// the current creation layer/style. Registry-style, app-level preference.
|
||||||
|
#[serde(default = "default_dimension_continue_mode")]
|
||||||
|
pub dimension_continue_mode: i16,
|
||||||
/// TEXTFILL: fill TrueType glyphs (true) or draw them hollow (false).
|
/// TEXTFILL: fill TrueType glyphs (true) or draw them hollow (false).
|
||||||
pub textfill: bool,
|
pub textfill: bool,
|
||||||
/// When true, saving over an existing file first copies it to a sibling
|
/// When true, saving over an existing file first copies it to a sibling
|
||||||
@@ -247,6 +254,10 @@ fn default_clipromptlines() -> i32 {
|
|||||||
3
|
3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_dimension_continue_mode() -> i16 {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
fn deserialize_clipromptlines<'de, D>(de: D) -> Result<i32, D::Error>
|
fn deserialize_clipromptlines<'de, D>(de: D) -> Result<i32, D::Error>
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
@@ -284,6 +295,8 @@ impl Default for UserSettings {
|
|||||||
// off (suppress bit 16384).
|
// off (suppress bit 16384).
|
||||||
osmode: 575 | OSMODE_SUPPRESS,
|
osmode: 575 | OSMODE_SUPPRESS,
|
||||||
texteditmode: false,
|
texteditmode: false,
|
||||||
|
quick_dimension_snap_priority: 0,
|
||||||
|
dimension_continue_mode: 1,
|
||||||
textfill: true,
|
textfill: true,
|
||||||
backup_on_save: true,
|
backup_on_save: true,
|
||||||
file_assoc_enabled: true,
|
file_assoc_enabled: true,
|
||||||
|
|||||||
@@ -2053,6 +2053,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
| "text_style_handle"
|
| "text_style_handle"
|
||||||
| "arrowhead_handle"
|
| "arrowhead_handle"
|
||||||
| "line_type_handle"
|
| "line_type_handle"
|
||||||
|
| "block_content_handle"
|
||||||
) {
|
) {
|
||||||
// Resolve a picked name back to the handle the MLEADER
|
// Resolve a picked name back to the handle the MLEADER
|
||||||
// stores. The style/text-style rows keep their existing
|
// stores. The style/text-style rows keep their existing
|
||||||
@@ -2098,6 +2099,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
.map(|l| l.handle)
|
.map(|l| l.handle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"block_content_handle" => doc
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.find(|block| block.name == value)
|
||||||
|
.map(|block| block.handle),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
for &handle in &handles {
|
for &handle in &handles {
|
||||||
@@ -2121,10 +2127,52 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
"text_style_handle" => {
|
"text_style_handle" => {
|
||||||
if let Some(h) = resolved {
|
if let Some(h) = resolved {
|
||||||
ml.text_style_handle = Some(h);
|
ml.text_style_handle = Some(h);
|
||||||
|
ml.context.text_style_handle = Some(h);
|
||||||
|
ml.property_override_flags.insert(
|
||||||
|
acadrust::entities::MultiLeaderPropertyOverrideFlags::TEXT_STYLE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"arrowhead_handle" => {
|
||||||
|
ml.arrowhead_handle = resolved;
|
||||||
|
for root in &mut ml.context.leader_roots {
|
||||||
|
for line in &mut root.lines {
|
||||||
|
line.arrowhead_handle = resolved;
|
||||||
|
line.override_flags.insert(
|
||||||
|
acadrust::entities::LeaderLinePropertyOverrideFlags::ARROWHEAD,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ml.property_override_flags.insert(
|
||||||
|
acadrust::entities::MultiLeaderPropertyOverrideFlags::ARROWHEAD,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"line_type_handle" => {
|
||||||
|
ml.line_type_handle = resolved;
|
||||||
|
for root in &mut ml.context.leader_roots {
|
||||||
|
for line in &mut root.lines {
|
||||||
|
line.line_type_handle = resolved;
|
||||||
|
line.override_flags.insert(
|
||||||
|
acadrust::entities::LeaderLinePropertyOverrideFlags::LINE_TYPE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ml.property_override_flags.insert(
|
||||||
|
acadrust::entities::MultiLeaderPropertyOverrideFlags::LEADER_LINE_TYPE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"block_content_handle" => {
|
||||||
|
if let Some(block_handle) = resolved {
|
||||||
|
ml.block_content_handle = Some(block_handle);
|
||||||
|
ml.context.block_content_handle = Some(block_handle);
|
||||||
|
ml.context.has_block_contents = true;
|
||||||
|
ml.context.block_content_location =
|
||||||
|
ml.context.content_base_point;
|
||||||
|
ml.property_override_flags.insert(
|
||||||
|
acadrust::entities::MultiLeaderPropertyOverrideFlags::BLOCK_CONTENT,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"arrowhead_handle" => ml.arrowhead_handle = resolved,
|
|
||||||
"line_type_handle" => ml.line_type_handle = resolved,
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -382,6 +382,8 @@ impl OpenCADStudio {
|
|||||||
self.snapper.snap_enabled,
|
self.snapper.snap_enabled,
|
||||||
),
|
),
|
||||||
texteditmode: self.texteditmode,
|
texteditmode: self.texteditmode,
|
||||||
|
quick_dimension_snap_priority: self.quick_dimension_snap_priority,
|
||||||
|
dimension_continue_mode: self.dimension_continue_mode,
|
||||||
textfill: crate::scene::text::sdf_atlas::textfill(),
|
textfill: crate::scene::text::sdf_atlas::textfill(),
|
||||||
backup_on_save: self.backup_on_save,
|
backup_on_save: self.backup_on_save,
|
||||||
file_assoc_enabled: self.file_assoc_enabled,
|
file_assoc_enabled: self.file_assoc_enabled,
|
||||||
@@ -440,6 +442,8 @@ impl OpenCADStudio {
|
|||||||
self.snapper.enabled = modes.into_iter().collect();
|
self.snapper.enabled = modes.into_iter().collect();
|
||||||
self.snapper.snap_enabled = snap_enabled;
|
self.snapper.snap_enabled = snap_enabled;
|
||||||
self.texteditmode = s.texteditmode;
|
self.texteditmode = s.texteditmode;
|
||||||
|
self.quick_dimension_snap_priority = s.quick_dimension_snap_priority.min(1);
|
||||||
|
self.dimension_continue_mode = s.dimension_continue_mode.clamp(0, 1);
|
||||||
crate::scene::text::sdf_atlas::set_textfill(s.textfill);
|
crate::scene::text::sdf_atlas::set_textfill(s.textfill);
|
||||||
self.backup_on_save = s.backup_on_save;
|
self.backup_on_save = s.backup_on_save;
|
||||||
self.file_assoc_enabled = s.file_assoc_enabled;
|
self.file_assoc_enabled = s.file_assoc_enabled;
|
||||||
|
|||||||
+81
-1
@@ -4657,6 +4657,32 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::PropFieldLwChanged { field, value } => {
|
||||||
|
let i = self.active_tab;
|
||||||
|
let handles = self.property_target_handles(i);
|
||||||
|
self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
|
||||||
|
if let Some(acadrust::EntityType::MultiLeader(leader)) =
|
||||||
|
app.tabs[i].scene.document.get_entity_mut(handle)
|
||||||
|
{
|
||||||
|
if field == "line_weight" {
|
||||||
|
leader.line_weight = value;
|
||||||
|
leader.property_override_flags.insert(
|
||||||
|
acadrust::entities::MultiLeaderPropertyOverrideFlags::LEADER_LINE_WEIGHT,
|
||||||
|
);
|
||||||
|
for root in &mut leader.context.leader_roots {
|
||||||
|
for line in &mut root.lines {
|
||||||
|
line.line_weight = value;
|
||||||
|
line.override_flags.insert(
|
||||||
|
acadrust::entities::LeaderLinePropertyOverrideFlags::LINE_WEIGHT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
Message::PropLinetypeChanged(lt) => {
|
Message::PropLinetypeChanged(lt) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let handles = self.property_target_handles(i);
|
let handles = self.property_target_handles(i);
|
||||||
@@ -4896,12 +4922,18 @@ impl OpenCADStudio {
|
|||||||
Message::PropVertexStep(delta) => {
|
Message::PropVertexStep(delta) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let handles = self.property_target_handles(i);
|
let handles = self.property_target_handles(i);
|
||||||
// Vertex navigation applies to a single selected polyline.
|
// Navigate the active vertex or table cell.
|
||||||
let n = if handles.len() == 1 {
|
let n = if handles.len() == 1 {
|
||||||
match self.tabs[i].scene.document.get_entity(handles[0]) {
|
match self.tabs[i].scene.document.get_entity(handles[0]) {
|
||||||
Some(acadrust::EntityType::LwPolyline(p)) => p.vertices.len(),
|
Some(acadrust::EntityType::LwPolyline(p)) => p.vertices.len(),
|
||||||
Some(acadrust::EntityType::Polyline2D(p)) => p.vertices.len(),
|
Some(acadrust::EntityType::Polyline2D(p)) => p.vertices.len(),
|
||||||
Some(acadrust::EntityType::PolygonMesh(p)) => p.vertices.len(),
|
Some(acadrust::EntityType::PolygonMesh(p)) => p.vertices.len(),
|
||||||
|
Some(acadrust::EntityType::Face3D(face)) => {
|
||||||
|
if face.is_triangle() { 3 } else { 4 }
|
||||||
|
}
|
||||||
|
Some(acadrust::EntityType::Polyline3D(p)) => {
|
||||||
|
crate::entities::polyline::polyline3d_control_vertex_count(p)
|
||||||
|
}
|
||||||
Some(acadrust::EntityType::Table(table)) => {
|
Some(acadrust::EntityType::Table(table)) => {
|
||||||
table.row_count().saturating_mul(table.column_count())
|
table.row_count().saturating_mul(table.column_count())
|
||||||
}
|
}
|
||||||
@@ -5115,6 +5147,54 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
if matches!(
|
||||||
|
field.as_str(),
|
||||||
|
"line_color" | "text_color" | "block_content_color"
|
||||||
|
| "background_fill_color"
|
||||||
|
) {
|
||||||
|
self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
|
||||||
|
if let Some(acadrust::EntityType::MultiLeader(leader)) =
|
||||||
|
app.tabs[i].scene.document.get_entity_mut(handle)
|
||||||
|
{
|
||||||
|
match field.as_str() {
|
||||||
|
"line_color" => {
|
||||||
|
leader.line_color = color;
|
||||||
|
leader.property_override_flags.insert(
|
||||||
|
acadrust::entities::MultiLeaderPropertyOverrideFlags::LINE_COLOR,
|
||||||
|
);
|
||||||
|
for root in &mut leader.context.leader_roots {
|
||||||
|
for line in &mut root.lines {
|
||||||
|
line.line_color = color;
|
||||||
|
line.override_flags.insert(
|
||||||
|
acadrust::entities::LeaderLinePropertyOverrideFlags::LINE_COLOR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"text_color" => {
|
||||||
|
leader.text_color = color;
|
||||||
|
leader.context.text_color = color;
|
||||||
|
leader.property_override_flags.insert(
|
||||||
|
acadrust::entities::MultiLeaderPropertyOverrideFlags::TEXT_COLOR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"block_content_color" => {
|
||||||
|
leader.block_content_color = color;
|
||||||
|
leader.context.block_content_color = color;
|
||||||
|
leader.property_override_flags.insert(
|
||||||
|
acadrust::entities::MultiLeaderPropertyOverrideFlags::BLOCK_CONTENT_COLOR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"background_fill_color" => {
|
||||||
|
leader.context.background_fill_color = color;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.tabs[i].properties.open_color_field = None;
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
if matches!(
|
if matches!(
|
||||||
field.as_str(),
|
field.as_str(),
|
||||||
"tbl_cell_content_color" | "tbl_cell_background_color"
|
"tbl_cell_content_color" | "tbl_cell_background_color"
|
||||||
|
|||||||
+4
-1
@@ -432,7 +432,10 @@ impl OpenCADStudio {
|
|||||||
let indexed = match tab.scene.document.get_entity(h) {
|
let indexed = match tab.scene.document.get_entity(h) {
|
||||||
Some(acadrust::EntityType::LwPolyline(_))
|
Some(acadrust::EntityType::LwPolyline(_))
|
||||||
| Some(acadrust::EntityType::Polyline2D(_))
|
| Some(acadrust::EntityType::Polyline2D(_))
|
||||||
| Some(acadrust::EntityType::Spline(_)) => true,
|
| Some(acadrust::EntityType::Polyline3D(_))
|
||||||
|
| Some(acadrust::EntityType::Spline(_))
|
||||||
|
| Some(acadrust::EntityType::Face3D(_))
|
||||||
|
| Some(acadrust::EntityType::PolygonMesh(_)) => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
indexed.then_some(tab.properties.prop_vertex)
|
indexed.then_some(tab.properties.prop_vertex)
|
||||||
|
|||||||
@@ -1248,6 +1248,28 @@ pub enum CmdResult {
|
|||||||
CommitDimension {
|
CommitDimension {
|
||||||
entity: EntityType,
|
entity: EntityType,
|
||||||
association: DimensionAssociationInput,
|
association: DimensionAssociationInput,
|
||||||
|
/// Retain the source dimension's layer and style instead of replacing
|
||||||
|
/// them with the current creation defaults (DIMCONTINUEMODE=1).
|
||||||
|
preserve_base_style: bool,
|
||||||
|
/// Keep collecting points after the dimension is committed.
|
||||||
|
continue_command: bool,
|
||||||
|
},
|
||||||
|
/// Commit several dimensions with independent association sources in one
|
||||||
|
/// undo step, then end the command.
|
||||||
|
CommitDimensionsAndExit(Vec<(EntityType, DimensionAssociationInput)>),
|
||||||
|
/// Persist the quick-dimension extension-origin priority while keeping the
|
||||||
|
/// active command at its current prompt (0 = endpoints, 1 = intersections).
|
||||||
|
SetQuickDimensionSnapPriority(u8),
|
||||||
|
/// Align multileader content points along a picked infinite line.
|
||||||
|
AlignMLeaders {
|
||||||
|
handles: Vec<Handle>,
|
||||||
|
from: DVec3,
|
||||||
|
to: DVec3,
|
||||||
|
},
|
||||||
|
/// Merge compatible block multileaders at a picked content point.
|
||||||
|
CollectMLeaders {
|
||||||
|
handles: Vec<Handle>,
|
||||||
|
point: DVec3,
|
||||||
},
|
},
|
||||||
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
|
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
|
||||||
/// persistence) plus its B-rep (cached for boolean ops + shaded
|
/// persistence) plus its B-rep (cached for boolean ops + shaded
|
||||||
@@ -1930,6 +1952,9 @@ pub trait CadCommand: Send {
|
|||||||
/// Commands that stay active across replaces should update their internal snapshots here.
|
/// Commands that stay active across replaces should update their internal snapshots here.
|
||||||
fn on_entity_replaced(&mut self, _old: Handle, _new_handles: &[Handle]) {}
|
fn on_entity_replaced(&mut self, _old: Handle, _new_handles: &[Handle]) {}
|
||||||
|
|
||||||
|
/// Called after a PEDIT operation changed its target.
|
||||||
|
fn on_pedit_applied(&mut self) {}
|
||||||
|
|
||||||
/// Consume a lasso or drag-box gesture while the command is active.
|
/// Consume a lasso or drag-box gesture while the command is active.
|
||||||
/// `fence` is the gesture boundary in drawing coordinates; `window` is
|
/// `fence` is the gesture boundary in drawing coordinates; `window` is
|
||||||
/// present for rectangular gestures. Returning `Some` prevents the normal
|
/// present for rectangular gestures. Returning `Some` prevents the normal
|
||||||
|
|||||||
+405
-82
@@ -1,106 +1,427 @@
|
|||||||
// Helix entity (AcDbHelix): a spline curve plus its generating parameters.
|
use acadrust::entities::{Helix, HelixConstraint, Spline};
|
||||||
//
|
use acadrust::types::Vector3;
|
||||||
// A loaded helix carries a fully evaluated `spline` (the NURBS the file
|
use cadkernel::space::{HelixCurve, HelixDirection, NurbsCurve3, Vec3};
|
||||||
// stored) alongside the parameters that generated it (base point, axis,
|
use glam::DVec3;
|
||||||
// radius, turns, turn height, twist, constraint). We render and grip the
|
|
||||||
// entity through its embedded spline, and expose the generating parameters
|
|
||||||
// as the Geometry property group.
|
|
||||||
|
|
||||||
use acadrust::entities::{Helix, HelixConstraint};
|
|
||||||
use crate::t;
|
|
||||||
|
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::entities::common::ro_prop as ro;
|
use crate::entities::common::{
|
||||||
use crate::entities::traits::{Grippable, Transformable, RenderConvertible};
|
center_grip, edit_prop as edit, format_angle, format_length, parse_f64, ro_prop as ro,
|
||||||
|
square_grip,
|
||||||
|
};
|
||||||
|
use crate::entities::traits::{Grippable, RenderConvertible, Transformable};
|
||||||
use crate::scene::convert::acad_to_render::RenderEntity;
|
use crate::scene::convert::acad_to_render::RenderEntity;
|
||||||
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||||
|
use crate::t;
|
||||||
|
|
||||||
/// Perpendicular distance from `p` to the helix axis (line through
|
const EPSILON: f64 = 1.0e-9;
|
||||||
/// `axis_base_point` along `axis_vector`). Used to recover the top radius
|
|
||||||
/// from the last defining point of the spline.
|
fn point(vector: Vector3) -> Vec3 {
|
||||||
fn radius_from_axis(helix: &Helix, p: &acadrust::types::Vector3) -> Option<f64> {
|
Vec3::new(vector.x, vector.y, vector.z)
|
||||||
let base = &helix.axis_base_point;
|
}
|
||||||
let axis = helix.axis_vector.normalize();
|
|
||||||
// A zero axis vector cannot define a line.
|
fn vector(value: Vec3) -> Vector3 {
|
||||||
if axis.length() < 1e-9 {
|
Vector3::new(value.x, value.y, value.z)
|
||||||
return None;
|
}
|
||||||
|
|
||||||
|
fn display(value: Vec3) -> DVec3 {
|
||||||
|
DVec3::new(value.x, value.y, value.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kernel(value: DVec3) -> Vec3 {
|
||||||
|
Vec3::new(value.x, value.y, value.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finite(value: Vec3) -> bool {
|
||||||
|
value.x.is_finite() && value.y.is_finite() && value.z.is_finite()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spline_curve(spline: &Spline) -> Option<NurbsCurve3> {
|
||||||
|
let controls: Vec<[f64; 3]> = spline
|
||||||
|
.control_points
|
||||||
|
.iter()
|
||||||
|
.map(|value| [value.x, value.y, value.z])
|
||||||
|
.collect();
|
||||||
|
let degree = usize::try_from(spline.degree).ok()?;
|
||||||
|
let weights = if spline.weights.is_empty() {
|
||||||
|
vec![1.0; controls.len()]
|
||||||
|
} else {
|
||||||
|
spline.weights.clone()
|
||||||
|
};
|
||||||
|
NurbsCurve3::new_strict(degree, controls, spline.knots.clone(), weights)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn axis_frame(helix: &Helix) -> Option<(Vec3, Vec3, Vec3)> {
|
||||||
|
let base = point(helix.axis_base_point);
|
||||||
|
let axis = point(helix.axis_vector).normalize()?;
|
||||||
|
let radial = point(helix.start_point) - base;
|
||||||
|
let start_direction = (radial - axis * radial.dot(axis)).normalize()?;
|
||||||
|
Some((base, axis, start_direction))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn radius_from_axis(helix: &Helix, value: Vec3) -> Option<f64> {
|
||||||
|
let (base, axis, _) = axis_frame(helix)?;
|
||||||
|
let delta = value - base;
|
||||||
|
Some((delta - axis * delta.dot(axis)).length())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn top_radius(helix: &Helix) -> f64 {
|
||||||
|
spline_curve(&helix.spline)
|
||||||
|
.map(|curve| Vec3::from(curve.point_at(1.0)))
|
||||||
|
.and_then(|endpoint| radius_from_axis(helix, endpoint))
|
||||||
|
.unwrap_or(helix.radius)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kernel_curve(helix: &Helix, top_radius: f64) -> Option<HelixCurve> {
|
||||||
|
let (base, axis, start_direction) = axis_frame(helix)?;
|
||||||
|
Some(HelixCurve {
|
||||||
|
base_center: base.to_array(),
|
||||||
|
axis_direction: axis.to_array(),
|
||||||
|
start_direction: start_direction.to_array(),
|
||||||
|
base_radius: helix.radius,
|
||||||
|
top_radius,
|
||||||
|
height: helix.turns * helix.turn_height,
|
||||||
|
turns: helix.turns,
|
||||||
|
direction: if helix.handedness {
|
||||||
|
HelixDirection::CounterClockwise
|
||||||
|
} else {
|
||||||
|
HelixDirection::Clockwise
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn projected_direction(direction: Vec3, axis: Vec3) -> Option<Vec3> {
|
||||||
|
(direction - axis * direction.dot(axis)).normalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perpendicular(axis: Vec3) -> Option<Vec3> {
|
||||||
|
let basis = if axis.x.abs() <= axis.y.abs() && axis.x.abs() <= axis.z.abs() {
|
||||||
|
Vec3::X
|
||||||
|
} else if axis.y.abs() <= axis.z.abs() {
|
||||||
|
Vec3::Y
|
||||||
|
} else {
|
||||||
|
Vec3::Z
|
||||||
|
};
|
||||||
|
projected_direction(basis, axis)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(helix: &mut Helix, top_radius: f64) -> bool {
|
||||||
|
let Some(curve) = kernel_curve(helix, top_radius) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(nurbs) = curve.nurbs() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
helix.spline.degree = nurbs.degree() as i32;
|
||||||
|
helix.spline.knots = nurbs.knots().to_vec();
|
||||||
|
helix.spline.control_points = nurbs
|
||||||
|
.control_points()
|
||||||
|
.iter()
|
||||||
|
.map(|value| Vector3::new(value[0], value[1], value[2]))
|
||||||
|
.collect();
|
||||||
|
helix.spline.weights = if nurbs.is_rational() {
|
||||||
|
nurbs.weights().to_vec()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
helix.spline.fit_points.clear();
|
||||||
|
helix.spline.flags.closed = false;
|
||||||
|
helix.spline.flags.periodic = false;
|
||||||
|
helix.spline.flags.rational = nurbs.is_rational();
|
||||||
|
helix.spline.flags.planar = false;
|
||||||
|
helix.spline.flags.linear = false;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn choice(label: &str, field: &'static str, selected: &str, options: &[String]) -> Property {
|
||||||
|
Property {
|
||||||
|
label: label.to_string(),
|
||||||
|
field,
|
||||||
|
value: PropValue::Choice {
|
||||||
|
selected: selected.to_string(),
|
||||||
|
options: options.to_vec(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
let vx = p.x - base.x;
|
|
||||||
let vy = p.y - base.y;
|
|
||||||
let vz = p.z - base.z;
|
|
||||||
let proj = vx * axis.x + vy * axis.y + vz * axis.z;
|
|
||||||
let px = vx - proj * axis.x;
|
|
||||||
let py = vy - proj * axis.y;
|
|
||||||
let pz = vz - proj * axis.z;
|
|
||||||
Some((px * px + py * py + pz * pz).sqrt())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn grips(helix: &Helix) -> Vec<GripDef> {
|
fn grips(helix: &Helix) -> Vec<GripDef> {
|
||||||
Grippable::grips(&helix.spline)
|
let Some((base, axis, _)) = axis_frame(helix) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let height = helix.turns * helix.turn_height;
|
||||||
|
let top_center = base + axis * height;
|
||||||
|
let top_endpoint = spline_curve(&helix.spline)
|
||||||
|
.map(|curve| Vec3::from(curve.point_at(1.0)))
|
||||||
|
.unwrap_or(top_center);
|
||||||
|
vec![
|
||||||
|
center_grip(0, display(base)),
|
||||||
|
square_grip(1, display(point(helix.start_point))),
|
||||||
|
square_grip(2, display(top_center)),
|
||||||
|
square_grip(3, display(top_endpoint)),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn properties(helix: &Helix) -> Vec<PropSection> {
|
fn properties(helix: &Helix) -> Vec<PropSection> {
|
||||||
let bp = &helix.axis_base_point;
|
let base = point(helix.axis_base_point);
|
||||||
let turns = helix.turns;
|
let height = helix.turns * helix.turn_height;
|
||||||
let turn_height = helix.turn_height;
|
let top_radius = top_radius(helix);
|
||||||
// Overall height is turns × per-turn rise.
|
let curve = kernel_curve(helix, top_radius);
|
||||||
let height = turns * turn_height;
|
let turn_slope = curve.as_ref().and_then(HelixCurve::turn_slope).unwrap_or(0.0);
|
||||||
let base_radius = helix.radius;
|
let total_length = curve.as_ref().and_then(HelixCurve::length).unwrap_or(0.0);
|
||||||
// Top radius: the axial distance of the last defining point. For a
|
|
||||||
// cylindrical helix this equals the base radius.
|
|
||||||
let top_radius = helix
|
|
||||||
.spline
|
|
||||||
.control_points
|
|
||||||
.last()
|
|
||||||
.or_else(|| helix.spline.fit_points.last())
|
|
||||||
.and_then(|p| radius_from_axis(helix, p))
|
|
||||||
.unwrap_or(base_radius);
|
|
||||||
// Turn slope: rise angle of a single turn against the base circumference.
|
|
||||||
let turn_slope = if base_radius.abs() > 1e-9 {
|
|
||||||
(turn_height / (std::f64::consts::TAU * base_radius))
|
|
||||||
.atan()
|
|
||||||
.to_degrees()
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
let twist = if helix.handedness { "CW" } else { "CCW" };
|
|
||||||
let constrain = match helix.constraint {
|
let constrain = match helix.constraint {
|
||||||
HelixConstraint::TurnHeight => "Turn Height",
|
HelixConstraint::TurnHeight => t!("Turn Height").into_owned(),
|
||||||
HelixConstraint::Turns => "Turns",
|
HelixConstraint::Turns => t!("Turns").into_owned(),
|
||||||
HelixConstraint::Height => "Height",
|
HelixConstraint::Height => t!("Height").into_owned(),
|
||||||
};
|
};
|
||||||
|
let twist = if helix.handedness {
|
||||||
|
t!("CCW").into_owned()
|
||||||
|
} else {
|
||||||
|
t!("CW").into_owned()
|
||||||
|
};
|
||||||
|
let constraint_options = vec![
|
||||||
|
t!("Turn Height").into_owned(),
|
||||||
|
t!("Turns").into_owned(),
|
||||||
|
t!("Height").into_owned(),
|
||||||
|
];
|
||||||
|
let twist_options = vec![t!("CW").into_owned(), t!("CCW").into_owned()];
|
||||||
|
|
||||||
vec![PropSection {
|
vec![PropSection {
|
||||||
title: t!("Geometry").into_owned(),
|
title: t!("Geometry").into_owned(),
|
||||||
props: vec![
|
props: vec![
|
||||||
ro(t!("Base point X").as_ref(), "base_x", format!("{:.4}", bp.x)),
|
edit(t!("Position X").as_ref(), "position_x", base.x),
|
||||||
ro(t!("Base point Y").as_ref(), "base_y", format!("{:.4}", bp.y)),
|
edit(t!("Position Y").as_ref(), "position_y", base.y),
|
||||||
ro(t!("Base point Z").as_ref(), "base_z", format!("{:.4}", bp.z)),
|
edit(t!("Position Z").as_ref(), "position_z", base.z),
|
||||||
ro(t!("Number of turns").as_ref(), "num_turns", format!("{turns:.4}")),
|
choice(t!("Constrain").as_ref(), "constrain", &constrain, &constraint_options),
|
||||||
ro(t!("Turn height").as_ref(), "turn_height", format!("{turn_height:.4}")),
|
edit(t!("Height").as_ref(), "height", height),
|
||||||
ro(t!("Turns").as_ref(), "turns", format!("{turns:.4}")),
|
edit(t!("Turns").as_ref(), "turns", helix.turns),
|
||||||
ro(t!("Height").as_ref(), "height", format!("{height:.4}")),
|
edit(t!("Turn height").as_ref(), "turn_height", helix.turn_height),
|
||||||
ro(t!("Base radius").as_ref(), "base_radius", format!("{base_radius:.4}")),
|
edit(t!("Base radius").as_ref(), "base_radius", helix.radius),
|
||||||
ro(t!("Top radius").as_ref(), "top_radius", format!("{top_radius:.4}")),
|
edit(t!("Top radius").as_ref(), "top_radius", top_radius),
|
||||||
ro(t!("Turn slope").as_ref(), "turn_slope", format!("{turn_slope:.4}")),
|
choice(t!("Twist").as_ref(), "twist", &twist, &twist_options),
|
||||||
ro(t!("Twist").as_ref(), "twist", twist),
|
ro(t!("Turn slope").as_ref(), "turn_slope", format_angle(turn_slope)),
|
||||||
ro(t!("Constrain").as_ref(), "constrain", constrain),
|
ro(t!("Total length").as_ref(), "total_length", format_length(total_length)),
|
||||||
],
|
],
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helix geometry is derived from the stored spline; the generating
|
fn apply_geom_prop(helix: &mut Helix, field: &str, value: &str) {
|
||||||
/// parameters are read-only (editing one would require regenerating the
|
if field == "constrain" {
|
||||||
/// spline, which the loaded entity does not carry a recipe for).
|
let constraint = if value == t!("Height").as_ref() {
|
||||||
fn apply_geom_prop(_helix: &mut Helix, _field: &str, _value: &str) {}
|
HelixConstraint::Height
|
||||||
|
} else if value == t!("Turns").as_ref() {
|
||||||
fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) {
|
HelixConstraint::Turns
|
||||||
Grippable::apply_grip(&mut helix.spline, grip_id, apply);
|
} else if value == t!("Turn Height").as_ref() {
|
||||||
|
HelixConstraint::TurnHeight
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
helix.constraint = constraint;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let original = helix.clone();
|
||||||
|
let top = top_radius(helix);
|
||||||
|
if field == "twist" {
|
||||||
|
if value == t!("CCW").as_ref() {
|
||||||
|
helix.handedness = true;
|
||||||
|
} else if value == t!("CW").as_ref() {
|
||||||
|
helix.handedness = false;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !rebuild(helix, top) {
|
||||||
|
*helix = original;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(number) = parse_f64(value).filter(|number| number.is_finite()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let old_base = point(helix.axis_base_point);
|
||||||
|
let old_height = helix.turns * helix.turn_height;
|
||||||
|
match field {
|
||||||
|
"position_x" | "position_y" | "position_z" => {
|
||||||
|
let mut base = old_base;
|
||||||
|
match field {
|
||||||
|
"position_x" => base.x = number,
|
||||||
|
"position_y" => base.y = number,
|
||||||
|
_ => base.z = number,
|
||||||
|
}
|
||||||
|
let delta = base - old_base;
|
||||||
|
helix.axis_base_point = vector(base);
|
||||||
|
helix.start_point = vector(point(helix.start_point) + delta);
|
||||||
|
}
|
||||||
|
"height" if number.abs() > EPSILON => {
|
||||||
|
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
|
||||||
|
let turns = number / helix.turn_height;
|
||||||
|
if turns <= EPSILON {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
helix.turns = turns;
|
||||||
|
} else {
|
||||||
|
helix.turn_height = number / helix.turns.max(EPSILON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"turns" if number > EPSILON => {
|
||||||
|
if helix.constraint == HelixConstraint::TurnHeight {
|
||||||
|
helix.turns = number;
|
||||||
|
} else {
|
||||||
|
helix.turns = number;
|
||||||
|
helix.turn_height = old_height / number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"turn_height" if number.abs() > EPSILON => {
|
||||||
|
if helix.constraint == HelixConstraint::Turns {
|
||||||
|
helix.turn_height = number;
|
||||||
|
} else {
|
||||||
|
let turns = old_height / number;
|
||||||
|
if turns <= EPSILON {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
helix.turn_height = number;
|
||||||
|
helix.turns = turns;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"base_radius" if number > EPSILON => {
|
||||||
|
let Some((base, _, start_direction)) = axis_frame(helix) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
helix.radius = number;
|
||||||
|
helix.start_point = vector(base + start_direction * number);
|
||||||
|
}
|
||||||
|
"top_radius" if number >= 0.0 => {
|
||||||
|
if !rebuild(helix, number) {
|
||||||
|
*helix = original;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => return,
|
||||||
|
}
|
||||||
|
if !rebuild(helix, top) {
|
||||||
|
*helix = original;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_transform(helix: &mut Helix, t: &EntityTransform) {
|
fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) {
|
||||||
// Move the visible curve, then keep the generating points in step.
|
let original = helix.clone();
|
||||||
Transformable::apply_transform(&mut helix.spline, t);
|
let top = top_radius(helix);
|
||||||
|
let Some((base, axis, start_direction)) = axis_frame(helix) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match (grip_id, apply) {
|
||||||
|
(0, GripApply::Translate(delta)) => {
|
||||||
|
let delta = kernel(delta);
|
||||||
|
if !finite(delta) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
helix.axis_base_point = vector(base + delta);
|
||||||
|
helix.start_point = vector(point(helix.start_point) + delta);
|
||||||
|
}
|
||||||
|
(0, GripApply::Absolute(position)) => {
|
||||||
|
let position = kernel(position);
|
||||||
|
if !finite(position) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let delta = position - base;
|
||||||
|
helix.axis_base_point = vector(position);
|
||||||
|
helix.start_point = vector(point(helix.start_point) + delta);
|
||||||
|
}
|
||||||
|
(1, GripApply::Absolute(position)) => {
|
||||||
|
let position = kernel(position);
|
||||||
|
if !finite(position) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let radial = position - base - axis * (position - base).dot(axis);
|
||||||
|
let radius = radial.length();
|
||||||
|
if radius <= EPSILON {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
helix.radius = radius;
|
||||||
|
helix.start_point = vector(base + radial);
|
||||||
|
}
|
||||||
|
(2, GripApply::Absolute(position)) => {
|
||||||
|
let position = kernel(position);
|
||||||
|
if !finite(position) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let axis_vector = position - base;
|
||||||
|
let height = axis_vector.length();
|
||||||
|
if height <= EPSILON {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let target_axis = axis_vector / height;
|
||||||
|
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
|
||||||
|
helix.axis_vector = vector(target_axis * helix.turn_height.signum());
|
||||||
|
helix.turns = height / helix.turn_height.abs();
|
||||||
|
} else {
|
||||||
|
helix.axis_vector = vector(target_axis);
|
||||||
|
helix.turn_height = height / helix.turns.max(EPSILON);
|
||||||
|
}
|
||||||
|
let new_axis = point(helix.axis_vector);
|
||||||
|
let start_direction = projected_direction(start_direction, new_axis)
|
||||||
|
.or_else(|| perpendicular(new_axis));
|
||||||
|
let Some(start_direction) = start_direction else {
|
||||||
|
*helix = original;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
helix.start_point = vector(base + start_direction * helix.radius);
|
||||||
|
}
|
||||||
|
(3, GripApply::Absolute(position)) => {
|
||||||
|
let position = kernel(position);
|
||||||
|
if !finite(position) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let delta = position - base;
|
||||||
|
let height = delta.dot(axis);
|
||||||
|
let radius = (delta - axis * height).length();
|
||||||
|
if height.abs() <= EPSILON {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
|
||||||
|
let turns = height / helix.turn_height;
|
||||||
|
if turns <= EPSILON {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
helix.turns = turns;
|
||||||
|
} else {
|
||||||
|
helix.turn_height = height / helix.turns.max(EPSILON);
|
||||||
|
}
|
||||||
|
if !rebuild(helix, radius) {
|
||||||
|
*helix = original;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => return,
|
||||||
|
}
|
||||||
|
if !rebuild(helix, top) {
|
||||||
|
*helix = original;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_transform(helix: &mut Helix, transform: &EntityTransform) {
|
||||||
|
match transform {
|
||||||
|
EntityTransform::Translate(delta) => {
|
||||||
|
acadrust::Entity::translate(helix, Vector3::new(delta.x, delta.y, delta.z));
|
||||||
|
}
|
||||||
|
EntityTransform::Rotate { center, axis, angle_rad } => {
|
||||||
|
crate::scene::view::transform::apply_standard_transform(
|
||||||
|
helix,
|
||||||
|
*center,
|
||||||
|
*axis,
|
||||||
|
*angle_rad,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
EntityTransform::Scale { center, factor } => {
|
||||||
|
crate::scene::view::transform::apply_standard_scale(helix, *center, *factor);
|
||||||
|
}
|
||||||
|
EntityTransform::Mirror { p1, p2, working_normal } => {
|
||||||
|
let reflection = crate::scene::view::transform::reflection_about_working_line(
|
||||||
|
*p1,
|
||||||
|
*p2,
|
||||||
|
*working_normal,
|
||||||
|
);
|
||||||
|
acadrust::Entity::apply_transform(helix, &reflection);
|
||||||
|
}
|
||||||
|
EntityTransform::Affine(value) => acadrust::Entity::apply_transform(helix, value),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderConvertible for Helix {
|
impl RenderConvertible for Helix {
|
||||||
@@ -113,6 +434,7 @@ impl Grippable for Helix {
|
|||||||
fn grips(&self) -> Vec<GripDef> {
|
fn grips(&self) -> Vec<GripDef> {
|
||||||
grips(self)
|
grips(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||||
apply_grip(self, grip_id, apply);
|
apply_grip(self, grip_id, apply);
|
||||||
}
|
}
|
||||||
@@ -122,13 +444,14 @@ impl crate::entities::traits::PropertyEditable for Helix {
|
|||||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||||
properties(self)
|
properties(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||||
apply_geom_prop(self, field, value);
|
apply_geom_prop(self, field, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Transformable for Helix {
|
impl Transformable for Helix {
|
||||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
fn apply_transform(&mut self, transform: &EntityTransform) {
|
||||||
apply_transform(self, t);
|
apply_transform(self, transform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,11 +269,10 @@ fn properties(leader: &Leader) -> Vec<PropSection> {
|
|||||||
let vertex_label = if n == 0 {
|
let vertex_label = if n == 0 {
|
||||||
"—".to_string()
|
"—".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("{} / {}", vi + 1, n)
|
(vi + 1).to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Geometry sits right after General — a leader owns editable path vertices,
|
// Geometry exposes one editable vertex at a time.
|
||||||
// navigated one at a time by the Current Vertex spinner.
|
|
||||||
let mut geometry = vec![stepper(t!("Current Vertex").as_ref(), "current_vertex", vertex_label)];
|
let mut geometry = vec![stepper(t!("Current Vertex").as_ref(), "current_vertex", vertex_label)];
|
||||||
if let Some(v) = leader.vertices.get(vi) {
|
if let Some(v) = leader.vertices.get(vi) {
|
||||||
geometry.push(edit(t!("Vertex X").as_ref(), "vertex_x", v.x));
|
geometry.push(edit(t!("Vertex X").as_ref(), "vertex_x", v.x));
|
||||||
@@ -285,8 +284,7 @@ fn properties(leader: &Leader) -> Vec<PropSection> {
|
|||||||
geometry.push(ro(t!("Vertex Z").as_ref(), "vertex_z", String::new()));
|
geometry.push(ro(t!("Vertex Z").as_ref(), "vertex_z", String::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Misc: Dim style (upgraded to a dropdown by the panel builder), the
|
// The panel builder resolves the dimension style and annotative state.
|
||||||
// combined path/arrow Type, and annotative state (from the dim style).
|
|
||||||
let misc = vec![
|
let misc = vec![
|
||||||
Property {
|
Property {
|
||||||
label: t!("Dim style").into_owned(),
|
label: t!("Dim style").into_owned(),
|
||||||
|
|||||||
+181
-245
@@ -2,144 +2,57 @@ use acadrust::entities::{mesh::Mesh, polygon_mesh::PolygonMesh, Face3D, Polyface
|
|||||||
use cadkernel::geom2d::{triangulate, Tolerance};
|
use cadkernel::geom2d::{triangulate, Tolerance};
|
||||||
use cadkernel::space::{polygon, NurbsSurface3, Plane, Vec3 as KernelVec3};
|
use cadkernel::space::{polygon, NurbsSurface3, Plane, Vec3 as KernelVec3};
|
||||||
use glam::Vec3;
|
use glam::Vec3;
|
||||||
use crate::t;
|
|
||||||
|
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::entities::common::{
|
use crate::entities::common::{
|
||||||
edit_prop, edit_scalar_prop, parse_f64, ro_prop as ro, square_grip,
|
edit_prop as edit, edit_scalar_prop as edit_scalar, parse_f64, ro_prop as ro,
|
||||||
stepper_prop,
|
square_grip, stepper_prop as stepper,
|
||||||
};
|
};
|
||||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
|
||||||
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
||||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||||
use crate::scene::model::wire_model::SnapHint;
|
use crate::scene::model::wire_model::SnapHint;
|
||||||
|
use crate::t;
|
||||||
|
|
||||||
/// Triangulate a planar (possibly concave) polygon into a flat triangle-soup
|
/// Triangulate a planar polygon through the kernel.
|
||||||
/// (3 vertices per triangle), preserving the polygon's winding. A simple fan
|
|
||||||
/// from vertex 0 is only valid for convex faces — a concave face (e.g. an
|
|
||||||
/// L-shaped mesh face) fans into triangles that spill outside the outline. Ear
|
|
||||||
/// clipping handles both. Falls back to a fan when the polygon is degenerate.
|
|
||||||
pub(crate) fn triangulate_planar(poly: &[[f64; 3]]) -> Vec<[f64; 3]> {
|
pub(crate) fn triangulate_planar(poly: &[[f64; 3]]) -> Vec<[f64; 3]> {
|
||||||
let n = poly.len();
|
if poly.len() < 3 {
|
||||||
if n < 3 {
|
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
if n == 3 {
|
let origin = KernelVec3::from(poly[0]);
|
||||||
return vec![poly[0], poly[1], poly[2]];
|
let tolerance = Tolerance::default().linear();
|
||||||
}
|
let Some(axis) = poly[1..]
|
||||||
let cross = |a: [f64; 3], b: [f64; 3]| {
|
.iter()
|
||||||
[
|
.map(|point| KernelVec3::from(*point) - origin)
|
||||||
a[1] * b[2] - a[2] * b[1],
|
.find(|axis| axis.length() > tolerance)
|
||||||
a[2] * b[0] - a[0] * b[2],
|
else {
|
||||||
a[0] * b[1] - a[1] * b[0],
|
return Vec::new();
|
||||||
]
|
|
||||||
};
|
};
|
||||||
let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
let Some(normal) = polygon::normal(poly) else {
|
||||||
let fan = || {
|
return Vec::new();
|
||||||
let mut out = Vec::new();
|
|
||||||
for i in 1..n - 1 {
|
|
||||||
out.push(poly[0]);
|
|
||||||
out.push(poly[i]);
|
|
||||||
out.push(poly[i + 1]);
|
|
||||||
}
|
|
||||||
out
|
|
||||||
};
|
};
|
||||||
// Face normal via Newell's method (robust for near-planar polygons).
|
let Some(plane) = Plane::orthonormal(poly[0], axis.to_array(), normal) else {
|
||||||
let mut normal = [0.0f64; 3];
|
return Vec::new();
|
||||||
for i in 0..n {
|
|
||||||
let a = poly[i];
|
|
||||||
let b = poly[(i + 1) % n];
|
|
||||||
normal[0] += (a[1] - b[1]) * (a[2] + b[2]);
|
|
||||||
normal[1] += (a[2] - b[2]) * (a[0] + b[0]);
|
|
||||||
normal[2] += (a[0] - b[0]) * (a[1] + b[1]);
|
|
||||||
}
|
|
||||||
let nlen = dot(normal, normal).sqrt();
|
|
||||||
if nlen < 1e-12 {
|
|
||||||
return fan();
|
|
||||||
}
|
|
||||||
let normal = [normal[0] / nlen, normal[1] / nlen, normal[2] / nlen];
|
|
||||||
// Orthonormal in-plane basis.
|
|
||||||
let seed = if normal[0].abs() < 0.9 { [1.0, 0.0, 0.0] } else { [0.0, 1.0, 0.0] };
|
|
||||||
let mut u = cross(seed, normal);
|
|
||||||
let ul = dot(u, u).sqrt();
|
|
||||||
if ul < 1e-12 {
|
|
||||||
return fan();
|
|
||||||
}
|
|
||||||
u = [u[0] / ul, u[1] / ul, u[2] / ul];
|
|
||||||
let v = cross(normal, u);
|
|
||||||
let p2: Vec<[f64; 2]> = poly.iter().map(|&p| [dot(p, u), dot(p, v)]).collect();
|
|
||||||
// Signed area → winding (CCW when positive in the (u, v) frame).
|
|
||||||
let mut area = 0.0;
|
|
||||||
for i in 0..n {
|
|
||||||
let a = p2[i];
|
|
||||||
let b = p2[(i + 1) % n];
|
|
||||||
area += a[0] * b[1] - b[0] * a[1];
|
|
||||||
}
|
|
||||||
let ccw = area > 0.0;
|
|
||||||
let tri_area2 = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
|
|
||||||
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
|
||||||
};
|
};
|
||||||
let in_tri = |p: [f64; 2], a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
|
let projected: Vec<[f64; 2]> = poly
|
||||||
let d1 = tri_area2(a, b, p);
|
.iter()
|
||||||
let d2 = tri_area2(b, c, p);
|
.filter_map(|&point| plane.project(point))
|
||||||
let d3 = tri_area2(c, a, p);
|
.collect();
|
||||||
let neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
|
if projected.len() != poly.len() {
|
||||||
let pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
|
return Vec::new();
|
||||||
!(neg && pos)
|
|
||||||
};
|
|
||||||
let mut idx: Vec<usize> = (0..n).collect();
|
|
||||||
let mut out: Vec<[f64; 3]> = Vec::with_capacity((n - 2) * 3);
|
|
||||||
let mut guard = 0usize;
|
|
||||||
while idx.len() > 3 && guard < n * n {
|
|
||||||
guard += 1;
|
|
||||||
let m = idx.len();
|
|
||||||
let mut clipped = false;
|
|
||||||
for k in 0..m {
|
|
||||||
let i0 = idx[(k + m - 1) % m];
|
|
||||||
let i1 = idx[k];
|
|
||||||
let i2 = idx[(k + 1) % m];
|
|
||||||
let (a, b, c) = (p2[i0], p2[i1], p2[i2]);
|
|
||||||
let convex = if ccw { tri_area2(a, b, c) > 0.0 } else { tri_area2(a, b, c) < 0.0 };
|
|
||||||
if !convex {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut contains = false;
|
|
||||||
for &j in &idx {
|
|
||||||
if j == i0 || j == i1 || j == i2 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if in_tri(p2[j], a, b, c) {
|
|
||||||
contains = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if contains {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
out.push(poly[i0]);
|
|
||||||
out.push(poly[i1]);
|
|
||||||
out.push(poly[i2]);
|
|
||||||
idx.remove(k);
|
|
||||||
clipped = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if !clipped {
|
|
||||||
// No ear found (self-intersecting / numerically degenerate) — bail
|
|
||||||
// to a fan of the remainder rather than loop forever.
|
|
||||||
for i in 1..idx.len() - 1 {
|
|
||||||
out.push(poly[idx[0]]);
|
|
||||||
out.push(poly[idx[i]]);
|
|
||||||
out.push(poly[idx[i + 1]]);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if idx.len() == 3 {
|
let (points, triangles) = triangulate(&projected, &[]);
|
||||||
out.push(poly[idx[0]]);
|
let source: Vec<usize> = points
|
||||||
out.push(poly[idx[1]]);
|
.iter()
|
||||||
out.push(poly[idx[2]]);
|
.filter_map(|point| projected.iter().position(|candidate| candidate == point))
|
||||||
|
.collect();
|
||||||
|
if source.len() != points.len() {
|
||||||
|
return Vec::new();
|
||||||
}
|
}
|
||||||
out
|
triangles
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|triangle| triangle.map(|index| poly[source[index]]))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Face3D ────────────────────────────────────────────────────────────────────
|
// ── Face3D ────────────────────────────────────────────────────────────────────
|
||||||
@@ -177,32 +90,7 @@ fn face3d_fill(corners: [[f64; 3]; 4]) -> Vec<[f64; 3]> {
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(normal) = polygon::normal(&ring) else {
|
triangulate_planar(&ring)
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
let axis = (KernelVec3::from(ring[1]) - KernelVec3::from(ring[0])).to_array();
|
|
||||||
let Some(plane) = Plane::orthonormal(ring[0], axis, normal) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
let projected: Vec<[f64; 2]> = ring
|
|
||||||
.iter()
|
|
||||||
.filter_map(|&point| plane.project(point))
|
|
||||||
.collect();
|
|
||||||
if projected.len() != ring.len() {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
let (points, triangles) = triangulate(&projected, &[]);
|
|
||||||
let source: Vec<usize> = points
|
|
||||||
.iter()
|
|
||||||
.filter_map(|point| projected.iter().position(|candidate| candidate == point))
|
|
||||||
.collect();
|
|
||||||
if source.len() != points.len() {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
triangles
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|triangle| triangle.map(|index| ring[source[index]]))
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderConvertible for Face3D {
|
impl RenderConvertible for Face3D {
|
||||||
@@ -241,15 +129,6 @@ impl RenderConvertible for Face3D {
|
|||||||
add_edge(p3, p0);
|
add_edge(p3, p0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if pts.is_empty() {
|
|
||||||
// All edges invisible — show a tiny cross at centroid.
|
|
||||||
let cx = (p0[0] + p1[0] + p2[0] + p3[0]) / 4.0;
|
|
||||||
let cy = (p0[1] + p1[1] + p2[1] + p3[1]) / 4.0;
|
|
||||||
let cz = (p0[2] + p1[2] + p2[2] + p3[2]) / 4.0;
|
|
||||||
let s = 0.1_f64;
|
|
||||||
pts = vec![[cx - s, cy, cz], [cx + s, cy, cz]];
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(RenderEntity {
|
Some(RenderEntity {
|
||||||
pick_tris: Vec::new(),
|
pick_tris: Vec::new(),
|
||||||
object: RenderObject::Lines(pts),
|
object: RenderObject::Lines(pts),
|
||||||
@@ -268,15 +147,22 @@ impl RenderConvertible for Face3D {
|
|||||||
|
|
||||||
impl Grippable for Face3D {
|
impl Grippable for Face3D {
|
||||||
fn grips(&self) -> Vec<GripDef> {
|
fn grips(&self) -> Vec<GripDef> {
|
||||||
vec![
|
let mut grips = vec![
|
||||||
square_grip(0, dvec3(&self.first_corner)),
|
square_grip(0, dvec3(&self.first_corner)),
|
||||||
square_grip(1, dvec3(&self.second_corner)),
|
square_grip(1, dvec3(&self.second_corner)),
|
||||||
square_grip(2, dvec3(&self.third_corner)),
|
square_grip(2, dvec3(&self.third_corner)),
|
||||||
square_grip(3, dvec3(&self.fourth_corner)),
|
];
|
||||||
]
|
if !self.is_triangle() {
|
||||||
|
grips.push(square_grip(3, dvec3(&self.fourth_corner)));
|
||||||
|
}
|
||||||
|
grips
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||||
|
let was_triangle = self.is_triangle();
|
||||||
|
if was_triangle && grip_id == 3 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let corner = match grip_id {
|
let corner = match grip_id {
|
||||||
0 => &mut self.first_corner,
|
0 => &mut self.first_corner,
|
||||||
1 => &mut self.second_corner,
|
1 => &mut self.second_corner,
|
||||||
@@ -284,69 +170,107 @@ impl Grippable for Face3D {
|
|||||||
3 => &mut self.fourth_corner,
|
3 => &mut self.fourth_corner,
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
match apply {
|
let current = dvec3(corner);
|
||||||
GripApply::Translate(d) => {
|
let updated = match apply {
|
||||||
corner.x += d.x as f64;
|
GripApply::Translate(delta) => current + delta,
|
||||||
corner.y += d.y as f64;
|
GripApply::Absolute(position) => position,
|
||||||
corner.z += d.z as f64;
|
};
|
||||||
}
|
if !updated.is_finite() {
|
||||||
GripApply::Absolute(p) => {
|
return;
|
||||||
corner.x = p.x as f64;
|
}
|
||||||
corner.y = p.y as f64;
|
corner.x = updated.x;
|
||||||
corner.z = p.z as f64;
|
corner.y = updated.y;
|
||||||
}
|
corner.z = updated.z;
|
||||||
|
if was_triangle && grip_id == 2 {
|
||||||
|
self.fourth_corner = self.third_corner;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyEditable for Face3D {
|
impl PropertyEditable for Face3D {
|
||||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||||
use crate::entities::common::edit_prop as edit;
|
let was_triangle = self.is_triangle();
|
||||||
|
let vertex_count = if was_triangle { 3 } else { 4 };
|
||||||
|
let current = crate::scene::view::dispatch::prop_current_vertex()
|
||||||
|
.min(vertex_count - 1);
|
||||||
|
let corner = match current {
|
||||||
|
0 => &self.first_corner,
|
||||||
|
1 => &self.second_corner,
|
||||||
|
2 => &self.third_corner,
|
||||||
|
_ => &self.fourth_corner,
|
||||||
|
};
|
||||||
let inv = self.invisible_edges;
|
let inv = self.invisible_edges;
|
||||||
let edge = |hidden: bool| if hidden { "Invisible" } else { "Visible" };
|
let edge = |label: &str, field: &'static str, hidden: bool| Property {
|
||||||
|
label: label.to_string(),
|
||||||
|
field,
|
||||||
|
value: PropValue::Choice {
|
||||||
|
selected: if hidden {
|
||||||
|
t!("Invisible").into_owned()
|
||||||
|
} else {
|
||||||
|
t!("Visible").into_owned()
|
||||||
|
},
|
||||||
|
options: vec![t!("Visible").into_owned(), t!("Invisible").into_owned()],
|
||||||
|
},
|
||||||
|
};
|
||||||
vec![PropSection {
|
vec![PropSection {
|
||||||
title: t!("Geometry").into_owned(),
|
title: t!("Geometry").into_owned(),
|
||||||
props: vec![
|
props: vec![
|
||||||
ro(t!("Current vertex").as_ref(), "f3_current", String::new()),
|
stepper(
|
||||||
edit(t!("Vertex 1 X").as_ref(), "f3_p1x", self.first_corner.x),
|
t!("Current vertex").as_ref(),
|
||||||
edit(t!("Vertex 1 Y").as_ref(), "f3_p1y", self.first_corner.y),
|
"current_vertex",
|
||||||
edit(t!("Vertex 1 Z").as_ref(), "f3_p1z", self.first_corner.z),
|
format!("{}", current + 1),
|
||||||
edit(t!("Vertex 2 X").as_ref(), "f3_p2x", self.second_corner.x),
|
),
|
||||||
edit(t!("Vertex 2 Y").as_ref(), "f3_p2y", self.second_corner.y),
|
edit(t!("Vertex X").as_ref(), "f3_vertex_x", corner.x),
|
||||||
edit(t!("Vertex 2 Z").as_ref(), "f3_p2z", self.second_corner.z),
|
edit(t!("Vertex Y").as_ref(), "f3_vertex_y", corner.y),
|
||||||
edit(t!("Vertex 3 X").as_ref(), "f3_p3x", self.third_corner.x),
|
edit(t!("Vertex Z").as_ref(), "f3_vertex_z", corner.z),
|
||||||
edit(t!("Vertex 3 Y").as_ref(), "f3_p3y", self.third_corner.y),
|
edge(t!("Edge 1").as_ref(), "f3_edge1", inv.is_first_invisible()),
|
||||||
edit(t!("Vertex 3 Z").as_ref(), "f3_p3z", self.third_corner.z),
|
edge(t!("Edge 2").as_ref(), "f3_edge2", inv.is_second_invisible()),
|
||||||
edit(t!("Vertex 4 X").as_ref(), "f3_p4x", self.fourth_corner.x),
|
edge(t!("Edge 3").as_ref(), "f3_edge3", inv.is_third_invisible()),
|
||||||
edit(t!("Vertex 4 Y").as_ref(), "f3_p4y", self.fourth_corner.y),
|
edge(t!("Edge 4").as_ref(), "f3_edge4", inv.is_fourth_invisible()),
|
||||||
edit(t!("Vertex 4 Z").as_ref(), "f3_p4z", self.fourth_corner.z),
|
|
||||||
ro(t!("Edge 1").as_ref(), "f3_edge1", edge(inv.is_first_invisible())),
|
|
||||||
ro(t!("Edge 2").as_ref(), "f3_edge2", edge(inv.is_second_invisible())),
|
|
||||||
ro(t!("Edge 3").as_ref(), "f3_edge3", edge(inv.is_third_invisible())),
|
|
||||||
ro(t!("Edge 4").as_ref(), "f3_edge4", edge(inv.is_fourth_invisible())),
|
|
||||||
],
|
],
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||||
let Ok(v) = value.trim().parse::<f64>() else {
|
if field.starts_with("f3_edge") {
|
||||||
|
let invisible = if value == t!("Invisible").as_ref() {
|
||||||
|
true
|
||||||
|
} else if value == t!("Visible").as_ref() {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match field {
|
||||||
|
"f3_edge1" => self.invisible_edges.set_first_invisible(invisible),
|
||||||
|
"f3_edge2" => self.invisible_edges.set_second_invisible(invisible),
|
||||||
|
"f3_edge3" => self.invisible_edges.set_third_invisible(invisible),
|
||||||
|
"f3_edge4" => self.invisible_edges.set_fourth_invisible(invisible),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(value) = parse_f64(value).filter(|value| value.is_finite()) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let was_triangle = self.is_triangle();
|
||||||
|
let vertex_count = if was_triangle { 3 } else { 4 };
|
||||||
|
let current = crate::scene::view::dispatch::prop_current_vertex()
|
||||||
|
.min(vertex_count - 1);
|
||||||
|
let corner = match current {
|
||||||
|
0 => &mut self.first_corner,
|
||||||
|
1 => &mut self.second_corner,
|
||||||
|
2 => &mut self.third_corner,
|
||||||
|
_ => &mut self.fourth_corner,
|
||||||
|
};
|
||||||
match field {
|
match field {
|
||||||
"f3_p1x" => self.first_corner.x = v,
|
"f3_vertex_x" => corner.x = value,
|
||||||
"f3_p1y" => self.first_corner.y = v,
|
"f3_vertex_y" => corner.y = value,
|
||||||
"f3_p1z" => self.first_corner.z = v,
|
"f3_vertex_z" => corner.z = value,
|
||||||
"f3_p2x" => self.second_corner.x = v,
|
|
||||||
"f3_p2y" => self.second_corner.y = v,
|
|
||||||
"f3_p2z" => self.second_corner.z = v,
|
|
||||||
"f3_p3x" => self.third_corner.x = v,
|
|
||||||
"f3_p3y" => self.third_corner.y = v,
|
|
||||||
"f3_p3z" => self.third_corner.z = v,
|
|
||||||
"f3_p4x" => self.fourth_corner.x = v,
|
|
||||||
"f3_p4y" => self.fourth_corner.y = v,
|
|
||||||
"f3_p4z" => self.fourth_corner.z = v,
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
if was_triangle && current == 2 {
|
||||||
|
self.fourth_corner = self.third_corner;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,16 +317,13 @@ fn polygon_mesh_display(mesh: &PolygonMesh) -> (Vec<[f64; 3]>, usize, usize) {
|
|||||||
return (base, m, n);
|
return (base, m, n);
|
||||||
}
|
}
|
||||||
let control_points: Vec<Vec<[f64; 3]>> = base.chunks(n).map(|row| row.to_vec()).collect();
|
let control_points: Vec<Vec<[f64; 3]>> = base.chunks(n).map(|row| row.to_vec()).collect();
|
||||||
let Some(surface) = NurbsSurface3::new(
|
let Some(surface) = NurbsSurface3::from_control_net(
|
||||||
u_degree,
|
u_degree,
|
||||||
v_degree,
|
v_degree,
|
||||||
control_points,
|
control_points,
|
||||||
Vec::new(),
|
mesh.is_closed_m(),
|
||||||
Vec::new(),
|
mesh.is_closed_n(),
|
||||||
None,
|
) else {
|
||||||
)
|
|
||||||
.map(|surface| surface.with_periodicity(mesh.is_closed_m(), mesh.is_closed_n()))
|
|
||||||
else {
|
|
||||||
return (base, m, n);
|
return (base, m, n);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -499,18 +420,17 @@ impl Grippable for PolygonMesh {
|
|||||||
|
|
||||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||||
if let Some(v) = self.vertices.get_mut(grip_id) {
|
if let Some(v) = self.vertices.get_mut(grip_id) {
|
||||||
match apply {
|
let current = glam::DVec3::new(v.location.x, v.location.y, v.location.z);
|
||||||
GripApply::Translate(d) => {
|
let updated = match apply {
|
||||||
v.location.x += d.x as f64;
|
GripApply::Translate(delta) => current + delta,
|
||||||
v.location.y += d.y as f64;
|
GripApply::Absolute(position) => position,
|
||||||
v.location.z += d.z as f64;
|
};
|
||||||
}
|
if !updated.is_finite() {
|
||||||
GripApply::Absolute(p) => {
|
return;
|
||||||
v.location.x = p.x as f64;
|
|
||||||
v.location.y = p.y as f64;
|
|
||||||
v.location.z = p.z as f64;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
v.location.x = updated.x;
|
||||||
|
v.location.y = updated.y;
|
||||||
|
v.location.z = updated.z;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,7 +457,7 @@ impl PropertyEditable for PolygonMesh {
|
|||||||
let smoothed = self.smooth_type != SurfaceSmoothType::NoSmooth;
|
let smoothed = self.smooth_type != SurfaceSmoothType::NoSmooth;
|
||||||
let density_prop = |label: &str, field: &'static str, value: i16| {
|
let density_prop = |label: &str, field: &'static str, value: i16| {
|
||||||
if smoothed {
|
if smoothed {
|
||||||
edit_scalar_prop(label, field, value as f64)
|
edit_scalar(label, field, value as f64)
|
||||||
} else {
|
} else {
|
||||||
ro(label, field, value.to_string())
|
ro(label, field, value.to_string())
|
||||||
}
|
}
|
||||||
@@ -546,16 +466,16 @@ impl PropertyEditable for PolygonMesh {
|
|||||||
PropSection {
|
PropSection {
|
||||||
title: t!("Geometry").into_owned(),
|
title: t!("Geometry").into_owned(),
|
||||||
props: vec![
|
props: vec![
|
||||||
stepper_prop(t!("Current Vertex").as_ref(), "pm_current_vertex", display),
|
stepper(t!("Current Vertex").as_ref(), "pm_current_vertex", display),
|
||||||
edit_prop(t!("Vertex X").as_ref(),
|
edit(t!("Vertex X").as_ref(),
|
||||||
"pm_vx",
|
"pm_vx",
|
||||||
vertex.map_or(0.0, |vertex| vertex.location.x),
|
vertex.map_or(0.0, |vertex| vertex.location.x),
|
||||||
),
|
),
|
||||||
edit_prop(t!("Vertex Y").as_ref(),
|
edit(t!("Vertex Y").as_ref(),
|
||||||
"pm_vy",
|
"pm_vy",
|
||||||
vertex.map_or(0.0, |vertex| vertex.location.y),
|
vertex.map_or(0.0, |vertex| vertex.location.y),
|
||||||
),
|
),
|
||||||
edit_prop(t!("Vertex Z").as_ref(),
|
edit(t!("Vertex Z").as_ref(),
|
||||||
"pm_vz",
|
"pm_vz",
|
||||||
vertex.map_or(0.0, |vertex| vertex.location.z),
|
vertex.map_or(0.0, |vertex| vertex.location.z),
|
||||||
),
|
),
|
||||||
@@ -605,48 +525,64 @@ impl PropertyEditable for PolygonMesh {
|
|||||||
|
|
||||||
let bool_value = |current: bool| {
|
let bool_value = |current: bool| {
|
||||||
if value == "toggle" {
|
if value == "toggle" {
|
||||||
!current
|
Some(!current)
|
||||||
|
} else if value.eq_ignore_ascii_case("true")
|
||||||
|
|| value.eq_ignore_ascii_case("yes")
|
||||||
|
{
|
||||||
|
Some(true)
|
||||||
|
} else if value.eq_ignore_ascii_case("false")
|
||||||
|
|| value.eq_ignore_ascii_case("no")
|
||||||
|
{
|
||||||
|
Some(false)
|
||||||
} else {
|
} else {
|
||||||
value.eq_ignore_ascii_case("true") || value.eq_ignore_ascii_case("yes")
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match field {
|
match field {
|
||||||
"pm_closed_m" => self.flags.set(
|
"pm_closed_m" => {
|
||||||
PolygonMeshFlags::CLOSED_M,
|
if let Some(closed) = bool_value(self.is_closed_m()) {
|
||||||
bool_value(self.is_closed_m()),
|
self.flags.set(PolygonMeshFlags::CLOSED_M, closed);
|
||||||
),
|
}
|
||||||
"pm_closed_n" => self.flags.set(
|
}
|
||||||
PolygonMeshFlags::CLOSED_N,
|
"pm_closed_n" => {
|
||||||
bool_value(self.is_closed_n()),
|
if let Some(closed) = bool_value(self.is_closed_n()) {
|
||||||
),
|
self.flags.set(PolygonMeshFlags::CLOSED_N, closed);
|
||||||
|
}
|
||||||
|
}
|
||||||
"pm_smooth" => {
|
"pm_smooth" => {
|
||||||
self.smooth_type = match value.trim().to_ascii_uppercase().as_str() {
|
let smooth = match value.trim().to_ascii_uppercase().as_str() {
|
||||||
|
"NONE" => SurfaceSmoothType::NoSmooth,
|
||||||
"QUADRATIC" => SurfaceSmoothType::Quadratic,
|
"QUADRATIC" => SurfaceSmoothType::Quadratic,
|
||||||
"CUBIC" => SurfaceSmoothType::Cubic,
|
"CUBIC" => SurfaceSmoothType::Cubic,
|
||||||
"BEZIER" => SurfaceSmoothType::Bezier,
|
"BEZIER" => SurfaceSmoothType::Bezier,
|
||||||
_ => SurfaceSmoothType::NoSmooth,
|
_ => return,
|
||||||
};
|
};
|
||||||
|
self.smooth_type = smooth;
|
||||||
if self.smooth_type != SurfaceSmoothType::NoSmooth {
|
if self.smooth_type != SurfaceSmoothType::NoSmooth {
|
||||||
if self.m_smooth_density < 2 {
|
if !(2..=256).contains(&self.m_smooth_density) {
|
||||||
self.m_smooth_density = 6;
|
self.m_smooth_density = 6;
|
||||||
}
|
}
|
||||||
if self.n_smooth_density < 2 {
|
if !(2..=256).contains(&self.n_smooth_density) {
|
||||||
self.n_smooth_density = 6;
|
self.n_smooth_density = 6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"pm_smooth_m" => {
|
"pm_smooth_m" => {
|
||||||
if let Ok(density @ 2..=256) = value.trim().parse::<i16>() {
|
if self.smooth_type != SurfaceSmoothType::NoSmooth {
|
||||||
self.m_smooth_density = density;
|
if let Ok(density @ 2..=256) = value.trim().parse::<i16>() {
|
||||||
|
self.m_smooth_density = density;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"pm_smooth_n" => {
|
"pm_smooth_n" => {
|
||||||
if let Ok(density @ 2..=256) = value.trim().parse::<i16>() {
|
if self.smooth_type != SurfaceSmoothType::NoSmooth {
|
||||||
self.n_smooth_density = density;
|
if let Ok(density @ 2..=256) = value.trim().parse::<i16>() {
|
||||||
|
self.n_smooth_density = density;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"pm_vx" | "pm_vy" | "pm_vz" => {
|
"pm_vx" | "pm_vy" | "pm_vz" => {
|
||||||
let Some(number) = parse_f64(value) else {
|
let Some(number) = parse_f64(value).filter(|number| number.is_finite()) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let index = crate::scene::view::dispatch::prop_current_vertex()
|
let index = crate::scene::view::dispatch::prop_current_vertex()
|
||||||
|
|||||||
+300
-77
@@ -1,7 +1,9 @@
|
|||||||
use acadrust::entities::{
|
use acadrust::entities::{
|
||||||
FlowDirectionType, LeaderContentType, LineSpacingStyle, MultiLeader, MultiLeaderPathType,
|
BlockContentConnectionType, FlowDirectionType, LeaderContentType, LineSpacingStyle,
|
||||||
TextAlignmentType, TextAttachmentDirectionType, TextAttachmentType,
|
MultiLeader, MultiLeaderPathType, MultiLeaderPropertyOverrideFlags, TextAlignmentType,
|
||||||
|
TextAttachmentDirectionType, TextAttachmentType,
|
||||||
};
|
};
|
||||||
|
use cadkernel::geom2d::Transform;
|
||||||
|
|
||||||
use crate::entities::text_support::{
|
use crate::entities::text_support::{
|
||||||
layout_mtext, resolve_text_style, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle,
|
layout_mtext, resolve_text_style, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle,
|
||||||
@@ -904,9 +906,7 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
|||||||
// ── Leaders ──────────────────────────────────────────────────────────
|
// ── Leaders ──────────────────────────────────────────────────────────
|
||||||
// Landing rows are folded in here: the standalone "Leader Structure" group
|
// Landing rows are folded in here: the standalone "Leader Structure" group
|
||||||
// is a style-dialog tab, not a palette group.
|
// is a style-dialog tab, not a palette group.
|
||||||
let leaders = PropSection {
|
let mut leader_props = vec![
|
||||||
title: t!("Leaders").into_owned(),
|
|
||||||
props: vec![
|
|
||||||
choice(
|
choice(
|
||||||
t!("Leader type").as_ref(),
|
t!("Leader type").as_ref(),
|
||||||
"path_type",
|
"path_type",
|
||||||
@@ -923,20 +923,34 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
|||||||
Property {
|
Property {
|
||||||
label: t!("Leader lineweight").into_owned(),
|
label: t!("Leader lineweight").into_owned(),
|
||||||
field: "line_weight",
|
field: "line_weight",
|
||||||
value: PropValue::LwChoice(ml.line_weight),
|
value: PropValue::FieldLwChoice {
|
||||||
|
field: "line_weight",
|
||||||
|
value: ml.line_weight,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// Arrowhead block name resolved by the panel builder (default "Closed filled").
|
// Arrowhead block name resolved by the panel builder (default "Closed filled").
|
||||||
ro(t!("Arrowhead").as_ref(), "arrowhead_handle", "Closed filled"),
|
ro(t!("Arrowhead").as_ref(), "arrowhead_handle", "Closed filled"),
|
||||||
edit(t!("Arrowhead size").as_ref(), "arrowhead_size", ml.arrowhead_size),
|
edit(t!("Arrowhead Size").as_ref(), "arrowhead_size", ml.arrowhead_size),
|
||||||
bool_toggle(t!("Horizontal Landing").as_ref(), "enable_dogleg", ml.enable_dogleg),
|
bool_toggle(t!("Horizontal Landing").as_ref(), "enable_dogleg", ml.enable_dogleg),
|
||||||
num_row(
|
num_row(
|
||||||
t!("Landing distance").as_ref(),
|
t!("Landing distance").as_ref(),
|
||||||
"landing_distance",
|
"landing_distance",
|
||||||
ml.dogleg_length,
|
if ml.enable_dogleg { ml.dogleg_length } else { 0.0 },
|
||||||
ml.enable_dogleg,
|
ml.enable_dogleg,
|
||||||
),
|
),
|
||||||
bool_toggle(t!("Leader extension").as_ref(), "enable_landing", ml.enable_landing),
|
];
|
||||||
],
|
// The extension is an MText-only landing option. It is absent for block
|
||||||
|
// content and whenever the horizontal landing itself is disabled.
|
||||||
|
if ml.content_type == LeaderContentType::MText && ml.enable_dogleg {
|
||||||
|
leader_props.push(bool_toggle(
|
||||||
|
t!("Leader extension").as_ref(),
|
||||||
|
"extend_leader_to_text",
|
||||||
|
ml.extend_leader_to_text,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let leaders = PropSection {
|
||||||
|
title: t!("Leaders").into_owned(),
|
||||||
|
props: leader_props,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Text (shown only for MText content) ──────────────────────────────
|
// ── Text (shown only for MText content) ──────────────────────────────
|
||||||
@@ -988,6 +1002,18 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
|||||||
attach_dir_str(&ml.text_attachment_direction),
|
attach_dir_str(&ml.text_attachment_direction),
|
||||||
&["Horizontal", "Vertical"],
|
&["Horizontal", "Vertical"],
|
||||||
),
|
),
|
||||||
|
edit(
|
||||||
|
t!("Landing gap").as_ref(),
|
||||||
|
"landing_gap",
|
||||||
|
ctx.landing_gap / ctx.scale_factor.max(1.0e-12),
|
||||||
|
),
|
||||||
|
bool_toggle(t!("Text frame").as_ref(), "text_frame", ml.text_frame),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let mut text = text;
|
||||||
|
let attachment_insert = text.props.len().saturating_sub(2);
|
||||||
|
let attachment_rows = match ml.text_attachment_direction {
|
||||||
|
TextAttachmentDirectionType::Horizontal => vec![
|
||||||
choice(
|
choice(
|
||||||
t!("Left Attachment").as_ref(),
|
t!("Left Attachment").as_ref(),
|
||||||
"text_left_attachment",
|
"text_left_attachment",
|
||||||
@@ -1000,10 +1026,23 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
|||||||
attachment_str(&ml.text_right_attachment),
|
attachment_str(&ml.text_right_attachment),
|
||||||
&ATTACH_LABELS,
|
&ATTACH_LABELS,
|
||||||
),
|
),
|
||||||
edit(t!("Landing gap").as_ref(), "landing_gap", ctx.landing_gap),
|
],
|
||||||
bool_toggle(t!("Text frame").as_ref(), "text_frame", ml.text_frame),
|
TextAttachmentDirectionType::Vertical => vec![
|
||||||
|
choice(
|
||||||
|
t!("Top Attachment").as_ref(),
|
||||||
|
"text_top_attachment",
|
||||||
|
attachment_str(&ml.text_top_attachment),
|
||||||
|
&ATTACH_LABELS,
|
||||||
|
),
|
||||||
|
choice(
|
||||||
|
t!("Bottom Attachment").as_ref(),
|
||||||
|
"text_bottom_attachment",
|
||||||
|
attachment_str(&ml.text_bottom_attachment),
|
||||||
|
&ATTACH_LABELS,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
text.props.splice(attachment_insert..attachment_insert, attachment_rows);
|
||||||
|
|
||||||
// ── Block ────────────────────────────────────────────────────────────
|
// ── Block ────────────────────────────────────────────────────────────
|
||||||
let block = PropSection {
|
let block = PropSection {
|
||||||
@@ -1014,29 +1053,16 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
|||||||
"block_content_handle",
|
"block_content_handle",
|
||||||
hexh(ml.block_content_handle),
|
hexh(ml.block_content_handle),
|
||||||
),
|
),
|
||||||
ro(
|
choice(
|
||||||
t!("Block connection").as_ref(),
|
t!("Attachment").as_ref(),
|
||||||
"block_connection_type",
|
"block_connection_type",
|
||||||
format!("{:?}", ml.block_connection_type),
|
match ml.block_connection_type {
|
||||||
),
|
BlockContentConnectionType::BasePoint => "Insertion point",
|
||||||
ro(
|
BlockContentConnectionType::BlockExtents => "Extents",
|
||||||
t!("Block color").as_ref(),
|
},
|
||||||
"block_content_color",
|
&["Extents", "Insertion point"],
|
||||||
format!("{:?}", ml.block_content_color),
|
|
||||||
),
|
|
||||||
ro(
|
|
||||||
t!("Block scale").as_ref(),
|
|
||||||
"block_scale",
|
|
||||||
format!(
|
|
||||||
"{:.3} × {:.3} × {:.3}",
|
|
||||||
ml.block_scale.x, ml.block_scale.y, ml.block_scale.z
|
|
||||||
),
|
|
||||||
),
|
|
||||||
edit(
|
|
||||||
t!("Block rotation").as_ref(),
|
|
||||||
"block_rotation",
|
|
||||||
ml.block_rotation.to_degrees(),
|
|
||||||
),
|
),
|
||||||
|
edit(t!("Scale").as_ref(), "block_scale", ml.block_scale.x),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1067,7 +1093,9 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
|||||||
"text_height" => {
|
"text_height" => {
|
||||||
if let Some(v) = f64(value) {
|
if let Some(v) = f64(value) {
|
||||||
ml.text_height = v;
|
ml.text_height = v;
|
||||||
ml.context.text_height = v;
|
ml.context.text_height = v * ml.context.scale_factor.max(1.0e-12);
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::TEXT_HEIGHT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"text_x" => {
|
"text_x" => {
|
||||||
@@ -1093,7 +1121,9 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
|||||||
!ml.text_frame
|
!ml.text_frame
|
||||||
} else {
|
} else {
|
||||||
value == "true"
|
value == "true"
|
||||||
}
|
};
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::TEXT_FRAME);
|
||||||
}
|
}
|
||||||
"path_type" => {
|
"path_type" => {
|
||||||
ml.path_type = match value {
|
ml.path_type = match value {
|
||||||
@@ -1101,19 +1131,40 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
|||||||
"None" => MultiLeaderPathType::Invisible,
|
"None" => MultiLeaderPathType::Invisible,
|
||||||
_ => MultiLeaderPathType::StraightLineSegments,
|
_ => MultiLeaderPathType::StraightLineSegments,
|
||||||
};
|
};
|
||||||
|
for root in &mut ml.context.leader_roots {
|
||||||
|
for line in &mut root.lines {
|
||||||
|
line.path_type = ml.path_type;
|
||||||
|
line.override_flags.insert(
|
||||||
|
acadrust::entities::LeaderLinePropertyOverrideFlags::PATH_TYPE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::PATH_TYPE);
|
||||||
}
|
}
|
||||||
"enable_landing" => {
|
"enable_landing" => {
|
||||||
ml.enable_landing = if value == "toggle" {
|
ml.enable_landing = if value == "toggle" {
|
||||||
!ml.enable_landing
|
!ml.enable_landing
|
||||||
} else {
|
} else {
|
||||||
value == "true"
|
value == "true"
|
||||||
}
|
};
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::ENABLE_LANDING);
|
||||||
}
|
}
|
||||||
"enable_dogleg" => {
|
"enable_dogleg" => {
|
||||||
ml.enable_dogleg = if value == "toggle" {
|
ml.enable_dogleg = if value == "toggle" {
|
||||||
!ml.enable_dogleg
|
!ml.enable_dogleg
|
||||||
} else {
|
} else {
|
||||||
value == "true"
|
value == "true"
|
||||||
|
};
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::ENABLE_DOGLEG);
|
||||||
|
}
|
||||||
|
"extend_leader_to_text" => {
|
||||||
|
ml.extend_leader_to_text = if value == "toggle" {
|
||||||
|
!ml.extend_leader_to_text
|
||||||
|
} else {
|
||||||
|
value == "true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"enable_annotation_scale" => {
|
"enable_annotation_scale" => {
|
||||||
@@ -1130,39 +1181,130 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
|||||||
value == "true"
|
value == "true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"background_mask_fill_on" => {
|
||||||
|
ml.context.background_mask_fill_on = if value == "toggle" {
|
||||||
|
!ml.context.background_mask_fill_on
|
||||||
|
} else {
|
||||||
|
value == "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"background_scale_factor" => {
|
||||||
|
if let Some(v) = f64(value) {
|
||||||
|
if v > 0.0 {
|
||||||
|
ml.context.background_scale_factor = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"background_transparency" => {
|
||||||
|
if let Ok(v) = value.trim().parse::<i32>() {
|
||||||
|
ml.context.background_transparency = v.clamp(0, 90);
|
||||||
|
}
|
||||||
|
}
|
||||||
"dogleg_length" => {
|
"dogleg_length" => {
|
||||||
if let Some(v) = f64(value) {
|
if let Some(v) = f64(value) {
|
||||||
ml.dogleg_length = v;
|
ml.dogleg_length = v;
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::LANDING_DISTANCE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"arrowhead_size" => {
|
"arrowhead_size" => {
|
||||||
if let Some(v) = f64(value) {
|
if let Some(v) = f64(value) {
|
||||||
ml.arrowhead_size = v;
|
ml.arrowhead_size = v;
|
||||||
|
ml.context.arrowhead_size = v * ml.context.scale_factor.max(1.0e-12);
|
||||||
|
for root in &mut ml.context.leader_roots {
|
||||||
|
for line in &mut root.lines {
|
||||||
|
line.arrowhead_size = v;
|
||||||
|
line.override_flags.insert(
|
||||||
|
acadrust::entities::LeaderLinePropertyOverrideFlags::ARROWHEAD_SIZE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::ARROWHEAD_SIZE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"scale_factor" => {
|
"scale_factor" => {
|
||||||
if let Some(v) = f64(value) {
|
if let Some(v) = f64(value) {
|
||||||
ml.scale_factor = v;
|
if v > 0.0 {
|
||||||
|
let old = ml.context.scale_factor.max(1.0e-12);
|
||||||
|
let ratio = v / old;
|
||||||
|
ml.scale_factor = v;
|
||||||
|
ml.context.scale_factor = v;
|
||||||
|
ml.context.arrowhead_size *= ratio;
|
||||||
|
ml.context.text_height *= ratio;
|
||||||
|
ml.context.landing_gap *= ratio;
|
||||||
|
ml.block_scale.x *= ratio;
|
||||||
|
ml.block_scale.y *= ratio;
|
||||||
|
ml.block_scale.z *= ratio;
|
||||||
|
ml.context.block_content_scale = ml.block_scale;
|
||||||
|
if let Some(root) = ml.context.leader_roots.first() {
|
||||||
|
let anchor = root.connection_point;
|
||||||
|
for point in [
|
||||||
|
&mut ml.context.content_base_point,
|
||||||
|
&mut ml.context.text_location,
|
||||||
|
&mut ml.context.block_content_location,
|
||||||
|
] {
|
||||||
|
point.x = anchor.x + (point.x - anchor.x) * ratio;
|
||||||
|
point.y = anchor.y + (point.y - anchor.y) * ratio;
|
||||||
|
point.z = anchor.z + (point.z - anchor.z) * ratio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for root in &mut ml.context.leader_roots {
|
||||||
|
root.landing_distance *= ratio;
|
||||||
|
}
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::SCALE_FACTOR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"landing_distance" => {
|
"landing_distance" => {
|
||||||
if let Some(v) = f64(value) {
|
if let Some(v) = f64(value) {
|
||||||
ml.dogleg_length = v;
|
ml.dogleg_length = v;
|
||||||
if let Some(root) = ml.context.leader_roots.first_mut() {
|
let display = v * ml.context.scale_factor.max(1.0e-12);
|
||||||
root.landing_distance = v;
|
for root in &mut ml.context.leader_roots {
|
||||||
|
root.landing_distance = display;
|
||||||
}
|
}
|
||||||
|
place_content_after_landing(ml);
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::LANDING_DISTANCE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"landing_gap" => {
|
"landing_gap" => {
|
||||||
if let Some(v) = f64(value) {
|
if let Some(v) = f64(value) {
|
||||||
ml.context.landing_gap = v;
|
ml.context.landing_gap = v * ml.context.scale_factor.max(1.0e-12);
|
||||||
|
place_content_after_landing(ml);
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::LANDING_GAP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"block_rotation" => {
|
"block_rotation" => {
|
||||||
if let Some(v) = f64(value) {
|
if let Some(v) = f64(value) {
|
||||||
ml.block_rotation = v.to_radians();
|
ml.block_rotation = v.to_radians();
|
||||||
|
ml.context.block_rotation = ml.block_rotation;
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::BLOCK_CONTENT_ROTATION);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"block_scale" => {
|
||||||
|
if let Some(v) = f64(value) {
|
||||||
|
if v > 0.0 {
|
||||||
|
ml.block_scale = acadrust::types::Vector3::new(v, v, v);
|
||||||
|
ml.context.block_content_scale = ml.block_scale;
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::BLOCK_CONTENT_SCALE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"block_connection_type" => {
|
||||||
|
ml.block_connection_type = if value == "Insertion point" {
|
||||||
|
BlockContentConnectionType::BasePoint
|
||||||
|
} else {
|
||||||
|
BlockContentConnectionType::BlockExtents
|
||||||
|
};
|
||||||
|
ml.context.block_connection_type = ml.block_connection_type;
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::BLOCK_CONTENT_CONNECTION);
|
||||||
|
}
|
||||||
"conn_x" => {
|
"conn_x" => {
|
||||||
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
|
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
|
||||||
root.connection_point.x = v;
|
root.connection_point.x = v;
|
||||||
@@ -1181,18 +1323,26 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
|||||||
"text_left_attachment" => {
|
"text_left_attachment" => {
|
||||||
ml.text_left_attachment = parse_attachment(value);
|
ml.text_left_attachment = parse_attachment(value);
|
||||||
ml.context.text_left_attachment = parse_attachment(value);
|
ml.context.text_left_attachment = parse_attachment(value);
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::TEXT_LEFT_ATTACHMENT);
|
||||||
}
|
}
|
||||||
"text_right_attachment" => {
|
"text_right_attachment" => {
|
||||||
ml.text_right_attachment = parse_attachment(value);
|
ml.text_right_attachment = parse_attachment(value);
|
||||||
ml.context.text_right_attachment = parse_attachment(value);
|
ml.context.text_right_attachment = parse_attachment(value);
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::TEXT_RIGHT_ATTACHMENT);
|
||||||
}
|
}
|
||||||
"text_top_attachment" => {
|
"text_top_attachment" => {
|
||||||
ml.text_top_attachment = parse_attachment(value);
|
ml.text_top_attachment = parse_attachment(value);
|
||||||
ml.context.text_top_attachment = parse_attachment(value);
|
ml.context.text_top_attachment = parse_attachment(value);
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::TEXT_TOP_ATTACHMENT);
|
||||||
}
|
}
|
||||||
"text_bottom_attachment" => {
|
"text_bottom_attachment" => {
|
||||||
ml.text_bottom_attachment = parse_attachment(value);
|
ml.text_bottom_attachment = parse_attachment(value);
|
||||||
ml.context.text_bottom_attachment = parse_attachment(value);
|
ml.context.text_bottom_attachment = parse_attachment(value);
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::TEXT_BOTTOM_ATTACHMENT);
|
||||||
}
|
}
|
||||||
"text_alignment" => {
|
"text_alignment" => {
|
||||||
ml.text_alignment = match value {
|
ml.text_alignment = match value {
|
||||||
@@ -1205,6 +1355,8 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
|||||||
"Right" => TextAlignmentType::Right,
|
"Right" => TextAlignmentType::Right,
|
||||||
_ => TextAlignmentType::Left,
|
_ => TextAlignmentType::Left,
|
||||||
};
|
};
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::TEXT_ALIGNMENT);
|
||||||
}
|
}
|
||||||
"text_flow_direction" => {
|
"text_flow_direction" => {
|
||||||
ml.context.text_flow_direction = match value {
|
ml.context.text_flow_direction = match value {
|
||||||
@@ -1218,6 +1370,16 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
|||||||
"Vertical" => TextAttachmentDirectionType::Vertical,
|
"Vertical" => TextAttachmentDirectionType::Vertical,
|
||||||
_ => TextAttachmentDirectionType::Horizontal,
|
_ => TextAttachmentDirectionType::Horizontal,
|
||||||
};
|
};
|
||||||
|
if ml.text_attachment_direction == TextAttachmentDirectionType::Vertical {
|
||||||
|
ml.enable_dogleg = false;
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::ENABLE_DOGLEG);
|
||||||
|
}
|
||||||
|
for root in &mut ml.context.leader_roots {
|
||||||
|
root.text_attachment_direction = ml.text_attachment_direction;
|
||||||
|
}
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::TEXT_ATTACHMENT_DIRECTION);
|
||||||
}
|
}
|
||||||
"line_space_style" => {
|
"line_space_style" => {
|
||||||
ml.context.line_spacing_style = match value {
|
ml.context.line_spacing_style = match value {
|
||||||
@@ -1233,6 +1395,13 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
|||||||
"text_rotation" => {
|
"text_rotation" => {
|
||||||
if let Some(v) = f64(value) {
|
if let Some(v) = f64(value) {
|
||||||
ml.context.text_rotation = v.to_radians();
|
ml.context.text_rotation = v.to_radians();
|
||||||
|
ml.context.text_direction = acadrust::types::Vector3::new(
|
||||||
|
ml.context.text_rotation.cos(),
|
||||||
|
ml.context.text_rotation.sin(),
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
ml.property_override_flags
|
||||||
|
.insert(MultiLeaderPropertyOverrideFlags::TEXT_ANGLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"line_spacing" => {
|
"line_spacing" => {
|
||||||
@@ -1254,6 +1423,27 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn place_content_after_landing(ml: &mut MultiLeader) {
|
||||||
|
let Some(root) = ml.context.leader_roots.first() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let length = (root.direction.x * root.direction.x + root.direction.y * root.direction.y).sqrt();
|
||||||
|
let (ux, uy) = if length > 1.0e-12 {
|
||||||
|
(root.direction.x / length, root.direction.y / length)
|
||||||
|
} else {
|
||||||
|
(1.0, 0.0)
|
||||||
|
};
|
||||||
|
let distance = root.landing_distance.max(0.0) + ml.context.landing_gap.max(0.0);
|
||||||
|
let location = acadrust::types::Vector3::new(
|
||||||
|
root.connection_point.x + ux * distance,
|
||||||
|
root.connection_point.y + uy * distance,
|
||||||
|
root.connection_point.z,
|
||||||
|
);
|
||||||
|
ml.context.content_base_point = location;
|
||||||
|
ml.context.text_location = location;
|
||||||
|
ml.context.block_content_location = location;
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_attachment(s: &str) -> TextAttachmentType {
|
fn parse_attachment(s: &str) -> TextAttachmentType {
|
||||||
match s {
|
match s {
|
||||||
"Top of top line" => TextAttachmentType::TopOfTopLine,
|
"Top of top line" => TextAttachmentType::TopOfTopLine,
|
||||||
@@ -1758,9 +1948,7 @@ impl MultiLeaderTess for MultiLeader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertically-attached leaders (text above/below) have no dogleg:
|
// Vertically attached leaders meet the text edge directly.
|
||||||
// the leader meets the text edge directly, so the short horizontal
|
|
||||||
// dash would be a stray mark AutoCAD never draws.
|
|
||||||
let vertical_attach = matches!(
|
let vertical_attach = matches!(
|
||||||
ml.text_attachment_direction,
|
ml.text_attachment_direction,
|
||||||
acadrust::entities::multileader::TextAttachmentDirectionType::Vertical
|
acadrust::entities::multileader::TextAttachmentDirectionType::Vertical
|
||||||
@@ -1785,11 +1973,7 @@ impl MultiLeaderTess for MultiLeader {
|
|||||||
// for a rotated leader that is the angled baseline, not world
|
// for a rotated leader that is the angled baseline, not world
|
||||||
// X. Roots without a usable direction keep the legacy
|
// X. Roots without a usable direction keep the legacy
|
||||||
// horizontal toward the text side.
|
// horizontal toward the text side.
|
||||||
let (ux, uy) = ml
|
let (ux, uy) = Some((root.direction.x, root.direction.y))
|
||||||
.context
|
|
||||||
.leader_roots
|
|
||||||
.first()
|
|
||||||
.map(|r| (r.direction.x, r.direction.y))
|
|
||||||
.filter(|(x, y)| (x * x + y * y).sqrt() > 1e-9)
|
.filter(|(x, y)| (x * x + y * y).sqrt() > 1e-9)
|
||||||
.map(|(x, y)| {
|
.map(|(x, y)| {
|
||||||
let l = (x * x + y * y).sqrt();
|
let l = (x * x + y * y).sqrt();
|
||||||
@@ -1804,6 +1988,15 @@ impl MultiLeaderTess for MultiLeader {
|
|||||||
points.push(nan);
|
points.push(nan);
|
||||||
points.push(cp_f);
|
points.push(cp_f);
|
||||||
points.push(landing_end);
|
points.push(landing_end);
|
||||||
|
if ml.extend_leader_to_text {
|
||||||
|
let gap = ml.context.landing_gap.max(0.0)
|
||||||
|
* context_scale_correction as f64;
|
||||||
|
points.push([
|
||||||
|
(text_loc_w.x - ux * gap) as f32,
|
||||||
|
(text_loc_w.y - uy * gap) as f32,
|
||||||
|
text_loc_w.z as f32,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1856,31 +2049,66 @@ impl MultiLeaderTess for MultiLeader {
|
|||||||
// synthetic Insert supplies that context to the shared scene graph;
|
// synthetic Insert supplies that context to the shared scene graph;
|
||||||
// every child then uses its normal tessellator.
|
// every child then uses its normal tessellator.
|
||||||
if ml.content_type == LeaderContentType::Block && ml.context.has_block_contents {
|
if ml.content_type == LeaderContentType::Block && ml.context.has_block_contents {
|
||||||
let block_name = match ml.block_content_handle {
|
let block_record = match ml.block_content_handle {
|
||||||
Some(h) if !h.is_null() => document
|
Some(h) if !h.is_null() => document
|
||||||
.block_records
|
.block_records
|
||||||
.iter()
|
.iter()
|
||||||
.find(|br| br.handle == h)
|
.find(|br| br.handle == h)
|
||||||
.map(|br| br.name.clone()),
|
.cloned(),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
if let Some(block_name) = block_name {
|
if let Some(block_record) = block_record {
|
||||||
|
let block_name = block_record.name.clone();
|
||||||
let block_color = if selected {
|
let block_color = if selected {
|
||||||
line_color
|
line_color
|
||||||
} else {
|
} else {
|
||||||
color_or_inherit(&ml.block_content_color, entity_color)
|
color_or_inherit(&ml.block_content_color, entity_color)
|
||||||
};
|
};
|
||||||
let mut synth_ins =
|
let mut insertion = ml.context.block_content_location;
|
||||||
acadrust::entities::Insert::new(block_name, ml.context.block_content_location);
|
if ml.block_connection_type == BlockContentConnectionType::BlockExtents {
|
||||||
|
let mut bounds = [f32::MAX, f32::MAX, f32::MIN, f32::MIN];
|
||||||
|
for entity_handle in &block_record.entity_handles {
|
||||||
|
if let Some(entity) = document.get_entity(*entity_handle) {
|
||||||
|
let aabb = crate::scene::convert::tess::entity_aabb(entity);
|
||||||
|
if aabb != WireModel::UNBOUNDED_AABB {
|
||||||
|
bounds[0] = bounds[0].min(aabb[0]);
|
||||||
|
bounds[1] = bounds[1].min(aabb[1]);
|
||||||
|
bounds[2] = bounds[2].max(aabb[2]);
|
||||||
|
bounds[3] = bounds[3].max(aabb[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bounds[0] <= bounds[2] && bounds[1] <= bounds[3] {
|
||||||
|
let to_block = Transform::rotation(-ml.block_rotation);
|
||||||
|
let from_left = ml
|
||||||
|
.context
|
||||||
|
.leader_roots
|
||||||
|
.first()
|
||||||
|
.map_or(true, |root| {
|
||||||
|
to_block.apply_vector([
|
||||||
|
root.direction.x,
|
||||||
|
root.direction.y,
|
||||||
|
])[0]
|
||||||
|
>= 0.0
|
||||||
|
});
|
||||||
|
let anchor_x = if from_left { bounds[0] } else { bounds[2] } as f64;
|
||||||
|
let anchor_y = ((bounds[1] + bounds[3]) * 0.5) as f64;
|
||||||
|
let local_x =
|
||||||
|
(anchor_x - block_record.base_point.x) * ml.block_scale.x;
|
||||||
|
let local_y =
|
||||||
|
(anchor_y - block_record.base_point.y) * ml.block_scale.y;
|
||||||
|
let rotated = Transform::rotation(ml.block_rotation)
|
||||||
|
.apply_vector([local_x, local_y]);
|
||||||
|
insertion.x -= rotated[0];
|
||||||
|
insertion.y -= rotated[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut synth_ins = acadrust::entities::Insert::new(block_name, insertion);
|
||||||
synth_ins.set_x_scale(ml.block_scale.x);
|
synth_ins.set_x_scale(ml.block_scale.x);
|
||||||
synth_ins.set_y_scale(ml.block_scale.y);
|
synth_ins.set_y_scale(ml.block_scale.y);
|
||||||
synth_ins.set_z_scale(ml.block_scale.z);
|
synth_ins.set_z_scale(ml.block_scale.z);
|
||||||
synth_ins.rotation = ml.block_rotation;
|
synth_ins.rotation = ml.block_rotation;
|
||||||
synth_ins.common.layer = ml.common.layer.clone();
|
synth_ins.common.layer = ml.common.layer.clone();
|
||||||
// block_connection_type (BlockExtents vs BasePoint) chooses the
|
|
||||||
// anchor when *creating* the multileader; at render time the
|
|
||||||
// file's stored leader endpoints already encode that choice.
|
|
||||||
let _ = ml.block_connection_type;
|
|
||||||
let depths = rustc_hash::FxHashMap::default();
|
let depths = rustc_hash::FxHashMap::default();
|
||||||
let graph = crate::scene::render_graph::RenderSceneGraph::new(
|
let graph = crate::scene::render_graph::RenderSceneGraph::new(
|
||||||
document,
|
document,
|
||||||
@@ -1956,12 +2184,7 @@ impl MultiLeaderTess for MultiLeader {
|
|||||||
let local_ins_x = (ins.x) as f32;
|
let local_ins_x = (ins.x) as f32;
|
||||||
let z = (ins.z) as f32;
|
let z = (ins.z) as f32;
|
||||||
|
|
||||||
// Rotation: the annotation context is AutoCAD's BAKED display
|
// The context stores the resolved display rotation.
|
||||||
// state — text_direction / text_rotation already reflect whatever
|
|
||||||
// the style's angle setting produced at edit time. Re-deriving
|
|
||||||
// from ml.text_angle_type here (e.g. forcing Horizontal to 0)
|
|
||||||
// un-rotated leaders whose stored context is angled; the angle
|
|
||||||
// type only guides recomputation during edits, not display.
|
|
||||||
let td = ctx.text_direction;
|
let td = ctx.text_direction;
|
||||||
let mut rot = if td.x.abs() > 1e-9 || td.y.abs() > 1e-9 {
|
let mut rot = if td.x.abs() > 1e-9 || td.y.abs() > 1e-9 {
|
||||||
(td.y as f32).atan2(td.x as f32)
|
(td.y as f32).atan2(td.x as f32)
|
||||||
@@ -2013,14 +2236,14 @@ impl MultiLeaderTess for MultiLeader {
|
|||||||
// anchor within the text block (Left/Center/Right) — honour it
|
// anchor within the text block (Left/Center/Right) — honour it
|
||||||
// instead of guessing from the leader side.
|
// instead of guessing from the leader side.
|
||||||
use acadrust::entities::multileader::{
|
use acadrust::entities::multileader::{
|
||||||
TextAttachmentDirectionType, TextAttachmentPointType,
|
TextAttachmentDirectionType,
|
||||||
};
|
};
|
||||||
// The context's attachment point exists in every DWG version;
|
// The context's attachment point exists in every DWG version;
|
||||||
// the entity-level copy only exists from R2010 on.
|
// the entity-level copy only exists from R2010 on.
|
||||||
let h_anchor: f32 = match ctx.text_attachment_point {
|
let h_anchor: f32 = match ctx.text_alignment {
|
||||||
TextAttachmentPointType::Left => 0.0,
|
TextAlignmentType::Left => 0.0,
|
||||||
TextAttachmentPointType::Center => 0.5,
|
TextAlignmentType::Center => 0.5,
|
||||||
TextAttachmentPointType::Right => 1.0,
|
TextAlignmentType::Right => 1.0,
|
||||||
};
|
};
|
||||||
// Pick the vertical-anchor attachment based on text_attachment_direction:
|
// Pick the vertical-anchor attachment based on text_attachment_direction:
|
||||||
// Horizontal — leader attaches left/right; use ml.text_left_attachment
|
// Horizontal — leader attaches left/right; use ml.text_left_attachment
|
||||||
@@ -2273,14 +2496,9 @@ impl MultiLeaderTess for MultiLeader {
|
|||||||
|
|
||||||
// Text frame / background-fill rectangle in local frame, then rotated to WCS.
|
// Text frame / background-fill rectangle in local frame, then rotated to WCS.
|
||||||
if ml.text_frame || ctx.background_fill_enabled {
|
if ml.text_frame || ctx.background_fill_enabled {
|
||||||
// Frame/fill offset from the glyphs: the context's landing gap
|
// Border offset is a scale factor around the laid-out text box.
|
||||||
// (AutoCAD spaces the box by exactly that), with the old
|
let pad = height
|
||||||
// quarter-height heuristic as the no-gap fallback.
|
* ((ctx.background_scale_factor.max(1.0) as f32 - 1.0) * 0.5);
|
||||||
let pad = if ctx.landing_gap > 0.0 {
|
|
||||||
ctx.landing_gap as f32
|
|
||||||
} else {
|
|
||||||
height * 0.25
|
|
||||||
};
|
|
||||||
// Box the glyphs that were actually laid out (valid for
|
// Box the glyphs that were actually laid out (valid for
|
||||||
// vertical flow too); the metric-derived box is only the
|
// vertical flow too); the metric-derived box is only the
|
||||||
// no-glyph fallback.
|
// no-glyph fallback.
|
||||||
@@ -2311,11 +2529,16 @@ impl MultiLeaderTess for MultiLeader {
|
|||||||
|
|
||||||
// Background fill — emit two triangles; renders under the text strokes.
|
// Background fill — emit two triangles; renders under the text strokes.
|
||||||
if ctx.background_fill_enabled {
|
if ctx.background_fill_enabled {
|
||||||
let fill_color = if selected {
|
let mut fill_color = if selected {
|
||||||
line_color
|
line_color
|
||||||
|
} else if ctx.background_mask_fill_on {
|
||||||
|
bg_color
|
||||||
} else {
|
} else {
|
||||||
color_or_inherit(&ctx.background_fill_color, entity_color)
|
color_or_inherit(&ctx.background_fill_color, entity_color)
|
||||||
};
|
};
|
||||||
|
fill_color[3] *=
|
||||||
|
(1.0 - ctx.background_transparency.clamp(0, 90) as f32 / 100.0)
|
||||||
|
.clamp(0.1, 1.0);
|
||||||
let fill_tris: Vec<[f32; 3]> = vec![
|
let fill_tris: Vec<[f32; 3]> = vec![
|
||||||
wcs_corners[0],
|
wcs_corners[0],
|
||||||
wcs_corners[1],
|
wcs_corners[1],
|
||||||
|
|||||||
+344
-48
@@ -3,12 +3,14 @@ use crate::t;
|
|||||||
|
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::entities::common::{
|
use crate::entities::common::{
|
||||||
edit_prop as edit, parse_f64, ro_prop as ro, square_grip, stepper_prop as stepper,
|
edit_prop as edit, format_length, parse_f64, ro_prop as ro, square_grip,
|
||||||
|
stepper_prop as stepper,
|
||||||
};
|
};
|
||||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
||||||
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
|
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
|
||||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||||
use crate::scene::model::wire_model::TangentGeom;
|
use crate::scene::model::wire_model::TangentGeom;
|
||||||
|
use cadkernel::space::{NurbsCurve3, Vec3};
|
||||||
|
|
||||||
// ── Polyline (old-style 3D heavy polyline) ────────────────────────────────────
|
// ── Polyline (old-style 3D heavy polyline) ────────────────────────────────────
|
||||||
|
|
||||||
@@ -783,20 +785,191 @@ impl Transformable for Polyline2D {
|
|||||||
|
|
||||||
// ── Polyline3D ────────────────────────────────────────────────────────────────
|
// ── Polyline3D ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const POLY3D_SPLINE_POINT: i32 = 8;
|
||||||
|
const POLY3D_SPLINE_CONTROL: i32 = 16;
|
||||||
|
const POLY3D_VERTEX: i32 = 32;
|
||||||
|
|
||||||
|
fn polyline3d_control_indices(pl: &Polyline3D) -> Vec<usize> {
|
||||||
|
if pl
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.any(|vertex| vertex.flags & POLY3D_SPLINE_CONTROL != 0)
|
||||||
|
{
|
||||||
|
return pl
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, vertex)| {
|
||||||
|
(vertex.flags & POLY3D_SPLINE_CONTROL != 0).then_some(index)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let controls: Vec<usize> = pl
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, vertex)| {
|
||||||
|
(vertex.flags & POLY3D_SPLINE_POINT == 0).then_some(index)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if controls.is_empty() {
|
||||||
|
(0..pl.vertices.len()).collect()
|
||||||
|
} else {
|
||||||
|
controls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn polyline3d_control_vertex_count(pl: &Polyline3D) -> usize {
|
||||||
|
polyline3d_control_indices(pl).len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn polyline3d_controls(pl: &Polyline3D) -> Vec<acadrust::entities::Vertex3DPolyline> {
|
||||||
|
polyline3d_control_indices(pl)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|index| pl.vertices.get(index).cloned())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn polyline3d_wire_points(pl: &Polyline3D) -> Vec<[f64; 3]> {
|
||||||
|
let curve: Vec<[f64; 3]> = pl
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.filter(|vertex| vertex.flags & POLY3D_SPLINE_POINT != 0)
|
||||||
|
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
|
||||||
|
.collect();
|
||||||
|
if !curve.is_empty() {
|
||||||
|
curve
|
||||||
|
} else {
|
||||||
|
polyline3d_controls(pl)
|
||||||
|
.iter()
|
||||||
|
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn polyline3d_length(pl: &Polyline3D) -> f64 {
|
||||||
|
let points = polyline3d_wire_points(pl);
|
||||||
|
let mut length = points
|
||||||
|
.windows(2)
|
||||||
|
.map(|pair| Vec3::from(pair[0]).distance(Vec3::from(pair[1])))
|
||||||
|
.sum::<f64>();
|
||||||
|
if pl.is_closed() && points.len() >= 2 {
|
||||||
|
length += Vec3::from(points[0])
|
||||||
|
.distance(Vec3::from(points[points.len() - 1]));
|
||||||
|
}
|
||||||
|
length
|
||||||
|
}
|
||||||
|
|
||||||
|
fn polyline3d_spline_samples(
|
||||||
|
controls: &[[f64; 3]],
|
||||||
|
degree: usize,
|
||||||
|
closed: bool,
|
||||||
|
) -> Option<Vec<[f64; 3]>> {
|
||||||
|
if !controls.iter().flatten().all(|value| value.is_finite()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let curve = if closed {
|
||||||
|
let mut periodic_controls = controls.to_vec();
|
||||||
|
periodic_controls.extend(controls.iter().take(degree).copied());
|
||||||
|
let knots = (0..periodic_controls.len() + degree + 1)
|
||||||
|
.map(|index| index as f64)
|
||||||
|
.collect();
|
||||||
|
NurbsCurve3::new(degree, periodic_controls, knots, None)?.with_periodicity(true)
|
||||||
|
} else {
|
||||||
|
NurbsCurve3::new(degree, controls.to_vec(), Vec::new(), None)?
|
||||||
|
};
|
||||||
|
let mut samples = curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE);
|
||||||
|
if closed
|
||||||
|
&& samples.len() >= 2
|
||||||
|
&& Vec3::from(samples[0]).distance(Vec3::from(samples[samples.len() - 1])) <= 1.0e-9
|
||||||
|
{
|
||||||
|
samples.pop();
|
||||||
|
}
|
||||||
|
Some(samples)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild_polyline3d_fit(pl: &mut Polyline3D) -> bool {
|
||||||
|
use acadrust::entities::polyline3d::SmoothSurfaceType as SST;
|
||||||
|
|
||||||
|
let mut controls = polyline3d_controls(pl);
|
||||||
|
if pl.smooth_type == SST::None {
|
||||||
|
for vertex in &mut controls {
|
||||||
|
vertex.flags =
|
||||||
|
(vertex.flags & !(POLY3D_SPLINE_POINT | POLY3D_SPLINE_CONTROL)) | POLY3D_VERTEX;
|
||||||
|
}
|
||||||
|
pl.vertices = controls;
|
||||||
|
pl.flags.spline_fit = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if pl.smooth_type == SST::Bezier {
|
||||||
|
pl.smooth_type = SST::CubicBSpline;
|
||||||
|
}
|
||||||
|
let degree = match pl.smooth_type {
|
||||||
|
SST::QuadraticBSpline => 2,
|
||||||
|
SST::CubicBSpline => 3,
|
||||||
|
SST::Bezier => unreachable!(),
|
||||||
|
SST::None => unreachable!(),
|
||||||
|
};
|
||||||
|
if controls.len() <= degree {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let points: Vec<[f64; 3]> = controls
|
||||||
|
.iter()
|
||||||
|
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
|
||||||
|
.collect();
|
||||||
|
let Some(samples) = polyline3d_spline_samples(&points, degree, pl.is_closed()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
for vertex in &mut controls {
|
||||||
|
vertex.flags = (vertex.flags & !(POLY3D_SPLINE_POINT | POLY3D_SPLINE_CONTROL))
|
||||||
|
| POLY3D_VERTEX
|
||||||
|
| POLY3D_SPLINE_CONTROL;
|
||||||
|
}
|
||||||
|
let vertex_layer = controls
|
||||||
|
.first()
|
||||||
|
.map(|vertex| vertex.layer.clone())
|
||||||
|
.unwrap_or_else(|| "0".to_string());
|
||||||
|
let curve_vertices = samples.into_iter().map(|point| {
|
||||||
|
let mut vertex = acadrust::entities::Vertex3DPolyline::from_xyz(
|
||||||
|
point[0], point[1], point[2],
|
||||||
|
);
|
||||||
|
vertex.layer = vertex_layer.clone();
|
||||||
|
vertex.flags = POLY3D_VERTEX | POLY3D_SPLINE_POINT;
|
||||||
|
vertex
|
||||||
|
});
|
||||||
|
controls.extend(curve_vertices);
|
||||||
|
pl.vertices = controls;
|
||||||
|
pl.flags.spline_fit = true;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn tessellate_polyline3d(pl: &Polyline3D) -> RenderEntity {
|
fn tessellate_polyline3d(pl: &Polyline3D) -> RenderEntity {
|
||||||
let to_pt = |v: &acadrust::entities::Vertex3DPolyline| -> [f64; 3] {
|
let to_pt = |v: &acadrust::entities::Vertex3DPolyline| -> [f64; 3] {
|
||||||
[v.position.x, v.position.y, v.position.z]
|
[v.position.x, v.position.y, v.position.z]
|
||||||
};
|
};
|
||||||
|
|
||||||
// DXF vertex flags: 8 = spline-fit curve point, 16 = spline frame control point.
|
// Spline samples draw the wire; frame vertices remain snap points.
|
||||||
// When spline-fit vertices are present use them for the wire and control points for snap;
|
let spline_curve: Vec<_> = pl
|
||||||
// otherwise treat all vertices uniformly.
|
.vertices
|
||||||
let spline_curve: Vec<_> = pl.vertices.iter().filter(|v| v.flags & 8 != 0).collect();
|
.iter()
|
||||||
let ctrl_pts: Vec<_> = pl.vertices.iter().filter(|v| v.flags & 16 != 0).collect();
|
.filter(|v| v.flags & POLY3D_SPLINE_POINT != 0)
|
||||||
|
.collect();
|
||||||
|
let ctrl_pts: Vec<_> = pl
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.filter(|v| v.flags & POLY3D_SPLINE_CONTROL != 0)
|
||||||
|
.collect();
|
||||||
|
|
||||||
let (wire_pts, key_verts) = if !spline_curve.is_empty() {
|
let (wire_pts, key_verts) = if !spline_curve.is_empty() {
|
||||||
let wire: Vec<[f64; 3]> = spline_curve.iter().map(|v| to_pt(v)).collect();
|
let wire: Vec<[f64; 3]> = spline_curve.iter().map(|v| to_pt(v)).collect();
|
||||||
let ctrl: Vec<[f64; 3]> = ctrl_pts.iter().map(|v| to_pt(v)).collect();
|
let ctrl: Vec<[f64; 3]> = if ctrl_pts.is_empty() {
|
||||||
|
polyline3d_controls(pl).iter().map(to_pt).collect()
|
||||||
|
} else {
|
||||||
|
ctrl_pts.iter().map(|v| to_pt(v)).collect()
|
||||||
|
};
|
||||||
(wire, ctrl)
|
(wire, ctrl)
|
||||||
} else {
|
} else {
|
||||||
let pts: Vec<[f64; 3]> = pl.vertices.iter().map(to_pt).collect();
|
let pts: Vec<[f64; 3]> = pl.vertices.iter().map(to_pt).collect();
|
||||||
@@ -826,8 +999,9 @@ impl RenderConvertible for Polyline3D {
|
|||||||
|
|
||||||
impl Grippable for Polyline3D {
|
impl Grippable for Polyline3D {
|
||||||
fn grips(&self) -> Vec<GripDef> {
|
fn grips(&self) -> Vec<GripDef> {
|
||||||
self.vertices
|
polyline3d_control_indices(self)
|
||||||
.iter()
|
.into_iter()
|
||||||
|
.filter_map(|index| self.vertices.get(index))
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, v)| {
|
.map(|(i, v)| {
|
||||||
square_grip(
|
square_grip(
|
||||||
@@ -839,7 +1013,8 @@ impl Grippable for Polyline3D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||||
if let Some(v) = self.vertices.get_mut(grip_id) {
|
let raw_index = polyline3d_control_indices(self).get(grip_id).copied();
|
||||||
|
if let Some(v) = raw_index.and_then(|index| self.vertices.get_mut(index)) {
|
||||||
match apply {
|
match apply {
|
||||||
GripApply::Translate(d) => {
|
GripApply::Translate(d) => {
|
||||||
v.position.x += d.x as f64;
|
v.position.x += d.x as f64;
|
||||||
@@ -852,12 +1027,19 @@ impl Grippable for Polyline3D {
|
|||||||
v.position.z = p.z as f64;
|
v.position.z = p.z as f64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if self.smooth_type
|
||||||
|
!= acadrust::entities::polyline3d::SmoothSurfaceType::None
|
||||||
|
{
|
||||||
|
let _ = rebuild_polyline3d_fit(self);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
||||||
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
||||||
vec![
|
use acadrust::entities::polyline3d::SmoothSurfaceType as SST;
|
||||||
|
|
||||||
|
let mut items = vec![
|
||||||
GripMenuItem {
|
GripMenuItem {
|
||||||
label: "Stretch",
|
label: "Stretch",
|
||||||
action: GripMenuAction::Stretch,
|
action: GripMenuAction::Stretch,
|
||||||
@@ -866,40 +1048,75 @@ impl Grippable for Polyline3D {
|
|||||||
label: "Add Vertex",
|
label: "Add Vertex",
|
||||||
action: GripMenuAction::AddVertex,
|
action: GripMenuAction::AddVertex,
|
||||||
},
|
},
|
||||||
GripMenuItem {
|
];
|
||||||
|
let minimum = match self.smooth_type {
|
||||||
|
SST::None if self.is_closed() => 3,
|
||||||
|
SST::None => 2,
|
||||||
|
SST::QuadraticBSpline => 3,
|
||||||
|
SST::CubicBSpline | SST::Bezier => 4,
|
||||||
|
};
|
||||||
|
if polyline3d_control_vertex_count(self) > minimum {
|
||||||
|
items.push(GripMenuItem {
|
||||||
label: "Remove Vertex",
|
label: "Remove Vertex",
|
||||||
action: GripMenuAction::RemoveVertex,
|
action: GripMenuAction::RemoveVertex,
|
||||||
},
|
});
|
||||||
]
|
}
|
||||||
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
||||||
use crate::scene::model::object::GripMenuAction as A;
|
use crate::scene::model::object::GripMenuAction as A;
|
||||||
let n = self.vertices.len();
|
use acadrust::entities::polyline3d::SmoothSurfaceType as SST;
|
||||||
|
|
||||||
|
let mut controls = polyline3d_controls(self);
|
||||||
|
let n = controls.len();
|
||||||
match action {
|
match action {
|
||||||
A::AddVertex if grip_id < n => {
|
A::AddVertex if grip_id < n => {
|
||||||
if grip_id == n - 1 && !self.is_closed() {
|
if grip_id == n - 1 && !self.is_closed() {
|
||||||
let mut new_v = self.vertices[grip_id].clone();
|
let mut new_v = controls[grip_id].clone();
|
||||||
|
if n >= 2 {
|
||||||
|
let previous = &controls[n - 2].position;
|
||||||
|
let last = &controls[n - 1].position;
|
||||||
|
let next = Vec3::new(previous.x, previous.y, previous.z).lerp(
|
||||||
|
Vec3::new(last.x, last.y, last.z),
|
||||||
|
2.0,
|
||||||
|
);
|
||||||
|
new_v.position.x = next.x;
|
||||||
|
new_v.position.y = next.y;
|
||||||
|
new_v.position.z = next.z;
|
||||||
|
}
|
||||||
new_v.handle = acadrust::Handle::NULL;
|
new_v.handle = acadrust::Handle::NULL;
|
||||||
self.vertices.push(new_v);
|
controls.push(new_v);
|
||||||
return;
|
} else {
|
||||||
|
let i1 = (grip_id + 1) % n;
|
||||||
|
let v0 = &controls[grip_id];
|
||||||
|
let v1 = &controls[i1];
|
||||||
|
let midpoint = Vec3::new(v0.position.x, v0.position.y, v0.position.z).lerp(
|
||||||
|
Vec3::new(v1.position.x, v1.position.y, v1.position.z),
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
let mut new_v = v0.clone();
|
||||||
|
new_v.position.x = midpoint.x;
|
||||||
|
new_v.position.y = midpoint.y;
|
||||||
|
new_v.position.z = midpoint.z;
|
||||||
|
new_v.handle = acadrust::Handle::NULL;
|
||||||
|
controls.insert(grip_id + 1, new_v);
|
||||||
}
|
}
|
||||||
let i1 = (grip_id + 1) % n;
|
self.vertices = controls;
|
||||||
let v0 = &self.vertices[grip_id];
|
let _ = rebuild_polyline3d_fit(self);
|
||||||
let v1 = &self.vertices[i1];
|
|
||||||
let mx = (v0.position.x + v1.position.x) * 0.5;
|
|
||||||
let my = (v0.position.y + v1.position.y) * 0.5;
|
|
||||||
let mz = (v0.position.z + v1.position.z) * 0.5;
|
|
||||||
let mut new_v = v0.clone();
|
|
||||||
new_v.position.x = mx;
|
|
||||||
new_v.position.y = my;
|
|
||||||
new_v.position.z = mz;
|
|
||||||
new_v.handle = acadrust::Handle::NULL;
|
|
||||||
let insert_at = (grip_id + 1).min(self.vertices.len());
|
|
||||||
self.vertices.insert(insert_at, new_v);
|
|
||||||
}
|
}
|
||||||
A::RemoveVertex if grip_id < n && n > 2 => {
|
A::RemoveVertex if grip_id < n => {
|
||||||
self.vertices.remove(grip_id);
|
let minimum = match self.smooth_type {
|
||||||
|
SST::None if self.is_closed() => 3,
|
||||||
|
SST::None => 2,
|
||||||
|
SST::QuadraticBSpline => 3,
|
||||||
|
SST::CubicBSpline | SST::Bezier => 4,
|
||||||
|
};
|
||||||
|
if n > minimum {
|
||||||
|
controls.remove(grip_id);
|
||||||
|
self.vertices = controls;
|
||||||
|
let _ = rebuild_polyline3d_fit(self);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -909,40 +1126,79 @@ impl Grippable for Polyline3D {
|
|||||||
impl PropertyEditable for Polyline3D {
|
impl PropertyEditable for Polyline3D {
|
||||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||||
use acadrust::entities::polyline3d::SmoothSurfaceType as SST;
|
use acadrust::entities::polyline3d::SmoothSurfaceType as SST;
|
||||||
let n = self.vertices.len();
|
let control_indices = polyline3d_control_indices(self);
|
||||||
let v0 = self.vertices.first();
|
let n = control_indices.len();
|
||||||
let vertex_x = v0.map(|v| v.position.x).unwrap_or_default();
|
let vi = if n == 0 {
|
||||||
let vertex_y = v0.map(|v| v.position.y).unwrap_or_default();
|
0
|
||||||
let vertex_z = v0.map(|v| v.position.z).unwrap_or_default();
|
} else {
|
||||||
|
crate::scene::view::dispatch::prop_current_vertex().min(n - 1)
|
||||||
|
};
|
||||||
|
let vertex = control_indices
|
||||||
|
.get(vi)
|
||||||
|
.and_then(|index| self.vertices.get(*index));
|
||||||
|
let vertex_x = vertex.map(|v| v.position.x).unwrap_or_default();
|
||||||
|
let vertex_y = vertex.map(|v| v.position.y).unwrap_or_default();
|
||||||
|
let vertex_z = vertex.map(|v| v.position.z).unwrap_or_default();
|
||||||
|
let vertex_label = if n == 0 {
|
||||||
|
"—".to_string()
|
||||||
|
} else {
|
||||||
|
format!("{} / {}", vi + 1, n)
|
||||||
|
};
|
||||||
let fit_smooth = match self.smooth_type {
|
let fit_smooth = match self.smooth_type {
|
||||||
SST::None => "None",
|
SST::None => "None",
|
||||||
SST::QuadraticBSpline => "Quadratic",
|
SST::QuadraticBSpline => "Quadratic",
|
||||||
SST::CubicBSpline => "Cubic",
|
SST::CubicBSpline | SST::Bezier => "Cubic",
|
||||||
SST::Bezier => "Bezier",
|
|
||||||
};
|
};
|
||||||
|
let mut smooth_options = vec!["None".to_string()];
|
||||||
|
if n >= 3 {
|
||||||
|
smooth_options.push("Quadratic".to_string());
|
||||||
|
}
|
||||||
|
if n >= 4 {
|
||||||
|
smooth_options.push("Cubic".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
vec![
|
vec![
|
||||||
PropSection {
|
PropSection {
|
||||||
title: t!("Geometry").into_owned(),
|
title: t!("Geometry").into_owned(),
|
||||||
props: vec![
|
props: vec![
|
||||||
ro(t!("Vertex").as_ref(), "pl3_vertex", if n > 0 { "1" } else { "" }),
|
stepper(
|
||||||
|
t!("Current Vertex").as_ref(),
|
||||||
|
"pl3_current_vertex",
|
||||||
|
vertex_label,
|
||||||
|
),
|
||||||
edit(t!("Vertex X").as_ref(), "pl3_vertex_x", vertex_x),
|
edit(t!("Vertex X").as_ref(), "pl3_vertex_x", vertex_x),
|
||||||
edit(t!("Vertex Y").as_ref(), "pl3_vertex_y", vertex_y),
|
edit(t!("Vertex Y").as_ref(), "pl3_vertex_y", vertex_y),
|
||||||
edit(t!("Vertex Z").as_ref(), "pl3_vertex_z", vertex_z),
|
edit(t!("Vertex Z").as_ref(), "pl3_vertex_z", vertex_z),
|
||||||
|
ro(
|
||||||
|
t!("Length").as_ref(),
|
||||||
|
"pl3_length",
|
||||||
|
format_length(polyline3d_length(self)),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
PropSection {
|
PropSection {
|
||||||
title: t!("Misc").into_owned(),
|
title: t!("Misc").into_owned(),
|
||||||
props: vec![
|
props: vec![
|
||||||
Property {
|
Property {
|
||||||
|
label: t!("Fit/Smooth").into_owned(),
|
||||||
|
field: "pl3_smooth",
|
||||||
|
value: PropValue::Choice {
|
||||||
|
selected: fit_smooth.to_string(),
|
||||||
|
options: smooth_options,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Property {
|
||||||
label: t!("Closed").into_owned(),
|
label: t!("Closed").into_owned(),
|
||||||
field: "pl3_closed",
|
field: "pl3_closed",
|
||||||
value: PropValue::BoolToggle {
|
value: if n >= 3 {
|
||||||
field: "pl3_closed",
|
PropValue::BoolToggle {
|
||||||
value: self.is_closed(),
|
field: "pl3_closed",
|
||||||
|
value: self.is_closed(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PropValue::ReadOnly("No".to_string())
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ro(t!("Fit/Smooth").as_ref(), "pl3_smooth", fit_smooth),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -956,25 +1212,65 @@ impl PropertyEditable for Polyline3D {
|
|||||||
} else {
|
} else {
|
||||||
value == "true"
|
value == "true"
|
||||||
};
|
};
|
||||||
|
if closed && polyline3d_control_vertex_count(self) < 3 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if closed {
|
if closed {
|
||||||
self.close();
|
self.close();
|
||||||
} else {
|
} else {
|
||||||
self.open();
|
self.open();
|
||||||
}
|
}
|
||||||
|
if self.smooth_type
|
||||||
|
!= acadrust::entities::polyline3d::SmoothSurfaceType::None
|
||||||
|
{
|
||||||
|
let _ = rebuild_polyline3d_fit(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"pl3_smooth" => {
|
||||||
|
use acadrust::entities::polyline3d::SmoothSurfaceType as SST;
|
||||||
|
let next = match value {
|
||||||
|
"None" => SST::None,
|
||||||
|
"Quadratic" => SST::QuadraticBSpline,
|
||||||
|
"Cubic" => SST::CubicBSpline,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
let previous = self.smooth_type;
|
||||||
|
self.smooth_type = next;
|
||||||
|
if !rebuild_polyline3d_fit(self) {
|
||||||
|
self.smooth_type = previous;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"pl3_vertex_x" => {
|
"pl3_vertex_x" => {
|
||||||
if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) {
|
let vi = crate::scene::view::dispatch::prop_current_vertex();
|
||||||
|
let raw_index = polyline3d_control_indices(self).get(vi).copied();
|
||||||
|
if let (Some(v), Some(vert)) = (
|
||||||
|
parse_f64(value).filter(|value| value.is_finite()),
|
||||||
|
raw_index.and_then(|index| self.vertices.get_mut(index)),
|
||||||
|
) {
|
||||||
vert.position.x = v;
|
vert.position.x = v;
|
||||||
|
let _ = rebuild_polyline3d_fit(self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"pl3_vertex_y" => {
|
"pl3_vertex_y" => {
|
||||||
if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) {
|
let vi = crate::scene::view::dispatch::prop_current_vertex();
|
||||||
|
let raw_index = polyline3d_control_indices(self).get(vi).copied();
|
||||||
|
if let (Some(v), Some(vert)) = (
|
||||||
|
parse_f64(value).filter(|value| value.is_finite()),
|
||||||
|
raw_index.and_then(|index| self.vertices.get_mut(index)),
|
||||||
|
) {
|
||||||
vert.position.y = v;
|
vert.position.y = v;
|
||||||
|
let _ = rebuild_polyline3d_fit(self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"pl3_vertex_z" => {
|
"pl3_vertex_z" => {
|
||||||
if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) {
|
let vi = crate::scene::view::dispatch::prop_current_vertex();
|
||||||
|
let raw_index = polyline3d_control_indices(self).get(vi).copied();
|
||||||
|
if let (Some(v), Some(vert)) = (
|
||||||
|
parse_f64(value).filter(|value| value.is_finite()),
|
||||||
|
raw_index.and_then(|index| self.vertices.get_mut(index)),
|
||||||
|
) {
|
||||||
vert.position.z = v;
|
vert.position.z = v;
|
||||||
|
let _ = rebuild_polyline3d_fit(self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -325,6 +325,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"Arrow size (DIMASZ)" => Some(("styles", "arrow-size-dimasz")),
|
"Arrow size (DIMASZ)" => Some(("styles", "arrow-size-dimasz")),
|
||||||
"Arrowhead" => Some(("common", "arrowhead")),
|
"Arrowhead" => Some(("common", "arrowhead")),
|
||||||
"Arrowhead (DIMBLK)" => Some(("styles", "arrowhead-dimblk")),
|
"Arrowhead (DIMBLK)" => Some(("styles", "arrowhead-dimblk")),
|
||||||
|
"Arrowhead Size" => Some(("properties", "arrowhead-size")),
|
||||||
"Arrowhead block:" => Some(("styles", "arrowhead-block")),
|
"Arrowhead block:" => Some(("styles", "arrowhead-block")),
|
||||||
"Arrowhead size" => Some(("common", "arrowhead-size")),
|
"Arrowhead size" => Some(("common", "arrowhead-size")),
|
||||||
"Arrowhead size:" => Some(("styles", "arrowhead-size")),
|
"Arrowhead size:" => Some(("styles", "arrowhead-size")),
|
||||||
@@ -332,6 +333,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"Arrows out first" => Some(("styles", "arrows-out-first")),
|
"Arrows out first" => Some(("styles", "arrows-out-first")),
|
||||||
"As displayed" => Some(("common", "as-displayed")),
|
"As displayed" => Some(("common", "as-displayed")),
|
||||||
"Associative" => Some(("properties", "associative")),
|
"Associative" => Some(("properties", "associative")),
|
||||||
|
"Attachment" => Some(("properties", "attachment")),
|
||||||
"At first ext" => Some(("styles", "at-first-ext")),
|
"At first ext" => Some(("styles", "at-first-ext")),
|
||||||
"At least" => Some(("properties", "at-least")),
|
"At least" => Some(("properties", "at-least")),
|
||||||
"At second ext" => Some(("styles", "at-second-ext")),
|
"At second ext" => Some(("styles", "at-second-ext")),
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ impl CadCommand for AlignedDimensionCommand {
|
|||||||
CmdResult::CommitDimension {
|
CmdResult::CommitDimension {
|
||||||
entity: self.plane.place_entity(EntityType::Dimension(Dimension::Aligned(dim))),
|
entity: self.plane.place_entity(EntityType::Dimension(Dimension::Aligned(dim))),
|
||||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||||
|
preserve_base_style: false,
|
||||||
|
continue_command: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,6 +234,8 @@ impl AngularDimensionCommand {
|
|||||||
CmdResult::CommitDimension {
|
CmdResult::CommitDimension {
|
||||||
entity,
|
entity,
|
||||||
association: DimensionAssociationInput::Explicit(self.source_refs.clone()),
|
association: DimensionAssociationInput::Explicit(self.source_refs.clone()),
|
||||||
|
preserve_base_style: false,
|
||||||
|
continue_command: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -265,6 +265,8 @@ impl ArcLengthDimensionCommand {
|
|||||||
CmdResult::CommitDimension {
|
CmdResult::CommitDimension {
|
||||||
entity: plane.place_entity(EntityType::Dimension(Dimension::Arc(dimension))),
|
entity: plane.place_entity(EntityType::Dimension(Dimension::Arc(dimension))),
|
||||||
association,
|
association,
|
||||||
|
preserve_base_style: false,
|
||||||
|
continue_command: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ impl CadCommand for DiameterDimensionCommand {
|
|||||||
entity: source_plane
|
entity: source_plane
|
||||||
.place_entity(EntityType::Dimension(Dimension::Diameter(dim))),
|
.place_entity(EntityType::Dimension(Dimension::Diameter(dim))),
|
||||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||||
|
preserve_base_style: false,
|
||||||
|
continue_command: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,20 +1,19 @@
|
|||||||
// DIMCONTINUE command — chain linear/aligned dimensions end-to-end.
|
use acadrust::entities::{
|
||||||
//
|
Dimension, DimensionAngular2Ln, DimensionAngular3Pt, DimensionBase, DimensionLinear,
|
||||||
// Each new point becomes the second extension line origin of a new dimension,
|
DimensionOrdinate,
|
||||||
// whose first extension line origin is the second extension line of the previous dim.
|
};
|
||||||
// The dimension line stays at the same perpendicular offset as the base dimension.
|
|
||||||
//
|
|
||||||
// Constructed from commands.rs after finding the last placed linear/aligned dimension.
|
|
||||||
|
|
||||||
use acadrust::entities::{Dimension, DimensionLinear};
|
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
use acadrust::EntityType;
|
use acadrust::{EntityType, Handle};
|
||||||
use glam::{DVec3, Vec3};
|
use cadkernel::geom2d::{
|
||||||
|
arc_span, closest_point, intersect, nearest_of, Arc, Curve, Ray, Tolerance, Transform, Vec2,
|
||||||
|
XLine,
|
||||||
|
};
|
||||||
|
use cadkernel::space::Plane;
|
||||||
|
use glam::DVec3;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdOption, CmdResult, DimensionAssociationInput};
|
||||||
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;
|
|
||||||
|
|
||||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_continue.svg"));
|
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_continue.svg"));
|
||||||
|
|
||||||
@@ -27,74 +26,292 @@ pub fn tool() -> ToolDef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DimContinueCommand {
|
#[derive(Clone)]
|
||||||
/// Fixed first-extension-line origin for the current step (moves each iteration).
|
struct SourceStyle {
|
||||||
chain_p1: DVec3,
|
layer: String,
|
||||||
/// Direction along the dimension axis (0.0 = horizontal, PI/2 = vertical).
|
style_name: String,
|
||||||
rotation: f64,
|
normal: Vector3,
|
||||||
/// Text reading rotation inherited from the base dim so a UCS-aligned chain
|
|
||||||
/// keeps its text consistent with the originating DIMLINEAR. 0 = natural.
|
|
||||||
text_rotation: f64,
|
text_rotation: f64,
|
||||||
/// Absolute perpendicular coordinate (dot with `perp`) of the base
|
horizontal_direction: f64,
|
||||||
/// dimension line. Each continued dim line is projected onto this exact
|
|
||||||
/// coordinate so the whole chain stays collinear — even when the extension
|
|
||||||
/// origins sit at different perpendicular positions (extension lines of
|
|
||||||
/// different lengths). (#151)
|
|
||||||
dim_line_perp: f64,
|
|
||||||
/// Direction of "up" perpendicular to the dim axis (points toward the dim line).
|
|
||||||
perp: DVec3,
|
|
||||||
/// True once we have a base dimension loaded.
|
|
||||||
ready: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DimContinueCommand {
|
impl SourceStyle {
|
||||||
/// No base dim found — will show an error prompt and cancel immediately.
|
fn from_base(base: &DimensionBase) -> Self {
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
chain_p1: DVec3::ZERO,
|
layer: base.common.layer.clone(),
|
||||||
rotation: 0.0,
|
style_name: base.style_name.clone(),
|
||||||
text_rotation: 0.0,
|
normal: base.normal,
|
||||||
dim_line_perp: 0.0,
|
text_rotation: base.text_rotation,
|
||||||
perp: DVec3::Y,
|
horizontal_direction: base.horizontal_direction,
|
||||||
ready: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build from the last placed dimension.
|
fn apply(&self, base: &mut DimensionBase) {
|
||||||
///
|
base.common.layer = self.layer.clone();
|
||||||
/// `p2` — second extension line origin of the base dim (the chain starts
|
base.style_name = self.style_name.clone();
|
||||||
/// here). `p1` is unused — the dim line position comes from
|
base.normal = self.normal;
|
||||||
/// `definition_point` alone — but kept for signature parity with the
|
base.text_rotation = self.text_rotation;
|
||||||
/// shared `find_last_linear_dim` tuple (DIMBASELINE uses p1).
|
base.horizontal_direction = self.horizontal_direction;
|
||||||
/// `definition_point` — where the dim line was placed; its perpendicular
|
}
|
||||||
/// coordinate is the line the whole chain stays collinear with.
|
}
|
||||||
/// `rotation` — 0.0 = horizontal dim, PI/2 = vertical dim.
|
|
||||||
pub fn from_base(
|
#[derive(Clone)]
|
||||||
_p1: Vec3,
|
enum ContinueKind {
|
||||||
p2: Vec3,
|
Linear {
|
||||||
definition_point: Vec3,
|
current: Vec2,
|
||||||
rotation: f64,
|
rotation: f64,
|
||||||
text_rotation: f64,
|
perpendicular: Vec2,
|
||||||
) -> Self {
|
line_coordinate: f64,
|
||||||
// Widen the base-dim points to f64 so the committed chain math runs in
|
},
|
||||||
// full precision. (The base points arrive as f32 from the entity, but
|
Angular2Ln {
|
||||||
// every derived committed coordinate must stay f64 from here on.)
|
vertex: Vec2,
|
||||||
let p2 = p2.as_dvec3();
|
current: Vec2,
|
||||||
let definition_point = definition_point.as_dvec3();
|
radius: f64,
|
||||||
// Axis unit vector along the measurement direction — the base dim's
|
},
|
||||||
// rotation angle (any angle, incl. a UCS-aligned one), not a world H/V.
|
Angular3Pt {
|
||||||
let axis = DVec3::new(rotation.cos(), rotation.sin(), 0.0);
|
vertex: Vec2,
|
||||||
// Perpendicular unit vector toward the dim line.
|
current: Vec2,
|
||||||
let perp = DVec3::new(-axis.y, axis.x, 0.0);
|
radius: f64,
|
||||||
let dim_line_perp = definition_point.dot(perp);
|
},
|
||||||
Self {
|
Ordinate {
|
||||||
chain_p1: p2,
|
definition: Vec2,
|
||||||
rotation,
|
leader: Vec2,
|
||||||
text_rotation,
|
is_x: bool,
|
||||||
dim_line_perp,
|
},
|
||||||
perp,
|
}
|
||||||
ready: true,
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct ContinueState {
|
||||||
|
kind: ContinueKind,
|
||||||
|
style: SourceStyle,
|
||||||
|
plane: Plane,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DimContinueCommand {
|
||||||
|
base: Option<ContinueState>,
|
||||||
|
injected: Option<EntityType>,
|
||||||
|
history: Vec<ContinueState>,
|
||||||
|
preserve_base_style: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DimContinueCommand {
|
||||||
|
pub fn new(recent: Option<EntityType>, preserve_base_style: bool) -> Self {
|
||||||
|
let mut command = Self {
|
||||||
|
base: None,
|
||||||
|
injected: None,
|
||||||
|
history: Vec::new(),
|
||||||
|
preserve_base_style,
|
||||||
|
};
|
||||||
|
if let Some(entity) = recent {
|
||||||
|
let _ = command.install_base(&entity, None);
|
||||||
}
|
}
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_base(&mut self, entity: &EntityType, pick_world: Option<DVec3>) -> bool {
|
||||||
|
let EntityType::Dimension(dimension) = entity else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let style = SourceStyle::from_base(dimension.base());
|
||||||
|
let plane = plane_from_normal(dimension.base().definition_point, style.normal);
|
||||||
|
let pick = pick_world.and_then(|point| plane.project(point.to_array()).map(Vec2::from));
|
||||||
|
let kind = match dimension {
|
||||||
|
Dimension::Linear(source) => {
|
||||||
|
let rotation = source.rotation;
|
||||||
|
let perpendicular = Vec2::new(-rotation.sin(), rotation.cos());
|
||||||
|
ContinueKind::Linear {
|
||||||
|
current: selected_linear_end(
|
||||||
|
project(&plane, source.first_point),
|
||||||
|
project(&plane, source.second_point),
|
||||||
|
pick,
|
||||||
|
),
|
||||||
|
rotation,
|
||||||
|
perpendicular,
|
||||||
|
line_coordinate: project(&plane, source.base.definition_point)
|
||||||
|
.dot(perpendicular),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Dimension::Aligned(source) => {
|
||||||
|
let first = project(&plane, source.first_point);
|
||||||
|
let second = project(&plane, source.second_point);
|
||||||
|
let Some(direction) = (second - first).normalize() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let rotation = direction.angle();
|
||||||
|
let perpendicular = direction.perpendicular();
|
||||||
|
ContinueKind::Linear {
|
||||||
|
current: selected_linear_end(first, second, pick),
|
||||||
|
rotation,
|
||||||
|
perpendicular,
|
||||||
|
line_coordinate: project(&plane, source.base.definition_point)
|
||||||
|
.dot(perpendicular),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Dimension::Angular2Ln(source) => {
|
||||||
|
let lines = [
|
||||||
|
[
|
||||||
|
project(&plane, source.first_point),
|
||||||
|
project(&plane, source.second_point),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
project(&plane, source.angle_vertex),
|
||||||
|
project(&plane, source.definition_point),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
let vertex = angular2_vertex(lines).unwrap_or(lines[1][0]);
|
||||||
|
ContinueKind::Angular2Ln {
|
||||||
|
vertex,
|
||||||
|
current: selected_angular_end(vertex, [lines[0][1], lines[1][1]], pick),
|
||||||
|
radius: project(&plane, source.dimension_arc)
|
||||||
|
.distance(vertex)
|
||||||
|
.max(1.0e-9),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Dimension::Angular3Pt(source) => {
|
||||||
|
let vertex = project(&plane, source.angle_vertex);
|
||||||
|
ContinueKind::Angular3Pt {
|
||||||
|
vertex,
|
||||||
|
current: selected_angular_end(
|
||||||
|
vertex,
|
||||||
|
[
|
||||||
|
project(&plane, source.first_point),
|
||||||
|
project(&plane, source.second_point),
|
||||||
|
],
|
||||||
|
pick,
|
||||||
|
),
|
||||||
|
radius: project(&plane, source.definition_point)
|
||||||
|
.distance(vertex)
|
||||||
|
.max(1.0e-9),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Dimension::Ordinate(source) => ContinueKind::Ordinate {
|
||||||
|
definition: project(&plane, source.definition_point),
|
||||||
|
leader: project(&plane, source.leader_endpoint),
|
||||||
|
is_x: source.is_ordinate_type_x,
|
||||||
|
},
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
self.base = Some(ContinueState { kind, style, plane });
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_dimension(&self, point_world: DVec3) -> Option<Dimension> {
|
||||||
|
let state = self.base.as_ref()?;
|
||||||
|
let point = state.plane.project(point_world.to_array()).map(Vec2::from)?;
|
||||||
|
let mut dimension = match &state.kind {
|
||||||
|
ContinueKind::Linear {
|
||||||
|
current,
|
||||||
|
rotation,
|
||||||
|
perpendicular,
|
||||||
|
line_coordinate,
|
||||||
|
} => build_linear(
|
||||||
|
state.plane,
|
||||||
|
*current,
|
||||||
|
point,
|
||||||
|
*rotation,
|
||||||
|
*perpendicular,
|
||||||
|
*line_coordinate,
|
||||||
|
),
|
||||||
|
ContinueKind::Angular2Ln {
|
||||||
|
vertex,
|
||||||
|
current,
|
||||||
|
radius,
|
||||||
|
} => {
|
||||||
|
let previous = *current - *vertex;
|
||||||
|
let moving = point - *vertex;
|
||||||
|
let (definition, text) =
|
||||||
|
angular_definition_and_text(*vertex, previous, moving, *radius)?;
|
||||||
|
let mut result = DimensionAngular2Ln::default();
|
||||||
|
result.first_point = world(state.plane, *vertex);
|
||||||
|
result.second_point = world(state.plane, *current);
|
||||||
|
result.angle_vertex = world(state.plane, *vertex);
|
||||||
|
result.definition_point = world(state.plane, point);
|
||||||
|
result.dimension_arc = world(state.plane, definition);
|
||||||
|
result.base.definition_point = result.dimension_arc;
|
||||||
|
result.base.text_middle_point = world(state.plane, text);
|
||||||
|
result.base.insertion_point = result.base.text_middle_point;
|
||||||
|
Dimension::Angular2Ln(result)
|
||||||
|
}
|
||||||
|
ContinueKind::Angular3Pt {
|
||||||
|
vertex,
|
||||||
|
current,
|
||||||
|
radius,
|
||||||
|
} => {
|
||||||
|
let previous = *current - *vertex;
|
||||||
|
let moving = point - *vertex;
|
||||||
|
let (definition, text) =
|
||||||
|
angular_definition_and_text(*vertex, previous, moving, *radius)?;
|
||||||
|
let mut result = DimensionAngular3Pt::new(
|
||||||
|
world(state.plane, *vertex),
|
||||||
|
world(state.plane, *current),
|
||||||
|
world(state.plane, point),
|
||||||
|
);
|
||||||
|
result.definition_point = world(state.plane, definition);
|
||||||
|
result.base.definition_point = result.definition_point;
|
||||||
|
result.base.text_middle_point = world(state.plane, text);
|
||||||
|
result.base.insertion_point = result.base.text_middle_point;
|
||||||
|
Dimension::Angular3Pt(result)
|
||||||
|
}
|
||||||
|
ContinueKind::Ordinate {
|
||||||
|
definition,
|
||||||
|
leader,
|
||||||
|
is_x,
|
||||||
|
} => {
|
||||||
|
let to_dimension = Transform::rotation(-state.style.horizontal_direction);
|
||||||
|
let from_dimension = Transform::rotation(state.style.horizontal_direction);
|
||||||
|
let feature = Vec2::from(to_dimension.apply_point(point.into()));
|
||||||
|
let source_leader = Vec2::from(to_dimension.apply_point((*leader).into()));
|
||||||
|
let leader = if *is_x {
|
||||||
|
Vec2::new(feature.x, source_leader.y)
|
||||||
|
} else {
|
||||||
|
Vec2::new(source_leader.x, feature.y)
|
||||||
|
};
|
||||||
|
let leader = Vec2::from(from_dimension.apply_point(leader.into()));
|
||||||
|
let mut result = DimensionOrdinate::new(
|
||||||
|
world(state.plane, point),
|
||||||
|
world(state.plane, leader),
|
||||||
|
*is_x,
|
||||||
|
);
|
||||||
|
result.definition_point = world(state.plane, *definition);
|
||||||
|
result.base.definition_point = result.definition_point;
|
||||||
|
result.base.text_middle_point = result.leader_endpoint;
|
||||||
|
result.base.insertion_point = result.base.text_middle_point;
|
||||||
|
Dimension::Ordinate(result)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
state.style.apply(dimension.base_mut());
|
||||||
|
refresh_measurement(&mut dimension, state.plane);
|
||||||
|
dimension.base_mut().common.handle = Handle::NULL;
|
||||||
|
dimension.base_mut().block_name.clear();
|
||||||
|
Some(dimension)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance(&mut self, point_world: DVec3) {
|
||||||
|
let Some(point) = self
|
||||||
|
.base
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|state| state.plane.project(point_world.to_array()))
|
||||||
|
.map(Vec2::from)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(state) = self.base.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match &mut state.kind {
|
||||||
|
ContinueKind::Linear { current, .. }
|
||||||
|
| ContinueKind::Angular2Ln { current, .. }
|
||||||
|
| ContinueKind::Angular3Pt { current, .. } => *current = point,
|
||||||
|
ContinueKind::Ordinate { .. } => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn undo_one(&mut self) -> CmdResult {
|
||||||
|
let Some(previous) = self.history.pop() else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
self.base = Some(previous);
|
||||||
|
CmdResult::UndoDocument
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,110 +321,521 @@ impl CadCommand for DimContinueCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
if !self.ready {
|
if self.base.is_none() {
|
||||||
t!("DIMCONTINUE No base dimension found. Place a dimension first.").into_owned()
|
"DIMCONTINUE Select continued dimension:".to_string()
|
||||||
|
} else if self
|
||||||
|
.base
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|base| matches!(&base.kind, ContinueKind::Ordinate { .. }))
|
||||||
|
{
|
||||||
|
"DIMCONTINUE Specify feature location [Undo/Select] <Select>:".to_string()
|
||||||
} else {
|
} else {
|
||||||
t!("DIMCONTINUE Specify a second extension line origin (Enter to exit):").into_owned()
|
"DIMCONTINUE Specify second extension line origin [Select/Undo] <Select>:"
|
||||||
|
.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
if !self.ready {
|
self.base.as_ref().map_or_else(Vec::new, |base| {
|
||||||
return CmdResult::Cancel;
|
if matches!(&base.kind, ContinueKind::Ordinate { .. }) {
|
||||||
|
vec![CmdOption::new("Undo", "U"), CmdOption::new("Select", "S")]
|
||||||
|
} else {
|
||||||
|
vec![CmdOption::new("Select", "S"), CmdOption::new("Undo", "U")]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_entity_pick(&self) -> bool {
|
||||||
|
self.base.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_highlights_hover(&self) -> bool {
|
||||||
|
self.base.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_before_entity_pick(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||||
|
self.injected = Some(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_entity_pick(&mut self, _handle: Handle, point: DVec3) -> CmdResult {
|
||||||
|
let Some(entity) = self.injected.take() else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
let _ = self.install_base(&entity, Some(point));
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||||
|
let Some(dimension) = self.build_dimension(point) else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
if let Some(base) = self.base.clone() {
|
||||||
|
self.history.push(base);
|
||||||
}
|
}
|
||||||
let p1 = self.chain_p1;
|
self.advance(point);
|
||||||
let p2 = pt;
|
CmdResult::CommitDimension {
|
||||||
|
entity: EntityType::Dimension(dimension),
|
||||||
// Build a new linear dimension.
|
association: DimensionAssociationInput::Infer(None),
|
||||||
let mut dim = DimensionLinear::new(v3(p1), v3(p2));
|
preserve_base_style: self.preserve_base_style,
|
||||||
dim.rotation = self.rotation;
|
continue_command: true,
|
||||||
if self.text_rotation.abs() > 1e-9 {
|
|
||||||
dim.base.text_rotation = self.text_rotation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dim line stays collinear with the base: project both extension
|
|
||||||
// origins onto the base dim line's absolute perpendicular coordinate.
|
|
||||||
// (A fixed offset from p1 drifts off the base line whenever p1 and p2
|
|
||||||
// sit at different perpendicular positions.) (#151)
|
|
||||||
let d1 = p1 + self.perp * (self.dim_line_perp - p1.dot(self.perp));
|
|
||||||
let d2 = p2 + self.perp * (self.dim_line_perp - p2.dot(self.perp));
|
|
||||||
dim.definition_point = v3(d1);
|
|
||||||
dim.base.definition_point = v3(d1);
|
|
||||||
dim.base.text_middle_point = v3((d1 + d2) * 0.5);
|
|
||||||
dim.base.insertion_point = dim.base.text_middle_point;
|
|
||||||
dim.base.actual_measurement = dim.measurement();
|
|
||||||
|
|
||||||
// Advance chain: next dim's P1 = this dim's P2.
|
|
||||||
self.chain_p1 = p2;
|
|
||||||
|
|
||||||
CmdResult::CommitEntity(EntityType::Dimension(Dimension::Linear(dim)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
if self.base.take().is_some() {
|
||||||
}
|
CmdResult::NeedPoint
|
||||||
|
} else {
|
||||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
CmdResult::Cancel
|
||||||
if !self.ready {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
// Preview / rubber-band geometry feeds an f32 GPU buffer, so drop to f32
|
}
|
||||||
// at this screen-only boundary.
|
|
||||||
let pt = pt.as_vec3();
|
fn wants_text_input(&self) -> bool {
|
||||||
let p1 = self.chain_p1.as_vec3();
|
self.base.is_some()
|
||||||
let perp = self.perp.as_vec3();
|
}
|
||||||
let dim_line_perp = self.dim_line_perp as f32;
|
|
||||||
let dim_line_pt = p1 + perp * (dim_line_perp - p1.dot(perp));
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
let dim_line_pt2 = pt + perp * (dim_line_perp - pt.dot(perp));
|
self.base.is_some()
|
||||||
Some(WireModel {
|
}
|
||||||
point_marker: None,
|
|
||||||
taper_widths: Vec::new(),
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
pattern_stations: Vec::new(),
|
match text.trim().to_ascii_uppercase().as_str() {
|
||||||
world_width: 0.0,
|
"S" | "SELECT" => {
|
||||||
depth_override: None,
|
self.base = None;
|
||||||
display_visible: true,
|
Some(CmdResult::NeedPoint)
|
||||||
plot_visible: true,
|
}
|
||||||
fill_is_3d: false,
|
"U" | "UNDO" => Some(self.undo_one()),
|
||||||
fill_is_2d_solid: false,
|
_ => None,
|
||||||
render_instance: None,
|
}
|
||||||
pick_tris: Vec::new(),
|
}
|
||||||
pick_tris_low: Vec::new(),
|
|
||||||
dash_from_start: false,
|
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
||||||
dash_align_end: None,
|
if self.history.is_empty() {
|
||||||
text_verts: Vec::new(),
|
None
|
||||||
name: "dimcont_preview".into(),
|
} else {
|
||||||
points: vec![
|
Some(self.undo_one())
|
||||||
[p1.x, p1.y, p1.z],
|
}
|
||||||
[dim_line_pt.x, dim_line_pt.y, dim_line_pt.z],
|
}
|
||||||
[f32::NAN, 0.0, 0.0],
|
|
||||||
[pt.x, pt.y, pt.z],
|
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||||
[dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z],
|
self.build_dimension(point)
|
||||||
[f32::NAN, 0.0, 0.0],
|
.map(|dimension| preview_for_dimension(&dimension))
|
||||||
[dim_line_pt.x, dim_line_pt.y, dim_line_pt.z],
|
|
||||||
[dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z],
|
|
||||||
],
|
|
||||||
points_low: Vec::new(),
|
|
||||||
color: WireModel::CYAN,
|
|
||||||
selected: false,
|
|
||||||
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 v3(p: DVec3) -> Vector3 {
|
fn build_linear(
|
||||||
Vector3::new(p.x, p.y, p.z)
|
plane: Plane,
|
||||||
|
first: Vec2,
|
||||||
|
second: Vec2,
|
||||||
|
rotation: f64,
|
||||||
|
perpendicular: Vec2,
|
||||||
|
line_coordinate: f64,
|
||||||
|
) -> Dimension {
|
||||||
|
let first_line = project_to_line(first, perpendicular, line_coordinate);
|
||||||
|
let second_line = project_to_line(second, perpendicular, line_coordinate);
|
||||||
|
let mut result = DimensionLinear::new(world(plane, first), world(plane, second));
|
||||||
|
result.rotation = rotation;
|
||||||
|
result.definition_point = world(plane, second_line);
|
||||||
|
result.base.definition_point = result.definition_point;
|
||||||
|
result.base.text_middle_point = world(plane, first_line.lerp(second_line, 0.5));
|
||||||
|
result.base.insertion_point = result.base.text_middle_point;
|
||||||
|
Dimension::Linear(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selected_linear_end(first: Vec2, second: Vec2, pick: Option<Vec2>) -> Vec2 {
|
||||||
|
pick.map_or(second, |point| {
|
||||||
|
if point.distance_squared(first) <= point.distance_squared(second) {
|
||||||
|
first
|
||||||
|
} else {
|
||||||
|
second
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
fn selected_angular_end(vertex: Vec2, ends: [Vec2; 2], pick: Option<Vec2>) -> Vec2 {
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] }); // DimContinueCommand
|
let Some(pick) = pick else {
|
||||||
|
return ends[0];
|
||||||
|
};
|
||||||
|
let rays = ends.map(|end| {
|
||||||
|
Curve::Ray(Ray {
|
||||||
|
origin: vertex.into(),
|
||||||
|
direction: (end - vertex).into(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
nearest_of(rays.iter(), pick.into())
|
||||||
|
.map(|(index, _)| ends[index])
|
||||||
|
.unwrap_or(ends[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn angular2_vertex(lines: [[Vec2; 2]; 2]) -> Option<Vec2> {
|
||||||
|
let first = Curve::XLine(XLine {
|
||||||
|
base: lines[0][0].into(),
|
||||||
|
direction: (lines[0][1] - lines[0][0]).into(),
|
||||||
|
});
|
||||||
|
let second = Curve::XLine(XLine {
|
||||||
|
base: lines[1][0].into(),
|
||||||
|
direction: (lines[1][1] - lines[1][0]).into(),
|
||||||
|
});
|
||||||
|
let scale = (lines[0][1] - lines[0][0])
|
||||||
|
.length()
|
||||||
|
.max((lines[1][1] - lines[1][0]).length())
|
||||||
|
.max(1.0);
|
||||||
|
intersect(&first, &second, Tolerance::new(scale * 1.0e-9))
|
||||||
|
.first()
|
||||||
|
.map(|crossing| Vec2::from(crossing.point))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn angular_definition_and_text(
|
||||||
|
vertex: Vec2,
|
||||||
|
previous: Vec2,
|
||||||
|
moving: Vec2,
|
||||||
|
radius: f64,
|
||||||
|
) -> Option<(Vec2, Vec2)> {
|
||||||
|
let arc = angular_arc(vertex, previous, moving, radius)?;
|
||||||
|
Some((
|
||||||
|
Vec2::from(arc.point_at(2.0 / 3.0)),
|
||||||
|
Vec2::from(arc.point_at(0.5)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn angular_arc(vertex: Vec2, first: Vec2, second: Vec2, radius: f64) -> Option<Curve> {
|
||||||
|
let first = first.normalize()?;
|
||||||
|
let second = second.normalize()?;
|
||||||
|
if first.distance_squared(second) <= 1.0e-18 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let first_angle = first.angle();
|
||||||
|
let second_angle = second.angle();
|
||||||
|
let (start_angle, end_angle) =
|
||||||
|
if arc_span(first_angle, second_angle) <= std::f64::consts::PI {
|
||||||
|
(first_angle, second_angle)
|
||||||
|
} else {
|
||||||
|
(second_angle, first_angle)
|
||||||
|
};
|
||||||
|
Some(Curve::Arc(Arc {
|
||||||
|
centre: vertex.into(),
|
||||||
|
radius,
|
||||||
|
start_angle,
|
||||||
|
end_angle,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_measurement(dimension: &mut Dimension, plane: Plane) {
|
||||||
|
match dimension {
|
||||||
|
Dimension::Linear(value) => {
|
||||||
|
let first = project(&plane, value.first_point);
|
||||||
|
let second = project(&plane, value.second_point);
|
||||||
|
let axis = Vec2::new(value.rotation.cos(), value.rotation.sin());
|
||||||
|
value.base.actual_measurement = (second - first).dot(axis).abs();
|
||||||
|
}
|
||||||
|
Dimension::Angular2Ln(value) => {
|
||||||
|
value.base.actual_measurement = value.measurement_degrees()
|
||||||
|
}
|
||||||
|
Dimension::Angular3Pt(value) => {
|
||||||
|
value.base.actual_measurement = value.measurement_degrees()
|
||||||
|
}
|
||||||
|
Dimension::Ordinate(value) => value.refresh_measurement(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview_for_dimension(dimension: &Dimension) -> WireModel {
|
||||||
|
let mut points = Vec::<[f64; 3]>::new();
|
||||||
|
match dimension {
|
||||||
|
Dimension::Linear(value) => {
|
||||||
|
let plane = plane_from_normal(value.first_point, value.base.normal);
|
||||||
|
linear_preview(
|
||||||
|
&mut points,
|
||||||
|
plane,
|
||||||
|
project(&plane, value.first_point),
|
||||||
|
project(&plane, value.second_point),
|
||||||
|
value.rotation,
|
||||||
|
project(&plane, value.definition_point),
|
||||||
|
project(&plane, value.base.text_middle_point),
|
||||||
|
value.base.actual_measurement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Dimension::Angular2Ln(value) => {
|
||||||
|
let plane = plane_from_normal(value.first_point, value.base.normal);
|
||||||
|
let lines = [
|
||||||
|
[
|
||||||
|
project(&plane, value.first_point),
|
||||||
|
project(&plane, value.second_point),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
project(&plane, value.angle_vertex),
|
||||||
|
project(&plane, value.definition_point),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
let vertex = angular2_vertex(lines).unwrap_or(lines[1][0]);
|
||||||
|
angular_preview(
|
||||||
|
&mut points,
|
||||||
|
plane,
|
||||||
|
vertex,
|
||||||
|
lines[0][1] - vertex,
|
||||||
|
lines[1][1] - vertex,
|
||||||
|
project(&plane, value.dimension_arc),
|
||||||
|
project(&plane, value.base.text_middle_point),
|
||||||
|
value.base.actual_measurement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Dimension::Angular3Pt(value) => {
|
||||||
|
let plane = plane_from_normal(value.angle_vertex, value.base.normal);
|
||||||
|
let vertex = project(&plane, value.angle_vertex);
|
||||||
|
angular_preview(
|
||||||
|
&mut points,
|
||||||
|
plane,
|
||||||
|
vertex,
|
||||||
|
project(&plane, value.first_point) - vertex,
|
||||||
|
project(&plane, value.second_point) - vertex,
|
||||||
|
project(&plane, value.definition_point),
|
||||||
|
project(&plane, value.base.text_middle_point),
|
||||||
|
value.base.actual_measurement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Dimension::Ordinate(value) => {
|
||||||
|
let plane = plane_from_normal(value.definition_point, value.base.normal);
|
||||||
|
ordinate_preview(
|
||||||
|
&mut points,
|
||||||
|
plane,
|
||||||
|
project(&plane, value.feature_location),
|
||||||
|
project(&plane, value.leader_endpoint),
|
||||||
|
value.base.actual_measurement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
WireModel::solid_f64(
|
||||||
|
"dimcontinue_preview".to_string(),
|
||||||
|
points,
|
||||||
|
WireModel::CYAN,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn linear_preview(
|
||||||
|
points: &mut Vec<[f64; 3]>,
|
||||||
|
plane: Plane,
|
||||||
|
first: Vec2,
|
||||||
|
second: Vec2,
|
||||||
|
rotation: f64,
|
||||||
|
definition: Vec2,
|
||||||
|
text: Vec2,
|
||||||
|
measurement: f64,
|
||||||
|
) {
|
||||||
|
let axis = Vec2::new(rotation.cos(), rotation.sin());
|
||||||
|
let perpendicular = axis.perpendicular();
|
||||||
|
let coordinate = definition.dot(perpendicular);
|
||||||
|
let first_line = project_to_line(first, perpendicular, coordinate);
|
||||||
|
let second_line = project_to_line(second, perpendicular, coordinate);
|
||||||
|
segment(points, plane, first, first_line);
|
||||||
|
segment(points, plane, second, second_line);
|
||||||
|
segment(points, plane, first_line, second_line);
|
||||||
|
arrow(
|
||||||
|
points,
|
||||||
|
plane,
|
||||||
|
first_line,
|
||||||
|
axis,
|
||||||
|
perpendicular,
|
||||||
|
first_line.distance(second_line),
|
||||||
|
);
|
||||||
|
arrow(
|
||||||
|
points,
|
||||||
|
plane,
|
||||||
|
second_line,
|
||||||
|
-axis,
|
||||||
|
perpendicular,
|
||||||
|
first_line.distance(second_line),
|
||||||
|
);
|
||||||
|
text_box(points, plane, text, axis, perpendicular, measurement);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn angular_preview(
|
||||||
|
points: &mut Vec<[f64; 3]>,
|
||||||
|
plane: Plane,
|
||||||
|
vertex: Vec2,
|
||||||
|
first: Vec2,
|
||||||
|
second: Vec2,
|
||||||
|
arc_point: Vec2,
|
||||||
|
text: Vec2,
|
||||||
|
measurement: f64,
|
||||||
|
) {
|
||||||
|
let radius = arc_point.distance(vertex).max(1.0e-9);
|
||||||
|
let Some(first_direction) = first.normalize() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(second_direction) = second.normalize() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(arc) = angular_arc(vertex, first, second, radius) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
segment(points, plane, vertex, vertex + first_direction * radius);
|
||||||
|
segment(points, plane, vertex, vertex + second_direction * radius);
|
||||||
|
for index in 0..=24 {
|
||||||
|
push_local(points, plane, Vec2::from(arc.point_at(index as f64 / 24.0)));
|
||||||
|
}
|
||||||
|
points.push([f64::NAN; 3]);
|
||||||
|
let start = Vec2::from(arc.point_at(0.0));
|
||||||
|
let start_next = Vec2::from(arc.point_at(1.0 / 1024.0));
|
||||||
|
let end = Vec2::from(arc.point_at(1.0));
|
||||||
|
let end_previous = Vec2::from(arc.point_at(1.0 - 1.0 / 1024.0));
|
||||||
|
arrow(
|
||||||
|
points,
|
||||||
|
plane,
|
||||||
|
start,
|
||||||
|
(start_next - start).normalize().unwrap_or(first_direction),
|
||||||
|
first_direction,
|
||||||
|
radius,
|
||||||
|
);
|
||||||
|
arrow(
|
||||||
|
points,
|
||||||
|
plane,
|
||||||
|
end,
|
||||||
|
(end_previous - end).normalize().unwrap_or(-second_direction),
|
||||||
|
second_direction,
|
||||||
|
radius,
|
||||||
|
);
|
||||||
|
let before_middle = Vec2::from(arc.point_at(0.5 - 1.0 / 1024.0));
|
||||||
|
let after_middle = Vec2::from(arc.point_at(0.5 + 1.0 / 1024.0));
|
||||||
|
let text_axis = (after_middle - before_middle)
|
||||||
|
.normalize()
|
||||||
|
.unwrap_or(Vec2::new(1.0, 0.0));
|
||||||
|
text_box(
|
||||||
|
points,
|
||||||
|
plane,
|
||||||
|
text,
|
||||||
|
text_axis,
|
||||||
|
text_axis.perpendicular(),
|
||||||
|
measurement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ordinate_preview(
|
||||||
|
points: &mut Vec<[f64; 3]>,
|
||||||
|
plane: Plane,
|
||||||
|
feature: Vec2,
|
||||||
|
leader: Vec2,
|
||||||
|
measurement: f64,
|
||||||
|
) {
|
||||||
|
segment(points, plane, feature, leader);
|
||||||
|
let axis = (leader - feature)
|
||||||
|
.normalize()
|
||||||
|
.unwrap_or(Vec2::new(0.0, 1.0));
|
||||||
|
let perpendicular = axis.perpendicular();
|
||||||
|
arrow(
|
||||||
|
points,
|
||||||
|
plane,
|
||||||
|
feature,
|
||||||
|
axis,
|
||||||
|
perpendicular,
|
||||||
|
feature.distance(leader),
|
||||||
|
);
|
||||||
|
text_box(
|
||||||
|
points,
|
||||||
|
plane,
|
||||||
|
leader,
|
||||||
|
perpendicular,
|
||||||
|
axis,
|
||||||
|
measurement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arrow(
|
||||||
|
points: &mut Vec<[f64; 3]>,
|
||||||
|
plane: Plane,
|
||||||
|
tip: Vec2,
|
||||||
|
inward: Vec2,
|
||||||
|
perpendicular: Vec2,
|
||||||
|
span: f64,
|
||||||
|
) {
|
||||||
|
let inward = inward.normalize().unwrap_or(Vec2::new(1.0, 0.0));
|
||||||
|
let perpendicular = perpendicular
|
||||||
|
.normalize()
|
||||||
|
.unwrap_or(Vec2::new(0.0, 1.0));
|
||||||
|
let size = span.clamp(1.0, 100.0) * 0.035;
|
||||||
|
let back = tip + inward * size;
|
||||||
|
segment(
|
||||||
|
points,
|
||||||
|
plane,
|
||||||
|
tip,
|
||||||
|
back + perpendicular * size * 0.4,
|
||||||
|
);
|
||||||
|
segment(
|
||||||
|
points,
|
||||||
|
plane,
|
||||||
|
tip,
|
||||||
|
back - perpendicular * size * 0.4,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_box(
|
||||||
|
points: &mut Vec<[f64; 3]>,
|
||||||
|
plane: Plane,
|
||||||
|
center: Vec2,
|
||||||
|
axis: Vec2,
|
||||||
|
perpendicular: Vec2,
|
||||||
|
measurement: f64,
|
||||||
|
) {
|
||||||
|
let digits = if measurement.abs() < 1.0 {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
measurement.abs().log10().floor() + 1.0
|
||||||
|
};
|
||||||
|
let half_width = (digits + 2.0) * 0.12;
|
||||||
|
let half_height = 0.16;
|
||||||
|
let a = center - axis * half_width - perpendicular * half_height;
|
||||||
|
let b = center + axis * half_width - perpendicular * half_height;
|
||||||
|
let c = center + axis * half_width + perpendicular * half_height;
|
||||||
|
let d = center - axis * half_width + perpendicular * half_height;
|
||||||
|
for point in [a, b, c, d, a] {
|
||||||
|
push_local(points, plane, point);
|
||||||
|
}
|
||||||
|
points.push([f64::NAN; 3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_to_line(point: Vec2, perpendicular: Vec2, coordinate: f64) -> Vec2 {
|
||||||
|
let line = Curve::XLine(XLine {
|
||||||
|
base: (perpendicular * coordinate).into(),
|
||||||
|
direction: perpendicular.perpendicular().into(),
|
||||||
|
});
|
||||||
|
Vec2::from(closest_point(&line, point.into()).point)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plane_from_normal(origin: Vector3, normal: Vector3) -> Plane {
|
||||||
|
let (x_axis, y_axis) =
|
||||||
|
crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z));
|
||||||
|
Plane::from_axes(
|
||||||
|
point(origin),
|
||||||
|
[x_axis.0, x_axis.1, x_axis.2],
|
||||||
|
[y_axis.0, y_axis.1, y_axis.2],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project(plane: &Plane, value: Vector3) -> Vec2 {
|
||||||
|
Vec2::from(plane.project(point(value)).unwrap_or([0.0; 2]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn world(plane: Plane, value: Vec2) -> Vector3 {
|
||||||
|
let value = plane.point_at(value.into());
|
||||||
|
Vector3::new(value[0], value[1], value[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point(value: Vector3) -> [f64; 3] {
|
||||||
|
[value.x, value.y, value.z]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn segment(points: &mut Vec<[f64; 3]>, plane: Plane, first: Vec2, second: Vec2) {
|
||||||
|
push_local(points, plane, first);
|
||||||
|
push_local(points, plane, second);
|
||||||
|
points.push([f64::NAN; 3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_local(points: &mut Vec<[f64; 3]>, plane: Plane, point: Vec2) {
|
||||||
|
points.push(plane.point_at(point.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] });
|
||||||
@@ -139,6 +139,8 @@ impl JoggedRadiusDimensionCommand {
|
|||||||
dimension,
|
dimension,
|
||||||
))),
|
))),
|
||||||
association,
|
association,
|
||||||
|
preserve_base_style: false,
|
||||||
|
continue_command: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,6 +187,8 @@ impl CadCommand for LinearDimensionCommand {
|
|||||||
CmdResult::CommitDimension {
|
CmdResult::CommitDimension {
|
||||||
entity,
|
entity,
|
||||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||||
|
preserve_base_style: false,
|
||||||
|
continue_command: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// MLEADER: pick the arrow and elbow, then edit the text.
|
// MLEADER: pick the arrow and elbow, then edit the text.
|
||||||
|
|
||||||
use acadrust::entities::MultiLeader;
|
use acadrust::entities::{LeaderContentType, MultiLeader, MultiLeaderPathType};
|
||||||
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, 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;
|
||||||
@@ -26,6 +26,47 @@ pub struct MLeaderCommand {
|
|||||||
plane: WorkingPlane,
|
plane: WorkingPlane,
|
||||||
style: Option<acadrust::objects::MultiLeaderStyle>,
|
style: Option<acadrust::objects::MultiLeaderStyle>,
|
||||||
display_scale: f64,
|
display_scale: f64,
|
||||||
|
order: CreationOrder,
|
||||||
|
step: Step,
|
||||||
|
content_type: LeaderContentType,
|
||||||
|
path_type: MultiLeaderPathType,
|
||||||
|
enable_landing: bool,
|
||||||
|
landing_length: f64,
|
||||||
|
max_points: usize,
|
||||||
|
first_angle: Option<f64>,
|
||||||
|
second_angle: Option<f64>,
|
||||||
|
layer: String,
|
||||||
|
text: String,
|
||||||
|
picked_entity: Option<EntityType>,
|
||||||
|
selected_mtext: Option<acadrust::entities::MText>,
|
||||||
|
block_sources: Vec<(String, acadrust::Handle)>,
|
||||||
|
block_handle: Option<acadrust::Handle>,
|
||||||
|
layers: Vec<String>,
|
||||||
|
text_styles: Vec<(String, acadrust::Handle)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum CreationOrder {
|
||||||
|
ArrowFirst,
|
||||||
|
LandingFirst,
|
||||||
|
ContentFirst,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Step {
|
||||||
|
PickPoints,
|
||||||
|
Options,
|
||||||
|
LeaderType,
|
||||||
|
Landing,
|
||||||
|
LandingLength,
|
||||||
|
ContentType,
|
||||||
|
MaxPoints,
|
||||||
|
FirstAngle,
|
||||||
|
SecondAngle,
|
||||||
|
Layer,
|
||||||
|
PreEnterText,
|
||||||
|
SelectMText,
|
||||||
|
BlockSource,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MLeaderCommand {
|
impl MLeaderCommand {
|
||||||
@@ -35,6 +76,23 @@ impl MLeaderCommand {
|
|||||||
plane: WorkingPlane::default(),
|
plane: WorkingPlane::default(),
|
||||||
style: None,
|
style: None,
|
||||||
display_scale: 1.0,
|
display_scale: 1.0,
|
||||||
|
order: CreationOrder::ArrowFirst,
|
||||||
|
step: Step::PickPoints,
|
||||||
|
content_type: LeaderContentType::MText,
|
||||||
|
path_type: MultiLeaderPathType::StraightLineSegments,
|
||||||
|
enable_landing: true,
|
||||||
|
landing_length: 2.5,
|
||||||
|
max_points: 2,
|
||||||
|
first_angle: None,
|
||||||
|
second_angle: None,
|
||||||
|
layer: String::new(),
|
||||||
|
text: String::new(),
|
||||||
|
picked_entity: None,
|
||||||
|
selected_mtext: None,
|
||||||
|
block_sources: Vec::new(),
|
||||||
|
block_handle: None,
|
||||||
|
layers: Vec::new(),
|
||||||
|
text_styles: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,10 +108,214 @@ impl MLeaderCommand {
|
|||||||
Self {
|
Self {
|
||||||
verts: Vec::new(),
|
verts: Vec::new(),
|
||||||
plane: WorkingPlane::default(),
|
plane: WorkingPlane::default(),
|
||||||
style: Some(style),
|
style: Some(style.clone()),
|
||||||
display_scale,
|
display_scale,
|
||||||
|
order: CreationOrder::ArrowFirst,
|
||||||
|
step: Step::PickPoints,
|
||||||
|
content_type: (style.content_type as i16).into(),
|
||||||
|
path_type: (style.path_type as i16).into(),
|
||||||
|
enable_landing: style.enable_landing,
|
||||||
|
landing_length: style.landing_distance,
|
||||||
|
max_points: style.max_leader_points.max(2) as usize,
|
||||||
|
first_angle: (style.first_segment_angle.abs() > 1.0e-12)
|
||||||
|
.then_some(style.first_segment_angle),
|
||||||
|
second_angle: (style.second_segment_angle.abs() > 1.0e-12)
|
||||||
|
.then_some(style.second_segment_angle),
|
||||||
|
layer: String::new(),
|
||||||
|
text: style.default_text.clone(),
|
||||||
|
picked_entity: None,
|
||||||
|
selected_mtext: None,
|
||||||
|
block_sources: Vec::new(),
|
||||||
|
block_handle: style.block_content_handle,
|
||||||
|
layers: Vec::new(),
|
||||||
|
text_styles: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_drawing_resources(
|
||||||
|
mut self,
|
||||||
|
block_sources: Vec<(String, acadrust::Handle)>,
|
||||||
|
layers: Vec<String>,
|
||||||
|
text_styles: Vec<(String, acadrust::Handle)>,
|
||||||
|
) -> Self {
|
||||||
|
self.block_sources = block_sources;
|
||||||
|
self.layers = layers;
|
||||||
|
self.text_styles = text_styles;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_limit(&self) -> usize {
|
||||||
|
if self.order == CreationOrder::ContentFirst {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
self.max_points.max(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&self) -> CmdResult {
|
||||||
|
if self.verts.len() < 2 {
|
||||||
|
return CmdResult::Cancel;
|
||||||
|
}
|
||||||
|
let local: Vec<DVec3> = self
|
||||||
|
.verts
|
||||||
|
.iter()
|
||||||
|
.map(|point| self.plane.to_local(*point))
|
||||||
|
.collect();
|
||||||
|
let (leader_points, content_point) = match self.order {
|
||||||
|
CreationOrder::ArrowFirst => (local, None),
|
||||||
|
CreationOrder::LandingFirst => {
|
||||||
|
let mut points = local;
|
||||||
|
points.reverse();
|
||||||
|
(points, None)
|
||||||
|
}
|
||||||
|
CreationOrder::ContentFirst => {
|
||||||
|
let content = local[0];
|
||||||
|
let arrow = local[1];
|
||||||
|
let sign = if content.x >= arrow.x { 1.0 } else { -1.0 };
|
||||||
|
let elbow = DVec3::new(
|
||||||
|
content.x - sign * self.landing_length * self.display_scale,
|
||||||
|
content.y,
|
||||||
|
content.z,
|
||||||
|
);
|
||||||
|
(vec![arrow, elbow], Some(content))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut ml = build_mleader(
|
||||||
|
&self.text,
|
||||||
|
&leader_points,
|
||||||
|
content_point,
|
||||||
|
Mat4::IDENTITY,
|
||||||
|
self.style.as_ref(),
|
||||||
|
self.display_scale,
|
||||||
|
);
|
||||||
|
ml.content_type = self.content_type;
|
||||||
|
ml.path_type = self.path_type;
|
||||||
|
ml.enable_landing = self.enable_landing;
|
||||||
|
ml.enable_dogleg = self.enable_landing;
|
||||||
|
ml.dogleg_length = self.landing_length;
|
||||||
|
if self.content_type == LeaderContentType::Block {
|
||||||
|
ml.block_content_handle = self.block_handle;
|
||||||
|
}
|
||||||
|
if !self.layer.trim().is_empty() {
|
||||||
|
ml.common.layer = format!("__MLEADER_LAYER__{}", self.layer.trim());
|
||||||
|
}
|
||||||
|
for root in &mut ml.context.leader_roots {
|
||||||
|
root.landing_distance = self.landing_length * self.display_scale;
|
||||||
|
for line in &mut root.lines {
|
||||||
|
line.path_type = self.path_type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalize_content_context(&mut ml);
|
||||||
|
if let Some(source) = &self.selected_mtext {
|
||||||
|
let scale = self.display_scale.max(1.0e-12);
|
||||||
|
ml.text_height = source.height / scale;
|
||||||
|
ml.context.text_height = source.height;
|
||||||
|
ml.context.text_width = source.rectangle_width;
|
||||||
|
ml.context.text_rotation = source.rotation;
|
||||||
|
ml.context.text_direction = source.dwg_x_direction.unwrap_or_else(|| {
|
||||||
|
Vector3::new(source.rotation.cos(), source.rotation.sin(), 0.0)
|
||||||
|
});
|
||||||
|
ml.context.line_spacing_factor = source.line_spacing_factor;
|
||||||
|
ml.context.line_spacing_style = source.line_spacing_style;
|
||||||
|
ml.text_color = source.common.color;
|
||||||
|
ml.context.text_color = source.common.color;
|
||||||
|
ml.text_frame = source.background_fill_flags & 0x10 != 0;
|
||||||
|
ml.context.background_fill_enabled = source.background_fill_flags & 0x01 != 0;
|
||||||
|
ml.context.background_mask_fill_on = source.background_fill_flags & 0x02 != 0;
|
||||||
|
ml.context.background_scale_factor = source.background_scale;
|
||||||
|
ml.context.background_fill_color = source.background_color;
|
||||||
|
ml.context.background_transparency = source.background_transparency;
|
||||||
|
ml.context.text_flow_direction = match source.drawing_direction {
|
||||||
|
acadrust::entities::mtext::DrawingDirection::LeftToRight => {
|
||||||
|
acadrust::entities::multileader::FlowDirectionType::Horizontal
|
||||||
|
}
|
||||||
|
acadrust::entities::mtext::DrawingDirection::TopToBottom => {
|
||||||
|
acadrust::entities::multileader::FlowDirectionType::Vertical
|
||||||
|
}
|
||||||
|
acadrust::entities::mtext::DrawingDirection::ByStyle => {
|
||||||
|
acadrust::entities::multileader::FlowDirectionType::ByStyle
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (attachment, alignment) = match source.attachment_point {
|
||||||
|
acadrust::entities::mtext::AttachmentPoint::TopCenter
|
||||||
|
| acadrust::entities::mtext::AttachmentPoint::MiddleCenter
|
||||||
|
| acadrust::entities::mtext::AttachmentPoint::BottomCenter => {
|
||||||
|
(
|
||||||
|
acadrust::entities::multileader::TextAttachmentPointType::Center,
|
||||||
|
acadrust::entities::TextAlignmentType::Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
acadrust::entities::mtext::AttachmentPoint::TopRight
|
||||||
|
| acadrust::entities::mtext::AttachmentPoint::MiddleRight
|
||||||
|
| acadrust::entities::mtext::AttachmentPoint::BottomRight => {
|
||||||
|
(
|
||||||
|
acadrust::entities::multileader::TextAttachmentPointType::Right,
|
||||||
|
acadrust::entities::TextAlignmentType::Right,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => (
|
||||||
|
acadrust::entities::multileader::TextAttachmentPointType::Left,
|
||||||
|
acadrust::entities::TextAlignmentType::Left,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
ml.context.text_attachment_point = attachment;
|
||||||
|
ml.text_alignment = alignment;
|
||||||
|
ml.context.text_alignment = alignment;
|
||||||
|
let style_handle = self
|
||||||
|
.text_styles
|
||||||
|
.iter()
|
||||||
|
.find(|(name, _)| name.eq_ignore_ascii_case(&source.style))
|
||||||
|
.map(|(_, handle)| *handle);
|
||||||
|
ml.text_style_handle = style_handle;
|
||||||
|
ml.context.text_style_handle = style_handle;
|
||||||
|
}
|
||||||
|
let entity = self.plane.place_entity(EntityType::MultiLeader(ml));
|
||||||
|
if self.content_type == LeaderContentType::MText && self.text.is_empty() {
|
||||||
|
CmdResult::CommitAndEditText(entity)
|
||||||
|
} else {
|
||||||
|
CmdResult::CommitAndExit(entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn constrained_point(&self, point: DVec3) -> DVec3 {
|
||||||
|
let Some(previous) = self.verts.last().copied() else {
|
||||||
|
return point;
|
||||||
|
};
|
||||||
|
let segment_index = self.verts.len() - 1;
|
||||||
|
let constraint = match (self.order, segment_index) {
|
||||||
|
(CreationOrder::LandingFirst, 0) => self.second_angle,
|
||||||
|
(CreationOrder::LandingFirst, 1) => self.first_angle,
|
||||||
|
(_, 0) => self.first_angle,
|
||||||
|
(_, 1) => self.second_angle,
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let Some(angle) = constraint else {
|
||||||
|
return point;
|
||||||
|
};
|
||||||
|
let previous_local = self.plane.to_local(previous);
|
||||||
|
let point_local = self.plane.to_local(point);
|
||||||
|
let delta = point_local - previous_local;
|
||||||
|
let length = delta.truncate().length();
|
||||||
|
if length <= 1.0e-12 {
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
let candidates = [angle, -angle, std::f64::consts::PI - angle, angle - std::f64::consts::PI];
|
||||||
|
let current = delta.y.atan2(delta.x);
|
||||||
|
let chosen = candidates
|
||||||
|
.into_iter()
|
||||||
|
.min_by(|a, b| {
|
||||||
|
angle_delta(current, *a)
|
||||||
|
.partial_cmp(&angle_delta(current, *b))
|
||||||
|
.unwrap_or(std::cmp::Ordering::Equal)
|
||||||
|
})
|
||||||
|
.unwrap_or(angle);
|
||||||
|
let local = DVec3::new(
|
||||||
|
previous_local.x + length * chosen.cos(),
|
||||||
|
previous_local.y + length * chosen.sin(),
|
||||||
|
point_local.z,
|
||||||
|
);
|
||||||
|
self.plane.to_world(local)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CadCommand for MLeaderCommand {
|
impl CadCommand for MLeaderCommand {
|
||||||
@@ -66,60 +328,86 @@ impl CadCommand for MLeaderCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
if self.verts.is_empty() {
|
match self.step {
|
||||||
t!("MLEADER Specify arrowhead point:").into_owned()
|
Step::Options => t!("MLEADER Options [Leader type/Landing/Content type/Max points/First angle/Second angle/Layer/Exit]:").into_owned(),
|
||||||
} else {
|
Step::LeaderType => t!("MLEADER Leader type [Straight/Spline/None]:").into_owned(),
|
||||||
t!("MLEADER Specify landing point:").into_owned()
|
Step::Landing => t!("MLEADER Leader landing [Yes/No]:").into_owned(),
|
||||||
|
Step::LandingLength => t!("MLEADER Specify landing distance:").into_owned(),
|
||||||
|
Step::ContentType => t!("MLEADER Content type [MText/Block/None]:").into_owned(),
|
||||||
|
Step::MaxPoints => t!("MLEADER Enter maximum leader points:").into_owned(),
|
||||||
|
Step::FirstAngle => t!("MLEADER First segment angle [Any/15/30/45/60/90]:").into_owned(),
|
||||||
|
Step::SecondAngle => t!("MLEADER Second segment angle [Any/15/30/45/60/90]:").into_owned(),
|
||||||
|
Step::Layer => t!("MLEADER Enter layer name:").into_owned(),
|
||||||
|
Step::PreEnterText => t!("MLEADER Enter text:").into_owned(),
|
||||||
|
Step::SelectMText => t!("MLEADER Select an MText object:").into_owned(),
|
||||||
|
Step::BlockSource => t!("MLEADER Enter source block name:").into_owned(),
|
||||||
|
Step::PickPoints if self.verts.is_empty() => match self.order {
|
||||||
|
CreationOrder::ArrowFirst => t!("MLEADER Specify arrowhead point or [Landing first/Content first/Text/Select MText/Options]:").into_owned(),
|
||||||
|
CreationOrder::LandingFirst => t!("MLEADER Specify landing point or [Arrowhead first/Content first/Text/Select MText/Options]:").into_owned(),
|
||||||
|
CreationOrder::ContentFirst => t!("MLEADER Specify content location or [Arrowhead first/Landing first/Text/Select MText/Options]:").into_owned(),
|
||||||
|
},
|
||||||
|
Step::PickPoints if self.order == CreationOrder::ContentFirst => {
|
||||||
|
t!("MLEADER Specify arrowhead point:").into_owned()
|
||||||
|
}
|
||||||
|
Step::PickPoints => t!("MLEADER Specify next leader point or press Enter to finish:").into_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
match self.step {
|
||||||
|
Step::PickPoints if self.verts.is_empty() => vec![
|
||||||
|
CmdOption::new("Arrowhead first", "A"),
|
||||||
|
CmdOption::new("Landing first", "L"),
|
||||||
|
CmdOption::new("Content first", "C"),
|
||||||
|
CmdOption::new("Text", "T"),
|
||||||
|
CmdOption::new("Select MText", "S"),
|
||||||
|
CmdOption::new("Options", "O"),
|
||||||
|
],
|
||||||
|
Step::Options => vec![
|
||||||
|
CmdOption::new("Leader type", "LT"),
|
||||||
|
CmdOption::new("Landing", "LD"),
|
||||||
|
CmdOption::new("Content type", "CT"),
|
||||||
|
CmdOption::new("Max points", "M"),
|
||||||
|
CmdOption::new("First angle", "F"),
|
||||||
|
CmdOption::new("Second angle", "S"),
|
||||||
|
CmdOption::new("Layer", "LA"),
|
||||||
|
CmdOption::new("Exit", "X"),
|
||||||
|
],
|
||||||
|
Step::LeaderType => ["Straight", "Spline", "None"].into_iter().map(|v| CmdOption::new(v, v)).collect(),
|
||||||
|
Step::Landing => ["Yes", "No"].into_iter().map(|v| CmdOption::new(v, v)).collect(),
|
||||||
|
Step::ContentType => ["MText", "Block", "None"].into_iter().map(|v| CmdOption::new(v, v)).collect(),
|
||||||
|
Step::BlockSource => self
|
||||||
|
.block_sources
|
||||||
|
.iter()
|
||||||
|
.map(|(name, _)| CmdOption::new(name, name))
|
||||||
|
.collect(),
|
||||||
|
Step::Layer => self
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.map(|name| CmdOption::new(name, name))
|
||||||
|
.collect(),
|
||||||
|
Step::FirstAngle | Step::SecondAngle => ["Any", "15", "30", "45", "60", "90"].into_iter().map(|v| CmdOption::new(v, v)).collect(),
|
||||||
|
_ => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
self.verts.push(pt);
|
if self.step != Step::PickPoints {
|
||||||
|
|
||||||
if self.verts.len() < 2 {
|
|
||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
|
self.verts.push(self.constrained_point(pt));
|
||||||
|
|
||||||
let local: Vec<DVec3> = self
|
if self.verts.len() < self.point_limit() {
|
||||||
.verts
|
return CmdResult::NeedPoint;
|
||||||
.iter()
|
}
|
||||||
.map(|point| self.plane.to_local(*point))
|
self.finish()
|
||||||
.collect();
|
|
||||||
|
|
||||||
let ml = build_mleader(
|
|
||||||
"",
|
|
||||||
&local,
|
|
||||||
Mat4::IDENTITY,
|
|
||||||
self.style.as_ref(),
|
|
||||||
self.display_scale,
|
|
||||||
);
|
|
||||||
|
|
||||||
CmdResult::CommitAndEditText(
|
|
||||||
self.plane.place_entity(EntityType::MultiLeader(ml)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
if self.verts.len() < 2 {
|
if self.step != Step::PickPoints || self.verts.len() < 2 {
|
||||||
return CmdResult::Cancel;
|
return CmdResult::Cancel;
|
||||||
}
|
}
|
||||||
// Place the leader with empty text, then open the in-place MText editor
|
self.finish()
|
||||||
// so the user types the annotation into the rich editor.
|
|
||||||
let local: Vec<DVec3> = self
|
|
||||||
.verts
|
|
||||||
.iter()
|
|
||||||
.map(|point| self.plane.to_local(*point))
|
|
||||||
.collect();
|
|
||||||
let ml = build_mleader(
|
|
||||||
"",
|
|
||||||
&local,
|
|
||||||
Mat4::IDENTITY,
|
|
||||||
self.style.as_ref(),
|
|
||||||
self.display_scale,
|
|
||||||
);
|
|
||||||
CmdResult::CommitAndEditText(
|
|
||||||
self.plane.place_entity(EntityType::MultiLeader(ml)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_escape(&mut self) -> CmdResult {
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
@@ -127,16 +415,37 @@ impl CadCommand for MLeaderCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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::PickPoints || self.verts.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// Preview / rubber-band is GPU screen-space: downcast to f32 here.
|
// Preview / rubber-band is GPU screen-space: downcast to f32 here.
|
||||||
let mut pts: Vec<Vec3> = self
|
let mut local_points: Vec<DVec3> = self
|
||||||
.verts
|
.verts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|point| self.plane.to_local(*point).as_vec3())
|
.map(|point| self.plane.to_local(*point))
|
||||||
.collect();
|
.collect();
|
||||||
pts.push(self.plane.to_local(pt).as_vec3());
|
local_points.push(self.plane.to_local(self.constrained_point(pt)));
|
||||||
|
let pts: Vec<Vec3> = match self.order {
|
||||||
|
CreationOrder::ArrowFirst => local_points,
|
||||||
|
CreationOrder::LandingFirst => {
|
||||||
|
local_points.reverse();
|
||||||
|
local_points
|
||||||
|
}
|
||||||
|
CreationOrder::ContentFirst => {
|
||||||
|
let content = local_points[0];
|
||||||
|
let arrow = *local_points.last().unwrap_or(&content);
|
||||||
|
let sign = if content.x >= arrow.x { 1.0 } else { -1.0 };
|
||||||
|
let elbow = DVec3::new(
|
||||||
|
content.x - sign * self.landing_length * self.display_scale,
|
||||||
|
content.y,
|
||||||
|
content.z,
|
||||||
|
);
|
||||||
|
vec![arrow, elbow]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.into_iter()
|
||||||
|
.map(DVec3::as_vec3)
|
||||||
|
.collect();
|
||||||
let arrow_size = self
|
let arrow_size = self
|
||||||
.style
|
.style
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -159,6 +468,178 @@ impl CadCommand for MLeaderCommand {
|
|||||||
.collect();
|
.collect();
|
||||||
Some(preview)
|
Some(preview)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn wants_text_input(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants_text_with_spaces(&self) -> bool {
|
||||||
|
matches!(self.step, Step::Layer | Step::PreEnterText)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
|
self.step == Step::PickPoints
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, input: &str) -> Option<CmdResult> {
|
||||||
|
let text = input.trim();
|
||||||
|
let upper = text.to_ascii_uppercase();
|
||||||
|
match self.step {
|
||||||
|
Step::PickPoints if self.verts.is_empty() => match upper.as_str() {
|
||||||
|
"A" | "ARROWHEAD" | "ARROWHEAD FIRST" => self.order = CreationOrder::ArrowFirst,
|
||||||
|
"L" | "LANDING" | "LANDING FIRST" => self.order = CreationOrder::LandingFirst,
|
||||||
|
"C" | "CONTENT" | "CONTENT FIRST" => self.order = CreationOrder::ContentFirst,
|
||||||
|
"T" | "TEXT" => self.step = Step::PreEnterText,
|
||||||
|
"S" | "SELECT" | "SELECT MTEXT" => self.step = Step::SelectMText,
|
||||||
|
"O" | "OPTIONS" => self.step = Step::Options,
|
||||||
|
_ => return None,
|
||||||
|
},
|
||||||
|
Step::Options => match upper.as_str() {
|
||||||
|
"LT" | "LEADER TYPE" => self.step = Step::LeaderType,
|
||||||
|
"LD" | "LANDING" => self.step = Step::Landing,
|
||||||
|
"CT" | "CONTENT TYPE" => self.step = Step::ContentType,
|
||||||
|
"M" | "MAX" | "MAX POINTS" => self.step = Step::MaxPoints,
|
||||||
|
"F" | "FIRST" | "FIRST ANGLE" => self.step = Step::FirstAngle,
|
||||||
|
"S" | "SECOND" | "SECOND ANGLE" => self.step = Step::SecondAngle,
|
||||||
|
"LA" | "LAYER" => self.step = Step::Layer,
|
||||||
|
"X" | "EXIT" => self.step = Step::PickPoints,
|
||||||
|
_ => return None,
|
||||||
|
},
|
||||||
|
Step::LeaderType => {
|
||||||
|
self.path_type = match upper.as_str() {
|
||||||
|
"SPLINE" => MultiLeaderPathType::Spline,
|
||||||
|
"NONE" => MultiLeaderPathType::Invisible,
|
||||||
|
"STRAIGHT" => MultiLeaderPathType::StraightLineSegments,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
self.step = Step::Options;
|
||||||
|
}
|
||||||
|
Step::Landing => match upper.as_str() {
|
||||||
|
"Y" | "YES" => {
|
||||||
|
self.enable_landing = true;
|
||||||
|
self.step = Step::LandingLength;
|
||||||
|
}
|
||||||
|
"N" | "NO" => {
|
||||||
|
self.enable_landing = false;
|
||||||
|
self.step = Step::Options;
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
},
|
||||||
|
Step::LandingLength => {
|
||||||
|
let value = crate::entities::common::parse_typed_length(text)?;
|
||||||
|
if value <= 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.landing_length = value;
|
||||||
|
self.step = Step::Options;
|
||||||
|
}
|
||||||
|
Step::ContentType => {
|
||||||
|
self.content_type = match upper.as_str() {
|
||||||
|
"MTEXT" => LeaderContentType::MText,
|
||||||
|
"BLOCK" => LeaderContentType::Block,
|
||||||
|
"NONE" => LeaderContentType::None,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
self.step = if self.content_type == LeaderContentType::Block
|
||||||
|
&& self.block_handle.is_none()
|
||||||
|
{
|
||||||
|
Step::BlockSource
|
||||||
|
} else {
|
||||||
|
Step::Options
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Step::MaxPoints => {
|
||||||
|
let value = text.parse::<usize>().ok()?;
|
||||||
|
if value < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.max_points = value;
|
||||||
|
self.step = Step::Options;
|
||||||
|
}
|
||||||
|
Step::FirstAngle | Step::SecondAngle => {
|
||||||
|
let angle = if upper == "ANY" {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let degrees = text.parse::<f64>().ok()?;
|
||||||
|
if !matches!(degrees as i32, 15 | 30 | 45 | 60 | 90) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(degrees.to_radians())
|
||||||
|
};
|
||||||
|
if self.step == Step::FirstAngle {
|
||||||
|
self.first_angle = angle;
|
||||||
|
} else {
|
||||||
|
self.second_angle = angle;
|
||||||
|
}
|
||||||
|
self.step = Step::Options;
|
||||||
|
}
|
||||||
|
Step::Layer => {
|
||||||
|
if text.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let Some(layer) = self
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.find(|layer| layer.eq_ignore_ascii_case(text))
|
||||||
|
.cloned()
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
self.layer = layer;
|
||||||
|
self.step = Step::Options;
|
||||||
|
}
|
||||||
|
Step::PreEnterText => {
|
||||||
|
self.text = input.to_string();
|
||||||
|
self.content_type = LeaderContentType::MText;
|
||||||
|
self.step = Step::PickPoints;
|
||||||
|
}
|
||||||
|
Step::SelectMText => return None,
|
||||||
|
Step::BlockSource => {
|
||||||
|
let Some((_, handle)) = self
|
||||||
|
.block_sources
|
||||||
|
.iter()
|
||||||
|
.find(|(name, _)| name.eq_ignore_ascii_case(text))
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
self.block_handle = Some(*handle);
|
||||||
|
self.content_type = LeaderContentType::Block;
|
||||||
|
self.step = Step::Options;
|
||||||
|
}
|
||||||
|
Step::PickPoints => return None,
|
||||||
|
}
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_entity_pick(&self) -> bool {
|
||||||
|
self.step == Step::SelectMText
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_before_entity_pick(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||||
|
self.picked_entity = Some(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_entity_pick(&mut self, _handle: acadrust::Handle, _point: DVec3) -> CmdResult {
|
||||||
|
let Some(EntityType::MText(text)) = self.picked_entity.take() else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
self.verts.clear();
|
||||||
|
self.verts.push(DVec3::new(
|
||||||
|
text.insertion_point.x,
|
||||||
|
text.insertion_point.y,
|
||||||
|
text.insertion_point.z,
|
||||||
|
));
|
||||||
|
self.text = text.value.clone();
|
||||||
|
self.selected_mtext = Some(text);
|
||||||
|
self.content_type = LeaderContentType::MText;
|
||||||
|
self.order = CreationOrder::ContentFirst;
|
||||||
|
self.step = Step::PickPoints;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
@@ -170,18 +651,24 @@ fn v3(p: DVec3) -> Vector3 {
|
|||||||
fn build_mleader(
|
fn build_mleader(
|
||||||
text: &str,
|
text: &str,
|
||||||
verts: &[DVec3],
|
verts: &[DVec3],
|
||||||
|
content_point: Option<DVec3>,
|
||||||
ucs: Mat4,
|
ucs: Mat4,
|
||||||
style: Option<&acadrust::objects::MultiLeaderStyle>,
|
style: Option<&acadrust::objects::MultiLeaderStyle>,
|
||||||
display_scale: f64,
|
display_scale: f64,
|
||||||
) -> MultiLeader {
|
) -> MultiLeader {
|
||||||
// First click is the arrow; second click is the elbow.
|
// First point is the arrow, last point is the elbow. Intermediate points
|
||||||
|
// are leader vertices governed by the configured maximum-point count.
|
||||||
let arrow_pt = verts[0];
|
let arrow_pt = verts[0];
|
||||||
let elbow_pt = verts[1];
|
let elbow_pt = *verts.last().unwrap_or(&verts[0]);
|
||||||
|
|
||||||
let arrow_v3 = v3(arrow_pt);
|
|
||||||
let elbow_v3 = v3(elbow_pt);
|
let elbow_v3 = v3(elbow_pt);
|
||||||
|
|
||||||
let mut ml = MultiLeader::with_text(text, elbow_v3, vec![arrow_v3]);
|
let leader_vertices = verts[..verts.len().saturating_sub(1)]
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(v3)
|
||||||
|
.collect();
|
||||||
|
let mut ml = MultiLeader::with_text(text, elbow_v3, leader_vertices);
|
||||||
if let Some(style) = style {
|
if let Some(style) = style {
|
||||||
crate::scene::annotative::apply_mleader_style(&mut ml, style);
|
crate::scene::annotative::apply_mleader_style(&mut ml, style);
|
||||||
} else {
|
} else {
|
||||||
@@ -225,11 +712,13 @@ fn build_mleader(
|
|||||||
// Place text beyond the landing and gap.
|
// Place text beyond the landing and gap.
|
||||||
let off = landing * (landing_distance + landing_gap) as f32;
|
let off = landing * (landing_distance + landing_gap) as f32;
|
||||||
|
|
||||||
let text_location = Vector3::new(
|
let text_location = content_point.map(v3).unwrap_or_else(|| {
|
||||||
elbow_v3.x + off.x as f64,
|
Vector3::new(
|
||||||
elbow_v3.y + off.y as f64,
|
elbow_v3.x + off.x as f64,
|
||||||
elbow_v3.z,
|
elbow_v3.y + off.y as f64,
|
||||||
);
|
elbow_v3.z,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
ml.context.text_location = text_location;
|
ml.context.text_location = text_location;
|
||||||
ml.context.content_base_point = text_location;
|
ml.context.content_base_point = text_location;
|
||||||
@@ -237,6 +726,34 @@ fn build_mleader(
|
|||||||
ml
|
ml
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_content_context(ml: &mut MultiLeader) {
|
||||||
|
match ml.content_type {
|
||||||
|
LeaderContentType::MText => {
|
||||||
|
ml.context.has_text_contents = true;
|
||||||
|
ml.context.has_block_contents = false;
|
||||||
|
}
|
||||||
|
LeaderContentType::Block => {
|
||||||
|
ml.context.has_text_contents = false;
|
||||||
|
ml.context.has_block_contents = ml.block_content_handle.is_some();
|
||||||
|
ml.context.block_content_handle = ml.block_content_handle;
|
||||||
|
ml.context.block_content_location = ml.context.content_base_point;
|
||||||
|
ml.context.block_content_scale = ml.block_scale;
|
||||||
|
ml.context.block_rotation = ml.block_rotation;
|
||||||
|
ml.context.block_content_color = ml.block_content_color;
|
||||||
|
ml.context.block_connection_type = ml.block_connection_type;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
ml.context.has_text_contents = false;
|
||||||
|
ml.context.has_block_contents = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn angle_delta(a: f64, b: f64) -> f64 {
|
||||||
|
let two_pi = std::f64::consts::TAU;
|
||||||
|
((a - b + std::f64::consts::PI).rem_euclid(two_pi) - std::f64::consts::PI).abs()
|
||||||
|
}
|
||||||
|
|
||||||
fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel {
|
fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel {
|
||||||
let mut points: Vec<[f32; 3]> = pts.iter().map(|p| [p.x, p.y, p.z]).collect();
|
let mut points: Vec<[f32; 3]> = pts.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||||
if pts.len() >= 2 {
|
if pts.len() >= 2 {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
use acadrust::entities::{LeaderLine, MultiLeader};
|
use acadrust::entities::{LeaderLine, MultiLeader};
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
use acadrust::{EntityType, Handle};
|
use acadrust::{EntityType, Handle};
|
||||||
|
use cadkernel::geom2d::{closest_point, Curve, Line};
|
||||||
use glam::{DVec3, Vec3};
|
use glam::{DVec3, Vec3};
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdResult};
|
||||||
@@ -92,6 +93,9 @@ impl CadCommand for MLeaderAddCommand {
|
|||||||
match &mut self.step {
|
match &mut self.step {
|
||||||
AddStep::PickArrowhead { handle, entity } => {
|
AddStep::PickArrowhead { handle, entity } => {
|
||||||
if let Some(ent) = entity.take() {
|
if let Some(ent) = entity.take() {
|
||||||
|
if !matches!(ent, EntityType::MultiLeader(_)) {
|
||||||
|
return CmdResult::Cancel;
|
||||||
|
}
|
||||||
let h = *handle;
|
let h = *handle;
|
||||||
self.step = AddStep::CollectPoints {
|
self.step = AddStep::CollectPoints {
|
||||||
handle: h,
|
handle: h,
|
||||||
@@ -121,14 +125,46 @@ impl CadCommand for MLeaderAddCommand {
|
|||||||
return CmdResult::Cancel;
|
return CmdResult::Cancel;
|
||||||
}
|
}
|
||||||
let h = *handle;
|
let h = *handle;
|
||||||
if let EntityType::MultiLeader(ref mut ml) = entity {
|
if let EntityType::MultiLeader(ref mut ml) = entity {
|
||||||
// Add a new leader root with the collected points
|
|
||||||
let points: Vec<Vector3> = pts
|
let points: Vec<Vector3> = pts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| Vector3::new(p.x, p.y, p.z))
|
.map(|p| Vector3::new(p.x, p.y, p.z))
|
||||||
.collect();
|
.collect();
|
||||||
|
let template_root = ml.context.leader_roots.first().cloned();
|
||||||
|
let template_line = template_root
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|root| root.lines.first())
|
||||||
|
.cloned();
|
||||||
|
let path_type = ml.path_type;
|
||||||
|
let line_color = ml.line_color;
|
||||||
|
let line_type_handle = ml.line_type_handle;
|
||||||
|
let line_weight = ml.line_weight;
|
||||||
|
let arrowhead_handle = ml.arrowhead_handle;
|
||||||
|
let arrowhead_size = ml.arrowhead_size;
|
||||||
let root = ml.context.add_leader_root();
|
let root = ml.context.add_leader_root();
|
||||||
root.create_line(points);
|
if let Some(template) = template_root {
|
||||||
|
root.connection_point = template.connection_point;
|
||||||
|
root.direction = template.direction;
|
||||||
|
root.landing_distance = template.landing_distance;
|
||||||
|
root.text_attachment_direction = template.text_attachment_direction;
|
||||||
|
}
|
||||||
|
let line = root.create_line(points);
|
||||||
|
if let Some(template) = template_line {
|
||||||
|
line.path_type = template.path_type;
|
||||||
|
line.line_color = template.line_color;
|
||||||
|
line.line_type_handle = template.line_type_handle;
|
||||||
|
line.line_weight = template.line_weight;
|
||||||
|
line.arrowhead_handle = template.arrowhead_handle;
|
||||||
|
line.arrowhead_size = template.arrowhead_size;
|
||||||
|
line.override_flags = template.override_flags;
|
||||||
|
} else {
|
||||||
|
line.path_type = path_type;
|
||||||
|
line.line_color = line_color;
|
||||||
|
line.line_type_handle = line_type_handle;
|
||||||
|
line.line_weight = line_weight;
|
||||||
|
line.arrowhead_handle = arrowhead_handle;
|
||||||
|
line.arrowhead_size = arrowhead_size;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let updated = std::mem::replace(entity, EntityType::XLine(Default::default()));
|
let updated = std::mem::replace(entity, EntityType::XLine(Default::default()));
|
||||||
return CmdResult::ReplaceEntity(h, vec![updated]);
|
return CmdResult::ReplaceEntity(h, vec![updated]);
|
||||||
@@ -223,26 +259,43 @@ impl CadCommand for MLeaderRemoveCommand {
|
|||||||
if let Some(mut ent) = entity.take() {
|
if let Some(mut ent) = entity.take() {
|
||||||
let h = *handle;
|
let h = *handle;
|
||||||
if let EntityType::MultiLeader(ref mut ml) = ent {
|
if let EntityType::MultiLeader(ref mut ml) = ent {
|
||||||
// Remove the leader root whose first point is closest to `pt`
|
|
||||||
let pick = Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64);
|
let pick = Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64);
|
||||||
let best = ml
|
let best = ml
|
||||||
.context
|
.context
|
||||||
.leader_roots
|
.leader_roots
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(i, root)| {
|
.flat_map(|(root_index, root)| {
|
||||||
root.lines.first().and_then(|l| l.points.first()).map(|p| {
|
root.lines.iter().enumerate().map(move |(line_index, line)| {
|
||||||
let dx = p.x - pick.x;
|
let mut distance = line
|
||||||
let dz = p.z - pick.z;
|
.points
|
||||||
(i, dx * dx + dz * dz)
|
.windows(2)
|
||||||
|
.map(|segment| point_segment_distance_xy(pick, segment[0], segment[1]))
|
||||||
|
.fold(f64::INFINITY, f64::min);
|
||||||
|
if let Some(last) = line.points.last().copied() {
|
||||||
|
distance = distance.min(point_segment_distance_xy(
|
||||||
|
pick,
|
||||||
|
last,
|
||||||
|
root.connection_point,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
(root_index, line_index, distance)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
.min_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
if let Some((idx, _)) = best {
|
let Some((root_index, line_index, _)) = best else {
|
||||||
if ml.context.leader_roots.len() > 1 {
|
return CmdResult::Cancel;
|
||||||
ml.context.leader_roots.remove(idx);
|
};
|
||||||
}
|
if ml.total_leader_line_count() <= 1 {
|
||||||
|
return CmdResult::Cancel;
|
||||||
}
|
}
|
||||||
|
let root = &mut ml.context.leader_roots[root_index];
|
||||||
|
root.lines.remove(line_index);
|
||||||
|
if root.lines.is_empty() {
|
||||||
|
ml.context.leader_roots.remove(root_index);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return CmdResult::Cancel;
|
||||||
}
|
}
|
||||||
return CmdResult::ReplaceEntity(h, vec![ent]);
|
return CmdResult::ReplaceEntity(h, vec![ent]);
|
||||||
}
|
}
|
||||||
@@ -338,25 +391,11 @@ impl CadCommand for MLeaderAlignCommand {
|
|||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
AlignStep::PickEndDir { handles, from } => {
|
AlignStep::PickEndDir { handles, from } => {
|
||||||
let h = handles.clone();
|
CmdResult::AlignMLeaders {
|
||||||
let f = *from;
|
handles: handles.clone(),
|
||||||
// Emit sentinel for commands.rs
|
from: *from,
|
||||||
use acadrust::entities::XLine;
|
to: pt,
|
||||||
let mut xl = XLine::default();
|
|
||||||
let hstr: Vec<String> = h.iter().map(|hh| hh.value().to_string()).collect();
|
|
||||||
xl.common.layer = format!(
|
|
||||||
"__MLEADERALIGN__{};{:.4},{:.4};{:.4},{:.4}",
|
|
||||||
hstr.join(","),
|
|
||||||
f.x,
|
|
||||||
f.z,
|
|
||||||
pt.x,
|
|
||||||
pt.z
|
|
||||||
);
|
|
||||||
// Use first handle as the "replaced" entity (sentinel)
|
|
||||||
if let Some(&first) = h.first() {
|
|
||||||
return CmdResult::ReplaceEntity(first, vec![EntityType::XLine(xl)]);
|
|
||||||
}
|
}
|
||||||
CmdResult::Cancel
|
|
||||||
}
|
}
|
||||||
_ => CmdResult::NeedPoint,
|
_ => CmdResult::NeedPoint,
|
||||||
}
|
}
|
||||||
@@ -431,21 +470,10 @@ impl CadCommand for MLeaderCollectCommand {
|
|||||||
|
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
if let CollectStep::PickLocation { handles } = &self.step {
|
if let CollectStep::PickLocation { handles } = &self.step {
|
||||||
let h = handles.clone();
|
return CmdResult::CollectMLeaders {
|
||||||
// Build a new combined multileader at the collection point, delete originals.
|
handles: handles.clone(),
|
||||||
// The first handle is the "base" — the rest are erased and their leaders merged.
|
point: pt,
|
||||||
use acadrust::entities::XLine;
|
};
|
||||||
let mut xl = XLine::default();
|
|
||||||
let hstr: Vec<String> = h.iter().map(|hh| hh.value().to_string()).collect();
|
|
||||||
xl.common.layer = format!(
|
|
||||||
"__MLEADERCOLLECT__{};{:.4},{:.4}",
|
|
||||||
hstr.join(","),
|
|
||||||
pt.x,
|
|
||||||
pt.z
|
|
||||||
);
|
|
||||||
if let Some(&first) = h.first() {
|
|
||||||
return CmdResult::ReplaceEntity(first, vec![EntityType::XLine(xl)]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
@@ -496,6 +524,17 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn point_segment_distance_xy(point: Vector3, start: Vector3, end: Vector3) -> f64 {
|
||||||
|
closest_point(
|
||||||
|
&Curve::Line(Line {
|
||||||
|
start: [start.x, start.y],
|
||||||
|
end: [end.x, end.y],
|
||||||
|
}),
|
||||||
|
[point.x, point.y],
|
||||||
|
)
|
||||||
|
.distance
|
||||||
|
}
|
||||||
|
|
||||||
// Silence unused-import warning for MultiLeader and LeaderLine if not used in all paths
|
// Silence unused-import warning for MultiLeader and LeaderLine if not used in all paths
|
||||||
fn _uses_ml_types(_ml: &MultiLeader, _ll: &LeaderLine) {}
|
fn _uses_ml_types(_ml: &MultiLeader, _ll: &LeaderLine) {}
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ impl CadCommand for OrdinateDimCommand {
|
|||||||
Dimension::Ordinate(dim),
|
Dimension::Ordinate(dim),
|
||||||
)),
|
)),
|
||||||
association: DimensionAssociationInput::Infer(None),
|
association: DimensionAssociationInput::Infer(None),
|
||||||
|
preserve_base_style: false,
|
||||||
|
continue_command: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+362
-105
@@ -1,17 +1,15 @@
|
|||||||
// QDIM command — quick dimension: select objects then pick placement line.
|
//! Quick dimension creation from a selected set of drawing entities.
|
||||||
//
|
|
||||||
// Workflow:
|
|
||||||
// 1. Select objects (window/click, Enter to finish selection)
|
|
||||||
// 2. Pick a point that defines the dimension-line position
|
|
||||||
//
|
|
||||||
// Creates one linear dimension per detected endpoint pair on the selected objects.
|
|
||||||
|
|
||||||
use acadrust::entities::{Dimension, DimensionLinear};
|
use acadrust::entities::{Dimension, DimensionDiameter, DimensionLinear, DimensionOrdinate, DimensionRadius};
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
use acadrust::{EntityType, Handle};
|
use acadrust::{EntityType, Handle};
|
||||||
|
use cadkernel::geom2d::{characteristic_points, intersect, Curve, SnapKind, Tolerance};
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{
|
||||||
|
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionAssociationSource,
|
||||||
|
EntityTransform, SelectionEntity, 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;
|
||||||
@@ -27,129 +25,388 @@ pub fn tool() -> ToolDef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Step {
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
Gathering,
|
enum Step { Gathering, Place, Datum, Edit, Settings }
|
||||||
PlaceLine { handles: Vec<Handle> },
|
|
||||||
}
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Mode { Continuous, Staggered, Baseline, Ordinate, Radius, Diameter }
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum SnapPriority { Endpoints, Intersections }
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct SourceCurve { handle: Handle, curve: Curve }
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Candidate { point: DVec3, sources: Vec<DimensionAssociationSource> }
|
||||||
|
|
||||||
pub struct QdimCommand {
|
pub struct QdimCommand {
|
||||||
step: Step,
|
step: Step,
|
||||||
|
mode: Mode,
|
||||||
|
snap_priority: SnapPriority,
|
||||||
|
plane: WorkingPlane,
|
||||||
|
selection: Vec<SelectionEntity>,
|
||||||
|
datum: Option<DVec3>,
|
||||||
|
excluded: Vec<DVec3>,
|
||||||
|
dim_spacing: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QdimCommand {
|
impl QdimCommand {
|
||||||
pub fn new() -> Self {
|
pub fn new(selection: Vec<SelectionEntity>, dim_spacing: f64, snap_priority: u8) -> Self {
|
||||||
|
let step = if selection.is_empty() { Step::Gathering } else { Step::Place };
|
||||||
Self {
|
Self {
|
||||||
step: Step::Gathering,
|
step,
|
||||||
|
mode: Mode::Continuous,
|
||||||
|
snap_priority: if snap_priority == 1 { SnapPriority::Intersections } else { SnapPriority::Endpoints },
|
||||||
|
plane: WorkingPlane::default(),
|
||||||
|
selection,
|
||||||
|
datum: None,
|
||||||
|
excluded: Vec::new(),
|
||||||
|
dim_spacing: dim_spacing.abs().max(1e-9),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn local_sources(&self) -> Vec<SourceCurve> {
|
||||||
|
let transform = EntityTransform::Affine(self.plane.to_local_transform());
|
||||||
|
self.selection.iter().filter_map(|selected| {
|
||||||
|
let mut entity = selected.entity.clone();
|
||||||
|
crate::scene::view::dispatch::apply_transform(&mut entity, &transform);
|
||||||
|
crate::entities::curve::entity_curve_xy(&entity)
|
||||||
|
.map(|curve| SourceCurve { handle: selected.handle, curve })
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn candidates(&self) -> Vec<Candidate> {
|
||||||
|
let curves = self.local_sources();
|
||||||
|
let mut candidates = if self.snap_priority == SnapPriority::Intersections {
|
||||||
|
let mut hits = Vec::new();
|
||||||
|
for first in 0..curves.len() {
|
||||||
|
for second in first + 1..curves.len() {
|
||||||
|
for crossing in intersect(&curves[first].curve, &curves[second].curve, Tolerance::default()) {
|
||||||
|
hits.push(Candidate {
|
||||||
|
point: DVec3::new(crossing.point[0], crossing.point[1], 0.0),
|
||||||
|
// Intersections depend on two curves, but each
|
||||||
|
// extension origin stores one source.
|
||||||
|
sources: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hits
|
||||||
|
} else { Vec::new() };
|
||||||
|
|
||||||
|
if candidates.is_empty() {
|
||||||
|
for source in &curves {
|
||||||
|
for point in characteristic_points(&source.curve).into_iter().filter(|point| point.kind == SnapKind::Endpoint) {
|
||||||
|
candidates.push(Candidate {
|
||||||
|
point: DVec3::new(point.point[0], point.point[1], 0.0),
|
||||||
|
sources: vec![DimensionAssociationSource::inferred(source.handle)],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for selected in &self.selection {
|
||||||
|
if let EntityType::Dimension(dimension) = &selected.entity {
|
||||||
|
for point in dimension_reference_points(dimension) {
|
||||||
|
candidates.push(Candidate {
|
||||||
|
point: self.plane.to_local(point),
|
||||||
|
sources: vec![DimensionAssociationSource::inferred(selected.handle)],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tolerance = candidate_tolerance(candidates.iter().map(|candidate| candidate.point));
|
||||||
|
let mut unique: Vec<Candidate> = Vec::new();
|
||||||
|
for candidate in candidates {
|
||||||
|
if let Some(existing) = unique.iter_mut().find(|existing| existing.point.distance(candidate.point) <= tolerance) {
|
||||||
|
for source in candidate.sources {
|
||||||
|
if !existing.sources.iter().any(|current| current.handle == source.handle) {
|
||||||
|
existing.sources.push(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unique.push(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unique.retain(|candidate| !self.excluded.iter().any(|point| point.distance(candidate.point) <= tolerance));
|
||||||
|
unique
|
||||||
|
}
|
||||||
|
|
||||||
|
fn radial_sources(&self) -> Vec<(Handle, Curve)> {
|
||||||
|
self.local_sources().into_iter()
|
||||||
|
.filter(|source| matches!(source.curve, Curve::Circle(_) | Curve::Arc(_)))
|
||||||
|
.map(|source| (source.handle, source.curve)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn orientation(&self, points: &[Candidate], place: DVec3) -> bool {
|
||||||
|
let min_x = points.iter().map(|point| point.point.x).fold(f64::INFINITY, f64::min);
|
||||||
|
let max_x = points.iter().map(|point| point.point.x).fold(f64::NEG_INFINITY, f64::max);
|
||||||
|
let min_y = points.iter().map(|point| point.point.y).fold(f64::INFINITY, f64::min);
|
||||||
|
let max_y = points.iter().map(|point| point.point.y).fold(f64::NEG_INFINITY, f64::max);
|
||||||
|
let outside_y = (place.y - max_y).max(min_y - place.y).max(0.0);
|
||||||
|
let outside_x = (place.x - max_x).max(min_x - place.x).max(0.0);
|
||||||
|
if (outside_x - outside_y).abs() > 1e-9 { outside_y >= outside_x } else { max_x - min_x >= max_y - min_y }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_dimensions(&self, place_world: DVec3) -> Vec<(EntityType, DimensionAssociationInput)> {
|
||||||
|
let place = self.plane.to_local(place_world);
|
||||||
|
if matches!(self.mode, Mode::Radius | Mode::Diameter) {
|
||||||
|
return self.build_radial_dimensions(place);
|
||||||
|
}
|
||||||
|
let mut points = self.candidates();
|
||||||
|
if points.is_empty() { return Vec::new(); }
|
||||||
|
let horizontal = self.orientation(&points, place);
|
||||||
|
points.sort_by(|first, second| if horizontal { first.point.x.total_cmp(&second.point.x) } else { first.point.y.total_cmp(&second.point.y) });
|
||||||
|
let tolerance = candidate_tolerance(points.iter().map(|point| point.point));
|
||||||
|
points.dedup_by(|first, second| {
|
||||||
|
let delta = if horizontal { first.point.x - second.point.x } else { first.point.y - second.point.y };
|
||||||
|
delta.abs() <= tolerance
|
||||||
|
});
|
||||||
|
|
||||||
|
match self.mode {
|
||||||
|
Mode::Continuous | Mode::Staggered => points.windows(2).enumerate().map(|(index, pair)| {
|
||||||
|
let offset = if self.mode == Mode::Staggered { index as f64 * self.dim_spacing } else { 0.0 };
|
||||||
|
let direction = if horizontal {
|
||||||
|
if place.y >= (pair[0].point.y + pair[1].point.y) * 0.5 { 1.0 } else { -1.0 }
|
||||||
|
} else if place.x >= (pair[0].point.x + pair[1].point.x) * 0.5 { 1.0 } else { -1.0 };
|
||||||
|
let shifted = if horizontal { DVec3::new(place.x, place.y + direction * offset, 0.0) } else { DVec3::new(place.x + direction * offset, place.y, 0.0) };
|
||||||
|
self.linear_dimension(&pair[0], &pair[1], shifted, horizontal)
|
||||||
|
}).collect(),
|
||||||
|
Mode::Baseline => {
|
||||||
|
let datum = self.datum.unwrap_or(points[0].point);
|
||||||
|
let datum_candidate = Candidate {
|
||||||
|
point: datum,
|
||||||
|
sources: points
|
||||||
|
.iter()
|
||||||
|
.find(|point| point.point.distance(datum) <= tolerance)
|
||||||
|
.map(|point| point.sources.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
};
|
||||||
|
points.iter().filter(|point| point.point.distance(datum) > tolerance)
|
||||||
|
.map(|point| self.linear_dimension(&datum_candidate, point, place, horizontal)).collect()
|
||||||
|
}
|
||||||
|
Mode::Ordinate => points.iter().map(|point| self.ordinate_dimension(point, place, horizontal)).collect(),
|
||||||
|
Mode::Radius | Mode::Diameter => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn linear_dimension(&self, first: &Candidate, second: &Candidate, place: DVec3, horizontal: bool) -> (EntityType, DimensionAssociationInput) {
|
||||||
|
let mut dimension = DimensionLinear::new(v3(first.point), v3(second.point));
|
||||||
|
dimension.rotation = if horizontal { 0.0 } else { std::f64::consts::FRAC_PI_2 };
|
||||||
|
let definition = if horizontal { DVec3::new(second.point.x, place.y, 0.0) } else { DVec3::new(place.x, second.point.y, 0.0) };
|
||||||
|
let text = if horizontal { DVec3::new((first.point.x + second.point.x) * 0.5, place.y, 0.0) } else { DVec3::new(place.x, (first.point.y + second.point.y) * 0.5, 0.0) };
|
||||||
|
dimension.definition_point = v3(definition);
|
||||||
|
dimension.base.definition_point = v3(definition);
|
||||||
|
dimension.base.text_middle_point = v3(text);
|
||||||
|
dimension.base.insertion_point = v3(text);
|
||||||
|
dimension.base.actual_measurement = dimension.measurement();
|
||||||
|
let association = DimensionAssociationInput::Explicit(vec![first.sources.first().copied(), second.sources.first().copied()]);
|
||||||
|
(self.plane.place_entity(EntityType::Dimension(Dimension::Linear(dimension))), association)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ordinate_dimension(&self, point: &Candidate, place: DVec3, horizontal: bool) -> (EntityType, DimensionAssociationInput) {
|
||||||
|
let datum = self.datum.unwrap_or(DVec3::ZERO);
|
||||||
|
let leader = if horizontal { DVec3::new(point.point.x, place.y, 0.0) } else { DVec3::new(place.x, point.point.y, 0.0) };
|
||||||
|
let mut dimension = DimensionOrdinate::new(v3(point.point), v3(leader), horizontal);
|
||||||
|
dimension.definition_point = v3(datum);
|
||||||
|
dimension.base.definition_point = v3(datum);
|
||||||
|
dimension.base.text_middle_point = v3(leader);
|
||||||
|
dimension.base.insertion_point = v3(leader);
|
||||||
|
dimension.refresh_measurement();
|
||||||
|
(self.plane.place_entity(EntityType::Dimension(Dimension::Ordinate(dimension))), DimensionAssociationInput::Explicit(vec![point.sources.first().copied()]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_radial_dimensions(&self, place: DVec3) -> Vec<(EntityType, DimensionAssociationInput)> {
|
||||||
|
self.radial_sources().into_iter().enumerate().map(|(index, (handle, curve))| {
|
||||||
|
let (center, radius) = match curve { Curve::Circle(circle) => (circle.centre, circle.radius), Curve::Arc(arc) => (arc.centre, arc.radius), _ => unreachable!() };
|
||||||
|
let center = DVec3::new(center[0], center[1], 0.0);
|
||||||
|
let mut direction = place - center;
|
||||||
|
direction.z = 0.0;
|
||||||
|
direction = if direction.length_squared() <= f64::EPSILON { DVec3::X } else { direction.normalize() };
|
||||||
|
let chord = center + direction * radius;
|
||||||
|
let text = place + DVec3::new(0.0, index as f64 * self.dim_spacing, 0.0);
|
||||||
|
let association = DimensionAssociationInput::Explicit(vec![Some(DimensionAssociationSource::inferred(handle))]);
|
||||||
|
let entity = if self.mode == Mode::Radius {
|
||||||
|
let mut dimension = DimensionRadius::new(v3(center), v3(chord));
|
||||||
|
dimension.base.definition_point = v3(chord);
|
||||||
|
dimension.base.text_middle_point = v3(text);
|
||||||
|
dimension.base.insertion_point = v3(text);
|
||||||
|
dimension.base.text_user_positioned = true;
|
||||||
|
dimension.leader_length = chord.distance(text);
|
||||||
|
dimension.base.actual_measurement = dimension.measurement();
|
||||||
|
EntityType::Dimension(Dimension::Radius(dimension))
|
||||||
|
} else {
|
||||||
|
let opposite = center - direction * radius;
|
||||||
|
let mut dimension = DimensionDiameter::new(v3(chord), v3(opposite));
|
||||||
|
dimension.base.definition_point = v3(opposite);
|
||||||
|
dimension.base.text_middle_point = v3(text);
|
||||||
|
dimension.base.insertion_point = v3(text);
|
||||||
|
dimension.base.text_user_positioned = true;
|
||||||
|
dimension.leader_length = chord.distance(text);
|
||||||
|
dimension.base.actual_measurement = dimension.measurement();
|
||||||
|
EntityType::Dimension(Dimension::Diameter(dimension))
|
||||||
|
};
|
||||||
|
(self.plane.place_entity(entity), association)
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_candidate(&mut self, point_world: DVec3) {
|
||||||
|
let point = self.plane.to_local(point_world);
|
||||||
|
let all = self.candidates();
|
||||||
|
let Some(nearest) = all.iter().min_by(|first, second| first.point.distance(point).total_cmp(&second.point.distance(point))) else { return; };
|
||||||
|
self.excluded.push(nearest.point);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview_dimensions(&self, point: DVec3) -> Vec<WireModel> {
|
||||||
|
self.build_dimensions(point).into_iter().enumerate().flat_map(|(index, (entity, _))| preview_entity(index, &entity)).collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CadCommand for QdimCommand {
|
impl CadCommand for QdimCommand {
|
||||||
fn name(&self) -> &'static str {
|
fn set_working_plane(&mut self, plane: WorkingPlane) { self.plane = plane; }
|
||||||
"QDIM"
|
fn name(&self) -> &'static str { "QDIM" }
|
||||||
}
|
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match &self.step {
|
match self.step {
|
||||||
Step::Gathering => {
|
Step::Gathering => t!("QDIM Select geometry to dimension (Enter when done):").into_owned(),
|
||||||
t!("QDIM Select geometry to dimension (Enter when done):").into_owned()
|
Step::Place => t!("QDIM Specify dimension line position or choose an option:").into_owned(),
|
||||||
|
Step::Datum => t!("QDIM Specify datum point:").into_owned(),
|
||||||
|
Step::Edit => t!("QDIM Select a point to remove (Enter when done):").into_owned(),
|
||||||
|
Step::Settings => t!("QDIM Choose extension origin priority [Endpoints/Intersections]:").into_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
match self.step {
|
||||||
|
Step::Place => vec![
|
||||||
|
CmdOption::new("Continuous", "CONTINUOUS"), CmdOption::new("Staggered", "STAGGERED"),
|
||||||
|
CmdOption::new("Baseline", "BASELINE"), CmdOption::new("Ordinate", "ORDINATE"),
|
||||||
|
CmdOption::new("Radius", "RADIUS"), CmdOption::new("Diameter", "DIAMETER"),
|
||||||
|
CmdOption::new("Datum point", "DATUM"), CmdOption::new("Edit", "EDIT"),
|
||||||
|
CmdOption::new("Settings", "SETTINGS"),
|
||||||
|
],
|
||||||
|
Step::Settings => vec![CmdOption::new("Endpoints", "ENDPOINTS"), CmdOption::new("Intersections", "INTERSECTIONS")],
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_selection_gathering(&self) -> bool { self.step == Step::Gathering }
|
||||||
|
fn selection_forces_add(&self) -> bool { self.step == Step::Gathering }
|
||||||
|
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult { CmdResult::NeedPoint }
|
||||||
|
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) { self.selection = entities; }
|
||||||
|
fn wants_text_input(&self) -> bool { matches!(self.step, Step::Place | Step::Settings) }
|
||||||
|
fn point_step_accepts_keywords(&self) -> bool { self.step == Step::Place }
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
|
let keyword = text.trim().to_ascii_uppercase();
|
||||||
|
if self.step == Step::Settings {
|
||||||
|
match keyword.as_str() {
|
||||||
|
"E" | "ENDPOINT" | "ENDPOINTS" => { self.snap_priority = SnapPriority::Endpoints; self.step = Step::Place; return Some(CmdResult::SetQuickDimensionSnapPriority(0)); }
|
||||||
|
"I" | "INTERSECTION" | "INTERSECTIONS" => { self.snap_priority = SnapPriority::Intersections; self.step = Step::Place; return Some(CmdResult::SetQuickDimensionSnapPriority(1)); }
|
||||||
|
_ => return Some(CmdResult::NeedPoint),
|
||||||
}
|
}
|
||||||
Step::PlaceLine { .. } => t!("QDIM Specify dimension line position:").into_owned(),
|
|
||||||
}
|
}
|
||||||
}
|
match keyword.as_str() {
|
||||||
|
"C" | "CONTINUOUS" => self.mode = Mode::Continuous,
|
||||||
fn is_selection_gathering(&self) -> bool {
|
"S" | "STAGGERED" => self.mode = Mode::Staggered,
|
||||||
matches!(self.step, Step::Gathering)
|
"B" | "BASELINE" => self.mode = Mode::Baseline,
|
||||||
}
|
"O" | "ORDINATE" => self.mode = Mode::Ordinate,
|
||||||
|
"R" | "RADIUS" => self.mode = Mode::Radius,
|
||||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
"DI" | "DIAMETER" => self.mode = Mode::Diameter,
|
||||||
if handles.is_empty() {
|
"DA" | "DATUM" | "DATUMPOINT" => { self.step = Step::Datum; return Some(CmdResult::NeedPoint); }
|
||||||
return CmdResult::Cancel;
|
"E" | "EDIT" => { self.step = Step::Edit; return Some(CmdResult::NeedPoint); }
|
||||||
|
"SE" | "SETTINGS" => { self.step = Step::Settings; return Some(CmdResult::NeedPoint); }
|
||||||
|
_ => return Some(CmdResult::NeedPoint),
|
||||||
}
|
}
|
||||||
self.step = Step::PlaceLine { handles };
|
Some(CmdResult::NeedPoint)
|
||||||
CmdResult::NeedPoint
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||||
if let Step::PlaceLine { handles } = &self.step {
|
match self.step {
|
||||||
CmdResult::Relaunch(
|
Step::Gathering | Step::Settings => CmdResult::NeedPoint,
|
||||||
// Keep the picked placement point in full f64 precision so the
|
Step::Datum => { self.datum = Some(self.plane.to_local(point)); self.step = Step::Place; CmdResult::NeedPoint }
|
||||||
// committed dimension coordinates are not quantized to f32.
|
Step::Edit => { self.remove_candidate(point); CmdResult::NeedPoint }
|
||||||
format!("QDIM_PLACE {:.6} {:.6} {:.6}", pt.x, pt.y, pt.z),
|
Step::Place => {
|
||||||
handles.clone(),
|
let dimensions = self.build_dimensions(point);
|
||||||
)
|
if dimensions.is_empty() { CmdResult::NeedPoint } else { CmdResult::CommitDimensionsAndExit(dimensions) }
|
||||||
} else {
|
}
|
||||||
CmdResult::NeedPoint
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
match self.step {
|
||||||
}
|
Step::Gathering if !self.selection.is_empty() => { self.step = Step::Place; CmdResult::NeedPoint }
|
||||||
|
Step::Edit | Step::Datum | Step::Settings => { self.step = Step::Place; CmdResult::NeedPoint }
|
||||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
|
_ => CmdResult::Cancel,
|
||||||
if !matches!(self.step, Step::PlaceLine { .. }) {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
let d = 0.5_f32;
|
}
|
||||||
Some(WireModel {
|
|
||||||
point_marker: None,
|
fn on_preview_wires(&mut self, point: DVec3) -> Vec<WireModel> {
|
||||||
taper_widths: Vec::new(),
|
if self.step == Step::Place { return self.preview_dimensions(point); }
|
||||||
pattern_stations: Vec::new(),
|
if self.step == Step::Edit {
|
||||||
world_width: 0.0,
|
let size = self.dim_spacing * 0.12;
|
||||||
depth_override: None,
|
return self.candidates().into_iter().enumerate().flat_map(|(index, candidate)| {
|
||||||
display_visible: true,
|
let x_start = self.plane.to_world(candidate.point - DVec3::X * size);
|
||||||
plot_visible: true,
|
let x_end = self.plane.to_world(candidate.point + DVec3::X * size);
|
||||||
fill_is_3d: false,
|
let y_start = self.plane.to_world(candidate.point - DVec3::Y * size);
|
||||||
fill_is_2d_solid: false,
|
let y_end = self.plane.to_world(candidate.point + DVec3::Y * size);
|
||||||
render_instance: None,
|
[
|
||||||
pick_tris: Vec::new(),
|
WireModel::solid_f64(format!("qdim_edit_{index}_x"), vec![
|
||||||
pick_tris_low: Vec::new(),
|
x_start.to_array(), x_end.to_array(),
|
||||||
dash_from_start: false,
|
], WireModel::CYAN, false),
|
||||||
dash_align_end: None,
|
WireModel::solid_f64(format!("qdim_edit_{index}_y"), vec![
|
||||||
text_verts: Vec::new(),
|
y_start.to_array(), y_end.to_array(),
|
||||||
name: "qdim_preview".into(),
|
], WireModel::CYAN, false),
|
||||||
points: vec![[pt.x - d * 3.0, pt.y, pt.z], [pt.x + d * 3.0, pt.y, pt.z]],
|
]
|
||||||
points_low: Vec::new(),
|
}).collect();
|
||||||
color: WireModel::CYAN,
|
}
|
||||||
selected: false,
|
Vec::new()
|
||||||
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(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a linear dimension between two points at the given dimension-line position.
|
fn v3(point: DVec3) -> Vector3 { Vector3::new(point.x, point.y, point.z) }
|
||||||
#[allow(dead_code)]
|
fn d3(point: Vector3) -> DVec3 { DVec3::new(point.x, point.y, point.z) }
|
||||||
pub fn make_linear_dim(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> EntityType {
|
|
||||||
let v = |p: DVec3| Vector3::new(p.x, p.y, p.z);
|
fn candidate_tolerance(points: impl Iterator<Item = DVec3>) -> f64 {
|
||||||
let mut dim = DimensionLinear::new(v(p1), v(p2));
|
let magnitude = points.map(|point| point.x.abs().max(point.y.abs()).max(point.z.abs())).fold(1.0_f64, f64::max);
|
||||||
// Determine axis: horizontal if Δx > Δz, else vertical
|
Tolerance::default().linear().max(magnitude * f64::EPSILON * 64.0)
|
||||||
let dx = (p2.x - p1.x).abs();
|
|
||||||
let dz = (p2.z - p1.z).abs();
|
|
||||||
dim.rotation = if dz > dx {
|
|
||||||
std::f64::consts::FRAC_PI_2
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
dim.definition_point = v(dim_pt);
|
|
||||||
dim.base.definition_point = v(dim_pt);
|
|
||||||
let text_pos = (p1 + p2) * 0.5;
|
|
||||||
dim.base.text_middle_point = v(text_pos);
|
|
||||||
dim.base.insertion_point = v(text_pos);
|
|
||||||
dim.base.actual_measurement = dim.measurement();
|
|
||||||
EntityType::Dimension(Dimension::Linear(dim))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dimension_reference_points(dimension: &Dimension) -> Vec<DVec3> {
|
||||||
|
match dimension {
|
||||||
|
Dimension::Linear(value) => vec![d3(value.first_point), d3(value.second_point)],
|
||||||
|
Dimension::Aligned(value) => vec![d3(value.first_point), d3(value.second_point)],
|
||||||
|
Dimension::Radius(value) => vec![d3(value.angle_vertex), d3(value.definition_point)],
|
||||||
|
Dimension::Diameter(value) => vec![d3(value.angle_vertex), d3(value.definition_point)],
|
||||||
|
Dimension::Ordinate(value) => vec![d3(value.feature_location)],
|
||||||
|
Dimension::Angular2Ln(value) => vec![d3(value.first_point), d3(value.second_point), d3(value.angle_vertex), d3(value.definition_point)],
|
||||||
|
Dimension::Angular3Pt(value) => vec![d3(value.angle_vertex), d3(value.first_point), d3(value.second_point)],
|
||||||
|
Dimension::Arc(value) => vec![d3(value.center_point), d3(value.first_extension_point), d3(value.second_extension_point)],
|
||||||
|
Dimension::LargeRadial(value) => vec![d3(value.definition_point), d3(value.chord_point)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
fn preview_entity(index: usize, entity: &EntityType) -> Vec<WireModel> {
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["QDIM"] }); // QdimCommand
|
let EntityType::Dimension(dimension) = entity else { return Vec::new(); };
|
||||||
|
let mut lines: Vec<Vec<[f64; 3]>> = Vec::new();
|
||||||
|
match dimension {
|
||||||
|
Dimension::Linear(value) => {
|
||||||
|
let first = d3(value.first_point); let second = d3(value.second_point); let definition = d3(value.definition_point);
|
||||||
|
let horizontal = value.rotation.cos().abs() >= value.rotation.sin().abs();
|
||||||
|
let first_line = if horizontal { DVec3::new(first.x, definition.y, definition.z) } else { DVec3::new(definition.x, first.y, definition.z) };
|
||||||
|
let second_line = if horizontal { DVec3::new(second.x, definition.y, definition.z) } else { DVec3::new(definition.x, second.y, definition.z) };
|
||||||
|
lines.push(vec![first.to_array(), first_line.to_array()]);
|
||||||
|
lines.push(vec![second.to_array(), second_line.to_array()]);
|
||||||
|
lines.push(vec![first_line.to_array(), second_line.to_array()]);
|
||||||
|
}
|
||||||
|
Dimension::Ordinate(value) => lines.push(vec![d3(value.feature_location).to_array(), d3(value.leader_endpoint).to_array()]),
|
||||||
|
Dimension::Radius(value) => lines.push(vec![d3(value.angle_vertex).to_array(), d3(value.definition_point).to_array(), d3(value.base.text_middle_point).to_array()]),
|
||||||
|
Dimension::Diameter(value) => lines.push(vec![d3(value.definition_point).to_array(), d3(value.angle_vertex).to_array(), d3(value.base.text_middle_point).to_array()]),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
lines.into_iter().enumerate().map(|(part, points)| WireModel::solid_f64(format!("qdim_preview_{index}_{part}"), points, WireModel::CYAN, false)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["QDIM"] });
|
||||||
@@ -111,6 +111,8 @@ impl CadCommand for RadiusDimensionCommand {
|
|||||||
CmdResult::CommitDimension {
|
CmdResult::CommitDimension {
|
||||||
entity: source_plane.place_entity(EntityType::Dimension(Dimension::Radius(dim))),
|
entity: source_plane.place_entity(EntityType::Dimension(Dimension::Radius(dim))),
|
||||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||||
|
preserve_base_style: false,
|
||||||
|
continue_command: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// Persistent command defaults — last-used values survive across command runs
|
// Last-used command values for this session.
|
||||||
// within the same session. Uses thread_local! so no synchronisation needed.
|
|
||||||
|
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
|
||||||
@@ -28,6 +27,10 @@ thread_local! {
|
|||||||
static RECT_FILLET: Cell<f64> = Cell::new(0.0);
|
static RECT_FILLET: Cell<f64> = Cell::new(0.0);
|
||||||
static DONUT_INNER_DIAMETER: Cell<f64> = Cell::new(0.5);
|
static DONUT_INNER_DIAMETER: Cell<f64> = Cell::new(0.5);
|
||||||
static DONUT_OUTER_DIAMETER: Cell<f64> = Cell::new(1.0);
|
static DONUT_OUTER_DIAMETER: Cell<f64> = Cell::new(1.0);
|
||||||
|
static HELIX_BASE_RADIUS: Cell<f64> = Cell::new(1.0);
|
||||||
|
static HELIX_HEIGHT: Cell<f64> = Cell::new(1.0);
|
||||||
|
static HELIX_TURNS: Cell<f64> = Cell::new(3.0);
|
||||||
|
static HELIX_COUNTER_CLOCKWISE: Cell<bool> = Cell::new(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! accessors {
|
macro_rules! accessors {
|
||||||
@@ -70,6 +73,16 @@ accessors!(get_rect_rotation, set_rect_rotation, RECT_ROTATION);
|
|||||||
accessors!(get_rect_chamfer1, set_rect_chamfer1, RECT_CHAMFER1);
|
accessors!(get_rect_chamfer1, set_rect_chamfer1, RECT_CHAMFER1);
|
||||||
accessors!(get_rect_chamfer2, set_rect_chamfer2, RECT_CHAMFER2);
|
accessors!(get_rect_chamfer2, set_rect_chamfer2, RECT_CHAMFER2);
|
||||||
accessors!(get_rect_fillet, set_rect_fillet, RECT_FILLET);
|
accessors!(get_rect_fillet, set_rect_fillet, RECT_FILLET);
|
||||||
|
accessors!(get_helix_base_radius, set_helix_base_radius, HELIX_BASE_RADIUS);
|
||||||
|
accessors!(get_helix_height, set_helix_height, HELIX_HEIGHT);
|
||||||
|
accessors!(get_helix_turns, set_helix_turns, HELIX_TURNS);
|
||||||
|
pub fn get_helix_counter_clockwise() -> bool {
|
||||||
|
HELIX_COUNTER_CLOCKWISE.with(|value| value.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_helix_counter_clockwise(value: bool) {
|
||||||
|
HELIX_COUNTER_CLOCKWISE.with(|current| current.set(value));
|
||||||
|
}
|
||||||
accessors!(
|
accessors!(
|
||||||
get_donut_inner_diameter,
|
get_donut_inner_diameter,
|
||||||
set_donut_inner_diameter,
|
set_donut_inner_diameter,
|
||||||
|
|||||||
@@ -0,0 +1,500 @@
|
|||||||
|
use acadrust::entities::{face3d::InvisibleEdgeFlags, Face3D};
|
||||||
|
use acadrust::types::Vector3;
|
||||||
|
use acadrust::{EntityType, Handle};
|
||||||
|
use cadkernel::geom2d::Tolerance;
|
||||||
|
use cadkernel::space::{curve::segments_overlap_collinearly, Vec3 as KernelVec3};
|
||||||
|
use glam::DVec3;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use crate::command::{CadCommand, CmdOption, CmdResult};
|
||||||
|
use crate::scene::model::wire_model::WireModel;
|
||||||
|
use crate::t;
|
||||||
|
|
||||||
|
fn vector(point: DVec3) -> Vector3 {
|
||||||
|
Vector3::new(point.x, point.y, point.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point(value: Vector3) -> DVec3 {
|
||||||
|
DVec3::new(value.x, value.y, value.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_edge_invisible(flags: &mut InvisibleEdgeFlags, edge: usize, invisible: bool) {
|
||||||
|
match edge {
|
||||||
|
0 => flags.set_first_invisible(invisible),
|
||||||
|
1 => flags.set_second_invisible(invisible),
|
||||||
|
2 => flags.set_third_invisible(invisible),
|
||||||
|
3 => flags.set_fourth_invisible(invisible),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edge_is_invisible(flags: InvisibleEdgeFlags, edge: usize) -> bool {
|
||||||
|
match edge {
|
||||||
|
0 => flags.is_first_invisible(),
|
||||||
|
1 => flags.is_second_invisible(),
|
||||||
|
2 => flags.is_third_invisible(),
|
||||||
|
3 => flags.is_fourth_invisible(),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_segment(points: &mut Vec<[f64; 3]>, start: [f64; 3], end: [f64; 3]) {
|
||||||
|
if !points.is_empty() {
|
||||||
|
points.push([f64::NAN; 3]);
|
||||||
|
}
|
||||||
|
points.push(start);
|
||||||
|
points.push(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Face3dCommand {
|
||||||
|
points: Vec<DVec3>,
|
||||||
|
invisible: [bool; 4],
|
||||||
|
pending_invisible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Face3dCommand {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
points: Vec::new(),
|
||||||
|
invisible: [false; 4],
|
||||||
|
pending_invisible: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(&self, fourth: DVec3) -> Option<EntityType> {
|
||||||
|
if self.points.len() != 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut face = Face3D::new(
|
||||||
|
vector(self.points[0]),
|
||||||
|
vector(self.points[1]),
|
||||||
|
vector(self.points[2]),
|
||||||
|
vector(fourth),
|
||||||
|
);
|
||||||
|
for edge in 0..4 {
|
||||||
|
set_edge_invisible(&mut face.invisible_edges, edge, self.invisible[edge]);
|
||||||
|
}
|
||||||
|
Some(EntityType::Face3D(face))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn continue_from_last_edge(&mut self, fourth: DVec3) {
|
||||||
|
let third = self.points[2];
|
||||||
|
let shared_edge_invisible = self.invisible[2];
|
||||||
|
self.points.clear();
|
||||||
|
self.points.push(third);
|
||||||
|
self.points.push(fourth);
|
||||||
|
self.invisible = [shared_edge_invisible, false, false, false];
|
||||||
|
self.pending_invisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn continue_from_triangle(&mut self) {
|
||||||
|
let second = self.points[1];
|
||||||
|
let third = self.points[2];
|
||||||
|
let shared_edge_invisible = self.invisible[1];
|
||||||
|
self.points.clear();
|
||||||
|
self.points.push(second);
|
||||||
|
self.points.push(third);
|
||||||
|
self.invisible = [shared_edge_invisible, false, false, false];
|
||||||
|
self.pending_invisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for Face3dCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"3DFACE"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
match self.points.len() {
|
||||||
|
0 => t!("3DFACE Specify first point or [Invisible]:").into_owned(),
|
||||||
|
1 => t!("3DFACE Specify second point or [Invisible]:").into_owned(),
|
||||||
|
2 => t!("3DFACE Specify third point or [Invisible] <exit>:").into_owned(),
|
||||||
|
_ => t!("3DFACE Specify fourth point or [Invisible] <create three-sided face>:")
|
||||||
|
.into_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
vec![CmdOption::new(t!("Invisible").as_ref(), "I")]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, picked: DVec3) -> CmdResult {
|
||||||
|
let point_index = self.points.len();
|
||||||
|
if self.pending_invisible && point_index < 4 {
|
||||||
|
self.invisible[point_index] = true;
|
||||||
|
}
|
||||||
|
self.pending_invisible = false;
|
||||||
|
if self.points.len() == 3 {
|
||||||
|
let entity = self.build(picked);
|
||||||
|
self.continue_from_last_edge(picked);
|
||||||
|
return entity.map_or(CmdResult::Cancel, CmdResult::CommitEntity);
|
||||||
|
}
|
||||||
|
self.points.push(picked);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
if self.points.len() != 3 {
|
||||||
|
return CmdResult::Cancel;
|
||||||
|
}
|
||||||
|
if self.pending_invisible {
|
||||||
|
self.invisible[3] = true;
|
||||||
|
}
|
||||||
|
let third = self.points[2];
|
||||||
|
let entity = self.build(third);
|
||||||
|
self.continue_from_triangle();
|
||||||
|
entity.map_or(CmdResult::Cancel, CmdResult::CommitEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants_text_input(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, value: &str) -> Option<CmdResult> {
|
||||||
|
match value.trim().to_ascii_uppercase().as_str() {
|
||||||
|
"I" | "INVISIBLE" => {
|
||||||
|
self.pending_invisible = true;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_mouse_move(&mut self, cursor: DVec3) -> Option<WireModel> {
|
||||||
|
if self.points.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut preview = Vec::new();
|
||||||
|
for edge in 0..self.points.len().saturating_sub(1) {
|
||||||
|
if !self.invisible[edge] {
|
||||||
|
append_segment(
|
||||||
|
&mut preview,
|
||||||
|
self.points[edge].to_array(),
|
||||||
|
self.points[edge + 1].to_array(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let pending_edge = self.points.len() - 1;
|
||||||
|
if !self.invisible[pending_edge] {
|
||||||
|
append_segment(
|
||||||
|
&mut preview,
|
||||||
|
self.points.last()?.to_array(),
|
||||||
|
cursor.to_array(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self.points.len() == 3 && !(self.invisible[3] || self.pending_invisible) {
|
||||||
|
append_segment(&mut preview, cursor.to_array(), self.points[0].to_array());
|
||||||
|
}
|
||||||
|
Some(WireModel::solid_f64(
|
||||||
|
"face3d_preview".to_string(),
|
||||||
|
preview,
|
||||||
|
WireModel::CYAN,
|
||||||
|
false,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum EdgeMode {
|
||||||
|
Toggle,
|
||||||
|
Display,
|
||||||
|
DisplaySelect,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FaceEdgeCommand {
|
||||||
|
faces: Vec<(Handle, Face3D)>,
|
||||||
|
mode: EdgeMode,
|
||||||
|
show_all_hidden: bool,
|
||||||
|
displayed_faces: HashSet<Handle>,
|
||||||
|
pending_replacements: HashMap<Handle, Face3D>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FaceEdgeCommand {
|
||||||
|
pub fn new(faces: Vec<(Handle, Face3D)>) -> Self {
|
||||||
|
Self {
|
||||||
|
faces,
|
||||||
|
mode: EdgeMode::Toggle,
|
||||||
|
show_all_hidden: false,
|
||||||
|
displayed_faces: HashSet::new(),
|
||||||
|
pending_replacements: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn corners(face: &Face3D) -> [KernelVec3; 4] {
|
||||||
|
[
|
||||||
|
KernelVec3::from(point(face.first_corner).to_array()),
|
||||||
|
KernelVec3::from(point(face.second_corner).to_array()),
|
||||||
|
KernelVec3::from(point(face.third_corner).to_array()),
|
||||||
|
KernelVec3::from(point(face.fourth_corner).to_array()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nearest_edge(face: &Face3D, picked: KernelVec3) -> Option<usize> {
|
||||||
|
let corners = Self::corners(face);
|
||||||
|
let tolerance = Tolerance::default().linear();
|
||||||
|
let mut best = None;
|
||||||
|
for edge in 0..4 {
|
||||||
|
let start = corners[edge];
|
||||||
|
let end = corners[(edge + 1) % 4];
|
||||||
|
if start.distance(end) <= tolerance {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let distance = picked.distance_to_segment(start, end);
|
||||||
|
if best.is_none_or(|(_, best_distance)| distance < best_distance) {
|
||||||
|
best = Some((edge, distance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best.map(|(edge, _)| edge)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edge_wire(face: &Face3D, edge: usize, name: &str) -> WireModel {
|
||||||
|
let corners = Self::corners(face);
|
||||||
|
WireModel::solid_f64(
|
||||||
|
name.to_string(),
|
||||||
|
vec![corners[edge].to_array(), corners[(edge + 1) % 4].to_array()],
|
||||||
|
WireModel::CYAN,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collinear_replacements(
|
||||||
|
&mut self,
|
||||||
|
selected_face: usize,
|
||||||
|
selected_edge: usize,
|
||||||
|
) -> Vec<(Handle, Vec<EntityType>)> {
|
||||||
|
let selected_corners = Self::corners(&self.faces[selected_face].1);
|
||||||
|
let selected_start = selected_corners[selected_edge];
|
||||||
|
let selected_end = selected_corners[(selected_edge + 1) % 4];
|
||||||
|
let make_invisible = !edge_is_invisible(
|
||||||
|
self.faces[selected_face].1.invisible_edges,
|
||||||
|
selected_edge,
|
||||||
|
);
|
||||||
|
let mut replacements = Vec::new();
|
||||||
|
self.pending_replacements.clear();
|
||||||
|
for (handle, face) in &self.faces {
|
||||||
|
let corners = Self::corners(face);
|
||||||
|
let mut updated = face.clone();
|
||||||
|
let mut changed = false;
|
||||||
|
for edge in 0..4 {
|
||||||
|
if segments_overlap_collinearly(
|
||||||
|
selected_start.to_array(),
|
||||||
|
selected_end.to_array(),
|
||||||
|
corners[edge].to_array(),
|
||||||
|
corners[(edge + 1) % 4].to_array(),
|
||||||
|
Tolerance::default().linear(),
|
||||||
|
) && edge_is_invisible(updated.invisible_edges, edge) != make_invisible
|
||||||
|
{
|
||||||
|
set_edge_invisible(&mut updated.invisible_edges, edge, make_invisible);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
self.pending_replacements.insert(*handle, updated.clone());
|
||||||
|
replacements.push((*handle, vec![EntityType::Face3D(updated)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replacements
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hidden_edges_wire(&self) -> WireModel {
|
||||||
|
let mut points = Vec::new();
|
||||||
|
for (handle, face) in &self.faces {
|
||||||
|
if !self.show_all_hidden && !self.displayed_faces.contains(handle) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let corners = Self::corners(face);
|
||||||
|
for edge in 0..4 {
|
||||||
|
if edge_is_invisible(face.invisible_edges, edge)
|
||||||
|
&& corners[edge].distance(corners[(edge + 1) % 4])
|
||||||
|
> Tolerance::default().linear()
|
||||||
|
{
|
||||||
|
append_segment(
|
||||||
|
&mut points,
|
||||||
|
corners[edge].to_array(),
|
||||||
|
corners[(edge + 1) % 4].to_array(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WireModel::solid_f64(
|
||||||
|
"face3d_hidden_edges".to_string(),
|
||||||
|
points,
|
||||||
|
WireModel::CYAN,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for FaceEdgeCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"EDGE"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
match self.mode {
|
||||||
|
EdgeMode::Toggle => {
|
||||||
|
t!("EDGE Select edge of 3D face to toggle visibility or [Display]:").into_owned()
|
||||||
|
}
|
||||||
|
EdgeMode::Display => {
|
||||||
|
t!("EDGE Display invisible edges [All/Select] <Select>:").into_owned()
|
||||||
|
}
|
||||||
|
EdgeMode::DisplaySelect => {
|
||||||
|
t!("EDGE Select 3D faces to display invisible edges <done>:").into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
match self.mode {
|
||||||
|
EdgeMode::Toggle => vec![CmdOption::new(t!("Display").as_ref(), "D")],
|
||||||
|
EdgeMode::Display => vec![
|
||||||
|
CmdOption::new(t!("All").as_ref(), "A"),
|
||||||
|
CmdOption::new(t!("Select").as_ref(), "S"),
|
||||||
|
],
|
||||||
|
EdgeMode::DisplaySelect => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_entity_pick(&self) -> bool {
|
||||||
|
matches!(self.mode, EdgeMode::Toggle | EdgeMode::DisplaySelect)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_includes_fills(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_uses_surface_point(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_highlights_hover(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_entity_pick(&mut self, handle: Handle, picked: DVec3) -> CmdResult {
|
||||||
|
let Some(index) = self.faces.iter().position(|(candidate, _)| *candidate == handle) else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
if self.mode == EdgeMode::DisplaySelect {
|
||||||
|
self.displayed_faces.insert(handle);
|
||||||
|
return CmdResult::Preview(self.hidden_edges_wire());
|
||||||
|
}
|
||||||
|
let picked = KernelVec3::from(picked.to_array());
|
||||||
|
let Some(edge) = Self::nearest_edge(&self.faces[index].1, picked) else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
let replacements = self.collinear_replacements(index, edge);
|
||||||
|
if replacements.is_empty() {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
} else {
|
||||||
|
CmdResult::ReplaceManyContinue(replacements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_hover_entity(&mut self, handle: Handle, picked: DVec3) -> Vec<WireModel> {
|
||||||
|
let Some((_, face)) = self.faces.iter().find(|(candidate, _)| *candidate == handle) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
if self.mode == EdgeMode::DisplaySelect
|
||||||
|
|| self.show_all_hidden
|
||||||
|
|| self.displayed_faces.contains(&handle)
|
||||||
|
{
|
||||||
|
let mut points = Vec::new();
|
||||||
|
let corners = Self::corners(face);
|
||||||
|
for edge in 0..4 {
|
||||||
|
if corners[edge].distance(corners[(edge + 1) % 4])
|
||||||
|
> Tolerance::default().linear()
|
||||||
|
{
|
||||||
|
append_segment(
|
||||||
|
&mut points,
|
||||||
|
corners[edge].to_array(),
|
||||||
|
corners[(edge + 1) % 4].to_array(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vec![WireModel::solid_f64(
|
||||||
|
"face3d_edges".to_string(),
|
||||||
|
points,
|
||||||
|
WireModel::CYAN,
|
||||||
|
false,
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
Self::nearest_edge(face, KernelVec3::from(picked.to_array()))
|
||||||
|
.map(|edge| vec![Self::edge_wire(face, edge, "face3d_edge")])
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, value: &str) -> Option<CmdResult> {
|
||||||
|
let keyword = value.trim().to_ascii_uppercase();
|
||||||
|
match self.mode {
|
||||||
|
EdgeMode::Toggle if matches!(keyword.as_str(), "D" | "DISPLAY") => {
|
||||||
|
self.mode = EdgeMode::Display;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
EdgeMode::Display if matches!(keyword.as_str(), "A" | "ALL") => {
|
||||||
|
self.mode = EdgeMode::Toggle;
|
||||||
|
self.show_all_hidden = true;
|
||||||
|
Some(CmdResult::Preview(self.hidden_edges_wire()))
|
||||||
|
}
|
||||||
|
EdgeMode::Display if matches!(keyword.as_str(), "S" | "SELECT") => {
|
||||||
|
self.mode = EdgeMode::DisplaySelect;
|
||||||
|
self.show_all_hidden = false;
|
||||||
|
self.displayed_faces.clear();
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
_ => Some(CmdResult::NeedPoint),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants_text_input(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
match self.mode {
|
||||||
|
EdgeMode::Display => {
|
||||||
|
self.mode = EdgeMode::DisplaySelect;
|
||||||
|
self.show_all_hidden = false;
|
||||||
|
self.displayed_faces.clear();
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
EdgeMode::DisplaySelect => {
|
||||||
|
self.mode = EdgeMode::Toggle;
|
||||||
|
CmdResult::Preview(self.hidden_edges_wire())
|
||||||
|
}
|
||||||
|
EdgeMode::Toggle => CmdResult::Cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_entity_replaced(&mut self, old: Handle, new_handles: &[Handle]) {
|
||||||
|
if let Some(new_handle) = new_handles.first().copied() {
|
||||||
|
if let Some((handle, face)) =
|
||||||
|
self.faces.iter_mut().find(|(handle, _)| *handle == old)
|
||||||
|
{
|
||||||
|
if let Some(updated) = self.pending_replacements.remove(&old) {
|
||||||
|
*face = updated;
|
||||||
|
}
|
||||||
|
*handle = new_handle;
|
||||||
|
}
|
||||||
|
if self.displayed_faces.remove(&old) {
|
||||||
|
self.displayed_faces.insert(new_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
|
names: &["3DFACE", "EDGE"]
|
||||||
|
});
|
||||||
+368
-185
@@ -1,35 +1,18 @@
|
|||||||
// Parametric helix tool — interactive command.
|
use acadrust::entities::{Helix, HelixConstraint, Spline};
|
||||||
//
|
|
||||||
// Command: HELIX — build a parametric 3D helix and commit it as a single 3D
|
|
||||||
// polyline whose straight segments approximate the curve. The user picks a
|
|
||||||
// base centre point, then types the base radius, the top radius (Enter keeps
|
|
||||||
// it equal to the base radius for a cylindrical helix), the height, and the
|
|
||||||
// number of turns (Enter accepts a default of three turns).
|
|
||||||
//
|
|
||||||
// The curve is sampled at a fixed angular resolution: with `SEG_PER_TURN`
|
|
||||||
// samples per turn the helix has `turns * SEG_PER_TURN` segments. For each
|
|
||||||
// sample the radius interpolates linearly from base to top, the elevation
|
|
||||||
// interpolates linearly from zero to the height, and the planar position is
|
|
||||||
// `(radius*cos(angle), radius*sin(angle))` offset from the centre.
|
|
||||||
|
|
||||||
use acadrust::entities::Polyline3D;
|
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
use acadrust::EntityType;
|
use acadrust::EntityType;
|
||||||
use crate::t;
|
use cadkernel::space::{HelixCurve, HelixDirection, Vec3};
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult, DynField, WorkingPlane};
|
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
|
||||||
use crate::scene::model::wire_model::WireModel;
|
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
|
|
||||||
/// Angular samples per full turn of the helix.
|
use crate::command::{CadCommand, CmdOption, CmdResult, DynField, WorkingPlane};
|
||||||
const SEG_PER_TURN: usize = 36;
|
use crate::modules::draw::defaults;
|
||||||
/// Default number of turns when the user accepts the prompt with Enter.
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
const DEFAULT_TURNS: f64 = 3.0;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
|
use crate::t;
|
||||||
|
|
||||||
// ── Ribbon definition ─────────────────────────────────────────────────────
|
const EPSILON: f64 = 1.0e-9;
|
||||||
|
|
||||||
#[allow(dead_code)] // ribbon definition ready for wiring; command works via the command line
|
#[allow(dead_code)]
|
||||||
pub fn tool() -> ToolDef {
|
pub fn tool() -> ToolDef {
|
||||||
ToolDef {
|
ToolDef {
|
||||||
id: "HELIX",
|
id: "HELIX",
|
||||||
@@ -39,16 +22,18 @@ pub fn tool() -> ToolDef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Command implementation ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Which parameter the command is currently collecting.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
enum Step {
|
enum Step {
|
||||||
Center,
|
Center,
|
||||||
BaseRadius,
|
BaseRadius,
|
||||||
|
BaseDiameter,
|
||||||
TopRadius,
|
TopRadius,
|
||||||
Height,
|
TopDiameter,
|
||||||
|
Final,
|
||||||
|
AxisEndpoint,
|
||||||
Turns,
|
Turns,
|
||||||
|
TurnHeight,
|
||||||
|
Twist,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HelixCommand {
|
pub struct HelixCommand {
|
||||||
@@ -58,58 +43,130 @@ pub struct HelixCommand {
|
|||||||
top_radius: f64,
|
top_radius: f64,
|
||||||
height: f64,
|
height: f64,
|
||||||
turns: f64,
|
turns: f64,
|
||||||
|
requested_turn_height: Option<f64>,
|
||||||
|
counter_clockwise: bool,
|
||||||
|
constraint: HelixConstraint,
|
||||||
plane: WorkingPlane,
|
plane: WorkingPlane,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HelixCommand {
|
impl HelixCommand {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
let base_radius = defaults::get_helix_base_radius();
|
||||||
Self {
|
Self {
|
||||||
step: Step::Center,
|
step: Step::Center,
|
||||||
center: DVec3::ZERO,
|
center: DVec3::ZERO,
|
||||||
base_radius: 0.0,
|
base_radius,
|
||||||
top_radius: 0.0,
|
top_radius: base_radius,
|
||||||
height: 0.0,
|
height: defaults::get_helix_height(),
|
||||||
turns: DEFAULT_TURNS,
|
turns: defaults::get_helix_turns(),
|
||||||
|
requested_turn_height: None,
|
||||||
|
counter_clockwise: defaults::get_helix_counter_clockwise(),
|
||||||
|
constraint: HelixConstraint::Turns,
|
||||||
plane: WorkingPlane::default(),
|
plane: WorkingPlane::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sample the helix into a list of world points.
|
|
||||||
fn sample_points(&self, base_radius: f64, top_radius: f64, height: f64, turns: f64) -> Vec<DVec3> {
|
|
||||||
let turns = if turns <= 0.0 { DEFAULT_TURNS } else { turns };
|
|
||||||
let segments = ((turns * SEG_PER_TURN as f64).round() as usize).max(1);
|
|
||||||
let mut pts = Vec::with_capacity(segments + 1);
|
|
||||||
for t in 0..=segments {
|
|
||||||
let frac = t as f64 / segments as f64;
|
|
||||||
let angle = turns * std::f64::consts::TAU * frac;
|
|
||||||
let radius = base_radius + (top_radius - base_radius) * frac;
|
|
||||||
let z = height * frac;
|
|
||||||
pts.push(
|
|
||||||
self.center
|
|
||||||
+ self.plane.x * (radius * angle.cos())
|
|
||||||
+ self.plane.y * (radius * angle.sin())
|
|
||||||
+ self.plane.z * z,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
pts
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build the committed helix entity from the collected parameters.
|
|
||||||
fn build(&self) -> Option<EntityType> {
|
|
||||||
let pts = self.sample_points(self.base_radius, self.top_radius, self.height, self.turns);
|
|
||||||
if pts.len() < 2 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let verts: Vec<Vector3> = pts.iter().map(|p| Vector3::new(p.x, p.y, p.z)).collect();
|
|
||||||
let mut pl = Polyline3D::from_points(verts);
|
|
||||||
pl.flags.closed = false;
|
|
||||||
Some(EntityType::Polyline3D(pl))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a positive distance from typed text; returns `None` on failure.
|
|
||||||
fn parse_value(text: &str) -> Option<f64> {
|
fn parse_value(text: &str) -> Option<f64> {
|
||||||
crate::entities::common::parse_typed_length(text)
|
crate::entities::common::parse_typed_length(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_turns(text: &str) -> Option<f64> {
|
||||||
|
text.trim().replace(',', ".").parse().ok().filter(|value: &f64| value.is_finite())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn axis_start_direction(&self, axis: Vec3) -> Option<Vec3> {
|
||||||
|
let project = |direction: DVec3| {
|
||||||
|
let direction = Vec3::from(direction.to_array());
|
||||||
|
(direction - axis * direction.dot(axis)).normalize()
|
||||||
|
};
|
||||||
|
project(self.plane.x).or_else(|| project(self.plane.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolved_turns(&self, height: f64) -> f64 {
|
||||||
|
match self.requested_turn_height {
|
||||||
|
Some(turn_height) if turn_height > EPSILON => (height.abs() / turn_height).max(EPSILON),
|
||||||
|
_ => self.turns,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kernel_curve(&self, height: f64, axis: DVec3) -> Option<HelixCurve> {
|
||||||
|
let axis = Vec3::from(axis.to_array())
|
||||||
|
.normalize()
|
||||||
|
.or_else(|| Vec3::from(self.plane.z.to_array()).normalize())?;
|
||||||
|
Some(HelixCurve {
|
||||||
|
base_center: self.center.to_array(),
|
||||||
|
axis_direction: axis.to_array(),
|
||||||
|
start_direction: self.axis_start_direction(axis)?.to_array(),
|
||||||
|
base_radius: self.base_radius,
|
||||||
|
top_radius: self.top_radius,
|
||||||
|
height,
|
||||||
|
turns: self.resolved_turns(height),
|
||||||
|
direction: if self.counter_clockwise {
|
||||||
|
HelixDirection::CounterClockwise
|
||||||
|
} else {
|
||||||
|
HelixDirection::Clockwise
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(&self, height: f64, axis: DVec3) -> Option<EntityType> {
|
||||||
|
let curve = self.kernel_curve(height, axis)?;
|
||||||
|
let nurbs = curve.nurbs()?;
|
||||||
|
let axis = DVec3::from_array(curve.axis_direction).normalize_or(self.plane.z);
|
||||||
|
let start_direction = DVec3::from_array(curve.start_direction).normalize_or(self.plane.x);
|
||||||
|
let mut spline = Spline::new();
|
||||||
|
spline.degree = nurbs.degree() as i32;
|
||||||
|
spline.knots = nurbs.knots().to_vec();
|
||||||
|
spline.control_points = nurbs
|
||||||
|
.control_points()
|
||||||
|
.iter()
|
||||||
|
.map(|point| Vector3::new(point[0], point[1], point[2]))
|
||||||
|
.collect();
|
||||||
|
spline.flags.planar = false;
|
||||||
|
spline.flags.rational = nurbs.is_rational();
|
||||||
|
if spline.flags.rational {
|
||||||
|
spline.weights = nurbs.weights().to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut helix = Helix::new();
|
||||||
|
helix.spline = spline;
|
||||||
|
helix.axis_base_point = Vector3::new(self.center.x, self.center.y, self.center.z);
|
||||||
|
let start = self.center + start_direction * curve.base_radius;
|
||||||
|
helix.start_point = Vector3::new(start.x, start.y, start.z);
|
||||||
|
helix.axis_vector = Vector3::new(axis.x, axis.y, axis.z);
|
||||||
|
helix.radius = curve.base_radius;
|
||||||
|
helix.turns = curve.turns;
|
||||||
|
helix.turn_height = height / curve.turns;
|
||||||
|
helix.handedness = self.counter_clockwise;
|
||||||
|
helix.constraint = self.constraint;
|
||||||
|
Some(EntityType::Helix(helix))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commit(&mut self, height: f64, axis: DVec3) -> CmdResult {
|
||||||
|
let Some(entity) = self.build(height, axis) else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
self.height = height;
|
||||||
|
self.turns = self.resolved_turns(height);
|
||||||
|
defaults::set_helix_base_radius(self.base_radius);
|
||||||
|
defaults::set_helix_height(height);
|
||||||
|
defaults::set_helix_turns(self.turns);
|
||||||
|
defaults::set_helix_counter_clockwise(self.counter_clockwise);
|
||||||
|
CmdResult::CommitAndExit(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview(&self, height: f64, axis: DVec3) -> Option<WireModel> {
|
||||||
|
let points = self
|
||||||
|
.kernel_curve(height, axis)?
|
||||||
|
.nurbs()?
|
||||||
|
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE);
|
||||||
|
Some(WireModel::solid_f64(
|
||||||
|
"helix_preview".to_string(),
|
||||||
|
points,
|
||||||
|
WireModel::CYAN,
|
||||||
|
false,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CadCommand for HelixCommand {
|
impl CadCommand for HelixCommand {
|
||||||
@@ -123,65 +180,158 @@ impl CadCommand for HelixCommand {
|
|||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::Center => t!("HELIX Specify centre point of base:").into_owned(),
|
Step::Center => crate::tf!(
|
||||||
Step::BaseRadius => t!("HELIX Specify base radius:").into_owned(),
|
"HELIX Number of turns = {:.4} Twist = {}\nSpecify center point of base:",
|
||||||
Step::TopRadius => t!("HELIX Specify top radius <same as base>:").into_owned(),
|
self.turns,
|
||||||
Step::Height => t!("HELIX Specify helix height:").into_owned(),
|
if self.counter_clockwise { "CCW" } else { "CW" }
|
||||||
Step::Turns => t!("HELIX Enter number of turns <3>:").into_owned(),
|
)
|
||||||
|
.into_owned(),
|
||||||
|
Step::BaseRadius => crate::tf!(
|
||||||
|
"HELIX Specify base radius or [Diameter] <{:.4}>:",
|
||||||
|
self.base_radius
|
||||||
|
)
|
||||||
|
.into_owned(),
|
||||||
|
Step::BaseDiameter => crate::tf!(
|
||||||
|
"HELIX Specify base diameter <{:.4}>:",
|
||||||
|
self.base_radius * 2.0
|
||||||
|
)
|
||||||
|
.into_owned(),
|
||||||
|
Step::TopRadius => crate::tf!(
|
||||||
|
"HELIX Specify top radius or [Diameter] <{:.4}>:",
|
||||||
|
self.base_radius
|
||||||
|
)
|
||||||
|
.into_owned(),
|
||||||
|
Step::TopDiameter => crate::tf!(
|
||||||
|
"HELIX Specify top diameter <{:.4}>:",
|
||||||
|
self.base_radius * 2.0
|
||||||
|
)
|
||||||
|
.into_owned(),
|
||||||
|
Step::Final => crate::tf!(
|
||||||
|
"HELIX Specify helix height or [Axis endpoint/Turns/turn Height/tWist] <{:.4}>:",
|
||||||
|
self.height
|
||||||
|
)
|
||||||
|
.into_owned(),
|
||||||
|
Step::AxisEndpoint => t!("HELIX Specify axis endpoint:").into_owned(),
|
||||||
|
Step::Turns => crate::tf!("HELIX Enter number of turns <{:.4}>:", self.turns).into_owned(),
|
||||||
|
Step::TurnHeight => crate::tf!(
|
||||||
|
"HELIX Specify distance between turns <{:.4}>:",
|
||||||
|
self.height.abs() / self.turns.max(EPSILON)
|
||||||
|
)
|
||||||
|
.into_owned(),
|
||||||
|
Step::Twist => crate::tf!(
|
||||||
|
"HELIX Enter twist direction [Clockwise/Counterclockwise] <{}>:",
|
||||||
|
if self.counter_clockwise { "CCW" } else { "CW" }
|
||||||
|
)
|
||||||
|
.into_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
match self.step {
|
||||||
|
Step::BaseRadius | Step::TopRadius => vec![CmdOption::new(t!("Diameter").as_ref(), "D")],
|
||||||
|
Step::Final => vec![
|
||||||
|
CmdOption::new(t!("Axis endpoint").as_ref(), "A"),
|
||||||
|
CmdOption::new(t!("Turns").as_ref(), "T"),
|
||||||
|
CmdOption::new(t!("Turn Height").as_ref(), "H"),
|
||||||
|
CmdOption::new(t!("Twist").as_ref(), "W"),
|
||||||
|
],
|
||||||
|
Step::Twist => vec![
|
||||||
|
CmdOption::new(t!("Clockwise").as_ref(), "CW"),
|
||||||
|
CmdOption::new(t!("Counterclockwise").as_ref(), "CCW"),
|
||||||
|
],
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
|
matches!(self.step, Step::BaseRadius | Step::TopRadius | Step::Final | Step::Twist)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::Center => {
|
Step::Center => {
|
||||||
self.center = pt;
|
self.center = point;
|
||||||
self.step = Step::BaseRadius;
|
self.step = Step::BaseRadius;
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
Step::BaseRadius => {
|
Step::BaseRadius | Step::BaseDiameter => {
|
||||||
// A point click resolves the base radius as the planar distance
|
let local = self.plane.vector_to_local(point - self.center);
|
||||||
// from the centre to the picked point.
|
let distance = local.x.hypot(local.y);
|
||||||
let delta = self.plane.vector_to_local(pt - self.center);
|
let radius = if self.step == Step::BaseDiameter {
|
||||||
let r = delta.x.hypot(delta.y);
|
distance * 0.5
|
||||||
if r > 0.0 {
|
} else {
|
||||||
self.base_radius = r;
|
distance
|
||||||
|
};
|
||||||
|
if radius > EPSILON {
|
||||||
|
self.base_radius = radius;
|
||||||
|
self.top_radius = radius;
|
||||||
self.step = Step::TopRadius;
|
self.step = Step::TopRadius;
|
||||||
}
|
}
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
Step::TopRadius => {
|
Step::TopRadius | Step::TopDiameter => {
|
||||||
let delta = self.plane.vector_to_local(pt - self.center);
|
let local = self.plane.vector_to_local(point - self.center);
|
||||||
let r = delta.x.hypot(delta.y);
|
let distance = local.x.hypot(local.y);
|
||||||
self.top_radius = if r > 0.0 { r } else { self.base_radius };
|
self.top_radius = if self.step == Step::TopDiameter {
|
||||||
self.step = Step::Height;
|
distance * 0.5
|
||||||
|
} else {
|
||||||
|
distance
|
||||||
|
};
|
||||||
|
self.step = Step::Final;
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
Step::Height => {
|
Step::Final => {
|
||||||
// Use the elevation of the picked point relative to the centre.
|
let height = (point - self.center).dot(self.plane.z);
|
||||||
let h = (pt - self.center).dot(self.plane.z);
|
if height.abs() <= EPSILON {
|
||||||
if h.abs() > 0.0 {
|
CmdResult::NeedPoint
|
||||||
self.height = h;
|
} else {
|
||||||
self.step = Step::Turns;
|
self.commit(height, self.plane.z)
|
||||||
}
|
}
|
||||||
CmdResult::NeedPoint
|
|
||||||
}
|
}
|
||||||
Step::Turns => CmdResult::NeedPoint,
|
Step::AxisEndpoint => {
|
||||||
|
let vector = point - self.center;
|
||||||
|
let height = vector.length();
|
||||||
|
if height <= EPSILON {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
} else {
|
||||||
|
self.commit(height, vector / height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Step::Turns | Step::TurnHeight | Step::Twist => CmdResult::NeedPoint,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::TopRadius => {
|
Step::Center => CmdResult::Cancel,
|
||||||
// Enter keeps the top radius equal to the base radius.
|
Step::BaseRadius | Step::BaseDiameter => {
|
||||||
self.top_radius = self.base_radius;
|
self.top_radius = self.base_radius;
|
||||||
self.step = Step::Height;
|
self.step = Step::TopRadius;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::TopRadius | Step::TopDiameter => {
|
||||||
|
self.top_radius = self.base_radius;
|
||||||
|
self.step = Step::Final;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::Final => self.commit(self.height, self.plane.z),
|
||||||
|
Step::AxisEndpoint => CmdResult::NeedPoint,
|
||||||
|
Step::Turns => {
|
||||||
|
self.constraint = HelixConstraint::Turns;
|
||||||
|
self.requested_turn_height = None;
|
||||||
|
self.step = Step::Final;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::TurnHeight => {
|
||||||
|
self.constraint = HelixConstraint::TurnHeight;
|
||||||
|
self.requested_turn_height = Some(self.height.abs() / self.turns.max(EPSILON));
|
||||||
|
self.step = Step::Final;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::Twist => {
|
||||||
|
self.step = Step::Final;
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
Step::Turns => match self.build() {
|
|
||||||
Some(e) => CmdResult::CommitAndExit(e),
|
|
||||||
None => CmdResult::Cancel,
|
|
||||||
},
|
|
||||||
_ => CmdResult::NeedPoint,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,123 +340,156 @@ impl CadCommand for HelixCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn wants_text_input(&self) -> bool {
|
||||||
// Every step past the centre pick reads a typed numeric value.
|
|
||||||
self.step != Step::Center
|
self.step != Step::Center
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dyn_field(&self) -> DynField {
|
fn dyn_field(&self) -> DynField {
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::Center => DynField::Point,
|
Step::Center | Step::AxisEndpoint => DynField::Point,
|
||||||
Step::BaseRadius | Step::TopRadius => DynField::Distance,
|
Step::BaseRadius | Step::BaseDiameter | Step::TopRadius | Step::TopDiameter | Step::Final | Step::TurnHeight => DynField::Distance,
|
||||||
Step::Height => DynField::Distance,
|
Step::Turns | Step::Twist => DynField::Scalar,
|
||||||
Step::Turns => DynField::Scalar,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
let t = text.trim();
|
let value = text.trim();
|
||||||
|
let keyword = value.to_ascii_uppercase();
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::Center => None,
|
Step::Center => None,
|
||||||
|
Step::BaseRadius if matches!(keyword.as_str(), "D" | "DIAMETER") => {
|
||||||
|
self.step = Step::BaseDiameter;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
Step::TopRadius if matches!(keyword.as_str(), "D" | "DIAMETER") => {
|
||||||
|
self.step = Step::TopDiameter;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
Step::Final if matches!(keyword.as_str(), "A" | "AXIS" | "AXIS ENDPOINT") => {
|
||||||
|
self.step = Step::AxisEndpoint;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
Step::Final if matches!(keyword.as_str(), "T" | "TURNS") => {
|
||||||
|
self.step = Step::Turns;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
Step::Final if matches!(keyword.as_str(), "H" | "HEIGHT" | "TURN HEIGHT") => {
|
||||||
|
self.step = Step::TurnHeight;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
Step::Final if matches!(keyword.as_str(), "W" | "TWIST") => {
|
||||||
|
self.step = Step::Twist;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
Step::BaseRadius => {
|
Step::BaseRadius => {
|
||||||
if let Some(v) = Self::parse_value(t) {
|
let radius = Self::parse_value(value)?;
|
||||||
if v > 0.0 {
|
if radius > EPSILON {
|
||||||
self.base_radius = v;
|
self.base_radius = radius;
|
||||||
self.step = Step::TopRadius;
|
self.top_radius = radius;
|
||||||
}
|
self.step = Step::TopRadius;
|
||||||
|
}
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
Step::BaseDiameter => {
|
||||||
|
let diameter = Self::parse_value(value)?;
|
||||||
|
if diameter > EPSILON {
|
||||||
|
self.base_radius = diameter * 0.5;
|
||||||
|
self.top_radius = self.base_radius;
|
||||||
|
self.step = Step::TopRadius;
|
||||||
}
|
}
|
||||||
Some(CmdResult::NeedPoint)
|
Some(CmdResult::NeedPoint)
|
||||||
}
|
}
|
||||||
Step::TopRadius => {
|
Step::TopRadius => {
|
||||||
if t.is_empty() {
|
let radius = Self::parse_value(value)?;
|
||||||
// Empty entry keeps the top radius equal to the base radius.
|
if radius >= 0.0 {
|
||||||
self.top_radius = self.base_radius;
|
self.top_radius = radius;
|
||||||
self.step = Step::Height;
|
self.step = Step::Final;
|
||||||
} else if let Some(v) = Self::parse_value(t) {
|
|
||||||
self.top_radius = if v > 0.0 { v } else { self.base_radius };
|
|
||||||
self.step = Step::Height;
|
|
||||||
}
|
}
|
||||||
Some(CmdResult::NeedPoint)
|
Some(CmdResult::NeedPoint)
|
||||||
}
|
}
|
||||||
Step::Height => {
|
Step::TopDiameter => {
|
||||||
if let Some(v) = Self::parse_value(t) {
|
let diameter = Self::parse_value(value)?;
|
||||||
if v != 0.0 {
|
if diameter >= 0.0 {
|
||||||
self.height = v;
|
self.top_radius = diameter * 0.5;
|
||||||
self.step = Step::Turns;
|
self.step = Step::Final;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some(CmdResult::NeedPoint)
|
Some(CmdResult::NeedPoint)
|
||||||
}
|
}
|
||||||
|
Step::Final => {
|
||||||
|
let height = Self::parse_value(value)?;
|
||||||
|
(height.abs() > EPSILON).then(|| self.commit(height, self.plane.z))
|
||||||
|
}
|
||||||
|
Step::AxisEndpoint => None,
|
||||||
Step::Turns => {
|
Step::Turns => {
|
||||||
self.turns = if t.is_empty() {
|
let turns = Self::parse_turns(value)?;
|
||||||
DEFAULT_TURNS
|
if turns > EPSILON {
|
||||||
} else {
|
self.turns = turns;
|
||||||
match Self::parse_value(t) {
|
self.requested_turn_height = None;
|
||||||
Some(v) if v > 0.0 => v,
|
self.constraint = HelixConstraint::Turns;
|
||||||
_ => return Some(CmdResult::NeedPoint),
|
self.step = Step::Final;
|
||||||
}
|
|
||||||
};
|
|
||||||
match self.build() {
|
|
||||||
Some(e) => Some(CmdResult::CommitAndExit(e)),
|
|
||||||
None => Some(CmdResult::Cancel),
|
|
||||||
}
|
}
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
Step::TurnHeight => {
|
||||||
|
let turn_height = Self::parse_value(value)?;
|
||||||
|
if turn_height > EPSILON {
|
||||||
|
self.requested_turn_height = Some(turn_height);
|
||||||
|
self.constraint = HelixConstraint::TurnHeight;
|
||||||
|
self.step = Step::Final;
|
||||||
|
}
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
Step::Twist => {
|
||||||
|
match keyword.as_str() {
|
||||||
|
"C" | "CW" | "CLOCKWISE" => self.counter_clockwise = false,
|
||||||
|
"CC" | "CCW" | "COUNTERCLOCKWISE" => self.counter_clockwise = true,
|
||||||
|
_ => return Some(CmdResult::NeedPoint),
|
||||||
|
}
|
||||||
|
self.step = Step::Final;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||||
// Preview the helix once enough parameters are known. Before a base
|
|
||||||
// radius is fixed, track the cursor distance to size a live ring.
|
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::Center => None,
|
Step::Center => None,
|
||||||
Step::BaseRadius => {
|
Step::BaseRadius | Step::BaseDiameter => {
|
||||||
let delta = self.plane.vector_to_local(pt - self.center);
|
let local = self.plane.vector_to_local(point - self.center);
|
||||||
let r = delta.x.hypot(delta.y);
|
let old = (self.base_radius, self.top_radius);
|
||||||
if r <= 0.0 {
|
let distance = local.x.hypot(local.y);
|
||||||
return None;
|
self.base_radius = if self.step == Step::BaseDiameter {
|
||||||
}
|
distance * 0.5
|
||||||
let pts = self.sample_points(r, r, 0.0, 1.0);
|
|
||||||
let raw: Vec<[f64; 3]> = pts.iter().map(|p| [p.x, p.y, p.z]).collect();
|
|
||||||
Some(WireModel::solid_f64(
|
|
||||||
"helix_preview".to_string(),
|
|
||||||
raw,
|
|
||||||
WireModel::CYAN,
|
|
||||||
false,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Use the parameters fixed so far; for the height step let the
|
|
||||||
// cursor elevation drive the live height.
|
|
||||||
let height = if self.step == Step::Height {
|
|
||||||
(pt - self.center).dot(self.plane.z)
|
|
||||||
} else {
|
} else {
|
||||||
self.height
|
distance
|
||||||
};
|
|
||||||
let top = if self.step == Step::TopRadius {
|
|
||||||
let delta = self.plane.vector_to_local(pt - self.center);
|
|
||||||
let r = delta.x.hypot(delta.y);
|
|
||||||
if r > 0.0 {
|
|
||||||
r
|
|
||||||
} else {
|
|
||||||
self.base_radius
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.top_radius
|
|
||||||
};
|
|
||||||
let pts = self.sample_points(self.base_radius, top, height, self.turns);
|
|
||||||
if pts.len() < 2 {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
let raw: Vec<[f64; 3]> = pts.iter().map(|p| [p.x, p.y, p.z]).collect();
|
.max(EPSILON);
|
||||||
Some(WireModel::solid_f64(
|
self.top_radius = self.base_radius;
|
||||||
"helix_preview".to_string(),
|
let preview = self.preview(0.0, self.plane.z);
|
||||||
raw,
|
(self.base_radius, self.top_radius) = old;
|
||||||
WireModel::CYAN,
|
preview
|
||||||
false,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
Step::TopRadius | Step::TopDiameter => {
|
||||||
|
let local = self.plane.vector_to_local(point - self.center);
|
||||||
|
let old = self.top_radius;
|
||||||
|
let distance = local.x.hypot(local.y);
|
||||||
|
self.top_radius = if self.step == Step::TopDiameter {
|
||||||
|
distance * 0.5
|
||||||
|
} else {
|
||||||
|
distance
|
||||||
|
};
|
||||||
|
let preview = self.preview(0.0, self.plane.z);
|
||||||
|
self.top_radius = old;
|
||||||
|
preview
|
||||||
|
}
|
||||||
|
Step::Final => self.preview((point - self.center).dot(self.plane.z), self.plane.z),
|
||||||
|
Step::AxisEndpoint => {
|
||||||
|
let vector = point - self.center;
|
||||||
|
let height = vector.length();
|
||||||
|
(height > EPSILON).then(|| self.preview(height, vector / height)).flatten()
|
||||||
|
}
|
||||||
|
Step::Turns | Step::TurnHeight | Step::Twist => self.preview(self.height, self.plane.z),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
inventory::submit!(crate::command::CommandRegistration { names: &["HELIX"] });
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["HELIX"] }); // HelixCommand
|
|
||||||
@@ -17,7 +17,7 @@ enum Step {
|
|||||||
Vertices,
|
Vertices,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a legacy M by N polygon mesh from row-major 3D vertex input.
|
/// Creates an M by N polygon mesh from row-major 3D input.
|
||||||
pub struct Mesh3dCommand {
|
pub struct Mesh3dCommand {
|
||||||
step: Step,
|
step: Step,
|
||||||
m: usize,
|
m: usize,
|
||||||
@@ -159,6 +159,9 @@ impl CadCommand for Mesh3dCommand {
|
|||||||
if self.step != Step::Vertices {
|
if self.step != Step::Vertices {
|
||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
|
if !point.is_finite() {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
self.points.push(point);
|
self.points.push(point);
|
||||||
if self.points.len() == self.total_vertices() {
|
if self.points.len() == self.total_vertices() {
|
||||||
self.build()
|
self.build()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub mod circle;
|
|||||||
pub mod dimcenter;
|
pub mod dimcenter;
|
||||||
pub mod donut;
|
pub mod donut;
|
||||||
pub mod ellipse;
|
pub mod ellipse;
|
||||||
|
pub mod face3d;
|
||||||
pub mod hatch;
|
pub mod hatch;
|
||||||
pub mod hatchedit;
|
pub mod hatchedit;
|
||||||
pub mod helix;
|
pub mod helix;
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
// 3D polyline tool — interactive command.
|
// 3DPOLY creates open or closed non-planar paths.
|
||||||
//
|
|
||||||
// Command: 3DPOLY — pick a sequence of points whose Z ordinates may differ and
|
|
||||||
// commit them as a single 3D polyline of straight segments. Unlike PLINE (a
|
|
||||||
// planar lightweight polyline with bulge arcs), every vertex keeps its own Z,
|
|
||||||
// so the result is a true non-planar path. Close (C) adds a closing segment
|
|
||||||
// back to the first point; Undo (U) drops the last picked vertex. Enter / Esc
|
|
||||||
// finishes the open polyline.
|
|
||||||
|
|
||||||
use acadrust::entities::Polyline3D;
|
use acadrust::entities::Polyline3D;
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
@@ -67,7 +60,7 @@ impl CadCommand for Poly3dCommand {
|
|||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match self.points.len() {
|
match self.points.len() {
|
||||||
0 => t!("3DPOLY Specify start point of polyline:").into_owned(),
|
0 => t!("3DPOLY Specify start point of polyline:").into_owned(),
|
||||||
1 => t!("3DPOLY Specify next point [Undo]:").into_owned(),
|
1 | 2 => t!("3DPOLY Specify next point [Undo]:").into_owned(),
|
||||||
_ => t!("3DPOLY Specify next point [Close/Undo]:").into_owned(),
|
_ => t!("3DPOLY Specify next point [Close/Undo]:").into_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,10 +91,10 @@ impl CadCommand for Poly3dCommand {
|
|||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
match text.trim().to_uppercase().as_str() {
|
match text.trim().to_uppercase().as_str() {
|
||||||
"C" | "CLOSE" => match self.build(true) {
|
"C" | "CLOSE" if self.points.len() >= 3 => self
|
||||||
Some(e) => Some(CmdResult::CommitAndExit(e)),
|
.build(true)
|
||||||
None => Some(CmdResult::NeedPoint),
|
.map(CmdResult::CommitAndExit),
|
||||||
},
|
"C" | "CLOSE" => Some(CmdResult::NeedPoint),
|
||||||
"U" | "UNDO" => {
|
"U" | "UNDO" => {
|
||||||
self.points.pop();
|
self.points.pop();
|
||||||
Some(CmdResult::NeedPoint)
|
Some(CmdResult::NeedPoint)
|
||||||
|
|||||||
@@ -1,26 +1,15 @@
|
|||||||
// PEDIT command — edit a polyline entity (#263).
|
// PEDIT edits polylines and polygon meshes.
|
||||||
//
|
|
||||||
// Flow:
|
|
||||||
// Select polyline — picking a LINE or ARC instead asks "Turn it into one?"
|
|
||||||
// (Yes converts in place and continues editing the result).
|
|
||||||
// Options: Close / Open / Join / Width / Fit / Spline / Decurve / eXit.
|
|
||||||
// Join gathers more segments (lines / arcs / polylines) and merges every
|
|
||||||
// contiguous run through the JOIN machinery. (Vertex editing lives on the
|
|
||||||
// polyline's grips, not in PEDIT.)
|
|
||||||
// Fit smooths the segments into tangent-blended arcs, Spline replaces the
|
|
||||||
// shape with a sampled cubic B-spline of the vertex frame, Decurve
|
|
||||||
// straightens every segment.
|
|
||||||
|
|
||||||
use acadrust::entities::LwVertex;
|
use acadrust::entities::LwVertex;
|
||||||
use cadkernel::geom2d::nurbs::clamped_uniform_knots;
|
|
||||||
use cadkernel::geom2d::NurbsCurve;
|
|
||||||
use acadrust::types::Vector2;
|
use acadrust::types::Vector2;
|
||||||
use acadrust::{EntityType, Handle};
|
use acadrust::{EntityType, Handle};
|
||||||
|
use cadkernel::geom2d::nurbs::clamped_uniform_knots;
|
||||||
|
use cadkernel::geom2d::NurbsCurve;
|
||||||
use glam::{DVec2, DVec3};
|
use glam::{DVec2, DVec3};
|
||||||
use crate::t;
|
|
||||||
use rustc_hash::FxHashMap as HashMap;
|
use rustc_hash::FxHashMap as HashMap;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdResult};
|
||||||
|
use crate::t;
|
||||||
|
|
||||||
const TAU: f64 = std::f64::consts::TAU;
|
const TAU: f64 = std::f64::consts::TAU;
|
||||||
|
|
||||||
@@ -53,6 +42,7 @@ pub struct PeditCommand {
|
|||||||
target: Option<Handle>,
|
target: Option<Handle>,
|
||||||
info: HashMap<u64, PeditTarget>,
|
info: HashMap<u64, PeditTarget>,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
|
undo_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PeditCommand {
|
impl PeditCommand {
|
||||||
@@ -61,6 +51,7 @@ impl PeditCommand {
|
|||||||
target: None,
|
target: None,
|
||||||
info,
|
info,
|
||||||
mode: Mode::PickTarget,
|
mode: Mode::PickTarget,
|
||||||
|
undo_count: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +79,6 @@ impl PeditCommand {
|
|||||||
let handle = self.target?;
|
let handle = self.target?;
|
||||||
self.info.get(&handle.value())?.mesh_size
|
self.info.get(&handle.value())?.mesh_size
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CadCommand for PeditCommand {
|
impl CadCommand for PeditCommand {
|
||||||
@@ -140,15 +130,15 @@ impl CadCommand for PeditCommand {
|
|||||||
CmdOption::new(t!("eXit").as_ref(), "X"),
|
CmdOption::new(t!("eXit").as_ref(), "X"),
|
||||||
],
|
],
|
||||||
Mode::Options => vec![
|
Mode::Options => vec![
|
||||||
CmdOption::new(t!("Close").as_ref(), "C"),
|
CmdOption::new(t!("Close").as_ref(), "C"),
|
||||||
CmdOption::new(t!("Open").as_ref(), "O"),
|
CmdOption::new(t!("Open").as_ref(), "O"),
|
||||||
CmdOption::new(t!("Join").as_ref(), "J"),
|
CmdOption::new(t!("Join").as_ref(), "J"),
|
||||||
CmdOption::new(t!("Width").as_ref(), "W"),
|
CmdOption::new(t!("Width").as_ref(), "W"),
|
||||||
CmdOption::new(t!("Fit").as_ref(), "F"),
|
CmdOption::new(t!("Fit").as_ref(), "F"),
|
||||||
CmdOption::new(t!("Spline").as_ref(), "S"),
|
CmdOption::new(t!("Spline").as_ref(), "S"),
|
||||||
CmdOption::new(t!("Decurve").as_ref(), "D"),
|
CmdOption::new(t!("Decurve").as_ref(), "D"),
|
||||||
CmdOption::new(t!("eXit").as_ref(), "X"),
|
CmdOption::new(t!("eXit").as_ref(), "X"),
|
||||||
],
|
],
|
||||||
Mode::MeshVertex(_) => vec![
|
Mode::MeshVertex(_) => vec![
|
||||||
CmdOption::new(t!("Next").as_ref(), "N"),
|
CmdOption::new(t!("Next").as_ref(), "N"),
|
||||||
CmdOption::new(t!("Previous").as_ref(), "P"),
|
CmdOption::new(t!("Previous").as_ref(), "P"),
|
||||||
@@ -218,19 +208,23 @@ impl CadCommand for PeditCommand {
|
|||||||
// piece as the live target and carry its bookkeeping over.
|
// piece as the live target and carry its bookkeeping over.
|
||||||
if let Some(&nh) = new_handles.first() {
|
if let Some(&nh) = new_handles.first() {
|
||||||
self.info.remove(&old.value());
|
self.info.remove(&old.value());
|
||||||
self.info.insert(
|
self.info.insert(
|
||||||
nh.value(),
|
nh.value(),
|
||||||
PeditTarget {
|
PeditTarget {
|
||||||
is_poly: true,
|
is_poly: true,
|
||||||
convertible: false,
|
convertible: false,
|
||||||
mesh_size: None,
|
mesh_size: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.target = Some(nh);
|
self.target = Some(nh);
|
||||||
self.mode = Mode::Options;
|
self.mode = Mode::Options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_pedit_applied(&mut self) {
|
||||||
|
self.undo_count = self.undo_count.saturating_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn wants_text_input(&self) -> bool {
|
||||||
!matches!(self.mode, Mode::PickTarget | Mode::MeshVertexMove(_))
|
!matches!(self.mode, Mode::PickTarget | Mode::MeshVertexMove(_))
|
||||||
}
|
}
|
||||||
@@ -332,7 +326,11 @@ impl CadCommand for PeditCommand {
|
|||||||
handle,
|
handle,
|
||||||
op: PeditOp::SetMeshClosedN(false),
|
op: PeditOp::SetMeshClosedN(false),
|
||||||
}),
|
}),
|
||||||
"U" | "UNDO" => Some(CmdResult::UndoDocument),
|
"U" | "UNDO" if self.undo_count > 0 => {
|
||||||
|
self.undo_count -= 1;
|
||||||
|
Some(CmdResult::UndoDocument)
|
||||||
|
}
|
||||||
|
"U" | "UNDO" => Some(CmdResult::NeedPoint),
|
||||||
"X" | "EXIT" => Some(CmdResult::Cancel),
|
"X" | "EXIT" => Some(CmdResult::Cancel),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
@@ -481,6 +479,9 @@ pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool {
|
|||||||
},
|
},
|
||||||
PeditOp::SetMeshClosedM(closed) => match entity {
|
PeditOp::SetMeshClosedM(closed) => match entity {
|
||||||
EntityType::PolygonMesh(mesh) => {
|
EntityType::PolygonMesh(mesh) => {
|
||||||
|
if mesh.is_closed_m() == *closed {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
mesh.flags.set(
|
mesh.flags.set(
|
||||||
acadrust::entities::polygon_mesh::PolygonMeshFlags::CLOSED_M,
|
acadrust::entities::polygon_mesh::PolygonMeshFlags::CLOSED_M,
|
||||||
*closed,
|
*closed,
|
||||||
@@ -491,6 +492,9 @@ pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool {
|
|||||||
},
|
},
|
||||||
PeditOp::SetMeshClosedN(closed) => match entity {
|
PeditOp::SetMeshClosedN(closed) => match entity {
|
||||||
EntityType::PolygonMesh(mesh) => {
|
EntityType::PolygonMesh(mesh) => {
|
||||||
|
if mesh.is_closed_n() == *closed {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
mesh.flags.set(
|
mesh.flags.set(
|
||||||
acadrust::entities::polygon_mesh::PolygonMeshFlags::CLOSED_N,
|
acadrust::entities::polygon_mesh::PolygonMeshFlags::CLOSED_N,
|
||||||
*closed,
|
*closed,
|
||||||
@@ -501,26 +505,38 @@ pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool {
|
|||||||
},
|
},
|
||||||
PeditOp::SetMeshSmooth(smooth) => match entity {
|
PeditOp::SetMeshSmooth(smooth) => match entity {
|
||||||
EntityType::PolygonMesh(mesh) => {
|
EntityType::PolygonMesh(mesh) => {
|
||||||
|
let mut changed = mesh.smooth_type != *smooth;
|
||||||
mesh.smooth_type = *smooth;
|
mesh.smooth_type = *smooth;
|
||||||
if mesh.smooth_type
|
if mesh.smooth_type
|
||||||
!= acadrust::entities::polygon_mesh::SurfaceSmoothType::NoSmooth
|
!= acadrust::entities::polygon_mesh::SurfaceSmoothType::NoSmooth
|
||||||
{
|
{
|
||||||
if mesh.m_smooth_density < 2 {
|
if !(2..=256).contains(&mesh.m_smooth_density) {
|
||||||
mesh.m_smooth_density = 6;
|
mesh.m_smooth_density = 6;
|
||||||
|
changed = true;
|
||||||
}
|
}
|
||||||
if mesh.n_smooth_density < 2 {
|
if !(2..=256).contains(&mesh.n_smooth_density) {
|
||||||
mesh.n_smooth_density = 6;
|
mesh.n_smooth_density = 6;
|
||||||
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
changed
|
||||||
}
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
},
|
},
|
||||||
PeditOp::MoveMeshVertex { index, point } => match entity {
|
PeditOp::MoveMeshVertex { index, point } => match entity {
|
||||||
EntityType::PolygonMesh(mesh) => {
|
EntityType::PolygonMesh(mesh) => {
|
||||||
|
if !point.is_finite() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let Some(vertex) = mesh.vertices.get_mut(*index) else {
|
let Some(vertex) = mesh.vertices.get_mut(*index) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
if vertex.location.x == point.x
|
||||||
|
&& vertex.location.y == point.y
|
||||||
|
&& vertex.location.z == point.z
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
vertex.location = acadrust::types::Vector3::new(point.x, point.y, point.z);
|
vertex.location = acadrust::types::Vector3::new(point.x, point.y, point.z);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
+164
-59
@@ -1502,66 +1502,153 @@ pub fn apply_mleader_style(
|
|||||||
entity: &mut acadrust::entities::MultiLeader,
|
entity: &mut acadrust::entities::MultiLeader,
|
||||||
style: &acadrust::objects::MultiLeaderStyle,
|
style: &acadrust::objects::MultiLeaderStyle,
|
||||||
) {
|
) {
|
||||||
|
use acadrust::entities::MultiLeaderPropertyOverrideFlags as F;
|
||||||
|
|
||||||
|
let flags = entity.property_override_flags;
|
||||||
entity.style_handle = Some(style.handle);
|
entity.style_handle = Some(style.handle);
|
||||||
entity.content_type = (style.content_type as i16).into();
|
if !flags.contains(F::CONTENT_TYPE) {
|
||||||
entity.path_type = (style.path_type as i16).into();
|
entity.content_type = (style.content_type as i16).into();
|
||||||
entity.line_color = style.line_color;
|
}
|
||||||
entity.line_type_handle = style.line_type_handle;
|
if !flags.contains(F::PATH_TYPE) {
|
||||||
entity.line_weight = style.line_weight;
|
entity.path_type = (style.path_type as i16).into();
|
||||||
entity.enable_landing = style.enable_landing;
|
}
|
||||||
entity.enable_dogleg = style.enable_dogleg;
|
if !flags.contains(F::LINE_COLOR) {
|
||||||
entity.dogleg_length = style.landing_distance;
|
entity.line_color = style.line_color;
|
||||||
entity.arrowhead_handle = style.arrowhead_handle;
|
}
|
||||||
entity.arrowhead_size = style.arrowhead_size;
|
if !flags.contains(F::LEADER_LINE_TYPE) {
|
||||||
entity.context.arrowhead_size = style.arrowhead_size;
|
entity.line_type_handle = style.line_type_handle;
|
||||||
entity.context.landing_gap = style.landing_gap;
|
}
|
||||||
entity.text_style_handle = style.text_style_handle;
|
if !flags.contains(F::LEADER_LINE_WEIGHT) {
|
||||||
entity.text_color = style.text_color;
|
entity.line_weight = style.line_weight;
|
||||||
entity.text_frame = style.text_frame;
|
}
|
||||||
entity.text_height = style.text_height;
|
if !flags.contains(F::ENABLE_LANDING) {
|
||||||
entity.context.text_height = style.text_height;
|
entity.enable_landing = style.enable_landing;
|
||||||
entity.context.text_style_handle = style.text_style_handle;
|
}
|
||||||
entity.context.text_color = style.text_color;
|
if !flags.contains(F::ENABLE_DOGLEG) {
|
||||||
entity.text_left_attachment = (style.text_left_attachment as i16).into();
|
entity.enable_dogleg = style.enable_dogleg;
|
||||||
entity.text_right_attachment = (style.text_right_attachment as i16).into();
|
}
|
||||||
entity.text_top_attachment = (style.text_top_attachment as i16).into();
|
if !flags.contains(F::LANDING_DISTANCE) {
|
||||||
entity.text_bottom_attachment = (style.text_bottom_attachment as i16).into();
|
entity.dogleg_length = style.landing_distance;
|
||||||
entity.text_attachment_direction = (style.text_attachment_direction as i16).into();
|
}
|
||||||
entity.text_alignment = (style.text_alignment as i16).into();
|
if !flags.contains(F::LANDING_GAP) {
|
||||||
entity.text_angle_type = (style.text_angle_type as i16).into();
|
entity.context.landing_gap = style.landing_gap;
|
||||||
|
}
|
||||||
|
if !flags.contains(F::ARROWHEAD) {
|
||||||
|
entity.arrowhead_handle = style.arrowhead_handle;
|
||||||
|
}
|
||||||
|
if !flags.contains(F::ARROWHEAD_SIZE) {
|
||||||
|
entity.arrowhead_size = style.arrowhead_size;
|
||||||
|
entity.context.arrowhead_size = style.arrowhead_size;
|
||||||
|
}
|
||||||
|
if !flags.contains(F::TEXT_STYLE) {
|
||||||
|
entity.text_style_handle = style.text_style_handle;
|
||||||
|
}
|
||||||
|
if !flags.contains(F::TEXT_COLOR) {
|
||||||
|
entity.text_color = style.text_color;
|
||||||
|
}
|
||||||
|
if !flags.contains(F::TEXT_FRAME) {
|
||||||
|
entity.text_frame = style.text_frame;
|
||||||
|
}
|
||||||
|
if !flags.contains(F::TEXT_HEIGHT) {
|
||||||
|
entity.text_height = style.text_height;
|
||||||
|
entity.context.text_height = style.text_height;
|
||||||
|
}
|
||||||
|
if !flags.contains(F::TEXT_LEFT_ATTACHMENT) {
|
||||||
|
entity.text_left_attachment = (style.text_left_attachment as i16).into();
|
||||||
|
}
|
||||||
|
if !flags.contains(F::TEXT_RIGHT_ATTACHMENT) {
|
||||||
|
entity.text_right_attachment = (style.text_right_attachment as i16).into();
|
||||||
|
}
|
||||||
|
if !flags.contains(F::TEXT_TOP_ATTACHMENT) {
|
||||||
|
entity.text_top_attachment = (style.text_top_attachment as i16).into();
|
||||||
|
}
|
||||||
|
if !flags.contains(F::TEXT_BOTTOM_ATTACHMENT) {
|
||||||
|
entity.text_bottom_attachment = (style.text_bottom_attachment as i16).into();
|
||||||
|
}
|
||||||
|
if !flags.contains(F::TEXT_ATTACHMENT_DIRECTION) {
|
||||||
|
entity.text_attachment_direction = (style.text_attachment_direction as i16).into();
|
||||||
|
}
|
||||||
|
if !flags.contains(F::TEXT_ALIGNMENT) {
|
||||||
|
entity.text_alignment = (style.text_alignment as i16).into();
|
||||||
|
}
|
||||||
|
if !flags.contains(F::TEXT_ANGLE) {
|
||||||
|
entity.text_angle_type = (style.text_angle_type as i16).into();
|
||||||
|
}
|
||||||
entity.context.text_left_attachment = entity.text_left_attachment;
|
entity.context.text_left_attachment = entity.text_left_attachment;
|
||||||
entity.context.text_right_attachment = entity.text_right_attachment;
|
entity.context.text_right_attachment = entity.text_right_attachment;
|
||||||
entity.context.text_top_attachment = entity.text_top_attachment;
|
entity.context.text_top_attachment = entity.text_top_attachment;
|
||||||
entity.context.text_bottom_attachment = entity.text_bottom_attachment;
|
entity.context.text_bottom_attachment = entity.text_bottom_attachment;
|
||||||
entity.context.text_alignment = entity.text_alignment;
|
entity.context.text_alignment = entity.text_alignment;
|
||||||
entity.block_content_handle = style.block_content_handle;
|
entity.context.text_style_handle = entity.text_style_handle;
|
||||||
entity.block_content_color = style.block_content_color;
|
entity.context.text_color = entity.text_color;
|
||||||
entity.block_connection_type = (style.block_content_connection as i16).into();
|
if !flags.contains(F::BLOCK_CONTENT) {
|
||||||
entity.block_rotation = style.block_content_rotation;
|
entity.block_content_handle = style.block_content_handle;
|
||||||
entity.block_scale = Vector3::new(
|
}
|
||||||
style.block_content_scale_x,
|
if !flags.contains(F::BLOCK_CONTENT_COLOR) {
|
||||||
style.block_content_scale_y,
|
entity.block_content_color = style.block_content_color;
|
||||||
style.block_content_scale_z,
|
}
|
||||||
);
|
if !flags.contains(F::BLOCK_CONTENT_CONNECTION) {
|
||||||
entity.scale_factor = style.scale_factor;
|
entity.block_connection_type = (style.block_content_connection as i16).into();
|
||||||
entity.context.block_content_handle = style.block_content_handle;
|
}
|
||||||
entity.context.block_content_color = style.block_content_color;
|
if !flags.contains(F::BLOCK_CONTENT_ROTATION) {
|
||||||
|
entity.block_rotation = style.block_content_rotation;
|
||||||
|
}
|
||||||
|
if !flags.contains(F::BLOCK_CONTENT_SCALE) {
|
||||||
|
entity.block_scale = Vector3::new(
|
||||||
|
style.block_content_scale_x,
|
||||||
|
style.block_content_scale_y,
|
||||||
|
style.block_content_scale_z,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !flags.contains(F::SCALE_FACTOR) {
|
||||||
|
entity.scale_factor = style.scale_factor;
|
||||||
|
entity.context.scale_factor = style.scale_factor;
|
||||||
|
}
|
||||||
|
entity.context.block_content_handle = entity.block_content_handle;
|
||||||
|
entity.context.block_content_color = entity.block_content_color;
|
||||||
entity.context.block_connection_type = entity.block_connection_type;
|
entity.context.block_connection_type = entity.block_connection_type;
|
||||||
entity.context.block_rotation = style.block_content_rotation;
|
entity.context.block_rotation = entity.block_rotation;
|
||||||
entity.context.block_content_scale = entity.block_scale;
|
entity.context.block_content_scale = entity.block_scale;
|
||||||
entity.context.scale_factor = style.scale_factor;
|
|
||||||
entity.enable_annotation_scale = style.is_annotative;
|
entity.enable_annotation_scale = style.is_annotative;
|
||||||
|
match entity.content_type {
|
||||||
|
acadrust::entities::LeaderContentType::MText => {
|
||||||
|
entity.context.has_text_contents = true;
|
||||||
|
entity.context.has_block_contents = false;
|
||||||
|
}
|
||||||
|
acadrust::entities::LeaderContentType::Block => {
|
||||||
|
entity.context.has_text_contents = false;
|
||||||
|
entity.context.has_block_contents = entity.block_content_handle.is_some();
|
||||||
|
entity.context.block_content_location = entity.context.content_base_point;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
entity.context.has_text_contents = false;
|
||||||
|
entity.context.has_block_contents = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
for root in &mut entity.context.leader_roots {
|
for root in &mut entity.context.leader_roots {
|
||||||
root.landing_distance = style.landing_distance;
|
if !flags.contains(F::LANDING_DISTANCE) {
|
||||||
|
root.landing_distance = entity.dogleg_length;
|
||||||
|
}
|
||||||
root.text_attachment_direction = entity.text_attachment_direction;
|
root.text_attachment_direction = entity.text_attachment_direction;
|
||||||
for line in &mut root.lines {
|
for line in &mut root.lines {
|
||||||
if line.override_flags.is_empty() {
|
use acadrust::entities::LeaderLinePropertyOverrideFlags as F;
|
||||||
|
if !line.override_flags.contains(F::PATH_TYPE) {
|
||||||
line.path_type = entity.path_type;
|
line.path_type = entity.path_type;
|
||||||
line.line_color = style.line_color;
|
}
|
||||||
line.line_type_handle = style.line_type_handle;
|
if !line.override_flags.contains(F::LINE_COLOR) {
|
||||||
line.line_weight = style.line_weight;
|
line.line_color = entity.line_color;
|
||||||
line.arrowhead_handle = style.arrowhead_handle;
|
}
|
||||||
line.arrowhead_size = style.arrowhead_size;
|
if !line.override_flags.contains(F::LINE_TYPE) {
|
||||||
|
line.line_type_handle = entity.line_type_handle;
|
||||||
|
}
|
||||||
|
if !line.override_flags.contains(F::LINE_WEIGHT) {
|
||||||
|
line.line_weight = entity.line_weight;
|
||||||
|
}
|
||||||
|
if !line.override_flags.contains(F::ARROWHEAD) {
|
||||||
|
line.arrowhead_handle = entity.arrowhead_handle;
|
||||||
|
}
|
||||||
|
if !line.override_flags.contains(F::ARROWHEAD_SIZE) {
|
||||||
|
line.arrowhead_size = entity.arrowhead_size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1571,6 +1658,16 @@ fn apply_mleader_style_at_display_scale(
|
|||||||
style: &acadrust::objects::MultiLeaderStyle,
|
style: &acadrust::objects::MultiLeaderStyle,
|
||||||
display_scale: f64,
|
display_scale: f64,
|
||||||
) {
|
) {
|
||||||
|
use acadrust::entities::MultiLeaderPropertyOverrideFlags as F;
|
||||||
|
|
||||||
|
let old_scale = entity.context.scale_factor.max(1.0e-12);
|
||||||
|
let landing_gap = entity.context.landing_gap / old_scale;
|
||||||
|
let landing_distances = entity
|
||||||
|
.context
|
||||||
|
.leader_roots
|
||||||
|
.iter()
|
||||||
|
.map(|root| root.landing_distance / old_scale)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
apply_mleader_style(entity, style);
|
apply_mleader_style(entity, style);
|
||||||
|
|
||||||
let scale = if display_scale.is_finite()
|
let scale = if display_scale.is_finite()
|
||||||
@@ -1584,18 +1681,27 @@ fn apply_mleader_style_at_display_scale(
|
|||||||
// Context sizes are stored at their display scale.
|
// Context sizes are stored at their display scale.
|
||||||
entity.context.scale_factor = scale;
|
entity.context.scale_factor = scale;
|
||||||
|
|
||||||
entity.context.text_height =
|
entity.context.text_height = entity.text_height * scale;
|
||||||
style.text_height * scale;
|
entity.context.arrowhead_size = entity.arrowhead_size * scale;
|
||||||
|
entity.context.landing_gap = if entity.property_override_flags.contains(F::LANDING_GAP) {
|
||||||
|
landing_gap * scale
|
||||||
|
} else {
|
||||||
|
style.landing_gap * scale
|
||||||
|
};
|
||||||
|
|
||||||
entity.context.arrowhead_size =
|
for (index, root) in entity.context.leader_roots.iter_mut().enumerate() {
|
||||||
style.arrowhead_size * scale;
|
root.landing_distance = if entity
|
||||||
|
.property_override_flags
|
||||||
entity.context.landing_gap =
|
.contains(F::LANDING_DISTANCE)
|
||||||
style.landing_gap * scale;
|
{
|
||||||
|
landing_distances
|
||||||
for root in &mut entity.context.leader_roots {
|
.get(index)
|
||||||
root.landing_distance =
|
.copied()
|
||||||
style.landing_distance * scale;
|
.unwrap_or(entity.dogleg_length)
|
||||||
|
* scale
|
||||||
|
} else {
|
||||||
|
entity.dogleg_length * scale
|
||||||
|
};
|
||||||
|
|
||||||
root.text_attachment_direction =
|
root.text_attachment_direction =
|
||||||
entity.text_attachment_direction;
|
entity.text_attachment_direction;
|
||||||
@@ -1854,4 +1960,3 @@ pub fn is_annotative(doc: &CadDocument, entity: &EntityType) -> bool {
|
|||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,12 +408,23 @@ fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Opti
|
|||||||
.copied()
|
.copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dimension_inference_points(dimension: &Dimension) -> Option<[Option<Vector3>; 2]> {
|
fn dimension_inference_points(dimension: &Dimension) -> Vec<Option<Vector3>> {
|
||||||
match dimension {
|
match dimension {
|
||||||
Dimension::Linear(linear) => Some([Some(linear.first_point), Some(linear.second_point)]),
|
Dimension::Linear(linear) => vec![Some(linear.first_point), Some(linear.second_point)],
|
||||||
Dimension::Aligned(aligned) => Some([Some(aligned.first_point), Some(aligned.second_point)]),
|
Dimension::Aligned(aligned) => vec![Some(aligned.first_point), Some(aligned.second_point)],
|
||||||
Dimension::Ordinate(ordinate) => Some([Some(ordinate.feature_location), None]),
|
Dimension::Angular3Pt(angular) => vec![
|
||||||
_ => None,
|
Some(angular.angle_vertex),
|
||||||
|
Some(angular.first_point),
|
||||||
|
Some(angular.second_point),
|
||||||
|
],
|
||||||
|
Dimension::Angular2Ln(angular) => vec![
|
||||||
|
Some(angular.first_point),
|
||||||
|
Some(angular.second_point),
|
||||||
|
Some(angular.angle_vertex),
|
||||||
|
Some(angular.definition_point),
|
||||||
|
],
|
||||||
|
Dimension::Ordinate(ordinate) => vec![Some(ordinate.feature_location)],
|
||||||
|
_ => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -800,10 +811,10 @@ impl Scene {
|
|||||||
pub(crate) fn infer_dimension_sources(
|
pub(crate) fn infer_dimension_sources(
|
||||||
&self,
|
&self,
|
||||||
dimension: Handle,
|
dimension: Handle,
|
||||||
) -> [Option<Handle>; 2] {
|
) -> Vec<Option<Handle>> {
|
||||||
let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension)
|
let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension)
|
||||||
else {
|
else {
|
||||||
return [None, None];
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let radial_data = match entity {
|
let radial_data = match entity {
|
||||||
Dimension::Radius(radius) => Some((
|
Dimension::Radius(radius) => Some((
|
||||||
@@ -845,23 +856,25 @@ impl Scene {
|
|||||||
})
|
})
|
||||||
.min_by(|first, second| first.0.total_cmp(&second.0))
|
.min_by(|first, second| first.0.total_cmp(&second.0))
|
||||||
.map(|(_, handle)| handle);
|
.map(|(_, handle)| handle);
|
||||||
return [source, None];
|
return vec![source];
|
||||||
}
|
}
|
||||||
let Some(points) = dimension_inference_points(entity) else {
|
dimension_inference_points(entity)
|
||||||
return [None, None];
|
.into_iter()
|
||||||
};
|
.map(|point| {
|
||||||
points.map(|point| {
|
point.and_then(|point| {
|
||||||
point.and_then(|point| self.document
|
self.document
|
||||||
.entities()
|
.entities()
|
||||||
.filter(|entity| entity.common().handle != dimension)
|
.filter(|entity| entity.common().handle != dimension)
|
||||||
.filter_map(|entity| {
|
.filter_map(|entity| {
|
||||||
source_distance_squared(entity, point)
|
source_distance_squared(entity, point)
|
||||||
.map(|distance| (distance, entity.common().handle))
|
.map(|distance| (distance, entity.common().handle))
|
||||||
|
})
|
||||||
|
.filter(|(distance, _)| *distance <= 1e-16)
|
||||||
|
.min_by(|first, second| first.0.total_cmp(&second.0))
|
||||||
|
.map(|(_, handle)| handle)
|
||||||
})
|
})
|
||||||
.filter(|(distance, _)| *distance <= 1e-16)
|
})
|
||||||
.min_by(|first, second| first.0.total_cmp(&second.0))
|
.collect()
|
||||||
.map(|(_, handle)| handle))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn refresh_associative_dimensions(
|
pub(crate) fn refresh_associative_dimensions(
|
||||||
@@ -970,7 +983,7 @@ impl Scene {
|
|||||||
if let Some(point) = resolved[3] {
|
if let Some(point) = resolved[3] {
|
||||||
angular.definition_point = point;
|
angular.definition_point = point;
|
||||||
}
|
}
|
||||||
angular.base.definition_point = angular.definition_point;
|
angular.base.definition_point = angular.dimension_arc;
|
||||||
}
|
}
|
||||||
Dimension::Radius(radius) => {
|
Dimension::Radius(radius) => {
|
||||||
let Some((radial, angle)) = radial_source else {
|
let Some((radial, angle)) = radial_source else {
|
||||||
@@ -1137,9 +1150,21 @@ impl Scene {
|
|||||||
arc.base.actual_measurement = arc.measurement();
|
arc.base.actual_measurement = arc.measurement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dimension.base_mut().actual_measurement = dimension.measurement();
|
let measurement = match &*dimension {
|
||||||
|
Dimension::Linear(linear) => linear_measurement(linear),
|
||||||
|
_ => dimension.measurement(),
|
||||||
|
};
|
||||||
|
dimension.base_mut().actual_measurement = measurement;
|
||||||
refreshed.push((association.dimension, ChangeKind::Modified));
|
refreshed.push((association.dimension, ChangeKind::Modified));
|
||||||
}
|
}
|
||||||
refreshed
|
refreshed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn linear_measurement(linear: &acadrust::entities::DimensionLinear) -> f64 {
|
||||||
|
let plane = plane_from_normal(linear.first_point, linear.base.normal);
|
||||||
|
let first = plane.project(dpoint(linear.first_point)).unwrap_or([0.0; 2]);
|
||||||
|
let second = plane.project(dpoint(linear.second_point)).unwrap_or(first);
|
||||||
|
let axis = [linear.rotation.cos(), linear.rotation.sin()];
|
||||||
|
((second[0] - first[0]) * axis[0] + (second[1] - first[1]) * axis[1]).abs()
|
||||||
|
}
|
||||||
@@ -1420,6 +1420,9 @@ pub struct Scene {
|
|||||||
pub selection: Rc<RefCell<SelectionState>>,
|
pub selection: Rc<RefCell<SelectionState>>,
|
||||||
/// The CAD document — single source of truth for all entities.
|
/// The CAD document — single source of truth for all entities.
|
||||||
pub document: CadDocument,
|
pub document: CadDocument,
|
||||||
|
/// Last chain-compatible dimension created this session.
|
||||||
|
/// Never reconstructed from loaded file handles.
|
||||||
|
pub last_created_dimension: Option<Handle>,
|
||||||
/// File-open-prepared index for non-graphical semantic object lookups.
|
/// File-open-prepared index for non-graphical semantic object lookups.
|
||||||
pub(crate) object_data_cache: crate::entities::object_data::ObjectDataCache,
|
pub(crate) object_data_cache: crate::entities::object_data::ObjectDataCache,
|
||||||
/// Native AcDbLight/Sun inputs, separated by rendered block and viewport
|
/// Native AcDbLight/Sun inputs, separated by rendered block and viewport
|
||||||
@@ -1848,6 +1851,7 @@ impl Scene {
|
|||||||
model_pane_min_px: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
|
model_pane_min_px: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
|
||||||
selection: Rc::new(RefCell::new(SelectionState::default())),
|
selection: Rc::new(RefCell::new(SelectionState::default())),
|
||||||
document: CadDocument::new(),
|
document: CadDocument::new(),
|
||||||
|
last_created_dimension: None,
|
||||||
object_data_cache: crate::entities::object_data::ObjectDataCache::default(),
|
object_data_cache: crate::entities::object_data::ObjectDataCache::default(),
|
||||||
lighting_cache: RefCell::new(HashMap::default()),
|
lighting_cache: RefCell::new(HashMap::default()),
|
||||||
selected: HashSet::default(),
|
selected: HashSet::default(),
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ pub enum PropValue {
|
|||||||
ColorVaries,
|
ColorVaries,
|
||||||
/// Line weight — rendered as a combo_box.
|
/// Line weight — rendered as a combo_box.
|
||||||
LwChoice(LineWeight),
|
LwChoice(LineWeight),
|
||||||
|
/// Object-specific line weight routed by field name.
|
||||||
|
FieldLwChoice {
|
||||||
|
field: &'static str,
|
||||||
|
value: LineWeight,
|
||||||
|
},
|
||||||
|
/// Object-specific lineweight varies across the current selection.
|
||||||
|
FieldLwVaries { field: &'static str },
|
||||||
/// Lineweight varies across the current multi-selection.
|
/// Lineweight varies across the current multi-selection.
|
||||||
LwVaries,
|
LwVaries,
|
||||||
/// Linetype name — rendered as a combo_box.
|
/// Linetype name — rendered as a combo_box.
|
||||||
|
|||||||
+11
-4
@@ -539,7 +539,8 @@ impl Scene {
|
|||||||
"ByLayer".into(),
|
"ByLayer".into(),
|
||||||
"ByBlock".into(),
|
"ByBlock".into(),
|
||||||
]),
|
]),
|
||||||
PropValue::LwChoice(_) => QSelectValueEditor::Choice(vec![
|
PropValue::LwChoice(_)
|
||||||
|
| PropValue::FieldLwChoice { .. } => QSelectValueEditor::Choice(vec![
|
||||||
"ByLayer".into(),
|
"ByLayer".into(),
|
||||||
"ByBlock".into(),
|
"ByBlock".into(),
|
||||||
"Default".into(),
|
"Default".into(),
|
||||||
@@ -567,7 +568,8 @@ impl Scene {
|
|||||||
PropValue::AttrText { .. } => QSelectValueEditor::Text,
|
PropValue::AttrText { .. } => QSelectValueEditor::Text,
|
||||||
PropValue::Stepper { .. }
|
PropValue::Stepper { .. }
|
||||||
| PropValue::ColorVaries
|
| PropValue::ColorVaries
|
||||||
| PropValue::LwVaries => continue,
|
| PropValue::LwVaries
|
||||||
|
| PropValue::FieldLwVaries { .. } => continue,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
out.push(choice(prop.field, prop.label, editor));
|
out.push(choice(prop.field, prop.label, editor));
|
||||||
@@ -696,13 +698,18 @@ impl Scene {
|
|||||||
PropValue::Choice { selected, .. } => selected,
|
PropValue::Choice { selected, .. } => selected,
|
||||||
PropValue::EditChoice { value, .. } => value,
|
PropValue::EditChoice { value, .. } => value,
|
||||||
PropValue::ColorChoice(c) => Self::format_color(c),
|
PropValue::ColorChoice(c) => Self::format_color(c),
|
||||||
PropValue::LwChoice(lw) => Self::format_lineweight(lw),
|
PropValue::LwChoice(lw)
|
||||||
|
| PropValue::FieldLwChoice { value: lw, .. } => {
|
||||||
|
Self::format_lineweight(lw)
|
||||||
|
}
|
||||||
PropValue::LinetypeChoice(s) => s,
|
PropValue::LinetypeChoice(s) => s,
|
||||||
PropValue::HatchPatternChoice(s) => s,
|
PropValue::HatchPatternChoice(s) => s,
|
||||||
PropValue::BoolToggle { value, .. } => value.to_string(),
|
PropValue::BoolToggle { value, .. } => value.to_string(),
|
||||||
PropValue::AttrText { value, .. } => value,
|
PropValue::AttrText { value, .. } => value,
|
||||||
PropValue::Stepper { display, .. } => display,
|
PropValue::Stepper { display, .. } => display,
|
||||||
PropValue::ColorVaries | PropValue::LwVaries => return None,
|
PropValue::ColorVaries
|
||||||
|
| PropValue::LwVaries
|
||||||
|
| PropValue::FieldLwVaries { .. } => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+91
-3
@@ -31,6 +31,7 @@ const PATTERN_PREVIEW_H: f32 = 58.0;
|
|||||||
const PATTERN_PICKER_W: f32 = 348.0;
|
const PATTERN_PICKER_W: f32 = 348.0;
|
||||||
const PATTERN_PICKER_H: f32 = 720.0;
|
const PATTERN_PICKER_H: f32 = 720.0;
|
||||||
const LINETYPE_MENU_W: f32 = 220.0;
|
const LINETYPE_MENU_W: f32 = 220.0;
|
||||||
|
const PROPERTIES_SCROLLBAR_GUTTER: f32 = 16.0;
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::scene::model::object::{PropSection, PropValue};
|
use crate::scene::model::object::{PropSection, PropValue};
|
||||||
@@ -620,7 +621,13 @@ impl PropertiesPanel {
|
|||||||
for section in &self.sections {
|
for section in &self.sections {
|
||||||
col = col.push(self.render_section(section));
|
col = col.push(self.render_section(section));
|
||||||
}
|
}
|
||||||
scrollable(col).into()
|
let gutter = Padding {
|
||||||
|
right: PROPERTIES_SCROLLBAR_GUTTER,
|
||||||
|
..Padding::ZERO
|
||||||
|
};
|
||||||
|
scrollable(container(col.width(Length::Fill)).padding(gutter))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
container(column![header, title_bar, content])
|
container(column![header, title_bar, content])
|
||||||
@@ -673,7 +680,15 @@ impl PropertiesPanel {
|
|||||||
sections = sections.push(self.render_section(section));
|
sections = sections.push(self.render_section(section));
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = scrollable(sections).height(Length::Shrink);
|
let gutter = Padding {
|
||||||
|
right: PROPERTIES_SCROLLBAR_GUTTER,
|
||||||
|
..Padding::ZERO
|
||||||
|
};
|
||||||
|
let content = scrollable(
|
||||||
|
container(sections.width(Length::Fill)).padding(gutter),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Shrink);
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
container(column![title, content].spacing(0))
|
container(column![title, content].spacing(0))
|
||||||
@@ -763,6 +778,12 @@ impl PropertiesPanel {
|
|||||||
PropValue::ColorVaries => self.render_color_varies_row(label),
|
PropValue::ColorVaries => self.render_color_varies_row(label),
|
||||||
PropValue::LayerChoice(layer) => self.render_layer_row(label, layer),
|
PropValue::LayerChoice(layer) => self.render_layer_row(label, layer),
|
||||||
PropValue::LwChoice(lw) => self.render_lw_row(label, *lw),
|
PropValue::LwChoice(lw) => self.render_lw_row(label, *lw),
|
||||||
|
PropValue::FieldLwChoice { field, value } => {
|
||||||
|
self.render_field_lw_row(label, field, *value)
|
||||||
|
}
|
||||||
|
PropValue::FieldLwVaries { field } => {
|
||||||
|
self.render_field_lw_varies_row(label, field)
|
||||||
|
}
|
||||||
PropValue::LwVaries => self.render_lw_varies_row(label),
|
PropValue::LwVaries => self.render_lw_varies_row(label),
|
||||||
PropValue::LinetypeChoice(lt) => self.render_linetype_row(label, lt),
|
PropValue::LinetypeChoice(lt) => self.render_linetype_row(label, lt),
|
||||||
PropValue::Choice { selected, options } => {
|
PropValue::Choice { selected, options } => {
|
||||||
@@ -861,6 +882,10 @@ impl PropertiesPanel {
|
|||||||
| "dim_ext_line_color"
|
| "dim_ext_line_color"
|
||||||
| "dim_text_color"
|
| "dim_text_color"
|
||||||
| "dim_text_fill_color"
|
| "dim_text_fill_color"
|
||||||
|
| "line_color"
|
||||||
|
| "text_color"
|
||||||
|
| "block_content_color"
|
||||||
|
| "background_fill_color"
|
||||||
) {
|
) {
|
||||||
let open = self.open_color_field.as_deref() == Some(field);
|
let open = self.open_color_field.as_deref() == Some(field);
|
||||||
let fsel = field.to_string();
|
let fsel = field.to_string();
|
||||||
@@ -871,7 +896,9 @@ impl PropertiesPanel {
|
|||||||
background: true,
|
background: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
} else if field.starts_with("dim_") {
|
} else if field.starts_with("dim_")
|
||||||
|
|| matches!(field, "line_color" | "text_color" | "block_content_color")
|
||||||
|
{
|
||||||
crate::ui::color_select::ColorExtras {
|
crate::ui::color_select::ColorExtras {
|
||||||
by_layer: true,
|
by_layer: true,
|
||||||
by_block: true,
|
by_block: true,
|
||||||
@@ -998,6 +1025,36 @@ impl PropertiesPanel {
|
|||||||
prop_row_widget(label, combo.into())
|
prop_row_widget(label, combo.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_field_lw_row<'a>(
|
||||||
|
&'a self,
|
||||||
|
label: &'a str,
|
||||||
|
field: &'static str,
|
||||||
|
lw: LineWeight,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
let selected = LwItem(lw);
|
||||||
|
let combo = combo_box(
|
||||||
|
&self.lineweight_combo,
|
||||||
|
"",
|
||||||
|
Some(&selected),
|
||||||
|
move |item: LwItem| Message::PropFieldLwChanged {
|
||||||
|
field,
|
||||||
|
value: item.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.size(FONT_SZ)
|
||||||
|
.padding(Padding {
|
||||||
|
top: COMBO_PAD_V,
|
||||||
|
bottom: COMBO_PAD_V,
|
||||||
|
left: 6.0,
|
||||||
|
right: 6.0,
|
||||||
|
})
|
||||||
|
.input_style(combo_input_style)
|
||||||
|
.on_open(Message::PropColorPickerClose)
|
||||||
|
.width(Length::Fill);
|
||||||
|
|
||||||
|
prop_row_widget(label, combo.into())
|
||||||
|
}
|
||||||
|
|
||||||
fn render_lw_varies_row<'a>(&'a self, label: &'a str) -> Element<'a, Message> {
|
fn render_lw_varies_row<'a>(&'a self, label: &'a str) -> Element<'a, Message> {
|
||||||
let combo = combo_box(
|
let combo = combo_box(
|
||||||
&self.lineweight_combo,
|
&self.lineweight_combo,
|
||||||
@@ -1019,6 +1076,34 @@ impl PropertiesPanel {
|
|||||||
prop_row_widget(label, combo.into())
|
prop_row_widget(label, combo.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_field_lw_varies_row<'a>(
|
||||||
|
&'a self,
|
||||||
|
label: &'a str,
|
||||||
|
field: &'static str,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
let combo = combo_box(
|
||||||
|
&self.lineweight_combo,
|
||||||
|
VARIES_LABEL,
|
||||||
|
None,
|
||||||
|
move |item: LwItem| Message::PropFieldLwChanged {
|
||||||
|
field,
|
||||||
|
value: item.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.size(FONT_SZ)
|
||||||
|
.padding(Padding {
|
||||||
|
top: COMBO_PAD_V,
|
||||||
|
bottom: COMBO_PAD_V,
|
||||||
|
left: 6.0,
|
||||||
|
right: 6.0,
|
||||||
|
})
|
||||||
|
.input_style(combo_input_style)
|
||||||
|
.on_open(Message::PropColorPickerClose)
|
||||||
|
.width(Length::Fill);
|
||||||
|
|
||||||
|
prop_row_widget(label, combo.into())
|
||||||
|
}
|
||||||
|
|
||||||
// ── Linetype row (combo_box) ──────────────────────────────────────────
|
// ── Linetype row (combo_box) ──────────────────────────────────────────
|
||||||
|
|
||||||
fn render_linetype_row<'a>(&'a self, label: &'a str, current: &'a str) -> Element<'a, Message> {
|
fn render_linetype_row<'a>(&'a self, label: &'a str, current: &'a str) -> Element<'a, Message> {
|
||||||
@@ -1701,6 +1786,9 @@ fn coord_suffix(label: &str) -> Option<(&str, usize)> {
|
|||||||
/// Length of the coordinate group starting at `idx`: consecutive text rows
|
/// Length of the coordinate group starting at `idx`: consecutive text rows
|
||||||
/// labelled "<Base> X", "<Base> Y" and optionally "<Base> Z". 0/1 = no group.
|
/// labelled "<Base> X", "<Base> Y" and optionally "<Base> Z". 0/1 = no group.
|
||||||
fn coord_group_len(props: &[crate::scene::model::object::Property], idx: usize) -> usize {
|
fn coord_group_len(props: &[crate::scene::model::object::Property], idx: usize) -> usize {
|
||||||
|
if props[idx].field == "pl3_vertex_x" {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
let groupable = |p: &crate::scene::model::object::Property| {
|
let groupable = |p: &crate::scene::model::object::Property| {
|
||||||
matches!(
|
matches!(
|
||||||
p.value,
|
p.value,
|
||||||
|
|||||||
Reference in new issue
Block a user