forked from 1b-agent/opencadstudio-upstream
Merge pull request #1475 from ramox81:codex/xclip-command
This commit is contained in:
commit
58671b191c
10 files changed
+948
-16
No files matched your search
@@ -169,9 +169,10 @@ pub(crate) fn expand_block_object(
|
||||
}
|
||||
let frame_mode =
|
||||
crate::scene::frame::mode(document, crate::scene::frame::FrameKind::Xclip);
|
||||
let outline = pick::xclip::clip_outline_world(document, filter, &transform);
|
||||
if polygon.len() >= 3 {
|
||||
let mut frame = pick::xclip::frame_wire(
|
||||
&polygon,
|
||||
&outline,
|
||||
owner.value().to_string(),
|
||||
style.color,
|
||||
selected,
|
||||
|
||||
@@ -45,6 +45,259 @@ pub fn insert_spatial_filter<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Editing (XCLIP) ─────────────────────────────────────────────────────────
|
||||
|
||||
/// The filter's handle, when the insert has one (enabled or not).
|
||||
pub fn filter_handle(doc: &CadDocument, insert: Handle) -> Option<Handle> {
|
||||
let Some(acadrust::EntityType::Insert(ins)) = doc.get_entity(insert) else {
|
||||
return None;
|
||||
};
|
||||
let acad_filter = dict_entry(doc, ins.common.xdictionary_handle?, "ACAD_FILTER")?;
|
||||
let spatial = dict_entry(doc, acad_filter, "SPATIAL")?;
|
||||
matches!(doc.objects.get(&spatial), Some(ObjectType::SpatialFilter(_))).then_some(spatial)
|
||||
}
|
||||
|
||||
const ROUNDTRIP: &str = "ACAD_XREC_ROUNDTRIP";
|
||||
const INVERTED: &str = "ACAD_INVERTEDCLIP_ROUNDTRIP";
|
||||
const INVERTED_COMPARE: &str = "ACAD_INVERTEDCLIP_ROUNDTRIP_COMPARE";
|
||||
|
||||
/// The boundary an inverted clip was drawn with (WCS), kept beside the
|
||||
/// filter's ring in an ACAD_XREC_ROUNDTRIP record as the reference keeps it.
|
||||
pub fn inverted_boundary(doc: &CadDocument, filter: &SpatialFilter) -> Option<Vec<[f64; 2]>> {
|
||||
let xdict = match doc.objects.get(&filter.handle)? {
|
||||
ObjectType::SpatialFilter(_) => filter_xdictionary(doc, filter.handle)?,
|
||||
_ => return None,
|
||||
};
|
||||
let record = dict_entry(doc, xdict, ROUNDTRIP)?;
|
||||
let Some(ObjectType::XRecord(x)) = doc.objects.get(&record) else {
|
||||
return None;
|
||||
};
|
||||
let mut points = Vec::new();
|
||||
let mut inside = false;
|
||||
for entry in &x.entries {
|
||||
match &entry.value {
|
||||
acadrust::objects::XRecordValue::String(s) if entry.code == 102 => {
|
||||
inside = s == INVERTED;
|
||||
}
|
||||
acadrust::objects::XRecordValue::Point3D(px, py, _) if inside && entry.code == 10 => {
|
||||
points.push([*px, *py]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(points.len() >= 2).then_some(points)
|
||||
}
|
||||
|
||||
/// Filters keep their extension dictionary in the document's owner map: the
|
||||
/// dictionary owned by the filter.
|
||||
fn filter_xdictionary(doc: &CadDocument, filter: Handle) -> Option<Handle> {
|
||||
doc.objects.iter().find_map(|(h, o)| match o {
|
||||
ObjectType::Dictionary(d) if d.owner == filter => Some(*h),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The inverted boundary drawn as the XCLIP frame and generated polyline,
|
||||
/// else the stored one; rectangles as four corners (WCS through the stored
|
||||
/// transforms).
|
||||
pub fn clip_outline_world(doc: &CadDocument, filter: &SpatialFilter, xform: &Transform) -> Vec<[f64; 2]> {
|
||||
match inverted_boundary(doc, filter) {
|
||||
Some(points) => {
|
||||
let mut outline = filter.clone();
|
||||
outline.boundary_points = points
|
||||
.iter()
|
||||
.map(|p| acadrust::types::Vector2::new(p[0], p[1]))
|
||||
.collect();
|
||||
world_clip_polygon_for_transform(&outline, xform)
|
||||
}
|
||||
None => world_clip_polygon_for_transform(filter, xform),
|
||||
}
|
||||
}
|
||||
|
||||
/// The ring that shows only what lies outside `inner` (WCS, drawn order):
|
||||
/// the block's extents grown by 5 % on each side, joined to the boundary by
|
||||
/// a hairline slit — the polygon the reference stores for an inverted clip.
|
||||
// ponytail: the slit runs left from the boundary's last corner; a concave
|
||||
// boundary it would cross clips wrongly there.
|
||||
pub fn inverted_ring(inner: &[[f64; 2]], extents: ([f64; 2], [f64; 2])) -> Vec<[f64; 2]> {
|
||||
let (lo, hi) = extents;
|
||||
let size = (hi[0] - lo[0]).max(hi[1] - lo[1]).max(1e-9);
|
||||
let pad = size * 0.05;
|
||||
let (x0, y0, x1, y1) = (
|
||||
lo[0].min(inner.iter().map(|p| p[0]).fold(f64::MAX, f64::min)) - pad,
|
||||
lo[1].min(inner.iter().map(|p| p[1]).fold(f64::MAX, f64::min)) - pad,
|
||||
hi[0].max(inner.iter().map(|p| p[0]).fold(f64::MIN, f64::max)) + pad,
|
||||
hi[1].max(inner.iter().map(|p| p[1]).fold(f64::MIN, f64::max)) + pad,
|
||||
);
|
||||
let eps = size * 4e-6;
|
||||
let last = *inner.last().expect("a boundary has points");
|
||||
let mut ring = inner.to_vec();
|
||||
ring.extend([
|
||||
[x0, last[1]],
|
||||
[x0, y1],
|
||||
[x1, y1],
|
||||
[x1, y0],
|
||||
[x0, y0],
|
||||
[x0, last[1] - eps],
|
||||
[last[0], last[1] - eps],
|
||||
]);
|
||||
ring
|
||||
}
|
||||
|
||||
/// A rectangle's two corners as its four, in drawing order.
|
||||
pub fn rectangle_corners(a: [f64; 2], b: [f64; 2]) -> Vec<[f64; 2]> {
|
||||
vec![a, [b[0], a[1]], b, [a[0], b[1]]]
|
||||
}
|
||||
|
||||
/// Remove an insert's clip (the filter and what it owns).
|
||||
pub fn remove_insert_clip(doc: &mut CadDocument, insert: Handle) -> bool {
|
||||
let Some(spatial) = filter_handle(doc, insert) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(xdict) = filter_xdictionary(doc, spatial) {
|
||||
let children: Vec<Handle> = match doc.objects.get(&xdict) {
|
||||
Some(ObjectType::Dictionary(d)) => d.entries.iter().map(|(_, h)| *h).collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for child in children {
|
||||
doc.objects.remove(&child);
|
||||
}
|
||||
doc.objects.remove(&xdict);
|
||||
}
|
||||
doc.objects.remove(&spatial);
|
||||
for object in doc.objects.values_mut() {
|
||||
if let ObjectType::Dictionary(d) = object {
|
||||
d.entries.retain(|(_, h)| *h != spatial);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Turn an insert's clip display on or off; false when it has none.
|
||||
pub fn set_insert_clip_enabled(doc: &mut CadDocument, insert: Handle, on: bool) -> bool {
|
||||
let Some(spatial) = filter_handle(doc, insert) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(ObjectType::SpatialFilter(f)) = doc.objects.get_mut(&spatial) {
|
||||
f.display_enabled = on;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Front and back clipping planes (None = off) of an insert's clip.
|
||||
pub fn set_insert_clip_depth(doc: &mut CadDocument, insert: Handle, front: Option<f64>, back: Option<f64>) -> bool {
|
||||
let Some(spatial) = filter_handle(doc, insert) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(ObjectType::SpatialFilter(f)) = doc.objects.get_mut(&spatial) {
|
||||
f.front_clip = front;
|
||||
f.back_clip = back;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// A new clip boundary on an insert, replacing any old one. `boundary` is in
|
||||
/// WCS (two points = rectangle corners); `inverted` hides what is inside,
|
||||
/// stored as the reference stores it (the ring plus the drawn boundary).
|
||||
pub fn set_insert_clip(
|
||||
doc: &mut CadDocument,
|
||||
insert: Handle,
|
||||
boundary: &[[f64; 2]],
|
||||
inverted: bool,
|
||||
extents: ([f64; 2], [f64; 2]),
|
||||
) -> bool {
|
||||
use acadrust::objects::{Dictionary, XRecord, XRecordEntry, XRecordValue};
|
||||
use acadrust::types::{Matrix4, Vector2};
|
||||
let Some(acadrust::EntityType::Insert(ins)) = doc.get_entity(insert) else {
|
||||
return false;
|
||||
};
|
||||
let t = ins.get_transform().matrix.m;
|
||||
let forward = glam::DMat4::from_cols_array_2d(&[
|
||||
[t[0][0], t[1][0], t[2][0], t[3][0]],
|
||||
[t[0][1], t[1][1], t[2][1], t[3][1]],
|
||||
[t[0][2], t[1][2], t[2][2], t[3][2]],
|
||||
[t[0][3], t[1][3], t[2][3], t[3][3]],
|
||||
]);
|
||||
let inv = forward.inverse().to_cols_array_2d();
|
||||
let inverse = Matrix4 {
|
||||
m: [
|
||||
[inv[0][0], inv[1][0], inv[2][0], inv[3][0]],
|
||||
[inv[0][1], inv[1][1], inv[2][1], inv[3][1]],
|
||||
[inv[0][2], inv[1][2], inv[2][2], inv[3][2]],
|
||||
[inv[0][3], inv[1][3], inv[2][3], inv[3][3]],
|
||||
],
|
||||
};
|
||||
let existing_xdict = ins.common.xdictionary_handle;
|
||||
remove_insert_clip(doc, insert);
|
||||
|
||||
// insert → extension dictionary → ACAD_FILTER → SPATIAL
|
||||
let xdict = match existing_xdict.filter(|h| matches!(doc.objects.get(h), Some(ObjectType::Dictionary(_)))) {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
let h = doc.allocate_handle();
|
||||
let mut d = Dictionary::new();
|
||||
(d.handle, d.owner, d.hard_owner) = (h, insert, true);
|
||||
doc.objects.insert(h, ObjectType::Dictionary(d));
|
||||
if let Some(entity) = doc.get_entity_mut(insert) {
|
||||
entity.common_mut().xdictionary_handle = Some(h);
|
||||
}
|
||||
h
|
||||
}
|
||||
};
|
||||
let acad_filter = match dict_entry(doc, xdict, "ACAD_FILTER") {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
let h = doc.allocate_handle();
|
||||
let mut d = Dictionary::new();
|
||||
(d.handle, d.owner, d.hard_owner) = (h, xdict, true);
|
||||
doc.objects.insert(h, ObjectType::Dictionary(d));
|
||||
if let Some(ObjectType::Dictionary(parent)) = doc.objects.get_mut(&xdict) {
|
||||
parent.add_entry("ACAD_FILTER", h);
|
||||
}
|
||||
h
|
||||
}
|
||||
};
|
||||
let stored: Vec<[f64; 2]> = if inverted {
|
||||
let inner = if boundary.len() == 2 {
|
||||
rectangle_corners(boundary[0], boundary[1])
|
||||
} else {
|
||||
boundary.to_vec()
|
||||
};
|
||||
inverted_ring(&inner, extents)
|
||||
} else {
|
||||
boundary.to_vec()
|
||||
};
|
||||
let spatial = doc.allocate_handle();
|
||||
let mut filter = SpatialFilter::new();
|
||||
filter.handle = spatial;
|
||||
filter.owner = acad_filter;
|
||||
filter.boundary_points = stored.iter().map(|p| Vector2::new(p[0], p[1])).collect();
|
||||
filter.inverse_block_transform = inverse;
|
||||
filter.clip_bound_transform = Matrix4::identity();
|
||||
doc.objects.insert(spatial, ObjectType::SpatialFilter(filter));
|
||||
if let Some(ObjectType::Dictionary(parent)) = doc.objects.get_mut(&acad_filter) {
|
||||
parent.add_entry("SPATIAL", spatial);
|
||||
}
|
||||
if inverted {
|
||||
let fdict = doc.allocate_handle();
|
||||
let record = doc.allocate_handle();
|
||||
let mut d = Dictionary::new();
|
||||
(d.handle, d.owner, d.hard_owner) = (fdict, spatial, true);
|
||||
d.add_entry(ROUNDTRIP, record);
|
||||
doc.objects.insert(fdict, ObjectType::Dictionary(d));
|
||||
let mut x = XRecord::new();
|
||||
(x.handle, x.owner) = (record, fdict);
|
||||
x.cloning_flags = acadrust::objects::DictionaryCloningFlags::KeepExisting;
|
||||
let point = |p: &[f64; 2]| XRecordEntry::new(10, XRecordValue::Point3D(p[0], p[1], 0.0));
|
||||
x.entries.push(XRecordEntry::string(102, INVERTED));
|
||||
x.entries.extend(boundary.iter().map(point));
|
||||
x.entries.push(XRecordEntry::string(102, INVERTED_COMPARE));
|
||||
x.entries.extend(stored.iter().map(point));
|
||||
doc.objects.insert(record, ObjectType::XRecord(x));
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn dict_entry(doc: &CadDocument, dict: Handle, key: &str) -> Option<Handle> {
|
||||
match doc.objects.get(&dict)? {
|
||||
ObjectType::Dictionary(d) => {
|
||||
|
||||
Reference in new issue
Block a user