2026-03-22 14:56:27 +03:00
|
|
|
use acadrust::entities::{Text, TextHorizontalAlignment as HA, TextVerticalAlignment as VA};
|
|
|
|
|
|
|
|
|
|
use crate::command::EntityTransform;
|
|
|
|
|
use crate::entities::common::{edit_prop as edit, parse_f64, square_grip};
|
2026-05-12 10:36:15 +03:00
|
|
|
use crate::entities::text_support::{
|
|
|
|
|
resolve_dxf_special_chars, resolve_text_style, text_local_bounds,
|
|
|
|
|
};
|
2026-03-22 14:56:27 +03:00
|
|
|
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
2026-06-16 11:33:46 +03:00
|
|
|
use crate::scene::convert::acad_to_truck::{TextStroke, TruckEntity, TruckObject};
|
|
|
|
|
use crate::scene::text::lff;
|
|
|
|
|
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
|
|
|
|
use crate::scene::model::wire_model::SnapHint;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
|
|
|
fn text_halign_str(a: &acadrust::entities::TextHorizontalAlignment) -> &'static str {
|
|
|
|
|
use acadrust::entities::TextHorizontalAlignment::*;
|
|
|
|
|
match a {
|
|
|
|
|
Left => "Left",
|
|
|
|
|
Center => "Center",
|
|
|
|
|
Right => "Right",
|
|
|
|
|
Aligned => "Aligned",
|
|
|
|
|
Middle => "Middle",
|
|
|
|
|
Fit => "Fit",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn text_valign_str(a: &acadrust::entities::TextVerticalAlignment) -> &'static str {
|
|
|
|
|
use acadrust::entities::TextVerticalAlignment::*;
|
|
|
|
|
match a {
|
|
|
|
|
Baseline => "Baseline",
|
|
|
|
|
Bottom => "Bottom",
|
|
|
|
|
Middle => "Middle",
|
|
|
|
|
Top => "Top",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn sync_text_alignment_point(t: &mut Text) {
|
|
|
|
|
let needs_alignment_point = !matches!(
|
|
|
|
|
(t.horizontal_alignment, t.vertical_alignment),
|
|
|
|
|
(HA::Left, VA::Baseline)
|
|
|
|
|
);
|
|
|
|
|
if needs_alignment_point {
|
|
|
|
|
if t.alignment_point.is_none() {
|
|
|
|
|
t.alignment_point = Some(t.insertion_point);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
t.alignment_point = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity {
|
2026-05-02 20:06:22 +03:00
|
|
|
let normal = (t.normal.x, t.normal.y, t.normal.z);
|
2026-06-16 11:33:46 +03:00
|
|
|
let (wsx, wsy, wsz) = crate::scene::view::transform::ocs_point_to_wcs(
|
2026-05-12 10:36:15 +03:00
|
|
|
(
|
|
|
|
|
t.insertion_point.x,
|
|
|
|
|
t.insertion_point.y,
|
|
|
|
|
t.insertion_point.z,
|
|
|
|
|
),
|
2026-05-02 20:06:22 +03:00
|
|
|
normal,
|
2026-03-22 14:56:27 +03:00
|
|
|
);
|
refactor(snap): producers emit f64 snap points (world_offset removal P3)
TruckEntity.snap_pts is now DVec3 and every entity producer builds snap
candidates straight from the acadrust f64 coordinates instead of casting
to f32 first (text, mtext, arc, circle, ellipse, point, shape, solid,
mline, mtext, attribute, tolerance, table, underlay, ole2frame, mesh,
multileader, dimension, block-cache). offset_snap_pts stays f64
throughout. Still offset-relative (inert) — but snap precision is no
longer capped at ~0.5 m by an early f32 cast, which fixes a latent
UTM-scale snap error and sets up the absolute-coordinate switch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 22:10:29 +03:00
|
|
|
let snap_pt = glam::DVec3::new(wsx, wsy, wsz);
|
2026-03-22 14:56:27 +03:00
|
|
|
let resolved_style = resolve_text_style(&t.style, document);
|
|
|
|
|
let font_name = resolved_style.font_name;
|
2026-05-21 12:38:03 +03:00
|
|
|
// AutoCAD text geometry rule: the entity stores the FINAL width factor /
|
|
|
|
|
// oblique angle, copied from the style at creation and persisting through
|
|
|
|
|
// style edits. Use it as-is. Only fall back to the style when the entity
|
|
|
|
|
// value is missing (the parser reports 0.0 for default-omitted fields).
|
|
|
|
|
let base_wf = if t.width_factor.abs() > 1e-9 {
|
|
|
|
|
(t.width_factor as f32).clamp(0.01, 100.0)
|
2026-05-12 10:36:15 +03:00
|
|
|
} else {
|
2026-05-21 12:38:03 +03:00
|
|
|
resolved_style.width_factor.max(0.01)
|
|
|
|
|
};
|
2026-04-26 22:10:32 +03:00
|
|
|
// is_backward mirrors text left-right via negative width factor.
|
2026-05-12 10:36:15 +03:00
|
|
|
let width_factor = if resolved_style.is_backward {
|
|
|
|
|
-base_wf
|
|
|
|
|
} else {
|
|
|
|
|
base_wf
|
|
|
|
|
};
|
2026-04-26 22:10:32 +03:00
|
|
|
// is_upside_down rotates 180° around the insertion point.
|
|
|
|
|
let rotation = if resolved_style.is_upside_down {
|
|
|
|
|
t.rotation as f32 + std::f32::consts::PI
|
|
|
|
|
} else {
|
|
|
|
|
t.rotation as f32
|
|
|
|
|
};
|
2026-05-21 12:38:03 +03:00
|
|
|
let oblique_angle = if t.oblique_angle.abs() > 1e-9 {
|
|
|
|
|
t.oblique_angle as f32
|
|
|
|
|
} else {
|
|
|
|
|
resolved_style.oblique_angle
|
|
|
|
|
};
|
2026-06-22 18:47:45 +03:00
|
|
|
// Anchor stays f64: large coordinates (UTM etc.) lose ~0.5 units of
|
|
|
|
|
// precision when cast to f32, which snaps text baselines onto a coarse
|
|
|
|
|
// grid and makes adjacent rows collide. Only the small local offsets
|
|
|
|
|
// below are computed in f32.
|
|
|
|
|
let anchor: [f64; 2] = match (
|
2026-03-22 14:56:27 +03:00
|
|
|
&t.horizontal_alignment,
|
|
|
|
|
&t.vertical_alignment,
|
|
|
|
|
&t.alignment_point,
|
|
|
|
|
) {
|
2026-06-22 18:47:45 +03:00
|
|
|
(HA::Aligned | HA::Middle | HA::Fit, _, Some(a)) => [a.x, a.y],
|
|
|
|
|
(HA::Center | HA::Right, _, Some(a)) => [a.x, a.y],
|
|
|
|
|
(_, VA::Bottom | VA::Middle | VA::Top, Some(a)) => [a.x, a.y],
|
|
|
|
|
_ => [t.insertion_point.x, t.insertion_point.y],
|
2026-03-22 14:56:27 +03:00
|
|
|
};
|
2026-04-26 21:57:51 +03:00
|
|
|
// Strip %%u/%%o for bounds (they add no width); resolve %%d/%%c/%%p for correct advance.
|
|
|
|
|
let value_for_bounds = resolve_dxf_special_chars(&t.value);
|
2026-03-22 14:56:27 +03:00
|
|
|
let bounds = text_local_bounds(
|
|
|
|
|
&font_name,
|
2026-04-26 21:57:51 +03:00
|
|
|
&value_for_bounds,
|
2026-03-22 14:56:27 +03:00
|
|
|
t.height as f32,
|
|
|
|
|
width_factor,
|
|
|
|
|
oblique_angle,
|
|
|
|
|
);
|
2026-06-22 18:47:45 +03:00
|
|
|
let (anchor_local_x, anchor_local_y) = if let Some(b) = bounds {
|
|
|
|
|
// Horizontal anchor uses the pen advance box [0, advance] so leading /
|
|
|
|
|
// trailing spaces keep their width instead of snapping to the inked glyphs.
|
2026-03-22 14:56:27 +03:00
|
|
|
let ax = match t.horizontal_alignment {
|
2026-06-22 18:47:45 +03:00
|
|
|
HA::Left => 0.0,
|
|
|
|
|
HA::Center | HA::Middle => b.advance * 0.5,
|
|
|
|
|
HA::Right | HA::Aligned | HA::Fit => b.advance,
|
2026-03-22 14:56:27 +03:00
|
|
|
};
|
2026-06-22 18:47:45 +03:00
|
|
|
// Vertical anchor uses the inked extent (cap / baseline geometry).
|
2026-03-22 14:56:27 +03:00
|
|
|
let ay = match t.vertical_alignment {
|
|
|
|
|
VA::Baseline => 0.0,
|
2026-06-22 18:47:45 +03:00
|
|
|
VA::Bottom => b.ink_min[1],
|
|
|
|
|
VA::Middle => (b.ink_min[1] + b.ink_max[1]) * 0.5,
|
|
|
|
|
VA::Top => b.ink_max[1],
|
2026-03-22 14:56:27 +03:00
|
|
|
};
|
|
|
|
|
(ax, ay)
|
|
|
|
|
} else {
|
|
|
|
|
(0.0, 0.0)
|
|
|
|
|
};
|
2026-04-29 20:58:44 +03:00
|
|
|
let (cos_r, sin_r) = (rotation.cos() as f64, rotation.sin() as f64);
|
|
|
|
|
// Keep origin as f64 — large coordinates (UTM etc.) must not be cast to
|
|
|
|
|
// f32 here; world_offset subtraction happens later in tessellate.rs.
|
2026-06-22 18:47:45 +03:00
|
|
|
let anchor_f64 = anchor;
|
2026-04-29 20:58:44 +03:00
|
|
|
let origin: [f64; 2] = [
|
|
|
|
|
anchor_f64[0] - (anchor_local_x as f64 * cos_r - anchor_local_y as f64 * sin_r),
|
|
|
|
|
anchor_f64[1] - (anchor_local_x as f64 * sin_r + anchor_local_y as f64 * cos_r),
|
2026-03-22 14:56:27 +03:00
|
|
|
];
|
2026-04-29 20:58:44 +03:00
|
|
|
// Strokes are in glyph-local space (origin = [0,0]).
|
2026-06-25 18:17:01 +01:00
|
|
|
let (strokes, fill_tris) = lff::tessellate_text_ex(
|
2026-04-29 20:58:44 +03:00
|
|
|
[0.0, 0.0],
|
2026-03-22 14:56:27 +03:00
|
|
|
t.height as f32,
|
2026-04-26 22:10:32 +03:00
|
|
|
rotation,
|
2026-03-22 14:56:27 +03:00
|
|
|
width_factor,
|
|
|
|
|
oblique_angle,
|
|
|
|
|
&font_name,
|
|
|
|
|
&t.value,
|
|
|
|
|
);
|
|
|
|
|
TruckEntity {
|
2026-06-11 00:04:37 +03:00
|
|
|
object: TruckObject::Text(vec![TextStroke {
|
|
|
|
|
strokes,
|
|
|
|
|
origin,
|
|
|
|
|
color: None,
|
2026-06-25 18:17:01 +01:00
|
|
|
fill_tris,
|
2026-06-11 00:04:37 +03:00
|
|
|
}]),
|
2026-03-22 14:56:27 +03:00
|
|
|
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
|
|
|
|
tangent_geoms: vec![],
|
|
|
|
|
key_vertices: vec![],
|
2026-05-06 23:24:26 +03:00
|
|
|
fill_tris: vec![],
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn grips(t: &Text) -> Vec<GripDef> {
|
2026-06-11 00:04:37 +03:00
|
|
|
let p = glam::DVec3::new(
|
|
|
|
|
t.insertion_point.x,
|
|
|
|
|
t.insertion_point.y,
|
|
|
|
|
t.insertion_point.z,
|
2026-03-22 14:56:27 +03:00
|
|
|
);
|
|
|
|
|
vec![square_grip(0, p)]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn properties(t: &Text, text_style_names: &[String]) -> PropSection {
|
|
|
|
|
PropSection {
|
|
|
|
|
title: "Geometry".into(),
|
|
|
|
|
props: vec![
|
|
|
|
|
edit("Insert X", "ins_x", t.insertion_point.x),
|
|
|
|
|
edit("Insert Y", "ins_y", t.insertion_point.y),
|
|
|
|
|
edit("Insert Z", "ins_z", t.insertion_point.z),
|
|
|
|
|
edit("Height", "height", t.height),
|
|
|
|
|
edit("Rotation", "rotation", t.rotation.to_degrees()),
|
|
|
|
|
edit("Width Factor", "width_factor", t.width_factor),
|
|
|
|
|
edit(
|
|
|
|
|
"Oblique Angle",
|
|
|
|
|
"oblique_angle",
|
|
|
|
|
t.oblique_angle.to_degrees(),
|
|
|
|
|
),
|
|
|
|
|
Property {
|
|
|
|
|
label: "H-Align".into(),
|
|
|
|
|
field: "h_align",
|
|
|
|
|
value: PropValue::Choice {
|
|
|
|
|
selected: text_halign_str(&t.horizontal_alignment).to_string(),
|
|
|
|
|
options: ["Left", "Center", "Right", "Aligned", "Middle", "Fit"]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(str::to_string)
|
|
|
|
|
.collect(),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Property {
|
|
|
|
|
label: "V-Align".into(),
|
|
|
|
|
field: "v_align",
|
|
|
|
|
value: PropValue::Choice {
|
|
|
|
|
selected: text_valign_str(&t.vertical_alignment).to_string(),
|
|
|
|
|
options: ["Baseline", "Bottom", "Middle", "Top"]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(str::to_string)
|
|
|
|
|
.collect(),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Property {
|
|
|
|
|
label: "Content".into(),
|
|
|
|
|
field: "content",
|
|
|
|
|
value: PropValue::EditText(t.value.clone()),
|
|
|
|
|
},
|
|
|
|
|
Property {
|
|
|
|
|
label: "Style".into(),
|
|
|
|
|
field: "style",
|
|
|
|
|
value: PropValue::Choice {
|
|
|
|
|
selected: if t.style.trim().is_empty() {
|
|
|
|
|
"Standard".into()
|
|
|
|
|
} else {
|
|
|
|
|
t.style.clone()
|
|
|
|
|
},
|
|
|
|
|
options: text_style_names.to_vec(),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_geom_prop(t: &mut Text, field: &str, value: &str) {
|
|
|
|
|
match field {
|
|
|
|
|
"content" => {
|
|
|
|
|
t.value = value.to_string();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
"style" => {
|
|
|
|
|
t.style = value.to_string();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
"h_align" => {
|
|
|
|
|
t.horizontal_alignment = match value {
|
|
|
|
|
"Left" => HA::Left,
|
|
|
|
|
"Center" => HA::Center,
|
|
|
|
|
"Right" => HA::Right,
|
|
|
|
|
"Aligned" => HA::Aligned,
|
|
|
|
|
"Middle" => HA::Middle,
|
|
|
|
|
"Fit" => HA::Fit,
|
|
|
|
|
_ => return,
|
|
|
|
|
};
|
|
|
|
|
sync_text_alignment_point(t);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
"v_align" => {
|
|
|
|
|
t.vertical_alignment = match value {
|
|
|
|
|
"Baseline" => VA::Baseline,
|
|
|
|
|
"Bottom" => VA::Bottom,
|
|
|
|
|
"Middle" => VA::Middle,
|
|
|
|
|
"Top" => VA::Top,
|
|
|
|
|
_ => return,
|
|
|
|
|
};
|
|
|
|
|
sync_text_alignment_point(t);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
let Some(v) = parse_f64(value) else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
match field {
|
|
|
|
|
"ins_x" => t.insertion_point.x = v,
|
|
|
|
|
"ins_y" => t.insertion_point.y = v,
|
|
|
|
|
"ins_z" => t.insertion_point.z = v,
|
|
|
|
|
"height" if v > 0.0 => t.height = v,
|
|
|
|
|
"rotation" => t.rotation = v.to_radians(),
|
|
|
|
|
"width_factor" if v > 0.0 => t.width_factor = v,
|
|
|
|
|
"oblique_angle" => t.oblique_angle = v.to_radians(),
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_grip(t: &mut Text, _grip_id: usize, apply: GripApply) {
|
|
|
|
|
match apply {
|
|
|
|
|
GripApply::Absolute(p) => {
|
|
|
|
|
t.insertion_point.x = p.x as f64;
|
|
|
|
|
t.insertion_point.y = p.y as f64;
|
|
|
|
|
t.insertion_point.z = p.z as f64;
|
|
|
|
|
}
|
|
|
|
|
GripApply::Translate(d) => {
|
|
|
|
|
t.insertion_point.x += d.x as f64;
|
|
|
|
|
t.insertion_point.y += d.y as f64;
|
|
|
|
|
t.insertion_point.z += d.z as f64;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_transform(t: &mut Text, tr: &EntityTransform) {
|
2026-06-16 11:33:46 +03:00
|
|
|
crate::scene::view::transform::apply_standard_entity_transform(t, tr, |entity, p1, p2| {
|
|
|
|
|
crate::scene::view::transform::reflect_xy_point(
|
2026-03-22 14:56:27 +03:00
|
|
|
&mut entity.insertion_point.x,
|
|
|
|
|
&mut entity.insertion_point.y,
|
|
|
|
|
p1,
|
|
|
|
|
p2,
|
|
|
|
|
);
|
|
|
|
|
if let Some(ref mut a) = entity.alignment_point {
|
2026-06-16 11:33:46 +03:00
|
|
|
crate::scene::view::transform::reflect_xy_point(&mut a.x, &mut a.y, p1, p2);
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
let dx = (p2.x - p1.x) as f64;
|
|
|
|
|
let dy = (p2.y - p1.y) as f64;
|
|
|
|
|
let line_angle = dy.atan2(dx);
|
|
|
|
|
entity.rotation = 2.0 * line_angle - entity.rotation;
|
|
|
|
|
entity.oblique_angle = -entity.oblique_angle;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TruckConvertible for Text {
|
|
|
|
|
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
|
|
|
|
Some(to_truck(self, document))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Grippable for Text {
|
|
|
|
|
fn grips(&self) -> Vec<GripDef> {
|
|
|
|
|
grips(self)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
|
|
|
|
apply_grip(self, grip_id, apply);
|
|
|
|
|
}
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
|
2026-06-16 11:33:46 +03:00
|
|
|
fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
|
|
|
|
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
vec![
|
2026-06-11 00:04:37 +03:00
|
|
|
GripMenuItem {
|
|
|
|
|
label: "Stretch",
|
|
|
|
|
action: GripMenuAction::Stretch,
|
|
|
|
|
},
|
|
|
|
|
GripMenuItem {
|
|
|
|
|
label: "Move with Text",
|
|
|
|
|
action: GripMenuAction::MoveWithText,
|
|
|
|
|
},
|
|
|
|
|
GripMenuItem {
|
|
|
|
|
label: "Rotate",
|
|
|
|
|
action: GripMenuAction::RotateText,
|
|
|
|
|
},
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 11:33:46 +03:00
|
|
|
fn apply_grip_menu(&mut self, _grip_id: usize, _action: crate::scene::model::object::GripMenuAction) {
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
// Move-with-Text falls through to Stretch (single grip moves
|
|
|
|
|
// the whole text); Rotate needs a follow-up angle handled by
|
|
|
|
|
// `apply_grip_menu_value`.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn grip_menu_value_prompt(
|
|
|
|
|
&self,
|
|
|
|
|
_grip_id: usize,
|
2026-06-16 11:33:46 +03:00
|
|
|
action: crate::scene::model::object::GripMenuAction,
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
) -> Option<&'static str> {
|
2026-06-16 11:33:46 +03:00
|
|
|
use crate::scene::model::object::GripMenuAction as A;
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
match action {
|
|
|
|
|
A::RotateText => Some("Rotation (deg)"),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_grip_menu_value(
|
|
|
|
|
&mut self,
|
|
|
|
|
_grip_id: usize,
|
2026-06-16 11:33:46 +03:00
|
|
|
action: crate::scene::model::object::GripMenuAction,
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
value: f64,
|
|
|
|
|
) {
|
2026-06-16 11:33:46 +03:00
|
|
|
use crate::scene::model::object::GripMenuAction as A;
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
if matches!(action, A::RotateText) {
|
|
|
|
|
self.rotation = value.to_radians();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PropertyEditable for Text {
|
|
|
|
|
fn geometry_properties(&self, text_style_names: &[String]) -> PropSection {
|
|
|
|
|
properties(self, text_style_names)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
|
|
|
|
apply_geom_prop(self, field, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Transformable for Text {
|
|
|
|
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
|
|
|
|
apply_transform(self, t);
|
|
|
|
|
}
|
|
|
|
|
}
|
refactor(commands): per-entity MassProps + TextContent traits
Two free helpers in app/commands.rs lived as big match-on-EntityType
blocks: massprop_entity (Circle/Arc/Line/LwPolyline/Ellipse area +
perimeter + centroid, ~140 lines) and entity_text_content /
replace_entity_text (Text/MText/AttributeDefinition/AttributeEntity).
Move each to its own trait in entities/traits.rs and implement
per-entity:
MassPropsCalc — Circle, Arc, Line, LwPolyline, Ellipse
TextContent — Text, MText, AttributeDefinition, AttributeEntity
EntityTypeOps gets dispatch shims that route to the trait via the
existing macro, so callsites just call entity.mass_props() /
entity.text_content() / entity.replace_text(search, rep).
The remaining free helpers in commands.rs (entity_list_details,
entity_extra_info, entity_type_name, build_data_extraction_csv) are
single-command formatting helpers tightly coupled to LIST / EXTRACT /
text-format output. Distributing them as per-entity methods would
inflate every entity file with command-side formatting concerns
without making the command-side any cleaner — left in place.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:30:34 +03:00
|
|
|
|
|
|
|
|
impl crate::entities::traits::TextContent for acadrust::entities::Text {
|
|
|
|
|
fn text_content(&self) -> Option<String> {
|
|
|
|
|
Some(self.value.clone())
|
|
|
|
|
}
|
|
|
|
|
fn replace_text(&mut self, search: &str, rep: &str) {
|
|
|
|
|
let search_lc = search.to_lowercase();
|
|
|
|
|
if self.value.to_lowercase().contains(&search_lc) {
|
|
|
|
|
self.value = self.value.replace(search, rep);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|