feat(mline): render multilines from their MLINESTYLE + complex linetypes
Multilines were drawn with a fixed ±scale/2 two-line guess in the entity colour, ignoring the MLINESTYLE entirely, so a drawing's custom style (per- element offsets, colours and linetypes) was lost — a red/yellow multiline rendered as a white box. - mline.rs: resolve the MLINESTYLE (by style_handle, then name) and build one parallel line per element from its offset x scale + justification shift. - tessellate.rs: special-case MLINE (like Leader) to emit one WireModel per element in the element's own colour, CPU-dashing every element through apply_along so the parallel lines stay in phase. - linetypes.rs: render a drawing's OWN complex linetypes (embedded text/shape), document definition first and the bundled .lin catalog only as a fallback, so a file-defined text linetype like PLYN shows its text. - Run the dash/glyph layout in a local frame (f64 origin subtracted, re-split to double-single after) so spacing stays precise at UTM coordinates, and pass tracking = 1.0 so embedded text keeps its letter spacing. Cargo.lock pins acadrust to the MLINE->MLINESTYLE handle-link reader fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e85e2aeb78
commit
0e18b5bb33
6 changed files with 453 additions and 74 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -73,7 +73,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#a7f4741eb9dbcc8f44170eca3a5c85143b6cf432"
|
source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#ecd1ac82985cfef77af54cb37b1b50bcdb29b7c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
|
||||||
|
|
@ -7,83 +7,141 @@ use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
|
||||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||||
use crate::scene::model::wire_model::SnapHint;
|
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.
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Offset a vertex along its miter direction by `d` drawing units.
|
||||||
|
let off = |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 (offset, color, linetype) in &elems {
|
||||||
|
let d = (*offset + shift) * scale;
|
||||||
|
let mut pts: Vec<[f64; 3]> = (0..m.vertices.len()).map(|i| off(i, d)).collect();
|
||||||
|
if closed && m.vertices.len() >= 2 {
|
||||||
|
pts.push(off(0, d));
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
let d_lo = (lo + shift) * scale;
|
||||||
|
let d_hi = (hi + shift) * scale;
|
||||||
|
if (d_hi - d_lo).abs() > 1e-9 && !closed {
|
||||||
|
let mut cap = |vi: usize| {
|
||||||
|
out.push(MLineLine {
|
||||||
|
points: vec![off(vi, d_lo), off(vi, d_hi)],
|
||||||
|
color: Color::ByLayer,
|
||||||
|
linetype: "ByLayer".to_string(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if s.flags.start_square_cap {
|
||||||
|
cap(0);
|
||||||
|
}
|
||||||
|
if s.flags.end_square_cap {
|
||||||
|
cap(m.vertices.len() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
impl TruckConvertible for MLine {
|
impl TruckConvertible for MLine {
|
||||||
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||||
if self.vertices.is_empty() {
|
if self.vertices.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let n = self.vertices.len();
|
// NaN-separated flat list of every element line (single-colour path used
|
||||||
let closed = self.flags.contains(acadrust::entities::MLineFlags::CLOSED);
|
// by pick and the edit commands; the coloured render is built in
|
||||||
|
// `tessellate`, which special-cases MLINE).
|
||||||
// Spine: center line connecting all vertex positions.
|
let lines = mline_lines(self, document);
|
||||||
// Also attempt to draw parallel offset lines (±scale/2 in miter direction)
|
|
||||||
// when scale_factor is non-zero.
|
|
||||||
let scale = self.scale_factor;
|
|
||||||
|
|
||||||
let mut pts: Vec<[f64; 3]> = Vec::new();
|
let mut pts: Vec<[f64; 3]> = Vec::new();
|
||||||
|
for (i, l) in lines.iter().enumerate() {
|
||||||
// Center spine.
|
if i > 0 {
|
||||||
for v in &self.vertices {
|
|
||||||
pts.push([v.position.x, v.position.y, v.position.z]);
|
|
||||||
}
|
|
||||||
if closed && n >= 2 {
|
|
||||||
pts.push([
|
|
||||||
self.vertices[0].position.x,
|
|
||||||
self.vertices[0].position.y,
|
|
||||||
self.vertices[0].position.z,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parallel offset lines — one at +scale/2 and one at -scale/2
|
|
||||||
// along each vertex's miter direction.
|
|
||||||
if scale.abs() > 1e-6 {
|
|
||||||
let half = scale * 0.5;
|
|
||||||
for sign in [-1.0_f64, 1.0_f64] {
|
|
||||||
let offset = half * sign;
|
|
||||||
pts.push([f64::NAN; 3]);
|
pts.push([f64::NAN; 3]);
|
||||||
for v in &self.vertices {
|
|
||||||
let mx = v.miter.x;
|
|
||||||
let my = v.miter.y;
|
|
||||||
let mz = v.miter.z;
|
|
||||||
pts.push([
|
|
||||||
v.position.x + mx * offset,
|
|
||||||
v.position.y + my * offset,
|
|
||||||
v.position.z + mz * offset,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
if closed && n >= 2 {
|
|
||||||
let v0 = &self.vertices[0];
|
|
||||||
let mx = v0.miter.x;
|
|
||||||
let my = v0.miter.y;
|
|
||||||
let mz = v0.miter.z;
|
|
||||||
pts.push([
|
|
||||||
v0.position.x + mx * offset,
|
|
||||||
v0.position.y + my * offset,
|
|
||||||
v0.position.z + mz * offset,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start and end caps: perpendicular line connecting the two offset lines
|
|
||||||
// at the first and last vertex of an open MLine.
|
|
||||||
if !closed {
|
|
||||||
let cap_v = |v: &acadrust::entities::MLineVertex| {
|
|
||||||
let mx = v.miter.x;
|
|
||||||
let my = v.miter.y;
|
|
||||||
let mz = v.miter.z;
|
|
||||||
let px = v.position.x;
|
|
||||||
let py = v.position.y;
|
|
||||||
let pz = v.position.z;
|
|
||||||
[
|
|
||||||
[f64::NAN; 3],
|
|
||||||
[px + mx * (-half), py + my * (-half), pz + mz * (-half)],
|
|
||||||
[px + mx * half, py + my * half, pz + mz * half],
|
|
||||||
]
|
|
||||||
};
|
|
||||||
pts.extend_from_slice(&cap_v(&self.vertices[0]));
|
|
||||||
pts.extend_from_slice(&cap_v(&self.vertices[n - 1]));
|
|
||||||
}
|
}
|
||||||
|
pts.extend_from_slice(&l.points);
|
||||||
}
|
}
|
||||||
|
|
||||||
let key_verts: Vec<[f64; 3]> = self
|
let key_verts: Vec<[f64; 3]> = self
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
use rustc_hash::FxHashMap as HashMap;
|
use rustc_hash::FxHashMap as HashMap;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use acadrust::tables::linetype::{LineType, LineTypeElement};
|
use acadrust::tables::linetype::{LineType, LineTypeComplexContent, LineTypeElement};
|
||||||
use acadrust::{CadDocument, TableEntry};
|
use acadrust::{CadDocument, TableEntry};
|
||||||
|
|
||||||
// ── Complex linetype types ────────────────────────────────────────────────
|
// ── Complex linetype types ────────────────────────────────────────────────
|
||||||
|
|
@ -69,6 +69,112 @@ pub fn complex_lt(name: &str) -> Option<&'static ComplexLt> {
|
||||||
.get(&name.to_ascii_uppercase())
|
.get(&name.to_ascii_uppercase())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the full segment list — dashes, spaces, dots **and** embedded
|
||||||
|
/// text / shape — for a linetype defined in the loaded **document**, whether it
|
||||||
|
/// is simple or complex.
|
||||||
|
///
|
||||||
|
/// Returns `None` when the linetype is missing or continuous (no pattern). Used
|
||||||
|
/// by the MLINE renderer: every element is CPU-dashed by the same walker so the
|
||||||
|
/// parallel lines (and any embedded text) stay in phase with each other. Ordinary
|
||||||
|
/// entities keep the cheaper shader dash for simple linetypes; see
|
||||||
|
/// [`document_complex_lt`] / [`resolve_complex_lt`].
|
||||||
|
pub fn document_lt_segments(document: &CadDocument, name: &str) -> Option<ComplexLt> {
|
||||||
|
let lt = document
|
||||||
|
.line_types
|
||||||
|
.iter()
|
||||||
|
.find(|l| l.name.eq_ignore_ascii_case(name))?;
|
||||||
|
|
||||||
|
let mut segments: Vec<LtSegment> = Vec::with_capacity(lt.elements.len() + 1);
|
||||||
|
let mut has_length = false;
|
||||||
|
for e in <.elements {
|
||||||
|
// Emit the dash / space for this element FIRST, then any embedded
|
||||||
|
// text / shape — the pen advances across the element's length before the
|
||||||
|
// glyph is placed, so the text lands at the element's *end* (inside the
|
||||||
|
// gap), matching the `.lin` token order (`dash, -space, ["TEXT"], …`) and
|
||||||
|
// AutoCAD's placement. Putting the text first drops it onto the preceding
|
||||||
|
// dash, so the dash strikes through the glyphs.
|
||||||
|
let len = e.length as f32;
|
||||||
|
if len > 1e-9 {
|
||||||
|
segments.push(LtSegment::Dash(len));
|
||||||
|
has_length = true;
|
||||||
|
} else if len < -1e-9 {
|
||||||
|
segments.push(LtSegment::Space(-len));
|
||||||
|
has_length = true;
|
||||||
|
} else if e.complex.is_none() {
|
||||||
|
// A zero-length *non-complex* element is a dot; a zero-length complex
|
||||||
|
// element only contributes its text/shape (pushed below).
|
||||||
|
segments.push(LtSegment::Dot);
|
||||||
|
has_length = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(c) = &e.complex {
|
||||||
|
match &c.content {
|
||||||
|
LineTypeComplexContent::Text { text } => {
|
||||||
|
let style_name = document
|
||||||
|
.text_styles
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.handle == c.style_handle)
|
||||||
|
.map(|s| s.name.as_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
let font = crate::entities::text_support::resolve_text_style(
|
||||||
|
style_name, document,
|
||||||
|
)
|
||||||
|
.font_name;
|
||||||
|
let scale = c.scale as f32;
|
||||||
|
segments.push(LtSegment::Text {
|
||||||
|
text: text.clone(),
|
||||||
|
style: font,
|
||||||
|
x: c.offset[0] as f32,
|
||||||
|
y: c.offset[1] as f32,
|
||||||
|
scale: if scale.abs() < 1e-6 { 1.0 } else { scale },
|
||||||
|
rot_deg: c.rotation as f32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Embedded SHAPE glyphs need a .shp set we don't ship; skip the
|
||||||
|
// draw (matches the bundled-catalog behaviour).
|
||||||
|
LineTypeComplexContent::Shape { .. } => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No dash/space/dot means "continuous" — let the caller draw a solid line.
|
||||||
|
if !has_length {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(ComplexLt { segments })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complex (embedded text/shape) segments for a **document** linetype, or `None`
|
||||||
|
/// when it is simple (dash-only) — simple dashes are handled by the ordinary
|
||||||
|
/// `resolve_pattern` shader path, so ordinary entities don't pay the CPU-dash
|
||||||
|
/// cost. This is the gate used by [`resolve_complex_lt`].
|
||||||
|
pub fn document_complex_lt(document: &CadDocument, name: &str) -> Option<ComplexLt> {
|
||||||
|
let lt = document
|
||||||
|
.line_types
|
||||||
|
.iter()
|
||||||
|
.find(|l| l.name.eq_ignore_ascii_case(name))?;
|
||||||
|
if !lt.elements.iter().any(|e| e.complex.is_some()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
document_lt_segments(document, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a complex linetype honouring "**document first, bundled catalog
|
||||||
|
/// second**": if the drawing defines the linetype, its own definition wins
|
||||||
|
/// (embedded text/shape, or `None` when the drawing's version is simple); only a
|
||||||
|
/// linetype absent from the drawing falls back to the bundled `.lin` catalog.
|
||||||
|
pub fn resolve_complex_lt(document: &CadDocument, name: &str) -> Option<ComplexLt> {
|
||||||
|
let in_document = document
|
||||||
|
.line_types
|
||||||
|
.iter()
|
||||||
|
.any(|l| l.name.eq_ignore_ascii_case(name));
|
||||||
|
if in_document {
|
||||||
|
document_complex_lt(document, name)
|
||||||
|
} else {
|
||||||
|
complex_lt(name).cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const LIN_SOURCE: &str = include_str!("../../assets/linetypes/OpenCADStudio.lin");
|
const LIN_SOURCE: &str = include_str!("../../assets/linetypes/OpenCADStudio.lin");
|
||||||
|
|
||||||
// ── Pattern art extraction ────────────────────────────────────────────────
|
// ── Pattern art extraction ────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -682,12 +682,12 @@ pub(crate) fn tessellate_entity(
|
||||||
// linetype assigned, so we only consult the first wire here — multi-wire
|
// linetype assigned, so we only consult the first wire here — multi-wire
|
||||||
// returns come exclusively from MTEXT colour splits which can't trigger
|
// returns come exclusively from MTEXT colour splits which can't trigger
|
||||||
// this path.
|
// this path.
|
||||||
if let Some(clt) = crate::io::linetypes::complex_lt(lt_name) {
|
if let Some(clt) = crate::io::linetypes::resolve_complex_lt(document, lt_name) {
|
||||||
if let Some(base) = bases.first() {
|
if let Some(base) = bases.first() {
|
||||||
let mut wires = text::complex_lt::apply_along(
|
let mut wires = text::complex_lt::apply_along(
|
||||||
&base.name,
|
&base.name,
|
||||||
&base.points,
|
&base.points,
|
||||||
clt,
|
&clt,
|
||||||
(lt_scale * pslt_factor).max(1e-4),
|
(lt_scale * pslt_factor).max(1e-4),
|
||||||
entity_color,
|
entity_color,
|
||||||
sel,
|
sel,
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,52 @@ fn points_to_ds(
|
||||||
(high, low)
|
(high, low)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lift a WireModel built in a local frame (a fixed f64 origin subtracted) back
|
||||||
|
/// to absolute world coordinates, re-splitting every position — polyline points
|
||||||
|
/// and SDF glyph vertices — into double-single so it stays precise at UTM scale.
|
||||||
|
/// The MLINE complex-linetype path uses this because `apply_along` lays out its
|
||||||
|
/// dashes and glyphs in f32, which would otherwise quantise fine spacing far
|
||||||
|
/// from the origin.
|
||||||
|
fn shift_wire_to_world(w: &mut WireModel, origin: [f64; 3]) {
|
||||||
|
if !w.points.is_empty() {
|
||||||
|
let mut hi = Vec::with_capacity(w.points.len());
|
||||||
|
let mut lo = Vec::with_capacity(w.points.len());
|
||||||
|
for p in &w.points {
|
||||||
|
if p[0].is_nan() {
|
||||||
|
hi.push([f32::NAN; 3]);
|
||||||
|
lo.push([0.0; 3]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (h, l) = split_ds_xyz(
|
||||||
|
p[0] as f64 + origin[0],
|
||||||
|
p[1] as f64 + origin[1],
|
||||||
|
p[2] as f64 + origin[2],
|
||||||
|
);
|
||||||
|
hi.push(h);
|
||||||
|
lo.push(l);
|
||||||
|
}
|
||||||
|
w.points = hi;
|
||||||
|
w.points_low = lo;
|
||||||
|
}
|
||||||
|
for tv in &mut w.text_verts {
|
||||||
|
let (h, l) = split_ds_xyz(
|
||||||
|
tv.pos[0] as f64 + tv.pos_low[0] as f64 + origin[0],
|
||||||
|
tv.pos[1] as f64 + tv.pos_low[1] as f64 + origin[1],
|
||||||
|
tv.pos[2] as f64 + tv.pos_low[2] as f64 + origin[2],
|
||||||
|
);
|
||||||
|
tv.pos = h;
|
||||||
|
tv.pos_low = l;
|
||||||
|
}
|
||||||
|
if w.aabb != WireModel::UNBOUNDED_AABB {
|
||||||
|
w.aabb = [
|
||||||
|
w.aabb[0] + origin[0] as f32,
|
||||||
|
w.aabb[1] + origin[1] as f32,
|
||||||
|
w.aabb[2] + origin[0] as f32,
|
||||||
|
w.aabb[3] + origin[1] as f32,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Public entry points ────────────────────────────────────────────────────
|
// ── Public entry points ────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Tessellate one entity into a WireModel.
|
/// Tessellate one entity into a WireModel.
|
||||||
|
|
@ -139,6 +185,172 @@ pub fn tessellate(
|
||||||
)];
|
)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MLINE emits one WireModel per style element so each parallel line keeps
|
||||||
|
// its own colour and linetype — a red Continuous line under a yellow dashed
|
||||||
|
// line reads as the two-tone multiline the style defines. Handled here, like
|
||||||
|
// Leader, because the single-colour truck `Lines` path can't carry
|
||||||
|
// per-element colour.
|
||||||
|
if let EntityType::MLine(m) = entity {
|
||||||
|
let lines = crate::entities::mline::mline_lines(m, document);
|
||||||
|
if lines.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let lt_scale =
|
||||||
|
document.header.linetype_scale as f32 * m.common.linetype_scale as f32;
|
||||||
|
let snap_pts: Vec<(glam::DVec3, SnapHint)> = m
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.map(|v| {
|
||||||
|
(
|
||||||
|
glam::DVec3::new(v.position.x, v.position.y, v.position.z),
|
||||||
|
SnapHint::Node,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let key_vertices: Vec<[f64; 3]> = m
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.map(|v| [v.position.x, v.position.y, v.position.z])
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Local-frame origin (mline start) for the CPU-dashed / glyph-laid
|
||||||
|
// elements. `apply_along` walks positions in f32, which quantises fine
|
||||||
|
// spacing — dash gaps AND inter-glyph advance — at UTM coordinates
|
||||||
|
// (the low half of the double-single is dropped). Subtracting this f64
|
||||||
|
// origin first keeps the walk near zero and precise; the result is
|
||||||
|
// shifted back to absolute double-single afterwards. Mirrors the
|
||||||
|
// Tolerance frame, which also builds geometry locally and applies its
|
||||||
|
// f64 origin later.
|
||||||
|
let origin = [
|
||||||
|
m.vertices[0].position.x,
|
||||||
|
m.vertices[0].position.y,
|
||||||
|
m.vertices[0].position.z,
|
||||||
|
];
|
||||||
|
let mut out: Vec<WireModel> = Vec::with_capacity(lines.len());
|
||||||
|
let mut snap_attached = false;
|
||||||
|
for l in lines {
|
||||||
|
if l.points.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Element colour: ByLayer / ByBlock inherit the entity's resolved
|
||||||
|
// colour; an explicit ACI / true-colour is used as-is.
|
||||||
|
let wcolor = if selected {
|
||||||
|
WireModel::SELECTED
|
||||||
|
} else {
|
||||||
|
match l.color {
|
||||||
|
AcadColor::ByLayer | AcadColor::ByBlock => entity_color,
|
||||||
|
other => {
|
||||||
|
let [r, g, b, _] =
|
||||||
|
crate::scene::convert::tess_util::aci_to_rgba(&other);
|
||||||
|
[r, g, b, entity_color[3]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let aci = match l.color {
|
||||||
|
AcadColor::Index(i) => i,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Every dashed element is CPU-dashed by `apply_along` (not the GPU
|
||||||
|
// pattern) so all parallel lines walk the polyline with the *same*
|
||||||
|
// arithmetic and stay in phase — otherwise a shader-dashed line and an
|
||||||
|
// apply_along-dashed sibling drift apart at large (UTM) coordinates and
|
||||||
|
// one line's dash lands in the other's gap, striking through embedded
|
||||||
|
// text. Document definition wins over the bundled catalog; a
|
||||||
|
// continuous element yields `None` and falls to the solid path below.
|
||||||
|
// Selection forces a plain solid highlight, so skip dashing then.
|
||||||
|
let clt = if selected {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
crate::io::linetypes::document_lt_segments(document, &l.linetype)
|
||||||
|
.or_else(|| crate::io::linetypes::complex_lt(&l.linetype).cloned())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut elem_wires: Vec<WireModel> = if let Some(clt) = clt {
|
||||||
|
// Walk the dash / glyph layout in a local frame so the f32 math
|
||||||
|
// stays precise, then lift each wire back to world DS.
|
||||||
|
let local: Vec<[f32; 3]> = l
|
||||||
|
.points
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
[
|
||||||
|
(p[0] - origin[0]) as f32,
|
||||||
|
(p[1] - origin[1]) as f32,
|
||||||
|
(p[2] - origin[2]) as f32,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut w = crate::scene::text::complex_lt::apply_along(
|
||||||
|
&name,
|
||||||
|
&local,
|
||||||
|
&clt,
|
||||||
|
lt_scale.max(1e-4),
|
||||||
|
wcolor,
|
||||||
|
selected,
|
||||||
|
line_weight_px,
|
||||||
|
);
|
||||||
|
for wm in &mut w {
|
||||||
|
wm.aci = aci;
|
||||||
|
shift_wire_to_world(wm, origin);
|
||||||
|
}
|
||||||
|
w
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simple path: the linetype isn't complex, or `apply_along` bailed
|
||||||
|
// (pattern blow-up guard) and returned nothing — draw the element as a
|
||||||
|
// dashed / solid polyline so it is never lost.
|
||||||
|
if elem_wires.is_empty() {
|
||||||
|
let (pts, pts_low) = points_to_ds(l.points);
|
||||||
|
let (pattern_length, pattern) = if selected {
|
||||||
|
(0.0, [0.0; 8])
|
||||||
|
} else {
|
||||||
|
crate::scene::view::render::resolve_pattern(
|
||||||
|
&document.line_types,
|
||||||
|
&l.linetype,
|
||||||
|
lt_scale,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
elem_wires.push(WireModel {
|
||||||
|
text_verts: Vec::new(),
|
||||||
|
name: name.clone(),
|
||||||
|
points: pts,
|
||||||
|
points_low: pts_low,
|
||||||
|
color: wcolor,
|
||||||
|
selected,
|
||||||
|
pattern_length,
|
||||||
|
pattern,
|
||||||
|
line_weight_px,
|
||||||
|
snap_pts: Vec::new(),
|
||||||
|
tangent_geoms: Vec::new(),
|
||||||
|
aci,
|
||||||
|
key_vertices: Vec::new(),
|
||||||
|
aabb: WireModel::UNBOUNDED_AABB,
|
||||||
|
plinegen: false,
|
||||||
|
vp_scissor: None,
|
||||||
|
fill_tris: Vec::new(),
|
||||||
|
fill_tris_low: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap / key vertices ride the first emitted wire only (they describe
|
||||||
|
// the whole entity, not one element).
|
||||||
|
if !snap_attached {
|
||||||
|
if let Some(w0) = elem_wires.first_mut() {
|
||||||
|
w0.snap_pts = snap_pts.clone();
|
||||||
|
w0.key_vertices = key_vertices.clone();
|
||||||
|
snap_attached = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.append(&mut elem_wires);
|
||||||
|
}
|
||||||
|
if out.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Try the truck path first ───────────────────────────────────────────
|
// ── Try the truck path first ───────────────────────────────────────────
|
||||||
// Relative-PDSIZE points size their glyph from the current zoom so they
|
// Relative-PDSIZE points size their glyph from the current zoom so they
|
||||||
// stay a roughly constant on-screen size; otherwise the header-driven path.
|
// stay a roughly constant on-screen size; otherwise the header-driven path.
|
||||||
|
|
|
||||||
|
|
@ -185,9 +185,12 @@ pub fn apply_along(
|
||||||
&mut atlas,
|
&mut atlas,
|
||||||
*tx_scale,
|
*tx_scale,
|
||||||
fwd_angle,
|
fwd_angle,
|
||||||
|
1.0, // width_factor
|
||||||
|
0.0, // oblique
|
||||||
|
// tracking = 1.0 applies the font's natural
|
||||||
|
// letter_spacing; 0.0 zeroes it and cramps the glyphs
|
||||||
|
// (every other text path passes 1.0).
|
||||||
1.0,
|
1.0,
|
||||||
0.0,
|
|
||||||
0.0,
|
|
||||||
style,
|
style,
|
||||||
false,
|
false,
|
||||||
&resolved,
|
&resolved,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue