Squash of PR #449 by @vangel76. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
151722feb0
commit
a652c57537
|
|
@ -26,6 +26,7 @@ mod modify2;
|
|||
mod mtext_opts;
|
||||
mod pline_opts;
|
||||
mod props;
|
||||
pub use props::MatchProps;
|
||||
mod qselect;
|
||||
mod selecttools;
|
||||
mod settings;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
//! Properties: per-object property edits, current properties, MATCHPROP, linetypes, units, RENAME.
|
||||
|
||||
mod matchprop;
|
||||
mod rename;
|
||||
|
||||
pub use matchprop::MatchProps;
|
||||
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::{EntityKind, Lineweight, Transparency};
|
||||
use cadcraft_geom::Vec3;
|
||||
|
|
@ -14,7 +17,11 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
vec![
|
||||
CommandSpec::new("properties", "Properties", run_get).menu(&["Modify", "Properties"]).alias(&["pr", "props", "ch"]).params("{handles?} → properties of the selection").noundo(),
|
||||
CommandSpec::new("properties.set", "Set Properties", run_set).params("{handles?, layer?, color?, linetype?, lineweight?, ltscale?, transparency?: \"ByLayer\" | \"ByBlock\" | 0..90, visible?, <geometry fields: radius, center, start, end, text, height, rotation…>}"),
|
||||
CommandSpec::new("matchprop", "Match Properties", run_matchprop).menu(&["Modify", "Match Properties"]).alias(&["ma", "painter"]).params("{source, targets: [hex]}"),
|
||||
CommandSpec::new("matchprop", "Match Properties", matchprop::run_matchprop)
|
||||
.menu(&["Modify", "Match Properties"])
|
||||
.alias(&["ma", "painter"])
|
||||
.params("{source, targets?: [hex] (default: selection), properties?: [name] | {name: bool} (default: all; names: color, layer, linetype, ltscale, lineweight, transparency, thickness, plotStyle, dimension, text, hatch, polyline, viewport, table, multileader)} → {changed}")
|
||||
.interactive(|_| Ok(Box::new(matchprop::MatchPropM::new()))),
|
||||
CommandSpec::new("color", "Color...", run_color).menu(&["Format", "Color..."]).alias(&["col", "colour"]).params("{color: \"ByLayer\" | \"red\" | 1..255 | \"r,g,b\"}"),
|
||||
CommandSpec::new("linetype", "Linetype...", run_linetype).menu(&["Format", "Linetype..."]).alias(&["lt", "ltype"]).params("{current?: name, load?: name | \"*\"}"),
|
||||
CommandSpec::new("lweight", "Lineweight...", run_lweight).menu(&["Format", "Lineweight..."]).alias(&["lw", "lineweight"]).params("{lineweight: mm | ByLayer}"),
|
||||
|
|
@ -341,29 +348,6 @@ fn run_set(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Ok(json!({ "changed": hs.len() }))
|
||||
}
|
||||
|
||||
fn run_matchprop(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let src =
|
||||
p.get("source").and_then(Value::as_str).and_then(cadcraft_doc::Handle::parse_hex).ok_or_else(|| bad("matchprop", "`source` is required"))?;
|
||||
let tg: Vec<cadcraft_doc::Handle> = p
|
||||
.get("targets")
|
||||
.and_then(Value::as_array)
|
||||
.map(|a| a.iter().filter_map(|v| v.as_str().and_then(cadcraft_doc::Handle::parse_hex)).collect())
|
||||
.unwrap_or_default();
|
||||
let d = s.doc_mut()?;
|
||||
let c = d.entity(src).map(|e| e.common.clone()).ok_or_else(|| bad("matchprop", "no such source"))?;
|
||||
for h in &tg {
|
||||
d.modify_entity(*h, |e| {
|
||||
e.common.layer = c.layer.clone();
|
||||
e.common.color = c.color;
|
||||
e.common.linetype = c.linetype.clone();
|
||||
e.common.lineweight = c.lineweight;
|
||||
e.common.ltscale = c.ltscale;
|
||||
e.common.transparency = c.transparency;
|
||||
})?;
|
||||
}
|
||||
Ok(json!({ "changed": tg.len() }))
|
||||
}
|
||||
|
||||
fn run_color(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = p.get("color").and_then(parse_color).ok_or_else(|| bad("color", "`color` is required"))?;
|
||||
let d = s.doc_mut()?;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,400 @@
|
|||
//! MATCHPROP (alias MA, PAINTER): copy properties from a source object to destination objects.
|
||||
//! Typed: "Select source object", then "Select destination object(s) or [Settings]" until Enter;
|
||||
//! Settings toggles which properties are copied (remembered for the next typed MATCHPROP). The
|
||||
//! JSON form copies every property unless `properties` names some.
|
||||
|
||||
use cadcraft_doc::{Entity, EntityKind, Handle};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::super::*;
|
||||
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
|
||||
|
||||
/// Which properties MATCHPROP copies (its Settings).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct MatchProps {
|
||||
pub color: bool,
|
||||
pub layer: bool,
|
||||
pub linetype: bool,
|
||||
pub ltscale: bool,
|
||||
pub lineweight: bool,
|
||||
pub transparency: bool,
|
||||
pub thickness: bool,
|
||||
pub plot_style: bool,
|
||||
/// Dimension style and overrides of dimensions; the style of leaders.
|
||||
pub dimension: bool,
|
||||
/// Text style, height, width factor and oblique angle of text and multiline text.
|
||||
pub text: bool,
|
||||
/// Pattern, scale, angle and gradient of hatches.
|
||||
pub hatch: bool,
|
||||
/// Global width and linetype generation of polylines.
|
||||
pub polyline: bool,
|
||||
/// Display locking and scale of layout viewports.
|
||||
pub viewport: bool,
|
||||
/// Table style of tables.
|
||||
pub table: bool,
|
||||
/// Style, arrowhead size, landing and leader type of multileaders.
|
||||
pub multileader: bool,
|
||||
}
|
||||
|
||||
impl Default for MatchProps {
|
||||
fn default() -> Self {
|
||||
MatchProps::all(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Keyword, settings name (JSON and the "Current active settings" echo), field.
|
||||
type Field = fn(&mut MatchProps) -> &mut bool;
|
||||
const FIELDS: &[(&str, &str, Field)] = &[
|
||||
("Color", "color", |m| &mut m.color),
|
||||
("LAyer", "layer", |m| &mut m.layer),
|
||||
("LType", "linetype", |m| &mut m.linetype),
|
||||
("LTScale", "ltscale", |m| &mut m.ltscale),
|
||||
("LWeight", "lineweight", |m| &mut m.lineweight),
|
||||
("TRansparency", "transparency", |m| &mut m.transparency),
|
||||
("THickness", "thickness", |m| &mut m.thickness),
|
||||
("PLotstyle", "plotStyle", |m| &mut m.plot_style),
|
||||
("DImension", "dimension", |m| &mut m.dimension),
|
||||
("TExt", "text", |m| &mut m.text),
|
||||
("Hatch", "hatch", |m| &mut m.hatch),
|
||||
("POlyline", "polyline", |m| &mut m.polyline),
|
||||
("Viewport", "viewport", |m| &mut m.viewport),
|
||||
("TAble", "table", |m| &mut m.table),
|
||||
("Multileader", "multileader", |m| &mut m.multileader),
|
||||
];
|
||||
|
||||
impl MatchProps {
|
||||
pub fn all(on: bool) -> Self {
|
||||
MatchProps {
|
||||
color: on,
|
||||
layer: on,
|
||||
linetype: on,
|
||||
ltscale: on,
|
||||
lineweight: on,
|
||||
transparency: on,
|
||||
thickness: on,
|
||||
plot_style: on,
|
||||
dimension: on,
|
||||
text: on,
|
||||
hatch: on,
|
||||
polyline: on,
|
||||
viewport: on,
|
||||
table: on,
|
||||
multileader: on,
|
||||
}
|
||||
}
|
||||
/// The settings that are on, by name.
|
||||
pub fn names(&self) -> Vec<&'static str> {
|
||||
let mut m = *self;
|
||||
FIELDS.iter().filter(|(_, _, f)| *f(&mut m)).map(|(_, n, _)| *n).collect()
|
||||
}
|
||||
fn set(&mut self, name: &str, on: bool) -> bool {
|
||||
match FIELDS.iter().find(|(k, n, _)| n.eq_ignore_ascii_case(name) || k.eq_ignore_ascii_case(name)) {
|
||||
Some((_, _, f)) => {
|
||||
*f(self) = on;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
fn toggle(&mut self, name: &str) -> bool {
|
||||
match FIELDS.iter().find(|(k, n, _)| n.eq_ignore_ascii_case(name) || k.eq_ignore_ascii_case(name)) {
|
||||
Some((_, _, f)) => {
|
||||
let b = f(self);
|
||||
*b = !*b;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
/// From JSON: a list of names to copy, or `{name: bool}` over the defaults (all on).
|
||||
fn from_json(v: &Value) -> Result<Self> {
|
||||
let unknown =
|
||||
|n: &str| bad("matchprop", format!("unknown property `{n}` (known: {})", FIELDS.iter().map(|f| f.1).collect::<Vec<_>>().join(", ")));
|
||||
match v {
|
||||
Value::Array(a) => {
|
||||
let mut m = MatchProps::all(false);
|
||||
for x in a {
|
||||
let n = x.as_str().ok_or_else(|| bad("matchprop", format!("`properties` holds names, got {x}")))?;
|
||||
if !m.set(n, true) {
|
||||
return Err(unknown(n));
|
||||
}
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
Value::Object(o) => {
|
||||
let mut m = MatchProps::default();
|
||||
for (n, x) in o {
|
||||
let on = x.as_bool().ok_or_else(|| bad("matchprop", format!("`properties.{n}` must be true or false")))?;
|
||||
if !m.set(n, on) {
|
||||
return Err(unknown(n));
|
||||
}
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
other => Err(bad("matchprop", format!("`properties` must be a list of names or {{name: bool}}, got {other}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy the chosen properties of `src` onto `dst`.
|
||||
fn copy_props(src: &Entity, dst: &mut Entity, m: &MatchProps) {
|
||||
let (c, d) = (&src.common, &mut dst.common);
|
||||
if m.color {
|
||||
d.color = c.color;
|
||||
}
|
||||
if m.layer {
|
||||
d.layer.clone_from(&c.layer);
|
||||
}
|
||||
if m.linetype {
|
||||
d.linetype.clone_from(&c.linetype);
|
||||
}
|
||||
if m.ltscale {
|
||||
d.ltscale = c.ltscale;
|
||||
}
|
||||
if m.lineweight {
|
||||
d.lineweight = c.lineweight;
|
||||
}
|
||||
if m.transparency {
|
||||
d.transparency = c.transparency;
|
||||
}
|
||||
if m.thickness {
|
||||
d.thickness = c.thickness;
|
||||
}
|
||||
if m.plot_style {
|
||||
d.plot_style.clone_from(&c.plot_style);
|
||||
}
|
||||
match (&src.kind, &mut dst.kind) {
|
||||
(EntityKind::Dimension(a), EntityKind::Dimension(b)) if m.dimension => {
|
||||
b.style.clone_from(&a.style);
|
||||
b.overrides.clone_from(&a.overrides);
|
||||
b.block = None;
|
||||
}
|
||||
(EntityKind::Leader(a), EntityKind::Leader(b)) if m.dimension => b.style.clone_from(&a.style),
|
||||
(EntityKind::Text(a), EntityKind::Text(b)) if m.text => {
|
||||
b.style.clone_from(&a.style);
|
||||
b.height = a.height;
|
||||
b.width_factor = a.width_factor;
|
||||
b.oblique = a.oblique;
|
||||
}
|
||||
(EntityKind::Text(a), EntityKind::MText(b)) if m.text => {
|
||||
b.style.clone_from(&a.style);
|
||||
b.height = a.height;
|
||||
}
|
||||
(EntityKind::MText(a), EntityKind::Text(b)) if m.text => {
|
||||
b.style.clone_from(&a.style);
|
||||
b.height = a.height;
|
||||
}
|
||||
(EntityKind::MText(a), EntityKind::MText(b)) if m.text => {
|
||||
b.style.clone_from(&a.style);
|
||||
b.height = a.height;
|
||||
}
|
||||
(EntityKind::Hatch(a), EntityKind::Hatch(b)) if m.hatch => {
|
||||
b.pattern.clone_from(&a.pattern);
|
||||
b.solid = a.solid;
|
||||
b.scale = a.scale;
|
||||
b.angle = a.angle;
|
||||
b.gradient.clone_from(&a.gradient);
|
||||
b.pattern_lines.clone_from(&a.pattern_lines);
|
||||
}
|
||||
(EntityKind::LwPolyline(a), EntityKind::LwPolyline(b)) if m.polyline => {
|
||||
b.const_width = a.const_width;
|
||||
b.plinegen = a.plinegen;
|
||||
if a.const_width > 0.0 {
|
||||
for v in &mut b.vertices {
|
||||
(v.start_width, v.end_width) = (0.0, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
(EntityKind::Viewport(a), EntityKind::Viewport(b)) if m.viewport => {
|
||||
b.locked = a.locked;
|
||||
// The same scale: paper height per model height.
|
||||
let k = a.view_height / a.height;
|
||||
if a.height > 0.0 && k.is_finite() && k > 0.0 && b.height > 0.0 {
|
||||
b.view_height = b.height * k;
|
||||
}
|
||||
}
|
||||
(EntityKind::Table(a), EntityKind::Table(b)) if m.table => b.style.clone_from(&a.style),
|
||||
(EntityKind::MLeader(a), EntityKind::MLeader(b)) if m.multileader => {
|
||||
b.style.clone_from(&a.style);
|
||||
b.arrow_size = a.arrow_size;
|
||||
b.dogleg = a.dogleg;
|
||||
b.spline = a.spline;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy the chosen properties from `src` to `targets`, skipping the source itself and objects on
|
||||
/// locked layers. Returns how many objects changed.
|
||||
fn match_onto(s: &mut Session, src: Handle, targets: &[Handle], m: &MatchProps) -> Result<usize> {
|
||||
let d = s.doc_mut()?;
|
||||
let source = d.entity(src).map(|e| (**e).clone()).ok_or_else(|| bad("matchprop", "no such source object"))?;
|
||||
if m.layer {
|
||||
d.ensure_layer(&source.common.layer);
|
||||
}
|
||||
let mut n = 0;
|
||||
for h in targets {
|
||||
if *h == src {
|
||||
continue;
|
||||
}
|
||||
let locked = d.entity(*h).and_then(|e| d.layer(&e.common.layer)).is_some_and(|l| l.locked);
|
||||
if locked || d.entity(*h).is_none() {
|
||||
continue;
|
||||
}
|
||||
d.modify_entity(*h, |e| copy_props(&source, e, m))?;
|
||||
n += 1;
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
pub(super) fn run_matchprop(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let src = match p.get("source") {
|
||||
None | Some(Value::Null) => return Err(bad("matchprop", "`source` (object handle) is required")),
|
||||
Some(v) => targets(s, &json!({ "handle": v }))?.first().copied().ok_or_else(|| bad("matchprop", "`source` is required"))?,
|
||||
};
|
||||
let tg = match p.get("targets") {
|
||||
None | Some(Value::Null) => targets(s, &json!({}))?,
|
||||
Some(v) => targets(s, &json!({ "handles": v }))?,
|
||||
};
|
||||
let m = match p.get("properties") {
|
||||
None | Some(Value::Null) => MatchProps::default(),
|
||||
Some(v) => MatchProps::from_json(v)?,
|
||||
};
|
||||
let n = match_onto(s, src, &tg, &m)?;
|
||||
Ok(json!({ "changed": n }))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Stage {
|
||||
Source,
|
||||
Dest,
|
||||
Settings,
|
||||
}
|
||||
|
||||
pub(super) struct MatchPropM {
|
||||
stage: Stage,
|
||||
source: Option<Handle>,
|
||||
}
|
||||
|
||||
impl MatchPropM {
|
||||
pub(super) fn new() -> Self {
|
||||
MatchPropM { stage: Stage::Source, source: None }
|
||||
}
|
||||
fn echo_settings(s: &mut Session) {
|
||||
let on = s.last_used.matchprop.names();
|
||||
let text = if on.is_empty() { "None".to_string() } else { on.iter().map(|n| label(n)).collect::<Vec<_>>().join(" ") };
|
||||
s.echo(format!("Current active settings: {text}"));
|
||||
}
|
||||
fn source(&mut self, s: &mut Session, h: Handle) -> Step {
|
||||
self.source = Some(h);
|
||||
self.stage = Stage::Dest;
|
||||
s.set_selection(Vec::new());
|
||||
Self::echo_settings(s);
|
||||
Step::Continue
|
||||
}
|
||||
}
|
||||
|
||||
/// The settings echo's word for a property.
|
||||
fn label(name: &str) -> &'static str {
|
||||
match name {
|
||||
"color" => "Color",
|
||||
"layer" => "Layer",
|
||||
"linetype" => "Ltype",
|
||||
"ltscale" => "Ltscale",
|
||||
"lineweight" => "Lineweight",
|
||||
"transparency" => "Transparency",
|
||||
"thickness" => "Thickness",
|
||||
"plotStyle" => "PlotStyle",
|
||||
"dimension" => "Dim",
|
||||
"text" => "Text",
|
||||
"hatch" => "Hatch",
|
||||
"polyline" => "Polyline",
|
||||
"viewport" => "Viewport",
|
||||
"table" => "Table",
|
||||
_ => "Multileader",
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactive for MatchPropM {
|
||||
fn name(&self) -> &'static str {
|
||||
"MATCHPROP"
|
||||
}
|
||||
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
||||
// One preselected object is the source.
|
||||
let pre = s.selection();
|
||||
if s.settings.pickfirst
|
||||
&& let [h] = pre.as_slice()
|
||||
{
|
||||
return Ok(self.source(s, *h));
|
||||
}
|
||||
s.set_selection(Vec::new());
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
match self.stage {
|
||||
Stage::Source => Prompt::new("Select source object", Accept::SELECT),
|
||||
Stage::Dest => Prompt::new("Select destination object(s)", Accept::SELECT).kw(&["Settings"]),
|
||||
Stage::Settings => {
|
||||
let mut kws: Vec<&str> = FIELDS.iter().map(|f| f.0).collect();
|
||||
kws.extend(["ALL", "None"]);
|
||||
Prompt::new("Enter a property to turn on or off", Accept::TEXT).kw(&kws).default("Done")
|
||||
}
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
match self.stage {
|
||||
Stage::Source => match i {
|
||||
Input::Pick(hs) => match hs.first() {
|
||||
Some(h) => Ok(self.source(s, *h)),
|
||||
None => Ok(Step::Continue),
|
||||
},
|
||||
Input::Enter => Ok(Step::Done),
|
||||
_ => Ok(Step::Continue),
|
||||
},
|
||||
Stage::Dest => match i {
|
||||
Input::Keyword(k) if k.eq_ignore_ascii_case("settings") => {
|
||||
self.stage = Stage::Settings;
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
Input::Pick(hs) => {
|
||||
let Some(src) = self.source else { return Ok(Step::Done) };
|
||||
let m = s.last_used.matchprop;
|
||||
match_onto(s, src, &hs, &m)?;
|
||||
s.set_selection(Vec::new());
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
Input::Enter => Ok(Step::Done),
|
||||
Input::Text(_) | Input::Keyword(_) => {
|
||||
s.echo("*Invalid selection*");
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
_ => Ok(Step::Continue),
|
||||
},
|
||||
Stage::Settings => {
|
||||
match &i {
|
||||
Input::Enter => {
|
||||
self.stage = Stage::Dest;
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
Input::Keyword(k) | Input::Text(k) => {
|
||||
let k = k.trim();
|
||||
if k.eq_ignore_ascii_case("all") {
|
||||
s.last_used.matchprop = MatchProps::all(true);
|
||||
} else if k.eq_ignore_ascii_case("none") {
|
||||
s.last_used.matchprop = MatchProps::all(false);
|
||||
} else if k.eq_ignore_ascii_case("done") {
|
||||
self.stage = Stage::Dest;
|
||||
return Ok(Step::Continue);
|
||||
} else if !s.last_used.matchprop.toggle(k) {
|
||||
s.echo("Invalid option keyword.");
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
Self::echo_settings(s);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -398,6 +398,8 @@ pub struct LastUsed {
|
|||
pub rotate_angle: f64,
|
||||
/// SCALE factor.
|
||||
pub scale_factor: f64,
|
||||
/// MATCHPROP Settings: the properties it copies.
|
||||
pub matchprop: cmd::MatchProps,
|
||||
/// MLEADER placement order and Options.
|
||||
pub mleader: MLeaderOptions,
|
||||
/// MLEADER's block content name (Options > Content type > Block).
|
||||
|
|
@ -411,6 +413,7 @@ impl Default for LastUsed {
|
|||
polygon_circumscribed: false,
|
||||
rotate_angle: 0.0,
|
||||
scale_factor: 1.0,
|
||||
matchprop: cmd::MatchProps::default(),
|
||||
mleader: MLeaderOptions::default(),
|
||||
mleader_block: String::new(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
//! MATCHPROP (#439): the typed source/destination picker with Settings, and the JSON form with a
|
||||
//! property list.
|
||||
|
||||
use cadcraft_engine::doc::color::Color;
|
||||
use cadcraft_engine::doc::{EntityKind, Handle};
|
||||
use cadcraft_engine::{Input, Session};
|
||||
use serde_json::json;
|
||||
|
||||
fn last(s: &Session) -> Handle {
|
||||
s.doc().unwrap().model.last().unwrap().handle
|
||||
}
|
||||
|
||||
/// A red line on layer A (the source) and two plain lines on layer 0.
|
||||
fn setup() -> (Session, Handle, Handle, Handle) {
|
||||
let mut s = Session::new();
|
||||
s.execute("layer.new", &json!({ "name": "A" })).unwrap();
|
||||
s.execute("line", &json!({ "points": [[0, 0], [10, 0]] })).unwrap();
|
||||
let src = last(&s);
|
||||
s.execute("properties.set", &json!({ "handles": [src.hex()], "layer": "A", "color": "red", "ltscale": 2.5 })).unwrap();
|
||||
s.execute("line", &json!({ "points": [[0, 5], [10, 5]] })).unwrap();
|
||||
let a = last(&s);
|
||||
s.execute("line", &json!({ "points": [[0, 9], [10, 9]] })).unwrap();
|
||||
let b = last(&s);
|
||||
(s, src, a, b)
|
||||
}
|
||||
|
||||
fn props(s: &Session, h: Handle) -> (String, Color, f64) {
|
||||
let e = s.doc().unwrap().entity(h).cloned().unwrap();
|
||||
(e.common.layer.clone(), e.common.color, e.common.ltscale)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matchprop_json_with_property_list() {
|
||||
let (mut s, src, a, b) = setup();
|
||||
let r = s.execute("matchprop", &json!({ "source": src.hex(), "targets": [a.hex()] })).unwrap();
|
||||
assert_eq!(r["changed"], 1);
|
||||
assert_eq!(props(&s, a), ("A".into(), Color::Index(1), 2.5));
|
||||
// Only the named properties; `{name: false}` turns single ones off.
|
||||
s.execute("matchprop", &json!({ "source": src.hex(), "targets": [b.hex()], "properties": ["color"] })).unwrap();
|
||||
assert_eq!(props(&s, b), ("0".into(), Color::Index(1), 1.0));
|
||||
s.execute("matchprop", &json!({ "source": src.hex(), "targets": [b.hex()], "properties": { "layer": false } })).unwrap();
|
||||
assert_eq!(props(&s, b), ("0".into(), Color::Index(1), 2.5));
|
||||
// The source itself is not counted; bad params are refused.
|
||||
let r = s.execute("matchprop", &json!({ "source": src.hex(), "targets": [src.hex()] })).unwrap();
|
||||
assert_eq!(r["changed"], 0);
|
||||
for bad in [
|
||||
json!({}),
|
||||
json!({ "source": null, "targets": [a.hex()] }),
|
||||
json!({ "source": "FFFFF", "targets": [a.hex()] }),
|
||||
json!({ "source": src.hex(), "targets": "1A" }),
|
||||
json!({ "source": src.hex(), "targets": [a.hex()], "properties": ["colour"] }),
|
||||
json!({ "source": src.hex(), "targets": [a.hex()], "properties": { "color": 1 } }),
|
||||
json!({ "source": src.hex(), "targets": [a.hex()], "properties": 7 }),
|
||||
] {
|
||||
assert!(s.execute("matchprop", &bad).is_err(), "{bad}");
|
||||
}
|
||||
assert!(s.running.is_none(), "JSON calls never prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matchprop_copies_object_specific_properties() {
|
||||
let mut s = Session::new();
|
||||
s.execute("text", &json!({ "at": [0, 0], "height": 5, "text": "src" })).unwrap();
|
||||
let t1 = last(&s);
|
||||
s.execute("text", &json!({ "at": [0, 20], "height": 1, "text": "dst" })).unwrap();
|
||||
let t2 = last(&s);
|
||||
s.execute("pline", &json!({ "vertices": [[0, 0], [10, 0], [10, 10]], "width": 2 })).unwrap();
|
||||
let p1 = last(&s);
|
||||
s.execute("pline", &json!({ "vertices": [[20, 0], [30, 0]] })).unwrap();
|
||||
let p2 = last(&s);
|
||||
s.execute("matchprop", &json!({ "source": t1.hex(), "targets": [t2.hex()], "properties": ["layer"] })).unwrap();
|
||||
let height = |s: &Session, h: Handle| match &s.doc().unwrap().entity(h).unwrap().kind {
|
||||
EntityKind::Text(t) => t.height,
|
||||
_ => f64::NAN,
|
||||
};
|
||||
assert_eq!(height(&s, t2), 1.0, "text off: height stays");
|
||||
s.execute("matchprop", &json!({ "source": t1.hex(), "targets": [t2.hex()] })).unwrap();
|
||||
assert_eq!(height(&s, t2), 5.0);
|
||||
s.execute("matchprop", &json!({ "source": p1.hex(), "targets": [p2.hex(), t2.hex()] })).unwrap();
|
||||
match &s.doc().unwrap().entity(p2).unwrap().kind {
|
||||
EntityKind::LwPolyline(p) => assert_eq!(p.const_width, 2.0),
|
||||
k => panic!("{k:?}"),
|
||||
}
|
||||
assert_eq!(height(&s, t2), 5.0, "a polyline source leaves text properties alone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matchprop_typed_picker_and_settings() {
|
||||
let (mut s, src, a, b) = setup();
|
||||
let undo = s.state().unwrap().undo.len();
|
||||
s.cmdline("ma").unwrap();
|
||||
assert!(s.prompt_text().contains("Select source object"), "{}", s.prompt_text());
|
||||
s.input(Input::Pick(vec![src])).unwrap();
|
||||
assert!(s.log.iter().any(|l| l.starts_with("Current active settings: Color Layer Ltype")), "{:?}", s.log);
|
||||
assert!(s.prompt_text().contains("Select destination object(s) or [Settings]"), "{}", s.prompt_text());
|
||||
s.input(Input::Pick(vec![a])).unwrap();
|
||||
assert_eq!(props(&s, a), ("A".into(), Color::Index(1), 2.5));
|
||||
// Settings: turn Layer and Ltscale off; Enter goes back to the destination prompt.
|
||||
s.cmdline("s").unwrap();
|
||||
assert!(s.prompt_text().contains("[Color/LAyer/LType"), "{}", s.prompt_text());
|
||||
s.cmdline("la").unwrap();
|
||||
s.cmdline("lts").unwrap();
|
||||
assert!(s.log.last().is_some_and(|l| l.contains("Color Ltype Lineweight")), "{:?}", s.log.last());
|
||||
s.cmdline("bogus").unwrap();
|
||||
assert!(s.log.iter().any(|l| l.contains("Invalid option keyword")), "{:?}", s.log);
|
||||
s.cmdline("").unwrap();
|
||||
assert!(s.prompt_text().contains("Select destination object(s)"), "{}", s.prompt_text());
|
||||
s.input(Input::Pick(vec![b])).unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
assert_eq!(props(&s, b), ("0".into(), Color::Index(1), 1.0));
|
||||
assert_eq!(s.state().unwrap().undo.len(), undo + 1, "one undo step");
|
||||
// Settings are remembered for the next typed MATCHPROP; a preselected object is the source.
|
||||
s.set_selection(vec![src]);
|
||||
s.cmdline("matchprop").unwrap();
|
||||
assert!(s.prompt_text().contains("Select destination"), "{}", s.prompt_text());
|
||||
assert!(s.log.last().is_some_and(|l| !l.contains("Layer")), "{:?}", s.log.last());
|
||||
s.cmdline("s").unwrap();
|
||||
s.cmdline("none").unwrap();
|
||||
assert!(s.log.last().is_some_and(|l| l.ends_with("None")), "{:?}", s.log.last());
|
||||
s.cmdline("all").unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
s.cancel();
|
||||
// A JSON call ignores the typed settings.
|
||||
s.execute("matchprop", &json!({ "source": src.hex(), "targets": [b.hex()] })).unwrap();
|
||||
assert_eq!(props(&s, b), ("A".into(), Color::Index(1), 2.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matchprop_skips_locked_layers() {
|
||||
let (mut s, src, a, _) = setup();
|
||||
s.execute("layer.new", &json!({ "name": "L" })).unwrap();
|
||||
s.execute("properties.set", &json!({ "handles": [a.hex()], "layer": "L" })).unwrap();
|
||||
s.execute("layer.set", &json!({ "name": "L", "locked": true })).unwrap();
|
||||
let r = s.execute("matchprop", &json!({ "source": src.hex(), "targets": [a.hex()] })).unwrap();
|
||||
assert_eq!(r["changed"], 0);
|
||||
assert_eq!(props(&s, a).0, "L");
|
||||
}
|
||||
Loading…
Reference in New Issue