mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
SELECTSIMILAR and object isolation: ISOLATEOBJECTS, HIDEOBJECTS, UNISOLATEOBJECTS (part of #428) (#434)
Squash of PR #434 by @vangel76. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b3a73626b2
commit
65d5ee48d3
9 files changed
+435
-6
No files matched your search
+13
-3
@@ -16,7 +16,7 @@ mod plotstyle;
|
||||
mod store;
|
||||
mod tables;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
pub use cadcraft_color as color;
|
||||
pub use cadcraft_geom as geom;
|
||||
@@ -77,6 +77,9 @@ pub struct Drawing {
|
||||
pub parametric: Parametric,
|
||||
/// Next free handle.
|
||||
pub handseed: u64,
|
||||
/// Objects hidden for this session by ISOLATEOBJECTS / HIDEOBJECTS. Never written to a file:
|
||||
/// a drawing opens with everything shown again.
|
||||
pub hidden_objects: BTreeSet<Handle>,
|
||||
}
|
||||
|
||||
impl Default for Drawing {
|
||||
@@ -124,6 +127,7 @@ impl Drawing {
|
||||
constraints: Vec::new(),
|
||||
parametric: Parametric::default(),
|
||||
handseed: 0x100,
|
||||
hidden_objects: BTreeSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,9 +273,15 @@ impl Drawing {
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
/// Effective visibility of an entity (its own flag, its layer on/thawed).
|
||||
/// Effective visibility of an entity (its own flag, its layer on/thawed, not hidden by object
|
||||
/// isolation).
|
||||
pub fn is_visible(&self, e: &Entity) -> bool {
|
||||
e.common.visible && self.layer(&e.common.layer).is_none_or(Layer::visible)
|
||||
e.common.visible && self.layer(&e.common.layer).is_none_or(Layer::visible) && !self.is_hidden_object(e.handle)
|
||||
}
|
||||
|
||||
/// Whether ISOLATEOBJECTS / HIDEOBJECTS hid `h` for this session.
|
||||
pub fn is_hidden_object(&self, h: Handle) -> bool {
|
||||
!self.hidden_objects.is_empty() && self.hidden_objects.contains(&h)
|
||||
}
|
||||
|
||||
/// Bounds of all visible entities in a space.
|
||||
|
||||
@@ -25,6 +25,7 @@ mod mtext_opts;
|
||||
mod pline_opts;
|
||||
mod props;
|
||||
mod qselect;
|
||||
mod selecttools;
|
||||
mod settings;
|
||||
mod spline_opts;
|
||||
mod table;
|
||||
@@ -162,6 +163,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(audit::specs());
|
||||
v.extend(edit::specs());
|
||||
v.extend(qselect::specs());
|
||||
v.extend(selecttools::specs());
|
||||
v.extend(view::specs());
|
||||
v.extend(draw::specs());
|
||||
v.extend(draw2::specs());
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
//! Selection tools: SELECTSIMILAR (objects like the selected ones, per SELECTSIMILARMODE) and
|
||||
//! object isolation — ISOLATEOBJECTS, HIDEOBJECTS, UNISOLATEOBJECTS.
|
||||
//!
|
||||
//! Isolation is temporary (OBJECTISOLATIONMODE 0): hidden objects are kept in
|
||||
//! [`Drawing::hidden_objects`], which display, selection, snapping and plotting skip and which
|
||||
//! is never written to a file. Hiding and showing are undoable like any other change.
|
||||
|
||||
use cadcraft_doc::{Drawing, Entity, EntityKind, Handle};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::machines::{SelOutcome, SelectPhase, SelectRun};
|
||||
use super::*;
|
||||
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
|
||||
|
||||
/// Most objects one call looks at (a hostile drawing can be huge).
|
||||
const MAX_SCAN: usize = 2_000_000;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
CommandSpec::new("selectsimilar", "Select Similar", run_selectsimilar)
|
||||
.params(
|
||||
"{handles? (default the selection), mode?: SELECTSIMILARMODE bits (1 colour, 2 layer, 4 linetype, 8 linetype scale, \
|
||||
16 lineweight, 32 plot style, 64 object style, 128 name; default the variable, 130)} → selects every visible object \
|
||||
in the current space of the same type with those properties: {selected, handles}",
|
||||
)
|
||||
.noundo()
|
||||
.interactive(|_| Ok(Box::new(SelectSimilarM::default()))),
|
||||
CommandSpec::new("isolateobjects", "Isolate Objects", run_isolate)
|
||||
.menu(&["Tools", "Isolate", "Isolate Objects"])
|
||||
.params("{handles? (default the selection)} → hides every other object of the current space for this session: {hidden}")
|
||||
.interactive(|_| Ok(Box::new(SelectRun::new("isolateobjects", "ISOLATEOBJECTS")))),
|
||||
CommandSpec::new("hideobjects", "Hide Objects", run_hide)
|
||||
.menu(&["Tools", "Isolate", "Hide Objects"])
|
||||
.params("{handles? (default the selection)} → hides these objects for this session: {hidden}")
|
||||
.interactive(|_| Ok(Box::new(SelectRun::new("hideobjects", "HIDEOBJECTS")))),
|
||||
CommandSpec::new("unisolateobjects", "End Object Isolation", run_unisolate)
|
||||
.menu(&["Tools", "Isolate", "End Object Isolation"])
|
||||
.params("{} → shows every object ISOLATEOBJECTS or HIDEOBJECTS hid: {shown}"),
|
||||
]
|
||||
}
|
||||
|
||||
// ---------------- SELECTSIMILAR ----------------
|
||||
|
||||
/// SELECTSIMILARMODE bits.
|
||||
const COLOR: u32 = 1;
|
||||
const LAYER: u32 = 2;
|
||||
const LINETYPE: u32 = 4;
|
||||
const LTSCALE: u32 = 8;
|
||||
const LINEWEIGHT: u32 = 16;
|
||||
const PLOT_STYLE: u32 = 32;
|
||||
const OBJECT_STYLE: u32 = 64;
|
||||
const NAME: u32 = 128;
|
||||
|
||||
/// The style an object is drawn with: text, dimension, leader, multileader or table style, or a
|
||||
/// hatch pattern.
|
||||
fn object_style(e: &Entity) -> Option<&str> {
|
||||
Some(match &e.kind {
|
||||
EntityKind::Text(t) => &t.style,
|
||||
EntityKind::MText(t) => &t.style,
|
||||
EntityKind::AttDef(a) => &a.text.style,
|
||||
EntityKind::Dimension(d) => &d.style,
|
||||
EntityKind::Leader(l) => &l.style,
|
||||
EntityKind::MLeader(m) => &m.style,
|
||||
EntityKind::Table(t) => &t.style,
|
||||
EntityKind::Hatch(h) => &h.pattern,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The name an object references: a block reference's block, a raster image's definition.
|
||||
fn object_name(e: &Entity) -> Option<&str> {
|
||||
match &e.kind {
|
||||
EntityKind::Insert(i) => Some(&i.block),
|
||||
EntityKind::Image(i) => Some(if i.name.is_empty() { &i.path } else { &i.name }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn same_text(a: Option<&str>, b: Option<&str>) -> bool {
|
||||
match (a, b) {
|
||||
(Some(a), Some(b)) => a.eq_ignore_ascii_case(b),
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is `e` like `r`: the same object type and, for each bit of `mode`, the same property?
|
||||
pub(crate) fn similar(e: &Entity, r: &Entity, mode: u32) -> bool {
|
||||
let (a, b) = (&e.common, &r.common);
|
||||
e.kind.dxf_name().eq_ignore_ascii_case(r.kind.dxf_name())
|
||||
&& (mode & COLOR == 0 || a.color == b.color)
|
||||
&& (mode & LAYER == 0 || a.layer.eq_ignore_ascii_case(&b.layer))
|
||||
&& (mode & LINETYPE == 0 || a.linetype.eq_ignore_ascii_case(&b.linetype))
|
||||
&& (mode & LTSCALE == 0 || (a.ltscale - b.ltscale).abs() <= 1e-9 * a.ltscale.abs().max(b.ltscale.abs()).max(1.0))
|
||||
&& (mode & LINEWEIGHT == 0 || a.lineweight == b.lineweight)
|
||||
&& (mode & PLOT_STYLE == 0 || a.plot_style.eq_ignore_ascii_case(&b.plot_style))
|
||||
&& (mode & OBJECT_STYLE == 0 || same_text(object_style(e), object_style(r)))
|
||||
&& (mode & NAME == 0 || same_text(object_name(e), object_name(r)))
|
||||
}
|
||||
|
||||
/// The visible objects of the current space like any of `refs`.
|
||||
fn similar_objects(s: &Session, d: &Drawing, refs: &[Handle], mode: u32) -> Vec<Handle> {
|
||||
let refs: Vec<&Entity> = refs.iter().filter_map(|h| d.entity(*h).map(|e| e.as_ref())).collect();
|
||||
if refs.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(store) = d.space(&s.space()) else { return Vec::new() };
|
||||
store.iter().take(MAX_SCAN).filter(|e| d.is_visible(e) && refs.iter().any(|r| similar(e, r, mode))).map(|e| e.handle).collect()
|
||||
}
|
||||
|
||||
fn mode_param(s: &Session, p: &Value) -> Result<u32> {
|
||||
match p.get("mode") {
|
||||
None | Some(Value::Null) => Ok(s.settings.selectsimilarmode),
|
||||
Some(v) => v
|
||||
.as_u64()
|
||||
.filter(|m| *m <= 255)
|
||||
.map(|m| m as u32)
|
||||
.ok_or_else(|| bad("selectsimilar", "`mode` must be a SELECTSIMILARMODE bit code from 0 to 255")),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_selectsimilar(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let refs = targets(s, p)?;
|
||||
let mode = mode_param(s, p)?;
|
||||
if refs.is_empty() {
|
||||
return Err(bad("selectsimilar", "select objects to match"));
|
||||
}
|
||||
let found = similar_objects(s, s.doc()?, &refs, mode);
|
||||
if found.is_empty() {
|
||||
return Err(bad("selectsimilar", "none of the given objects exist in the current space"));
|
||||
}
|
||||
s.set_selection(found);
|
||||
let sel = s.selection();
|
||||
let n = sel.len();
|
||||
Ok(json!({ "selected": n, "handles": sel.iter().map(|h| h.hex()).collect::<Vec<_>>(), "message": format!("{n} found") }))
|
||||
}
|
||||
|
||||
/// SELECTSIMILAR typed: "Select objects or [SEttings]:"; Enter selects the similar objects.
|
||||
/// SEttings asks for the SELECTSIMILARMODE bit code (the command line has no dialog).
|
||||
#[derive(Default)]
|
||||
struct SelectSimilarM {
|
||||
sel: SelectPhase,
|
||||
settings: bool,
|
||||
}
|
||||
|
||||
impl SelectSimilarM {
|
||||
fn finish(&self, s: &mut Session, hs: &[Handle]) -> Result<Step> {
|
||||
let r = run_selectsimilar(s, &json!({ "handles": hs.iter().map(|h| h.hex()).collect::<Vec<_>>() }))?;
|
||||
if let Some(m) = r.get("message").and_then(Value::as_str) {
|
||||
s.echo(m.to_string());
|
||||
}
|
||||
Ok(Step::Done)
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactive for SelectSimilarM {
|
||||
fn name(&self) -> &'static str {
|
||||
"SELECTSIMILAR"
|
||||
}
|
||||
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
||||
self.sel = SelectPhase::begin(s);
|
||||
if self.sel.done {
|
||||
let hs = self.sel.picked.clone();
|
||||
return self.finish(s, &hs);
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
fn prompt(&self, s: &Session) -> Prompt {
|
||||
if self.settings {
|
||||
return Prompt::new("Enter properties to match (SELECTSIMILARMODE bit code)", Accept::NUMBER)
|
||||
.default(s.settings.selectsimilarmode.to_string());
|
||||
}
|
||||
self.sel.prompt().kw(&["SEttings"])
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if self.settings {
|
||||
match &i {
|
||||
Input::Enter => {}
|
||||
Input::Text(t) | Input::Keyword(t) => crate::sysvars::set(s, "SELECTSIMILARMODE", &json!(t.trim()))?,
|
||||
_ => return Ok(Step::Continue),
|
||||
}
|
||||
self.settings = false;
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
if let Input::Keyword(k) | Input::Text(k) = &i
|
||||
&& k.trim().eq_ignore_ascii_case("settings")
|
||||
{
|
||||
self.settings = true;
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
match self.sel.feed(s, &i)? {
|
||||
SelOutcome::More => Ok(Step::Continue),
|
||||
SelOutcome::Empty => Ok(Step::Done),
|
||||
SelOutcome::Done(hs) => self.finish(s, &hs),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- isolation ----------------
|
||||
|
||||
/// Hide `hs` (objects that exist and aren't hidden yet); returns how many were hidden.
|
||||
fn hide(s: &mut Session, hs: Vec<Handle>) -> Result<usize> {
|
||||
let d = s.doc()?;
|
||||
let hs: Vec<Handle> = hs.into_iter().filter(|h| d.entity(*h).is_some() && !d.is_hidden_object(*h)).collect();
|
||||
if !hs.is_empty() {
|
||||
s.doc_mut()?.hidden_objects.extend(hs.iter().copied());
|
||||
}
|
||||
// Hidden objects can't stay selected.
|
||||
s.set_selection(Vec::new());
|
||||
Ok(hs.len())
|
||||
}
|
||||
|
||||
fn hidden_message(n: usize) -> String {
|
||||
if n == 1 { "1 object hidden".to_string() } else { format!("{n} objects hidden") }
|
||||
}
|
||||
|
||||
fn run_isolate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let keep = targets(s, p)?;
|
||||
if keep.is_empty() {
|
||||
return Err(bad("isolateobjects", "select objects to isolate"));
|
||||
}
|
||||
let d = s.doc()?;
|
||||
if !keep.iter().any(|h| d.entity(*h).is_some()) {
|
||||
return Err(bad("isolateobjects", "none of the given objects exist"));
|
||||
}
|
||||
let keep_set: std::collections::HashSet<Handle> = keep.iter().copied().collect();
|
||||
let others: Vec<Handle> = d
|
||||
.space(&s.space())
|
||||
.map(|st| st.iter().take(MAX_SCAN).filter(|e| !keep_set.contains(&e.handle)).map(|e| e.handle).collect())
|
||||
.unwrap_or_default();
|
||||
let n = hide(s, others)?;
|
||||
Ok(json!({ "hidden": n, "message": hidden_message(n) }))
|
||||
}
|
||||
|
||||
fn run_hide(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let hs = targets(s, p)?;
|
||||
if hs.is_empty() {
|
||||
return Err(bad("hideobjects", "select objects to hide"));
|
||||
}
|
||||
let n = hide(s, hs)?;
|
||||
Ok(json!({ "hidden": n, "message": hidden_message(n) }))
|
||||
}
|
||||
|
||||
fn run_unisolate(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
let n = s.doc()?.hidden_objects.len();
|
||||
if n == 0 {
|
||||
return Ok(json!({ "shown": 0, "message": "No objects are isolated or hidden." }));
|
||||
}
|
||||
s.doc_mut()?.hidden_objects.clear();
|
||||
let msg = if n == 1 { "1 object shown".to_string() } else { format!("{n} objects shown") };
|
||||
Ok(json!({ "shown": n, "message": msg }))
|
||||
}
|
||||
@@ -299,6 +299,10 @@ pub struct Settings {
|
||||
pub lwdisplay: bool,
|
||||
pub transparency_display: bool,
|
||||
pub selection_cycling: bool,
|
||||
/// SELECTSIMILARMODE: what SELECTSIMILAR matches besides the object type (bits: 1 colour,
|
||||
/// 2 layer, 4 linetype, 8 linetype scale, 16 lineweight, 32 plot style, 64 object style,
|
||||
/// 128 name).
|
||||
pub selectsimilarmode: u32,
|
||||
pub otrack: bool,
|
||||
/// PICKBOX in pixels.
|
||||
pub pickbox: f64,
|
||||
@@ -344,6 +348,7 @@ impl Default for Settings {
|
||||
lwdisplay: false,
|
||||
transparency_display: false,
|
||||
selection_cycling: false,
|
||||
selectsimilarmode: 130,
|
||||
otrack: true,
|
||||
pickbox: 3.0,
|
||||
aperture: 10.0,
|
||||
|
||||
@@ -21,6 +21,8 @@ const SESSION_VARS: &[&str] = &[
|
||||
"DYNPIFORMAT",
|
||||
"DYNPICOORDS",
|
||||
"LWDISPLAY",
|
||||
"SELECTSIMILARMODE",
|
||||
"OBJECTISOLATIONMODE",
|
||||
"QPMODE",
|
||||
"PICKBOX",
|
||||
"APERTURE",
|
||||
@@ -58,6 +60,9 @@ pub fn get(s: &Session, name: &str) -> Option<Value> {
|
||||
"DYNPIFORMAT" => json!(i32::from(st.dynpi_cartesian)),
|
||||
"DYNPICOORDS" => json!(i32::from(st.dynpi_absolute)),
|
||||
"LWDISPLAY" => json!(i32::from(st.lwdisplay)),
|
||||
"SELECTSIMILARMODE" => json!(st.selectsimilarmode),
|
||||
// Isolation is temporary: hidden objects show again when the drawing is reopened.
|
||||
"OBJECTISOLATIONMODE" => json!(0),
|
||||
"QPMODE" => json!(i32::from(st.qpmode)),
|
||||
"PICKBOX" => json!(st.pickbox),
|
||||
"APERTURE" => json!(st.aperture),
|
||||
@@ -169,6 +174,12 @@ pub fn set(s: &mut Session, name: &str, v: &Value) -> Result<()> {
|
||||
"DYNPIFORMAT" => st.dynpi_cartesian = as_bool(v).ok_or_else(bad)?,
|
||||
"DYNPICOORDS" => st.dynpi_absolute = as_bool(v).ok_or_else(bad)?,
|
||||
"LWDISPLAY" => st.lwdisplay = as_bool(v).ok_or_else(bad)?,
|
||||
"SELECTSIMILARMODE" => st.selectsimilarmode = as_i(v).filter(|b| (0..=255).contains(b)).ok_or_else(bad)? as u32,
|
||||
"OBJECTISOLATIONMODE" => match as_i(v) {
|
||||
Some(0) => {}
|
||||
Some(1) => return Err(EngineError::Other("OBJECTISOLATIONMODE 1 (isolation saved with the drawing) is not available yet".into())),
|
||||
_ => return Err(bad()),
|
||||
},
|
||||
"QPMODE" => st.qpmode = as_i(v).ok_or_else(bad)? > 0,
|
||||
"PICKBOX" => st.pickbox = as_f64(v).ok_or_else(bad)?.clamp(0.0, 50.0),
|
||||
"APERTURE" => st.aperture = as_f64(v).ok_or_else(bad)?.clamp(1.0, 50.0),
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
//! SELECTSIMILAR (with SELECTSIMILARMODE) and object isolation — ISOLATEOBJECTS, HIDEOBJECTS,
|
||||
//! UNISOLATEOBJECTS (#428): JSON forms, typed forms, and hidden objects skipped by display and
|
||||
//! selection.
|
||||
|
||||
use cadcraft_engine::doc::{Handle, Space};
|
||||
use cadcraft_engine::geom::{Bounds2, Vec2};
|
||||
use cadcraft_engine::{Session, render, select};
|
||||
use serde_json::json;
|
||||
|
||||
/// Two lines and a circle on layer 0, a line and a circle on "Walls" (the last circle red).
|
||||
fn drawing() -> (Session, Vec<Handle>) {
|
||||
let mut s = Session::new();
|
||||
s.execute("layer.new", &json!({ "name": "Walls" })).unwrap();
|
||||
s.execute("line", &json!({ "points": [[0, 0], [10, 0]] })).unwrap();
|
||||
s.execute("line", &json!({ "points": [[0, 5], [10, 5]] })).unwrap();
|
||||
s.execute("circle", &json!({ "center": [20, 0], "radius": 2 })).unwrap();
|
||||
s.execute("line", &json!({ "points": [[0, 10], [10, 10]] })).unwrap();
|
||||
s.execute("circle", &json!({ "center": [30, 0], "radius": 3 })).unwrap();
|
||||
let hs: Vec<Handle> = s.doc().unwrap().model.iter().map(|e| e.handle).collect();
|
||||
s.execute("properties.set", &json!({ "handles": [hs[3].hex(), hs[4].hex()], "layer": "Walls" })).unwrap();
|
||||
s.execute("properties.set", &json!({ "handles": [hs[4].hex()], "color": 1 })).unwrap();
|
||||
s.set_selection(Vec::new());
|
||||
(s, hs)
|
||||
}
|
||||
|
||||
fn sorted(mut v: Vec<Handle>) -> Vec<Handle> {
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selectsimilar_matches_type_and_mode_properties() {
|
||||
let (mut s, hs) = drawing();
|
||||
// Default SELECTSIMILARMODE 130: same type and layer (and name).
|
||||
let r = s.execute("selectsimilar", &json!({ "handles": [hs[0].hex()] })).unwrap();
|
||||
assert_eq!(r["selected"], 2);
|
||||
assert_eq!(sorted(s.selection()), vec![hs[0], hs[1]]);
|
||||
// Type only: every line.
|
||||
s.execute("selectsimilar", &json!({ "handles": [hs[0].hex()], "mode": 0 })).unwrap();
|
||||
assert_eq!(sorted(s.selection()), vec![hs[0], hs[1], hs[3]]);
|
||||
// Several references: objects like any of them. Colour only (1): the red circle stands alone.
|
||||
s.execute("selectsimilar", &json!({ "handles": [hs[0].hex(), hs[4].hex()], "mode": 1 })).unwrap();
|
||||
assert_eq!(sorted(s.selection()), vec![hs[0], hs[1], hs[3], hs[4]]);
|
||||
// The current selection is the default; the variable sets the default mode.
|
||||
s.execute("setvar", &json!({ "name": "SELECTSIMILARMODE", "value": 0 })).unwrap();
|
||||
s.set_selection(vec![hs[2]]);
|
||||
s.execute("selectsimilar", &json!({})).unwrap();
|
||||
assert_eq!(sorted(s.selection()), vec![hs[2], hs[4]]);
|
||||
// Hostile or empty input is an error, never a panic.
|
||||
s.set_selection(Vec::new());
|
||||
assert!(s.execute("selectsimilar", &json!({})).is_err());
|
||||
assert!(s.execute("selectsimilar", &json!({ "handles": ["FFFFFF"] })).is_err());
|
||||
for m in [json!(-1), json!(256), json!("x"), json!(1.5), json!(f64::MAX)] {
|
||||
assert!(s.execute("selectsimilar", &json!({ "handles": [hs[0].hex()], "mode": m })).is_err(), "{m}");
|
||||
}
|
||||
assert!(s.execute("setvar", &json!({ "name": "SELECTSIMILARMODE", "value": 999 })).is_err());
|
||||
assert!(s.running.is_none(), "JSON calls never prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selectsimilar_typed_with_settings_keyword() {
|
||||
let (mut s, hs) = drawing();
|
||||
s.cmdline("selectsimilar").unwrap();
|
||||
assert!(s.prompt_text().contains("Select objects or [SEttings]"), "{}", s.prompt_text());
|
||||
// SEttings asks for the bit code: 0 = type only.
|
||||
s.cmdline("se").unwrap();
|
||||
assert!(s.prompt_text().contains("SELECTSIMILARMODE"), "{}", s.prompt_text());
|
||||
s.cmdline("0").unwrap();
|
||||
assert_eq!(s.settings.selectsimilarmode, 0);
|
||||
s.cmdline("10,0").unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
assert_eq!(sorted(s.selection()), vec![hs[0], hs[1], hs[3]]);
|
||||
// With a pickfirst selection it selects at once.
|
||||
s.set_selection(vec![hs[2]]);
|
||||
s.cmdline("selectsimilar").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
assert_eq!(sorted(s.selection()), vec![hs[2], hs[4]]);
|
||||
}
|
||||
|
||||
/// Handles the display list draws something for.
|
||||
fn drawn(s: &Session) -> usize {
|
||||
let d = s.doc().unwrap();
|
||||
render::build(d, &Space::Model, &render::Options::default()).prims.len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolate_hide_and_unisolate() {
|
||||
let (mut s, hs) = drawing();
|
||||
let all_prims = drawn(&s);
|
||||
// ISOLATE: everything else is hidden — not drawn, not selectable, not in ALL.
|
||||
let r = s.execute("isolateobjects", &json!({ "handles": [hs[0].hex()] })).unwrap();
|
||||
assert_eq!(r["hidden"], 4);
|
||||
let d = s.doc().unwrap();
|
||||
assert!(d.is_hidden_object(hs[1]) && !d.is_hidden_object(hs[0]));
|
||||
assert!(drawn(&s) < all_prims);
|
||||
let everything = Bounds2::new(Vec2::new(-100.0, -100.0), Vec2::new(100.0, 100.0));
|
||||
assert_eq!(select::select_window(s.doc().unwrap(), &Space::Model, everything, true), vec![hs[0]]);
|
||||
assert!(select::pick(s.doc().unwrap(), &Space::Model, Vec2::new(5.0, 5.0), 0.5).is_none());
|
||||
// New objects show; HIDEOBJECTS hides more.
|
||||
s.execute("line", &json!({ "points": [[0, 20], [10, 20]] })).unwrap();
|
||||
let r = s.execute("hideobjects", &json!({ "handles": [hs[0].hex()] })).unwrap();
|
||||
assert_eq!(r["hidden"], 1);
|
||||
// Undo shows the hidden object again, redo hides it.
|
||||
s.execute("u", &json!({})).unwrap();
|
||||
assert!(!s.doc().unwrap().is_hidden_object(hs[0]));
|
||||
s.execute("redo", &json!({})).unwrap();
|
||||
assert!(s.doc().unwrap().is_hidden_object(hs[0]));
|
||||
// UNISOLATE shows everything again.
|
||||
let r = s.execute("unisolateobjects", &json!({})).unwrap();
|
||||
assert_eq!(r["shown"], 5);
|
||||
assert!(s.doc().unwrap().hidden_objects.is_empty());
|
||||
assert_eq!(drawn(&s), all_prims + 1);
|
||||
assert_eq!(s.execute("unisolateobjects", &json!({})).unwrap()["shown"], 0);
|
||||
// Nothing to isolate or hide is an error; isolation is temporary only.
|
||||
assert!(s.execute("isolateobjects", &json!({})).is_err());
|
||||
assert!(s.execute("hideobjects", &json!({ "handles": "1A" })).is_err());
|
||||
assert!(s.execute("isolateobjects", &json!({ "handles": ["FFFFFF"] })).is_err());
|
||||
assert!(s.execute("setvar", &json!({ "name": "OBJECTISOLATIONMODE", "value": 1 })).is_err());
|
||||
assert_eq!(s.execute("getvar", &json!({ "name": "OBJECTISOLATIONMODE" })).unwrap()["value"], json!(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolate_typed_and_all_skips_hidden() {
|
||||
let (mut s, hs) = drawing();
|
||||
s.cmdline("hideobjects").unwrap();
|
||||
assert!(s.prompt_text().contains("Select objects"), "{}", s.prompt_text());
|
||||
s.cmdline("10,0").unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
assert!(s.doc().unwrap().is_hidden_object(hs[0]));
|
||||
assert!(s.selection().is_empty(), "hidden objects don't stay selected");
|
||||
// ALL at Select objects leaves hidden objects out.
|
||||
s.cmdline("isolateobjects").unwrap();
|
||||
s.cmdline("all").unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
assert_eq!(s.doc().unwrap().hidden_objects.len(), 1);
|
||||
s.cmdline("unisolateobjects").unwrap();
|
||||
assert!(s.doc().unwrap().hidden_objects.is_empty());
|
||||
}
|
||||
@@ -358,7 +358,7 @@ const MAX_VIEWPORTS: usize = 256;
|
||||
/// A paper-space viewport: its border (as an ordinary entity) and model space seen through it.
|
||||
/// Viewport id 1 is the paper-space view itself and is not drawn.
|
||||
fn viewport(b: &mut Builder, d: &Drawing, e: &Entity, vp: &cadcraft_doc::Viewport) {
|
||||
if vp.id == 1 || !e.common.visible {
|
||||
if vp.id == 1 || !e.common.visible || d.is_hidden_object(e.handle) {
|
||||
return;
|
||||
}
|
||||
// The viewport's layer frozen hides its contents too (layer off hides only the border).
|
||||
@@ -520,6 +520,10 @@ fn default_lineweight(d: &Drawing) -> f32 {
|
||||
}
|
||||
|
||||
fn entity(b: &mut Builder, ctx: &Ctx, e: &Entity) {
|
||||
// Hidden by ISOLATEOBJECTS / HIDEOBJECTS (and so in viewports and plots too).
|
||||
if ctx.d.is_hidden_object(e.handle) {
|
||||
return;
|
||||
}
|
||||
// The entity's named plot style, for everything it draws (block contents set their own).
|
||||
let saved = b.named;
|
||||
b.named = b.styles.and_then(|s| s.named_index(ctx, &e.common));
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
Ellipse Еліпс
|
||||
Elliptical Arc Еліптична дуга
|
||||
Enable Dynamic Input (F12) Увімкнути динамічне введення (F12)
|
||||
End Object Isolation Завершити ізоляцію об'єктів
|
||||
End Width Кінцева ширина
|
||||
End X Кінець X
|
||||
End Y Кінець Y
|
||||
@@ -326,6 +327,7 @@
|
||||
Hide All Constraint Bars Приховати всі панелі обмежень
|
||||
Hide All Dynamic Dimensions Приховати всі динамічні розміри
|
||||
Hide Layer List Приховати список шарів
|
||||
Hide Objects Приховати об'єкти
|
||||
Hide Palettes Приховати палітри
|
||||
Home Початковий вид
|
||||
Horizontal Горизонтальність
|
||||
@@ -357,6 +359,7 @@
|
||||
Intersection Перетин
|
||||
Isolate Ізолювати
|
||||
Isolate Layer Ізолювати шар
|
||||
Isolate Objects Ізолювати об'єкти
|
||||
Isometric Drafting Ізометричне креслення
|
||||
Join Об'єднати
|
||||
Join the ArtCraft Discord… Приєднатися до Discord ArtCraft…
|
||||
@@ -645,6 +648,7 @@
|
||||
Select 2D polyline Виберіть 2D-полілінію
|
||||
Select All Вибрати все
|
||||
Select Objects Вибрати об'єкти
|
||||
Select Similar Вибрати подібні
|
||||
Select a 2D polyline. Виберіть 2D-полілінію.
|
||||
Select a block reference. Виберіть посилання на блок.
|
||||
Select a circle or arc. Виберіть коло або дугу.
|
||||
|
||||
|
+2
-2
@@ -131,8 +131,8 @@ down-arrow options menu (DYNPROMPT), and DYNDIM.
|
||||
| Selection cycling (SELECTIONCYCLING) | yes | toggle nothing reads |
|
||||
| Selection preview, hover highlight | yes | hover highlight |
|
||||
| PICKADD, PICKFIRST, PICKAUTO | yes | PICKADD (Shift), PICKFIRST |
|
||||
| QSELECT, SELECTSIMILAR, FILTER | yes | QSELECT dialog; no SELECTSIMILAR or FILTER |
|
||||
| Object isolate/hide (ISOLATEOBJECTS, HIDEOBJECTS) | yes | **no** (only layer isolate) |
|
||||
| QSELECT, SELECTSIMILAR, FILTER | yes | QSELECT dialog; SELECTSIMILAR with SELECTSIMILARMODE (#428, SEttings asks for the bit code; no dialog); no FILTER |
|
||||
| Object isolate/hide (ISOLATEOBJECTS, HIDEOBJECTS) | yes | yes, temporary (OBJECTISOLATIONMODE 0; #428): Tools > Isolate, UNISOLATEOBJECTS; no status-bar button |
|
||||
| Groups selectable as one (PICKSTYLE) | yes | no |
|
||||
|
||||
## Command line (≈ 60%)
|
||||
|
||||
Reference in new issue
Block a user