Complete multiline command and properties behavior

This commit is contained in:
ramox81 committed 2026-08-22 20:20:34 +03:00
1 parent 8f3ba6dd40
commit 2263168a2a
9 files changed
+876 -244

No files matched your search

+326 -59
View File
@@ -1,7 +1,7 @@
use acadrust::entities::MLine;
use crate::command::EntityTransform;
use crate::entities::common::{edit_prop as edit, square_grip};
use crate::entities::common::{edit_prop as edit, ro_prop, square_grip};
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
@@ -24,29 +24,51 @@ pub struct MLineLine {
/// 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};
pub fn resolved_mline_style<'a>(
m: &MLine,
document: &'a acadrust::CadDocument,
) -> Option<&'a acadrust::objects::MLineStyle> {
use acadrust::objects::ObjectType;
m.style_handle
.and_then(|handle| match document.objects.get(&handle) {
Some(ObjectType::MLineStyle(style)) => Some(style),
_ => None,
})
.or_else(|| {
document.objects.values().find_map(|object| match object {
ObjectType::MLineStyle(style)
if style.name.eq_ignore_ascii_case(&m.style_name) =>
{
Some(style)
}
_ => None,
})
})
}
pub fn mline_lines(m: &MLine, document: &acadrust::CadDocument) -> Vec<MLineLine> {
mline_lines_resolved(m, resolved_mline_style(m, document))
}
pub fn mline_lines_with_style(
m: &MLine,
style: &acadrust::objects::MLineStyle,
) -> Vec<MLineLine> {
mline_lines_resolved(m, Some(style))
}
fn mline_lines_resolved(
m: &MLine,
style: Option<&acadrust::objects::MLineStyle>,
) -> Vec<MLineLine> {
use acadrust::entities::{MLineFlags, MLineJustification};
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
@@ -100,6 +122,42 @@ pub fn mline_lines(m: &MLine, document: &acadrust::CadDocument) -> Vec<MLineLine
v.position.z + v.miter.z * d,
]
};
let endpoint_pt = |vi: usize, ei: usize| -> [f64; 3] {
let point = off_pt(vi, elem_off(vi, ei));
if closed || (vi != 0 && vi + 1 != n) {
return point;
}
let Some(style) = style else {
return point;
};
let angle = if vi == 0 {
style.start_angle
} else {
style.end_angle
};
let tangent = glam::DVec3::new(
m.vertices[vi].direction.x,
m.vertices[vi].direction.y,
m.vertices[vi].direction.z,
)
.normalize_or(glam::DVec3::X);
let normal = glam::DVec3::new(m.normal.x, m.normal.y, m.normal.z)
.normalize_or(glam::DVec3::Z);
let transverse = normal.cross(tangent).normalize_or(glam::DVec3::Y);
let base = glam::DVec3::new(
m.vertices[vi].position.x,
m.vertices[vi].position.y,
m.vertices[vi].position.z,
);
let current = glam::DVec3::new(point[0], point[1], point[2]);
let tangent_shift = if angle.tan().abs() > 1.0e-9 {
(current - base).dot(transverse) / angle.tan()
} else {
0.0
};
let adjusted = current + tangent * tangent_shift;
[adjusted.x, adjusted.y, adjusted.z]
};
let mut out: Vec<MLineLine> = Vec::with_capacity(elems.len() + 2);
for (ei, (_, color, linetype)) in elems.iter().enumerate() {
@@ -113,8 +171,16 @@ pub fn mline_lines(m: &MLine, document: &acadrust::CadDocument) -> Vec<MLineLine
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 a = if !closed && vi == 0 {
endpoint_pt(vi, ei)
} else {
off_pt(vi, elem_off(vi, ei))
};
let b = if !closed && wi + 1 == n {
endpoint_pt(wi, ei)
} else {
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 {
@@ -171,32 +237,105 @@ pub fn mline_lines(m: &MLine, document: &acadrust::CadDocument) -> Vec<MLineLine
});
}
// 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.
// Style-defined joints and end caps.
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);
let outer_points = |vi: usize, endpoint: bool| -> Option<([f64; 3], [f64; 3])> {
let mut order: Vec<usize> = (0..elems.len()).collect();
order.sort_by(|a, b| elem_off(vi, *a).total_cmp(&elem_off(vi, *b)));
let first = *order.first()?;
let last = *order.last()?;
let point = |ei| {
if endpoint {
endpoint_pt(vi, ei)
} else {
off_pt(vi, elem_off(vi, ei))
}
if (dhi - dlo).abs() > 1e-9 {
};
Some((point(first), point(last)))
};
if s.flags.display_joints {
let vertices: Box<dyn Iterator<Item = usize>> = if closed {
Box::new(0..n)
} else {
Box::new(1..n.saturating_sub(1))
};
for vi in vertices {
if let Some((a, b)) = outer_points(vi, false) {
out.push(MLineLine {
points: vec![off_pt(vi, dlo), off_pt(vi, dhi)],
points: vec![a, b],
color: Color::ByLayer,
linetype: "ByLayer".to_string(),
});
}
};
if s.flags.start_square_cap {
cap(0);
}
if s.flags.end_square_cap {
cap(n - 1);
}
if !closed && n >= 2 {
let start_suppressed = m.flags.contains(MLineFlags::NO_START_CAPS);
let end_suppressed = m.flags.contains(MLineFlags::NO_END_CAPS);
for (vi, start, suppressed, square, inner, round) in [
(
0,
true,
start_suppressed,
s.flags.start_square_cap,
s.flags.start_inner_arcs_cap,
s.flags.start_round_cap,
),
(
n - 1,
false,
end_suppressed,
s.flags.end_square_cap,
s.flags.end_inner_arcs_cap,
s.flags.end_round_cap,
),
] {
if suppressed {
continue;
}
let Some((a, b)) = outer_points(vi, true) else {
continue;
};
if square {
out.push(MLineLine {
points: vec![a, b],
color: Color::ByLayer,
linetype: "ByLayer".to_string(),
});
}
let direction = glam::DVec3::new(
m.vertices[vi].direction.x,
m.vertices[vi].direction.y,
m.vertices[vi].direction.z,
)
.normalize_or(glam::DVec3::X);
if round {
out.push(MLineLine {
points: semicircle_cap(a, b, direction, start),
color: Color::ByLayer,
linetype: "ByLayer".to_string(),
});
}
if inner && elems.len() > 2 {
let mut order: Vec<usize> = (0..elems.len()).collect();
order.sort_by(|left, right| {
elem_off(vi, *left).total_cmp(&elem_off(vi, *right))
});
for pair in order.windows(2) {
out.push(MLineLine {
points: semicircle_cap(
endpoint_pt(vi, pair[0]),
endpoint_pt(vi, pair[1]),
direction,
start,
),
color: Color::ByLayer,
linetype: "ByLayer".to_string(),
});
}
}
}
}
}
@@ -204,6 +343,84 @@ pub fn mline_lines(m: &MLine, document: &acadrust::CadDocument) -> Vec<MLineLine
out
}
fn semicircle_cap(
first: [f64; 3],
second: [f64; 3],
direction: glam::DVec3,
start: bool,
) -> Vec<[f64; 3]> {
let first = glam::DVec3::from_array(first);
let second = glam::DVec3::from_array(second);
let center = (first + second) * 0.5;
let transverse = (first - second) * 0.5;
let radius = transverse.length();
let outward = if start { -direction } else { direction } * radius;
(0..=24)
.map(|step| {
let angle = std::f64::consts::PI * step as f64 / 24.0;
(center + transverse * angle.cos() + outward * angle.sin()).to_array()
})
.collect()
}
pub fn mline_fill_triangles_with_style(
m: &MLine,
style: &acadrust::objects::MLineStyle,
) -> Vec<[f64; 3]> {
use acadrust::entities::MLineFlags;
if !style.flags.fill_on || m.vertices.len() < 2 || style.elements.len() < 2 {
return Vec::new();
}
let (low_index, high_index) = style
.elements
.iter()
.enumerate()
.fold((0, 0), |(low, high), (index, element)| {
let low = if element.offset < style.elements[low].offset {
index
} else {
low
};
let high = if element.offset > style.elements[high].offset {
index
} else {
high
};
(low, high)
});
let offset_point = |vertex: usize, element: usize| -> [f64; 3] {
let item = &m.vertices[vertex];
let distance = item
.segments
.get(element)
.and_then(|segment| segment.parameters.first())
.copied()
.unwrap_or(style.elements[element].offset * m.scale_factor);
[
item.position.x + item.miter.x * distance,
item.position.y + item.miter.y * distance,
item.position.z + item.miter.z * distance,
]
};
let closed = m.flags.contains(MLineFlags::CLOSED);
let segment_count = if closed {
m.vertices.len()
} else {
m.vertices.len() - 1
};
let mut triangles = Vec::with_capacity(segment_count * 6);
for vertex in 0..segment_count {
let next = (vertex + 1) % m.vertices.len();
let a = offset_point(vertex, low_index);
let b = offset_point(vertex, high_index);
let c = offset_point(next, high_index);
let d = offset_point(next, low_index);
triangles.extend([a, b, c, a, c, d]);
}
triangles
}
impl RenderConvertible for MLine {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
if self.vertices.is_empty() {
@@ -265,19 +482,22 @@ impl Grippable for MLine {
}
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;
}
let Some(vertex) = self.vertices.get(grip_id) else {
return;
};
let position = match apply {
GripApply::Translate(delta) => acadrust::types::Vector3::new(
vertex.position.x + delta.x as f64,
vertex.position.y + delta.y as f64,
vertex.position.z + delta.z as f64,
),
GripApply::Absolute(point) => {
acadrust::types::Vector3::new(point.x as f64, point.y as f64, point.z as f64)
}
};
let offsets = mline_perpendicular_offsets(self);
if self.set_vertex_position(grip_id, position) {
restore_mline_offsets(self, &offsets);
}
}
@@ -315,9 +535,16 @@ impl Grippable for MLine {
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);
let offsets = mline_perpendicular_offsets(self);
self.rebuild_geometry();
restore_mline_offsets(self, &offsets);
}
A::RemoveVertex if grip_id < n && n > 2 => {
let mut offsets = mline_perpendicular_offsets(self);
self.vertices.remove(grip_id);
offsets.remove(grip_id);
self.rebuild_geometry();
restore_mline_offsets(self, &offsets);
}
_ => {}
}
@@ -334,11 +561,7 @@ impl PropertyEditable for MLine {
vec![PropSection {
title: t!("Misc").into_owned(),
props: vec![
Property {
label: t!("Style").into_owned(),
field: "ml_style",
value: PropValue::PlainText(self.style_name.clone()),
},
ro_prop(t!("Style").as_ref(), "ml_style", self.style_name.clone()),
Property {
label: t!("Style justification").into_owned(),
field: "ml_justification",
@@ -375,10 +598,6 @@ impl PropertyEditable for MLine {
};
return;
}
"ml_style" => {
self.style_name = value.to_string();
return;
}
_ => {}
}
let Ok(v) = value.trim().parse::<f64>() else {
@@ -392,7 +611,11 @@ impl PropertyEditable for MLine {
fn set_mline_scale(mline: &mut MLine, scale: f64) {
let old = mline.scale_factor;
if !scale.is_finite() || scale == 0.0 || !old.is_finite() || old == 0.0 {
if !scale.is_finite() || !old.is_finite() {
return;
}
if old == 0.0 {
mline.scale_factor = scale;
return;
}
let ratio = scale / old;
@@ -420,6 +643,50 @@ fn set_mline_scale(mline: &mut MLine, scale: f64) {
mline.scale_factor = scale;
}
fn mline_vertex_factor(mline: &MLine, index: usize) -> f64 {
let vertex = &mline.vertices[index];
let normal = glam::DVec3::new(mline.normal.x, mline.normal.y, mline.normal.z)
.normalize_or(glam::DVec3::Z);
let direction = glam::DVec3::new(vertex.direction.x, vertex.direction.y, vertex.direction.z)
.normalize_or(glam::DVec3::X);
let miter = glam::DVec3::new(vertex.miter.x, vertex.miter.y, vertex.miter.z)
.normalize_or(glam::DVec3::Y);
miter.dot(normal.cross(direction)).abs().max(1.0e-9)
}
fn mline_perpendicular_offsets(mline: &MLine) -> Vec<Vec<Option<f64>>> {
mline
.vertices
.iter()
.enumerate()
.map(|(index, vertex)| {
let factor = mline_vertex_factor(mline, index);
vertex
.segments
.iter()
.map(|segment| segment.parameters.first().map(|value| value * factor))
.collect()
})
.collect()
}
fn restore_mline_offsets(mline: &mut MLine, offsets: &[Vec<Option<f64>>]) {
for index in 0..mline.vertices.len().min(offsets.len()) {
let factor = mline_vertex_factor(mline, index);
for (segment, offset) in mline.vertices[index]
.segments
.iter_mut()
.zip(&offsets[index])
{
if let Some(offset) = offset {
if let Some(first) = segment.parameters.first_mut() {
*first = *offset / factor;
}
}
}
}
}
impl Transformable for MLine {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {