Match the entity Properties panels to the standard CAD layout and web-verified behaviour: - Text: combined Justify (horizontal x vertical) dropdown; Position and Text-alignment rows become editable/grayed by justification; drop the Annotative-scale row. - MText: combined Justify; editable Direction, Line space distance (back-solves the factor) and Line space style; Defined width/height gated on column mode. - MLeader: regrouped to match the palette - drop the Leader Structure group (fold landing rows into Leaders), gate Text vs Block on content type, full 9-value attachment label set, resolve handle-backed rows (style, text style, arrowhead, linetype) to names. - Leader: Lines & Arrows / Text / Fit are resolved from the dimension style (arrowhead, arrow size, dim-line lineweight/colour, text gap, vertical text pos, overall scale); Current Vertex navigator; combined Type; Geometry group after General; no 3D-Visualisation group. - General: rename "LT Scale" to "Linetype scale"; show ByLayer transparency by name. Add shared num_prop helper for conditionally editable numeric rows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1804 lines
72 KiB
Rust
1804 lines
72 KiB
Rust
use acadrust::entities::{
|
||
FlowDirectionType, LeaderContentType, LineSpacingStyle, MultiLeader, MultiLeaderPathType,
|
||
TextAlignmentType, TextAttachmentDirectionType, TextAttachmentType,
|
||
};
|
||
|
||
use crate::entities::text_support::{
|
||
layout_mtext, resolve_text_style, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle,
|
||
};
|
||
|
||
/// Map MLEADER's vertical attachment enum onto the shared `MTextVAnchor`
|
||
/// used by `layout_mtext`. Replaces the old `v_offset_for_attachment`
|
||
/// (which inlined the offset math); the shared pipeline now derives the
|
||
/// offset from the variant + n_lines / line_h.
|
||
pub(crate) fn mleader_v_anchor(attach: TextAttachmentType) -> MTextVAnchor {
|
||
match attach {
|
||
TextAttachmentType::TopOfTopLine => MTextVAnchor::Top,
|
||
TextAttachmentType::MiddleOfTopLine => MTextVAnchor::MiddleOfTopLine,
|
||
TextAttachmentType::MiddleOfText
|
||
| TextAttachmentType::CenterOfText
|
||
| TextAttachmentType::CenterOfTextOverline => MTextVAnchor::Middle,
|
||
TextAttachmentType::MiddleOfBottomLine => MTextVAnchor::MiddleOfBottomLine,
|
||
TextAttachmentType::BottomOfBottomLine | TextAttachmentType::BottomLine => {
|
||
MTextVAnchor::Bottom
|
||
}
|
||
TextAttachmentType::BottomOfTopLineUnderlineBottomLine
|
||
| TextAttachmentType::BottomOfTopLineUnderlineTopLine
|
||
| TextAttachmentType::BottomOfTopLineUnderlineAll => MTextVAnchor::BottomOfTopLine,
|
||
}
|
||
}
|
||
use glam::DVec3;
|
||
|
||
use crate::command::EntityTransform;
|
||
use crate::entities::common::{
|
||
center_grip, edit_prop as edit, num_prop as num_row, ro_prop as ro, square_grip, triangle_grip,
|
||
};
|
||
use crate::entities::traits::TruckConvertible;
|
||
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
|
||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||
use crate::scene::model::wire_model::{SnapHint, TangentGeom};
|
||
|
||
// ── TruckConvertible ────────────────────────────────────────────────────────
|
||
|
||
/// Catmull-Rom spline tessellation through `ctrl` points, `segs_per_span` segments each.
|
||
/// Operates in f64 so it can be applied to either WCS-direct coordinates (entity path)
|
||
/// or offset-relative coordinates (scene path) without precision loss.
|
||
pub(crate) fn catmull_rom_pts(ctrl: &[[f64; 3]], segs_per_span: u32) -> Vec<[f64; 3]> {
|
||
let n = ctrl.len();
|
||
let mut out = Vec::new();
|
||
for i in 0..n.saturating_sub(1) {
|
||
let p0 = if i == 0 { ctrl[0] } else { ctrl[i - 1] };
|
||
let p1 = ctrl[i];
|
||
let p2 = ctrl[i + 1];
|
||
let p3 = if i + 2 < n { ctrl[i + 2] } else { ctrl[n - 1] };
|
||
for j in 0..=segs_per_span {
|
||
let t = j as f64 / segs_per_span as f64;
|
||
let t2 = t * t;
|
||
let t3 = t2 * t;
|
||
let mut pt = [0.0_f64; 3];
|
||
for k in 0..3 {
|
||
pt[k] = 0.5
|
||
* ((2.0 * p1[k])
|
||
+ (-p0[k] + p2[k]) * t
|
||
+ (2.0 * p0[k] - 5.0 * p1[k] + 4.0 * p2[k] - p3[k]) * t2
|
||
+ (-p0[k] + 3.0 * p1[k] - 3.0 * p2[k] + p3[k]) * t3);
|
||
}
|
||
out.push(pt);
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||
let nan = [f64::NAN; 3];
|
||
let p3 = |v: &acadrust::types::Vector3| -> [f64; 3] { [v.x, v.y, v.z] };
|
||
|
||
let arrow_size = ml.arrowhead_size;
|
||
let draw_arrow = arrow_size > 0.0;
|
||
let invisible = ml.path_type == MultiLeaderPathType::Invisible;
|
||
|
||
let mut points: Vec<[f64; 3]> = Vec::new();
|
||
let mut tangents: Vec<TangentGeom> = Vec::new();
|
||
let mut key_verts: Vec<[f64; 3]> = Vec::new();
|
||
let mut snap_pts: Vec<(glam::DVec3, SnapHint)> = Vec::new();
|
||
let mut first = true;
|
||
|
||
let node = |arr: [f64; 3]| (glam::DVec3::new(arr[0], arr[1], arr[2]), SnapHint::Node);
|
||
|
||
// Text-side geometry, recomputed every frame so dragging the arrow or the
|
||
// text re-mirrors the whole layout. The text block is centred on its grip
|
||
// (text_location); the landing meets the text's near edge and the leader
|
||
// line ends one dogleg before that.
|
||
let text_loc = ml.context.text_location;
|
||
let leader_ref = ml
|
||
.context
|
||
.leader_roots
|
||
.first()
|
||
.and_then(|r| r.lines.first())
|
||
.and_then(|l| l.points.last())
|
||
.copied()
|
||
.or_else(|| ml.context.leader_roots.first().map(|r| r.connection_point))
|
||
.unwrap_or(text_loc);
|
||
// Reading direction of the text / landing — the stored text_direction
|
||
// (UCS X for UCS-placed leaders, world X otherwise), so the whole text-side
|
||
// layout follows the UCS instead of world horizontal.
|
||
let (tdx, tdy) = {
|
||
let td = ml.context.text_direction;
|
||
let l = (td.x * td.x + td.y * td.y).sqrt();
|
||
if l > 1e-9 {
|
||
(td.x / l, td.y / l)
|
||
} else {
|
||
let a = ml.context.text_rotation;
|
||
(a.cos(), a.sin())
|
||
}
|
||
};
|
||
// Which side of the leader the text sits on, measured along that direction.
|
||
let rel_dot = (text_loc.x - leader_ref.x) * tdx + (text_loc.y - leader_ref.y) * tdy;
|
||
let text_sign: f64 = if rel_dot >= 0.0 { 1.0 } else { -1.0 };
|
||
|
||
// Lay the text out once, up front: its width fixes where the landing meets
|
||
// the text. The same layout is reused below to emit the glyph strokes.
|
||
let text_layout =
|
||
if ml.content_type == LeaderContentType::MText && !ml.context.text_string.is_empty() {
|
||
let ctx = &ml.context;
|
||
let height = if ctx.text_height > 0.0 {
|
||
ctx.text_height as f32
|
||
} else {
|
||
ml.text_height as f32 * ml.scale_factor as f32
|
||
};
|
||
let td = ctx.text_direction;
|
||
let mut rot = if td.x.abs() > 1e-9 || td.y.abs() > 1e-9 {
|
||
(td.y as f32).atan2(td.x as f32)
|
||
} else {
|
||
ctx.text_rotation as f32
|
||
};
|
||
let style_name = ctx
|
||
.text_style_handle
|
||
.as_ref()
|
||
.and_then(|h| {
|
||
document
|
||
.text_styles
|
||
.iter()
|
||
.find(|s| s.handle == *h)
|
||
.map(|s| s.name.clone())
|
||
})
|
||
.unwrap_or_else(|| "STANDARD".to_string());
|
||
let resolved = resolve_text_style(&style_name, document);
|
||
if resolved.is_upside_down {
|
||
rot += std::f32::consts::PI;
|
||
}
|
||
Some(layout_mtext(&MTextRenderOpts {
|
||
value: &ctx.text_string,
|
||
insertion: [text_loc.x, text_loc.y, text_loc.z],
|
||
height,
|
||
rect_w: ctx.text_width as f32,
|
||
rotation: rot,
|
||
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),
|
||
line_spacing_factor: ctx.line_spacing_factor as f32,
|
||
vertical_text: false,
|
||
want_glyph_boxes: false,
|
||
}))
|
||
} else {
|
||
None
|
||
};
|
||
let dogleg = if ml.enable_landing && ml.enable_dogleg {
|
||
ml.dogleg_length.max(0.0)
|
||
} 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,
|
||
text_loc.z,
|
||
];
|
||
|
||
for root in &ml.context.leader_roots {
|
||
let cp = &root.connection_point;
|
||
let cp_f = p3(cp);
|
||
snap_pts.push(node(cp_f));
|
||
|
||
for line in &root.lines {
|
||
if line.points.is_empty() {
|
||
continue;
|
||
}
|
||
|
||
if !invisible {
|
||
if !first {
|
||
points.push(nan);
|
||
}
|
||
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))
|
||
.sqrt();
|
||
if dist > 1e-9 {
|
||
ctrl.push(landing_pt);
|
||
}
|
||
for &c in &ctrl {
|
||
key_verts.push(c);
|
||
snap_pts.push(node(c));
|
||
}
|
||
|
||
if line.path_type == MultiLeaderPathType::Spline && ctrl.len() >= 2 {
|
||
// Catmull-Rom spline through the bend points. Use the leader
|
||
// line's own path type — a spline-style MultiLeader can carry
|
||
// straight lines, and splining a straight run bows it.
|
||
let pts = catmull_rom_pts(&ctrl, 8);
|
||
for &pt in &pts {
|
||
points.push(pt);
|
||
}
|
||
} else {
|
||
for &c in &ctrl {
|
||
points.push(c);
|
||
}
|
||
}
|
||
|
||
for i in 0..ctrl.len().saturating_sub(1) {
|
||
let a = ctrl[i];
|
||
let b = ctrl[i + 1];
|
||
tangents.push(TangentGeom::Line {
|
||
p1: [a[0] as f32, a[1] as f32, a[2] as f32],
|
||
p2: [b[0] as f32, b[1] as f32, b[2] as f32],
|
||
});
|
||
}
|
||
}
|
||
|
||
// Arrowhead
|
||
if draw_arrow {
|
||
let tip = &line.points[0];
|
||
let tip_f = p3(tip);
|
||
let next = if line.points.len() >= 2 {
|
||
line.points[1]
|
||
} else {
|
||
*cp
|
||
};
|
||
let dx = next.x - tip.x;
|
||
let dy = next.y - tip.y;
|
||
let dl = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||
let (dx, dy) = (dx / dl, dy / dl);
|
||
let a = std::f64::consts::PI / 6.0;
|
||
let (s, c) = a.sin_cos();
|
||
points.push(nan);
|
||
points.push([
|
||
tip_f[0] + (dx * c - dy * s) * arrow_size,
|
||
tip_f[1] + (dx * s + dy * c) * arrow_size,
|
||
tip_f[2],
|
||
]);
|
||
points.push(tip_f);
|
||
points.push([
|
||
tip_f[0] + (dx * c + dy * s) * arrow_size,
|
||
tip_f[1] + (-dx * s + dy * c) * arrow_size,
|
||
tip_f[2],
|
||
]);
|
||
}
|
||
}
|
||
|
||
// 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]);
|
||
}
|
||
}
|
||
|
||
// Text strokes, drawn from the layout computed up front (centred on the
|
||
// text grip). The snap node is the grip itself.
|
||
let mut fill_tris = Vec::new();
|
||
if let Some(layout) = &text_layout {
|
||
snap_pts.push(node([text_loc.x, text_loc.y, text_loc.z]));
|
||
for ts in &layout.strokes {
|
||
let ox = ts.origin[0];
|
||
let oy = ts.origin[1];
|
||
for stroke in &ts.strokes {
|
||
if stroke.len() < 2 {
|
||
continue;
|
||
}
|
||
points.push(nan);
|
||
for &[x, y] in stroke {
|
||
points.push([ox + x as f64, oy + y as f64, text_loc.z]);
|
||
}
|
||
}
|
||
for &[x, y] in &ts.fill_tris {
|
||
fill_tris.push([ox + x as f64, oy + y as f64, text_loc.z]);
|
||
}
|
||
}
|
||
}
|
||
|
||
if points.is_empty() {
|
||
return None;
|
||
}
|
||
|
||
Some(TruckEntity {
|
||
object: TruckObject::Lines(points),
|
||
snap_pts,
|
||
tangent_geoms: tangents,
|
||
key_vertices: key_verts,
|
||
fill_tris,
|
||
})
|
||
}
|
||
|
||
// ── Grips ──────────────────────────────────────────────────────────────────
|
||
//
|
||
// IDs are assigned in two passes:
|
||
// 0 .. total_line_pts - 1 : square grips on every LeaderLine vertex
|
||
// total_line_pts : diamond grip on context.text_location (if MText)
|
||
|
||
/// Side the text reads toward (+1 right / -1 left) and the text box's far
|
||
/// edge (the wrap-width grip position). The box width is the explicit wrap
|
||
/// width when set, else the natural laid-out width.
|
||
fn text_box_geom(ml: &MultiLeader) -> (f64, [f64; 3]) {
|
||
let tl = ml.context.text_location;
|
||
let leader_ref = ml
|
||
.context
|
||
.leader_roots
|
||
.first()
|
||
.and_then(|r| r.lines.first())
|
||
.and_then(|l| l.points.last())
|
||
.copied()
|
||
.or_else(|| ml.context.leader_roots.first().map(|r| r.connection_point))
|
||
.unwrap_or(tl);
|
||
let sign = if tl.x >= leader_ref.x { 1.0 } else { -1.0 };
|
||
let height = if ml.context.text_height > 0.0 {
|
||
ml.context.text_height as f32
|
||
} else {
|
||
ml.text_height as f32 * ml.scale_factor as f32
|
||
};
|
||
let style = ResolvedTextStyle {
|
||
font_name: "STANDARD".to_string(),
|
||
width_factor: 1.0,
|
||
oblique_angle: 0.0,
|
||
is_backward: false,
|
||
is_upside_down: false,
|
||
};
|
||
let layout = layout_mtext(&MTextRenderOpts {
|
||
value: &ml.context.text_string,
|
||
insertion: [0.0, 0.0, 0.0],
|
||
height,
|
||
rect_w: ml.context.text_width as f32,
|
||
rotation: 0.0,
|
||
style: &style,
|
||
attach_h_anchor: if sign >= 0.0 { 0.0 } else { 1.0 },
|
||
v_anchor: MTextVAnchor::Middle,
|
||
line_spacing_factor: ml.context.line_spacing_factor as f32,
|
||
vertical_text: false,
|
||
want_glyph_boxes: false,
|
||
});
|
||
let nat_w = layout.line_widths.iter().cloned().fold(0.0_f32, f32::max) as f64;
|
||
let box_w = if ml.context.text_width > 1e-6 {
|
||
ml.context.text_width
|
||
} else {
|
||
nat_w.max(height as f64)
|
||
};
|
||
(sign, [tl.x + sign * box_w, tl.y, tl.z])
|
||
}
|
||
|
||
fn grips(ml: &MultiLeader) -> Vec<GripDef> {
|
||
let mut result: Vec<GripDef> = Vec::new();
|
||
let mut id = 0usize;
|
||
|
||
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)));
|
||
id += 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
if ml.content_type == LeaderContentType::MText {
|
||
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)));
|
||
id += 1;
|
||
let (_, far) = text_box_geom(ml);
|
||
result.push(triangle_grip(id, glam::DVec3::new(far[0], far[1], far[2])));
|
||
}
|
||
|
||
result
|
||
}
|
||
|
||
/// Sentinel grip id meaning "translate the whole multileader" — used by the
|
||
/// 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) {
|
||
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::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,
|
||
),
|
||
};
|
||
for root in &mut ml.context.leader_roots {
|
||
for line in &mut root.lines {
|
||
for p in &mut line.points {
|
||
p.x += dx;
|
||
p.y += dy;
|
||
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;
|
||
|
||
for root in &mut ml.context.leader_roots {
|
||
for line in &mut root.lines {
|
||
for p in &mut line.points {
|
||
if idx == grip_id {
|
||
match apply {
|
||
GripApply::Absolute(a) => {
|
||
p.x = a.x as f64;
|
||
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;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
idx += 1;
|
||
if idx == grip_id {
|
||
// Dragging the box edge sets the MText wrap width.
|
||
let (sign, far) = text_box_geom(ml);
|
||
let new_far_x = match apply {
|
||
GripApply::Absolute(a) => a.x as f64,
|
||
GripApply::Translate(d) => far[0] + d.x as f64,
|
||
};
|
||
let min_w = ml.text_height.max(1.0) * 0.5;
|
||
ml.context.text_width = (sign * (new_far_x - ml.context.text_location.x)).max(min_w);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Properties ─────────────────────────────────────────────────────────────
|
||
|
||
/// The nine horizontal text-attachment options, indexed 1:1 to
|
||
/// `TextAttachmentType` values 0–8 (values 9/10 are the vertical set).
|
||
const ATTACH_LABELS: [&str; 9] = [
|
||
"Top of top line",
|
||
"Middle of top line",
|
||
"Middle of text",
|
||
"Middle of bottom line",
|
||
"Bottom of bottom line",
|
||
"Bottom of top line",
|
||
"Underline bottom line",
|
||
"Underline top line",
|
||
"Underline all",
|
||
];
|
||
|
||
fn attachment_str(a: &TextAttachmentType) -> &'static str {
|
||
match a {
|
||
TextAttachmentType::TopOfTopLine => ATTACH_LABELS[0],
|
||
TextAttachmentType::MiddleOfTopLine => ATTACH_LABELS[1],
|
||
TextAttachmentType::MiddleOfText => ATTACH_LABELS[2],
|
||
TextAttachmentType::MiddleOfBottomLine => ATTACH_LABELS[3],
|
||
TextAttachmentType::BottomOfBottomLine => ATTACH_LABELS[4],
|
||
TextAttachmentType::BottomLine => ATTACH_LABELS[5],
|
||
TextAttachmentType::BottomOfTopLineUnderlineBottomLine => ATTACH_LABELS[6],
|
||
TextAttachmentType::BottomOfTopLineUnderlineTopLine => ATTACH_LABELS[7],
|
||
TextAttachmentType::BottomOfTopLineUnderlineAll => ATTACH_LABELS[8],
|
||
TextAttachmentType::CenterOfText => "Center of text",
|
||
TextAttachmentType::CenterOfTextOverline => "Center of text (overline)",
|
||
}
|
||
}
|
||
|
||
fn leader_type_str(pt: &MultiLeaderPathType) -> &'static str {
|
||
match pt {
|
||
MultiLeaderPathType::Spline => "Spline",
|
||
MultiLeaderPathType::Invisible => "None",
|
||
MultiLeaderPathType::StraightLineSegments => "Straight",
|
||
}
|
||
}
|
||
|
||
fn text_align_str(a: &TextAlignmentType) -> &'static str {
|
||
match a {
|
||
TextAlignmentType::Left => "Left",
|
||
TextAlignmentType::Center => "Center",
|
||
TextAlignmentType::Right => "Right",
|
||
}
|
||
}
|
||
|
||
fn flow_dir_str(d: &FlowDirectionType) -> &'static str {
|
||
match d {
|
||
FlowDirectionType::Horizontal => "Left to right",
|
||
FlowDirectionType::Vertical => "Top to bottom",
|
||
FlowDirectionType::ByStyle => "By style",
|
||
}
|
||
}
|
||
|
||
fn attach_dir_str(d: &TextAttachmentDirectionType) -> &'static str {
|
||
match d {
|
||
TextAttachmentDirectionType::Horizontal => "Horizontal",
|
||
TextAttachmentDirectionType::Vertical => "Vertical",
|
||
}
|
||
}
|
||
|
||
fn line_style_str(s: &LineSpacingStyle) -> &'static str {
|
||
match s {
|
||
LineSpacingStyle::Exactly => "Exactly",
|
||
_ => "At least",
|
||
}
|
||
}
|
||
|
||
fn bool_toggle(label: &str, field: &'static str, value: bool) -> Property {
|
||
Property {
|
||
label: label.into(),
|
||
field,
|
||
value: PropValue::BoolToggle { field, value },
|
||
}
|
||
}
|
||
|
||
fn choice(label: &str, field: &'static str, selected: &str, opts: &[&str]) -> Property {
|
||
Property {
|
||
label: label.into(),
|
||
field,
|
||
value: PropValue::Choice {
|
||
selected: selected.to_string(),
|
||
options: opts.iter().map(|s| s.to_string()).collect(),
|
||
},
|
||
}
|
||
}
|
||
|
||
fn hexh(h: Option<acadrust::Handle>) -> String {
|
||
match h {
|
||
Some(h) if !h.is_null() => format!("{:X}", h.value()),
|
||
_ => "(none)".to_string(),
|
||
}
|
||
}
|
||
|
||
fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
||
let ctx = &ml.context;
|
||
|
||
// ── Misc ─────────────────────────────────────────────────────────────
|
||
let misc = PropSection {
|
||
title: "Misc".into(),
|
||
props: vec![
|
||
// Overall scale is grayed when annotative (annotation scale drives sizing).
|
||
num_row(
|
||
"Overall scale",
|
||
"scale_factor",
|
||
ml.scale_factor,
|
||
!ml.enable_annotation_scale,
|
||
),
|
||
// Style name is resolved from style_handle by the panel builder (needs doc).
|
||
ro("Multileader style", "mleader_style", "Standard"),
|
||
bool_toggle(
|
||
"Annotative",
|
||
"enable_annotation_scale",
|
||
ml.enable_annotation_scale,
|
||
),
|
||
],
|
||
};
|
||
|
||
// ── Leaders ──────────────────────────────────────────────────────────
|
||
// Landing rows are folded in here: the standalone "Leader Structure" group
|
||
// is a style-dialog tab, not a palette group.
|
||
let leaders = PropSection {
|
||
title: "Leaders".into(),
|
||
props: vec![
|
||
choice(
|
||
"Leader type",
|
||
"path_type",
|
||
leader_type_str(&ml.path_type),
|
||
&["Straight", "Spline", "None"],
|
||
),
|
||
Property {
|
||
label: "Leader color".into(),
|
||
field: "line_color",
|
||
value: PropValue::ColorChoice(ml.line_color),
|
||
},
|
||
// Linetype name resolved from line_type_handle by the panel builder.
|
||
ro("Leader linetype", "line_type_handle", "ByBlock"),
|
||
Property {
|
||
label: "Leader lineweight".into(),
|
||
field: "line_weight",
|
||
value: PropValue::LwChoice(ml.line_weight),
|
||
},
|
||
// Arrowhead block name resolved by the panel builder (default "Closed filled").
|
||
ro("Arrowhead", "arrowhead_handle", "Closed filled"),
|
||
edit("Arrowhead size", "arrowhead_size", ml.arrowhead_size),
|
||
bool_toggle("Horizontal Landing", "enable_dogleg", ml.enable_dogleg),
|
||
num_row(
|
||
"Landing distance",
|
||
"landing_distance",
|
||
ml.dogleg_length,
|
||
ml.enable_dogleg,
|
||
),
|
||
bool_toggle("Leader extension", "enable_landing", ml.enable_landing),
|
||
],
|
||
};
|
||
|
||
// ── Text (shown only for MText content) ──────────────────────────────
|
||
let text = PropSection {
|
||
title: "Text".into(),
|
||
props: vec![
|
||
Property {
|
||
label: "Contents".into(),
|
||
field: "text_string",
|
||
value: PropValue::EditText(ctx.text_string.clone()),
|
||
},
|
||
// Text-style name resolved from text_style_handle by the panel builder.
|
||
ro("Text style", "text_style_handle", "Standard"),
|
||
choice(
|
||
"Justify",
|
||
"text_alignment",
|
||
text_align_str(&ml.text_alignment),
|
||
&["Left", "Center", "Right"],
|
||
),
|
||
choice(
|
||
"Direction",
|
||
"text_flow_direction",
|
||
flow_dir_str(&ctx.text_flow_direction),
|
||
&["By style", "Left to right", "Top to bottom"],
|
||
),
|
||
edit("Width", "text_width", ctx.text_width),
|
||
edit("Height", "text_height", ml.text_height),
|
||
edit("Rotation", "text_rotation", ctx.text_rotation.to_degrees()),
|
||
edit("Line space factor", "line_spacing", ctx.line_spacing_factor),
|
||
edit(
|
||
"Line space distance",
|
||
"line_space_distance",
|
||
ml.text_height * 1.666_666_666_666_667 * ctx.line_spacing_factor,
|
||
),
|
||
choice(
|
||
"Line space style",
|
||
"line_space_style",
|
||
line_style_str(&ctx.line_spacing_style),
|
||
&["At least", "Exactly"],
|
||
),
|
||
bool_toggle(
|
||
"Background mask",
|
||
"background_fill_enabled",
|
||
ctx.background_fill_enabled,
|
||
),
|
||
choice(
|
||
"Attachment type",
|
||
"text_attachment_direction",
|
||
attach_dir_str(&ml.text_attachment_direction),
|
||
&["Horizontal", "Vertical"],
|
||
),
|
||
choice(
|
||
"Left Attachment",
|
||
"text_left_attachment",
|
||
attachment_str(&ml.text_left_attachment),
|
||
&ATTACH_LABELS,
|
||
),
|
||
choice(
|
||
"Right Attachment",
|
||
"text_right_attachment",
|
||
attachment_str(&ml.text_right_attachment),
|
||
&ATTACH_LABELS,
|
||
),
|
||
edit("Landing gap", "landing_gap", ctx.landing_gap),
|
||
bool_toggle("Text frame", "text_frame", ml.text_frame),
|
||
],
|
||
};
|
||
|
||
// ── Block ────────────────────────────────────────────────────────────
|
||
let block = PropSection {
|
||
title: "Block".into(),
|
||
props: vec![
|
||
ro(
|
||
"Source block",
|
||
"block_content_handle",
|
||
hexh(ml.block_content_handle),
|
||
),
|
||
ro(
|
||
"Block connection",
|
||
"block_connection_type",
|
||
format!("{:?}", ml.block_connection_type),
|
||
),
|
||
ro(
|
||
"Block color",
|
||
"block_content_color",
|
||
format!("{:?}", ml.block_content_color),
|
||
),
|
||
ro(
|
||
"Block scale",
|
||
"block_scale",
|
||
format!(
|
||
"{:.3} × {:.3} × {:.3}",
|
||
ml.block_scale.x, ml.block_scale.y, ml.block_scale.z
|
||
),
|
||
),
|
||
edit(
|
||
"Block rotation",
|
||
"block_rotation",
|
||
ml.block_rotation.to_degrees(),
|
||
),
|
||
],
|
||
};
|
||
|
||
// Text and Block groups are mutually exclusive, keyed on the content type;
|
||
// only one is ever shown (neither for None/Tolerance).
|
||
let mut sections = vec![misc, leaders];
|
||
match ml.content_type {
|
||
LeaderContentType::MText => sections.push(text),
|
||
LeaderContentType::Block => sections.push(block),
|
||
_ => {}
|
||
}
|
||
sections
|
||
}
|
||
|
||
fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||
let f64 = |s: &str| -> Option<f64> { s.trim().parse().ok() };
|
||
|
||
match field {
|
||
"content_type" => {
|
||
ml.content_type = match value {
|
||
"Block" => LeaderContentType::Block,
|
||
"MText" => LeaderContentType::MText,
|
||
"Tolerance" => LeaderContentType::Tolerance,
|
||
_ => LeaderContentType::None,
|
||
};
|
||
}
|
||
"text_string" => ml.context.text_string = value.to_string(),
|
||
"text_height" => {
|
||
if let Some(v) = f64(value) {
|
||
ml.text_height = v;
|
||
ml.context.text_height = v;
|
||
}
|
||
}
|
||
"text_x" => {
|
||
if let Some(v) = f64(value) {
|
||
ml.context.text_location.x = v;
|
||
}
|
||
}
|
||
"text_y" => {
|
||
if let Some(v) = f64(value) {
|
||
ml.context.text_location.y = v;
|
||
}
|
||
}
|
||
"text_z" => {
|
||
if let Some(v) = f64(value) {
|
||
ml.context.text_location.z = v;
|
||
}
|
||
}
|
||
"text_frame" => {
|
||
ml.text_frame = if value == "toggle" {
|
||
!ml.text_frame
|
||
} else {
|
||
value == "true"
|
||
}
|
||
}
|
||
"path_type" => {
|
||
ml.path_type = match value {
|
||
"Spline" => MultiLeaderPathType::Spline,
|
||
"None" => MultiLeaderPathType::Invisible,
|
||
_ => MultiLeaderPathType::StraightLineSegments,
|
||
};
|
||
}
|
||
"enable_landing" => {
|
||
ml.enable_landing = if value == "toggle" {
|
||
!ml.enable_landing
|
||
} else {
|
||
value == "true"
|
||
}
|
||
}
|
||
"enable_dogleg" => {
|
||
ml.enable_dogleg = if value == "toggle" {
|
||
!ml.enable_dogleg
|
||
} else {
|
||
value == "true"
|
||
}
|
||
}
|
||
"enable_annotation_scale" => {
|
||
ml.enable_annotation_scale = if value == "toggle" {
|
||
!ml.enable_annotation_scale
|
||
} else {
|
||
value == "true"
|
||
}
|
||
}
|
||
"background_fill_enabled" => {
|
||
ml.context.background_fill_enabled = if value == "toggle" {
|
||
!ml.context.background_fill_enabled
|
||
} else {
|
||
value == "true"
|
||
}
|
||
}
|
||
"dogleg_length" => {
|
||
if let Some(v) = f64(value) {
|
||
ml.dogleg_length = v;
|
||
}
|
||
}
|
||
"arrowhead_size" => {
|
||
if let Some(v) = f64(value) {
|
||
ml.arrowhead_size = v;
|
||
}
|
||
}
|
||
"scale_factor" => {
|
||
if let Some(v) = f64(value) {
|
||
ml.scale_factor = v;
|
||
}
|
||
}
|
||
"landing_distance" => {
|
||
if let Some(v) = f64(value) {
|
||
ml.dogleg_length = v;
|
||
if let Some(root) = ml.context.leader_roots.first_mut() {
|
||
root.landing_distance = v;
|
||
}
|
||
}
|
||
}
|
||
"landing_gap" => {
|
||
if let Some(v) = f64(value) {
|
||
ml.context.landing_gap = v;
|
||
}
|
||
}
|
||
"block_rotation" => {
|
||
if let Some(v) = f64(value) {
|
||
ml.block_rotation = v.to_radians();
|
||
}
|
||
}
|
||
"conn_x" => {
|
||
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
|
||
root.connection_point.x = v;
|
||
}
|
||
}
|
||
"conn_y" => {
|
||
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
|
||
root.connection_point.y = v;
|
||
}
|
||
}
|
||
"conn_z" => {
|
||
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
|
||
root.connection_point.z = v;
|
||
}
|
||
}
|
||
"text_left_attachment" => {
|
||
ml.text_left_attachment = parse_attachment(value);
|
||
ml.context.text_left_attachment = parse_attachment(value);
|
||
}
|
||
"text_right_attachment" => {
|
||
ml.text_right_attachment = parse_attachment(value);
|
||
ml.context.text_right_attachment = parse_attachment(value);
|
||
}
|
||
"text_top_attachment" => {
|
||
ml.text_top_attachment = parse_attachment(value);
|
||
ml.context.text_top_attachment = parse_attachment(value);
|
||
}
|
||
"text_bottom_attachment" => {
|
||
ml.text_bottom_attachment = parse_attachment(value);
|
||
ml.context.text_bottom_attachment = parse_attachment(value);
|
||
}
|
||
"text_alignment" => {
|
||
ml.text_alignment = match value {
|
||
"Center" => TextAlignmentType::Center,
|
||
"Right" => TextAlignmentType::Right,
|
||
_ => TextAlignmentType::Left,
|
||
};
|
||
ml.context.text_alignment = match value {
|
||
"Center" => TextAlignmentType::Center,
|
||
"Right" => TextAlignmentType::Right,
|
||
_ => TextAlignmentType::Left,
|
||
};
|
||
}
|
||
"text_flow_direction" => {
|
||
ml.context.text_flow_direction = match value {
|
||
"Left to right" => FlowDirectionType::Horizontal,
|
||
"Top to bottom" => FlowDirectionType::Vertical,
|
||
_ => FlowDirectionType::ByStyle,
|
||
};
|
||
}
|
||
"text_attachment_direction" => {
|
||
ml.text_attachment_direction = match value {
|
||
"Vertical" => TextAttachmentDirectionType::Vertical,
|
||
_ => TextAttachmentDirectionType::Horizontal,
|
||
};
|
||
}
|
||
"line_space_style" => {
|
||
ml.context.line_spacing_style = match value {
|
||
"Exactly" => LineSpacingStyle::Exactly,
|
||
_ => LineSpacingStyle::AtLeast,
|
||
};
|
||
}
|
||
"text_width" => {
|
||
if let Some(v) = f64(value) {
|
||
ml.context.text_width = v;
|
||
}
|
||
}
|
||
"text_rotation" => {
|
||
if let Some(v) = f64(value) {
|
||
ml.context.text_rotation = v.to_radians();
|
||
}
|
||
}
|
||
"line_spacing" => {
|
||
if let Some(v) = f64(value) {
|
||
if v > 0.0 {
|
||
ml.context.line_spacing_factor = v;
|
||
}
|
||
}
|
||
}
|
||
"line_space_distance" => {
|
||
if let Some(v) = f64(value) {
|
||
let denom = ml.text_height * 1.666_666_666_666_667;
|
||
if v > 0.0 && denom > 0.0 {
|
||
ml.context.line_spacing_factor = v / denom;
|
||
}
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
fn parse_attachment(s: &str) -> TextAttachmentType {
|
||
match s {
|
||
"Top of top line" => TextAttachmentType::TopOfTopLine,
|
||
"Middle of top line" => TextAttachmentType::MiddleOfTopLine,
|
||
"Middle of bottom line" => TextAttachmentType::MiddleOfBottomLine,
|
||
"Bottom of bottom line" => TextAttachmentType::BottomOfBottomLine,
|
||
"Bottom of top line" => TextAttachmentType::BottomLine,
|
||
"Underline bottom line" => TextAttachmentType::BottomOfTopLineUnderlineBottomLine,
|
||
"Underline top line" => TextAttachmentType::BottomOfTopLineUnderlineTopLine,
|
||
"Underline all" => TextAttachmentType::BottomOfTopLineUnderlineAll,
|
||
_ => TextAttachmentType::MiddleOfText,
|
||
}
|
||
}
|
||
|
||
// ── Transform ──────────────────────────────────────────────────────────────
|
||
|
||
fn apply_transform(ml: &mut MultiLeader, t: &EntityTransform) {
|
||
crate::scene::view::transform::apply_standard_entity_transform(ml, t, |entity, p1, p2| {
|
||
// Reflect every point on the leader (line points, connection points,
|
||
// break-point endpoints) AND every direction vector that drives the
|
||
// text orientation. Without the direction reflection text would keep
|
||
// its original rotation while the leader appears mirrored.
|
||
for root in &mut entity.context.leader_roots {
|
||
for line in &mut root.lines {
|
||
for p in &mut line.points {
|
||
crate::scene::view::transform::reflect_xy_point(&mut p.x, &mut p.y, p1, p2);
|
||
}
|
||
for bp in &mut line.break_points {
|
||
crate::scene::view::transform::reflect_xy_point(
|
||
&mut bp.start_point.x,
|
||
&mut bp.start_point.y,
|
||
p1,
|
||
p2,
|
||
);
|
||
crate::scene::view::transform::reflect_xy_point(
|
||
&mut bp.end_point.x,
|
||
&mut bp.end_point.y,
|
||
p1,
|
||
p2,
|
||
);
|
||
}
|
||
}
|
||
crate::scene::view::transform::reflect_xy_point(
|
||
&mut root.connection_point.x,
|
||
&mut root.connection_point.y,
|
||
p1,
|
||
p2,
|
||
);
|
||
reflect_xy_direction(&mut root.direction.x, &mut root.direction.y, p1, p2);
|
||
for bp in &mut root.break_points {
|
||
crate::scene::view::transform::reflect_xy_point(
|
||
&mut bp.start_point.x,
|
||
&mut bp.start_point.y,
|
||
p1,
|
||
p2,
|
||
);
|
||
crate::scene::view::transform::reflect_xy_point(
|
||
&mut bp.end_point.x,
|
||
&mut bp.end_point.y,
|
||
p1,
|
||
p2,
|
||
);
|
||
}
|
||
}
|
||
crate::scene::view::transform::reflect_xy_point(
|
||
&mut entity.context.text_location.x,
|
||
&mut entity.context.text_location.y,
|
||
p1,
|
||
p2,
|
||
);
|
||
reflect_xy_direction(
|
||
&mut entity.context.text_direction.x,
|
||
&mut entity.context.text_direction.y,
|
||
p1,
|
||
p2,
|
||
);
|
||
reflect_xy_direction(
|
||
&mut entity.context.base_direction.x,
|
||
&mut entity.context.base_direction.y,
|
||
p1,
|
||
p2,
|
||
);
|
||
});
|
||
}
|
||
|
||
/// Reflect a direction (not position) vector across the mirror line p1→p2.
|
||
/// Reflecting a direction is the same as reflecting `p1 + dir` then subtracting
|
||
/// the reflection of `p1`, which simplifies to reflecting around the origin.
|
||
fn reflect_xy_direction(dx: &mut f64, dy: &mut f64, p1: DVec3, p2: DVec3) {
|
||
let zero = DVec3::ZERO;
|
||
let p2_rel = DVec3::new(p2.x - p1.x, p2.y - p1.y, 0.0);
|
||
let mut tip_x = *dx;
|
||
let mut tip_y = *dy;
|
||
crate::scene::view::transform::reflect_xy_point(&mut tip_x, &mut tip_y, zero, p2_rel);
|
||
*dx = tip_x;
|
||
*dy = tip_y;
|
||
}
|
||
|
||
// ── Trait impls ────────────────────────────────────────────────────────────
|
||
|
||
impl TruckConvertible for MultiLeader {
|
||
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||
to_truck(self, document)
|
||
}
|
||
}
|
||
|
||
impl crate::entities::traits::Grippable for MultiLeader {
|
||
fn grips(&self) -> Vec<GripDef> {
|
||
grips(self)
|
||
}
|
||
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};
|
||
let n_vertices: usize = self
|
||
.context
|
||
.leader_roots
|
||
.iter()
|
||
.flat_map(|r| r.lines.iter())
|
||
.map(|l| l.points.len())
|
||
.sum();
|
||
if self.content_type == LeaderContentType::MText && grip_id >= n_vertices {
|
||
if grip_id == n_vertices {
|
||
// Text-location grip.
|
||
vec![
|
||
GripMenuItem {
|
||
label: "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()
|
||
}
|
||
} 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,
|
||
},
|
||
]
|
||
}
|
||
}
|
||
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
||
use crate::scene::model::object::GripMenuAction as A;
|
||
// Locate the (root, line) and vertex position owning this grip id.
|
||
let mut idx = 0usize;
|
||
let mut loc: Option<(usize, usize, acadrust::types::Vector3)> = None;
|
||
'find: for (ri, root) in self.context.leader_roots.iter().enumerate() {
|
||
for (li, line) in root.lines.iter().enumerate() {
|
||
let n = line.points.len();
|
||
if grip_id < idx + n {
|
||
loc = Some((ri, li, line.points[grip_id - idx]));
|
||
break 'find;
|
||
}
|
||
idx += n;
|
||
}
|
||
}
|
||
let Some((ri, li, vpos)) = loc else { return };
|
||
match action {
|
||
A::RemoveLeader => {
|
||
let total: usize = self
|
||
.context
|
||
.leader_roots
|
||
.iter()
|
||
.map(|r| r.lines.len())
|
||
.sum();
|
||
if total > 1 {
|
||
self.context.leader_roots[ri].lines.remove(li);
|
||
if self.context.leader_roots[ri].lines.is_empty()
|
||
&& self.context.leader_roots.len() > 1
|
||
{
|
||
self.context.leader_roots.remove(ri);
|
||
}
|
||
}
|
||
}
|
||
A::AddLeader => {
|
||
// Append to the last root so the new arrow is the last grip id
|
||
// (so the caller can immediately grab it for placement). Seed it
|
||
// below the picked vertex; the user drags it to the final spot.
|
||
let _ = ri;
|
||
let off = self.text_height.max(1.0) * 4.0;
|
||
let arrow = acadrust::types::Vector3::new(vpos.x, vpos.y - off, vpos.z);
|
||
if let Some(root) = self.context.leader_roots.last_mut() {
|
||
root.create_line(vec![arrow]);
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl crate::entities::traits::PropertyEditable for MultiLeader {
|
||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||
properties(self)
|
||
}
|
||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||
apply_geom_prop(self, field, value);
|
||
}
|
||
}
|
||
|
||
impl crate::entities::traits::Transformable for MultiLeader {
|
||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||
apply_transform(self, t);
|
||
}
|
||
}
|
||
|
||
/// Per-entity tessellation entry for `MultiLeader`. Returns multiple
|
||
/// `WireModel`s (leader/dogleg + arrow fill, optional block content,
|
||
/// text strokes, frame, background fill) so each piece can carry its
|
||
/// own colour.
|
||
pub trait MultiLeaderTess {
|
||
fn tessellate(
|
||
&self,
|
||
document: &acadrust::CadDocument,
|
||
handle: acadrust::Handle,
|
||
selected: bool,
|
||
entity_color: [f32; 4],
|
||
line_weight_px: f32,
|
||
anno_scale: f32,
|
||
world_per_pixel: Option<f32>,
|
||
bg_color: [f32; 4],
|
||
) -> Vec<crate::scene::model::wire_model::WireModel>;
|
||
}
|
||
|
||
impl MultiLeaderTess for MultiLeader {
|
||
fn tessellate(
|
||
&self,
|
||
document: &acadrust::CadDocument,
|
||
handle: acadrust::Handle,
|
||
selected: bool,
|
||
entity_color: [f32; 4],
|
||
line_weight_px: f32,
|
||
anno_scale: f32,
|
||
_world_per_pixel: Option<f32>,
|
||
bg_color: [f32; 4],
|
||
) -> Vec<crate::scene::model::wire_model::WireModel> {
|
||
use crate::scene::convert::tessellate::{
|
||
append_arrow, arrow_from_block, color_or_inherit, tessellate, ArrowKind, DimGeom,
|
||
};
|
||
use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel};
|
||
use glam::Vec3;
|
||
let ml = self;
|
||
// line_color falls back to the entity colour when the MultiLeader's own
|
||
// colour is ByBlock/ByLayer; otherwise the leader uses its stored hue.
|
||
let line_color = if selected {
|
||
WireModel::SELECTED
|
||
} else {
|
||
color_or_inherit(&ml.line_color, entity_color)
|
||
};
|
||
// ml.line_weight is the leader-line weight override. Negative codes
|
||
// (ByLayer/ByBlock/Default) fall through to the entity's already-resolved
|
||
// pixel width.
|
||
let leader_lw_px = match ml.line_weight {
|
||
acadrust::types::LineWeight::Value(v) if v >= 0 => (v as f32 / 100.0) * (96.0 / 25.4),
|
||
_ => line_weight_px,
|
||
};
|
||
// ml.line_type_handle — resolve via line_types table by handle and apply
|
||
// the resulting dash pattern to the leader wire.
|
||
let lt_scale = document.header.linetype_scale as f32 * ml.common.linetype_scale as f32;
|
||
let (leader_pat_len, leader_pat) = match ml.line_type_handle {
|
||
Some(h) if !h.is_null() => {
|
||
let name = document
|
||
.line_types
|
||
.iter()
|
||
.find(|lt| lt.handle == h)
|
||
.map(|lt| lt.name.clone());
|
||
match name {
|
||
Some(n) => {
|
||
crate::scene::view::render::resolve_pattern(&document.line_types, &n, lt_scale)
|
||
}
|
||
None => (0.0, [0.0; 8]),
|
||
}
|
||
}
|
||
_ => (0.0, [0.0; 8]),
|
||
};
|
||
|
||
let name = handle.value().to_string();
|
||
let nan = [f32::NAN; 3];
|
||
let p3 = |v: &acadrust::types::Vector3| -> [f32; 3] {
|
||
[(v.x) as f32, (v.y) as f32, (v.z) as f32]
|
||
};
|
||
|
||
// ── Scaling ──────────────────────────────────────────────────────────────
|
||
// ml.scale_factor is always applied; anno_scale is only applied when the
|
||
// multileader is marked annotative.
|
||
let effective_scale = (ml.scale_factor as f32)
|
||
* if ml.enable_annotation_scale {
|
||
anno_scale
|
||
} else {
|
||
1.0
|
||
};
|
||
|
||
let arrow_size = ml.arrowhead_size as f32 * effective_scale;
|
||
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
|
||
// block (matches DIMLDRBLK on Dimension). Null handle / unknown name →
|
||
// ClosedFilled triangle.
|
||
let arrow_kind = match ml.arrowhead_handle {
|
||
Some(h) if !h.is_null() => arrow_from_block(document, h, arrow_size.max(0.001)),
|
||
_ => ArrowKind::Triangle {
|
||
size: arrow_size.max(0.001),
|
||
filled: true,
|
||
size_mul: 1.0,
|
||
},
|
||
};
|
||
|
||
// ── Leader / arrow / dogleg points ───────────────────────────────────────
|
||
let mut points: Vec<[f32; 3]> = Vec::new();
|
||
let mut key_verts: Vec<[f64; 3]> = Vec::new();
|
||
let mut snap_pts: Vec<(glam::DVec3, SnapHint)> = Vec::new();
|
||
let mut tangents: Vec<TangentGeom> = Vec::new();
|
||
let mut arrow_fill: Vec<[f32; 3]> = Vec::new();
|
||
let mut first = true;
|
||
|
||
// 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 leader_ref_w = ml
|
||
.context
|
||
.leader_roots
|
||
.first()
|
||
.and_then(|r| r.lines.first())
|
||
.and_then(|l| l.points.last())
|
||
.copied()
|
||
.or_else(|| ml.context.leader_roots.first().map(|r| r.connection_point))
|
||
.unwrap_or(text_loc_w);
|
||
let text_sign_w: f64 = if text_loc_w.x >= leader_ref_w.x {
|
||
1.0
|
||
} else {
|
||
-1.0
|
||
};
|
||
|
||
for root in &ml.context.leader_roots {
|
||
let cp = &root.connection_point;
|
||
let cp_f = p3(cp);
|
||
snap_pts.push((Vec3::from(cp_f).as_dvec3(), SnapHint::Node));
|
||
|
||
for line in &root.lines {
|
||
if line.points.is_empty() {
|
||
continue;
|
||
}
|
||
|
||
if !invisible {
|
||
if !first {
|
||
points.push(nan);
|
||
}
|
||
first = false;
|
||
|
||
let mut ctrl: Vec<[f32; 3]> = line.points.iter().map(|p| p3(p)).collect();
|
||
let last_f = *ctrl.last().unwrap_or(&cp_f);
|
||
let dist =
|
||
((last_f[0] - cp_f[0]).powi(2) + (last_f[1] - cp_f[1]).powi(2)).sqrt();
|
||
if dist > 1e-9 {
|
||
ctrl.push(cp_f);
|
||
}
|
||
for &c in &ctrl {
|
||
key_verts.push([c[0] as f64, c[1] as f64, c[2] as f64]);
|
||
snap_pts.push((Vec3::from(c).as_dvec3(), SnapHint::Node));
|
||
}
|
||
|
||
// Use the leader LINE's own path type (its stored 170 code),
|
||
// not the MultiLeader's overall/style default: a spline-style
|
||
// leader can carry straight lines, and drawing those through a
|
||
// Catmull-Rom bows an otherwise-straight two-point segment.
|
||
if line.path_type == MultiLeaderPathType::Spline && ctrl.len() >= 2 {
|
||
let ctrl_f64: Vec<[f64; 3]> = ctrl
|
||
.iter()
|
||
.map(|c| [c[0] as f64, c[1] as f64, c[2] as f64])
|
||
.collect();
|
||
for pt in catmull_rom_pts(&ctrl_f64, 8) {
|
||
points.push([pt[0] as f32, pt[1] as f32, pt[2] as f32]);
|
||
}
|
||
} else {
|
||
for &c in &ctrl {
|
||
points.push(c);
|
||
}
|
||
}
|
||
for i in 0..ctrl.len().saturating_sub(1) {
|
||
tangents.push(TangentGeom::Line {
|
||
p1: ctrl[i],
|
||
p2: ctrl[i + 1],
|
||
});
|
||
}
|
||
}
|
||
|
||
if draw_arrow {
|
||
let tip = &line.points[0];
|
||
let tip_f = p3(tip);
|
||
let next = if line.points.len() >= 2 {
|
||
line.points[1]
|
||
} else {
|
||
*cp
|
||
};
|
||
let dx = (next.x - tip.x) as f32;
|
||
let dy = (next.y - tip.y) as f32;
|
||
let dl = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||
let dir = Vec3::new(dx / dl, dy / dl, 0.0);
|
||
let tip_v = Vec3::new(tip_f[0], tip_f[1], tip_f[2]);
|
||
// Reuse the dim/leader arrow emitter so MultiLeader's arrow
|
||
// matches the block referenced by arrowhead_handle.
|
||
let mut arrow_geom = DimGeom::new();
|
||
append_arrow(&mut arrow_geom, tip_v, dir, &arrow_kind);
|
||
if !arrow_geom.dim_lines.is_empty() {
|
||
points.push(nan);
|
||
points.extend(arrow_geom.dim_lines);
|
||
}
|
||
arrow_fill.extend(arrow_geom.arrow_fill);
|
||
}
|
||
}
|
||
|
||
if ml.enable_landing && ml.enable_dogleg && ml.dogleg_length > 0.0 {
|
||
// Horizontal landing (dogleg) from the leader elbow (connection
|
||
// point) toward the text side. The stored geometry places the
|
||
// text so its near-bottom edge sits one landing_gap past this
|
||
// dogleg end, so the dogleg stops here — drawing on to
|
||
// text_location (the block's top-left insertion) would streak a
|
||
// stray line up the side of the text.
|
||
let d = ml.dogleg_length * effective_scale as f64;
|
||
let landing_end = [
|
||
(cp.x + text_sign_w * d) as f32,
|
||
(cp.y) as f32,
|
||
cp_f[2],
|
||
];
|
||
points.push(nan);
|
||
points.push(cp_f);
|
||
points.push(landing_end);
|
||
}
|
||
}
|
||
|
||
// The leader/arrow/dogleg wire goes out as a single WireModel. Text, frame,
|
||
// and background fill (each with their own color) are appended as separate
|
||
// WireModels so the renderer respects per-piece coloring.
|
||
let mut wires: Vec<WireModel> = Vec::new();
|
||
wires.push(WireModel {
|
||
text_verts: Vec::new(),
|
||
name: name.clone(),
|
||
points,
|
||
points_low: Vec::new(),
|
||
color: line_color,
|
||
selected,
|
||
aci: 0,
|
||
pattern_length: leader_pat_len,
|
||
pattern: leader_pat,
|
||
line_weight_px: leader_lw_px,
|
||
snap_pts,
|
||
tangent_geoms: tangents,
|
||
key_vertices: key_verts,
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
vp_scissor: None,
|
||
fill_tris: arrow_fill,
|
||
// fill_tris_low intentionally empty: this fill renders on the
|
||
// top-level path, where consumers (face3d_gpu, xclip) treat a short
|
||
// low half as all-zero, so it draws at f32 precision (sub-metre
|
||
// error at UTM scale) — not a crash. Follow-up: double-single-split
|
||
// via points_to_ds to match emit_wire's paired fill path.
|
||
fill_tris_low: Vec::new(),
|
||
});
|
||
|
||
// ── Block content ───────────────────────────────────────────────────────
|
||
// When content_type == Block, the MultiLeader displays a block reference
|
||
// at block_content_location with the recorded rotation/scale. Synthesize
|
||
// an Insert and explode it through the standard tessellator. The block
|
||
// resolves via block_content_handle (handle → block_record name).
|
||
if ml.content_type == LeaderContentType::Block && ml.context.has_block_contents {
|
||
let block_name = match ml.block_content_handle {
|
||
Some(h) if !h.is_null() => document
|
||
.block_records
|
||
.iter()
|
||
.find(|br| br.handle == h)
|
||
.map(|br| br.name.clone()),
|
||
_ => None,
|
||
};
|
||
if let Some(block_name) = block_name {
|
||
let block_color = if selected {
|
||
line_color
|
||
} else {
|
||
color_or_inherit(&ml.block_content_color, entity_color)
|
||
};
|
||
let mut synth_ins =
|
||
acadrust::entities::Insert::new(block_name, ml.context.block_content_location);
|
||
synth_ins.set_x_scale(ml.block_scale.x);
|
||
synth_ins.set_y_scale(ml.block_scale.y);
|
||
synth_ins.set_z_scale(ml.block_scale.z);
|
||
synth_ins.rotation = ml.block_rotation;
|
||
synth_ins.common.layer = ml.common.layer.clone();
|
||
// block_connection_type (BlockExtents vs BasePoint) chooses the
|
||
// anchor when *creating* the multileader; at render time the
|
||
// file's stored leader endpoints already encode that choice.
|
||
let _ = ml.block_connection_type;
|
||
for sub in synth_ins.explode_from_document(document) {
|
||
let normalized =
|
||
crate::modules::draw::modify::explode::normalize_insert_entity(sub);
|
||
let mut sub_wires = tessellate(
|
||
document,
|
||
handle,
|
||
&normalized,
|
||
selected,
|
||
block_color,
|
||
leader_pat_len,
|
||
leader_pat,
|
||
leader_lw_px,
|
||
1.0,
|
||
None,
|
||
bg_color,
|
||
false,
|
||
);
|
||
for w in &mut sub_wires {
|
||
w.name = name.clone();
|
||
}
|
||
wires.extend(sub_wires);
|
||
}
|
||
// Block attributes attached to the multileader — render each as
|
||
// its own attribute entity at WCS location like INSERT does.
|
||
for ba in &ml.block_attributes {
|
||
let _ = ba; // BlockAttribute carries only the value override
|
||
// string; we'd need the AttributeDefinition handle
|
||
// to materialise it as ATTRIB geometry. Skipped
|
||
// until that wiring exists.
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Text strokes / frame / background fill ──────────────────────────────
|
||
// Strip inline format codes, split / word-wrap into lines, then place each
|
||
// line according to text_attachment_point (horizontal) and
|
||
// text_left_attachment (vertical), with text_rotation/text_direction applied.
|
||
if ml.content_type == LeaderContentType::MText && !ml.context.text_string.is_empty() {
|
||
let ctx = &ml.context;
|
||
// `ctx.text_height` (when > 0) is the already-resolved WCS text height
|
||
// stored in the per-instance context — style × scale_factor × anno
|
||
// scale are all already baked in. Multiplying by `effective_scale`
|
||
// again would double-scale (e.g., a context height of 100 in a file
|
||
// with scale_factor=20 was rendering at 2000 units — 20× too big).
|
||
// Only the fallback path (file omits the context value) needs
|
||
// scale_factor + annotation scale applied.
|
||
let height = if ctx.text_height > 0.0 {
|
||
ctx.text_height as f32
|
||
} else {
|
||
ml.text_height as f32 * effective_scale
|
||
};
|
||
|
||
let ins = &ctx.text_location;
|
||
// 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.
|
||
let local_ins_x = (ins.x) as f32;
|
||
// Horizontal leaders attach at the connection point: the dogleg runs
|
||
// horizontally at connection_point.y, and the text's attachment line
|
||
// (bottom/middle/top per text_left_attachment) sits there. Anchor the
|
||
// insertion Y to that connection instead of the stored text_location.y
|
||
// so the text meets the landing exactly — the two can disagree by a
|
||
// few percent because our MText line height need not match the writer's.
|
||
let local_ins_y = match ml.text_attachment_direction {
|
||
TextAttachmentDirectionType::Horizontal => ml
|
||
.context
|
||
.leader_roots
|
||
.first()
|
||
.map(|r| r.connection_point.y as f32)
|
||
.unwrap_or(ins.y as f32),
|
||
TextAttachmentDirectionType::Vertical => ins.y as f32,
|
||
};
|
||
let z = (ins.z) as f32;
|
||
|
||
// Rotation: prefer text_direction (transforms survive rotations /
|
||
// mirrors when acadrust updates it) and fall back to text_rotation.
|
||
// ml.text_angle_type then constrains the final angle:
|
||
// - Horizontal: always 0
|
||
// - ParallelToLastLeaderLine: keep stored direction
|
||
// - Optimized: clamp to (-π/2, π/2] (flip upside-down)
|
||
// text_direction_negative finally adds π so reading goes the other way.
|
||
let td = ctx.text_direction;
|
||
let mut rot = if td.x.abs() > 1e-9 || td.y.abs() > 1e-9 {
|
||
(td.y as f32).atan2(td.x as f32)
|
||
} else {
|
||
ctx.text_rotation as f32
|
||
};
|
||
match ml.text_angle_type {
|
||
acadrust::entities::multileader::TextAngleType::Horizontal => rot = 0.0,
|
||
acadrust::entities::multileader::TextAngleType::Optimized => {
|
||
let pi = std::f32::consts::PI;
|
||
if rot > pi / 2.0 {
|
||
rot -= pi;
|
||
} else if rot <= -pi / 2.0 {
|
||
rot += pi;
|
||
}
|
||
}
|
||
acadrust::entities::multileader::TextAngleType::ParallelToLastLeaderLine => {}
|
||
}
|
||
if ml.text_direction_negative {
|
||
rot += std::f32::consts::PI;
|
||
}
|
||
|
||
// Resolve text style via handle when available, falling back to STANDARD.
|
||
let style_name = ctx
|
||
.text_style_handle
|
||
.as_ref()
|
||
.and_then(|h| {
|
||
document
|
||
.text_styles
|
||
.iter()
|
||
.find(|s| s.handle == *h)
|
||
.map(|s| s.name.clone())
|
||
})
|
||
.unwrap_or_else(|| "STANDARD".to_string());
|
||
let style = resolve_text_style(&style_name, document);
|
||
let mut rot = rot;
|
||
if style.is_upside_down {
|
||
rot += std::f32::consts::PI;
|
||
}
|
||
let (cos_r, sin_r) = (rot.cos(), rot.sin());
|
||
|
||
// Side-anchored on the leader-facing edge so the text reads
|
||
// outward; flips live with the leader/text side.
|
||
use acadrust::entities::multileader::TextAttachmentDirectionType;
|
||
let h_anchor: f32 = if text_sign_w >= 0.0 { 0.0 } else { 1.0 };
|
||
// Pick the vertical-anchor attachment based on text_attachment_direction:
|
||
// Horizontal — leader attaches left/right; use ml.text_left_attachment
|
||
// (matches the file's stored ctx.text_left_attachment).
|
||
// Vertical — leader attaches top/bottom; use ml.text_top_attachment
|
||
// or ml.text_bottom_attachment depending on which side
|
||
// the leader is coming from (chosen via the first
|
||
// root.direction.y sign).
|
||
let leader_from_top = ml
|
||
.context
|
||
.leader_roots
|
||
.first()
|
||
.map(|r| r.direction.y < 0.0)
|
||
.unwrap_or(true);
|
||
let vertical_attach = match ml.text_attachment_direction {
|
||
TextAttachmentDirectionType::Vertical => {
|
||
if leader_from_top {
|
||
ml.text_top_attachment
|
||
} else {
|
||
ml.text_bottom_attachment
|
||
}
|
||
}
|
||
TextAttachmentDirectionType::Horizontal => ctx.text_left_attachment,
|
||
};
|
||
let v_anchor = mleader_v_anchor(vertical_attach);
|
||
|
||
// Shared MText pipeline — every inline format code (`\f`, `\C`/`\c`,
|
||
// `\H`, `\W`, `\Q`, `\T`, `\A`, `\p…`, decorations, stacked
|
||
// fractions, …) reaches the stroke output. Stroke origins are
|
||
// already in offset-relative space because we pass local_ins_x/y.
|
||
let layout = layout_mtext(&MTextRenderOpts {
|
||
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,
|
||
rotation: rot,
|
||
style: &style,
|
||
attach_h_anchor: h_anchor,
|
||
v_anchor,
|
||
line_spacing_factor: ctx.line_spacing_factor as f32,
|
||
vertical_text: false,
|
||
want_glyph_boxes: false,
|
||
});
|
||
let line_widths = &layout.line_widths;
|
||
let max_line_w = line_widths.iter().cloned().fold(0.0_f32, f32::max);
|
||
let line_h = layout.line_height;
|
||
let v_offset = layout.v_offset;
|
||
let n_lines = layout.line_count.max(1) as f32;
|
||
|
||
// Resolve text color (falls back to entity color for ByLayer / ByBlock).
|
||
let text_color = if selected {
|
||
line_color
|
||
} else {
|
||
color_or_inherit(&ctx.text_color, entity_color)
|
||
};
|
||
|
||
{
|
||
// SDF text: emit glyph quads instead of strokes (crisp at every
|
||
// zoom, so no baseline/greek LOD). `layout_mtext` already placed
|
||
// each run at its final position/rotation with a `GlyphRun`, so
|
||
// this mirrors the top-level Text arm (anno = 1, origin as-is).
|
||
// Base colour stays neutral — selection / hover recolouring is
|
||
// done by the text-highlight overlay.
|
||
let neutral = color_or_inherit(&ctx.text_color, entity_color);
|
||
let mut sdf_verts: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
|
||
if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() {
|
||
for ts in &layout.strokes {
|
||
let Some(run) = &ts.run else { continue };
|
||
let quads = crate::scene::text::glyph_quads::layout_glyph_quads(
|
||
&mut atlas,
|
||
run.height,
|
||
run.rotation,
|
||
run.width_factor,
|
||
run.oblique,
|
||
run.tracking,
|
||
&run.font,
|
||
run.bold,
|
||
&run.text,
|
||
);
|
||
crate::scene::pipeline::text_gpu::push_glyph_vertices(
|
||
&mut sdf_verts,
|
||
&quads,
|
||
[ts.origin[0], ts.origin[1], z as f64],
|
||
1.0,
|
||
neutral,
|
||
0.0,
|
||
);
|
||
}
|
||
}
|
||
if !sdf_verts.is_empty() {
|
||
// Pick box from the glyph quads (f64 accumulate → f32).
|
||
let (mut nx, mut ny, mut xx, mut xy) =
|
||
(f64::MAX, f64::MAX, f64::MIN, f64::MIN);
|
||
for v in &sdf_verts {
|
||
let x = v.pos[0] as f64 + v.pos_low[0] as f64;
|
||
let y = v.pos[1] as f64 + v.pos_low[1] as f64;
|
||
nx = nx.min(x);
|
||
xx = xx.max(x);
|
||
ny = ny.min(y);
|
||
xy = xy.max(y);
|
||
}
|
||
wires.push(WireModel {
|
||
text_verts: sdf_verts,
|
||
name: name.clone(),
|
||
points: vec![],
|
||
points_low: Vec::new(),
|
||
color: neutral,
|
||
selected,
|
||
aci: 0,
|
||
pattern_length: 0.0,
|
||
pattern: [0.0; 8],
|
||
line_weight_px,
|
||
snap_pts: vec![(
|
||
glam::DVec3::new(local_ins_x as f64, local_ins_y as f64, z as f64),
|
||
SnapHint::Node,
|
||
)],
|
||
tangent_geoms: vec![],
|
||
key_vertices: vec![],
|
||
aabb: [nx as f32, ny as f32, xx as f32, xy as f32],
|
||
plinegen: true,
|
||
vp_scissor: None,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
});
|
||
}
|
||
}
|
||
|
||
// Text frame / background-fill rectangle in local frame, then rotated to WCS.
|
||
if ml.text_frame || ctx.background_fill_enabled {
|
||
// Visual gap so the frame/fill doesn't touch glyph caps.
|
||
let pad = height * 0.25;
|
||
let block_top = v_offset + height + pad;
|
||
let block_bottom = v_offset - (n_lines - 1.0) * line_h - pad;
|
||
let block_left = -max_line_w * h_anchor - pad;
|
||
let block_right = max_line_w * (1.0 - h_anchor) + pad;
|
||
let local_corners: [[f32; 2]; 4] = [
|
||
[block_left, block_bottom],
|
||
[block_right, block_bottom],
|
||
[block_right, block_top],
|
||
[block_left, block_top],
|
||
];
|
||
let wcs_corners: [[f32; 3]; 4] = std::array::from_fn(|i| {
|
||
let lx = local_corners[i][0];
|
||
let ly = local_corners[i][1];
|
||
let wx = local_ins_x + lx * cos_r - ly * sin_r;
|
||
let wy = local_ins_y + lx * sin_r + ly * cos_r;
|
||
[wx, wy, z]
|
||
});
|
||
|
||
// Background fill — emit two triangles; renders under the text strokes.
|
||
if ctx.background_fill_enabled {
|
||
let fill_color = if selected {
|
||
line_color
|
||
} else {
|
||
color_or_inherit(&ctx.background_fill_color, entity_color)
|
||
};
|
||
let fill_tris: Vec<[f32; 3]> = vec![
|
||
wcs_corners[0],
|
||
wcs_corners[1],
|
||
wcs_corners[2],
|
||
wcs_corners[0],
|
||
wcs_corners[2],
|
||
wcs_corners[3],
|
||
];
|
||
wires.push(WireModel {
|
||
text_verts: Vec::new(),
|
||
name: name.clone(),
|
||
points: vec![],
|
||
points_low: Vec::new(),
|
||
color: fill_color,
|
||
selected,
|
||
aci: 0,
|
||
pattern_length: 0.0,
|
||
pattern: [0.0; 8],
|
||
line_weight_px: 1.0,
|
||
snap_pts: vec![],
|
||
tangent_geoms: vec![],
|
||
key_vertices: vec![],
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
vp_scissor: None,
|
||
fill_tris,
|
||
fill_tris_low: Vec::new(),
|
||
});
|
||
}
|
||
|
||
// Text frame — closed rectangle, matches text color.
|
||
if ml.text_frame {
|
||
let frame_points: Vec<[f32; 3]> = vec![
|
||
wcs_corners[0],
|
||
wcs_corners[1],
|
||
wcs_corners[2],
|
||
wcs_corners[3],
|
||
wcs_corners[0],
|
||
];
|
||
wires.push(WireModel {
|
||
text_verts: Vec::new(),
|
||
name,
|
||
points: frame_points,
|
||
points_low: Vec::new(),
|
||
color: text_color,
|
||
selected,
|
||
aci: 0,
|
||
pattern_length: 0.0,
|
||
pattern: [0.0; 8],
|
||
line_weight_px,
|
||
snap_pts: vec![],
|
||
tangent_geoms: vec![],
|
||
key_vertices: vec![],
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
vp_scissor: None,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
wires
|
||
}
|
||
}
|