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
@@ -479,6 +479,42 @@ impl OpenCADStudio {
|
||||
StepInput::SelectionComplete(kept)
|
||||
}
|
||||
|
||||
/// Keeps only block references in a completed selection when the active
|
||||
/// command asks for that (XCLIP), deselecting the rest and saying how many
|
||||
/// were ineligible.
|
||||
fn filter_block_reference_selection(&mut self, input: StepInput) -> StepInput {
|
||||
let StepInput::SelectionComplete(handles) = input else {
|
||||
return input;
|
||||
};
|
||||
let i = self.active_tab;
|
||||
if !self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|command| command.selection_keeps_block_references())
|
||||
{
|
||||
return StepInput::SelectionComplete(handles);
|
||||
}
|
||||
let (kept, dropped): (Vec<Handle>, Vec<Handle>) = handles.into_iter().partition(|h| {
|
||||
matches!(
|
||||
self.tabs[i].scene.document.get_entity(*h),
|
||||
Some(acadrust::EntityType::Insert(_))
|
||||
)
|
||||
});
|
||||
for handle in &dropped {
|
||||
self.tabs[i].scene.deselect_entity(*handle);
|
||||
}
|
||||
if !dropped.is_empty() {
|
||||
let count = dropped.len();
|
||||
self.command_line.push_info(&if count == 1 {
|
||||
"1 was ineligible for clipping.".to_string()
|
||||
} else {
|
||||
format!("{count} were ineligible for clipping.")
|
||||
});
|
||||
self.refresh_properties();
|
||||
}
|
||||
StepInput::SelectionComplete(kept)
|
||||
}
|
||||
|
||||
/// `feed_command`, also reporting whether the step actually took the input.
|
||||
/// Only a `Text` token can come back unclaimed (`on_text_input` returning
|
||||
/// `None`), which is what lets the caller read it as something else — a
|
||||
@@ -488,6 +524,18 @@ impl OpenCADStudio {
|
||||
// the rest leave the selection and the count of what went is reported,
|
||||
// as the reference does while gathering.
|
||||
let input = self.filter_associative_dimension_selection(input);
|
||||
let input = self.filter_block_reference_selection(input);
|
||||
// XCLIP / CLIP learn which chosen references carry a clip already.
|
||||
{
|
||||
let i = self.active_tab;
|
||||
let tab = &mut self.tabs[i];
|
||||
if let Some(command) = tab.active_cmd.as_mut() {
|
||||
let document = &tab.scene.document;
|
||||
command.inject_clipped(&|h| {
|
||||
crate::scene::pick::xclip::filter_handle(document, h).is_some()
|
||||
});
|
||||
}
|
||||
}
|
||||
// Selection keywords (P / PREVIOUS, L / LAST) consume the token
|
||||
// before it reaches the command (#426).
|
||||
if let StepInput::Text(s) = &input {
|
||||
@@ -3892,6 +3940,12 @@ impl OpenCADStudio {
|
||||
}
|
||||
self.refresh_properties();
|
||||
}
|
||||
CmdResult::XClip { inserts, action } => {
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.apply_xclip(i, inserts, action);
|
||||
}
|
||||
CmdResult::OpenPdfImportSettings => self.open_pdf_import_settings(),
|
||||
CmdResult::PdfImportFile(import) => {
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
|
||||
@@ -715,9 +715,39 @@ impl OpenCADStudio {
|
||||
Err(_) => self.command_line.push_error(&format!("{path} not found.")),
|
||||
}
|
||||
}
|
||||
"PDFCLIP" | "CLIP" => {
|
||||
"PDFCLIP" => {
|
||||
use crate::command::CadCommand;
|
||||
let command = crate::modules::insert::pdf_clip::PdfClipCommand::new(cmd == "CLIP");
|
||||
let command = crate::modules::insert::pdf_clip::PdfClipCommand::new();
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
"CLIP" => {
|
||||
use crate::command::CadCommand;
|
||||
let command = crate::modules::insert::xclip::ClipCommand::new();
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
// XCLIP takes block references chosen beforehand; others are
|
||||
// left out.
|
||||
"XCLIP" => {
|
||||
use crate::command::CadCommand;
|
||||
use crate::modules::insert::xclip::XclipCommand;
|
||||
let inserts: Vec<acadrust::Handle> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.iter()
|
||||
.filter(|(_, e)| matches!(e, acadrust::EntityType::Insert(_)))
|
||||
.map(|(h, _)| *h)
|
||||
.collect();
|
||||
let command = if inserts.is_empty() {
|
||||
XclipCommand::new()
|
||||
} else {
|
||||
let clipped = inserts.iter().any(|h| {
|
||||
crate::scene::pick::xclip::filter_handle(&self.tabs[i].scene.document, *h)
|
||||
.is_some()
|
||||
});
|
||||
XclipCommand::for_inserts(inserts, clipped)
|
||||
};
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ mod layers;
|
||||
pub(crate) mod pdf_import;
|
||||
mod pdf_underlay;
|
||||
mod pdf_dialogs;
|
||||
mod xclip;
|
||||
mod plotvars;
|
||||
mod styleprops;
|
||||
mod view;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
//! XCLIP's edits on block references, as one undo step.
|
||||
|
||||
use super::*;
|
||||
use crate::modules::insert::xclip::XclipAction;
|
||||
use crate::scene::pick::xclip as clip;
|
||||
|
||||
impl OpenCADStudio {
|
||||
/// A block reference's extents in WCS: its block's objects through the
|
||||
/// insert transform.
|
||||
fn insert_extents(&self, i: usize, insert: acadrust::Handle) -> ([f64; 2], [f64; 2]) {
|
||||
let document = &self.tabs[i].scene.document;
|
||||
let Some(acadrust::EntityType::Insert(ins)) = document.get_entity(insert) else {
|
||||
return ([0.0; 2], [0.0; 2]);
|
||||
};
|
||||
let xform = ins.get_transform();
|
||||
let (mut lo, mut hi) = ([f64::MAX; 2], [f64::MIN; 2]);
|
||||
for e in document.entities_in_block(&ins.block_name) {
|
||||
let (a, b) = crate::scene::convert::tess::entity_bounds_in(document, e);
|
||||
for x in [a[0], b[0]] {
|
||||
for y in [a[1], b[1]] {
|
||||
let w = xform.apply(acadrust::types::Vector3::new(x, y, a[2]));
|
||||
lo = [lo[0].min(w.x), lo[1].min(w.y)];
|
||||
hi = [hi[0].max(w.x), hi[1].max(w.y)];
|
||||
}
|
||||
}
|
||||
}
|
||||
if lo[0] > hi[0] {
|
||||
let p = xform.apply(acadrust::types::Vector3::new(0.0, 0.0, 0.0));
|
||||
return ([p.x, p.y], [p.x, p.y]);
|
||||
}
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
pub(in crate::app) fn apply_xclip(
|
||||
&mut self,
|
||||
i: usize,
|
||||
inserts: Vec<acadrust::Handle>,
|
||||
action: XclipAction,
|
||||
) {
|
||||
self.push_undo_snapshot(i, "XCLIP");
|
||||
let extents: Vec<_> = inserts.iter().map(|h| self.insert_extents(i, *h)).collect();
|
||||
let current_layer = self.tabs[i].scene.document.header.current_layer_name.clone();
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
let mut polylines = Vec::new();
|
||||
for (n, insert) in inserts.iter().enumerate() {
|
||||
let doc = &mut scene.document;
|
||||
match &action {
|
||||
XclipAction::Enable(on) => {
|
||||
clip::set_insert_clip_enabled(doc, *insert, *on);
|
||||
}
|
||||
XclipAction::Delete => {
|
||||
clip::remove_insert_clip(doc, *insert);
|
||||
}
|
||||
XclipAction::Depth { front, back } => {
|
||||
clip::set_insert_clip_depth(doc, *insert, *front, *back);
|
||||
}
|
||||
XclipAction::New { boundary, inverted } => {
|
||||
clip::set_insert_clip(doc, *insert, boundary, *inverted, extents[n]);
|
||||
}
|
||||
XclipAction::Polyline => {
|
||||
let Some(spatial) = clip::filter_handle(doc, *insert) else {
|
||||
continue;
|
||||
};
|
||||
let (Some(acadrust::objects::ObjectType::SpatialFilter(filter)), Some(acadrust::EntityType::Insert(ins))) =
|
||||
(doc.objects.get(&spatial), doc.get_entity(*insert))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let outline = clip::clip_outline_world(doc, filter, &ins.get_transform());
|
||||
let mut pl = acadrust::entities::LwPolyline::from_points(
|
||||
outline
|
||||
.iter()
|
||||
.map(|p| acadrust::types::Vector2::new(p[0], p[1]))
|
||||
.collect(),
|
||||
);
|
||||
pl.is_closed = true;
|
||||
if !current_layer.is_empty() {
|
||||
pl.common.layer = current_layer.clone();
|
||||
}
|
||||
polylines.push(acadrust::EntityType::LwPolyline(pl));
|
||||
}
|
||||
}
|
||||
scene.reseed_derived_caches(*insert);
|
||||
}
|
||||
for pl in polylines {
|
||||
scene.add_entity(pl);
|
||||
}
|
||||
let changes: Vec<_> = inserts
|
||||
.iter()
|
||||
.map(|h| (*h, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
scene.bump_entities(&changes);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
}
|
||||
@@ -1556,6 +1556,11 @@ pub enum CmdResult {
|
||||
BatchCopy(Vec<Handle>, Vec<EntityTransform>),
|
||||
/// Erase `handle` and replace with new entities; command stays active.
|
||||
ReplaceEntity(Handle, Vec<EntityType>),
|
||||
/// XCLIP on block references; ends the command.
|
||||
XClip {
|
||||
inserts: Vec<Handle>,
|
||||
action: crate::modules::insert::xclip::XclipAction,
|
||||
},
|
||||
/// Open the PDF Import Settings dialog; the command keeps its prompt.
|
||||
OpenPdfImportSettings,
|
||||
/// Import a page of a file (PDFIMPORT File); ends the command.
|
||||
@@ -2805,6 +2810,16 @@ pub trait CadCommand: Send {
|
||||
false
|
||||
}
|
||||
|
||||
/// The command takes block references only (XCLIP): the host drops the
|
||||
/// rest and says how many were ineligible.
|
||||
fn selection_keeps_block_references(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Before each input the host says which block references carry a clip
|
||||
/// boundary.
|
||||
fn inject_clipped(&mut self, _clipped: &dyn Fn(Handle) -> bool) {}
|
||||
|
||||
/// Called after a selection action completes while `is_selection_gathering` is true.
|
||||
/// `handles` is the full set of currently selected entities.
|
||||
/// Return `Relaunch` to fire the pending command, or `NeedPoint` to keep gathering.
|
||||
|
||||
@@ -26,7 +26,7 @@ pub(crate) mod wblock;
|
||||
mod xadjust;
|
||||
pub(crate) mod xattach;
|
||||
pub(crate) mod xref_cmd;
|
||||
mod xclip;
|
||||
pub(crate) mod xclip;
|
||||
|
||||
use crate::modules::{CadModule, IconKind, RibbonGroup, RibbonItem};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// PDFCLIP / CLIP — clip a PDF underlay to a boundary.
|
||||
// PDFCLIP — clip a PDF underlay to a boundary (CLIP continues here for a
|
||||
// PDF underlay).
|
||||
//
|
||||
// Select PDF to clip: (CLIP: Select Object to clip:)
|
||||
// Select PDF to clip:
|
||||
// Enter PDF clipping option [ON/OFF/Delete/New boundary] <New boundary>:
|
||||
// Delete old boundary? [Yes/No] <Yes>: (when one exists)
|
||||
// Outside mode - Objects outside boundary will be hidden.
|
||||
@@ -31,8 +32,6 @@ enum Step {
|
||||
}
|
||||
|
||||
pub struct PdfClipCommand {
|
||||
/// CLIP asks for any object; PDFCLIP for a PDF.
|
||||
generic: bool,
|
||||
step: Step,
|
||||
handle: Handle,
|
||||
underlay: Option<Underlay>,
|
||||
@@ -46,9 +45,8 @@ const INSIDE: &str = "Inside mode - Objects inside boundary will be hidden.";
|
||||
const BOUNDARY: &str = "Specify clipping boundary or select invert option:";
|
||||
|
||||
impl PdfClipCommand {
|
||||
pub fn new(generic: bool) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
generic,
|
||||
step: Step::Select,
|
||||
handle: Handle::NULL,
|
||||
underlay: None,
|
||||
@@ -62,7 +60,7 @@ impl PdfClipCommand {
|
||||
/// Create Clipping Boundary): the New boundary branch at once. Returns the
|
||||
/// command and the result of its first step.
|
||||
pub fn new_boundary(handle: Handle, underlay: Underlay) -> (Self, CmdResult) {
|
||||
let mut command = Self::new(false);
|
||||
let mut command = Self::new();
|
||||
command.handle = handle;
|
||||
command.underlay = Some(underlay);
|
||||
command.step = Step::Option;
|
||||
@@ -70,6 +68,15 @@ impl PdfClipCommand {
|
||||
(command, first)
|
||||
}
|
||||
|
||||
/// An underlay CLIP picked: straight to the clipping option.
|
||||
pub fn for_underlay(handle: Handle, underlay: Underlay) -> Self {
|
||||
let mut command = Self::new();
|
||||
command.handle = handle;
|
||||
command.underlay = Some(underlay);
|
||||
command.step = Step::Option;
|
||||
command
|
||||
}
|
||||
|
||||
fn finish(&self, underlay: Underlay) -> CmdResult {
|
||||
CmdResult::UpdateEntityAndFinish {
|
||||
handle: self.handle,
|
||||
@@ -167,12 +174,11 @@ impl PdfClipCommand {
|
||||
|
||||
impl CadCommand for PdfClipCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
if self.generic { "CLIP" } else { "PDFCLIP" }
|
||||
"PDFCLIP"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
Step::Select if self.generic => "Select Object to clip:".to_string(),
|
||||
Step::Select => "Select PDF to clip:".to_string(),
|
||||
Step::Option => {
|
||||
"Enter PDF clipping option [ON/OFF/Delete/New boundary] <New boundary>:".to_string()
|
||||
@@ -343,5 +349,5 @@ impl CadCommand for PdfClipCommand {
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["PDFCLIP", "CLIP"]
|
||||
names: &["PDFCLIP"]
|
||||
});
|
||||
+478
-2
@@ -1,10 +1,486 @@
|
||||
// XCLIP — clip block references (and drawing references) to a boundary.
|
||||
//
|
||||
// Select objects: (block references only;
|
||||
// "N was ineligible for clipping.")
|
||||
// Enter clipping option
|
||||
// [ON/OFF/Clipdepth/Delete/generate Polyline/New boundary] <New>:
|
||||
// Delete old boundary(s)? [Yes/No] <Yes>: (New, when one exists)
|
||||
// Outside mode - Objects outside boundary will be hidden.
|
||||
// Specify clipping boundary or select invert option:
|
||||
// [Select polyline/Polygonal/Rectangular/Invert clip] <Rectangular>:
|
||||
// Specify first corner: / Specify opposite corner:
|
||||
// Specify first point: / Specify next point or [Undo]:
|
||||
// Specify front clip point or [Distance/Remove]:
|
||||
//
|
||||
// CLIP asks for one object: a block reference continues as XCLIP, a PDF
|
||||
// underlay as PDFCLIP, anything else is "*Invalid selection*".
|
||||
|
||||
use acadrust::entities::UnderlayType;
|
||||
use acadrust::types::Handle;
|
||||
use acadrust::EntityType;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||
use crate::modules::insert::pdf_clip::PdfClipCommand;
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/xclip.svg"));
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "XCLIP",
|
||||
id: "CLIP",
|
||||
label: "Clip",
|
||||
icon: ICON,
|
||||
event: ModuleEvent::Command("XCLIP".to_string()),
|
||||
event: ModuleEvent::Command("CLIP".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// What XCLIP does to the chosen block references.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum XclipAction {
|
||||
Enable(bool),
|
||||
Delete,
|
||||
Polyline,
|
||||
/// Boundary in WCS (two points = rectangle corners).
|
||||
New { boundary: Vec<[f64; 2]>, inverted: bool },
|
||||
/// Front and back clipping planes; `None` removes one.
|
||||
Depth { front: Option<f64>, back: Option<f64> },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Step {
|
||||
Select,
|
||||
Option,
|
||||
DeleteOld,
|
||||
Mode,
|
||||
RectFirst,
|
||||
RectSecond,
|
||||
PolyPoints,
|
||||
Polyline,
|
||||
Front,
|
||||
FrontDistance,
|
||||
Back,
|
||||
BackDistance,
|
||||
}
|
||||
|
||||
const OPTION: &str = "Enter clipping option\n[ON/OFF/Clipdepth/Delete/generate Polyline/New boundary] <New>:";
|
||||
const OUTSIDE: &str = "Outside mode - Objects outside boundary will be hidden.";
|
||||
const INSIDE: &str = "Inside mode - Objects inside boundary will be hidden.";
|
||||
const BOUNDARY: &str = "Specify clipping boundary or select invert option:";
|
||||
|
||||
pub struct XclipCommand {
|
||||
step: Step,
|
||||
inserts: Vec<Handle>,
|
||||
/// Some chosen reference already has a clip boundary.
|
||||
clipped: bool,
|
||||
inverted: bool,
|
||||
points: Vec<DVec3>,
|
||||
picked: Option<EntityType>,
|
||||
front: Option<f64>,
|
||||
}
|
||||
|
||||
impl XclipCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: Step::Select,
|
||||
inserts: Vec::new(),
|
||||
clipped: false,
|
||||
inverted: false,
|
||||
points: Vec::new(),
|
||||
picked: None,
|
||||
front: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The references chosen before the command (or by CLIP), with whether
|
||||
/// any of them is clipped already: straight to the option prompt.
|
||||
pub fn for_inserts(inserts: Vec<Handle>, clipped: bool) -> Self {
|
||||
Self {
|
||||
step: Step::Option,
|
||||
inserts,
|
||||
clipped,
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn act(&self, action: XclipAction) -> CmdResult {
|
||||
CmdResult::XClip {
|
||||
inserts: self.inserts.clone(),
|
||||
action,
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_mode(&mut self) -> CmdResult {
|
||||
self.step = Step::Mode;
|
||||
let mode = if self.inverted { INSIDE } else { OUTSIDE };
|
||||
CmdResult::ReportMeasurement(format!("{mode}\n{BOUNDARY}"))
|
||||
}
|
||||
|
||||
fn new_boundary(&self, world: Vec<DVec3>) -> CmdResult {
|
||||
self.act(XclipAction::New {
|
||||
boundary: world.iter().map(|p| [p.x, p.y]).collect(),
|
||||
inverted: self.inverted,
|
||||
})
|
||||
}
|
||||
|
||||
fn option(&mut self, text: &str) -> CmdResult {
|
||||
let token = text.trim().to_ascii_uppercase();
|
||||
let needs_boundary = matches!(
|
||||
token.as_str(),
|
||||
"ON" | "OFF" | "D" | "DELETE" | "P" | "POLYLINE" | "GENERATE POLYLINE" | "C" | "CLIPDEPTH"
|
||||
);
|
||||
if needs_boundary && !self.clipped {
|
||||
return CmdResult::ReportError("No clip boundary found.".to_string());
|
||||
}
|
||||
match token.as_str() {
|
||||
"ON" => self.act(XclipAction::Enable(true)),
|
||||
"OFF" => self.act(XclipAction::Enable(false)),
|
||||
"D" | "DELETE" => self.act(XclipAction::Delete),
|
||||
"P" | "POLYLINE" | "GENERATE POLYLINE" => self.act(XclipAction::Polyline),
|
||||
"C" | "CLIPDEPTH" => {
|
||||
self.step = Step::Front;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
"" | "N" | "NEW" | "NEW BOUNDARY" => {
|
||||
if self.clipped {
|
||||
self.step = Step::DeleteOld;
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
self.enter_mode()
|
||||
}
|
||||
}
|
||||
_ => CmdResult::ReportError("Invalid option keyword.".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn mode(&mut self, text: &str) -> CmdResult {
|
||||
match text.trim().to_ascii_uppercase().as_str() {
|
||||
"" | "R" | "RECTANGULAR" => {
|
||||
self.step = Step::RectFirst;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
"P" | "POLYGONAL" => {
|
||||
self.points.clear();
|
||||
self.step = Step::PolyPoints;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
"S" | "SELECT" | "SELECT POLYLINE" => {
|
||||
self.step = Step::Polyline;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
"I" | "INVERT" | "INVERT CLIP" => {
|
||||
self.inverted = !self.inverted;
|
||||
self.enter_mode()
|
||||
}
|
||||
_ => CmdResult::ReportError("Invalid option keyword.".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn depth(&mut self, text: &str) -> CmdResult {
|
||||
match (self.step, text.trim().to_ascii_uppercase().as_str()) {
|
||||
(_, "R" | "REMOVE") => self.act(XclipAction::Depth { front: None, back: None }),
|
||||
(Step::Front, "D" | "DISTANCE") => {
|
||||
self.step = Step::FrontDistance;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
(Step::Back, "D" | "DISTANCE") => {
|
||||
self.step = Step::BackDistance;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
(Step::FrontDistance | Step::BackDistance, value) => match value.parse::<f64>() {
|
||||
Ok(d) => self.take_depth(d),
|
||||
Err(_) => CmdResult::ReportError("Requires numeric distance.".to_string()),
|
||||
},
|
||||
_ => CmdResult::ReportError("Invalid option keyword.".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn take_depth(&mut self, d: f64) -> CmdResult {
|
||||
match self.step {
|
||||
Step::Front | Step::FrontDistance => {
|
||||
self.front = Some(d);
|
||||
self.step = Step::Back;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => self.act(XclipAction::Depth { front: self.front, back: Some(d) }),
|
||||
}
|
||||
}
|
||||
|
||||
fn rectangle(a: DVec3, b: DVec3) -> Vec<DVec3> {
|
||||
vec![a, DVec3::new(b.x, a.y, a.z), b, DVec3::new(a.x, b.y, a.z)]
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for XclipCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"XCLIP"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
Step::Select => "Select objects:".to_string(),
|
||||
Step::Option => OPTION.to_string(),
|
||||
Step::DeleteOld => "Delete old boundary(s)? [Yes/No] <Yes>:".to_string(),
|
||||
Step::Mode => {
|
||||
"[Select polyline/Polygonal/Rectangular/Invert clip] <Rectangular>:".to_string()
|
||||
}
|
||||
Step::RectFirst => "Specify first corner:".to_string(),
|
||||
Step::RectSecond => "Specify opposite corner:".to_string(),
|
||||
Step::PolyPoints if self.points.is_empty() => "Specify first point:".to_string(),
|
||||
Step::PolyPoints => "Specify next point or [Undo]:".to_string(),
|
||||
Step::Polyline => "Select polyline:".to_string(),
|
||||
Step::Front => "Specify front clip point or [Distance/Remove]:".to_string(),
|
||||
Step::Back => "Specify back clip point or [Distance/Remove]:".to_string(),
|
||||
Step::FrontDistance | Step::BackDistance => {
|
||||
"Specify distance from boundary:".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::Option => vec![
|
||||
CmdOption::new("ON", "ON"),
|
||||
CmdOption::new("OFF", "OFF"),
|
||||
CmdOption::new("Clipdepth", "C"),
|
||||
CmdOption::new("Delete", "D"),
|
||||
CmdOption::new("generate Polyline", "P"),
|
||||
CmdOption::new("New boundary", "N"),
|
||||
],
|
||||
Step::DeleteOld => vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")],
|
||||
Step::Mode => vec![
|
||||
CmdOption::new("Select polyline", "S"),
|
||||
CmdOption::new("Polygonal", "P"),
|
||||
CmdOption::new("Rectangular", "R"),
|
||||
CmdOption::new("Invert clip", "I"),
|
||||
],
|
||||
Step::PolyPoints if !self.points.is_empty() => vec![CmdOption::new("Undo", "U")],
|
||||
Step::Front | Step::Back => {
|
||||
vec![CmdOption::new("Distance", "D"), CmdOption::new("Remove", "R")]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
match self.step {
|
||||
Step::Option | Step::DeleteOld | Step::Mode => InputKind::SingleToken,
|
||||
_ => InputKind::Point,
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
self.step != Step::Select
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
self.step == Step::Select
|
||||
}
|
||||
|
||||
fn selection_keeps_block_references(&self) -> bool {
|
||||
self.step == Step::Select
|
||||
}
|
||||
|
||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||
self.inserts = handles;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn inject_clipped(&mut self, clipped: &dyn Fn(Handle) -> bool) {
|
||||
self.clipped = self.inserts.iter().any(|h| clipped(*h));
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(
|
||||
self.step,
|
||||
Step::PolyPoints | Step::Front | Step::Back | Step::FrontDistance | Step::BackDistance
|
||||
)
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.step == Step::Polyline
|
||||
}
|
||||
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
self.picked = Some(entity);
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, _handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
match self.picked.take() {
|
||||
Some(EntityType::LwPolyline(pl)) if pl.is_closed && pl.vertices.len() >= 3 => {
|
||||
let z = pl.elevation;
|
||||
self.new_boundary(
|
||||
pl.vertices
|
||||
.iter()
|
||||
.map(|v| DVec3::new(v.location.x, v.location.y, z))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
_ => CmdResult::ReportError("Invalid object selected.".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
Step::RectFirst => {
|
||||
self.points = vec![pt];
|
||||
self.step = Step::RectSecond;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::RectSecond => self.new_boundary(vec![self.points[0], pt]),
|
||||
Step::PolyPoints => {
|
||||
self.points.push(pt);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
// A clip point sets the plane at its height above the boundary.
|
||||
Step::Front | Step::Back => self.take_depth(pt.z),
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
Some(match self.step {
|
||||
Step::Option => self.option(text),
|
||||
Step::DeleteOld => match text.trim().to_ascii_uppercase().as_str() {
|
||||
"" | "Y" | "YES" => self.enter_mode(),
|
||||
"N" | "NO" => CmdResult::Cancel,
|
||||
_ => CmdResult::ReportError("Invalid option keyword.".to_string()),
|
||||
},
|
||||
Step::Mode => self.mode(text),
|
||||
Step::PolyPoints => match text.trim().to_ascii_uppercase().as_str() {
|
||||
"U" | "UNDO" => {
|
||||
self.points.pop();
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => return None,
|
||||
},
|
||||
Step::Front | Step::Back | Step::FrontDistance | Step::BackDistance => self.depth(text),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
Step::Select if self.inserts.is_empty() => {
|
||||
CmdResult::CancelWithMessage("None found.".to_string())
|
||||
}
|
||||
Step::Select => {
|
||||
self.step = Step::Option;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Option => self.option(""),
|
||||
Step::DeleteOld => self.enter_mode(),
|
||||
Step::Mode => self.mode(""),
|
||||
Step::PolyPoints if self.points.len() >= 3 => {
|
||||
let points = self.points.clone();
|
||||
self.new_boundary(points)
|
||||
}
|
||||
Step::PolyPoints => CmdResult::NeedPoint,
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
let ring: Vec<DVec3> = match self.step {
|
||||
Step::RectSecond => Self::rectangle(self.points[0], pt),
|
||||
Step::PolyPoints if !self.points.is_empty() => {
|
||||
let mut ring = self.points.clone();
|
||||
ring.push(pt);
|
||||
ring
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let mut points: Vec<[f32; 3]> =
|
||||
ring.iter().map(|p| [p.x as f32, p.y as f32, p.z as f32]).collect();
|
||||
points.push(points[0]);
|
||||
Some(WireModel::solid("xclip_boundary".into(), points, WireModel::CYAN, false))
|
||||
}
|
||||
}
|
||||
|
||||
/// CLIP: one object, then that object's clip command.
|
||||
pub struct ClipCommand {
|
||||
inner: Option<Box<dyn CadCommand>>,
|
||||
picked: Option<EntityType>,
|
||||
}
|
||||
|
||||
impl ClipCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: None,
|
||||
picked: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ClipCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
self.inner.as_ref().map_or("CLIP", |c| c.name())
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
self.inner
|
||||
.as_ref()
|
||||
.map_or_else(|| "Select Object to clip:".to_string(), |c| c.prompt())
|
||||
}
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
self.inner.as_ref().map_or_else(Vec::new, |c| c.options())
|
||||
}
|
||||
fn input_kind(&self) -> InputKind {
|
||||
self.inner.as_ref().map_or(InputKind::Point, |c| c.input_kind())
|
||||
}
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
self.inner.as_ref().is_some_and(|c| c.point_step_accepts_keywords())
|
||||
}
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.inner.as_ref().is_none_or(|c| c.needs_entity_pick())
|
||||
}
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
match self.inner.as_mut() {
|
||||
Some(c) => c.inject_picked_entity(entity),
|
||||
None => self.picked = Some(entity),
|
||||
}
|
||||
}
|
||||
fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult {
|
||||
if let Some(c) = self.inner.as_mut() {
|
||||
return c.on_entity_pick(handle, pt);
|
||||
}
|
||||
match self.picked.take() {
|
||||
Some(EntityType::Insert(_)) => {
|
||||
self.inner = Some(Box::new(XclipCommand::for_inserts(vec![handle], false)));
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Some(EntityType::Underlay(u)) if u.underlay_type == UnderlayType::Pdf => {
|
||||
self.inner = Some(Box::new(PdfClipCommand::for_underlay(handle, u)));
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::ReportError("*Invalid selection*".to_string()),
|
||||
}
|
||||
}
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
self.inner.as_mut().map_or(CmdResult::NeedPoint, |c| c.on_point(pt))
|
||||
}
|
||||
fn inject_clipped(&mut self, clipped: &dyn Fn(Handle) -> bool) {
|
||||
if let Some(c) = self.inner.as_mut() {
|
||||
c.inject_clipped(clipped);
|
||||
}
|
||||
}
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
self.inner.as_mut().and_then(|c| c.on_text_input(text))
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
self.inner.as_mut().map_or(CmdResult::Cancel, |c| c.on_enter())
|
||||
}
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
self.inner.as_mut().and_then(|c| c.on_mouse_move(pt))
|
||||
}
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["XCLIP", "CLIP"]
|
||||
});
|
||||
@@ -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