forked from 1b-agent/opencadstudio-upstream
Element lines were re-derived from the style's flat offsets along the vertex miters, but the file already stores the real numbers: each vertex's per-element parameter list starts with the offset measured ALONG THE MITER — corner lengthening (1/cos of the half-angle), justification and scale baked in — followed by draw/gap toggle distances along the segment, which is how crossing multilines cut each other. Re-deriving pinched the channel at every corner (the ends looked flared) and drew full uncut lines through crossings, stacking intersecting multilines on top of each other. mline_lines now consumes the stored parameters directly (runs chain across vertices when continuous; an odd toggle count leaves the last run open to the segment end) and computes end caps from the actual per-vertex offsets. Files without parameters keep the style-offset fallback. Co-Authored-By: Claude Fable 5 <[email protected]>
443 lines
16 KiB
Rust
443 lines
16 KiB
Rust
use acadrust::entities::MLine;
|
||
|
||
use crate::command::EntityTransform;
|
||
use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip};
|
||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, 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;
|
||
|
||
/// One drawn line of a multiline: the polyline for a single style element (or
|
||
/// an end cap), tagged with the element's colour and linetype so the
|
||
/// tessellator can colour-bin and dash each one independently.
|
||
pub struct MLineLine {
|
||
pub points: Vec<[f64; 3]>,
|
||
pub color: acadrust::types::Color,
|
||
pub linetype: String,
|
||
}
|
||
|
||
/// Resolve a multiline into its per-element parallel lines in WCS.
|
||
///
|
||
/// Geometry comes from the referenced MLINESTYLE (element offsets, the
|
||
/// justification shift and the entity scale) rather than a fixed ±scale/2 guess,
|
||
/// so a custom style's offsets, colours and linetypes render the way the drawing
|
||
/// intends. Falls back to a ±0.5 two-line layout only when no MLINESTYLE can be
|
||
/// resolved (e.g. the style object is missing).
|
||
pub fn mline_lines(m: &MLine, document: &acadrust::CadDocument) -> Vec<MLineLine> {
|
||
use acadrust::entities::{MLineFlags, MLineJustification};
|
||
use acadrust::objects::ObjectType;
|
||
use acadrust::types::Color;
|
||
|
||
if m.vertices.is_empty() {
|
||
return Vec::new();
|
||
}
|
||
|
||
// MLINESTYLE lookup: prefer the hard-pointer handle, fall back to the name.
|
||
let style = m
|
||
.style_handle
|
||
.and_then(|h| match document.objects.get(&h) {
|
||
Some(ObjectType::MLineStyle(s)) => Some(s),
|
||
_ => None,
|
||
})
|
||
.or_else(|| {
|
||
document.objects.values().find_map(|o| match o {
|
||
ObjectType::MLineStyle(s) if s.name.eq_ignore_ascii_case(&m.style_name) => Some(s),
|
||
_ => None,
|
||
})
|
||
});
|
||
|
||
// (offset, colour, linetype) per element.
|
||
let elems: Vec<(f64, Color, String)> = match style {
|
||
Some(s) if !s.elements.is_empty() => s
|
||
.elements
|
||
.iter()
|
||
.map(|e| (e.offset, e.color, e.linetype.clone()))
|
||
.collect(),
|
||
_ => vec![
|
||
(0.5, Color::ByLayer, "ByLayer".to_string()),
|
||
(-0.5, Color::ByLayer, "ByLayer".to_string()),
|
||
],
|
||
};
|
||
|
||
// Justification shifts every element so the picked path runs along the top /
|
||
// centre / bottom element of the style. (Only the fallback path needs it —
|
||
// stored vertex parameters already bake justification in.)
|
||
let mut lo = f64::INFINITY;
|
||
let mut hi = f64::NEG_INFINITY;
|
||
for (o, _, _) in &elems {
|
||
lo = lo.min(*o);
|
||
hi = hi.max(*o);
|
||
}
|
||
let shift = match m.justification {
|
||
MLineJustification::Top => -hi,
|
||
MLineJustification::Bottom => -lo,
|
||
MLineJustification::Zero => 0.0,
|
||
};
|
||
|
||
let scale = m.scale_factor;
|
||
let closed = m.flags.contains(MLineFlags::CLOSED);
|
||
let n = m.vertices.len();
|
||
|
||
// The element's miter-space offset at vertex `vi`: the stored per-vertex
|
||
// parameter[0] when present — it is measured ALONG THE MITER, so corner
|
||
// vertices already carry the 1/cos(θ/2) miter lengthening and the
|
||
// justification shift (using the flat style offset instead pinched the
|
||
// channel at every corner and made the ends look flared). The style
|
||
// offset × scale is only the fallback for files without parameters.
|
||
let elem_off = |vi: usize, ei: usize| -> f64 {
|
||
m.vertices[vi]
|
||
.segments
|
||
.get(ei)
|
||
.and_then(|sg| sg.parameters.first().copied())
|
||
.unwrap_or_else(|| (elems[ei].0 + shift) * scale)
|
||
};
|
||
let off_pt = |vi: usize, d: f64| -> [f64; 3] {
|
||
let v = &m.vertices[vi];
|
||
[
|
||
v.position.x + v.miter.x * d,
|
||
v.position.y + v.miter.y * d,
|
||
v.position.z + v.miter.z * d,
|
||
]
|
||
};
|
||
|
||
let mut out: Vec<MLineLine> = Vec::with_capacity(elems.len() + 2);
|
||
for (ei, (_, color, linetype)) in elems.iter().enumerate() {
|
||
let mut pts: Vec<[f64; 3]> = Vec::new();
|
||
// Walk each segment (vi → vi+1, wrapping when closed); the vertex's
|
||
// parameters[1..] are draw-toggle distances along the element line —
|
||
// this is how crossing/merged multilines store their gaps. An odd
|
||
// toggle count leaves the final run open to the segment end.
|
||
let seg_count = if closed { n } else { n.saturating_sub(1) };
|
||
let mut pen_at_end = false;
|
||
for k in 0..seg_count {
|
||
let vi = k;
|
||
let wi = (k + 1) % n;
|
||
let a = off_pt(vi, elem_off(vi, ei));
|
||
let b = off_pt(wi, elem_off(wi, ei));
|
||
let seg = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||
let len = (seg[0] * seg[0] + seg[1] * seg[1] + seg[2] * seg[2]).sqrt();
|
||
if len < 1e-12 {
|
||
continue;
|
||
}
|
||
let dirn = [seg[0] / len, seg[1] / len, seg[2] / len];
|
||
let at = |t: f64| -> [f64; 3] {
|
||
[a[0] + dirn[0] * t, a[1] + dirn[1] * t, a[2] + dirn[2] * t]
|
||
};
|
||
let toggles: &[f64] = m
|
||
.vertices[vi]
|
||
.segments
|
||
.get(ei)
|
||
.map(|sg| sg.parameters.get(1..).unwrap_or(&[]))
|
||
.unwrap_or(&[]);
|
||
// Build (start, end) runs from the toggle list.
|
||
let mut runs: Vec<(f64, f64)> = Vec::new();
|
||
if toggles.is_empty() {
|
||
runs.push((0.0, len));
|
||
} else {
|
||
let mut i = 0;
|
||
while i < toggles.len() {
|
||
let start = toggles[i].clamp(0.0, len);
|
||
let end = toggles
|
||
.get(i + 1)
|
||
.copied()
|
||
.unwrap_or(len)
|
||
.clamp(0.0, len);
|
||
if end - start > 1e-9 {
|
||
runs.push((start, end));
|
||
}
|
||
i += 2;
|
||
}
|
||
}
|
||
for (ri, (t0, t1)) in runs.iter().enumerate() {
|
||
let continuous = pen_at_end && ri == 0 && *t0 <= 1e-9 && !pts.is_empty();
|
||
if !continuous {
|
||
if !pts.is_empty() {
|
||
pts.push([f64::NAN; 3]);
|
||
}
|
||
pts.push(at(*t0));
|
||
}
|
||
pts.push(at(*t1));
|
||
}
|
||
pen_at_end = runs.last().is_some_and(|(_, t1)| (len - t1).abs() <= 1e-9);
|
||
}
|
||
if pts.len() < 2 {
|
||
continue;
|
||
}
|
||
out.push(MLineLine {
|
||
points: pts,
|
||
color: *color,
|
||
linetype: linetype.clone(),
|
||
});
|
||
}
|
||
|
||
// End caps: a segment across the full style width at the first / last vertex,
|
||
// drawn only when the style requests square caps, the style has width, and
|
||
// the multiline is open.
|
||
if let Some(s) = style {
|
||
if !closed && n >= 2 {
|
||
let mut cap = |vi: usize| {
|
||
let mut dlo = f64::INFINITY;
|
||
let mut dhi = f64::NEG_INFINITY;
|
||
for ei in 0..elems.len() {
|
||
let d = elem_off(vi, ei);
|
||
dlo = dlo.min(d);
|
||
dhi = dhi.max(d);
|
||
}
|
||
if (dhi - dlo).abs() > 1e-9 {
|
||
out.push(MLineLine {
|
||
points: vec![off_pt(vi, dlo), off_pt(vi, dhi)],
|
||
color: Color::ByLayer,
|
||
linetype: "ByLayer".to_string(),
|
||
});
|
||
}
|
||
};
|
||
if s.flags.start_square_cap {
|
||
cap(0);
|
||
}
|
||
if s.flags.end_square_cap {
|
||
cap(n - 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
out
|
||
}
|
||
|
||
impl TruckConvertible for MLine {
|
||
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||
if self.vertices.is_empty() {
|
||
return None;
|
||
}
|
||
|
||
// NaN-separated flat list of every element line (single-colour path used
|
||
// by pick and the edit commands; the coloured render is built in
|
||
// `tessellate`, which special-cases MLINE).
|
||
let lines = mline_lines(self, document);
|
||
let mut pts: Vec<[f64; 3]> = Vec::new();
|
||
for (i, l) in lines.iter().enumerate() {
|
||
if i > 0 {
|
||
pts.push([f64::NAN; 3]);
|
||
}
|
||
pts.extend_from_slice(&l.points);
|
||
}
|
||
|
||
let key_verts: Vec<[f64; 3]> = self
|
||
.vertices
|
||
.iter()
|
||
.map(|v| [v.position.x, v.position.y, v.position.z])
|
||
.collect();
|
||
|
||
let snap_pts = self
|
||
.vertices
|
||
.iter()
|
||
.map(|v| {
|
||
(
|
||
glam::DVec3::new(v.position.x, v.position.y, v.position.z),
|
||
SnapHint::Node,
|
||
)
|
||
})
|
||
.collect();
|
||
|
||
Some(TruckEntity {
|
||
pick_tris: Vec::new(),
|
||
object: TruckObject::Lines(pts),
|
||
snap_pts,
|
||
tangent_geoms: vec![],
|
||
key_vertices: key_verts,
|
||
fill_tris: vec![],
|
||
})
|
||
}
|
||
}
|
||
|
||
impl Grippable for MLine {
|
||
fn grips(&self) -> Vec<GripDef> {
|
||
self.vertices
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, v)| {
|
||
square_grip(
|
||
i,
|
||
glam::DVec3::new(v.position.x, v.position.y, v.position.z),
|
||
)
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||
if let Some(v) = self.vertices.get_mut(grip_id) {
|
||
match apply {
|
||
GripApply::Translate(d) => {
|
||
v.position.x += d.x as f64;
|
||
v.position.y += d.y as f64;
|
||
v.position.z += d.z as f64;
|
||
}
|
||
GripApply::Absolute(p) => {
|
||
v.position.x = p.x as f64;
|
||
v.position.y = p.y as f64;
|
||
v.position.z = p.z as f64;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
||
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
||
vec![
|
||
GripMenuItem {
|
||
label: "Stretch",
|
||
action: GripMenuAction::Stretch,
|
||
},
|
||
GripMenuItem {
|
||
label: "Add Vertex",
|
||
action: GripMenuAction::AddVertex,
|
||
},
|
||
GripMenuItem {
|
||
label: "Remove Vertex",
|
||
action: GripMenuAction::RemoveVertex,
|
||
},
|
||
]
|
||
}
|
||
|
||
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
||
use crate::scene::model::object::GripMenuAction as A;
|
||
let n = self.vertices.len();
|
||
match action {
|
||
A::AddVertex if grip_id < n => {
|
||
let i1 = (grip_id + 1).min(n - 1);
|
||
if i1 == grip_id {
|
||
return;
|
||
}
|
||
let v0 = &self.vertices[grip_id];
|
||
let v1 = &self.vertices[i1];
|
||
let mut new_v = v0.clone();
|
||
new_v.position.x = (v0.position.x + v1.position.x) * 0.5;
|
||
new_v.position.y = (v0.position.y + v1.position.y) * 0.5;
|
||
new_v.position.z = (v0.position.z + v1.position.z) * 0.5;
|
||
self.vertices.insert(i1, new_v);
|
||
}
|
||
A::RemoveVertex if grip_id < n && n > 2 => {
|
||
self.vertices.remove(grip_id);
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl PropertyEditable for MLine {
|
||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||
let just_str = match self.justification {
|
||
acadrust::entities::MLineJustification::Top => "Top",
|
||
acadrust::entities::MLineJustification::Zero => "Zero",
|
||
acadrust::entities::MLineJustification::Bottom => "Bottom",
|
||
};
|
||
let cur = self.vertices.first();
|
||
let cur_x = cur.map(|v| v.position.x).unwrap_or(0.0);
|
||
let cur_y = cur.map(|v| v.position.y).unwrap_or(0.0);
|
||
let cur_z = cur.map(|v| v.position.z).unwrap_or(0.0);
|
||
vec![
|
||
PropSection {
|
||
title: "Geometry".into(),
|
||
props: vec![
|
||
ro("Vertex", "ml_vertex", if self.vertices.is_empty() { String::new() } else { "1".to_string() }),
|
||
edit("Vertex X", "ml_vertex_x", cur_x),
|
||
edit("Vertex Y", "ml_vertex_y", cur_y),
|
||
edit("Vertex Z", "ml_vertex_z", cur_z),
|
||
],
|
||
},
|
||
PropSection {
|
||
title: "Misc".into(),
|
||
props: vec![
|
||
Property {
|
||
label: "Style".into(),
|
||
field: "ml_style",
|
||
value: PropValue::EditText(self.style_name.clone()),
|
||
},
|
||
Property {
|
||
label: "Style justification".into(),
|
||
field: "ml_justification",
|
||
value: PropValue::Choice {
|
||
selected: just_str.to_string(),
|
||
options: ["Top", "Zero", "Bottom"]
|
||
.into_iter()
|
||
.map(str::to_string)
|
||
.collect(),
|
||
},
|
||
},
|
||
edit("Style scale", "ml_scale", self.scale_factor),
|
||
],
|
||
},
|
||
]
|
||
}
|
||
|
||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||
match field {
|
||
"ml_closed" => {
|
||
let closed = if value == "toggle" {
|
||
!self.flags.contains(acadrust::entities::MLineFlags::CLOSED)
|
||
} else {
|
||
value == "true"
|
||
};
|
||
self.flags
|
||
.set(acadrust::entities::MLineFlags::CLOSED, closed);
|
||
return;
|
||
}
|
||
"ml_justification" => {
|
||
self.justification = match value {
|
||
"Top" => acadrust::entities::MLineJustification::Top,
|
||
"Bottom" => acadrust::entities::MLineJustification::Bottom,
|
||
_ => acadrust::entities::MLineJustification::Zero,
|
||
};
|
||
return;
|
||
}
|
||
"ml_style" => {
|
||
self.style_name = value.to_string();
|
||
return;
|
||
}
|
||
_ => {}
|
||
}
|
||
let Ok(v) = value.trim().parse::<f64>() else {
|
||
return;
|
||
};
|
||
match field {
|
||
"ml_scale" if v != 0.0 => self.scale_factor = v,
|
||
"ml_vertex_x" => {
|
||
if let Some(vx) = self.vertices.first_mut() {
|
||
vx.position.x = v;
|
||
}
|
||
}
|
||
"ml_vertex_y" => {
|
||
if let Some(vx) = self.vertices.first_mut() {
|
||
vx.position.y = v;
|
||
}
|
||
}
|
||
"ml_vertex_z" => {
|
||
if let Some(vx) = self.vertices.first_mut() {
|
||
vx.position.z = v;
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Transformable for MLine {
|
||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
|
||
for v in &mut entity.vertices {
|
||
crate::scene::view::transform::reflect_xy_point(
|
||
&mut v.position.x,
|
||
&mut v.position.y,
|
||
p1,
|
||
p2,
|
||
);
|
||
}
|
||
crate::scene::view::transform::reflect_xy_point(
|
||
&mut entity.start_point.x,
|
||
&mut entity.start_point.y,
|
||
p1,
|
||
p2,
|
||
);
|
||
});
|
||
}
|
||
}
|