forked from 1b-agent/opencadstudio-upstream
The codec and kernel repositories and crates are renamed opencadcodec and opencadkernel. Inside OCS they are depended on under short keys, so code reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node graph engine). File-format and protocol spellings stay as they were: the ACIS product id `acadrust`, the ACADRUST_* switches and the plugin version keys `acadrust_version` / `acadrust_source`. A button left of MCP on the command line opens a translucent node graph over the viewport, with a palette of object, input, math, logic, list, text and geometry nodes added by click or drag. An object node owns drawing entities and its rows are the Properties panel's own rows for them: the single-object page is extracted from refresh_properties as entity_property_sections, values use the control protocol's property_json, and writes go through set_property_value with a property target override, so the panel, MCP and the graph share one read/write path. A list reaching an object row makes one entity per element. Each graph action is one undo step; the canvas pans and zooms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1398 lines
48 KiB
Rust
1398 lines
48 KiB
Rust
use codec::entities::{MLine, MLineFlags, MLineSegment};
|
|
use codec::types::Vector3;
|
|
use kernel::geom2d::{intersect::line_line, Vec2};
|
|
use kernel::space::Plane;
|
|
|
|
use crate::command::EntityTransform;
|
|
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};
|
|
use crate::scene::model::wire_model::SnapHint;
|
|
use crate::t;
|
|
|
|
/// One styled line or cap of a multiline.
|
|
pub struct MLineLine {
|
|
pub points: Vec<[f64; 3]>,
|
|
pub color: codec::types::Color,
|
|
pub linetype: String,
|
|
}
|
|
|
|
pub(crate) fn rebuild_mline_geometry(mline: &mut MLine) -> bool {
|
|
let count = mline.vertices.len();
|
|
mline.flags.set(MLineFlags::HAS_VERTICES, count > 0);
|
|
let Some(first) = mline.vertices.first() else {
|
|
mline.start_point = Vector3::ZERO;
|
|
return true;
|
|
};
|
|
mline.start_point = first.position;
|
|
if count == 1 {
|
|
return true;
|
|
}
|
|
|
|
let points: Vec<[f64; 3]> = mline
|
|
.vertices
|
|
.iter()
|
|
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
|
|
.collect();
|
|
let axis = points
|
|
.windows(2)
|
|
.find_map(|pair| {
|
|
(kernel::space::Vec3::from(pair[1]) - kernel::space::Vec3::from(pair[0]))
|
|
.normalize()
|
|
.map(kernel::space::Vec3::to_array)
|
|
})
|
|
.or_else(|| {
|
|
mline.is_closed()
|
|
.then(|| {
|
|
(kernel::space::Vec3::from(points[0])
|
|
- kernel::space::Vec3::from(points[count - 1]))
|
|
.normalize()
|
|
})
|
|
.flatten()
|
|
.map(kernel::space::Vec3::to_array)
|
|
});
|
|
let Some(plane) = axis.and_then(|axis| {
|
|
Plane::orthonormal(
|
|
points[0],
|
|
axis,
|
|
[mline.normal.x, mline.normal.y, mline.normal.z],
|
|
)
|
|
}) else {
|
|
return false;
|
|
};
|
|
let Some(flat): Option<Vec<Vec2>> = points
|
|
.iter()
|
|
.map(|point| plane.project(*point).map(Vec2::from))
|
|
.collect()
|
|
else {
|
|
return false;
|
|
};
|
|
let closed = mline.is_closed() && count >= 3;
|
|
let direction_after = |index: usize| {
|
|
let attempts = if closed {
|
|
count - 1
|
|
} else {
|
|
count.saturating_sub(index + 1)
|
|
};
|
|
(1..=attempts).find_map(|step| {
|
|
let next = if closed {
|
|
(index + step) % count
|
|
} else {
|
|
index + step
|
|
};
|
|
(flat[next] - flat[index]).normalize()
|
|
})
|
|
};
|
|
let direction_before = |index: usize| {
|
|
let attempts = if closed { count - 1 } else { index };
|
|
(1..=attempts).find_map(|step| {
|
|
let previous = if closed {
|
|
(index + count - step % count) % count
|
|
} else {
|
|
index - step
|
|
};
|
|
(flat[index] - flat[previous]).normalize()
|
|
})
|
|
};
|
|
|
|
for index in 0..count {
|
|
let before = direction_before(index);
|
|
let after = direction_after(index);
|
|
let Some(direction) = after.or(before) else {
|
|
continue;
|
|
};
|
|
let miter = match (before, after) {
|
|
(Some(before), Some(after)) => {
|
|
let first_offset = flat[index] + before.perpendicular();
|
|
let second_offset = flat[index] + after.perpendicular();
|
|
line_line(
|
|
first_offset.to_array(),
|
|
before.to_array(),
|
|
second_offset.to_array(),
|
|
after.to_array(),
|
|
)
|
|
.and_then(|(at, _)| {
|
|
(first_offset + before * at - flat[index]).normalize()
|
|
})
|
|
.unwrap_or_else(|| after.perpendicular())
|
|
}
|
|
(Some(direction), None) | (None, Some(direction)) => direction.perpendicular(),
|
|
(None, None) => continue,
|
|
};
|
|
let direction = plane.vector_at(direction.to_array());
|
|
let miter = plane.vector_at(miter.to_array());
|
|
mline.vertices[index].direction = Vector3::new(direction[0], direction[1], direction[2]);
|
|
mline.vertices[index].miter = Vector3::new(miter[0], miter[1], miter[2]);
|
|
}
|
|
true
|
|
}
|
|
|
|
fn adjusted_mline_endpoint(
|
|
mline: &MLine,
|
|
style: &codec::objects::MLineStyle,
|
|
vertex: usize,
|
|
point: [f64; 3],
|
|
) -> [f64; 3] {
|
|
let angle = if vertex == 0 {
|
|
style.start_angle
|
|
} else {
|
|
style.end_angle
|
|
};
|
|
let Some(tangent) = kernel::space::Vec3::new(
|
|
mline.vertices[vertex].direction.x,
|
|
mline.vertices[vertex].direction.y,
|
|
mline.vertices[vertex].direction.z,
|
|
)
|
|
.normalize()
|
|
else {
|
|
return point;
|
|
};
|
|
let Some(normal) =
|
|
kernel::space::Vec3::new(mline.normal.x, mline.normal.y, mline.normal.z).normalize()
|
|
else {
|
|
return point;
|
|
};
|
|
let Some(transverse) = normal.cross(tangent).normalize() else {
|
|
return point;
|
|
};
|
|
let base = kernel::space::Vec3::new(
|
|
mline.vertices[vertex].position.x,
|
|
mline.vertices[vertex].position.y,
|
|
mline.vertices[vertex].position.z,
|
|
);
|
|
let current = kernel::space::Vec3::from(point);
|
|
let tangent = if angle.tan().abs() > 1.0e-9 {
|
|
tangent * ((current - base).dot(transverse) / angle.tan())
|
|
} else {
|
|
kernel::space::Vec3::ZERO
|
|
};
|
|
(current + tangent).to_array()
|
|
}
|
|
|
|
pub(crate) fn mline_drawn_ranges(parameters: &[f64], length: f64) -> Vec<(f64, f64)> {
|
|
if parameters.len() <= 1 {
|
|
return vec![(0.0, length)];
|
|
}
|
|
let mut cursor = parameters[1].clamp(0.0, length);
|
|
if parameters.len() == 2 {
|
|
return (cursor < length - 1.0e-9)
|
|
.then_some((cursor, length))
|
|
.into_iter()
|
|
.collect();
|
|
}
|
|
let mut drawn = true;
|
|
let mut ranges = Vec::new();
|
|
for distance in ¶meters[2..] {
|
|
let next = (cursor + distance.max(0.0)).clamp(0.0, length);
|
|
if drawn && next - cursor > 1.0e-9 {
|
|
ranges.push((cursor, next));
|
|
}
|
|
cursor = next;
|
|
drawn = !drawn;
|
|
}
|
|
if drawn && cursor < length - 1.0e-9 {
|
|
ranges.push((cursor, length));
|
|
}
|
|
ranges
|
|
}
|
|
|
|
fn mline_fill_ranges(parameters: &[f64], length: f64) -> Vec<(f64, f64)> {
|
|
let mut cuts: Vec<(f64, f64)> = parameters
|
|
.chunks(2)
|
|
.filter_map(|pair| {
|
|
let start = pair[0].clamp(0.0, length);
|
|
let end = pair.get(1).copied().unwrap_or(length).clamp(0.0, length);
|
|
(end - start > 1.0e-9).then_some((start, end))
|
|
})
|
|
.collect();
|
|
cuts.sort_by(|left, right| left.0.total_cmp(&right.0));
|
|
let mut drawn = Vec::new();
|
|
let mut start = 0.0;
|
|
for cut in cuts {
|
|
if cut.0 > start + 1.0e-9 {
|
|
drawn.push((start, cut.0));
|
|
}
|
|
start = start.max(cut.1);
|
|
}
|
|
if start < length - 1.0e-9 {
|
|
drawn.push((start, length));
|
|
}
|
|
drawn
|
|
}
|
|
|
|
fn mline_cut_ranges(parameters: &[f64], length: f64) -> Vec<(f64, f64)> {
|
|
parameters
|
|
.chunks(2)
|
|
.filter_map(|pair| {
|
|
let start = pair[0].clamp(0.0, length);
|
|
let end = pair.get(1).copied().unwrap_or(length).clamp(0.0, length);
|
|
(end - start > 1.0e-9).then_some((start, end))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn store_mline_drawn_ranges(
|
|
parameters: &mut Vec<f64>,
|
|
length: f64,
|
|
ranges: &[(f64, f64)],
|
|
) {
|
|
let offset = parameters.first().copied().unwrap_or(0.0);
|
|
parameters.clear();
|
|
parameters.push(offset);
|
|
let ranges = merge_mline_ranges(
|
|
ranges
|
|
.iter()
|
|
.filter_map(|range| {
|
|
let start = range.0.clamp(0.0, length);
|
|
let end = range.1.clamp(0.0, length);
|
|
(end - start > 1.0e-9).then_some((start, end))
|
|
})
|
|
.collect(),
|
|
);
|
|
if ranges.is_empty() {
|
|
parameters.extend([0.0, 0.0]);
|
|
} else if ranges.len() == 1
|
|
&& ranges[0].0 <= 1.0e-9
|
|
&& ranges[0].1 >= length - 1.0e-9
|
|
{
|
|
parameters.push(0.0);
|
|
} else {
|
|
parameters.push(ranges[0].0);
|
|
for (index, (start, end)) in ranges.iter().enumerate() {
|
|
if index > 0 {
|
|
parameters.push(start - ranges[index - 1].1);
|
|
}
|
|
parameters.push(end - start);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn store_mline_cut_ranges(parameters: &mut Vec<f64>, length: f64, ranges: &[(f64, f64)]) {
|
|
parameters.clear();
|
|
for (start, end) in ranges {
|
|
parameters.push(*start);
|
|
if *end < length - 1.0e-9 {
|
|
parameters.push(*end);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn remap_mline_ranges(
|
|
ranges: &[(f64, f64)],
|
|
source_start: f64,
|
|
source_end: f64,
|
|
target_length: f64,
|
|
) -> Vec<(f64, f64)> {
|
|
let source_length = source_end - source_start;
|
|
if source_length <= 1.0e-12 || target_length <= 1.0e-12 {
|
|
return Vec::new();
|
|
}
|
|
ranges
|
|
.iter()
|
|
.filter_map(|range| {
|
|
let start = range.0.max(source_start);
|
|
let end = range.1.min(source_end);
|
|
(end - start > 1.0e-9).then_some((
|
|
(start - source_start) / source_length * target_length,
|
|
(end - source_start) / source_length * target_length,
|
|
))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn shift_mline_ranges(
|
|
ranges: &[(f64, f64)],
|
|
source_length: f64,
|
|
shift: f64,
|
|
target_length: f64,
|
|
) -> Vec<(f64, f64)> {
|
|
ranges
|
|
.iter()
|
|
.filter_map(|range| {
|
|
let start = if range.0 <= 1.0e-9 {
|
|
0.0
|
|
} else {
|
|
(range.0 + shift).clamp(0.0, target_length)
|
|
};
|
|
let end = if range.1 >= source_length - 1.0e-9 {
|
|
target_length
|
|
} else {
|
|
(range.1 + shift).clamp(0.0, target_length)
|
|
};
|
|
(end - start > 1.0e-9).then_some((start, end))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn merge_mline_ranges(mut ranges: Vec<(f64, f64)>) -> Vec<(f64, f64)> {
|
|
ranges.sort_by(|left, right| left.0.total_cmp(&right.0));
|
|
let mut merged: Vec<(f64, f64)> = Vec::new();
|
|
for range in ranges {
|
|
if let Some(last) = merged.last_mut() {
|
|
if range.0 <= last.1 + 1.0e-9 {
|
|
last.1 = last.1.max(range.1);
|
|
continue;
|
|
}
|
|
}
|
|
merged.push(range);
|
|
}
|
|
merged
|
|
}
|
|
|
|
fn mline_element_endpoints(
|
|
mline: &MLine,
|
|
vertex: usize,
|
|
element: usize,
|
|
) -> Option<(kernel::space::Vec3, kernel::space::Vec3)> {
|
|
let next = if vertex + 1 < mline.vertices.len() {
|
|
vertex + 1
|
|
} else if mline.is_closed() {
|
|
0
|
|
} else {
|
|
return None;
|
|
};
|
|
let point = |index: usize| {
|
|
let vertex = mline.vertices.get(index)?;
|
|
let offset = vertex.segments.get(element)?.parameters.first().copied()?;
|
|
let position = kernel::space::Vec3::new(
|
|
vertex.position.x,
|
|
vertex.position.y,
|
|
vertex.position.z,
|
|
);
|
|
let miter = kernel::space::Vec3::new(vertex.miter.x, vertex.miter.y, vertex.miter.z);
|
|
Some(position + miter * offset)
|
|
};
|
|
Some((point(vertex)?, point(next)?))
|
|
}
|
|
|
|
fn snapshot_mline_segment(mline: &MLine, vertex: usize) -> Option<Vec<(MLineSegment, f64)>> {
|
|
(0..mline.style_element_count)
|
|
.map(|element| {
|
|
let endpoints = mline_element_endpoints(mline, vertex, element)?;
|
|
Some((
|
|
mline.vertices.get(vertex)?.segments.get(element)?.clone(),
|
|
endpoints.0.distance(endpoints.1),
|
|
))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn restore_shifted_mline_segment(
|
|
mline: &mut MLine,
|
|
vertex: usize,
|
|
source: Vec<(MLineSegment, f64)>,
|
|
moved_start: bool,
|
|
) {
|
|
for (element, (source, source_length)) in source.into_iter().enumerate() {
|
|
let Some(endpoints) = mline_element_endpoints(mline, vertex, element) else {
|
|
continue;
|
|
};
|
|
let target_length = endpoints.0.distance(endpoints.1);
|
|
let shift = if moved_start {
|
|
target_length - source_length
|
|
} else {
|
|
0.0
|
|
};
|
|
let drawn = shift_mline_ranges(
|
|
&mline_drawn_ranges(&source.parameters, source_length),
|
|
source_length,
|
|
shift,
|
|
target_length,
|
|
);
|
|
let cuts = shift_mline_ranges(
|
|
&mline_cut_ranges(&source.area_fill_parameters, source_length),
|
|
source_length,
|
|
shift,
|
|
target_length,
|
|
);
|
|
let Some(target) = mline
|
|
.vertices
|
|
.get_mut(vertex)
|
|
.and_then(|vertex| vertex.segments.get_mut(element))
|
|
else {
|
|
continue;
|
|
};
|
|
store_mline_drawn_ranges(&mut target.parameters, target_length, &drawn);
|
|
store_mline_cut_ranges(&mut target.area_fill_parameters, target_length, &cuts);
|
|
}
|
|
}
|
|
|
|
/// Recompute the derived MLine state after a scripted create or edit:
|
|
/// vertex direction/miter, element segment layout and the per-element miter
|
|
/// offsets. Dash/cut parameters survive an edit unless a vertex moved.
|
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
|
pub(crate) fn normalize_scripted_mline(
|
|
old: Option<&MLine>,
|
|
new: &mut MLine,
|
|
document: &codec::CadDocument,
|
|
) -> Result<(), String> {
|
|
let style = resolved_mline_style(new, document)
|
|
.ok_or_else(|| format!("MLine style {:?} does not exist", new.style_name))?;
|
|
let unchanged = old.is_some_and(|old| {
|
|
old.vertices == new.vertices
|
|
&& old.normal == new.normal
|
|
&& old.flags == new.flags
|
|
&& old.justification == new.justification
|
|
&& old.scale_factor.to_bits() == new.scale_factor.to_bits()
|
|
&& old.style_name == new.style_name
|
|
&& old.style_handle == new.style_handle
|
|
});
|
|
if unchanged {
|
|
return Ok(());
|
|
}
|
|
let moved = old.is_none_or(|old| {
|
|
old.vertices.len() != new.vertices.len()
|
|
|| old
|
|
.vertices
|
|
.iter()
|
|
.zip(&new.vertices)
|
|
.any(|(a, b)| a.position != b.position)
|
|
});
|
|
let elements = style.elements.len().max(1);
|
|
for vertex in &mut new.vertices {
|
|
if vertex.segments.is_empty() {
|
|
vertex.init_segments(elements);
|
|
} else if moved {
|
|
for segment in &mut vertex.segments {
|
|
segment.parameters.truncate(1);
|
|
segment.area_fill_parameters.clear();
|
|
}
|
|
}
|
|
}
|
|
if !rebuild_mline_geometry(new) {
|
|
return Err("MLine vertices are degenerate".into());
|
|
}
|
|
crate::modules::draw::draw::mline::sync_mline_element_parameters(new, style);
|
|
Ok(())
|
|
}
|
|
|
|
/// Resolve a multiline into styled parallel lines in WCS.
|
|
pub fn resolved_mline_style<'a>(
|
|
m: &MLine,
|
|
document: &'a codec::CadDocument,
|
|
) -> Option<&'a codec::objects::MLineStyle> {
|
|
use codec::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: &codec::CadDocument) -> Vec<MLineLine> {
|
|
mline_lines_resolved(m, resolved_mline_style(m, document))
|
|
}
|
|
|
|
pub fn mline_lines_with_style(
|
|
m: &MLine,
|
|
style: &codec::objects::MLineStyle,
|
|
) -> Vec<MLineLine> {
|
|
mline_lines_resolved(m, Some(style))
|
|
}
|
|
|
|
fn mline_lines_resolved(
|
|
m: &MLine,
|
|
style: Option<&codec::objects::MLineStyle>,
|
|
) -> Vec<MLineLine> {
|
|
use codec::entities::{MLineFlags, MLineJustification};
|
|
use codec::types::Color;
|
|
|
|
if m.vertices.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
|
|
// (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()),
|
|
],
|
|
};
|
|
|
|
// Stored parameters already include justification; this shift is the fallback.
|
|
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();
|
|
|
|
// parameter[0] stores the element distance along the miter.
|
|
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];
|
|
(kernel::space::Vec3::new(v.position.x, v.position.y, v.position.z)
|
|
+ kernel::space::Vec3::new(v.miter.x, v.miter.y, v.miter.z) * d)
|
|
.to_array()
|
|
};
|
|
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;
|
|
};
|
|
adjusted_mline_endpoint(m, style, vi, point)
|
|
};
|
|
|
|
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();
|
|
// Parameters store the start offset, then alternating drawn and gap lengths.
|
|
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 = 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 a = kernel::space::Vec3::from(a);
|
|
let b = kernel::space::Vec3::from(b);
|
|
let segment = b - a;
|
|
let len = segment.length();
|
|
let Some(direction) = segment.normalize() else {
|
|
continue;
|
|
};
|
|
let at = |t: f64| (a + direction * t).to_array();
|
|
let parameters: &[f64] = m
|
|
.vertices[vi]
|
|
.segments
|
|
.get(ei)
|
|
.map(|segment| segment.parameters.as_slice())
|
|
.unwrap_or(&[]);
|
|
let runs = mline_drawn_ranges(parameters, len);
|
|
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(),
|
|
});
|
|
}
|
|
|
|
// Style-defined joints and end caps.
|
|
if let Some(s) = style {
|
|
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))
|
|
}
|
|
};
|
|
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![a, b],
|
|
color: Color::ByLayer,
|
|
linetype: "ByLayer".to_string(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = [
|
|
m.vertices[vi].direction.x,
|
|
m.vertices[vi].direction.y,
|
|
m.vertices[vi].direction.z,
|
|
];
|
|
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(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
out
|
|
}
|
|
|
|
fn semicircle_cap(
|
|
first: [f64; 3],
|
|
second: [f64; 3],
|
|
direction: [f64; 3],
|
|
start: bool,
|
|
) -> Vec<[f64; 3]> {
|
|
let first = kernel::space::Vec3::from(first);
|
|
let second = kernel::space::Vec3::from(second);
|
|
let center = (first + second) * 0.5;
|
|
let transverse = (first - second) * 0.5;
|
|
let radius = transverse.length();
|
|
let Some(x_axis) = transverse.normalize() else {
|
|
return Vec::new();
|
|
};
|
|
let Some(direction) = kernel::space::Vec3::from(direction).normalize() else {
|
|
return Vec::new();
|
|
};
|
|
let y_axis = if start { -direction } else { direction };
|
|
kernel::space::PlanarCurve::new(
|
|
Plane::from_axes(center.to_array(), x_axis.to_array(), y_axis.to_array()),
|
|
kernel::geom2d::Curve::Arc(kernel::geom2d::Arc {
|
|
centre: [0.0, 0.0],
|
|
radius,
|
|
start_angle: 0.0,
|
|
end_angle: std::f64::consts::PI,
|
|
}),
|
|
)
|
|
.tessellate_angle(kernel::tessellation::DEFAULT_ANGLE)
|
|
}
|
|
|
|
pub fn mline_fill_triangles_with_style(
|
|
m: &MLine,
|
|
style: &codec::objects::MLineStyle,
|
|
) -> Vec<[f64; 3]> {
|
|
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 offset = element.offset * m.scale_factor;
|
|
let low = if offset < style.elements[low].offset * m.scale_factor {
|
|
index
|
|
} else {
|
|
low
|
|
};
|
|
let high = if offset > style.elements[high].offset * m.scale_factor {
|
|
index
|
|
} else {
|
|
high
|
|
};
|
|
(low, high)
|
|
});
|
|
let minimum = style
|
|
.elements
|
|
.iter()
|
|
.map(|element| element.offset)
|
|
.fold(f64::INFINITY, f64::min);
|
|
let maximum = style
|
|
.elements
|
|
.iter()
|
|
.map(|element| element.offset)
|
|
.fold(f64::NEG_INFINITY, f64::max);
|
|
let shift = match m.justification {
|
|
codec::entities::MLineJustification::Top => -maximum,
|
|
codec::entities::MLineJustification::Zero => 0.0,
|
|
codec::entities::MLineJustification::Bottom => -minimum,
|
|
};
|
|
let offset_point = |vertex: usize, element: usize, endpoint: bool| -> [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 + shift) * m.scale_factor);
|
|
let point = (kernel::space::Vec3::new(
|
|
item.position.x,
|
|
item.position.y,
|
|
item.position.z,
|
|
) + kernel::space::Vec3::new(item.miter.x, item.miter.y, item.miter.z) * distance)
|
|
.to_array();
|
|
if endpoint {
|
|
adjusted_mline_endpoint(m, style, vertex, point)
|
|
} else {
|
|
point
|
|
}
|
|
};
|
|
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 start_endpoint = !closed && vertex == 0;
|
|
let end_endpoint = !closed && next + 1 == m.vertices.len();
|
|
let low_start = kernel::space::Vec3::from(offset_point(
|
|
vertex,
|
|
low_index,
|
|
start_endpoint,
|
|
));
|
|
let low_end = kernel::space::Vec3::from(offset_point(next, low_index, end_endpoint));
|
|
let high_start = kernel::space::Vec3::from(offset_point(
|
|
vertex,
|
|
high_index,
|
|
start_endpoint,
|
|
));
|
|
let high_end = kernel::space::Vec3::from(offset_point(next, high_index, end_endpoint));
|
|
let low_length = low_start.distance(low_end);
|
|
let high_length = high_start.distance(high_end);
|
|
if low_length <= 1.0e-12 || high_length <= 1.0e-12 {
|
|
continue;
|
|
}
|
|
let fill_ranges = |element: usize, length: f64| {
|
|
let parameters = m.vertices[vertex]
|
|
.segments
|
|
.get(element)
|
|
.map(|segment| segment.area_fill_parameters.as_slice())
|
|
.unwrap_or(&[]);
|
|
mline_fill_ranges(parameters, length)
|
|
.into_iter()
|
|
.map(|(start, end)| (start / length, end / length))
|
|
.collect::<Vec<_>>()
|
|
};
|
|
let low_ranges = fill_ranges(low_index, low_length);
|
|
let high_ranges = fill_ranges(high_index, high_length);
|
|
for low in &low_ranges {
|
|
for high in &high_ranges {
|
|
let start = low.0.max(high.0);
|
|
let end = low.1.min(high.1);
|
|
if end - start <= 1.0e-9 {
|
|
continue;
|
|
}
|
|
let a = low_start.lerp(low_end, start).to_array();
|
|
let b = high_start.lerp(high_end, start).to_array();
|
|
let c = high_start.lerp(high_end, end).to_array();
|
|
let d = low_start.lerp(low_end, end).to_array();
|
|
triangles.extend([a, b, c, a, c, d]);
|
|
}
|
|
}
|
|
}
|
|
triangles
|
|
}
|
|
|
|
impl RenderConvertible for MLine {
|
|
fn to_render(&self, document: &codec::CadDocument) -> Option<RenderEntity> {
|
|
if self.vertices.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Picking uses one NaN-separated line list; rendering keeps style colours.
|
|
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(RenderEntity {
|
|
pick_tris: Vec::new(),
|
|
object: RenderObject::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) {
|
|
let Some(vertex) = self.vertices.get(grip_id) else {
|
|
return;
|
|
};
|
|
let position = match apply {
|
|
GripApply::Translate(delta) => codec::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) => {
|
|
codec::types::Vector3::new(point.x as f64, point.y as f64, point.z as f64)
|
|
}
|
|
};
|
|
let count = self.vertices.len();
|
|
let segment_count = if self.is_closed() {
|
|
count
|
|
} else {
|
|
count.saturating_sub(1)
|
|
};
|
|
let mut affected = Vec::new();
|
|
let candidates = if self.is_closed() {
|
|
vec![
|
|
((grip_id + count - 2) % count, false),
|
|
((grip_id + count - 1) % count, false),
|
|
(grip_id, true),
|
|
((grip_id + 1) % count, true),
|
|
]
|
|
} else {
|
|
let mut candidates = Vec::new();
|
|
if grip_id >= 2 {
|
|
candidates.push((grip_id - 2, false));
|
|
}
|
|
if grip_id >= 1 {
|
|
candidates.push((grip_id - 1, false));
|
|
}
|
|
if grip_id < segment_count {
|
|
candidates.push((grip_id, true));
|
|
}
|
|
if grip_id + 1 < segment_count {
|
|
candidates.push((grip_id + 1, true));
|
|
}
|
|
candidates
|
|
};
|
|
for (segment, moved_start) in candidates {
|
|
if segment >= segment_count
|
|
|| affected
|
|
.iter()
|
|
.any(|(existing, _, _)| *existing == segment)
|
|
{
|
|
continue;
|
|
}
|
|
if let Some(source) = snapshot_mline_segment(self, segment) {
|
|
affected.push((segment, moved_start, source));
|
|
}
|
|
}
|
|
let offsets = mline_perpendicular_offsets(self);
|
|
self.vertices[grip_id].position = position;
|
|
if rebuild_mline_geometry(self) {
|
|
restore_mline_offsets(self, &offsets);
|
|
for (segment, moved_start, source) in affected {
|
|
restore_shifted_mline_segment(self, segment, source, moved_start);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 insert = if grip_id + 1 < n {
|
|
grip_id + 1
|
|
} else if self.is_closed() {
|
|
n
|
|
} else {
|
|
return;
|
|
};
|
|
let next = (grip_id + 1) % n;
|
|
let v0 = &self.vertices[grip_id];
|
|
let v1 = &self.vertices[next];
|
|
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;
|
|
let point = kernel::space::Vec3::new(
|
|
new_v.position.x,
|
|
new_v.position.y,
|
|
new_v.position.z,
|
|
);
|
|
let source: Option<Vec<_>> = (0..self.style_element_count)
|
|
.map(|element| {
|
|
let endpoints = mline_element_endpoints(self, grip_id, element)?;
|
|
let direction = endpoints.1 - endpoints.0;
|
|
let length = direction.length();
|
|
let split = (point - endpoints.0)
|
|
.dot(direction.normalize()?)
|
|
.clamp(0.0, length);
|
|
Some((
|
|
self.vertices[grip_id].segments.get(element)?.clone(),
|
|
length,
|
|
split,
|
|
))
|
|
})
|
|
.collect();
|
|
let Some(source) = source else {
|
|
return;
|
|
};
|
|
self.vertices.insert(insert, new_v);
|
|
let offsets = mline_perpendicular_offsets(self);
|
|
if !rebuild_mline_geometry(self) {
|
|
return;
|
|
}
|
|
restore_mline_offsets(self, &offsets);
|
|
for (element, (source, source_length, split)) in source.into_iter().enumerate() {
|
|
let Some(first) = mline_element_endpoints(self, grip_id, element) else {
|
|
continue;
|
|
};
|
|
let Some(second) = mline_element_endpoints(self, insert, element) else {
|
|
continue;
|
|
};
|
|
let first_length = first.0.distance(first.1);
|
|
let second_length = second.0.distance(second.1);
|
|
let drawn = mline_drawn_ranges(&source.parameters, source_length);
|
|
let cuts = mline_cut_ranges(&source.area_fill_parameters, source_length);
|
|
let first_drawn = remap_mline_ranges(&drawn, 0.0, split, first_length);
|
|
let second_drawn =
|
|
remap_mline_ranges(&drawn, split, source_length, second_length);
|
|
let first_cuts = remap_mline_ranges(&cuts, 0.0, split, first_length);
|
|
let second_cuts =
|
|
remap_mline_ranges(&cuts, split, source_length, second_length);
|
|
let first = &mut self.vertices[grip_id].segments[element];
|
|
store_mline_drawn_ranges(
|
|
&mut first.parameters,
|
|
first_length,
|
|
&first_drawn,
|
|
);
|
|
store_mline_cut_ranges(
|
|
&mut first.area_fill_parameters,
|
|
first_length,
|
|
&first_cuts,
|
|
);
|
|
let second = &mut self.vertices[insert].segments[element];
|
|
store_mline_drawn_ranges(
|
|
&mut second.parameters,
|
|
second_length,
|
|
&second_drawn,
|
|
);
|
|
store_mline_cut_ranges(
|
|
&mut second.area_fill_parameters,
|
|
second_length,
|
|
&second_cuts,
|
|
);
|
|
}
|
|
}
|
|
A::RemoveVertex
|
|
if grip_id < n && n > 2 && (!self.is_closed() || n > 3) =>
|
|
{
|
|
let merge = self.is_closed() || (grip_id > 0 && grip_id + 1 < n);
|
|
let previous = if grip_id == 0 { n - 1 } else { grip_id - 1 };
|
|
let source = merge.then(|| {
|
|
let first = snapshot_mline_segment(self, previous)?;
|
|
let second = snapshot_mline_segment(self, grip_id)?;
|
|
Some((first, second))
|
|
});
|
|
let source = match source {
|
|
Some(Some(source)) => Some(source),
|
|
Some(None) => return,
|
|
None => None,
|
|
};
|
|
let mut peripheral = Vec::new();
|
|
let candidates = if self.is_closed() {
|
|
vec![
|
|
((previous + n - 1) % n, false),
|
|
((grip_id + 1) % n, true),
|
|
]
|
|
} else {
|
|
let mut candidates = Vec::new();
|
|
if grip_id >= 2 {
|
|
candidates.push((grip_id - 2, false));
|
|
}
|
|
if grip_id + 2 < n {
|
|
candidates.push((grip_id + 1, true));
|
|
}
|
|
candidates
|
|
};
|
|
for (segment, moved_start) in candidates {
|
|
if (merge && (segment == previous || segment == grip_id))
|
|
|| peripheral
|
|
.iter()
|
|
.any(|(existing, _, _)| *existing == segment)
|
|
{
|
|
continue;
|
|
}
|
|
if let Some(snapshot) = snapshot_mline_segment(self, segment) {
|
|
let target = if segment > grip_id {
|
|
segment - 1
|
|
} else {
|
|
segment
|
|
};
|
|
peripheral.push((target, moved_start, snapshot));
|
|
}
|
|
}
|
|
let mut offsets = mline_perpendicular_offsets(self);
|
|
self.vertices.remove(grip_id);
|
|
offsets.remove(grip_id);
|
|
if !rebuild_mline_geometry(self) {
|
|
return;
|
|
}
|
|
restore_mline_offsets(self, &offsets);
|
|
for (segment, moved_start, snapshot) in peripheral {
|
|
restore_shifted_mline_segment(self, segment, snapshot, moved_start);
|
|
}
|
|
if let Some((first, second)) = source {
|
|
let previous = if grip_id == 0 {
|
|
self.vertices.len() - 1
|
|
} else {
|
|
grip_id - 1
|
|
};
|
|
for (element, ((first, first_length), (second, second_length))) in
|
|
first.into_iter().zip(second).enumerate()
|
|
{
|
|
let Some(endpoints) = mline_element_endpoints(self, previous, element)
|
|
else {
|
|
continue;
|
|
};
|
|
let target_length = endpoints.0.distance(endpoints.1);
|
|
let total = first_length + second_length;
|
|
let mut drawn = mline_drawn_ranges(&first.parameters, first_length);
|
|
drawn.extend(
|
|
mline_drawn_ranges(&second.parameters, second_length)
|
|
.into_iter()
|
|
.map(|range| {
|
|
(range.0 + first_length, range.1 + first_length)
|
|
}),
|
|
);
|
|
let mut cuts =
|
|
mline_cut_ranges(&first.area_fill_parameters, first_length);
|
|
cuts.extend(
|
|
mline_cut_ranges(&second.area_fill_parameters, second_length)
|
|
.into_iter()
|
|
.map(|range| {
|
|
(range.0 + first_length, range.1 + first_length)
|
|
}),
|
|
);
|
|
let drawn = merge_mline_ranges(remap_mline_ranges(
|
|
&drawn,
|
|
0.0,
|
|
total,
|
|
target_length,
|
|
));
|
|
let cuts = merge_mline_ranges(remap_mline_ranges(
|
|
&cuts,
|
|
0.0,
|
|
total,
|
|
target_length,
|
|
));
|
|
let target = &mut self.vertices[previous].segments[element];
|
|
store_mline_drawn_ranges(
|
|
&mut target.parameters,
|
|
target_length,
|
|
&drawn,
|
|
);
|
|
store_mline_cut_ranges(
|
|
&mut target.area_fill_parameters,
|
|
target_length,
|
|
&cuts,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PropertyEditable for MLine {
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
|
let just_str = match self.justification {
|
|
codec::entities::MLineJustification::Top => "Top",
|
|
codec::entities::MLineJustification::Zero => "Zero",
|
|
codec::entities::MLineJustification::Bottom => "Bottom",
|
|
};
|
|
vec![PropSection {
|
|
title: t!("Misc").into_owned(),
|
|
props: vec![
|
|
ro_prop(t!("Style").as_ref(), "ml_style", self.style_name.clone()),
|
|
Property {
|
|
label: t!("Style justification").into_owned(),
|
|
field: "ml_justification",
|
|
value: PropValue::Choice {
|
|
selected: just_str.to_string(),
|
|
options: ["Top", "Zero", "Bottom"]
|
|
.into_iter()
|
|
.map(str::to_string)
|
|
.collect(),
|
|
},
|
|
},
|
|
edit(t!("Scale").as_ref(), "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(codec::entities::MLineFlags::CLOSED)
|
|
} else {
|
|
value == "true"
|
|
};
|
|
let offsets = mline_perpendicular_offsets(self);
|
|
self.flags.set(
|
|
codec::entities::MLineFlags::CLOSED,
|
|
closed && self.vertices.len() >= 3,
|
|
);
|
|
rebuild_mline_geometry(self);
|
|
restore_mline_offsets(self, &offsets);
|
|
return;
|
|
}
|
|
"ml_justification" => {
|
|
self.justification = match value {
|
|
"Top" => codec::entities::MLineJustification::Top,
|
|
"Bottom" => codec::entities::MLineJustification::Bottom,
|
|
_ => codec::entities::MLineJustification::Zero,
|
|
};
|
|
return;
|
|
}
|
|
_ => {}
|
|
}
|
|
let Ok(v) = value.trim().parse::<f64>() else {
|
|
return;
|
|
};
|
|
if field == "ml_scale" {
|
|
set_mline_scale(self, v);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_mline_scale(mline: &mut MLine, scale: f64) {
|
|
let old = mline.scale_factor;
|
|
if !scale.is_finite() || scale == 0.0 || !old.is_finite() {
|
|
return;
|
|
}
|
|
if old == 0.0 {
|
|
mline.scale_factor = scale;
|
|
return;
|
|
}
|
|
let ratio = scale / old;
|
|
if !ratio.is_finite() {
|
|
return;
|
|
}
|
|
if mline
|
|
.vertices
|
|
.iter()
|
|
.flat_map(|vertex| &vertex.segments)
|
|
.filter_map(|segment| segment.parameters.first())
|
|
.any(|offset| !offset.is_finite() || !(offset * ratio).is_finite())
|
|
{
|
|
return;
|
|
}
|
|
for segment in mline
|
|
.vertices
|
|
.iter_mut()
|
|
.flat_map(|vertex| vertex.segments.iter_mut())
|
|
{
|
|
if let Some(offset) = segment.parameters.first_mut() {
|
|
*offset *= ratio;
|
|
}
|
|
}
|
|
mline.scale_factor = scale;
|
|
}
|
|
|
|
fn mline_vertex_factor(mline: &MLine, index: usize) -> f64 {
|
|
let vertex = &mline.vertices[index];
|
|
let normal = kernel::space::Vec3::new(mline.normal.x, mline.normal.y, mline.normal.z)
|
|
.normalize()
|
|
.unwrap_or(kernel::space::Vec3::Z);
|
|
let direction =
|
|
kernel::space::Vec3::new(vertex.direction.x, vertex.direction.y, vertex.direction.z)
|
|
.normalize()
|
|
.unwrap_or(kernel::space::Vec3::X);
|
|
let miter = kernel::space::Vec3::new(vertex.miter.x, vertex.miter.y, vertex.miter.z)
|
|
.normalize()
|
|
.unwrap_or(kernel::space::Vec3::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| {
|
|
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,
|
|
);
|
|
});
|
|
}
|
|
}
|