forked from 1b-agent/opencadstudio-upstream
fix(xclip): precise clip + carry clip across cross-drawing paste
Clipped blocks were broken at UTM and lost their clip when pasted into another drawing. - Clip geometry: world_clip_polygon_f64 keeps the boundary in absolute f64; clip_wires now clips in a frame relative to the boundary's first vertex (small coords → exact f32 Sutherland–Hodgman) and re-splits the result into the double-single high/low pair the relative-to-eye renderer needs, so the clipped block draws correctly and no longer poisons ZOOM Extents. - ZOOM Extents (fit_all): the outlier reject compared absolute UTM coordinates against a centre-RELATIVE span (local_extent_max), rejecting the whole drawing → no-op. Made the reject distance-from-median-centre. - Cross-drawing paste: clipboard now snapshots each copied entity's whole xdictionary object graph (ClipboardDeps.ext_objects) and recreates it on paste with fresh handles + remapped references, re-pointing the pasted entity's xdictionary at the new root. Wired into PASTECLIP and PASTEORIG. Fixed handle allocation (allocate_handle, not the non-advancing next_handle) so the dictionary chain doesn't collapse onto one handle. - Updated hit_test / plugin_host tests for the eye-relative + DVec3 signatures. Known gaps: PASTEBLOCK does not yet carry the clip (clip would need to live inside the new wrapper block definition); hatch-clip path still f32. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
bca8958025
commit
6667042372
7 files changed
+330
-26
No files matched your search
+22
-8
@@ -6670,9 +6670,6 @@ impl Scene {
|
||||
if !x.is_finite() || !y.is_finite() {
|
||||
continue;
|
||||
}
|
||||
if x.abs() > lim || y.abs() > lim {
|
||||
continue;
|
||||
}
|
||||
sx += x as f64;
|
||||
sy += y as f64;
|
||||
n += 1;
|
||||
@@ -6689,9 +6686,26 @@ impl Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
// Robust drawing centre (median centroid). `lim` is a span RELATIVE to
|
||||
// this centre, so every reject below is distance-from-centre — geometry
|
||||
// now reaches fit_all as absolute coordinates (no world_offset), which
|
||||
// at UTM scale are ~5.7e6; an absolute `|x| > lim` test would reject the
|
||||
// entire drawing and make ZOOM Extents a no-op.
|
||||
let (mcx, mcy) = {
|
||||
let mut xs: Vec<f32> = cents.iter().map(|c| c.cx).collect();
|
||||
let mut ys: Vec<f32> = cents.iter().map(|c| c.cy).collect();
|
||||
if xs.is_empty() {
|
||||
(0.0_f32, 0.0_f32)
|
||||
} else {
|
||||
xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
ys.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
(xs[xs.len() / 2], ys[ys.len() / 2])
|
||||
}
|
||||
};
|
||||
|
||||
// IQR-based reject only kicks in with enough samples for the quartiles
|
||||
// to be meaningful. Below that, the absolute `lim` filter is the only
|
||||
// gate (legacy behavior).
|
||||
// to be meaningful. Below that, the centre-relative `lim` filter is the
|
||||
// only gate (legacy behavior).
|
||||
let (rx_lo, rx_hi, ry_lo, ry_hi) = if cents.len() >= 8 {
|
||||
let mut xs: Vec<f32> = cents.iter().map(|c| c.cx).collect();
|
||||
let mut ys: Vec<f32> = cents.iter().map(|c| c.cy).collect();
|
||||
@@ -6712,7 +6726,7 @@ impl Scene {
|
||||
let dy = (q3y - q1y).max(1.0) * K;
|
||||
(q1x - dx, q3x + dx, q1y - dy, q3y + dy)
|
||||
} else {
|
||||
(-lim, lim, -lim, lim)
|
||||
(mcx - lim, mcx + lim, mcy - lim, mcy + lim)
|
||||
};
|
||||
|
||||
let mut min = glam::Vec3::splat(f32::MAX);
|
||||
@@ -6726,7 +6740,7 @@ impl Scene {
|
||||
if !x.is_finite() || !y.is_finite() || !z.is_finite() {
|
||||
continue;
|
||||
}
|
||||
if x.abs() > lim || y.abs() > lim {
|
||||
if (x - mcx).abs() > lim || (y - mcy).abs() > lim {
|
||||
continue;
|
||||
}
|
||||
min = min.min(glam::Vec3::new(x, y, z));
|
||||
@@ -8274,7 +8288,7 @@ fn tessellate_entity(
|
||||
// clip the expanded block geometry to the boundary polygon so
|
||||
// only the portion inside the clip is drawn.
|
||||
if let Some(sf) = pick::xclip::insert_spatial_filter(document, ins) {
|
||||
let poly = pick::xclip::world_clip_polygon(sf, ins, world_offset);
|
||||
let poly = pick::xclip::world_clip_polygon_f64(sf, ins, world_offset);
|
||||
pick::xclip::clip_wires(&mut wires, &poly);
|
||||
}
|
||||
|
||||
|
||||
@@ -759,9 +759,10 @@ mod aabb_reject_tests {
|
||||
let near = wire("5", vec![[-0.02, 0.0, 0.0], [0.02, 0.0, 0.0]], [-0.02, 0.0, 0.02, 0.0]);
|
||||
let far = wire("9", vec![[0.9, 0.9, 0.0], [0.95, 0.9, 0.0]], [0.9, 0.9, 0.95, 0.9]);
|
||||
|
||||
assert_eq!(click_hit(cursor, std::slice::from_ref(&near), vp, bounds), Some("5"));
|
||||
assert_eq!(click_hit(cursor, std::slice::from_ref(&far), vp, bounds), None);
|
||||
let eye = glam::DVec3::ZERO;
|
||||
assert_eq!(click_hit(cursor, std::slice::from_ref(&near), vp, eye, bounds), Some("5"));
|
||||
assert_eq!(click_hit(cursor, std::slice::from_ref(&far), vp, eye, bounds), None);
|
||||
// The far wire must be rejected without hiding the near one.
|
||||
assert_eq!(click_hit(cursor, &[far, near], vp, bounds), Some("5"));
|
||||
assert_eq!(click_hit(cursor, &[far, near], vp, eye, bounds), Some("5"));
|
||||
}
|
||||
}
|
||||
+100
-7
@@ -67,6 +67,21 @@ pub fn world_clip_polygon(
|
||||
ins: &Insert,
|
||||
world_offset: [f64; 3],
|
||||
) -> Vec<[f32; 2]> {
|
||||
world_clip_polygon_f64(sf, ins, world_offset)
|
||||
.into_iter()
|
||||
.map(|[x, y]| [x as f32, y as f32])
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// f64 variant of [`world_clip_polygon`]. The boundary stays in absolute world
|
||||
/// coordinates so clipping at UTM scale (~5.7e6) is precise — the f32 version
|
||||
/// quantizes each vertex by ~0.5 m, which warps the clip region and breaks both
|
||||
/// the clipped render and ZOOM Extents.
|
||||
pub fn world_clip_polygon_f64(
|
||||
sf: &SpatialFilter,
|
||||
ins: &Insert,
|
||||
world_offset: [f64; 3],
|
||||
) -> Vec<[f64; 2]> {
|
||||
let xform = ins.get_transform();
|
||||
let inv_block = &sf.inverse_block_transform;
|
||||
let [ox, oy, _] = world_offset;
|
||||
@@ -82,7 +97,7 @@ pub fn world_clip_polygon(
|
||||
.map(|[x, y]| {
|
||||
let block = inv_block.transform_point(Vector3::new(x, y, 0.0));
|
||||
let w = xform.apply(block);
|
||||
[(w.x - ox) as f32, (w.y - oy) as f32]
|
||||
[w.x - ox, w.y - oy]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -91,25 +106,102 @@ pub fn world_clip_polygon(
|
||||
/// world_offset-subtracted). Polylines are split into NaN-separated inside
|
||||
/// runs; fill triangles are clipped against the polygon; snap / key vertices
|
||||
/// outside the boundary are dropped. Wires left with no geometry are removed.
|
||||
pub fn clip_wires(wires: &mut Vec<WireModel>, poly: &[[f32; 2]]) {
|
||||
pub fn clip_wires(wires: &mut Vec<WireModel>, poly: &[[f64; 2]]) {
|
||||
if poly.len() < 3 {
|
||||
return;
|
||||
}
|
||||
// Clip in a frame relative to the boundary's first vertex: the wire points
|
||||
// arrive as absolute coordinates (double-single high+low), which at UTM
|
||||
// scale are ~5.7e6 and lose ~0.5 m in f32. Subtracting the f64 reference
|
||||
// makes every coordinate small, so the f32 Sutherland–Hodgman math is exact;
|
||||
// the reference is added back afterwards and re-split into the high/low pair
|
||||
// the relative-to-eye renderer expects.
|
||||
let (rx, ry) = (poly[0][0], poly[0][1]);
|
||||
let lpoly: Vec<[f32; 2]> = poly
|
||||
.iter()
|
||||
.map(|&[x, y]| [(x - rx) as f32, (y - ry) as f32])
|
||||
.collect();
|
||||
|
||||
// Reconstruct an absolute-f64 wire point from its high/low pair, NaN-safe.
|
||||
let abs = |hi: [f32; 3], lo: [f32; 3]| -> [f64; 3] {
|
||||
[
|
||||
hi[0] as f64 + lo[0] as f64,
|
||||
hi[1] as f64 + lo[1] as f64,
|
||||
hi[2] as f64 + lo[2] as f64,
|
||||
]
|
||||
};
|
||||
|
||||
for w in wires.iter_mut() {
|
||||
if !w.points.is_empty() {
|
||||
w.points = clip_polyline(&w.points, poly);
|
||||
// Absolute → local f32 (NaN separators preserved).
|
||||
let local: Vec<[f32; 3]> = (0..w.points.len())
|
||||
.map(|i| {
|
||||
let hi = w.points[i];
|
||||
if !hi[0].is_finite() || !hi[1].is_finite() {
|
||||
return NAN3;
|
||||
}
|
||||
let lo = w.points_low.get(i).copied().unwrap_or([0.0; 3]);
|
||||
let a = abs(hi, lo);
|
||||
[(a[0] - rx) as f32, (a[1] - ry) as f32, a[2] as f32]
|
||||
})
|
||||
.collect();
|
||||
let clipped = clip_polyline(&local, &lpoly);
|
||||
// Local → absolute, re-split into double-single high/low.
|
||||
let mut hi = Vec::with_capacity(clipped.len());
|
||||
let mut lo = Vec::with_capacity(clipped.len());
|
||||
for p in clipped {
|
||||
if !p[0].is_finite() || !p[1].is_finite() {
|
||||
hi.push(NAN3);
|
||||
lo.push([0.0; 3]);
|
||||
continue;
|
||||
}
|
||||
let (hx, lx) = split_ds(p[0] as f64 + rx);
|
||||
let (hy, ly) = split_ds(p[1] as f64 + ry);
|
||||
let (hz, lz) = split_ds(p[2] as f64);
|
||||
hi.push([hx, hy, hz]);
|
||||
lo.push([lx, ly, lz]);
|
||||
}
|
||||
w.points = hi;
|
||||
w.points_low = lo;
|
||||
}
|
||||
if !w.fill_tris.is_empty() {
|
||||
w.fill_tris = clip_triangles(&w.fill_tris, poly);
|
||||
let local: Vec<[f32; 3]> = (0..w.fill_tris.len())
|
||||
.map(|i| {
|
||||
let hi = w.fill_tris[i];
|
||||
let lo = w.fill_tris_low.get(i).copied().unwrap_or([0.0; 3]);
|
||||
let a = abs(hi, lo);
|
||||
[(a[0] - rx) as f32, (a[1] - ry) as f32, a[2] as f32]
|
||||
})
|
||||
.collect();
|
||||
let clipped = clip_triangles(&local, &lpoly);
|
||||
let mut hi = Vec::with_capacity(clipped.len());
|
||||
let mut lo = Vec::with_capacity(clipped.len());
|
||||
for p in clipped {
|
||||
let (hx, lx) = split_ds(p[0] as f64 + rx);
|
||||
let (hy, ly) = split_ds(p[1] as f64 + ry);
|
||||
let (hz, lz) = split_ds(p[2] as f64);
|
||||
hi.push([hx, hy, hz]);
|
||||
lo.push([lx, ly, lz]);
|
||||
}
|
||||
w.fill_tris = hi;
|
||||
w.fill_tris_low = lo;
|
||||
}
|
||||
w.key_vertices
|
||||
.retain(|v| point_in_poly(v[0] as f32, v[1] as f32, poly));
|
||||
w.snap_pts.retain(|(p, _)| point_in_poly(p.x as f32, p.y as f32, poly));
|
||||
.retain(|v| point_in_poly((v[0] - rx) as f32, (v[1] - ry) as f32, &lpoly));
|
||||
w.snap_pts
|
||||
.retain(|(p, _)| point_in_poly((p.x - rx) as f32, (p.y - ry) as f32, &lpoly));
|
||||
w.aabb = recompute_aabb(&w.points, &w.fill_tris);
|
||||
}
|
||||
wires.retain(|w| !w.points.is_empty() || !w.fill_tris.is_empty());
|
||||
}
|
||||
|
||||
/// Double-single split of an f64 into (high, low) f32 — mirrors
|
||||
/// `WireModel::split_ds` so clipped points match the renderer's reconstruction.
|
||||
fn split_ds(v: f64) -> (f32, f32) {
|
||||
let high = v as f32;
|
||||
(high, (v - high as f64) as f32)
|
||||
}
|
||||
|
||||
/// Clip a hatch fill boundary to `poly`.
|
||||
///
|
||||
/// `boundary` is a hatch's NaN-separated loops in f32 offsets from
|
||||
@@ -514,7 +606,7 @@ mod tests {
|
||||
ins.common.xdictionary_handle = Some(h_xdict);
|
||||
|
||||
let resolved = insert_spatial_filter(&doc, &ins).expect("filter resolves");
|
||||
let poly = world_clip_polygon(resolved, &ins, [0.0, 0.0, 0.0]);
|
||||
let poly = world_clip_polygon_f64(resolved, &ins, [0.0, 0.0, 0.0]);
|
||||
assert_eq!(poly.len(), 4);
|
||||
|
||||
// A polyline half inside, half outside the 0..10 square.
|
||||
@@ -603,3 +695,4 @@ mod tests {
|
||||
assert!(out.iter().all(|p| p[0] <= 10.0 + 1e-3));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user