use acadrust::entities::MLine; use acadrust::objects::MLineStyle; use acadrust::{EntityType, Handle}; use glam::{DVec2, DVec3}; use rustc_hash::FxHashMap as HashMap; use crate::command::{CadCommand, CmdOption, CmdResult}; #[derive(Clone)] pub struct MlineEditTarget { pub entity: MLine, pub style: MLineStyle, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Tool { ClosedCross, OpenCross, MergedCross, ClosedTee, OpenTee, MergedTee, CornerJoint, AddVertex, DeleteVertex, CutSingle, CutAll, WeldAll, } enum Mode { Choose, PickFirst(Tool), PickSecond { tool: Tool, first: Handle, first_pick: DVec3, }, PickRangeEnd { tool: Tool, target: Handle, start: DVec3, }, } pub struct MlineEditCommand { targets: HashMap, mode: Mode, } impl MlineEditCommand { pub fn new(targets: HashMap) -> Self { Self { targets, mode: Mode::Choose, } } fn target(&self, handle: Handle) -> Option<&MlineEditTarget> { self.targets.get(&handle.value()) } fn replace(handle: Handle, mline: MLine) -> CmdResult { CmdResult::ReplaceMany( vec![(handle, vec![EntityType::MLine(mline)])], Vec::new(), ) } fn edit_vertex(&self, tool: Tool, handle: Handle, point: DVec3) -> Option { let target = self.target(handle)?; let mut mline = target.entity.clone(); match tool { Tool::AddVertex => { let (segment, _, projected, _) = closest_segment(&mline, point)?; let insert = segment + 1; let mut vertex = mline.vertices[segment].clone(); vertex.position = acadrust::types::Vector3::new( projected.x, projected.y, projected.z, ); mline.vertices.insert(insert, vertex); mline.rebuild_geometry(); } Tool::DeleteVertex => { let vertex = closest_vertex(&mline, point)?; if !mline.remove_vertex(vertex) { return None; } } _ => return None, } crate::modules::draw::draw::mline::sync_mline_element_parameters( &mut mline, &target.style, ); Some(Self::replace(handle, mline)) } fn edit_range( &self, tool: Tool, handle: Handle, start: DVec3, end: DVec3, ) -> Option { let target = self.target(handle)?; let mut mline = target.entity.clone(); let (segment, _, _, _) = closest_segment(&mline, start)?; let elements: Vec = match tool { Tool::CutSingle => vec![closest_element(&mline, segment, start)?], _ => (0..mline.style_element_count).collect(), }; for element in elements { let (a, b) = element_segment(&mline, segment, element)?; let direction = b - a; let length = direction.length(); if length <= 1.0e-10 { continue; } let unit = direction / length; let first = (start - a).dot(unit).clamp(0.0, length); let second = (end - a).dot(unit).clamp(0.0, length); let low = first.min(second); let high = first.max(second); let parameters = &mut mline.vertices[segment].segments[element].parameters; if tool == Tool::WeldAll { add_drawn_range(parameters, length, low, high); } else { remove_drawn_range(parameters, length, low, high); } } Some(Self::replace(handle, mline)) } fn edit_pair( &self, tool: Tool, first_handle: Handle, first_pick: DVec3, second_handle: Handle, second_pick: DVec3, ) -> Option { if first_handle == second_handle { return None; } let first_target = self.target(first_handle)?; let second_target = self.target(second_handle)?; let mut first = first_target.entity.clone(); let mut second = second_target.entity.clone(); let (first_segment, _, _, _) = closest_segment(&first, first_pick)?; let (second_segment, _, _, _) = closest_segment(&second, second_pick)?; let (intersection, first_fraction, second_fraction, sine) = segment_intersection( center_segment(&first, first_segment)?, center_segment(&second, second_segment)?, )?; let first_length = center_segment(&first, first_segment)?.1.distance( center_segment(&first, first_segment)?.0, ); let second_length = center_segment(&second, second_segment)?.1.distance( center_segment(&second, second_segment)?.0, ); let first_at = first_fraction * first_length; let second_at = second_fraction * second_length; let divisor = sine.abs().max(0.15); let first_gap = style_width(&second_target.style, second.scale_factor) * 0.5 / divisor; let second_gap = style_width(&first_target.style, first.scale_factor) * 0.5 / divisor; match tool { Tool::ClosedCross => { gap_elements(&mut first, first_segment, first_at, first_gap, None); } Tool::OpenCross => { gap_elements(&mut first, first_segment, first_at, first_gap, None); gap_elements( &mut second, second_segment, second_at, second_gap, Some(outer_element_indices(&second_target.style)), ); } Tool::MergedCross => { gap_elements( &mut first, first_segment, first_at, first_gap, Some(inner_element_indices(&first_target.style)), ); gap_elements( &mut second, second_segment, second_at, second_gap, Some(inner_element_indices(&second_target.style)), ); } Tool::ClosedTee | Tool::OpenTee | Tool::MergedTee => { move_closest_end(&mut first, first_pick, intersection); crate::modules::draw::draw::mline::sync_mline_element_parameters( &mut first, &first_target.style, ); let elements = match tool { Tool::ClosedTee => None, Tool::OpenTee => Some(outer_element_indices(&second_target.style)), Tool::MergedTee => Some(inner_element_indices(&second_target.style)), _ => unreachable!(), }; gap_elements( &mut second, second_segment, second_at, second_gap, elements, ); } Tool::CornerJoint => { move_closest_end(&mut first, first_pick, intersection); move_closest_end(&mut second, second_pick, intersection); crate::modules::draw::draw::mline::sync_mline_element_parameters( &mut first, &first_target.style, ); crate::modules::draw::draw::mline::sync_mline_element_parameters( &mut second, &second_target.style, ); } _ => return None, } Some(CmdResult::ReplaceMany( vec![ (first_handle, vec![EntityType::MLine(first)]), (second_handle, vec![EntityType::MLine(second)]), ], Vec::new(), )) } } impl CadCommand for MlineEditCommand { fn name(&self) -> &'static str { "MLEDIT" } fn prompt(&self) -> String { match self.mode { Mode::Choose => "MLEDIT Choose an edit tool:".to_string(), Mode::PickFirst(_) => "MLEDIT Select first multiline:".to_string(), Mode::PickSecond { .. } => "MLEDIT Select second multiline:".to_string(), Mode::PickRangeEnd { tool: Tool::WeldAll, .. } => { "MLEDIT Specify the end of the weld range:".to_string() } Mode::PickRangeEnd { .. } => { "MLEDIT Specify the second cut point:".to_string() } } } fn options(&self) -> Vec { if !matches!(self.mode, Mode::Choose) { return Vec::new(); } vec![ CmdOption::new("Closed Cross", "CC"), CmdOption::new("Open Cross", "OC"), CmdOption::new("Merged Cross", "MC"), CmdOption::new("Closed Tee", "CT"), CmdOption::new("Open Tee", "OT"), CmdOption::new("Merged Tee", "MT"), CmdOption::new("Corner Joint", "CJ"), CmdOption::new("Add Vertex", "AV"), CmdOption::new("Delete Vertex", "DV"), CmdOption::new("Cut Single", "CS"), CmdOption::new("Cut All", "CA"), CmdOption::new("Weld All", "WA"), ] } fn wants_text_input(&self) -> bool { matches!(self.mode, Mode::Choose) } fn needs_entity_pick(&self) -> bool { matches!(self.mode, Mode::PickFirst(_) | Mode::PickSecond { .. }) } fn entity_pick_highlights_hover(&self) -> bool { self.needs_entity_pick() } fn on_text_input(&mut self, text: &str) -> Option { if !matches!(self.mode, Mode::Choose) { return None; } let tool = match text.trim().to_uppercase().as_str() { "CC" | "CLOSED CROSS" => Tool::ClosedCross, "OC" | "OPEN CROSS" => Tool::OpenCross, "MC" | "MERGED CROSS" => Tool::MergedCross, "CT" | "CLOSED TEE" => Tool::ClosedTee, "OT" | "OPEN TEE" => Tool::OpenTee, "MT" | "MERGED TEE" => Tool::MergedTee, "CJ" | "CORNER JOINT" => Tool::CornerJoint, "AV" | "ADD VERTEX" => Tool::AddVertex, "DV" | "DELETE VERTEX" => Tool::DeleteVertex, "CS" | "CUT SINGLE" => Tool::CutSingle, "CA" | "CUT ALL" => Tool::CutAll, "WA" | "WELD ALL" => Tool::WeldAll, _ => return None, }; self.mode = Mode::PickFirst(tool); Some(CmdResult::NeedPoint) } fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { if self.target(handle).is_none() { return CmdResult::NeedPoint; } match self.mode { Mode::PickFirst(tool @ (Tool::AddVertex | Tool::DeleteVertex)) => self .edit_vertex(tool, handle, point) .unwrap_or(CmdResult::NeedPoint), Mode::PickFirst(tool @ (Tool::CutSingle | Tool::CutAll | Tool::WeldAll)) => { self.mode = Mode::PickRangeEnd { tool, target: handle, start: point, }; CmdResult::NeedPoint } Mode::PickFirst(tool) => { self.mode = Mode::PickSecond { tool, first: handle, first_pick: point, }; CmdResult::NeedPoint } Mode::PickSecond { tool, first, first_pick, } => self .edit_pair(tool, first, first_pick, handle, point) .unwrap_or(CmdResult::NeedPoint), _ => CmdResult::NeedPoint, } } fn on_point(&mut self, point: DVec3) -> CmdResult { match self.mode { Mode::PickRangeEnd { tool, target, start, } => self .edit_range(tool, target, start, point) .unwrap_or(CmdResult::NeedPoint), _ => CmdResult::NeedPoint, } } fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } } fn center_segment(mline: &MLine, index: usize) -> Option<(DVec3, DVec3)> { let first = mline.vertices.get(index)?.position; let next = if index + 1 < mline.vertices.len() { index + 1 } else if mline.is_closed() { 0 } else { return None; }; let second = mline.vertices[next].position; Some(( DVec3::new(first.x, first.y, first.z), DVec3::new(second.x, second.y, second.z), )) } fn closest_segment(mline: &MLine, point: DVec3) -> Option<(usize, f64, DVec3, f64)> { let count = if mline.is_closed() { mline.vertices.len() } else { mline.vertices.len().saturating_sub(1) }; (0..count) .filter_map(|index| { let (a, b) = center_segment(mline, index)?; let delta = b - a; let length_squared = delta.length_squared(); if length_squared <= 1.0e-20 { return None; } let fraction = ((point - a).dot(delta) / length_squared).clamp(0.0, 1.0); let projected = a + delta * fraction; Some((index, fraction, projected, projected.distance_squared(point))) }) .min_by(|left, right| left.3.total_cmp(&right.3)) } fn closest_vertex(mline: &MLine, point: DVec3) -> Option { mline .vertices .iter() .enumerate() .min_by(|(_, left), (_, right)| { let left = DVec3::new(left.position.x, left.position.y, left.position.z) .distance_squared(point); let right = DVec3::new(right.position.x, right.position.y, right.position.z) .distance_squared(point); left.total_cmp(&right) }) .map(|(index, _)| index) } fn element_segment(mline: &MLine, index: usize, element: usize) -> Option<(DVec3, DVec3)> { let next = if index + 1 < mline.vertices.len() { index + 1 } else if mline.is_closed() { 0 } else { return None; }; let point = |vertex: usize| -> Option { let item = mline.vertices.get(vertex)?; let offset = item.segments.get(element)?.parameters.first().copied()?; Some(DVec3::new( item.position.x + item.miter.x * offset, item.position.y + item.miter.y * offset, item.position.z + item.miter.z * offset, )) }; Some((point(index)?, point(next)?)) } fn closest_element(mline: &MLine, segment: usize, point: DVec3) -> Option { (0..mline.style_element_count) .filter_map(|element| { let (a, b) = element_segment(mline, segment, element)?; let delta = b - a; let fraction = ((point - a).dot(delta) / delta.length_squared().max(1.0e-20)) .clamp(0.0, 1.0); Some((element, (a + delta * fraction).distance_squared(point))) }) .min_by(|left, right| left.1.total_cmp(&right.1)) .map(|(element, _)| element) } fn drawn_ranges(parameters: &[f64], length: f64) -> Vec<(f64, f64)> { if parameters.len() <= 1 { return vec![(0.0, length)]; } let toggles = ¶meters[1..]; let mut ranges = Vec::new(); let mut index = 0; while index < toggles.len() { let start = toggles[index].clamp(0.0, length); let end = toggles .get(index + 1) .copied() .unwrap_or(length) .clamp(0.0, length); if end - start > 1.0e-9 { ranges.push((start, end)); } index += 2; } ranges } fn store_drawn_ranges(parameters: &mut Vec, length: f64, ranges: &[(f64, f64)]) { let offset = parameters.first().copied().unwrap_or(0.0); parameters.clear(); parameters.push(offset); if ranges.len() == 1 && ranges[0].0 <= 1.0e-9 && ranges[0].1 >= length - 1.0e-9 { return; } for (start, end) in ranges { parameters.push(*start); if *end < length - 1.0e-9 { parameters.push(*end); } } } fn remove_drawn_range(parameters: &mut Vec, length: f64, low: f64, high: f64) { if high - low <= 1.0e-9 { return; } let mut result = Vec::new(); for (start, end) in drawn_ranges(parameters, length) { if low > start + 1.0e-9 { result.push((start, low.min(end))); } if high < end - 1.0e-9 { result.push((high.max(start), end)); } } store_drawn_ranges(parameters, length, &result); } fn add_drawn_range(parameters: &mut Vec, length: f64, low: f64, high: f64) { let mut ranges = drawn_ranges(parameters, length); ranges.push((low, high)); 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); } store_drawn_ranges(parameters, length, &merged); } fn style_width(style: &MLineStyle, scale: f64) -> f64 { let low = style .elements .iter() .map(|element| element.offset) .fold(f64::INFINITY, f64::min); let high = style .elements .iter() .map(|element| element.offset) .fold(f64::NEG_INFINITY, f64::max); if low.is_finite() && high.is_finite() { (high - low).abs() * scale.abs() } else { scale.abs() } } fn outer_element_indices(style: &MLineStyle) -> Vec { if style.elements.is_empty() { return Vec::new(); } let low = style .elements .iter() .enumerate() .min_by(|(_, left), (_, right)| left.offset.total_cmp(&right.offset)) .map(|(index, _)| index) .unwrap_or(0); let high = style .elements .iter() .enumerate() .max_by(|(_, left), (_, right)| left.offset.total_cmp(&right.offset)) .map(|(index, _)| index) .unwrap_or(low); if low == high { vec![low] } else { vec![low, high] } } fn inner_element_indices(style: &MLineStyle) -> Vec { let outer = outer_element_indices(style); let inner: Vec = (0..style.elements.len()) .filter(|index| !outer.contains(index)) .collect(); if inner.is_empty() { outer.into_iter().take(1).collect() } else { inner } } fn gap_elements( mline: &mut MLine, segment: usize, at: f64, half_gap: f64, elements: Option>, ) { let elements = elements.unwrap_or_else(|| (0..mline.style_element_count).collect()); for element in elements { let Some((a, b)) = element_segment(mline, segment, element) else { continue; }; let length = a.distance(b); if let Some(parameters) = mline.vertices[segment].segments.get_mut(element) { remove_drawn_range( &mut parameters.parameters, length, (at - half_gap).max(0.0), (at + half_gap).min(length), ); } } } fn move_closest_end(mline: &mut MLine, pick: DVec3, intersection: DVec3) { if mline.vertices.is_empty() || mline.is_closed() { return; } let first = DVec3::new( mline.vertices[0].position.x, mline.vertices[0].position.y, mline.vertices[0].position.z, ); let last_index = mline.vertices.len() - 1; let last = DVec3::new( mline.vertices[last_index].position.x, mline.vertices[last_index].position.y, mline.vertices[last_index].position.z, ); let index = if first.distance_squared(pick) <= last.distance_squared(pick) { 0 } else { last_index }; let _ = mline.set_vertex_position( index, acadrust::types::Vector3::new(intersection.x, intersection.y, intersection.z), ); } fn segment_intersection( first: (DVec3, DVec3), second: (DVec3, DVec3), ) -> Option<(DVec3, f64, f64, f64)> { let p = first.0.truncate(); let r = (first.1 - first.0).truncate(); let q = second.0.truncate(); let s = (second.1 - second.0).truncate(); let cross = |left: DVec2, right: DVec2| left.x * right.y - left.y * right.x; let denominator = cross(r, s); if denominator.abs() <= 1.0e-10 { return None; } let t = cross(q - p, s) / denominator; let u = cross(q - p, r) / denominator; if !(-1.0e-6..=1.0 + 1.0e-6).contains(&t) || !(-1.0e-6..=1.0 + 1.0e-6).contains(&u) { return None; } let xy = p + r * t; let z = first.0.z + (first.1.z - first.0.z) * t; let sine = denominator / (r.length() * s.length()).max(1.0e-20); Some((DVec3::new(xy.x, xy.y, z), t, u, sine)) } inventory::submit!(crate::command::CommandRegistration { names: &["MLEDIT"] });