forked from 1b-agent/opencadstudio-upstream
Merge pull request #896 from ramox81/codex/fix-mleader-parity
Complete multileader creation and property behavior
This commit is contained in:
17 files changed
+1624
-349
No files matched your search
@@ -2447,6 +2447,8 @@ properties =
|
||||
.associative = Associative
|
||||
.at-least = At least
|
||||
.attachment-type = Attachment type
|
||||
.attachment = Attachment
|
||||
.arrowhead-size = Arrowhead Size
|
||||
.attenuation = Attenuation
|
||||
.attribute-definition = Attribute Definition
|
||||
.attributedefinition = AttributeDefinition
|
||||
|
||||
@@ -2387,6 +2387,8 @@ properties =
|
||||
.associative = İlişkisel
|
||||
.at-least = En az
|
||||
.attachment-type = Bağlantı türü
|
||||
.attachment = Bağlantı
|
||||
.arrowhead-size = Ok Ucu Boyutu
|
||||
.attenuation = Zayıflama
|
||||
.attribute-definition = Öznitelik Tanımı
|
||||
.attributedefinition = Öznitelik Tanımı
|
||||
|
||||
+175
-95
@@ -1561,6 +1561,48 @@ impl OpenCADStudio {
|
||||
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 {
|
||||
entity,
|
||||
solid,
|
||||
@@ -2020,30 +2062,6 @@ impl OpenCADStudio {
|
||||
self.tabs[i].snap_result = 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4598,92 +4616,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.
|
||||
fn apply_mleader_align(scene: &mut crate::scene::Scene, encoded: &str) {
|
||||
// Format: "<h1>,<h2>,...;<fx>,<fz>;<tx>,<tz>"
|
||||
let parts: Vec<&str> = encoded.splitn(3, ';').collect();
|
||||
if parts.len() < 3 {
|
||||
return;
|
||||
}
|
||||
let handles: Vec<acadrust::Handle> = parts[0]
|
||||
.split(',')
|
||||
.filter_map(|s| s.parse::<u64>().ok().map(acadrust::Handle::from))
|
||||
.collect();
|
||||
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();
|
||||
if from_parts.len() < 2 || to_parts.len() < 2 || handles.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let fx = from_parts[0];
|
||||
let fz = from_parts[1];
|
||||
let tx = to_parts[0];
|
||||
let tz = to_parts[1];
|
||||
let dx = tx - fx;
|
||||
let dz = tz - fz;
|
||||
let len = (dx * dx + dz * dz).sqrt();
|
||||
if len < 1e-9 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Project each multileader's content point onto the alignment line, then
|
||||
// snap it to the line (preserve perpendicular offset from line is discarded;
|
||||
// align along direction through `from`).
|
||||
for h in handles {
|
||||
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;
|
||||
let Some(direction) = Vec2::new(to.x - from.x, to.y - from.y).normalize() else {
|
||||
return false;
|
||||
};
|
||||
let line = Curve::XLine(XLine {
|
||||
base: [from.x, from.y],
|
||||
direction: direction.into(),
|
||||
});
|
||||
let mut changed = Vec::new();
|
||||
for &handle in handles {
|
||||
if let Some(acadrust::EntityType::MultiLeader(ml)) =
|
||||
scene.document.get_entity_mut(handle)
|
||||
{
|
||||
let old = ml.context.content_base_point;
|
||||
let projected = closest_point(&line, [old.x, old.y]).point;
|
||||
let new_x = projected[0];
|
||||
let new_y = projected[1];
|
||||
let shift_x = new_x - old.x;
|
||||
let shift_y = new_y - old.y;
|
||||
if shift_x.abs() <= 1.0e-12 && shift_y.abs() <= 1.0e-12 {
|
||||
continue;
|
||||
}
|
||||
ml.context.content_base_point.x = new_x;
|
||||
ml.context.content_base_point.y = new_y;
|
||||
ml.context.text_location.x += shift_x;
|
||||
ml.context.text_location.y += shift_y;
|
||||
ml.context.block_content_location.x += shift_x;
|
||||
ml.context.block_content_location.y += shift_y;
|
||||
for root in &mut ml.context.leader_roots {
|
||||
root.connection_point.x += shift_x;
|
||||
root.connection_point.y += shift_y;
|
||||
}
|
||||
changed.push((handle, crate::scene::ChangeKind::Modified));
|
||||
}
|
||||
}
|
||||
if !changed.is_empty() {
|
||||
scene.bump_entities(&changed);
|
||||
}
|
||||
!changed.is_empty()
|
||||
}
|
||||
|
||||
// ── MLEADERCOLLECT helper ─────────────────────────────────────────────────────
|
||||
|
||||
/// Parse `h1,h2,...;px,pz` — merge all selected multileaders into the first one at position.
|
||||
fn apply_mleader_collect(scene: &mut crate::scene::Scene, encoded: &str) {
|
||||
let parts: Vec<&str> = encoded.splitn(2, ';').collect();
|
||||
if parts.len() < 2 {
|
||||
return;
|
||||
}
|
||||
let handles: Vec<acadrust::Handle> = parts[0]
|
||||
.split(',')
|
||||
.filter_map(|s| s.parse::<u64>().ok().map(acadrust::Handle::from))
|
||||
.collect();
|
||||
let pos_parts: Vec<f64> = parts[1].split(',').filter_map(|s| s.parse().ok()).collect();
|
||||
if handles.len() < 2 || pos_parts.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let px = pos_parts[0];
|
||||
let pz = pos_parts[1];
|
||||
|
||||
// Collect all leader roots from the secondary multileaders.
|
||||
let mut extra_roots: Vec<acadrust::entities::LeaderRoot> = Vec::new();
|
||||
for &h in &handles[1..] {
|
||||
if let Some(acadrust::EntityType::MultiLeader(ml)) = scene.document.get_entity(h) {
|
||||
extra_roots.extend(ml.context.leader_roots.iter().cloned());
|
||||
fn compatible_mleader_collect_handles(
|
||||
scene: &crate::scene::Scene,
|
||||
handles: &[acadrust::Handle],
|
||||
) -> Vec<acadrust::Handle> {
|
||||
let Some((base_block, base_style)) = handles.iter().find_map(|handle| {
|
||||
if scene.is_layer_locked(*handle) {
|
||||
return None;
|
||||
}
|
||||
let acadrust::EntityType::MultiLeader(leader) =
|
||||
scene.document.get_entity(*handle)?
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
(leader.content_type == acadrust::entities::LeaderContentType::Block)
|
||||
.then_some((leader.block_content_handle, leader.style_handle))
|
||||
}) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut compatible = Vec::new();
|
||||
for handle in handles.iter().copied() {
|
||||
if compatible.contains(&handle) {
|
||||
continue;
|
||||
}
|
||||
if !scene.is_layer_locked(handle)
|
||||
&& 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]) {
|
||||
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.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 {
|
||||
ml.context.leader_roots.push(root);
|
||||
}
|
||||
}
|
||||
|
||||
// Erase the secondary multileaders.
|
||||
scene.erase_entities(&handles[1..]);
|
||||
scene.erase_entities(&merged_handles);
|
||||
scene.bump_entities(&[(handles[0], crate::scene::ChangeKind::Modified)]);
|
||||
true
|
||||
}
|
||||
|
||||
/// MATCHPROP special properties (#281): copy every STYLE-affecting field from
|
||||
|
||||
+23
-1
@@ -604,9 +604,31 @@ impl OpenCADStudio {
|
||||
_ => None,
|
||||
});
|
||||
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| {
|
||||
MLeaderCommand::with_style(style, multiplier)
|
||||
});
|
||||
}).with_drawing_resources(block_sources, layers, text_styles);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
@@ -2402,6 +2402,11 @@ pub enum Message {
|
||||
PropColorChanged(AcadColor),
|
||||
/// User selected a lineweight from the Properties pick_list.
|
||||
PropLwChanged(LineWeight),
|
||||
/// User selected an object-specific lineweight override.
|
||||
PropFieldLwChanged {
|
||||
field: &'static str,
|
||||
value: LineWeight,
|
||||
},
|
||||
/// User selected a linetype from the linetype pick_list.
|
||||
PropLinetypeChanged(String),
|
||||
/// User toggled a boolean property (e.g. Invisible).
|
||||
|
||||
+66
-5
@@ -845,6 +845,12 @@ impl OpenCADStudio {
|
||||
.map(|b| b.name.clone()),
|
||||
)
|
||||
.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();
|
||||
// Currently selected names.
|
||||
let cur_style = ml
|
||||
@@ -878,6 +884,15 @@ impl OpenCADStudio {
|
||||
doc.line_types.iter().find(|l| l.handle == h).map(|l| l.name.clone())
|
||||
})
|
||||
.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 =
|
||||
|field: &str, selected: String, options: Vec<String>| {
|
||||
for section in sections.iter_mut() {
|
||||
@@ -896,6 +911,9 @@ impl OpenCADStudio {
|
||||
set_choice("text_style_handle", cur_tstyle, tstyle_names);
|
||||
set_choice("arrowhead_handle", cur_arrow, arrow_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.
|
||||
@@ -1394,6 +1412,21 @@ impl OpenCADStudio {
|
||||
// MultiLeader: max points + segment-angle constraints
|
||||
// are MLeaderStyle settings, not stored on the entity.
|
||||
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((mx, a1, a2)) =
|
||||
doc.objects.iter().find_map(|(h, o)| match o {
|
||||
@@ -2562,9 +2595,21 @@ impl OpenCADStudio {
|
||||
| acadrust::EntityType::Polyline2D(_)
|
||||
| acadrust::EntityType::Polyline3D(_)
|
||||
);
|
||||
let layer = &self.tabs[i].active_layer;
|
||||
if layer != "0" || entity.as_entity().layer().is_empty() {
|
||||
entity.as_entity_mut().set_layer(layer.clone());
|
||||
let explicit_mleader_layer = match &entity {
|
||||
acadrust::EntityType::MultiLeader(leader) => leader
|
||||
.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
|
||||
@@ -2806,8 +2851,13 @@ fn make_sections_read_only(
|
||||
acadrust::types::Color::Index(index) => index.to_string(),
|
||||
acadrust::types::Color::Rgb { r, g, b } => format!("{r},{g},{b}"),
|
||||
},
|
||||
PropValue::ColorVaries | PropValue::LwVaries => VARIES_LABEL.to_string(),
|
||||
PropValue::LwChoice(lineweight) => {
|
||||
PropValue::ColorVaries
|
||||
| PropValue::LwVaries
|
||||
| PropValue::FieldLwVaries { .. } => VARIES_LABEL.to_string(),
|
||||
PropValue::LwChoice(lineweight)
|
||||
| PropValue::FieldLwChoice {
|
||||
value: lineweight, ..
|
||||
} => {
|
||||
ui::properties::LwItem(*lineweight).to_string()
|
||||
}
|
||||
PropValue::BoolToggle { value, .. } => {
|
||||
@@ -2965,6 +3015,17 @@ fn merge_prop_value(
|
||||
(PropValue::LwChoice(_), PropValue::LwChoice(_))
|
||||
| (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(VARIES_LABEL.into())
|
||||
}
|
||||
|
||||
@@ -2053,6 +2053,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
| "text_style_handle"
|
||||
| "arrowhead_handle"
|
||||
| "line_type_handle"
|
||||
| "block_content_handle"
|
||||
) {
|
||||
// Resolve a picked name back to the handle the MLEADER
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
"block_content_handle" => doc
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|block| block.name == value)
|
||||
.map(|block| block.handle),
|
||||
_ => None,
|
||||
};
|
||||
for &handle in &handles {
|
||||
@@ -2121,10 +2127,52 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
"text_style_handle" => {
|
||||
if let Some(h) = resolved {
|
||||
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,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4657,6 +4657,32 @@ impl OpenCADStudio {
|
||||
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) => {
|
||||
let i = self.active_tab;
|
||||
let handles = self.property_target_handles(i);
|
||||
@@ -5114,6 +5140,54 @@ impl OpenCADStudio {
|
||||
}
|
||||
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!(
|
||||
field.as_str(),
|
||||
"tbl_cell_content_color" | "tbl_cell_background_color"
|
||||
|
||||
@@ -1260,6 +1260,17 @@ pub enum CmdResult {
|
||||
/// 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 /
|
||||
/// persistence) plus its B-rep (cached for boolean ops + shaded
|
||||
/// rendering). Ends the command.
|
||||
|
||||
+300
-77
@@ -1,7 +1,9 @@
|
||||
use acadrust::entities::{
|
||||
FlowDirectionType, LeaderContentType, LineSpacingStyle, MultiLeader, MultiLeaderPathType,
|
||||
TextAlignmentType, TextAttachmentDirectionType, TextAttachmentType,
|
||||
BlockContentConnectionType, FlowDirectionType, LeaderContentType, LineSpacingStyle,
|
||||
MultiLeader, MultiLeaderPathType, MultiLeaderPropertyOverrideFlags, TextAlignmentType,
|
||||
TextAttachmentDirectionType, TextAttachmentType,
|
||||
};
|
||||
use cadkernel::geom2d::Transform;
|
||||
|
||||
use crate::entities::text_support::{
|
||||
layout_mtext, resolve_text_style, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle,
|
||||
@@ -904,9 +906,7 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
||||
// ── Leaders ──────────────────────────────────────────────────────────
|
||||
// Landing rows are folded in here: the standalone "Leader Structure" group
|
||||
// is a style-dialog tab, not a palette group.
|
||||
let leaders = PropSection {
|
||||
title: t!("Leaders").into_owned(),
|
||||
props: vec![
|
||||
let mut leader_props = vec![
|
||||
choice(
|
||||
t!("Leader type").as_ref(),
|
||||
"path_type",
|
||||
@@ -923,20 +923,34 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
||||
Property {
|
||||
label: t!("Leader lineweight").into_owned(),
|
||||
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").
|
||||
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),
|
||||
num_row(
|
||||
t!("Landing distance").as_ref(),
|
||||
"landing_distance",
|
||||
ml.dogleg_length,
|
||||
if ml.enable_dogleg { ml.dogleg_length } else { 0.0 },
|
||||
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) ──────────────────────────────
|
||||
@@ -988,6 +1002,18 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
||||
attach_dir_str(&ml.text_attachment_direction),
|
||||
&["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(
|
||||
t!("Left Attachment").as_ref(),
|
||||
"text_left_attachment",
|
||||
@@ -1000,10 +1026,23 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
||||
attachment_str(&ml.text_right_attachment),
|
||||
&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 ────────────────────────────────────────────────────────────
|
||||
let block = PropSection {
|
||||
@@ -1014,29 +1053,16 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
||||
"block_content_handle",
|
||||
hexh(ml.block_content_handle),
|
||||
),
|
||||
ro(
|
||||
t!("Block connection").as_ref(),
|
||||
choice(
|
||||
t!("Attachment").as_ref(),
|
||||
"block_connection_type",
|
||||
format!("{:?}", ml.block_connection_type),
|
||||
),
|
||||
ro(
|
||||
t!("Block color").as_ref(),
|
||||
"block_content_color",
|
||||
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(),
|
||||
match ml.block_connection_type {
|
||||
BlockContentConnectionType::BasePoint => "Insertion point",
|
||||
BlockContentConnectionType::BlockExtents => "Extents",
|
||||
},
|
||||
&["Extents", "Insertion point"],
|
||||
),
|
||||
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" => {
|
||||
if let Some(v) = f64(value) {
|
||||
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" => {
|
||||
@@ -1093,7 +1121,9 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
!ml.text_frame
|
||||
} else {
|
||||
value == "true"
|
||||
}
|
||||
};
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::TEXT_FRAME);
|
||||
}
|
||||
"path_type" => {
|
||||
ml.path_type = match value {
|
||||
@@ -1101,19 +1131,40 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
"None" => MultiLeaderPathType::Invisible,
|
||||
_ => 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" => {
|
||||
ml.enable_landing = if value == "toggle" {
|
||||
!ml.enable_landing
|
||||
} else {
|
||||
value == "true"
|
||||
}
|
||||
};
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::ENABLE_LANDING);
|
||||
}
|
||||
"enable_dogleg" => {
|
||||
ml.enable_dogleg = if value == "toggle" {
|
||||
!ml.enable_dogleg
|
||||
} else {
|
||||
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" => {
|
||||
@@ -1130,39 +1181,130 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
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" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.dogleg_length = v;
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::LANDING_DISTANCE);
|
||||
}
|
||||
}
|
||||
"arrowhead_size" => {
|
||||
if let Some(v) = f64(value) {
|
||||
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" => {
|
||||
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" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.dogleg_length = v;
|
||||
if let Some(root) = ml.context.leader_roots.first_mut() {
|
||||
root.landing_distance = v;
|
||||
let display = v * ml.context.scale_factor.max(1.0e-12);
|
||||
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" => {
|
||||
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" => {
|
||||
if let Some(v) = f64(value) {
|
||||
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" => {
|
||||
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
|
||||
root.connection_point.x = v;
|
||||
@@ -1181,18 +1323,26 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
"text_left_attachment" => {
|
||||
ml.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" => {
|
||||
ml.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" => {
|
||||
ml.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" => {
|
||||
ml.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" => {
|
||||
ml.text_alignment = match value {
|
||||
@@ -1205,6 +1355,8 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
"Right" => TextAlignmentType::Right,
|
||||
_ => TextAlignmentType::Left,
|
||||
};
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::TEXT_ALIGNMENT);
|
||||
}
|
||||
"text_flow_direction" => {
|
||||
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,
|
||||
_ => 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" => {
|
||||
ml.context.line_spacing_style = match value {
|
||||
@@ -1233,6 +1395,13 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
"text_rotation" => {
|
||||
if let Some(v) = f64(value) {
|
||||
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" => {
|
||||
@@ -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 {
|
||||
match s {
|
||||
"Top of top line" => TextAttachmentType::TopOfTopLine,
|
||||
@@ -1758,9 +1948,7 @@ impl MultiLeaderTess for MultiLeader {
|
||||
}
|
||||
}
|
||||
|
||||
// Vertically-attached leaders (text above/below) have no dogleg:
|
||||
// the leader meets the text edge directly, so the short horizontal
|
||||
// dash would be a stray mark AutoCAD never draws.
|
||||
// Vertically attached leaders meet the text edge directly.
|
||||
let vertical_attach = matches!(
|
||||
ml.text_attachment_direction,
|
||||
acadrust::entities::multileader::TextAttachmentDirectionType::Vertical
|
||||
@@ -1785,11 +1973,7 @@ impl MultiLeaderTess for MultiLeader {
|
||||
// for a rotated leader that is the angled baseline, not world
|
||||
// X. Roots without a usable direction keep the legacy
|
||||
// horizontal toward the text side.
|
||||
let (ux, uy) = ml
|
||||
.context
|
||||
.leader_roots
|
||||
.first()
|
||||
.map(|r| (r.direction.x, r.direction.y))
|
||||
let (ux, uy) = Some((root.direction.x, root.direction.y))
|
||||
.filter(|(x, y)| (x * x + y * y).sqrt() > 1e-9)
|
||||
.map(|(x, y)| {
|
||||
let l = (x * x + y * y).sqrt();
|
||||
@@ -1804,6 +1988,15 @@ impl MultiLeaderTess for MultiLeader {
|
||||
points.push(nan);
|
||||
points.push(cp_f);
|
||||
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;
|
||||
// every child then uses its normal tessellator.
|
||||
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
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|br| br.handle == h)
|
||||
.map(|br| br.name.clone()),
|
||||
.cloned(),
|
||||
_ => 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 {
|
||||
line_color
|
||||
} else {
|
||||
color_or_inherit(&ml.block_content_color, entity_color)
|
||||
};
|
||||
let mut synth_ins =
|
||||
acadrust::entities::Insert::new(block_name, ml.context.block_content_location);
|
||||
let mut insertion = 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_y_scale(ml.block_scale.y);
|
||||
synth_ins.set_z_scale(ml.block_scale.z);
|
||||
synth_ins.rotation = ml.block_rotation;
|
||||
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 graph = crate::scene::render_graph::RenderSceneGraph::new(
|
||||
document,
|
||||
@@ -1956,12 +2184,7 @@ impl MultiLeaderTess for MultiLeader {
|
||||
let local_ins_x = (ins.x) as f32;
|
||||
let z = (ins.z) as f32;
|
||||
|
||||
// Rotation: the annotation context is AutoCAD's BAKED display
|
||||
// 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.
|
||||
// The context stores the resolved display rotation.
|
||||
let td = ctx.text_direction;
|
||||
let mut rot = if td.x.abs() > 1e-9 || td.y.abs() > 1e-9 {
|
||||
(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
|
||||
// instead of guessing from the leader side.
|
||||
use acadrust::entities::multileader::{
|
||||
TextAttachmentDirectionType, TextAttachmentPointType,
|
||||
TextAttachmentDirectionType,
|
||||
};
|
||||
// The context's attachment point exists in every DWG version;
|
||||
// the entity-level copy only exists from R2010 on.
|
||||
let h_anchor: f32 = match ctx.text_attachment_point {
|
||||
TextAttachmentPointType::Left => 0.0,
|
||||
TextAttachmentPointType::Center => 0.5,
|
||||
TextAttachmentPointType::Right => 1.0,
|
||||
let h_anchor: f32 = match ctx.text_alignment {
|
||||
TextAlignmentType::Left => 0.0,
|
||||
TextAlignmentType::Center => 0.5,
|
||||
TextAlignmentType::Right => 1.0,
|
||||
};
|
||||
// Pick the vertical-anchor attachment based on text_attachment_direction:
|
||||
// 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.
|
||||
if ml.text_frame || ctx.background_fill_enabled {
|
||||
// Frame/fill offset from the glyphs: the context's landing gap
|
||||
// (AutoCAD spaces the box by exactly that), with the old
|
||||
// quarter-height heuristic as the no-gap fallback.
|
||||
let pad = if ctx.landing_gap > 0.0 {
|
||||
ctx.landing_gap as f32
|
||||
} else {
|
||||
height * 0.25
|
||||
};
|
||||
// Border offset is a scale factor around the laid-out text box.
|
||||
let pad = height
|
||||
* ((ctx.background_scale_factor.max(1.0) as f32 - 1.0) * 0.5);
|
||||
// Box the glyphs that were actually laid out (valid for
|
||||
// vertical flow too); the metric-derived box is only the
|
||||
// no-glyph fallback.
|
||||
@@ -2311,11 +2529,16 @@ impl MultiLeaderTess for MultiLeader {
|
||||
|
||||
// Background fill — emit two triangles; renders under the text strokes.
|
||||
if ctx.background_fill_enabled {
|
||||
let fill_color = if selected {
|
||||
let mut fill_color = if selected {
|
||||
line_color
|
||||
} else if ctx.background_mask_fill_on {
|
||||
bg_color
|
||||
} else {
|
||||
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![
|
||||
wcs_corners[0],
|
||||
wcs_corners[1],
|
||||
|
||||
@@ -325,6 +325,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Arrow size (DIMASZ)" => Some(("styles", "arrow-size-dimasz")),
|
||||
"Arrowhead" => Some(("common", "arrowhead")),
|
||||
"Arrowhead (DIMBLK)" => Some(("styles", "arrowhead-dimblk")),
|
||||
"Arrowhead Size" => Some(("properties", "arrowhead-size")),
|
||||
"Arrowhead block:" => Some(("styles", "arrowhead-block")),
|
||||
"Arrowhead size" => Some(("common", "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")),
|
||||
"As displayed" => Some(("common", "as-displayed")),
|
||||
"Associative" => Some(("properties", "associative")),
|
||||
"Attachment" => Some(("properties", "attachment")),
|
||||
"At first ext" => Some(("styles", "at-first-ext")),
|
||||
"At least" => Some(("properties", "at-least")),
|
||||
"At second ext" => Some(("styles", "at-second-ext")),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// 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::EntityType;
|
||||
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::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
@@ -26,6 +26,47 @@ pub struct MLeaderCommand {
|
||||
plane: WorkingPlane,
|
||||
style: Option<acadrust::objects::MultiLeaderStyle>,
|
||||
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 {
|
||||
@@ -35,6 +76,23 @@ impl MLeaderCommand {
|
||||
plane: WorkingPlane::default(),
|
||||
style: None,
|
||||
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 {
|
||||
verts: Vec::new(),
|
||||
plane: WorkingPlane::default(),
|
||||
style: Some(style),
|
||||
style: Some(style.clone()),
|
||||
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 {
|
||||
@@ -66,60 +328,86 @@ impl CadCommand for MLeaderCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.verts.is_empty() {
|
||||
t!("MLEADER Specify arrowhead point:").into_owned()
|
||||
} else {
|
||||
t!("MLEADER Specify landing point:").into_owned()
|
||||
match self.step {
|
||||
Step::Options => t!("MLEADER Options [Leader type/Landing/Content type/Max points/First angle/Second angle/Layer/Exit]:").into_owned(),
|
||||
Step::LeaderType => t!("MLEADER Leader type [Straight/Spline/None]:").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 {
|
||||
self.verts.push(pt);
|
||||
|
||||
if self.verts.len() < 2 {
|
||||
if self.step != Step::PickPoints {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.verts.push(self.constrained_point(pt));
|
||||
|
||||
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)),
|
||||
)
|
||||
if self.verts.len() < self.point_limit() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.finish()
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.verts.len() < 2 {
|
||||
if self.step != Step::PickPoints || self.verts.len() < 2 {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
// Place the leader with empty text, then open the in-place MText editor
|
||||
// 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)),
|
||||
)
|
||||
self.finish()
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
@@ -127,16 +415,37 @@ impl CadCommand for MLeaderCommand {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// 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
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point).as_vec3())
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.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
|
||||
.style
|
||||
.as_ref()
|
||||
@@ -159,6 +468,178 @@ impl CadCommand for MLeaderCommand {
|
||||
.collect();
|
||||
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 ─────────────────────────────────────────────────────────────────
|
||||
@@ -170,18 +651,24 @@ fn v3(p: DVec3) -> Vector3 {
|
||||
fn build_mleader(
|
||||
text: &str,
|
||||
verts: &[DVec3],
|
||||
content_point: Option<DVec3>,
|
||||
ucs: Mat4,
|
||||
style: Option<&acadrust::objects::MultiLeaderStyle>,
|
||||
display_scale: f64,
|
||||
) -> 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 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 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 {
|
||||
crate::scene::annotative::apply_mleader_style(&mut ml, style);
|
||||
} else {
|
||||
@@ -225,11 +712,13 @@ fn build_mleader(
|
||||
// Place text beyond the landing and gap.
|
||||
let off = landing * (landing_distance + landing_gap) as f32;
|
||||
|
||||
let text_location = Vector3::new(
|
||||
elbow_v3.x + off.x as f64,
|
||||
elbow_v3.y + off.y as f64,
|
||||
elbow_v3.z,
|
||||
);
|
||||
let text_location = content_point.map(v3).unwrap_or_else(|| {
|
||||
Vector3::new(
|
||||
elbow_v3.x + off.x as f64,
|
||||
elbow_v3.y + off.y as f64,
|
||||
elbow_v3.z,
|
||||
)
|
||||
});
|
||||
|
||||
ml.context.text_location = text_location;
|
||||
ml.context.content_base_point = text_location;
|
||||
@@ -237,6 +726,34 @@ fn build_mleader(
|
||||
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 {
|
||||
let mut points: Vec<[f32; 3]> = pts.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||
if pts.len() >= 2 {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
use acadrust::entities::{LeaderLine, MultiLeader};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use cadkernel::geom2d::{closest_point, Curve, Line};
|
||||
use glam::{DVec3, Vec3};
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
@@ -92,6 +93,9 @@ impl CadCommand for MLeaderAddCommand {
|
||||
match &mut self.step {
|
||||
AddStep::PickArrowhead { handle, entity } => {
|
||||
if let Some(ent) = entity.take() {
|
||||
if !matches!(ent, EntityType::MultiLeader(_)) {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
let h = *handle;
|
||||
self.step = AddStep::CollectPoints {
|
||||
handle: h,
|
||||
@@ -121,14 +125,46 @@ impl CadCommand for MLeaderAddCommand {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
let h = *handle;
|
||||
if let EntityType::MultiLeader(ref mut ml) = entity {
|
||||
// Add a new leader root with the collected points
|
||||
if let EntityType::MultiLeader(ref mut ml) = entity {
|
||||
let points: Vec<Vector3> = pts
|
||||
.iter()
|
||||
.map(|p| Vector3::new(p.x, p.y, p.z))
|
||||
.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();
|
||||
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()));
|
||||
return CmdResult::ReplaceEntity(h, vec![updated]);
|
||||
@@ -223,26 +259,43 @@ impl CadCommand for MLeaderRemoveCommand {
|
||||
if let Some(mut ent) = entity.take() {
|
||||
let h = *handle;
|
||||
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 best = ml
|
||||
.context
|
||||
.leader_roots
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, root)| {
|
||||
root.lines.first().and_then(|l| l.points.first()).map(|p| {
|
||||
let dx = p.x - pick.x;
|
||||
let dz = p.z - pick.z;
|
||||
(i, dx * dx + dz * dz)
|
||||
.flat_map(|(root_index, root)| {
|
||||
root.lines.iter().enumerate().map(move |(line_index, line)| {
|
||||
let mut distance = line
|
||||
.points
|
||||
.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));
|
||||
if let Some((idx, _)) = best {
|
||||
if ml.context.leader_roots.len() > 1 {
|
||||
ml.context.leader_roots.remove(idx);
|
||||
}
|
||||
.min_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let Some((root_index, line_index, _)) = best else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
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]);
|
||||
}
|
||||
@@ -338,25 +391,11 @@ impl CadCommand for MLeaderAlignCommand {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
AlignStep::PickEndDir { handles, from } => {
|
||||
let h = handles.clone();
|
||||
let f = *from;
|
||||
// Emit sentinel for commands.rs
|
||||
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!(
|
||||
"__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::AlignMLeaders {
|
||||
handles: handles.clone(),
|
||||
from: *from,
|
||||
to: pt,
|
||||
}
|
||||
CmdResult::Cancel
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
@@ -431,21 +470,10 @@ impl CadCommand for MLeaderCollectCommand {
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if let CollectStep::PickLocation { handles } = &self.step {
|
||||
let h = handles.clone();
|
||||
// Build a new combined multileader at the collection point, delete originals.
|
||||
// The first handle is the "base" — the rest are erased and their leaders merged.
|
||||
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)]);
|
||||
}
|
||||
return CmdResult::CollectMLeaders {
|
||||
handles: handles.clone(),
|
||||
point: pt,
|
||||
};
|
||||
}
|
||||
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
|
||||
fn _uses_ml_types(_ml: &MultiLeader, _ll: &LeaderLine) {}
|
||||
|
||||
|
||||
+164
-59
@@ -1502,66 +1502,153 @@ pub fn apply_mleader_style(
|
||||
entity: &mut acadrust::entities::MultiLeader,
|
||||
style: &acadrust::objects::MultiLeaderStyle,
|
||||
) {
|
||||
use acadrust::entities::MultiLeaderPropertyOverrideFlags as F;
|
||||
|
||||
let flags = entity.property_override_flags;
|
||||
entity.style_handle = Some(style.handle);
|
||||
entity.content_type = (style.content_type as i16).into();
|
||||
entity.path_type = (style.path_type as i16).into();
|
||||
entity.line_color = style.line_color;
|
||||
entity.line_type_handle = style.line_type_handle;
|
||||
entity.line_weight = style.line_weight;
|
||||
entity.enable_landing = style.enable_landing;
|
||||
entity.enable_dogleg = style.enable_dogleg;
|
||||
entity.dogleg_length = style.landing_distance;
|
||||
entity.arrowhead_handle = style.arrowhead_handle;
|
||||
entity.arrowhead_size = style.arrowhead_size;
|
||||
entity.context.arrowhead_size = style.arrowhead_size;
|
||||
entity.context.landing_gap = style.landing_gap;
|
||||
entity.text_style_handle = style.text_style_handle;
|
||||
entity.text_color = style.text_color;
|
||||
entity.text_frame = style.text_frame;
|
||||
entity.text_height = style.text_height;
|
||||
entity.context.text_height = style.text_height;
|
||||
entity.context.text_style_handle = style.text_style_handle;
|
||||
entity.context.text_color = style.text_color;
|
||||
entity.text_left_attachment = (style.text_left_attachment as i16).into();
|
||||
entity.text_right_attachment = (style.text_right_attachment as i16).into();
|
||||
entity.text_top_attachment = (style.text_top_attachment as i16).into();
|
||||
entity.text_bottom_attachment = (style.text_bottom_attachment as i16).into();
|
||||
entity.text_attachment_direction = (style.text_attachment_direction as i16).into();
|
||||
entity.text_alignment = (style.text_alignment as i16).into();
|
||||
entity.text_angle_type = (style.text_angle_type as i16).into();
|
||||
if !flags.contains(F::CONTENT_TYPE) {
|
||||
entity.content_type = (style.content_type as i16).into();
|
||||
}
|
||||
if !flags.contains(F::PATH_TYPE) {
|
||||
entity.path_type = (style.path_type as i16).into();
|
||||
}
|
||||
if !flags.contains(F::LINE_COLOR) {
|
||||
entity.line_color = style.line_color;
|
||||
}
|
||||
if !flags.contains(F::LEADER_LINE_TYPE) {
|
||||
entity.line_type_handle = style.line_type_handle;
|
||||
}
|
||||
if !flags.contains(F::LEADER_LINE_WEIGHT) {
|
||||
entity.line_weight = style.line_weight;
|
||||
}
|
||||
if !flags.contains(F::ENABLE_LANDING) {
|
||||
entity.enable_landing = style.enable_landing;
|
||||
}
|
||||
if !flags.contains(F::ENABLE_DOGLEG) {
|
||||
entity.enable_dogleg = style.enable_dogleg;
|
||||
}
|
||||
if !flags.contains(F::LANDING_DISTANCE) {
|
||||
entity.dogleg_length = style.landing_distance;
|
||||
}
|
||||
if !flags.contains(F::LANDING_GAP) {
|
||||
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_right_attachment = entity.text_right_attachment;
|
||||
entity.context.text_top_attachment = entity.text_top_attachment;
|
||||
entity.context.text_bottom_attachment = entity.text_bottom_attachment;
|
||||
entity.context.text_alignment = entity.text_alignment;
|
||||
entity.block_content_handle = style.block_content_handle;
|
||||
entity.block_content_color = style.block_content_color;
|
||||
entity.block_connection_type = (style.block_content_connection as i16).into();
|
||||
entity.block_rotation = style.block_content_rotation;
|
||||
entity.block_scale = Vector3::new(
|
||||
style.block_content_scale_x,
|
||||
style.block_content_scale_y,
|
||||
style.block_content_scale_z,
|
||||
);
|
||||
entity.scale_factor = style.scale_factor;
|
||||
entity.context.block_content_handle = style.block_content_handle;
|
||||
entity.context.block_content_color = style.block_content_color;
|
||||
entity.context.text_style_handle = entity.text_style_handle;
|
||||
entity.context.text_color = entity.text_color;
|
||||
if !flags.contains(F::BLOCK_CONTENT) {
|
||||
entity.block_content_handle = style.block_content_handle;
|
||||
}
|
||||
if !flags.contains(F::BLOCK_CONTENT_COLOR) {
|
||||
entity.block_content_color = style.block_content_color;
|
||||
}
|
||||
if !flags.contains(F::BLOCK_CONTENT_CONNECTION) {
|
||||
entity.block_connection_type = (style.block_content_connection as i16).into();
|
||||
}
|
||||
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_rotation = style.block_content_rotation;
|
||||
entity.context.block_rotation = entity.block_rotation;
|
||||
entity.context.block_content_scale = entity.block_scale;
|
||||
entity.context.scale_factor = style.scale_factor;
|
||||
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 {
|
||||
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;
|
||||
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.line_color = style.line_color;
|
||||
line.line_type_handle = style.line_type_handle;
|
||||
line.line_weight = style.line_weight;
|
||||
line.arrowhead_handle = style.arrowhead_handle;
|
||||
line.arrowhead_size = style.arrowhead_size;
|
||||
}
|
||||
if !line.override_flags.contains(F::LINE_COLOR) {
|
||||
line.line_color = entity.line_color;
|
||||
}
|
||||
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,
|
||||
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);
|
||||
|
||||
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.
|
||||
entity.context.scale_factor = scale;
|
||||
|
||||
entity.context.text_height =
|
||||
style.text_height * scale;
|
||||
entity.context.text_height = entity.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 =
|
||||
style.arrowhead_size * scale;
|
||||
|
||||
entity.context.landing_gap =
|
||||
style.landing_gap * scale;
|
||||
|
||||
for root in &mut entity.context.leader_roots {
|
||||
root.landing_distance =
|
||||
style.landing_distance * scale;
|
||||
for (index, root) in entity.context.leader_roots.iter_mut().enumerate() {
|
||||
root.landing_distance = if entity
|
||||
.property_override_flags
|
||||
.contains(F::LANDING_DISTANCE)
|
||||
{
|
||||
landing_distances
|
||||
.get(index)
|
||||
.copied()
|
||||
.unwrap_or(entity.dogleg_length)
|
||||
* scale
|
||||
} else {
|
||||
entity.dogleg_length * scale
|
||||
};
|
||||
|
||||
root.text_attachment_direction =
|
||||
entity.text_attachment_direction;
|
||||
@@ -1854,4 +1960,3 @@ pub fn is_annotative(doc: &CadDocument, entity: &EntityType) -> bool {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,13 @@ pub enum PropValue {
|
||||
ColorVaries,
|
||||
/// Line weight — rendered as a combo_box.
|
||||
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.
|
||||
LwVaries,
|
||||
/// Linetype name — rendered as a combo_box.
|
||||
|
||||
+11
-4
@@ -539,7 +539,8 @@ impl Scene {
|
||||
"ByLayer".into(),
|
||||
"ByBlock".into(),
|
||||
]),
|
||||
PropValue::LwChoice(_) => QSelectValueEditor::Choice(vec![
|
||||
PropValue::LwChoice(_)
|
||||
| PropValue::FieldLwChoice { .. } => QSelectValueEditor::Choice(vec![
|
||||
"ByLayer".into(),
|
||||
"ByBlock".into(),
|
||||
"Default".into(),
|
||||
@@ -567,7 +568,8 @@ impl Scene {
|
||||
PropValue::AttrText { .. } => QSelectValueEditor::Text,
|
||||
PropValue::Stepper { .. }
|
||||
| PropValue::ColorVaries
|
||||
| PropValue::LwVaries => continue,
|
||||
| PropValue::LwVaries
|
||||
| PropValue::FieldLwVaries { .. } => continue,
|
||||
}
|
||||
};
|
||||
out.push(choice(prop.field, prop.label, editor));
|
||||
@@ -696,13 +698,18 @@ impl Scene {
|
||||
PropValue::Choice { selected, .. } => selected,
|
||||
PropValue::EditChoice { value, .. } => value,
|
||||
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::HatchPatternChoice(s) => s,
|
||||
PropValue::BoolToggle { value, .. } => value.to_string(),
|
||||
PropValue::AttrText { value, .. } => value,
|
||||
PropValue::Stepper { display, .. } => display,
|
||||
PropValue::ColorVaries | PropValue::LwVaries => return None,
|
||||
PropValue::ColorVaries
|
||||
| PropValue::LwVaries
|
||||
| PropValue::FieldLwVaries { .. } => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+71
-1
@@ -763,6 +763,12 @@ impl PropertiesPanel {
|
||||
PropValue::ColorVaries => self.render_color_varies_row(label),
|
||||
PropValue::LayerChoice(layer) => self.render_layer_row(label, layer),
|
||||
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::LinetypeChoice(lt) => self.render_linetype_row(label, lt),
|
||||
PropValue::Choice { selected, options } => {
|
||||
@@ -861,6 +867,10 @@ impl PropertiesPanel {
|
||||
| "dim_ext_line_color"
|
||||
| "dim_text_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 fsel = field.to_string();
|
||||
@@ -871,7 +881,9 @@ impl PropertiesPanel {
|
||||
background: true,
|
||||
..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 {
|
||||
by_layer: true,
|
||||
by_block: true,
|
||||
@@ -998,6 +1010,36 @@ impl PropertiesPanel {
|
||||
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> {
|
||||
let combo = combo_box(
|
||||
&self.lineweight_combo,
|
||||
@@ -1019,6 +1061,34 @@ impl PropertiesPanel {
|
||||
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) ──────────────────────────────────────────
|
||||
|
||||
fn render_linetype_row<'a>(&'a self, label: &'a str, current: &'a str) -> Element<'a, Message> {
|
||||
|
||||
Reference in new issue
Block a user