Complete multiline command and properties behavior
This commit is contained in:
parent
8f3ba6dd40
commit
2263168a2a
9 changed files with 870 additions and 238 deletions
|
|
@ -662,6 +662,29 @@ impl OpenCADStudio {
|
|||
tab.dirty = true;
|
||||
}
|
||||
}
|
||||
let mline_settings = self.tabs[self.active_tab]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.and_then(|command| command.mline_settings());
|
||||
if let Some((scale, justification, style_name, style_handle)) = mline_settings {
|
||||
let tab = &mut self.tabs[self.active_tab];
|
||||
let header = &mut tab.scene.document.header;
|
||||
let changed = (header.multiline_scale - scale).abs() > f64::EPSILON
|
||||
|| header.multiline_justification != justification
|
||||
|| !header.multiline_style.eq_ignore_ascii_case(&style_name)
|
||||
|| style_handle.is_some_and(|handle| {
|
||||
header.current_multiline_style_handle != handle
|
||||
});
|
||||
if changed {
|
||||
header.multiline_scale = scale;
|
||||
header.multiline_justification = justification;
|
||||
header.multiline_style = style_name;
|
||||
if let Some(handle) = style_handle {
|
||||
header.current_multiline_style_handle = handle;
|
||||
}
|
||||
tab.dirty = true;
|
||||
}
|
||||
}
|
||||
let was_active = self.tabs[self.active_tab].active_cmd.is_some();
|
||||
let preserve_selection =
|
||||
matches!(result, CmdResult::Relaunch(..) | CmdResult::Dispatch(..));
|
||||
|
|
|
|||
|
|
@ -13,27 +13,27 @@ impl OpenCADStudio {
|
|||
|
||||
"MLINE" => {
|
||||
use crate::modules::draw::draw::mline::MlineCommand;
|
||||
let style_name = self.tabs[i].scene.document.header.multiline_style.clone();
|
||||
let style = self.tabs[i]
|
||||
let header = &self.tabs[i].scene.document.header;
|
||||
let style_name = header.multiline_style.clone();
|
||||
let scale = header.multiline_scale;
|
||||
let justification = header.multiline_justification;
|
||||
let styles = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.iter()
|
||||
.find_map(|(handle, object)| match object {
|
||||
acadrust::objects::ObjectType::MLineStyle(style)
|
||||
if style.name.eq_ignore_ascii_case(&style_name) =>
|
||||
{
|
||||
Some((*handle, style.elements.len()))
|
||||
.filter_map(|(handle, object)| match object {
|
||||
acadrust::objects::ObjectType::MLineStyle(style) => {
|
||||
Some((*handle, style.clone()))
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
let (style_handle, element_count) = style
|
||||
.map(|(handle, count)| (Some(handle), count))
|
||||
.unwrap_or((None, 2));
|
||||
let cmd_obj = MlineCommand::with_style(
|
||||
})
|
||||
.collect();
|
||||
let cmd_obj = MlineCommand::with_styles(
|
||||
styles,
|
||||
style_name,
|
||||
style_handle,
|
||||
element_count,
|
||||
scale,
|
||||
justification,
|
||||
);
|
||||
self.command_line.push_info(&cmd_obj.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
||||
|
|
|
|||
|
|
@ -512,6 +512,8 @@ inventory::submit!(crate::command::CommandRegistration {
|
|||
"SHADEDGE",
|
||||
"MAXACTVP",
|
||||
"CMLJUST",
|
||||
"CMLSCALE",
|
||||
"CMLSTYLE",
|
||||
"TEXTQLTY",
|
||||
"SORTENTS",
|
||||
"FRAME",
|
||||
|
|
|
|||
|
|
@ -920,6 +920,8 @@ impl OpenCADStudio {
|
|||
| "SHADEDGE"
|
||||
| "MAXACTVP"
|
||||
| "CMLJUST"
|
||||
| "CMLSCALE"
|
||||
| "CMLSTYLE"
|
||||
| "TEXTQLTY"
|
||||
| "SORTENTS"
|
||||
| "FRAME"
|
||||
|
|
@ -1697,17 +1699,35 @@ impl OpenCADStudio {
|
|||
}
|
||||
},
|
||||
"CMLJUST" => match &value {
|
||||
Some(v) => v
|
||||
.parse::<i16>()
|
||||
.map(|x| {
|
||||
Some(v) => match v.parse::<i16>() {
|
||||
Ok(x @ 0..=2) => {
|
||||
h.multiline_justification = x;
|
||||
(format!("CMLJUST = {x}"), true)
|
||||
})
|
||||
.map_err(|_| "SETVAR: integer value required.".into()),
|
||||
Ok((format!("CMLJUST = {x}"), true))
|
||||
}
|
||||
_ => Err("SETVAR: integer value from 0 to 2 required.".into()),
|
||||
},
|
||||
None => {
|
||||
Ok((format!("CMLJUST = {}", h.multiline_justification), false))
|
||||
}
|
||||
},
|
||||
"CMLSCALE" => match &value {
|
||||
Some(v) => match v.parse::<f64>() {
|
||||
Ok(x) if x.is_finite() => {
|
||||
let changed = h.multiline_scale != x;
|
||||
h.multiline_scale = x;
|
||||
Ok((format!("CMLSCALE = {x}"), changed))
|
||||
}
|
||||
_ => Err("SETVAR: finite numeric value required.".into()),
|
||||
},
|
||||
None => Ok((format!("CMLSCALE = {}", h.multiline_scale), false)),
|
||||
},
|
||||
"CMLSTYLE" => match &value {
|
||||
Some(_) => Err(
|
||||
"SETVAR: CMLSTYLE is read-only here — use the MLSTYLE command."
|
||||
.into(),
|
||||
),
|
||||
None => Ok((format!("CMLSTYLE = {}", h.multiline_style), false)),
|
||||
},
|
||||
"TEXTQLTY" => match &value {
|
||||
Some(v) => v
|
||||
.parse::<i16>()
|
||||
|
|
|
|||
|
|
@ -2195,6 +2195,20 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
crate::command::WorkingPlane::default()
|
||||
};
|
||||
for &handle in &handles {
|
||||
let mline_style = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.and_then(|entity| match entity {
|
||||
acadrust::EntityType::MLine(mline) => {
|
||||
crate::entities::mline::resolved_mline_style(
|
||||
mline,
|
||||
&self.tabs[i].scene.document,
|
||||
)
|
||||
.cloned()
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
crate::scene::view::dispatch::apply_geom_prop_in_working_plane(
|
||||
|
|
@ -2203,6 +2217,17 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
&value,
|
||||
plane,
|
||||
);
|
||||
if matches!(field, "ml_justification" | "ml_scale") {
|
||||
if let (
|
||||
acadrust::EntityType::MLine(mline),
|
||||
Some(style),
|
||||
) = (entity, mline_style.as_ref())
|
||||
{
|
||||
crate::modules::draw::draw::mline::sync_mline_element_parameters(
|
||||
mline, style,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2399,6 +2424,20 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
)
|
||||
.is_none()
|
||||
{
|
||||
let mline_style = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.and_then(|entity| match entity {
|
||||
acadrust::EntityType::MLine(mline) => {
|
||||
crate::entities::mline::resolved_mline_style(
|
||||
mline,
|
||||
&self.tabs[i].scene.document,
|
||||
)
|
||||
.cloned()
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if let Some(entity) = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
|
|
@ -2410,6 +2449,17 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
&val,
|
||||
plane,
|
||||
);
|
||||
if field == "ml_scale" {
|
||||
if let (
|
||||
acadrust::EntityType::MLine(mline),
|
||||
Some(style),
|
||||
) = (entity, mline_style.as_ref())
|
||||
{
|
||||
crate::modules::draw::draw::mline::sync_mline_element_parameters(
|
||||
mline, style,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1855,6 +1855,11 @@ pub trait CadCommand: Send {
|
|||
None
|
||||
}
|
||||
|
||||
/// Current drawing-persisted multiline creation settings.
|
||||
fn mline_settings(&self) -> Option<(f64, i16, String, Option<Handle>)> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns `true` when the active text prompt expects free-form prose
|
||||
/// that can legitimately contain whitespace (the body of a TEXT /
|
||||
/// MTEXT / DDEDIT entity, an attribute default value, etc.). For
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -1,58 +1,208 @@
|
|||
// MLINE command — create a multiline (parallel lines).
|
||||
//
|
||||
// Workflow: pick vertices, Enter to finish.
|
||||
// Text input (when >= 1 point picked):
|
||||
// C / CLOSE → close and commit
|
||||
// S <value> → set scale factor then continue picking
|
||||
// MLINE command — create a styled group of parallel lines.
|
||||
|
||||
use acadrust::entities::MLine;
|
||||
use acadrust::entities::{MLine, MLineJustification, MLineSegment};
|
||||
use acadrust::objects::MLineStyle;
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Step {
|
||||
Start,
|
||||
Next,
|
||||
Justification,
|
||||
Scale,
|
||||
Style,
|
||||
}
|
||||
|
||||
pub struct MlineCommand {
|
||||
points: Vec<DVec3>,
|
||||
scale: f64,
|
||||
waiting_scale: bool,
|
||||
justification: MLineJustification,
|
||||
step: Step,
|
||||
style_name: String,
|
||||
style_handle: Option<acadrust::Handle>,
|
||||
style_element_count: usize,
|
||||
style_handle: Option<Handle>,
|
||||
styles: Vec<(Handle, MLineStyle)>,
|
||||
notice: Option<String>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl MlineCommand {
|
||||
#[allow(dead_code)]
|
||||
pub fn new() -> Self {
|
||||
pub fn with_styles(
|
||||
mut styles: Vec<(Handle, MLineStyle)>,
|
||||
style_name: impl Into<String>,
|
||||
scale: f64,
|
||||
justification: i16,
|
||||
) -> Self {
|
||||
let requested = style_name.into();
|
||||
if styles.is_empty() {
|
||||
styles.push((Handle::NULL, MLineStyle::standard()));
|
||||
}
|
||||
let selected = styles
|
||||
.iter()
|
||||
.find(|(_, style)| style.name.eq_ignore_ascii_case(&requested))
|
||||
.or_else(|| styles.first());
|
||||
let (style_handle, style_name) = selected
|
||||
.map(|(handle, style)| {
|
||||
(
|
||||
(!handle.is_null()).then_some(*handle),
|
||||
style.name.clone(),
|
||||
)
|
||||
})
|
||||
.unwrap_or((None, requested));
|
||||
Self {
|
||||
points: vec![],
|
||||
scale: 1.0,
|
||||
waiting_scale: false,
|
||||
style_name: "Standard".into(),
|
||||
style_handle: None,
|
||||
style_element_count: 2,
|
||||
points: Vec::new(),
|
||||
scale: if scale.is_finite() { scale } else { 1.0 },
|
||||
justification: MLineJustification::from(justification),
|
||||
step: Step::Start,
|
||||
style_name,
|
||||
style_handle,
|
||||
styles,
|
||||
notice: None,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_style(
|
||||
style_name: impl Into<String>,
|
||||
style_handle: Option<acadrust::Handle>,
|
||||
style_element_count: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
points: vec![],
|
||||
scale: 1.0,
|
||||
waiting_scale: false,
|
||||
style_name: style_name.into(),
|
||||
style_handle,
|
||||
style_element_count: style_element_count.max(1),
|
||||
plane: WorkingPlane::default(),
|
||||
fn selected_style(&self) -> Option<&MLineStyle> {
|
||||
self.styles
|
||||
.iter()
|
||||
.find(|(handle, style)| {
|
||||
self.style_handle == Some(*handle)
|
||||
|| style.name.eq_ignore_ascii_case(&self.style_name)
|
||||
})
|
||||
.map(|(_, style)| style)
|
||||
}
|
||||
|
||||
fn justification_name(&self) -> &'static str {
|
||||
match self.justification {
|
||||
MLineJustification::Top => "Top",
|
||||
MLineJustification::Zero => "Zero",
|
||||
MLineJustification::Bottom => "Bottom",
|
||||
}
|
||||
}
|
||||
|
||||
fn select_style(&mut self, name: &str) -> bool {
|
||||
let Some((handle, style)) = self
|
||||
.styles
|
||||
.iter()
|
||||
.find(|(_, style)| style.name.eq_ignore_ascii_case(name.trim()))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
self.style_handle = Some(*handle);
|
||||
self.style_name = style.name.clone();
|
||||
true
|
||||
}
|
||||
|
||||
fn commit(&self, closed: bool) -> Option<EntityType> {
|
||||
let style = self.selected_style()?;
|
||||
let local: Vec<DVec3> = self
|
||||
.points
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect();
|
||||
let entity = build_mline(
|
||||
&local,
|
||||
self.scale,
|
||||
self.justification,
|
||||
closed,
|
||||
style,
|
||||
self.style_handle,
|
||||
);
|
||||
Some(self.plane.place_entity(entity))
|
||||
}
|
||||
|
||||
fn preview(&self, cursor: DVec3) -> Option<WireModel> {
|
||||
let style = self.selected_style()?;
|
||||
let mut local: Vec<DVec3> = self
|
||||
.points
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect();
|
||||
let cursor_local = self.plane.to_local(cursor);
|
||||
if local
|
||||
.last()
|
||||
.is_none_or(|last| last.distance_squared(cursor_local) > 1.0e-20)
|
||||
{
|
||||
local.push(cursor_local);
|
||||
}
|
||||
if local.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let EntityType::MLine(mline) = build_mline(
|
||||
&local,
|
||||
self.scale,
|
||||
self.justification,
|
||||
false,
|
||||
style,
|
||||
self.style_handle,
|
||||
) else {
|
||||
return None;
|
||||
};
|
||||
let mut points = Vec::new();
|
||||
for (line_index, line) in
|
||||
crate::entities::mline::mline_lines_with_style(&mline, style)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if line_index > 0 {
|
||||
points.push([f32::NAN; 3]);
|
||||
}
|
||||
points.extend(line.points.into_iter().map(|point| {
|
||||
if point[0].is_nan() {
|
||||
[f32::NAN; 3]
|
||||
} else {
|
||||
let world = self
|
||||
.plane
|
||||
.to_world(DVec3::new(point[0], point[1], point[2]));
|
||||
[world.x as f32, world.y as f32, world.z as f32]
|
||||
}
|
||||
}));
|
||||
}
|
||||
let fill_tris = crate::entities::mline::mline_fill_triangles_with_style(&mline, style)
|
||||
.into_iter()
|
||||
.map(|point| {
|
||||
let world = self
|
||||
.plane
|
||||
.to_world(DVec3::new(point[0], point[1], point[2]));
|
||||
[world.x as f32, world.y as f32, world.z as f32]
|
||||
})
|
||||
.collect();
|
||||
Some(WireModel {
|
||||
taper_widths: Vec::new(),
|
||||
world_width: 0.0,
|
||||
depth_override: None,
|
||||
display_visible: true,
|
||||
plot_visible: true,
|
||||
fill_is_3d: false,
|
||||
fill_is_2d_solid: true,
|
||||
render_instance: None,
|
||||
pick_tris: Vec::new(),
|
||||
pick_tris_low: Vec::new(),
|
||||
dash_from_start: false,
|
||||
dash_align_end: None,
|
||||
text_verts: Vec::new(),
|
||||
name: "mline_preview".into(),
|
||||
points,
|
||||
points_low: Vec::new(),
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: Vec::new(),
|
||||
tangent_geoms: Vec::new(),
|
||||
aci: 0,
|
||||
key_vertices: Vec::new(),
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
fill_tris,
|
||||
fill_tris_low: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for MlineCommand {
|
||||
|
|
@ -65,177 +215,242 @@ impl CadCommand for MlineCommand {
|
|||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.waiting_scale {
|
||||
t!("MLINE Enter scale factor:").into_owned()
|
||||
} else if self.points.is_empty() {
|
||||
t!(
|
||||
"MLINE Specify start point (scale=%{scale}):",
|
||||
scale = format!("{:.2}", self.scale)
|
||||
)
|
||||
.into_owned()
|
||||
} else {
|
||||
t!(
|
||||
"MLINE Specify next point (%{count} pts, Enter to finish, C to close, S to set scale):",
|
||||
count = self.points.len()
|
||||
)
|
||||
.into_owned()
|
||||
let notice = self
|
||||
.notice
|
||||
.as_ref()
|
||||
.map(|value| format!("{value}\n"))
|
||||
.unwrap_or_default();
|
||||
let body = match self.step {
|
||||
Step::Start => format!(
|
||||
"MLINE Current settings: Justification = {}, Scale = {}, Style = {}\nSpecify start point or [Justification/Scale/STyle]:",
|
||||
self.justification_name(), self.scale, self.style_name
|
||||
),
|
||||
Step::Next if self.points.len() >= 3 => {
|
||||
"MLINE Specify next point or [Close/Undo]:".to_string()
|
||||
}
|
||||
Step::Next => "MLINE Specify next point or [Undo]:".to_string(),
|
||||
Step::Justification => format!(
|
||||
"MLINE Enter justification type [Top/Zero/Bottom] <{}>:",
|
||||
self.justification_name()
|
||||
),
|
||||
Step::Scale => format!("MLINE Enter scale factor <{}>:", self.scale),
|
||||
Step::Style => format!("MLINE Enter style name or [?] <{}>:", self.style_name),
|
||||
};
|
||||
format!("{notice}{body}")
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::Start => vec![
|
||||
CmdOption::new("Justification", "J"),
|
||||
CmdOption::new("Scale", "S"),
|
||||
CmdOption::new("Style", "ST"),
|
||||
],
|
||||
Step::Next if self.points.len() >= 3 => vec![
|
||||
CmdOption::new("Close", "C"),
|
||||
CmdOption::new("Undo", "U"),
|
||||
],
|
||||
Step::Next => vec![CmdOption::new("Undo", "U")],
|
||||
Step::Justification => vec![
|
||||
CmdOption::new("Top", "T"),
|
||||
CmdOption::new("Zero", "Z"),
|
||||
CmdOption::new("Bottom", "B"),
|
||||
],
|
||||
Step::Scale => Vec::new(),
|
||||
Step::Style => self
|
||||
.styles
|
||||
.iter()
|
||||
.map(|(_, style)| CmdOption::new(&style.name, &style.name))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
self.waiting_scale || !self.points.is_empty()
|
||||
true
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
// The vertex steps accept J / S keywords but are point picks, so keep
|
||||
// polar dynamic input. The scale prompt (`waiting_scale`) is genuine
|
||||
// text entry and is excluded.
|
||||
!self.waiting_scale && !self.points.is_empty()
|
||||
matches!(self.step, Step::Start | Step::Next)
|
||||
}
|
||||
|
||||
fn mline_settings(&self) -> Option<(f64, i16, String, Option<Handle>)> {
|
||||
Some((
|
||||
self.scale,
|
||||
self.justification as i16,
|
||||
self.style_name.clone(),
|
||||
self.style_handle,
|
||||
))
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
// Waiting for scale value
|
||||
if self.waiting_scale {
|
||||
let v: f64 = text
|
||||
.trim()
|
||||
.replace(',', ".")
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(|&v: &f64| v > 0.0)?;
|
||||
self.scale = v;
|
||||
self.waiting_scale = false;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
|
||||
let up = text.trim().to_uppercase();
|
||||
|
||||
// Close command
|
||||
if (up == "C" || up == "CLOSE") && self.points.len() >= 3 {
|
||||
let entity = build_mline(
|
||||
&self
|
||||
.points
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect::<Vec<_>>(),
|
||||
self.scale,
|
||||
true,
|
||||
&self.style_name,
|
||||
self.style_handle,
|
||||
self.style_element_count,
|
||||
);
|
||||
return Some(CmdResult::CommitAndExit(self.plane.place_entity(entity)));
|
||||
}
|
||||
|
||||
// Scale: "S" alone → prompt for value
|
||||
if up == "S" {
|
||||
self.waiting_scale = true;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
|
||||
// Scale: "S <value>" inline
|
||||
if let Some(rest) = up.strip_prefix("S ") {
|
||||
if let Ok(v) = rest.trim().replace(',', ".").parse::<f64>() {
|
||||
if v > 0.0 {
|
||||
self.scale = v;
|
||||
let token = text.trim();
|
||||
let upper = token.to_uppercase();
|
||||
self.notice = None;
|
||||
match self.step {
|
||||
Step::Start => match upper.as_str() {
|
||||
"J" | "JUSTIFICATION" => self.step = Step::Justification,
|
||||
"S" | "SCALE" => self.step = Step::Scale,
|
||||
"ST" | "STYLE" => self.step = Step::Style,
|
||||
_ => return None,
|
||||
},
|
||||
Step::Next => match upper.as_str() {
|
||||
"U" | "UNDO" => {
|
||||
self.points.pop();
|
||||
if self.points.is_empty() {
|
||||
self.step = Step::Start;
|
||||
}
|
||||
}
|
||||
"C" | "CLOSE" if self.points.len() >= 3 => {
|
||||
return self.commit(true).map(CmdResult::CommitAndExit);
|
||||
}
|
||||
_ => return None,
|
||||
},
|
||||
Step::Justification => {
|
||||
self.justification = match upper.as_str() {
|
||||
"T" | "TOP" => MLineJustification::Top,
|
||||
"Z" | "ZERO" => MLineJustification::Zero,
|
||||
"B" | "BOTTOM" => MLineJustification::Bottom,
|
||||
_ => return None,
|
||||
};
|
||||
self.step = Step::Start;
|
||||
}
|
||||
Step::Scale => {
|
||||
let value = token.replace(',', ".").parse::<f64>().ok()?;
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
self.scale = value;
|
||||
self.step = Step::Start;
|
||||
}
|
||||
Step::Style => {
|
||||
if token == "?" {
|
||||
self.notice = Some(format!(
|
||||
"Loaded multiline styles: {}",
|
||||
self.styles
|
||||
.iter()
|
||||
.map(|(_, style)| style.name.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
} else if self.select_style(token) {
|
||||
self.step = Step::Start;
|
||||
} else {
|
||||
self.notice = Some(format!("Multiline style \"{token}\" was not found."));
|
||||
}
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
self.points.push(pt);
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
if !matches!(self.step, Step::Start | Step::Next) {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self
|
||||
.points
|
||||
.last()
|
||||
.is_some_and(|last| last.distance_squared(point) <= 1.0e-20)
|
||||
{
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.points.push(point);
|
||||
self.step = Step::Next;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.points.len() < 2 {
|
||||
return CmdResult::Cancel;
|
||||
match self.step {
|
||||
Step::Start if self.points.is_empty() => CmdResult::Cancel,
|
||||
Step::Next if self.points.len() >= 2 => self
|
||||
.commit(false)
|
||||
.map(CmdResult::CommitAndExit)
|
||||
.unwrap_or(CmdResult::Cancel),
|
||||
Step::Justification | Step::Scale | Step::Style => {
|
||||
self.step = Step::Start;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
let entity = build_mline(
|
||||
&self
|
||||
.points
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect::<Vec<_>>(),
|
||||
self.scale,
|
||||
false,
|
||||
&self.style_name,
|
||||
self.style_handle,
|
||||
self.style_element_count,
|
||||
);
|
||||
CmdResult::CommitAndExit(self.plane.place_entity(entity))
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
let pt = pt.as_vec3();
|
||||
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
||||
if self.points.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut pts: Vec<[f32; 3]> = self
|
||||
.points
|
||||
.iter()
|
||||
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
|
||||
.collect();
|
||||
pts.push([pt.x, pt.y, pt.z]);
|
||||
Some(WireModel {
|
||||
taper_widths: Vec::new(),
|
||||
world_width: 0.0,
|
||||
depth_override: None,
|
||||
display_visible: true,
|
||||
plot_visible: true,
|
||||
fill_is_3d: false,
|
||||
fill_is_2d_solid: false,
|
||||
render_instance: None,
|
||||
pick_tris: Vec::new(),
|
||||
pick_tris_low: Vec::new(),
|
||||
dash_from_start: false,
|
||||
dash_align_end: None,
|
||||
text_verts: Vec::new(),
|
||||
name: "mline_preview".into(),
|
||||
points: pts,
|
||||
points_low: Vec::new(),
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
fill_tris: vec![],
|
||||
fill_tris_low: Vec::new(),
|
||||
})
|
||||
self.points.pop();
|
||||
if self.points.is_empty() {
|
||||
self.step = Step::Start;
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||
(!self.points.is_empty()).then(|| self.preview(point)).flatten()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn sync_mline_element_parameters(mline: &mut MLine, style: &MLineStyle) {
|
||||
let offsets: Vec<f64> = if style.elements.is_empty() {
|
||||
vec![0.5, -0.5]
|
||||
} else {
|
||||
style.elements.iter().map(|element| element.offset).collect()
|
||||
};
|
||||
let minimum = offsets.iter().copied().fold(f64::INFINITY, f64::min);
|
||||
let maximum = offsets
|
||||
.iter()
|
||||
.copied()
|
||||
.fold(f64::NEG_INFINITY, f64::max);
|
||||
let shift = match mline.justification {
|
||||
MLineJustification::Top => -maximum,
|
||||
MLineJustification::Zero => 0.0,
|
||||
MLineJustification::Bottom => -minimum,
|
||||
};
|
||||
let normal = glam::DVec3::new(mline.normal.x, mline.normal.y, mline.normal.z)
|
||||
.normalize_or(glam::DVec3::Z);
|
||||
for vertex in &mut mline.vertices {
|
||||
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);
|
||||
let factor = miter.dot(normal.cross(direction)).abs().max(1.0e-9);
|
||||
vertex
|
||||
.segments
|
||||
.resize_with(offsets.len(), MLineSegment::new);
|
||||
vertex.segments.truncate(offsets.len());
|
||||
for (segment, offset) in vertex.segments.iter_mut().zip(&offsets) {
|
||||
let value = (offset + shift) * mline.scale_factor / factor;
|
||||
if let Some(first) = segment.parameters.first_mut() {
|
||||
*first = value;
|
||||
} else {
|
||||
segment.parameters.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
mline.style_element_count = offsets.len();
|
||||
}
|
||||
|
||||
fn build_mline(
|
||||
pts: &[DVec3],
|
||||
points: &[DVec3],
|
||||
scale: f64,
|
||||
justification: MLineJustification,
|
||||
closed: bool,
|
||||
style_name: &str,
|
||||
style_handle: Option<acadrust::Handle>,
|
||||
style_element_count: usize,
|
||||
style: &MLineStyle,
|
||||
style_handle: Option<Handle>,
|
||||
) -> EntityType {
|
||||
let mut mline = MLine::new();
|
||||
mline.scale_factor = scale;
|
||||
mline.style_name = style_name.to_string();
|
||||
mline.justification = justification;
|
||||
mline.style_name = style.name.clone();
|
||||
mline.style_handle = style_handle;
|
||||
mline.style_element_count = style_element_count;
|
||||
for point in pts {
|
||||
mline.style_element_count = style.elements.len().max(1);
|
||||
for point in points {
|
||||
mline.add_vertex(Vector3::new(point.x, point.y, point.z));
|
||||
}
|
||||
if closed {
|
||||
mline.close();
|
||||
}
|
||||
sync_mline_element_parameters(&mut mline, style);
|
||||
EntityType::MLine(mline)
|
||||
}
|
||||
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["MLINE"] }); // MlineCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["MLINE"] });
|
||||
|
|
|
|||
|
|
@ -371,6 +371,56 @@ pub fn tessellate(
|
|||
acc as f32
|
||||
};
|
||||
let mut out: Vec<WireModel> = Vec::with_capacity(lines.len());
|
||||
if let Some(style) = crate::entities::mline::resolved_mline_style(m, document) {
|
||||
let triangles =
|
||||
crate::entities::mline::mline_fill_triangles_with_style(m, style);
|
||||
if !triangles.is_empty() {
|
||||
let (fill_tris, fill_tris_low) = points_to_ds(triangles);
|
||||
let fill_color = if selected {
|
||||
WireModel::SELECTED
|
||||
} else {
|
||||
match style.fill_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]]
|
||||
}
|
||||
}
|
||||
};
|
||||
out.push(WireModel {
|
||||
taper_widths: Vec::new(),
|
||||
world_width: 0.0,
|
||||
depth_override: None,
|
||||
display_visible: true,
|
||||
plot_visible: true,
|
||||
fill_is_3d: false,
|
||||
fill_is_2d_solid: true,
|
||||
render_instance: None,
|
||||
pick_tris: Vec::new(),
|
||||
pick_tris_low: Vec::new(),
|
||||
dash_from_start: false,
|
||||
dash_align_end: None,
|
||||
text_verts: Vec::new(),
|
||||
name: name.clone(),
|
||||
points: Vec::new(),
|
||||
points_low: Vec::new(),
|
||||
color: fill_color,
|
||||
selected,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: Vec::new(),
|
||||
tangent_geoms: Vec::new(),
|
||||
aci: 0,
|
||||
key_vertices: Vec::new(),
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
fill_tris,
|
||||
fill_tris_low,
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut snap_attached = false;
|
||||
for l in lines {
|
||||
if l.points.is_empty() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue