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]]
name = "cadkernel"
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 = [
"acadrust",
"cavalier_contours",

View file

@ -28,7 +28,7 @@ rfd = "0.17"
clap = { version = "4", features = ["derive"] }
env_logger = "0.11"
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" }
flate2 = "1"
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 cadkernel::geom2d::nurbs::clamped_uniform_knots;
use cadkernel::geom2d::{
intersect as kernel_intersect, Arc as KernelArc, Circle as KernelCircle, Curve,
Extent as KernelExtent,
intersect as kernel_intersect, trim_spans as kernel_trim_spans, Arc as KernelArc,
Circle as KernelCircle, Curve, Extent as KernelExtent,
Ellipse as KernelEllipse, EllipseArc as KernelEllipseArc, Line as KernelLine,
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)];
}
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()
.filter_map(|(ta, tb)| {
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).
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![];
};
let (t0, t1) = curve.domain();
let curve = Curve::Nurbs(curve);
trim_intervals(ts, t_click)
trim_intervals(&curve, ts, t_click)
.into_iter()
.filter_map(|(ta, tb)| {
let t_lo = t0 + ta * (t1 - t0);
@ -642,39 +661,16 @@ fn extend_spline(spl: &SplineEnt, t_click: f64, geos: &[Geo]) -> Option<EntityTy
// ── Trim helpers ──────────────────────────────────────────────────────────
/// Remove the t-interval containing `t_click` from sorted ts. Returns surviving pieces.
fn trim_intervals(ts: &[f64], t_click: f64) -> Vec<(f64, f64)> {
cut_intervals(ts, t_click, KernelExtent::Bounded)
}
/// The stretches a target keeps after the one holding the click is removed.
///
/// The outer bounds come from how far the target's parameter runs, so a ray
/// keeps an interval that reaches infinity rather than one that stops at an
/// 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()
fn trim_intervals(curve: &Curve, ts: &[f64], t_click: f64) -> Vec<(f64, f64)> {
kernel_trim_spans(
curve,
ts,
t_click,
KernelTolerance::new(CUT_TOLERANCE),
)
.into_iter()
.map(|span| (span[0], span[1]))
.collect()
}
/// Trim a Line entity. Returns the surviving line segments.
@ -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 p2 = [orig.end.x, orig.end.y];
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()
.filter_map(|(ta, tb)| {
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 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()
.filter_map(|(ta, tb)| {
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 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()
.filter_map(|(ta, tb)| {
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 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()
.filter_map(|(ta, tb)| {
let pa = pt(ta);