Merge updated pull request #1188 from Ramox81/pedit-enhancements

This commit is contained in:
Hakan Seven committed 2026-09-11 14:55:26 +03:00
commit 7edf0de603
7 files changed
+154 -23

No files matched your search

Generated
+3 -3
View File
@@ -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",
]
+3 -3
View File
@@ -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"] }
+2 -2
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"] }
+10 -3
View File
@@ -1311,13 +1311,16 @@ impl OpenCADStudio {
) -> Task<Message> {
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::<Vec<_>>();
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));
+134 -10
View File
@@ -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<f64> = 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<Handle>,
multiple_history: Vec<Vec<Handle>>,
pending_multiple: Option<Vec<Handle>>,
join_fuzz: f64,
info: HashMap<u64, PeditTarget>,
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] <Yes>:").into_owned(),
Mode::ConvertPrompt(_) => t!(
"PEDIT Object is not a polyline. Turn it into one? [Yes/No] <Y>:"
@@ -439,10 +442,12 @@ impl CadCommand for PeditCommand {
Mode::MultipleGather => None,
Mode::MultipleJoin => {
let fuzz = text.trim().parse::<f64>().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<Handle>, Box<PeditOp>),
JoinSelection(Vec<Handle>),
JoinSelection(Vec<Handle>, 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), &parallel)],
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<Handle>)> {
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))
}