fix(mleader): improve grips, styles and annotative behavior
This commit is contained in:
parent
43c150199e
commit
d693bcf968
8 changed files with 1071 additions and 185 deletions
|
|
@ -1195,23 +1195,66 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
CmdResult::CommitAndEditText(entity) => {
|
||||
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
||||
let delta_safe = self.delta_add_safe(i, &entity);
|
||||
let pending = self.begin_undo(i, label, 1, delta_safe);
|
||||
let handle = self.commit_entity_handle(entity);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.ribbon.deactivate_tool();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
if let Some(h) = handle {
|
||||
return self.begin_text_edit(h);
|
||||
// Una MLEADER cuyo estilo es anotativo necesita además del flag
|
||||
// enable_annotation_scale un contexto real asociado a la escala
|
||||
// anotativa actual.
|
||||
let annotative_mleader = matches!(
|
||||
&entity,
|
||||
acadrust::EntityType::MultiLeader(ml)
|
||||
if ml.enable_annotation_scale
|
||||
);
|
||||
|
||||
let label =
|
||||
self.history_label_from_active_cmd(i, "ENTITY");
|
||||
|
||||
// Crear un contexto anotativo agrega objetos/diccionarios al DWG,
|
||||
// así que no puede tratarse como un simple delta de entidad.
|
||||
let delta_safe =
|
||||
self.delta_add_safe(i, &entity)
|
||||
&& !annotative_mleader;
|
||||
|
||||
let pending =
|
||||
self.begin_undo(i, label, 1, delta_safe);
|
||||
|
||||
let handle =
|
||||
self.commit_entity_handle(entity);
|
||||
|
||||
// Registrar la representación de la escala actual.
|
||||
if annotative_mleader {
|
||||
if let (Some(handle), Some(scale_handle)) = (
|
||||
handle,
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.current_annotation_scale_handle(),
|
||||
) {
|
||||
crate::scene::annotative::create_annotation_context(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
scale_handle,
|
||||
);
|
||||
|
||||
self.tabs[i].scene.bump_entities(&[(
|
||||
handle,
|
||||
crate::scene::ChangeKind::Modified,
|
||||
)]);
|
||||
}
|
||||
}
|
||||
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.ribbon.deactivate_tool();
|
||||
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
|
||||
if let Some(h) = handle {
|
||||
return self.begin_text_edit(h);
|
||||
}
|
||||
}
|
||||
CmdResult::CommitManyAndEditText {
|
||||
entities,
|
||||
edit_index,
|
||||
|
|
|
|||
|
|
@ -143,15 +143,22 @@ impl OpenCADStudio {
|
|||
}
|
||||
})
|
||||
.collect();
|
||||
let active_mleader = self.tabs[i].active_mleader_style.clone();
|
||||
let active_mleader = if mleader_names.contains(&active_mleader) {
|
||||
active_mleader
|
||||
} else {
|
||||
let current_mleader =
|
||||
doc.header.current_mleader_style_name.clone();
|
||||
|
||||
let active_mleader =
|
||||
mleader_names
|
||||
.first()
|
||||
.iter()
|
||||
.find(|name| {
|
||||
name.eq_ignore_ascii_case(
|
||||
¤t_mleader
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
.or_else(|| {
|
||||
mleader_names.first().cloned()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let table_names: Vec<String> = doc
|
||||
.objects
|
||||
|
|
|
|||
|
|
@ -1248,6 +1248,17 @@ impl super::OpenCADStudio {
|
|||
}
|
||||
_ => {}
|
||||
}
|
||||
// En una MLEADER anotativa, el texto forma parte del
|
||||
// propio contexto por escala. Copiar la edición también
|
||||
// a la representación que está actualmente en pantalla.
|
||||
if matches!(
|
||||
self.tabs[i].scene.document.get_entity(h),
|
||||
Some(EntityType::MultiLeader(_))
|
||||
) {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.sync_displayed_annotation_context(h);
|
||||
}
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
|
||||
|
|
@ -1336,6 +1347,14 @@ impl super::OpenCADStudio {
|
|||
}
|
||||
_ => {}
|
||||
}
|
||||
if matches!(
|
||||
self.tabs[i].scene.document.get_entity(h),
|
||||
Some(EntityType::MultiLeader(_))
|
||||
) {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.sync_displayed_annotation_context(h);
|
||||
}
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
|
||||
|
|
|
|||
|
|
@ -18,16 +18,135 @@
|
|||
//! style added without a handle (dropped on DWG save, issue #67).
|
||||
|
||||
use super::OpenCADStudio;
|
||||
use acadrust::objects::{MLineStyle, MultiLeaderStyle, ObjectType, TableStyle};
|
||||
use acadrust::objects::{
|
||||
Dictionary, MLineStyle, MultiLeaderStyle, ObjectType, TableStyle,
|
||||
};
|
||||
use acadrust::tables::{DimStyle, TextStyle};
|
||||
use acadrust::types::Handle;
|
||||
|
||||
const MLEADERSTYLE_DICT_NAME: &str = "ACAD_MLEADERSTYLE";
|
||||
|
||||
fn mleaderstyle_dict_handle(doc: &acadrust::CadDocument) -> Option<Handle> {
|
||||
let root_h = doc.header.named_objects_dict_handle;
|
||||
|
||||
let root = match doc.objects.get(&root_h) {
|
||||
Some(ObjectType::Dictionary(root)) => root,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
root.entries
|
||||
.iter()
|
||||
.find(|(name, _)| name.eq_ignore_ascii_case(MLEADERSTYLE_DICT_NAME))
|
||||
.map(|(_, handle)| *handle)
|
||||
.filter(|handle| {
|
||||
matches!(
|
||||
doc.objects.get(handle),
|
||||
Some(ObjectType::Dictionary(_))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn import_mleaderstyle_names_from_dictionary(doc: &mut acadrust::CadDocument) {
|
||||
let Some(dict_h) = mleaderstyle_dict_handle(doc) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let entries = match doc.objects.get(&dict_h) {
|
||||
Some(ObjectType::Dictionary(dict)) => dict.entries.clone(),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
for (name, handle) in entries {
|
||||
if let Some(ObjectType::MultiLeaderStyle(style)) =
|
||||
doc.objects.get_mut(&handle)
|
||||
{
|
||||
style.name = name;
|
||||
style.owner_handle = dict_h;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_mleaderstyle_dictionary(doc: &mut acadrust::CadDocument) {
|
||||
let root_h = crate::scene::annotative::root_named_dict_handle(doc);
|
||||
|
||||
let existing = match doc.objects.get(&root_h) {
|
||||
Some(ObjectType::Dictionary(root)) => root
|
||||
.entries
|
||||
.iter()
|
||||
.find(|(name, _)| name.eq_ignore_ascii_case(MLEADERSTYLE_DICT_NAME))
|
||||
.map(|(_, handle)| *handle),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let dict_h = match existing.filter(|handle| {
|
||||
matches!(
|
||||
doc.objects.get(handle),
|
||||
Some(ObjectType::Dictionary(_))
|
||||
)
|
||||
}) {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
let handle = doc.allocate_handle();
|
||||
|
||||
let mut dict = Dictionary::new();
|
||||
dict.handle = handle;
|
||||
dict.owner = root_h;
|
||||
|
||||
doc.objects
|
||||
.insert(handle, ObjectType::Dictionary(dict));
|
||||
|
||||
handle
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ObjectType::Dictionary(root)) = doc.objects.get_mut(&root_h) {
|
||||
root.entries
|
||||
.retain(|(name, _)| !name.eq_ignore_ascii_case(MLEADERSTYLE_DICT_NAME));
|
||||
|
||||
root.add_entry(MLEADERSTYLE_DICT_NAME, dict_h);
|
||||
}
|
||||
|
||||
let mut entries: Vec<(String, Handle)> = doc
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|(&handle, object)| match object {
|
||||
ObjectType::MultiLeaderStyle(style) => {
|
||||
Some((style.name.clone(), handle))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
entries.sort_by(|a, b| {
|
||||
a.0.to_lowercase().cmp(&b.0.to_lowercase())
|
||||
});
|
||||
|
||||
for (_, handle) in &entries {
|
||||
if let Some(ObjectType::MultiLeaderStyle(style)) =
|
||||
doc.objects.get_mut(handle)
|
||||
{
|
||||
style.owner_handle = dict_h;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ObjectType::Dictionary(dict)) = doc.objects.get_mut(&dict_h) {
|
||||
dict.owner = root_h;
|
||||
dict.entries = entries;
|
||||
|
||||
// AutoCAD stores MLEADERSTYLE entries as soft-owner references (350),
|
||||
// not hard-owner references (360).
|
||||
dict.hard_owner = false;
|
||||
dict.hard_owner_entries.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Guarantee the built-in "Standard" style of every kind exists in `doc` —
|
||||
/// a foreign or damaged file saved without them leaves the style dropdowns
|
||||
/// broken with no way to recover, and new text/dimensions have nothing to
|
||||
/// reference (#366). Missing entries are re-seeded with the app defaults.
|
||||
/// Called on every file open; a no-op for healthy documents.
|
||||
pub(crate) fn ensure_standard_styles(doc: &mut acadrust::CadDocument) {
|
||||
import_mleaderstyle_names_from_dictionary(doc);
|
||||
if !doc
|
||||
.text_styles
|
||||
.iter()
|
||||
|
|
@ -68,6 +187,8 @@ pub(crate) fn ensure_standard_styles(doc: &mut acadrust::CadDocument) {
|
|||
s.handle = doc.allocate_handle();
|
||||
doc.objects.insert(s.handle, ObjectType::MultiLeaderStyle(s));
|
||||
}
|
||||
sync_mleaderstyle_dictionary(doc);
|
||||
|
||||
if !has(doc, |o| match o {
|
||||
ObjectType::MLineStyle(s) => Some(&s.name),
|
||||
_ => None,
|
||||
|
|
@ -741,6 +862,7 @@ impl OpenCADStudio {
|
|||
for (h, o) in &snap.style_objects {
|
||||
doc.objects.insert(*h, o.clone());
|
||||
}
|
||||
sync_mleaderstyle_dictionary(doc);
|
||||
doc.header.current_text_style_name = snap.current_text.clone();
|
||||
doc.header.current_dimstyle_name = snap.current_dim.clone();
|
||||
doc.header.multiline_style = snap.multiline_style.clone();
|
||||
|
|
@ -772,11 +894,77 @@ impl OpenCADStudio {
|
|||
self.sync_ribbon_styles();
|
||||
return;
|
||||
};
|
||||
sync_mleaderstyle_dictionary(
|
||||
&mut self.tabs[i].scene.document,
|
||||
);
|
||||
|
||||
let edited = self.capture_style_state();
|
||||
let changed = edited != stage.baseline;
|
||||
if changed {
|
||||
self.tabs[i].dirty = true;
|
||||
let (text_names, dim_names, object_handles) = edited.changed_keys(&stage.baseline);
|
||||
let changed_mleader_styles:
|
||||
Vec<acadrust::objects::MultiLeaderStyle> =
|
||||
object_handles
|
||||
.iter()
|
||||
.filter_map(|handle| {
|
||||
match self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.get(handle)
|
||||
{
|
||||
Some(
|
||||
acadrust::objects::ObjectType::
|
||||
MultiLeaderStyle(style),
|
||||
) => Some(style.clone()),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut changed_mleaders = Vec::new();
|
||||
|
||||
for style in changed_mleader_styles {
|
||||
let entity_handles: Vec<acadrust::Handle> = {
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
|
||||
doc.entities()
|
||||
.filter_map(|entity| {
|
||||
match entity {
|
||||
acadrust::EntityType::MultiLeader(ml)
|
||||
if ml.style_handle
|
||||
== Some(style.handle) =>
|
||||
{
|
||||
Some(ml.common.handle)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
for handle in entity_handles {
|
||||
if crate::scene::annotative::
|
||||
apply_mleader_style_to_object(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
&style,
|
||||
)
|
||||
{
|
||||
changed_mleaders.push((
|
||||
handle,
|
||||
crate::scene::ChangeKind::Modified,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !changed_mleaders.is_empty() {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&changed_mleaders);
|
||||
}
|
||||
self.tabs[i].scene.invalidate_text_style_dependencies_many(&text_names);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
|
|
|
|||
|
|
@ -1672,9 +1672,20 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
}
|
||||
}
|
||||
StyleKey::MLeaderStyle => {
|
||||
self.ribbon.active_mleader_style = name.clone();
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].active_mleader_style = name;
|
||||
|
||||
self.ribbon.active_mleader_style = name.clone();
|
||||
self.tabs[i].active_mleader_style = name.clone();
|
||||
|
||||
// Esta es la fuente que consulta el comando MLEADER
|
||||
// al crear una entidad nueva.
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.current_mleader_style_name = name;
|
||||
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
StyleKey::TableStyle => {
|
||||
self.ribbon.active_table_style = name;
|
||||
|
|
|
|||
|
|
@ -182,8 +182,18 @@ fn to_render(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<Rende
|
|||
style: &resolved,
|
||||
// Side-anchored on the leader-facing edge so the text reads
|
||||
// outward; flips live with the leader/text side.
|
||||
attach_h_anchor: if text_sign >= 0.0 { 0.0 } else { 1.0 },
|
||||
v_anchor: mleader_v_anchor(ctx.text_left_attachment),
|
||||
attach_h_anchor: match ctx.text_attachment_point {
|
||||
acadrust::entities::multileader::TextAttachmentPointType::Left => 0.0,
|
||||
acadrust::entities::multileader::TextAttachmentPointType::Center => 0.5,
|
||||
acadrust::entities::multileader::TextAttachmentPointType::Right => 1.0,
|
||||
},
|
||||
v_anchor: mleader_v_anchor(
|
||||
if text_sign >= 0.0 {
|
||||
ctx.text_left_attachment
|
||||
} else {
|
||||
ctx.text_right_attachment
|
||||
},
|
||||
),
|
||||
line_spacing_factor: ctx.line_spacing_factor as f32,
|
||||
vertical_text: false,
|
||||
want_glyph_boxes: false,
|
||||
|
|
@ -200,11 +210,21 @@ fn to_render(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<Rende
|
|||
} else {
|
||||
0.0
|
||||
};
|
||||
// text_location is the leader-facing (near) edge; the landing runs one
|
||||
// dogleg from there back toward the leader.
|
||||
let landing_pt = [
|
||||
text_loc.x - text_sign * dogleg * tdx,
|
||||
text_loc.y - text_sign * dogleg * tdy,
|
||||
|
||||
let landing_gap = ml.context.landing_gap.max(0.0);
|
||||
|
||||
// Codo visual: desde aquí arranca la horizontal.
|
||||
let elbow_pt = [
|
||||
text_loc.x - text_sign * (dogleg + landing_gap) * tdx,
|
||||
text_loc.y - text_sign * (dogleg + landing_gap) * tdy,
|
||||
text_loc.z,
|
||||
];
|
||||
|
||||
// Extremo de la línea horizontal, justo antes del texto.
|
||||
// Esto evita que la justificación quede solapada con la línea.
|
||||
let text_edge_pt = [
|
||||
text_loc.x - text_sign * landing_gap * tdx,
|
||||
text_loc.y - text_sign * landing_gap * tdy,
|
||||
text_loc.z,
|
||||
];
|
||||
|
||||
|
|
@ -225,13 +245,17 @@ fn to_render(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<Rende
|
|||
first = false;
|
||||
|
||||
// Build the full control-point list: line.points + landing point
|
||||
let mut ctrl: Vec<[f64; 3]> = line.points.iter().map(|p| p3(p)).collect();
|
||||
let last_f = *ctrl.last().unwrap_or(&landing_pt);
|
||||
let dist = ((last_f[0] - landing_pt[0]).powi(2)
|
||||
+ (last_f[1] - landing_pt[1]).powi(2))
|
||||
let mut ctrl: Vec<[f64; 3]> =
|
||||
line.points.iter().map(|p| p3(p)).collect();
|
||||
|
||||
let last_f = *ctrl.last().unwrap_or(&elbow_pt);
|
||||
|
||||
let dist = ((last_f[0] - elbow_pt[0]).powi(2)
|
||||
+ (last_f[1] - elbow_pt[1]).powi(2))
|
||||
.sqrt();
|
||||
|
||||
if dist > 1e-9 {
|
||||
ctrl.push(landing_pt);
|
||||
ctrl.push(elbow_pt);
|
||||
}
|
||||
for &c in &ctrl {
|
||||
key_verts.push(c);
|
||||
|
|
@ -295,8 +319,8 @@ fn to_render(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<Rende
|
|||
// Horizontal landing from the leader end to the text's near edge.
|
||||
if dogleg > 0.0 {
|
||||
points.push(nan);
|
||||
points.push(landing_pt);
|
||||
points.push([text_loc.x, text_loc.y, text_loc.z]);
|
||||
points.push(elbow_pt);
|
||||
points.push(text_edge_pt);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -428,27 +452,134 @@ fn text_box_geom(ml: &MultiLeader) -> ([f64; 2], [f64; 3]) {
|
|||
],
|
||||
)
|
||||
}
|
||||
fn mleader_landing_geom(
|
||||
ml: &MultiLeader,
|
||||
) -> Option<(DVec3, DVec3, f64, f64, f64)> {
|
||||
if ml.content_type != LeaderContentType::MText
|
||||
|| !ml.enable_landing
|
||||
|| !ml.enable_dogleg
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = DVec3::new(
|
||||
ml.context.text_location.x,
|
||||
ml.context.text_location.y,
|
||||
ml.context.text_location.z,
|
||||
);
|
||||
|
||||
let td = ml.context.text_direction;
|
||||
|
||||
let axis = {
|
||||
let d = DVec3::new(td.x, td.y, td.z);
|
||||
|
||||
if d.length_squared() > 1.0e-18 {
|
||||
d.normalize()
|
||||
} else {
|
||||
let a = ml.context.text_rotation;
|
||||
|
||||
DVec3::new(a.cos(), a.sin(), 0.0)
|
||||
}
|
||||
};
|
||||
|
||||
let leader_ref = ml
|
||||
.context
|
||||
.leader_roots
|
||||
.first()
|
||||
.and_then(|root| root.lines.first())
|
||||
.and_then(|line| line.points.last())
|
||||
.map(|p| DVec3::new(p.x, p.y, p.z))
|
||||
.or_else(|| {
|
||||
ml.context.leader_roots.first().map(|root| {
|
||||
DVec3::new(
|
||||
root.connection_point.x,
|
||||
root.connection_point.y,
|
||||
root.connection_point.z,
|
||||
)
|
||||
})
|
||||
})
|
||||
.unwrap_or(text);
|
||||
|
||||
let sign = if (text - leader_ref).dot(axis) >= 0.0 {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
};
|
||||
|
||||
let dogleg = ml
|
||||
.context
|
||||
.leader_roots
|
||||
.first()
|
||||
.map(|root| root.landing_distance.max(0.0))
|
||||
.unwrap_or_else(|| ml.dogleg_length.max(0.0));
|
||||
|
||||
let gap = ml.context.landing_gap.max(0.0);
|
||||
|
||||
if dogleg <= 1.0e-9 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// El codo visual queda ANTES del tramo horizontal y del gap al texto.
|
||||
let elbow =
|
||||
text - axis * (sign * (dogleg + gap));
|
||||
|
||||
Some((elbow, axis, sign, dogleg, gap))
|
||||
}
|
||||
fn grips(ml: &MultiLeader) -> Vec<GripDef> {
|
||||
let mut result: Vec<GripDef> = Vec::new();
|
||||
let mut result = Vec::new();
|
||||
let mut id = 0usize;
|
||||
|
||||
// Grips reales de las líneas:
|
||||
// flecha + cualquier vértice intermedio.
|
||||
for root in &ml.context.leader_roots {
|
||||
for line in &root.lines {
|
||||
for p in &line.points {
|
||||
result.push(square_grip(id, glam::DVec3::new(p.x, p.y, p.z)));
|
||||
result.push(square_grip(
|
||||
id,
|
||||
DVec3::new(p.x, p.y, p.z),
|
||||
));
|
||||
|
||||
id += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ml.content_type == LeaderContentType::MText {
|
||||
// Grip virtual del codo.
|
||||
if let Some((elbow, _, _, _, _)) = mleader_landing_geom(ml)
|
||||
{
|
||||
result.push(square_grip(
|
||||
id,
|
||||
elbow,
|
||||
));
|
||||
|
||||
id += 1;
|
||||
}
|
||||
|
||||
// Extremo del landing.
|
||||
//
|
||||
// Coincide geométricamente con el punto donde
|
||||
// comienza el contenido.
|
||||
let tl = &ml.context.text_location;
|
||||
// Text-location grip, then the wrap-width grip at the box's far edge.
|
||||
result.push(center_grip(id, glam::DVec3::new(tl.x, tl.y, tl.z)));
|
||||
|
||||
result.push(center_grip(
|
||||
id,
|
||||
DVec3::new(tl.x, tl.y, tl.z),
|
||||
));
|
||||
|
||||
id += 1;
|
||||
|
||||
// Grip triangular del ancho del MTEXT.
|
||||
let (_, far) = text_box_geom(ml);
|
||||
result.push(triangle_grip(id, glam::DVec3::new(far[0], far[1], far[2])));
|
||||
|
||||
result.push(triangle_grip(
|
||||
id,
|
||||
DVec3::new(
|
||||
far[0],
|
||||
far[1],
|
||||
far[2],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
result
|
||||
|
|
@ -458,16 +589,30 @@ fn grips(ml: &MultiLeader) -> Vec<GripDef> {
|
|||
/// text grip's "Move with Leader" action so the leader follows the text.
|
||||
pub(crate) const MOVE_ALL_GRIP: usize = usize::MAX;
|
||||
|
||||
fn apply_grip(ml: &mut MultiLeader, grip_id: usize, apply: GripApply) {
|
||||
fn apply_grip(
|
||||
ml: &mut MultiLeader,
|
||||
grip_id: usize,
|
||||
apply: GripApply,
|
||||
) {
|
||||
// Movimiento completo de la MLEADER.
|
||||
if grip_id == MOVE_ALL_GRIP {
|
||||
let (dx, dy, dz) = match apply {
|
||||
GripApply::Translate(d) => (d.x as f64, d.y as f64, d.z as f64),
|
||||
GripApply::Translate(d) => (
|
||||
d.x as f64,
|
||||
d.y as f64,
|
||||
d.z as f64,
|
||||
),
|
||||
|
||||
GripApply::Absolute(a) => (
|
||||
a.x as f64 - ml.context.text_location.x,
|
||||
a.y as f64 - ml.context.text_location.y,
|
||||
a.z as f64 - ml.context.text_location.z,
|
||||
a.x as f64
|
||||
- ml.context.text_location.x,
|
||||
a.y as f64
|
||||
- ml.context.text_location.y,
|
||||
a.z as f64
|
||||
- ml.context.text_location.z,
|
||||
),
|
||||
};
|
||||
|
||||
for root in &mut ml.context.leader_roots {
|
||||
for line in &mut root.lines {
|
||||
for p in &mut line.points {
|
||||
|
|
@ -476,18 +621,24 @@ fn apply_grip(ml: &mut MultiLeader, grip_id: usize, apply: GripApply) {
|
|||
p.z += dz;
|
||||
}
|
||||
}
|
||||
|
||||
root.connection_point.x += dx;
|
||||
root.connection_point.y += dy;
|
||||
root.connection_point.z += dz;
|
||||
}
|
||||
|
||||
ml.context.text_location.x += dx;
|
||||
ml.context.text_location.y += dy;
|
||||
ml.context.text_location.z += dz;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let mut idx = 0usize;
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// GRIPS DE LA LÍNEA
|
||||
// ─────────────────────────────────────────────
|
||||
for root in &mut ml.context.leader_roots {
|
||||
for line in &mut root.lines {
|
||||
for p in &mut line.points {
|
||||
|
|
@ -498,55 +649,270 @@ fn apply_grip(ml: &mut MultiLeader, grip_id: usize, apply: GripApply) {
|
|||
p.y = a.y as f64;
|
||||
p.z = a.z as f64;
|
||||
}
|
||||
|
||||
GripApply::Translate(d) => {
|
||||
p.x += d.x as f64;
|
||||
p.y += d.y as f64;
|
||||
p.z += d.z as f64;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Text-location grip (idx == n_vertices), then the wrap-width grip.
|
||||
if ml.content_type == LeaderContentType::MText {
|
||||
if idx == grip_id {
|
||||
let tl = &mut ml.context.text_location;
|
||||
match apply {
|
||||
GripApply::Absolute(a) => {
|
||||
tl.x = a.x as f64;
|
||||
tl.y = a.y as f64;
|
||||
tl.z = a.z as f64;
|
||||
}
|
||||
GripApply::Translate(d) => {
|
||||
tl.x += d.x as f64;
|
||||
tl.y += d.y as f64;
|
||||
tl.z += d.z as f64;
|
||||
}
|
||||
if ml.content_type != LeaderContentType::MText {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculamos la geometría antes de modificar nada.
|
||||
let landing =
|
||||
mleader_landing_geom(ml);
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// GRIP DEL CODO
|
||||
// ─────────────────────────────────────────────
|
||||
if let Some((old_elbow, axis, old_sign, dogleg, gap)) = landing {
|
||||
if grip_id == idx {
|
||||
let target = match apply {
|
||||
GripApply::Absolute(a) => {
|
||||
DVec3::new(
|
||||
a.x as f64,
|
||||
a.y as f64,
|
||||
a.z as f64,
|
||||
)
|
||||
}
|
||||
|
||||
GripApply::Translate(d) => {
|
||||
old_elbow
|
||||
+ DVec3::new(
|
||||
d.x as f64,
|
||||
d.y as f64,
|
||||
d.z as f64,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// Punto de flecha de la directriz.
|
||||
//
|
||||
// Usamos el primer punto de la primera LeaderLine,
|
||||
// que corresponde a la punta de flecha.
|
||||
let arrow = ml
|
||||
.context
|
||||
.leader_roots
|
||||
.first()
|
||||
.and_then(|root| root.lines.first())
|
||||
.and_then(|line| line.points.first())
|
||||
.map(|p| {
|
||||
DVec3::new(
|
||||
p.x,
|
||||
p.y,
|
||||
p.z,
|
||||
)
|
||||
})
|
||||
.unwrap_or(old_elbow);
|
||||
|
||||
// Posición del codo respecto de la flecha,
|
||||
// proyectada sobre la dirección horizontal
|
||||
// propia de la MLEADER.
|
||||
//
|
||||
// Esto funciona también con UCS girado.
|
||||
let side_distance =
|
||||
(target - arrow).dot(axis);
|
||||
|
||||
// Histeresis: no espejar apenas cruza 1 px.
|
||||
// Tiene que pasarse un 10% del largo del dogleg.
|
||||
let flip_tol =
|
||||
(dogleg * 0.10).max(1.0e-6);
|
||||
|
||||
let new_sign =
|
||||
if side_distance > flip_tol {
|
||||
1.0
|
||||
} else if side_distance < -flip_tol {
|
||||
-1.0
|
||||
} else {
|
||||
old_sign
|
||||
};
|
||||
// Cambiar el borde de attachment, NO la justificación interna
|
||||
// del texto.
|
||||
//
|
||||
// Texto a la derecha -> se conecta por su borde izquierdo.
|
||||
// Texto a la izquierda -> se conecta por su borde derecho.
|
||||
ml.context.text_attachment_point =
|
||||
if new_sign >= 0.0 {
|
||||
acadrust::entities::multileader::TextAttachmentPointType::Left
|
||||
} else {
|
||||
acadrust::entities::multileader::TextAttachmentPointType::Right
|
||||
};
|
||||
// El texto se coloca a una distancia:
|
||||
// dogleg + landing_gap
|
||||
//
|
||||
// Así el codo sigue coincidiendo con el grip,
|
||||
// y además queda un espacio limpio antes del texto.
|
||||
let new_text =
|
||||
target
|
||||
+ axis
|
||||
* (new_sign * (dogleg + gap));
|
||||
|
||||
ml.context.text_location.x =
|
||||
new_text.x;
|
||||
|
||||
ml.context.text_location.y =
|
||||
new_text.y;
|
||||
|
||||
ml.context.text_location.z =
|
||||
new_text.z;
|
||||
|
||||
// Mantener coherente la información de conexión
|
||||
// almacenada por el MLEADER.
|
||||
if let Some(root) =
|
||||
ml.context.leader_roots.first_mut()
|
||||
{
|
||||
root.connection_point.x =
|
||||
target.x;
|
||||
|
||||
root.connection_point.y =
|
||||
target.y;
|
||||
|
||||
root.connection_point.z =
|
||||
target.z;
|
||||
|
||||
root.direction.x =
|
||||
axis.x * new_sign;
|
||||
|
||||
root.direction.y =
|
||||
axis.y * new_sign;
|
||||
|
||||
root.direction.z =
|
||||
axis.z * new_sign;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
// GRIP DEL EXTREMO HORIZONTAL
|
||||
// ─────────────────────────────────────────
|
||||
if grip_id == idx {
|
||||
let Some((
|
||||
elbow,
|
||||
axis,
|
||||
sign,
|
||||
_old_length,
|
||||
gap,
|
||||
)) = mleader_landing_geom(ml)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let old_end = DVec3::new(
|
||||
ml.context.text_location.x,
|
||||
ml.context.text_location.y,
|
||||
ml.context.text_location.z,
|
||||
);
|
||||
|
||||
let target = match apply {
|
||||
GripApply::Absolute(a) => {
|
||||
DVec3::new(
|
||||
a.x as f64,
|
||||
a.y as f64,
|
||||
a.z as f64,
|
||||
)
|
||||
}
|
||||
|
||||
GripApply::Translate(d) => {
|
||||
old_end
|
||||
+ DVec3::new(
|
||||
d.x as f64,
|
||||
d.y as f64,
|
||||
d.z as f64,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// Proyección exclusivamente sobre la
|
||||
// horizontal propia de la MLEADER.
|
||||
// El grip está en text_location, no en el final de la línea.
|
||||
// Por eso restamos el gap para obtener el largo real del dogleg.
|
||||
let requested_total =
|
||||
(target - elbow).dot(axis) * sign;
|
||||
|
||||
let new_length =
|
||||
(requested_total - gap).max(1.0e-6);
|
||||
|
||||
let new_text =
|
||||
elbow
|
||||
+ axis
|
||||
* (sign * (new_length + gap));
|
||||
|
||||
ml.context.text_location.x =
|
||||
new_text.x;
|
||||
|
||||
ml.context.text_location.y =
|
||||
new_text.y;
|
||||
|
||||
ml.context.text_location.z =
|
||||
new_text.z;
|
||||
|
||||
// landing_distance es justamente la longitud
|
||||
// del dogleg de la MLEADER.
|
||||
ml.dogleg_length =
|
||||
new_length;
|
||||
|
||||
for root in &mut ml.context.leader_roots {
|
||||
root.landing_distance =
|
||||
new_length;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
if idx == grip_id {
|
||||
// Dragging the box edge sets the MText wrap limit, projected on
|
||||
// the box's flow axis (rotated baseline, or down the column for
|
||||
// vertical flow).
|
||||
let (dir, far) = text_box_geom(ml);
|
||||
let (nx, ny) = match apply {
|
||||
GripApply::Absolute(a) => (a.x as f64, a.y as f64),
|
||||
GripApply::Translate(d) => (far[0] + d.x as f64, far[1] + d.y as f64),
|
||||
};
|
||||
let tl = &ml.context.text_location;
|
||||
// `dir` carries the attachment-side factor (may be ±half), so
|
||||
// normalise by its squared length: width = proj(flow)/k.
|
||||
let d2 = (dir[0] * dir[0] + dir[1] * dir[1]).max(1e-12);
|
||||
let proj = ((nx - tl.x) * dir[0] + (ny - tl.y) * dir[1]) / d2;
|
||||
let min_w = ml.text_height.max(1.0) * 0.5;
|
||||
ml.context.text_width = proj.max(min_w);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// GRIP DEL ANCHO DEL TEXTO
|
||||
// ─────────────────────────────────────────────
|
||||
if grip_id == idx {
|
||||
let (dir, far) =
|
||||
text_box_geom(ml);
|
||||
|
||||
let (nx, ny) = match apply {
|
||||
GripApply::Absolute(a) => (
|
||||
a.x as f64,
|
||||
a.y as f64,
|
||||
),
|
||||
|
||||
GripApply::Translate(d) => (
|
||||
far[0] + d.x as f64,
|
||||
far[1] + d.y as f64,
|
||||
),
|
||||
};
|
||||
|
||||
let tl =
|
||||
&ml.context.text_location;
|
||||
|
||||
let d2 =
|
||||
(dir[0] * dir[0]
|
||||
+ dir[1] * dir[1])
|
||||
.max(1.0e-12);
|
||||
|
||||
let proj =
|
||||
((nx - tl.x) * dir[0]
|
||||
+ (ny - tl.y) * dir[1])
|
||||
/ d2;
|
||||
|
||||
let min_w =
|
||||
ml.text_height.max(1.0) * 0.5;
|
||||
|
||||
ml.context.text_width =
|
||||
proj.max(min_w);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1133,53 +1499,87 @@ impl crate::entities::traits::Grippable for MultiLeader {
|
|||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
fn grip_menu(&self, grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
||||
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
||||
fn grip_menu(
|
||||
&self,
|
||||
grip_id: usize,
|
||||
) -> Vec<crate::scene::model::object::GripMenuItem> {
|
||||
use crate::scene::model::object::{
|
||||
GripMenuAction,
|
||||
GripMenuItem,
|
||||
};
|
||||
|
||||
let n_vertices: usize = self
|
||||
.context
|
||||
.leader_roots
|
||||
.iter()
|
||||
.flat_map(|r| r.lines.iter())
|
||||
.map(|l| l.points.len())
|
||||
.flat_map(|root| root.lines.iter())
|
||||
.map(|line| line.points.len())
|
||||
.sum();
|
||||
if self.content_type == LeaderContentType::MText && grip_id >= n_vertices {
|
||||
if grip_id == n_vertices {
|
||||
// Text-location grip.
|
||||
vec![
|
||||
|
||||
if self.content_type
|
||||
== LeaderContentType::MText
|
||||
{
|
||||
let has_elbow =
|
||||
mleader_landing_geom(self).is_some();
|
||||
|
||||
let elbow_id =
|
||||
n_vertices;
|
||||
|
||||
let landing_id =
|
||||
n_vertices
|
||||
+ usize::from(has_elbow);
|
||||
|
||||
let width_id =
|
||||
landing_id + 1;
|
||||
|
||||
// Codo.
|
||||
if has_elbow
|
||||
&& grip_id == elbow_id
|
||||
{
|
||||
return vec![
|
||||
GripMenuItem {
|
||||
label: "Stretch",
|
||||
action: GripMenuAction::Stretch,
|
||||
action:
|
||||
GripMenuAction::Stretch,
|
||||
},
|
||||
GripMenuItem {
|
||||
label: "Move with Leader",
|
||||
action: GripMenuAction::MoveWithLeader,
|
||||
},
|
||||
GripMenuItem {
|
||||
label: "Move Independent",
|
||||
action: GripMenuAction::MoveIndependent,
|
||||
},
|
||||
]
|
||||
} else {
|
||||
// Wrap-width grip: drag only.
|
||||
Vec::new()
|
||||
];
|
||||
}
|
||||
|
||||
// Extremo horizontal.
|
||||
if grip_id == landing_id {
|
||||
return vec![
|
||||
GripMenuItem {
|
||||
label: "Stretch",
|
||||
action:
|
||||
GripMenuAction::Stretch,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Ancho del texto.
|
||||
if grip_id == width_id {
|
||||
return Vec::new();
|
||||
}
|
||||
} else {
|
||||
// Leader-line vertex.
|
||||
vec![
|
||||
GripMenuItem {
|
||||
label: "Stretch",
|
||||
action: GripMenuAction::Stretch,
|
||||
},
|
||||
GripMenuItem {
|
||||
label: "Add Leader",
|
||||
action: GripMenuAction::AddLeader,
|
||||
},
|
||||
GripMenuItem {
|
||||
label: "Remove Leader",
|
||||
action: GripMenuAction::RemoveLeader,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Flecha / vértices de la línea.
|
||||
vec![
|
||||
GripMenuItem {
|
||||
label: "Stretch",
|
||||
action:
|
||||
GripMenuAction::Stretch,
|
||||
},
|
||||
GripMenuItem {
|
||||
label: "Add Leader",
|
||||
action:
|
||||
GripMenuAction::AddLeader,
|
||||
},
|
||||
GripMenuItem {
|
||||
label: "Remove Leader",
|
||||
action:
|
||||
GripMenuAction::RemoveLeader,
|
||||
},
|
||||
]
|
||||
}
|
||||
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
||||
use crate::scene::model::object::GripMenuAction as A;
|
||||
|
|
@ -1323,16 +1723,53 @@ impl MultiLeaderTess for MultiLeader {
|
|||
|
||||
// ── Scaling ──────────────────────────────────────────────────────────────
|
||||
// Used only when a context omits an already-resolved content size.
|
||||
let fallback_content_scale = (ml.scale_factor as f32)
|
||||
* if crate::scene::annotative::mleader_is_annotative(document, ml) {
|
||||
anno_scale
|
||||
// ── Scaling ──────────────────────────────────────────────────────────────
|
||||
|
||||
let annotative =
|
||||
crate::scene::annotative::mleader_is_annotative(document, ml);
|
||||
|
||||
// MLEADER context data stores sizes already resolved for the annotation
|
||||
// scale at which that context was created.
|
||||
//
|
||||
// If the current CANNOSCALE is different, apply only the ratio between
|
||||
// the requested scale and the context's stored scale.
|
||||
//
|
||||
// Example:
|
||||
// context created at 1:200 -> context.scale_factor = 200
|
||||
// current scale 1:100 -> correction = 100 / 200 = 0.5
|
||||
//
|
||||
// If an AutoCAD file already supplies a real 1:100 context, its
|
||||
// scale_factor is already 100 and the correction naturally becomes 1.
|
||||
let base_scale = if ml.scale_factor.abs() > 1.0e-12 {
|
||||
ml.scale_factor as f32
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let stored_context_scale = ml.context.scale_factor as f32;
|
||||
|
||||
let context_scale_correction =
|
||||
if annotative && stored_context_scale.abs() > 1.0e-12 {
|
||||
let correction =
|
||||
(base_scale * anno_scale) / stored_context_scale;
|
||||
|
||||
if correction.is_finite() && correction > 0.0 {
|
||||
correction
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let fallback_content_scale =
|
||||
base_scale * if annotative { anno_scale } else { 1.0 };
|
||||
|
||||
// The active context stores the resolved world-space arrow size.
|
||||
// Reapplying the entity scale here makes context-sized arrows grow twice.
|
||||
let arrow_size = ml.context.arrowhead_size as f32;
|
||||
let arrow_size =
|
||||
ml.context.arrowhead_size as f32
|
||||
* context_scale_correction;
|
||||
let draw_arrow = arrow_size > 0.0;
|
||||
let invisible = ml.path_type == MultiLeaderPathType::Invisible;
|
||||
// arrowhead_handle resolves through the block records to a named arrow
|
||||
|
|
@ -1357,7 +1794,27 @@ impl MultiLeaderTess for MultiLeader {
|
|||
|
||||
// Which side the text grip sits on, recomputed every frame so the text
|
||||
// alignment and the landing mirror live when the arrow or text moves.
|
||||
let text_loc_w = ml.context.text_location;
|
||||
let mut text_loc_w = ml.context.text_location;
|
||||
|
||||
// Keep the leader elbow fixed in model space and scale only the
|
||||
// annotation-side offset. This makes text + landing follow CANNOSCALE
|
||||
// without moving the arrowhead or the user-defined leader geometry.
|
||||
if annotative
|
||||
&& (context_scale_correction - 1.0).abs() > 1.0e-6
|
||||
{
|
||||
if let Some(root) = ml.context.leader_roots.first() {
|
||||
let anchor = root.connection_point;
|
||||
let k = context_scale_correction as f64;
|
||||
|
||||
text_loc_w = acadrust::types::Vector3::new(
|
||||
anchor.x
|
||||
+ (text_loc_w.x - anchor.x) * k,
|
||||
anchor.y
|
||||
+ (text_loc_w.y - anchor.y) * k,
|
||||
text_loc_w.z,
|
||||
);
|
||||
}
|
||||
}
|
||||
let leader_ref_w = ml
|
||||
.context
|
||||
.leader_roots
|
||||
|
|
@ -1471,7 +1928,9 @@ impl MultiLeaderTess for MultiLeader {
|
|||
// stray line up the side of the text.
|
||||
// Landing distance belongs to the selected leader-root context
|
||||
// and is already resolved in world units.
|
||||
let d = root.landing_distance;
|
||||
let d =
|
||||
root.landing_distance
|
||||
* context_scale_correction as f64;
|
||||
// The dogleg runs along the leader root's stored direction —
|
||||
// for a rotated leader that is the angled baseline, not world
|
||||
// X. Roots without a usable direction keep the legacy
|
||||
|
|
@ -1633,11 +2092,13 @@ impl MultiLeaderTess for MultiLeader {
|
|||
// scale_factor + annotation scale applied.
|
||||
let height = if ctx.text_height > 0.0 {
|
||||
ctx.text_height as f32
|
||||
* context_scale_correction
|
||||
} else {
|
||||
ml.text_height as f32 * fallback_content_scale
|
||||
ml.text_height as f32
|
||||
* fallback_content_scale
|
||||
};
|
||||
|
||||
let ins = &ctx.text_location;
|
||||
let ins = &text_loc_w;
|
||||
// Subtract world_offset in f64 before casting to f32: drawings often
|
||||
// sit at large absolute coordinates and casting first then subtracting
|
||||
// throws away the precision needed for the rotated sub-glyph offsets.
|
||||
|
|
@ -1730,7 +2191,9 @@ impl MultiLeaderTess for MultiLeader {
|
|||
value: &ctx.text_string,
|
||||
insertion: [local_ins_x as f64, local_ins_y as f64, z as f64],
|
||||
height,
|
||||
rect_w: ctx.text_width as f32,
|
||||
rect_w:
|
||||
ctx.text_width as f32
|
||||
* context_scale_correction,
|
||||
rotation: rot,
|
||||
style: &style,
|
||||
attach_h_anchor: h_anchor,
|
||||
|
|
|
|||
|
|
@ -74,17 +74,34 @@ impl CadCommand for MLeaderCommand {
|
|||
if self.verts.is_empty() {
|
||||
t!("MLEADER Specify arrowhead point:").into_owned()
|
||||
} else {
|
||||
t!(
|
||||
"MLEADER Specify next point [%{count} pts — Enter to place text]:",
|
||||
count = self.verts.len()
|
||||
)
|
||||
.into_owned()
|
||||
t!("MLEADER Specify landing point:").into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
self.verts.push(pt);
|
||||
CmdResult::NeedPoint
|
||||
|
||||
if self.verts.len() < 2 {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
|
||||
let local: Vec<DVec3> = self
|
||||
.verts
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect();
|
||||
|
||||
let ml = build_mleader(
|
||||
"",
|
||||
&local,
|
||||
Mat4::IDENTITY,
|
||||
self.style.as_ref(),
|
||||
self.display_scale,
|
||||
);
|
||||
|
||||
CmdResult::CommitAndEditText(
|
||||
self.plane.place_entity(EntityType::MultiLeader(ml)),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
|
|
@ -163,13 +180,23 @@ fn build_mleader(
|
|||
display_scale: f64,
|
||||
) -> MultiLeader {
|
||||
// Last vertex = content/text location; remaining = leader line points
|
||||
let (leader_pts, content_pt) = verts.split_at(verts.len() - 1);
|
||||
let content_pt = content_pt[0];
|
||||
// Standard MLEADER creation:
|
||||
// verts[0] = arrowhead point
|
||||
// verts[1] = elbow / leader-root connection point
|
||||
let arrow_pt = verts[0];
|
||||
let elbow_pt = verts[1];
|
||||
|
||||
let leader_v3: Vec<Vector3> = leader_pts.iter().map(|p| v3(*p)).collect();
|
||||
let content_v3 = v3(content_pt);
|
||||
let arrow_v3 = v3(arrow_pt);
|
||||
let elbow_v3 = v3(elbow_pt);
|
||||
|
||||
let mut ml = MultiLeader::with_text(text, content_v3, leader_v3);
|
||||
// Start with one native leader line.
|
||||
// The text/content position is corrected below once the dogleg
|
||||
// direction and landing distance are known.
|
||||
let mut ml = MultiLeader::with_text(
|
||||
text,
|
||||
elbow_v3,
|
||||
vec![arrow_v3],
|
||||
);
|
||||
if let Some(style) = style {
|
||||
crate::scene::annotative::apply_mleader_style(&mut ml, style);
|
||||
} else {
|
||||
|
|
@ -190,10 +217,21 @@ fn build_mleader(
|
|||
// world), so the annotation reads square to the user's coordinate system.
|
||||
let ux = ucs.transform_vector3(Vec3::X).normalize_or(Vec3::X);
|
||||
// Which side of the leader the text sits on, measured along the UCS X axis.
|
||||
let last_leader = leader_pts.last().copied().unwrap_or(content_pt);
|
||||
let to_right = (content_pt - last_leader).dot(ux.as_dvec3()) >= 0.0;
|
||||
let to_right = (elbow_pt - arrow_pt).dot(ux.as_dvec3()) >= 0.0;
|
||||
let sign = if to_right { 1.0 } else { -1.0 };
|
||||
let landing = ux * (sign as f32);
|
||||
// text_location es el punto de unión del contenido con la directriz.
|
||||
//
|
||||
// Si el texto está a la derecha, el punto de inserción corresponde
|
||||
// al borde IZQUIERDO del texto.
|
||||
//
|
||||
// Si está a la izquierda, corresponde al borde DERECHO.
|
||||
ml.context.text_attachment_point =
|
||||
if to_right {
|
||||
acadrust::entities::multileader::TextAttachmentPointType::Left
|
||||
} else {
|
||||
acadrust::entities::multileader::TextAttachmentPointType::Right
|
||||
};
|
||||
|
||||
// Text + landing read along the UCS X axis. text_direction is what the
|
||||
// renderer consults first, so set both.
|
||||
|
|
@ -201,18 +239,29 @@ fn build_mleader(
|
|||
ml.context.text_direction = Vector3::new(ux.x as f64, ux.y as f64, 0.0);
|
||||
|
||||
if let Some(root) = ml.context.leader_roots.first_mut() {
|
||||
// Leader ends at the clicked point; the landing runs from there toward
|
||||
// the text along the UCS X axis.
|
||||
root.direction = Vector3::new(landing.x as f64, landing.y as f64, 0.0);
|
||||
root.connection_point = content_v3;
|
||||
root.direction =
|
||||
Vector3::new(landing.x as f64, landing.y as f64, 0.0);
|
||||
|
||||
// The second click is the elbow/root connection.
|
||||
root.connection_point = elbow_v3;
|
||||
|
||||
// Dogleg length from the active MLeaderStyle.
|
||||
root.landing_distance = landing_distance;
|
||||
}
|
||||
|
||||
// Seed the text one landing-length past the leader end, on the side the
|
||||
// user dragged toward, offset along the UCS X axis.
|
||||
let off = landing * (landing_distance + landing_gap) as f32;
|
||||
ml.context.text_location =
|
||||
Vector3::new(content_v3.x + off.x as f64, content_v3.y + off.y as f64, content_v3.z);
|
||||
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,
|
||||
);
|
||||
|
||||
ml.context.text_location = text_location;
|
||||
ml.context.content_base_point = text_location;
|
||||
|
||||
ml
|
||||
}
|
||||
|
|
|
|||
|
|
@ -697,25 +697,16 @@ pub fn effective_annotation_scale_for(
|
|||
// already-scaled text height and overall scale factor. Keep the text
|
||||
// height as stored; make `ml.scale_factor * anno_scale` resolve to the
|
||||
// active context's scale factor for arrows, doglegs, and fallback text.
|
||||
if let EntityType::MultiLeader(mleader) = entity {
|
||||
let Some(active) =
|
||||
active_object_context_for_scale(doc, entity.common().handle, scale_handle)
|
||||
else {
|
||||
return fallback;
|
||||
};
|
||||
let ObjectContextKind::MLeader(context) = &active.kind else {
|
||||
return fallback;
|
||||
};
|
||||
let base = mleader.scale_factor;
|
||||
if base.abs() <= 1.0e-12 {
|
||||
return fallback;
|
||||
}
|
||||
let relative = context.scale_factor / base;
|
||||
return if relative.is_finite() && relative > 0.0 {
|
||||
relative as f32
|
||||
} else {
|
||||
fallback
|
||||
};
|
||||
// MLEADER needs the absolute current annotation multiplier here.
|
||||
//
|
||||
// Its stored context may belong to another annotation scale. The MLEADER
|
||||
// tessellator compares this current multiplier with the context's stored
|
||||
// scale_factor and applies only the required correction.
|
||||
//
|
||||
// When the active context already belongs to this scale the correction is 1,
|
||||
// preserving AutoCAD per-scale context geometry unchanged.
|
||||
if matches!(entity, EntityType::MultiLeader(_)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
let Some(coll_h) = annotation_scales_dict(doc, entity.common().handle) else {
|
||||
|
|
@ -1586,47 +1577,162 @@ pub fn apply_mleader_style(
|
|||
}
|
||||
}
|
||||
}
|
||||
fn apply_mleader_style_at_display_scale(
|
||||
entity: &mut acadrust::entities::MultiLeader,
|
||||
style: &acadrust::objects::MultiLeaderStyle,
|
||||
display_scale: f64,
|
||||
) {
|
||||
apply_mleader_style(entity, style);
|
||||
|
||||
let scale = if display_scale.is_finite()
|
||||
&& display_scale > 1.0e-12
|
||||
{
|
||||
display_scale
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
// Estos valores del estilo son tamaños de papel.
|
||||
// El contexto MLEADER guarda la representación ya
|
||||
// escalada para la escala anotativa/modelo activa.
|
||||
entity.context.scale_factor = scale;
|
||||
|
||||
entity.context.text_height =
|
||||
style.text_height * 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;
|
||||
|
||||
root.text_attachment_direction =
|
||||
entity.text_attachment_direction;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_mleader_style_to_object(
|
||||
doc: &mut CadDocument,
|
||||
handle: Handle,
|
||||
style: &acadrust::objects::MultiLeaderStyle,
|
||||
) -> bool {
|
||||
let Some(EntityType::MultiLeader(original)) = doc.get_entity(handle).cloned() else {
|
||||
let Some(EntityType::MultiLeader(original)) =
|
||||
doc.get_entity(handle).cloned()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Escala visual de la representación base.
|
||||
//
|
||||
// En una MLEADER anotativa context.scale_factor
|
||||
// contiene la escala real de esa representación
|
||||
// (por ejemplo 50 para 1:50).
|
||||
//
|
||||
// En una no anotativa manda el scale_factor del estilo.
|
||||
let base_display_scale = if style.is_annotative {
|
||||
let scale = original.context.scale_factor;
|
||||
|
||||
if scale.is_finite() && scale > 1.0e-12 {
|
||||
scale
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
} else {
|
||||
let scale = style.scale_factor;
|
||||
|
||||
if scale.is_finite() && scale > 1.0e-12 {
|
||||
scale
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
};
|
||||
|
||||
let mut styled = original.clone();
|
||||
apply_mleader_style(&mut styled, style);
|
||||
if let Some(EntityType::MultiLeader(entity)) = doc.get_entity_mut(handle) {
|
||||
|
||||
apply_mleader_style_at_display_scale(
|
||||
&mut styled,
|
||||
style,
|
||||
base_display_scale,
|
||||
);
|
||||
|
||||
if let Some(EntityType::MultiLeader(entity)) =
|
||||
doc.get_entity_mut(handle)
|
||||
{
|
||||
*entity = styled;
|
||||
}
|
||||
|
||||
let leaf_handles: Vec<_> = annotation_scales_dict(doc, handle)
|
||||
.and_then(|collection| as_dict(doc, collection))
|
||||
.map(|collection| collection.entries.iter().map(|(_, leaf)| *leaf).collect())
|
||||
.unwrap_or_default();
|
||||
// Actualizar también TODAS las representaciones
|
||||
// anotativas almacenadas de la misma MLEADER.
|
||||
let leaf_handles: Vec<_> =
|
||||
annotation_scales_dict(doc, handle)
|
||||
.and_then(|collection| as_dict(doc, collection))
|
||||
.map(|collection| {
|
||||
collection
|
||||
.entries
|
||||
.iter()
|
||||
.map(|(_, leaf)| *leaf)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
for leaf_handle in leaf_handles {
|
||||
let Some(ObjectType::ObjectContextData(leaf)) = doc.objects.get_mut(&leaf_handle) else {
|
||||
continue;
|
||||
};
|
||||
let ObjectContextKind::MLeader(context) = &mut leaf.kind else {
|
||||
continue;
|
||||
};
|
||||
let context_scale = context.scale_factor;
|
||||
let text_height_ratio = if original.text_height.abs() > 1.0e-12 {
|
||||
context.text_height / original.text_height
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
// Primero clonamos los datos necesarios para
|
||||
// evitar mantener un borrow mutable sobre doc.
|
||||
let context_before =
|
||||
match doc.objects.get(&leaf_handle) {
|
||||
Some(
|
||||
ObjectType::ObjectContextData(leaf)
|
||||
) => {
|
||||
let ObjectContextKind::MLeader(context) =
|
||||
&leaf.kind
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
context.clone()
|
||||
}
|
||||
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let context_scale =
|
||||
if context_before.scale_factor.is_finite()
|
||||
&& context_before.scale_factor > 1.0e-12
|
||||
{
|
||||
context_before.scale_factor
|
||||
} else {
|
||||
base_display_scale
|
||||
};
|
||||
|
||||
let mut per_scale = original.clone();
|
||||
per_scale.context.clone_from(context);
|
||||
apply_mleader_style(&mut per_scale, style);
|
||||
per_scale.context.scale_factor = context_scale;
|
||||
if style.text_height > 0.0 && text_height_ratio.is_finite() {
|
||||
per_scale.context.text_height = style.text_height * text_height_ratio;
|
||||
|
||||
per_scale.context =
|
||||
context_before;
|
||||
|
||||
apply_mleader_style_at_display_scale(
|
||||
&mut per_scale,
|
||||
style,
|
||||
context_scale,
|
||||
);
|
||||
|
||||
if let Some(
|
||||
ObjectType::ObjectContextData(leaf)
|
||||
) = doc.objects.get_mut(&leaf_handle)
|
||||
{
|
||||
if let ObjectContextKind::MLeader(context) =
|
||||
&mut leaf.kind
|
||||
{
|
||||
context.clone_from(
|
||||
&per_scale.context,
|
||||
);
|
||||
}
|
||||
}
|
||||
context.clone_from(&per_scale.context);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue