From d693bcf9686fd0251849b5866d1081de8c1288fb Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Mon, 24 Aug 2026 21:12:30 -0300 Subject: [PATCH] fix(mleader): improve grips, styles and annotative behavior --- src/app/command_driver.rs | 73 ++- src/app/layers.rs | 21 +- src/app/mtext_editor.rs | 19 + src/app/style_ops.rs | 190 +++++++- src/app/update/command.rs | 15 +- src/entities/multileader.rs | 667 +++++++++++++++++++++++----- src/modules/annotate/mleader_cmd.rs | 89 +++- src/scene/annotative.rs | 194 ++++++-- 8 files changed, 1077 insertions(+), 191 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 7c69d400..5f8eebb6 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -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, diff --git a/src/app/layers.rs b/src/app/layers.rs index 6f00ce26..6be69c8f 100644 --- a/src/app/layers.rs +++ b/src/app/layers.rs @@ -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 = doc .objects diff --git a/src/app/mtext_editor.rs b/src/app/mtext_editor.rs index ce6d370c..35a6aafc 100644 --- a/src/app/mtext_editor.rs +++ b/src/app/mtext_editor.rs @@ -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)]); diff --git a/src/app/style_ops.rs b/src/app/style_ops.rs index 0bdad07b..c12fc337 100644 --- a/src/app/style_ops.rs +++ b/src/app/style_ops.rs @@ -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 { + 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 = + 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 = { + 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 diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 3ecaec32..09de2535 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1672,9 +1672,20 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } } 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; diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index d2568013..65b85eec 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -182,8 +182,18 @@ fn to_render(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option= 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 Option = 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 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 { - let mut result: Vec = 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 { /// 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 { - use crate::scene::model::object::{GripMenuAction, GripMenuItem}; + fn grip_menu( + &self, + grip_id: usize, + ) -> Vec { + 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, diff --git a/src/modules/annotate/mleader_cmd.rs b/src/modules/annotate/mleader_cmd.rs index 383a8d66..e138e4a2 100644 --- a/src/modules/annotate/mleader_cmd.rs +++ b/src/modules/annotate/mleader_cmd.rs @@ -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 = 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 = 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 } diff --git a/src/scene/annotative.rs b/src/scene/annotative.rs index 0c887277..9172e46d 100644 --- a/src/scene/annotative.rs +++ b/src/scene/annotative.rs @@ -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 }