diff --git a/Cargo.lock b/Cargo.lock index ff3c923d6..bf2583875 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.5.4" -source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=b49cbbfc49ba267712045831a601d2f3d0ed386a#b49cbbfc49ba267712045831a601d2f3d0ed386a" +source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=2f2cd25832db298524fb5eb36ced5a438a877e95#2f2cd25832db298524fb5eb36ced5a438a877e95" dependencies = [ "ahash 0.8.12", "anyhow", @@ -894,7 +894,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=84933f9d2e441555a860f843f2577378edee157e#84933f9d2e441555a860f843f2577378edee157e" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=0a24ed40d0872ca8b3ac54bf8c280ce7824ec6c5#0a24ed40d0872ca8b3ac54bf8c280ce7824ec6c5" dependencies = [ "acadrust", "cavalier_contours", @@ -905,7 +905,7 @@ dependencies = [ [[package]] name = "cadkernel-constraints" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=84933f9d2e441555a860f843f2577378edee157e#84933f9d2e441555a860f843f2577378edee157e" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=0a24ed40d0872ca8b3ac54bf8c280ce7824ec6c5#0a24ed40d0872ca8b3ac54bf8c280ce7824ec6c5" dependencies = [ "nalgebra", ] diff --git a/Cargo.toml b/Cargo.toml index 99cf0fe9b..df90de83a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,9 +30,9 @@ glam = { version = "0.33", features = ["bytemuck"] } rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "b49cbbfc49ba267712045831a601d2f3d0ed386a", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "84933f9d2e441555a860f843f2577378edee157e", features = ["acis", "offset"] } -cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "84933f9d2e441555a860f843f2577378edee157e" } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "2f2cd25832db298524fb5eb36ced5a438a877e95", features = ["serde"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "0a24ed40d0872ca8b3ac54bf8c280ce7824ec6c5", features = ["acis", "offset"] } +cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "0a24ed40d0872ca8b3ac54bf8c280ce7824ec6c5" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index 2cc7cd0bd..7adc10b53 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] } # Pulled in only by the `host` feature, which adds the `acadrust`-typed # `HostApi` runtime surface. The default crate stays dependency-free so engine # crates and external tooling can depend on the manifest/ribbon contract cheaply. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "b49cbbfc49ba267712045831a601d2f3d0ed386a", optional = true, features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "2f2cd25832db298524fb5eb36ced5a438a877e95", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } @@ -38,7 +38,7 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } cargo-lock = "11" # acadrust is scanned at build time to generate the embedded type registry. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "b49cbbfc49ba267712045831a601d2f3d0ed386a", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "2f2cd25832db298524fb5eb36ced5a438a877e95", features = ["serde"] } [dev-dependencies] serde_json = "1" diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index 4c162959e..8b718cda5 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "b49cbbfc49ba267712045831a601d2f3d0ed386a", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "2f2cd25832db298524fb5eb36ced5a438a877e95", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } console_error_panic_hook = "0.1" diff --git a/crates/plugin-template-api2/Cargo.toml b/crates/plugin-template-api2/Cargo.toml index 2e0b57661..bb15cc159 100644 --- a/crates/plugin-template-api2/Cargo.toml +++ b/crates/plugin-template-api2/Cargo.toml @@ -10,4 +10,4 @@ crate-type = ["cdylib"] [dependencies] ocs_plugin_api = { path = "../ocs_plugin_api", features = ["host"] } -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "b49cbbfc49ba267712045831a601d2f3d0ed386a", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "2f2cd25832db298524fb5eb36ced5a438a877e95", features = ["serde"] } diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index eacb1a6e1..03dab8980 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1311,13 +1311,16 @@ impl OpenCADStudio { ) -> Task { use crate::modules::draw::modify::pedit::{apply_pedit, convert_to_polyline, PeditOp}; - if !matches!(&op, PeditOp::Multiple(_, _) | PeditOp::JoinSelection(_)) + if !matches!( + &op, + PeditOp::Multiple(_, _) | PeditOp::JoinSelection(_, _) + ) && self.reject_locked_edit(tab, handle) { return Task::none(); } match &op { - PeditOp::JoinSelection(handles) => { + PeditOp::JoinSelection(handles, fuzz) => { let mut available = handles .iter() .filter_map(|handle| { @@ -1340,7 +1343,11 @@ impl OpenCADStudio { .map(|(handle, entity)| (*handle, entity)) .collect::>(); if let Some((mut result, consumed)) = - crate::modules::draw::modify::join::join_to_source(&source, &candidates) + crate::modules::draw::modify::pedit::join_selection_extend( + &source, + &candidates, + *fuzz, + ) { *result.common_mut() = source.common().clone(); available.retain(|(handle, _)| !consumed.contains(handle)); diff --git a/src/modules/draw/modify/pedit.rs b/src/modules/draw/modify/pedit.rs index 8df23f3c3..671504347 100644 --- a/src/modules/draw/modify/pedit.rs +++ b/src/modules/draw/modify/pedit.rs @@ -12,6 +12,7 @@ use crate::command::{CadCommand, CmdResult}; use crate::t; const TAU: f64 = std::f64::consts::TAU; +static JOIN_FUZZ: std::sync::Mutex = std::sync::Mutex::new(0.0); /// What PEDIT knows about a pickable entity, captured at dispatch. #[derive(Clone, Copy)] @@ -58,6 +59,7 @@ pub struct PeditCommand { multiple: Vec, multiple_history: Vec>, pending_multiple: Option>, + join_fuzz: f64, info: HashMap, mode: Mode, undo_count: usize, @@ -91,6 +93,7 @@ impl PeditCommand { multiple: Vec::new(), multiple_history: Vec::new(), pending_multiple: None, + join_fuzz: *JOIN_FUZZ.lock().unwrap_or_else(|error| error.into_inner()), info, mode: Mode::PickTarget, undo_count: 0, @@ -235,7 +238,7 @@ impl CadCommand for PeditCommand { t!("PEDIT Select polyline (or a line/arc to convert) or [Multiple]:").into_owned() } Mode::MultipleGather => format!("PEDIT Select objects ({} selected, Enter when done):", self.multiple.len()), - Mode::MultipleJoin => "PEDIT Enter fuzz distance <0>:".to_string(), + Mode::MultipleJoin => format!("PEDIT Join type: Extend. Enter fuzz distance <{}>:", self.join_fuzz), Mode::MultipleConvert => t!("PEDIT Convert lines and arcs to polylines [Yes/No] :").into_owned(), Mode::ConvertPrompt(_) => t!( "PEDIT Object is not a polyline. Turn it into one? [Yes/No] :" @@ -439,10 +442,12 @@ impl CadCommand for PeditCommand { Mode::MultipleGather => None, Mode::MultipleJoin => { let fuzz = text.trim().parse::().ok()?; - if fuzz != 0.0 { return Some(CmdResult::ReportError("PEDIT: only zero fuzz distance is currently supported.".to_string())); } + if !fuzz.is_finite() || fuzz < 0.0 { return Some(CmdResult::ReportError("PEDIT: fuzz distance must be nonnegative and finite.".to_string())); } + self.join_fuzz = fuzz; + *JOIN_FUZZ.lock().unwrap_or_else(|error| error.into_inner()) = fuzz; self.pending_multiple = Some(self.multiple.clone()); self.mode = Mode::Options; - return Some(CmdResult::PeditOp { handle: self.target?, op: PeditOp::JoinSelection(self.multiple.clone()) }); + return Some(CmdResult::PeditOp { handle: self.target?, op: PeditOp::JoinSelection(self.multiple.clone(), fuzz) }); }, Mode::MultipleConvert => { match up.as_str() { @@ -762,7 +767,7 @@ impl CadCommand for PeditCommand { CmdResult::NeedPoint } Mode::MultipleConvert => self.on_text_input("Y").unwrap_or(CmdResult::NeedPoint), - Mode::MultipleJoin => self.on_text_input("0").unwrap_or(CmdResult::NeedPoint), + Mode::MultipleJoin => self.on_text_input(&self.join_fuzz.to_string()).unwrap_or(CmdResult::NeedPoint), Mode::PolyWidthStart(_, width) | Mode::PolyWidthEnd(_, width) => { let value = width.to_string(); self.on_text_input(&value).unwrap_or(CmdResult::NeedPoint) } Mode::PolyVertex(_) | Mode::PolyRange { .. } => self.on_text_input("N").unwrap_or(CmdResult::NeedPoint), Mode::PolyMove(_) | Mode::PolyInsert(_) => { self.mode = Mode::Options; CmdResult::NeedPoint } @@ -800,7 +805,7 @@ impl CadCommand for PeditCommand { #[derive(Clone)] pub enum PeditOp { Multiple(Vec, Box), - JoinSelection(Vec), + JoinSelection(Vec, f64), SetClosed(bool), SetWidth(f64), SetVertexWidth { index: usize, start: f64, end: f64 }, @@ -866,7 +871,7 @@ pub fn edit_vertex_range(entity: &EntityType, first: usize, last: usize, split: pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool { match op { - PeditOp::Multiple(_, _) | PeditOp::JoinSelection(_) | PeditOp::VertexRange { .. } => false, + PeditOp::Multiple(_, _) | PeditOp::JoinSelection(_, _) | PeditOp::VertexRange { .. } => false, PeditOp::SetVertexWidth { index, start, end } => { if !start.is_finite() || !end.is_finite() || *start < 0.0 || *end < 0.0 { return false; } match entity { @@ -1465,7 +1470,7 @@ mod editing_tests { } #[test] - fn multiple_decline_and_zero_fuzz_join_keep_the_command_active() { + fn multiple_decline_and_fuzz_join_keep_the_command_active() { let mut line = acadrust::entities::Line::new(); line.common.handle = Handle::new(11); let mut arc = acadrust::entities::Arc::new(); @@ -1486,15 +1491,134 @@ mod editing_tests { &handles, ); assert!(matches!(joined.on_text_input("J"), Some(CmdResult::NeedPoint))); - assert!(matches!(joined.on_text_input("0.25"), Some(CmdResult::ReportError(_)))); - let Some(CmdResult::PeditOp { op: PeditOp::JoinSelection(selected), .. }) = - joined.on_text_input("0") + let Some(CmdResult::PeditOp { + op: PeditOp::JoinSelection(selected, fuzz), + .. + }) = joined.on_text_input("0.25") else { panic!("expected a source-directed multiple join"); }; assert_eq!(selected, handles); + assert_eq!(fuzz, 0.25); + + let mut remembered = command( + vec![ + lightweight(21, &[[0.0, 0.0], [1.0, 0.0]]), + lightweight(22, &[[1.0, 0.0], [2.0, 0.0]]), + ], + &handles, + ); + assert!(matches!( + remembered.on_text_input("J"), + Some(CmdResult::NeedPoint) + )); + assert!(remembered.prompt().contains("<0.25>")); + *JOIN_FUZZ.lock().unwrap_or_else(|error| error.into_inner()) = 0.0; + } + + #[test] + fn join_fuzz_extends_only_supported_straight_terminals_within_the_limit() { + let source = lightweight(31, &[[0.0, 0.0], [1.0, 0.0]]); + let candidate = lightweight(32, &[[2.0, 2.0], [2.0, 1.0]]); + let candidates = [(Handle::new(32), &candidate)]; + + assert!(join_selection_extend(&source, &candidates, 0.99).is_none()); + let (joined, consumed) = join_selection_extend(&source, &candidates, 1.0).unwrap(); + assert_eq!(consumed, vec![Handle::new(32)]); + let EntityType::LwPolyline(joined) = joined else { + panic!("expected a lightweight polyline"); + }; + assert_eq!(joined.common.handle, Handle::new(31)); + let points: Vec<_> = joined + .vertices + .iter() + .map(|vertex| [vertex.location.x, vertex.location.y]) + .collect(); + assert_eq!(points, vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0]]); + + let mut curved = source.clone(); + let EntityType::LwPolyline(polyline) = &mut curved else { + unreachable!() + }; + polyline.vertices[0].bulge = 0.25; + assert!(join_selection_extend(&curved, &candidates, 2.0).is_none()); + + let parallel = lightweight(33, &[[2.0, 1.0], [1.0, 1.0]]); + assert!(join_selection_extend( + &source, + &[(Handle::new(33), ¶llel)], + 2.0, + ) + .is_none()); } } // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["PEDIT"] }); // PeditCommand + + +/// Extend straight terminal spans before reusing the ordinary source join. +/// Curved terminal spans and unsupported representations remain unchanged. +pub fn join_selection_extend(source: &EntityType, candidates: &[(Handle, &EntityType)], fuzz: f64) -> Option<(EntityType, Vec)> { + fn endpoint(entity: &EntityType, start: bool) -> Option<[[f64; 3]; 2]> { + let (points, normal, elevation) = match entity { + EntityType::LwPolyline(p) if !p.is_closed && p.vertices.len() >= 2 => { + let n = p.vertices.len(); let segment = if start {0} else {n-2}; + if p.vertices[segment].bulge != 0.0 { return None; } + let indices = if start {[1,0]} else {[n-2,n-1]}; + (indices.map(|i| [p.vertices[i].location.x,p.vertices[i].location.y]),p.normal.clone(),p.elevation) + } + EntityType::Polyline2D(p) if !p.is_closed() && p.vertices.len() >= 2 => { + let n = p.vertices.len(); let segment = if start {0} else {n-2}; + if p.vertices[segment].bulge != 0.0 { return None; } + let indices = if start {[1,0]} else {[n-2,n-1]}; + (indices.map(|i| [p.vertices[i].location.x,p.vertices[i].location.y]),p.normal.clone(),p.elevation) + } + _ => return None, + }; + let plane = crate::entities::curve::ocs_plane(normal,elevation); + Some(points.map(|point| plane.point_at(point))) + } + fn move_endpoint(entity: &mut EntityType, start: bool, point: [f64;3]) -> Option<()> { + match entity { + EntityType::LwPolyline(p) => { + let local = crate::entities::curve::ocs_plane(p.normal.clone(),p.elevation).project(point)?; + let index = if start {0} else {p.vertices.len().checked_sub(1)?}; + p.vertices[index].location = Vector2::new(local[0],local[1]); + } + EntityType::Polyline2D(p) => { + let local = crate::entities::curve::ocs_plane(p.normal.clone(),p.elevation).project(point)?; + let index = if start {0} else {p.vertices.len().checked_sub(1)?}; + p.vertices[index].location.x = local[0]; p.vertices[index].location.y = local[1]; + } + _ => return None, + } + Some(()) + } + let mut result = source.clone(); let mut consumed = Vec::new(); + loop { + let mut progress = false; + for (handle,candidate) in candidates { + if consumed.contains(handle) {continue;} + let mut joined = super::join::join_to_source(&result,&[(*handle,*candidate)]).map(|(entity,_)|entity); + if joined.is_none() && fuzz > 0.0 && fuzz.is_finite() { + 'ends: for a_start in [false,true] { for b_start in [true,false] { + let Some(a) = endpoint(&result,a_start) else {continue;}; + let Some(b) = endpoint(candidate,b_start) else {continue;}; + let Some(point) = cadkernel::space::endpoint_join::extend_line_ends(a,b,fuzz) else {continue;}; + let mut a = result.clone(); let mut b = (*candidate).clone(); + if move_endpoint(&mut a,a_start,point).is_none() || move_endpoint(&mut b,b_start,point).is_none() {continue;} + if let Some((entity,_)) = super::join::join_to_source(&a,&[(*handle,&b)]) { + joined = Some(entity); break 'ends; + } + }} + } + if let Some(mut entity) = joined { + *entity.common_mut() = source.common().clone(); + result = entity; consumed.push(*handle); progress = true; + } + } + if !progress {break;} + } + (!consumed.is_empty()).then_some((result,consumed)) +}