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 <noreply@anthropic.com>
This commit is contained in:
parent
bca8958025
commit
6667042372
7 changed files with 330 additions and 26 deletions
|
|
@ -627,7 +627,10 @@ impl OpenCADStudio {
|
||||||
// else a block reference renders empty. (#135)
|
// else a block reference renders empty. (#135)
|
||||||
self.merge_clipboard_blocks(i);
|
self.merge_clipboard_blocks(i);
|
||||||
let count = self.clipboard.len();
|
let count = self.clipboard.len();
|
||||||
let new_handles: Vec<Handle> = self
|
// Index-aligned with `clipboard` (NULL = add failed) so the
|
||||||
|
// captured extension-dictionary subtrees can be matched back
|
||||||
|
// to their pasted entity by index.
|
||||||
|
let by_index: Vec<Handle> = self
|
||||||
.clipboard
|
.clipboard
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -635,10 +638,12 @@ impl OpenCADStudio {
|
||||||
crate::scene::view::dispatch::apply_transform(&mut entity, &translate);
|
crate::scene::view::dispatch::apply_transform(&mut entity, &translate);
|
||||||
self.tabs[i].scene.add_entity_clone(entity)
|
self.tabs[i].scene.add_entity_clone(entity)
|
||||||
})
|
})
|
||||||
.filter(|h| !h.is_null())
|
|
||||||
.collect();
|
.collect();
|
||||||
|
// Recreate each pasted entity's xdictionary graph (XCLIP
|
||||||
|
// spatial filters etc.) in this document. (#xclip-paste)
|
||||||
|
self.merge_clipboard_ext_objects(i, &by_index);
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
for h in new_handles {
|
for h in by_index.iter().copied().filter(|h| !h.is_null()) {
|
||||||
self.tabs[i].scene.select_entity(h, false);
|
self.tabs[i].scene.select_entity(h, false);
|
||||||
}
|
}
|
||||||
// Tessellate any pasted ACIS solids — top-level ones and
|
// Tessellate any pasted ACIS solids — top-level ones and
|
||||||
|
|
@ -1867,6 +1872,54 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recreate the extension-dictionary object graph (XCLIP spatial filters,
|
||||||
|
/// attached XRecords, …) captured for each copied entity, cloning every
|
||||||
|
/// object into this document with fresh handles, remapping all internal
|
||||||
|
/// references, and re-pointing the pasted entity's `xdictionary_handle` at
|
||||||
|
/// the new root. `by_index` is the paste's new entity handles, aligned with
|
||||||
|
/// the clipboard order (NULL where the add failed). No-op without captures.
|
||||||
|
pub(super) fn merge_clipboard_ext_objects(&mut self, i: usize, by_index: &[Handle]) {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
if self.clipboard_deps.ext_objects.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let captures = self.clipboard_deps.ext_objects.clone();
|
||||||
|
let doc = &mut self.tabs[i].scene.document;
|
||||||
|
for cap in captures {
|
||||||
|
let Some(&new_entity) = by_index.get(cap.entity_index) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if new_entity.is_null() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// old handle → fresh handle for the whole subtree, plus the entity
|
||||||
|
// itself so an object owned by the entity remaps onto the new copy.
|
||||||
|
let mut remap: HashMap<Handle, Handle> = HashMap::new();
|
||||||
|
remap.insert(cap.src_entity_handle, new_entity);
|
||||||
|
for (old, _) in &cap.objects {
|
||||||
|
// `allocate_handle` advances the counter; `next_handle()` only
|
||||||
|
// peeks, so reusing it here would hand every object the same
|
||||||
|
// handle and they'd overwrite each other in `doc.objects`.
|
||||||
|
remap.insert(*old, doc.allocate_handle());
|
||||||
|
}
|
||||||
|
for (old, obj) in &cap.objects {
|
||||||
|
let mut obj = obj.clone();
|
||||||
|
let new_h = remap[old];
|
||||||
|
remap_object(&mut obj, new_h, &remap);
|
||||||
|
doc.objects.insert(new_h, obj);
|
||||||
|
}
|
||||||
|
// Point the pasted entity at the cloned root dictionary.
|
||||||
|
if let Some(new_root) = remap.get(&cap.root).copied() {
|
||||||
|
if let Some(e) = doc.get_entity_mut(new_entity) {
|
||||||
|
e.common_mut().xdictionary_handle = Some(new_root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The wires were tessellated before the filters existed; force a rebuild
|
||||||
|
// so the clip is applied to the freshly-pasted, now-filtered inserts.
|
||||||
|
self.tabs[i].scene.bump_geometry();
|
||||||
|
}
|
||||||
|
|
||||||
fn restore_pre_cmd_tangent(&mut self) {
|
fn restore_pre_cmd_tangent(&mut self) {
|
||||||
if let Some(was_on) = self.pre_cmd_tangent.take() {
|
if let Some(was_on) = self.pre_cmd_tangent.take() {
|
||||||
if !was_on {
|
if !was_on {
|
||||||
|
|
@ -1881,6 +1934,65 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rewrite a cloned extension-dictionary object onto fresh handles: set its own
|
||||||
|
/// handle to `new_handle` and remap its owner and any handle references it holds
|
||||||
|
/// through `remap` (a handle still in the source space stays unchanged, which is
|
||||||
|
/// correct for cross-references that point outside the captured subtree).
|
||||||
|
fn remap_object(
|
||||||
|
obj: &mut acadrust::objects::ObjectType,
|
||||||
|
new_handle: acadrust::Handle,
|
||||||
|
remap: &std::collections::HashMap<acadrust::Handle, acadrust::Handle>,
|
||||||
|
) {
|
||||||
|
use acadrust::objects::ObjectType;
|
||||||
|
let map = |h: acadrust::Handle| remap.get(&h).copied().unwrap_or(h);
|
||||||
|
match obj {
|
||||||
|
ObjectType::Dictionary(d) => {
|
||||||
|
d.handle = new_handle;
|
||||||
|
d.owner = map(d.owner);
|
||||||
|
for (_, h) in d.entries.iter_mut() {
|
||||||
|
*h = map(*h);
|
||||||
|
}
|
||||||
|
if let Some(x) = d.xdictionary_handle.as_mut() {
|
||||||
|
*x = map(*x);
|
||||||
|
}
|
||||||
|
for r in d.reactors.iter_mut() {
|
||||||
|
*r = map(*r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ObjectType::DictionaryWithDefault(d) => {
|
||||||
|
d.handle = new_handle;
|
||||||
|
d.owner = map(d.owner);
|
||||||
|
for (_, h) in d.entries.iter_mut() {
|
||||||
|
*h = map(*h);
|
||||||
|
}
|
||||||
|
d.default_handle = map(d.default_handle);
|
||||||
|
}
|
||||||
|
ObjectType::DictionaryVariable(v) => {
|
||||||
|
v.handle = new_handle;
|
||||||
|
v.owner_handle = map(v.owner_handle);
|
||||||
|
}
|
||||||
|
ObjectType::SpatialFilter(s) => {
|
||||||
|
s.handle = new_handle;
|
||||||
|
s.owner = map(s.owner);
|
||||||
|
}
|
||||||
|
ObjectType::XRecord(x) => {
|
||||||
|
x.handle = new_handle;
|
||||||
|
x.owner = map(x.owner);
|
||||||
|
}
|
||||||
|
ObjectType::Group(g) => {
|
||||||
|
g.handle = new_handle;
|
||||||
|
g.owner = map(g.owner);
|
||||||
|
for h in g.entities.iter_mut() {
|
||||||
|
*h = map(*h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Other leaf object kinds don't appear in an entity xdictionary; if one
|
||||||
|
// does, it's inserted with the fresh handle below via the caller's key,
|
||||||
|
// but its internal owner is left as-is (best effort).
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── DIMSPACE helper ───────────────────────────────────────────────────────────
|
// ── DIMSPACE helper ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Parse `base_val,h1;h2;...;hN,spacing` and adjust parallel dimension positions.
|
/// Parse `base_val,h1;h2;...;hN,spacing` and adjust parallel dimension positions.
|
||||||
|
|
|
||||||
|
|
@ -631,9 +631,16 @@ impl OpenCADStudio {
|
||||||
// else a block reference renders empty in a drawing that
|
// else a block reference renders empty in a drawing that
|
||||||
// lacks the definition. (#135 / #158)
|
// lacks the definition. (#135 / #158)
|
||||||
self.merge_clipboard_blocks(i);
|
self.merge_clipboard_blocks(i);
|
||||||
for entity in self.clipboard.clone() {
|
let by_index: Vec<acadrust::Handle> = self
|
||||||
self.tabs[i].scene.add_entity_clone(entity);
|
.clipboard
|
||||||
}
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|entity| self.tabs[i].scene.add_entity_clone(entity))
|
||||||
|
.collect();
|
||||||
|
// Recreate each pasted entity's xdictionary graph (XCLIP
|
||||||
|
// spatial filters etc.) so a cross-drawing paste keeps its
|
||||||
|
// clip instead of showing the whole block. (#xclip-paste)
|
||||||
|
self.merge_clipboard_ext_objects(i, &by_index);
|
||||||
// Tessellate any pasted ACIS solids (top-level + inside a
|
// Tessellate any pasted ACIS solids (top-level + inside a
|
||||||
// recreated block) so they render instead of staying blank.
|
// recreated block) so they render instead of staying blank.
|
||||||
self.tabs[i].scene.populate_meshes_from_document();
|
self.tabs[i].scene.populate_meshes_from_document();
|
||||||
|
|
|
||||||
|
|
@ -665,6 +665,11 @@ pub struct ClipboardDeps {
|
||||||
/// reference doesn't render empty in a drawing that lacks the
|
/// reference doesn't render empty in a drawing that lacks the
|
||||||
/// definition. (#135)
|
/// definition. (#135)
|
||||||
pub blocks: Vec<BlockDef>,
|
pub blocks: Vec<BlockDef>,
|
||||||
|
/// Extension-dictionary object subtrees hanging off the copied entities
|
||||||
|
/// (XCLIP spatial filters, attached XRecords, …). Each entity's whole
|
||||||
|
/// `xdictionary` graph is snapshotted so a cross-drawing paste recreates it
|
||||||
|
/// — without this a pasted clipped block loses its clip and renders whole.
|
||||||
|
pub ext_objects: Vec<ClipExtObjects>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A captured block definition: its base point and the entities it owns
|
/// A captured block definition: its base point and the entities it owns
|
||||||
|
|
@ -677,6 +682,19 @@ pub struct BlockDef {
|
||||||
pub entities: Vec<acadrust::EntityType>,
|
pub entities: Vec<acadrust::EntityType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The extension-dictionary object graph captured for one copied entity.
|
||||||
|
/// `objects` holds every object reachable from the entity's `xdictionary`
|
||||||
|
/// (dictionaries + their leaf objects), keyed by their source handles; `root`
|
||||||
|
/// is the xdictionary handle. On paste the whole set is cloned into the target
|
||||||
|
/// document with fresh handles and the references are remapped.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ClipExtObjects {
|
||||||
|
pub entity_index: usize,
|
||||||
|
pub src_entity_handle: acadrust::Handle,
|
||||||
|
pub root: acadrust::Handle,
|
||||||
|
pub objects: Vec<(acadrust::Handle, acadrust::objects::ObjectType)>,
|
||||||
|
}
|
||||||
|
|
||||||
impl ClipboardDeps {
|
impl ClipboardDeps {
|
||||||
/// Snapshot the records `entities` reference that exist in `doc`.
|
/// Snapshot the records `entities` reference that exist in `doc`.
|
||||||
pub fn capture(doc: &acadrust::CadDocument, entities: &[acadrust::EntityType]) -> Self {
|
pub fn capture(doc: &acadrust::CadDocument, entities: &[acadrust::EntityType]) -> Self {
|
||||||
|
|
@ -728,15 +746,74 @@ impl ClipboardDeps {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Extension-dictionary subtree per entity (XCLIP filters etc.).
|
||||||
|
let mut ext_objects = Vec::new();
|
||||||
|
for (entity_index, e) in entities.iter().enumerate() {
|
||||||
|
let c = e.common();
|
||||||
|
if let Some(root) = c.xdictionary_handle {
|
||||||
|
if root.is_null() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let objects = Self::collect_ext_subtree(doc, root);
|
||||||
|
if !objects.is_empty() {
|
||||||
|
ext_objects.push(ClipExtObjects {
|
||||||
|
entity_index,
|
||||||
|
src_entity_handle: c.handle,
|
||||||
|
root,
|
||||||
|
objects,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ClipboardDeps {
|
ClipboardDeps {
|
||||||
layers: layers.iter().filter_map(|n| doc.layers.get(n).cloned()).collect(),
|
layers: layers.iter().filter_map(|n| doc.layers.get(n).cloned()).collect(),
|
||||||
linetypes: ltypes.iter().filter_map(|n| doc.line_types.get(n).cloned()).collect(),
|
linetypes: ltypes.iter().filter_map(|n| doc.line_types.get(n).cloned()).collect(),
|
||||||
text_styles: tstyles.iter().filter_map(|n| doc.text_styles.get(n).cloned()).collect(),
|
text_styles: tstyles.iter().filter_map(|n| doc.text_styles.get(n).cloned()).collect(),
|
||||||
dim_styles: dstyles.iter().filter_map(|n| doc.dim_styles.get(n).cloned()).collect(),
|
dim_styles: dstyles.iter().filter_map(|n| doc.dim_styles.get(n).cloned()).collect(),
|
||||||
blocks,
|
blocks,
|
||||||
|
ext_objects,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Breadth-first collect of every object reachable from extension-dictionary
|
||||||
|
/// `root` (dictionary entries, nested xdictionaries, dictionary defaults),
|
||||||
|
/// returned as `(source_handle, object)` pairs. Cycle-safe.
|
||||||
|
fn collect_ext_subtree(
|
||||||
|
doc: &acadrust::CadDocument,
|
||||||
|
root: acadrust::Handle,
|
||||||
|
) -> Vec<(acadrust::Handle, acadrust::objects::ObjectType)> {
|
||||||
|
use acadrust::objects::ObjectType;
|
||||||
|
use rustc_hash::FxHashSet;
|
||||||
|
let mut seen: FxHashSet<acadrust::Handle> = FxHashSet::default();
|
||||||
|
let mut queue = vec![root];
|
||||||
|
let mut out = Vec::new();
|
||||||
|
while let Some(h) = queue.pop() {
|
||||||
|
if h.is_null() || !seen.insert(h) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(obj) = doc.objects.get(&h) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// Enqueue children referenced by this object.
|
||||||
|
match obj {
|
||||||
|
ObjectType::Dictionary(d) => {
|
||||||
|
queue.extend(d.entries.iter().map(|(_, ch)| *ch));
|
||||||
|
if let Some(x) = d.xdictionary_handle {
|
||||||
|
queue.push(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ObjectType::DictionaryWithDefault(d) => {
|
||||||
|
queue.extend(d.entries.iter().map(|(_, ch)| *ch));
|
||||||
|
queue.push(d.default_handle);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
out.push((h, obj.clone()));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
/// Snapshot every block definition the `entities` reference through an
|
/// Snapshot every block definition the `entities` reference through an
|
||||||
/// INSERT, walking nested INSERTs transitively. Model/paper space and
|
/// INSERT, walking nested INSERTs transitively. Model/paper space and
|
||||||
/// xref blocks are skipped — those aren't portable definitions.
|
/// xref blocks are skipped — those aren't portable definitions.
|
||||||
|
|
|
||||||
|
|
@ -356,7 +356,7 @@ mod tests {
|
||||||
host.start_interactive(Box::new(PlacePoint { got_first: false }));
|
host.start_interactive(Box::new(PlacePoint { got_first: false }));
|
||||||
}
|
}
|
||||||
assert!(app.tabs[0].active_cmd.is_some());
|
assert!(app.tabs[0].active_cmd.is_some());
|
||||||
for pt in [glam::Vec3::new(0.0, 0.0, 0.0), glam::Vec3::new(5.0, 5.0, 0.0)] {
|
for pt in [glam::DVec3::new(0.0, 0.0, 0.0), glam::DVec3::new(5.0, 5.0, 0.0)] {
|
||||||
let r = app.tabs[0].active_cmd.as_mut().unwrap().on_point(pt);
|
let r = app.tabs[0].active_cmd.as_mut().unwrap().on_point(pt);
|
||||||
let _ = app.apply_cmd_result(r);
|
let _ = app.apply_cmd_result(r);
|
||||||
}
|
}
|
||||||
|
|
@ -410,7 +410,7 @@ mod tests {
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.on_entity_pick(target, glam::Vec3::new(3.0, 4.0, 0.0));
|
.on_entity_pick(target, glam::DVec3::new(3.0, 4.0, 0.0));
|
||||||
let _ = app.apply_cmd_result(r);
|
let _ = app.apply_cmd_result(r);
|
||||||
// Original point + the mark the command committed.
|
// Original point + the mark the command committed.
|
||||||
assert_eq!(app.tabs[0].scene.document.entities().count(), 2);
|
assert_eq!(app.tabs[0].scene.document.entities().count(), 2);
|
||||||
|
|
|
||||||
|
|
@ -6670,9 +6670,6 @@ impl Scene {
|
||||||
if !x.is_finite() || !y.is_finite() {
|
if !x.is_finite() || !y.is_finite() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if x.abs() > lim || y.abs() > lim {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
sx += x as f64;
|
sx += x as f64;
|
||||||
sy += y as f64;
|
sy += y as f64;
|
||||||
n += 1;
|
n += 1;
|
||||||
|
|
@ -6689,9 +6686,26 @@ impl Scene {
|
||||||
return;
|
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
|
// IQR-based reject only kicks in with enough samples for the quartiles
|
||||||
// to be meaningful. Below that, the absolute `lim` filter is the only
|
// to be meaningful. Below that, the centre-relative `lim` filter is the
|
||||||
// gate (legacy behavior).
|
// only gate (legacy behavior).
|
||||||
let (rx_lo, rx_hi, ry_lo, ry_hi) = if cents.len() >= 8 {
|
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 xs: Vec<f32> = cents.iter().map(|c| c.cx).collect();
|
||||||
let mut ys: Vec<f32> = cents.iter().map(|c| c.cy).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;
|
let dy = (q3y - q1y).max(1.0) * K;
|
||||||
(q1x - dx, q3x + dx, q1y - dy, q3y + dy)
|
(q1x - dx, q3x + dx, q1y - dy, q3y + dy)
|
||||||
} else {
|
} else {
|
||||||
(-lim, lim, -lim, lim)
|
(mcx - lim, mcx + lim, mcy - lim, mcy + lim)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut min = glam::Vec3::splat(f32::MAX);
|
let mut min = glam::Vec3::splat(f32::MAX);
|
||||||
|
|
@ -6726,7 +6740,7 @@ impl Scene {
|
||||||
if !x.is_finite() || !y.is_finite() || !z.is_finite() {
|
if !x.is_finite() || !y.is_finite() || !z.is_finite() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if x.abs() > lim || y.abs() > lim {
|
if (x - mcx).abs() > lim || (y - mcy).abs() > lim {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
min = min.min(glam::Vec3::new(x, y, z));
|
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
|
// clip the expanded block geometry to the boundary polygon so
|
||||||
// only the portion inside the clip is drawn.
|
// only the portion inside the clip is drawn.
|
||||||
if let Some(sf) = pick::xclip::insert_spatial_filter(document, ins) {
|
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);
|
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 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]);
|
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"));
|
let eye = glam::DVec3::ZERO;
|
||||||
assert_eq!(click_hit(cursor, std::slice::from_ref(&far), vp, bounds), None);
|
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.
|
// 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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,21 @@ pub fn world_clip_polygon(
|
||||||
ins: &Insert,
|
ins: &Insert,
|
||||||
world_offset: [f64; 3],
|
world_offset: [f64; 3],
|
||||||
) -> Vec<[f32; 2]> {
|
) -> 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 xform = ins.get_transform();
|
||||||
let inv_block = &sf.inverse_block_transform;
|
let inv_block = &sf.inverse_block_transform;
|
||||||
let [ox, oy, _] = world_offset;
|
let [ox, oy, _] = world_offset;
|
||||||
|
|
@ -82,7 +97,7 @@ pub fn world_clip_polygon(
|
||||||
.map(|[x, y]| {
|
.map(|[x, y]| {
|
||||||
let block = inv_block.transform_point(Vector3::new(x, y, 0.0));
|
let block = inv_block.transform_point(Vector3::new(x, y, 0.0));
|
||||||
let w = xform.apply(block);
|
let w = xform.apply(block);
|
||||||
[(w.x - ox) as f32, (w.y - oy) as f32]
|
[w.x - ox, w.y - oy]
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
@ -91,25 +106,102 @@ pub fn world_clip_polygon(
|
||||||
/// world_offset-subtracted). Polylines are split into NaN-separated inside
|
/// world_offset-subtracted). Polylines are split into NaN-separated inside
|
||||||
/// runs; fill triangles are clipped against the polygon; snap / key vertices
|
/// runs; fill triangles are clipped against the polygon; snap / key vertices
|
||||||
/// outside the boundary are dropped. Wires left with no geometry are removed.
|
/// 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 {
|
if poly.len() < 3 {
|
||||||
return;
|
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() {
|
for w in wires.iter_mut() {
|
||||||
if !w.points.is_empty() {
|
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() {
|
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
|
w.key_vertices
|
||||||
.retain(|v| point_in_poly(v[0] as f32, v[1] 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 as f32, p.y as f32, poly));
|
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);
|
w.aabb = recompute_aabb(&w.points, &w.fill_tris);
|
||||||
}
|
}
|
||||||
wires.retain(|w| !w.points.is_empty() || !w.fill_tris.is_empty());
|
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`.
|
/// Clip a hatch fill boundary to `poly`.
|
||||||
///
|
///
|
||||||
/// `boundary` is a hatch's NaN-separated loops in f32 offsets from
|
/// `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);
|
ins.common.xdictionary_handle = Some(h_xdict);
|
||||||
|
|
||||||
let resolved = insert_spatial_filter(&doc, &ins).expect("filter resolves");
|
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);
|
assert_eq!(poly.len(), 4);
|
||||||
|
|
||||||
// A polyline half inside, half outside the 0..10 square.
|
// 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));
|
assert!(out.iter().all(|p| p[0] <= 10.0 + 1e-3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue