fix(trim): keep retained geometry continuous

Use kernel trim spans so unrelated boundary crossings do not split survivors.
This commit is contained in:
Hakan Seven 2026-08-18 20:12:30 +03:00
commit f9c3546551
3 changed files with 49 additions and 44 deletions

2
Cargo.lock generated
View file

@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]] [[package]]
name = "cadkernel" name = "cadkernel"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=b8ec104#b8ec104e86060fe7d5c698870915b5388569649b" source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=ff950ce#ff950ce4146c9fb94a5478e6704ada01264dbf9f"
dependencies = [ dependencies = [
"acadrust", "acadrust",
"cavalier_contours", "cavalier_contours",

View file

@ -28,7 +28,7 @@ rfd = "0.17"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
env_logger = "0.11" env_logger = "0.11"
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "931c4ab", features = ["serde"] } acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "931c4ab", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "b8ec104", features = ["acis", "offset"] } cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "ff950ce", features = ["acis", "offset"] }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1" flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }

View file

@ -29,8 +29,8 @@ use acadrust::{EntityType, Handle};
use glam::DVec3; use glam::DVec3;
use cadkernel::geom2d::nurbs::clamped_uniform_knots; use cadkernel::geom2d::nurbs::clamped_uniform_knots;
use cadkernel::geom2d::{ use cadkernel::geom2d::{
intersect as kernel_intersect, Arc as KernelArc, Circle as KernelCircle, Curve, intersect as kernel_intersect, trim_spans as kernel_trim_spans, Arc as KernelArc,
Extent as KernelExtent, Circle as KernelCircle, Curve, Extent as KernelExtent,
Ellipse as KernelEllipse, EllipseArc as KernelEllipseArc, Line as KernelLine, Ellipse as KernelEllipse, EllipseArc as KernelEllipseArc, Line as KernelLine,
NurbsCurve, Ray as KernelRay, Tolerance as KernelTolerance, XLine as KernelXLine, NurbsCurve, Ray as KernelRay, Tolerance as KernelTolerance, XLine as KernelXLine,
}; };
@ -485,7 +485,24 @@ fn trim_ellipse(orig: &EllipseEnt, ts: &[f64], t_click: f64) -> Vec<EntityType>
return vec![EntityType::Ellipse(e)]; return vec![EntityType::Ellipse(e)];
} }
trim_intervals(ts, t_click) let major_radius = orig.major_axis.x.hypot(orig.major_axis.y);
if major_radius < 1e-9 {
return vec![];
}
let curve = Curve::Ellipse(KernelEllipseArc {
ellipse: KernelEllipse {
centre: [orig.center.x, orig.center.y],
major_radius,
minor_radius: major_radius * orig.minor_axis_ratio,
major_axis: [
orig.major_axis.x / major_radius,
orig.major_axis.y / major_radius,
],
},
start_parameter: t0,
end_parameter: t1,
});
trim_intervals(&curve, ts, t_click)
.into_iter() .into_iter()
.filter_map(|(ta, tb)| { .filter_map(|(ta, tb)| {
if (tb - ta).abs() < 1e-6 { if (tb - ta).abs() < 1e-6 {
@ -545,11 +562,13 @@ fn extend_ellipse(orig: &EllipseEnt, t_click: f64, geos: &[Geo]) -> Option<Entit
/// Trim a Spline entity. Returns surviving spline pieces (one or two). /// Trim a Spline entity. Returns surviving spline pieces (one or two).
fn trim_spline(spl: &SplineEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> { fn trim_spline(spl: &SplineEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
let Some((t0, t1)) = spline_range(spl) else { let Some(curve) = spline_to_nurbs(spl) else {
return vec![]; return vec![];
}; };
let (t0, t1) = curve.domain();
let curve = Curve::Nurbs(curve);
trim_intervals(ts, t_click) trim_intervals(&curve, ts, t_click)
.into_iter() .into_iter()
.filter_map(|(ta, tb)| { .filter_map(|(ta, tb)| {
let t_lo = t0 + ta * (t1 - t0); let t_lo = t0 + ta * (t1 - t0);
@ -642,38 +661,15 @@ fn extend_spline(spl: &SplineEnt, t_click: f64, geos: &[Geo]) -> Option<EntityTy
// ── Trim helpers ────────────────────────────────────────────────────────── // ── Trim helpers ──────────────────────────────────────────────────────────
/// Remove the t-interval containing `t_click` from sorted ts. Returns surviving pieces. fn trim_intervals(curve: &Curve, ts: &[f64], t_click: f64) -> Vec<(f64, f64)> {
fn trim_intervals(ts: &[f64], t_click: f64) -> Vec<(f64, f64)> { kernel_trim_spans(
cut_intervals(ts, t_click, KernelExtent::Bounded) curve,
} ts,
t_click,
/// The stretches a target keeps after the one holding the click is removed. KernelTolerance::new(CUT_TOLERANCE),
/// )
/// The outer bounds come from how far the target's parameter runs, so a ray .into_iter()
/// keeps an interval that reaches infinity rather than one that stops at an .map(|span| (span[0], span[1]))
/// invented far end. Those infinities are what tell the caller a surviving
/// piece is still unbounded.
fn cut_intervals(ts: &[f64], t_click: f64, extent: KernelExtent) -> Vec<(f64, f64)> {
let (first, last) = match extent {
KernelExtent::Bounded => (0.0, 1.0),
KernelExtent::Forward => (0.0, f64::INFINITY),
KernelExtent::Infinite => (f64::NEG_INFINITY, f64::INFINITY),
};
let mut bounds = vec![first];
bounds.extend_from_slice(ts);
bounds.push(last);
bounds.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
let remove = bounds
.windows(2)
.position(|w| t_click >= w[0] - 1e-6 && t_click <= w[1] + 1e-6);
bounds
.windows(2)
.enumerate()
.filter(|(idx, _)| Some(*idx) != remove)
.filter(|(_, w)| (w[1] - w[0]) > 1e-6)
.map(|(_, w)| (w[0], w[1]))
.collect() .collect()
} }
@ -682,7 +678,8 @@ fn trim_line(orig: &LineEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
let p1 = [orig.start.x, orig.start.y]; let p1 = [orig.start.x, orig.start.y];
let p2 = [orig.end.x, orig.end.y]; let p2 = [orig.end.x, orig.end.y];
let z = orig.start.z; let z = orig.start.z;
trim_intervals(ts, t_click) let curve = Curve::Line(KernelLine { start: p1, end: p2 });
trim_intervals(&curve, ts, t_click)
.into_iter() .into_iter()
.filter_map(|(ta, tb)| { .filter_map(|(ta, tb)| {
let a = lerp2(p1, p2, ta); let a = lerp2(p1, p2, ta);
@ -768,8 +765,14 @@ fn trim_arc(orig: &ArcEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
} }
}; };
let angle_at = |t: f64| norm(a0) + span * t; let angle_at = |t: f64| norm(a0) + span * t;
let curve = Curve::Arc(KernelArc {
centre: [orig.center.x, orig.center.y],
radius: orig.radius,
start_angle: a0,
end_angle: a1,
});
trim_intervals(ts, t_click) trim_intervals(&curve, ts, t_click)
.into_iter() .into_iter()
.filter_map(|(ta, tb)| { .filter_map(|(ta, tb)| {
if (tb - ta).abs() < 1e-6 { if (tb - ta).abs() < 1e-6 {
@ -1113,7 +1116,8 @@ fn trim_ray(orig: &RayEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
let dz = orig.direction.z; let dz = orig.direction.z;
let pt = |t: f64| [bx + t * dx, by + t * dy, bz + t * dz]; let pt = |t: f64| [bx + t * dx, by + t * dy, bz + t * dz];
cut_intervals(ts, t_click, KernelExtent::Forward) let curve = ray_curve(orig);
trim_intervals(&curve, ts, t_click)
.into_iter() .into_iter()
.filter_map(|(ta, tb)| { .filter_map(|(ta, tb)| {
let pa = pt(ta); let pa = pt(ta);
@ -1159,7 +1163,8 @@ fn trim_xline(orig: &XLineEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
let dz = orig.direction.z; let dz = orig.direction.z;
let pt = |t: f64| [bx + t * dx, by + t * dy, bz + t * dz]; let pt = |t: f64| [bx + t * dx, by + t * dy, bz + t * dz];
cut_intervals(ts, t_click, KernelExtent::Infinite) let curve = xline_curve(orig);
trim_intervals(&curve, ts, t_click)
.into_iter() .into_iter()
.filter_map(|(ta, tb)| { .filter_map(|(ta, tb)| {
let pa = pt(ta); let pa = pt(ta);