Files
cadcraft/crates/engine/src/cmd/layer.rs
T
Kostykandkostyk348 acb28313c6 Layer Off: stay active until Enter or Esc (#1418) (#1426)
LAYOFF ended after a single pick, so turning off five layers took five command
invocations. AutoCAD's Layer Off keeps prompting: you pick object after object
and the command ends on Enter or Esc only.

`SelectRun` grows a `repeat` mode next to the existing single-pick and plain
select modes: `SelectRun::repeat(..)` acts on every pick, seeds the next prompt
straight away, and finishes on Enter (or Esc through the usual cancel path).
The non-repeating paths are untouched — a repeating run must not let one pick
end the command, so `sel.single` is disabled for it and Enter is handled before
the selection machine's own keywords.

The whole run stays a single undo step, because the command body is still
called once per pick but the running command records one step, as before.

Tests: `crates/engine/tests/layoff_repeat.rs`
  cargo test -p cadcraft-engine --test layoff_repeat   -> 4 passed
  - keeps asking until Enter, two layers off, one undo takes both back
  - Esc ends it and keeps what was already turned off
  - an empty pick ("0 found") keeps it running
  - pickfirst and JSON calls are unchanged

Co-authored-by: kostyk348 <kostyk348@users.noreply.github.com>
2026-10-19 02:32:14 -04:00

537 lines
24 KiB
Rust

//! Layers: LAYER (programmatic sub-ids, -LAYER prompts), layer tools (LAYMCUR, LAYISO, LAYOFF, LAYFRZ…).
mod prompts;
use cadcraft_color::Color;
use cadcraft_doc::{Layer, Lineweight};
use serde_json::{Value, json};
use super::machines::SelectRun;
use super::*;
use crate::{Result, Session};
pub fn specs() -> Vec<CommandSpec> {
vec![
CommandSpec::new("layer", "Layers", run_list)
.menu(&["Format", "Layers"])
.alias(&["la", "layers", "-layer"])
.params("{} → layers, states (typed -LAYER: option prompts)")
.interactive(|_| Ok(Box::new(prompts::LayerM::default()))),
CommandSpec::new("layer.new", "New Layer", run_new).params("{name, color?, linetype?, lineweight? (mm), current?: bool}"),
CommandSpec::new("layer.set", "Set Layer Properties", run_set)
.params("{name, on?, frozen?, locked?, plot?, color?, linetype?, lineweight?, transparency?, description?, newVpFreeze?, plotStyle? (named plot style), newName?}"),
CommandSpec::new("layer.current", "Make Current", run_current)
.menu(&["Format", "Layer Tools", "Make Current"])
.alias(&["clayer"])
.params("{name}"),
CommandSpec::new("layer.delete", "Delete Layer", run_delete).params("{name}"),
CommandSpec::new("laymcur", "Make Object's Layer Current", run_laymcur).params("{handles?}"),
CommandSpec::new("laymch", "Layer Match", run_laymch).menu(&["Format", "Layer Tools", "Layer Match"]).params("{handles?, layer}"),
CommandSpec::new("laycur", "Change to Current Layer", run_laycur)
.menu(&["Format", "Layer Tools", "Change to Current Layer"])
.params("{handles?}")
.interactive(|_| Ok(Box::new(SelectRun::new("laycur", "LAYCUR")))),
CommandSpec::new("layiso", "Isolate Layer", run_layiso).menu(&["Format", "Layer Tools", "Isolate Layer"]).params("{handles?}"),
CommandSpec::new("layuniso", "Unisolate Layer", run_layuniso).menu(&["Format", "Layer Tools", "Unisolate Layer"]),
CommandSpec::new("layfrz", "Freeze Layer", |s, p| set_obj_layers(s, p, |l| l.frozen = true))
.menu(&["Format", "Layer Tools", "Freeze Layer"])
.params("{handles?}")
.interactive(|_| Ok(Box::new(SelectRun::single("layfrz", "LAYFRZ", "Select an object on the layer to be frozen")))),
CommandSpec::new("layoff", "Layer Off", |s, p| set_obj_layers(s, p, |l| l.on = false))
.menu(&["Format", "Layer Tools", "Layer Off"])
.params("{handles?}")
// Layer Off stays active until Enter or Esc, as in AutoCAD (#1418): one object per
// pick and the prompt comes straight back.
.interactive(|_| Ok(Box::new(SelectRun::repeat("layoff", "LAYOFF", "Select an object on the layer to be turned off")))),
CommandSpec::new("laylck", "Lock Layer", |s, p| set_obj_layers(s, p, |l| l.locked = true))
.menu(&["Format", "Layer Tools", "Lock Layer"])
.params("{handles?}")
.interactive(|_| Ok(Box::new(SelectRun::single("laylck", "LAYLCK", "Select an object on the layer to be locked")))),
CommandSpec::new("layulk", "Unlock Layer", |s, p| set_obj_layers(s, p, |l| l.locked = false))
.menu(&["Format", "Layer Tools", "Unlock Layer"])
.params("{handles?}")
.interactive(|_| Ok(Box::new(SelectRun::single("layulk", "LAYULK", "Select an object on the layer to be unlocked")))),
CommandSpec::new("layon", "Turn All Layers On", |s, _| all_layers(s, |l| l.on = true)),
CommandSpec::new("laythw", "Thaw All Layers", |s, _| all_layers(s, |l| l.frozen = false)),
CommandSpec::new("layerp", "Previous Layer", run_layerp).menu(&["Format", "Layer Tools", "Previous Layer"]),
CommandSpec::new("layerstate.save", "Save Layer State", run_state_save).menu(&["Format", "Layer States Manager..."]).params("{name}"),
CommandSpec::new("layerstate.restore", "Restore Layer State", run_state_restore).params("{name}"),
CommandSpec::new("layerstate.list", "List Layer States", run_state_list).params("{} → states").noundo(),
CommandSpec::new("layerstate.delete", "Delete Layer State", run_state_delete).params("{name}"),
CommandSpec::new("layerstate.rename", "Rename Layer State", run_state_rename).params("{from, to}"),
CommandSpec::new("laymrg", "Layer Merge", merge::run_laymrg)
.menu(&["Format", "Layer Tools", "Layer Merge"])
.params("{from: [names], to} (the from layers' objects move to `to`, then the from layers are deleted)")
.interactive(|_| Ok(Box::new(merge::LayerRemove::merge()))),
CommandSpec::new("laydel", "Layer Delete", merge::run_laydel)
.menu(&["Format", "Layer Tools", "Layer Delete"])
.params("{names: [...]} (deletes the layers and every object on them)")
.interactive(|_| Ok(Box::new(merge::LayerRemove::delete()))),
]
}
fn layer_json(l: &Layer, current: bool) -> Value {
json!({
"name": l.name, "on": l.on, "frozen": l.frozen, "locked": l.locked, "plot": l.plot,
"color": l.color.name(), "colorRgb": l.color.resolve(Color::Index(7), Color::Index(7)).hex(),
"linetype": l.linetype, "lineweight": l.lineweight.name(), "transparency": l.transparency,
"description": l.description, "current": current, "newVpFreeze": l.vp_freeze_new, "plotStyle": l.plot_style,
})
}
fn run_list(s: &mut Session, _p: &Value) -> Result<Value> {
let d = s.doc()?;
let cur = d.header.str("CLAYER", "0");
let used = used_layers(d);
let layers: Vec<Value> = d
.layers
.iter()
.map(|l| {
let mut v = layer_json(l, l.name.eq_ignore_ascii_case(&cur));
if let Some(o) = v.as_object_mut() {
o.insert("used".into(), json!(used.contains(&l.name.to_ascii_lowercase())));
}
v
})
.collect();
Ok(json!({ "layers": layers, "states": d.layer_states.iter().map(|st| st.name.clone()).collect::<Vec<_>>() }))
}
/// Lower-case names of layers that have objects on them (model, layouts and block definitions).
pub fn used_layers(d: &cadcraft_doc::Drawing) -> std::collections::HashSet<String> {
let mut set = std::collections::HashSet::new();
let ents = d.model.iter().chain(d.layouts.iter().flat_map(|l| l.entities.iter())).chain(d.blocks.values().flat_map(|b| b.entities.iter()));
for e in ents.take(5_000_000) {
if !set.contains(&e.common.layer.to_ascii_lowercase()) {
set.insert(e.common.layer.to_ascii_lowercase());
}
}
set
}
pub(super) fn parse_lw(v: &Value) -> Option<Lineweight> {
if let Some(f) = v.as_f64() {
return Some(Lineweight::Mm100((f * 100.0).round().clamp(0.0, 211.0) as u16));
}
match v.as_str()?.to_ascii_lowercase().as_str() {
"bylayer" => Some(Lineweight::ByLayer),
"byblock" => Some(Lineweight::ByBlock),
"default" => Some(Lineweight::Default),
s => s.trim_end_matches("mm").trim().parse::<f64>().ok().map(|f| Lineweight::Mm100((f * 100.0).round().clamp(0.0, 211.0) as u16)),
}
}
fn apply(l: &mut Layer, p: &Value) -> Result<()> {
if let Some(v) = p.get("on").and_then(Value::as_bool) {
l.on = v;
}
if let Some(v) = p.get("frozen").and_then(Value::as_bool) {
l.frozen = v;
}
if let Some(v) = p.get("locked").and_then(Value::as_bool) {
l.locked = v;
}
if let Some(v) = p.get("plot").and_then(Value::as_bool) {
l.plot = v;
}
if let Some(v) = str_param(p, "plotStyle").map(str::trim) {
if v.is_empty() || v.eq_ignore_ascii_case("bylayer") || v.eq_ignore_ascii_case("byblock") {
return Err(bad("layer", "a layer's plot style is a style name (such as Normal)"));
}
l.plot_style = v.chars().take(255).collect();
}
if let Some(c) = p.get("color") {
let c = c
.as_str()
.and_then(Color::parse)
.or_else(|| c.as_u64().and_then(|i| u8::try_from(i).ok()).filter(|i| *i > 0).map(Color::Index))
.ok_or_else(|| bad("layer", "bad colour"))?;
if matches!(c, Color::ByLayer | Color::ByBlock) {
return Err(bad("layer", "a layer colour must be an index or true colour"));
}
l.color = c;
}
if let Some(lt) = str_param(p, "linetype") {
l.linetype = lt.to_string();
}
if let Some(lw) = p.get("lineweight") {
l.lineweight = parse_lw(lw).ok_or_else(|| bad("layer", "bad lineweight"))?;
}
if let Some(t) = p.get("transparency").and_then(Value::as_u64) {
l.transparency = t.min(90) as u8;
}
if let Some(v) = p.get("newVpFreeze").and_then(Value::as_bool) {
l.vp_freeze_new = v;
}
if let Some(d) = str_param(p, "description") {
l.description = d.to_string();
}
Ok(())
}
/// The loaded linetype named by `linetype`, if given: a layer cannot use ByLayer, ByBlock or a
/// linetype the drawing does not have.
fn loaded_linetype(d: &cadcraft_doc::Drawing, cmd: &str, p: &Value) -> Result<Option<String>> {
let Some(lt) = str_param(p, "linetype") else { return Ok(None) };
if lt.eq_ignore_ascii_case("ByLayer") || lt.eq_ignore_ascii_case("ByBlock") {
return Err(bad(cmd, "a layer linetype cannot be ByLayer or ByBlock"));
}
let t = d.linetype(lt).ok_or_else(|| bad(cmd, format!("linetype `{lt}` is not loaded (load it with LINETYPE)")))?;
Ok(Some(t.name.clone()))
}
fn valid_name(n: &str) -> bool {
!n.trim().is_empty() && n.len() <= 255 && !n.chars().any(|c| "<>/\\\":;?*|,=`".contains(c))
}
fn run_new(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "name").ok_or_else(|| bad("layer.new", "`name` is required"))?.to_string();
if !valid_name(&name) {
return Err(bad("layer.new", "invalid layer name"));
}
let d = s.doc_mut()?;
if d.layer(&name).is_some() {
return Err(bad("layer.new", format!("layer `{name}` already exists")));
}
let lt = loaded_linetype(d, "layer.new", p)?;
let mut l = Layer::new(&name);
apply(&mut l, p)?;
if let Some(lt) = lt {
l.linetype = lt;
}
d.layers.push(l);
if bool_or(p, "current", false) {
d.header.set_str("CLAYER", &name);
}
ok()
}
fn run_set(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "name").ok_or_else(|| bad("layer.set", "`name` is required"))?.to_string();
let new_name = str_param(p, "newName").map(str::to_string);
// Validate on a copy before borrowing the drawing mutably, so a rejected call
// leaves the layer untouched and records no undo step.
let d = s.doc()?;
let cur = d.header.str("CLAYER", "0");
let mut updated = d.layer(&name).cloned().ok_or_else(|| bad("layer.set", format!("no layer `{name}`")))?;
let old = updated.name.clone();
let lt = loaded_linetype(d, "layer.set", p)?;
apply(&mut updated, p)?;
if let Some(lt) = lt {
updated.linetype = lt;
}
if p.get("frozen").and_then(Value::as_bool) == Some(true) && old.eq_ignore_ascii_case(&cur) {
return Err(bad("layer.set", "cannot freeze the current layer"));
}
if let Some(nn) = &new_name {
if old == "0" || old.eq_ignore_ascii_case("Defpoints") {
return Err(bad("layer.set", "cannot rename layer 0 or Defpoints"));
}
if !valid_name(nn) {
return Err(bad("layer.set", "invalid layer name"));
}
if !nn.eq_ignore_ascii_case(&old) && d.layer(nn).is_some() {
return Err(bad("layer.set", format!("layer `{nn}` already exists")));
}
}
let d = s.doc_mut()?;
let l = d.layer_mut(&old).ok_or_else(|| bad("layer.set", format!("no layer `{name}`")))?;
*l = updated;
if let Some(nn) = new_name {
l.name = nn.clone();
// Re-point entities in model space, every layout and every block definition.
repoint(&mut d.model, &old, &nn);
for layout in &mut d.layouts {
repoint(&mut layout.entities, &old, &nn);
}
for block in d.blocks.values_mut() {
if block.entities.iter().any(|e| e.common.layer.eq_ignore_ascii_case(&old)) {
repoint(&mut std::sync::Arc::make_mut(block).entities, &old, &nn);
}
}
if cur.eq_ignore_ascii_case(&old) {
d.header.set_str("CLAYER", &nn);
}
}
ok()
}
fn repoint(store: &mut cadcraft_doc::EntityStore, old: &str, new: &str) {
let hs: Vec<_> = store.iter().filter(|e| e.common.layer.eq_ignore_ascii_case(old)).map(|e| e.handle).collect();
for h in hs {
store.modify(h, |e| e.common.layer = new.to_string());
}
}
fn run_current(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "name").ok_or_else(|| bad("layer.current", "`name` is required"))?.to_string();
let d = s.doc_mut()?;
let l = d.layer_mut(&name).ok_or_else(|| bad("layer.current", format!("no layer `{name}`")))?;
l.frozen = false;
let n = l.name.clone();
prev_push(d);
d.header.set_str("CLAYER", &n);
ok()
}
fn prev_push(d: &mut cadcraft_doc::Drawing) {
let snap = serde_json::to_string(&d.layers).unwrap_or_default();
d.header.set_str("CADCRAFT_LAYERP", &snap);
}
fn run_layerp(s: &mut Session, _p: &Value) -> Result<Value> {
let d = s.doc_mut()?;
let snap = d.header.str("CADCRAFT_LAYERP", "");
let layers: Vec<Layer> = serde_json::from_str(&snap).map_err(|_| bad("layerp", "No previous layer state."))?;
d.layers = layers;
Ok(json!({"message": "Restored previous layer states."}))
}
fn run_delete(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "name").ok_or_else(|| bad("layer.delete", "`name` is required"))?.to_string();
let d = s.doc_mut()?;
if name == "0" || name.eq_ignore_ascii_case("Defpoints") || d.header.str("CLAYER", "0").eq_ignore_ascii_case(&name) {
return Err(bad("layer.delete", "cannot delete layer 0, Defpoints or the current layer"));
}
let used = d.model.iter().any(|e| e.common.layer.eq_ignore_ascii_case(&name))
|| d.layouts.iter().any(|l| l.entities.iter().any(|e| e.common.layer.eq_ignore_ascii_case(&name)))
|| d.blocks.values().any(|b| b.entities.iter().any(|e| e.common.layer.eq_ignore_ascii_case(&name)));
if used {
return Err(bad("layer.delete", "layer has objects on it"));
}
d.layers.retain(|l| !l.name.eq_ignore_ascii_case(&name));
merge::forget_in_viewports(d, &std::collections::HashSet::from([name.to_ascii_lowercase()]));
ok()
}
fn obj_layers(s: &Session, p: &Value) -> Result<Vec<String>> {
let hs = targets(s, p)?;
let d = s.doc()?;
let mut v: Vec<String> = hs.iter().filter_map(|h| d.entity(*h).map(|e| e.common.layer.clone())).collect();
v.sort();
v.dedup();
Ok(v)
}
fn run_laymcur(s: &mut Session, p: &Value) -> Result<Value> {
let ls = obj_layers(s, p)?;
let l = ls.first().cloned().ok_or_else(|| bad("laymcur", "select an object"))?;
s.doc_mut()?.header.set_str("CLAYER", &l);
Ok(json!({ "message": format!("{l} is now the current layer.") }))
}
fn run_laymch(s: &mut Session, p: &Value) -> Result<Value> {
let hs = targets(s, p)?;
let layer = str_param(p, "layer").ok_or_else(|| bad("laymch", "`layer` is required"))?.to_string();
let d = s.doc_mut()?;
d.ensure_layer(&layer);
for h in &hs {
d.modify_entity(*h, |e| e.common.layer = layer.clone())?;
}
Ok(json!({ "changed": hs.len() }))
}
fn run_laycur(s: &mut Session, p: &Value) -> Result<Value> {
let hs = targets(s, p)?;
let d = s.doc_mut()?;
let cur = d.header.str("CLAYER", "0");
for h in &hs {
d.modify_entity(*h, |e| e.common.layer = cur.clone())?;
}
Ok(json!({ "changed": hs.len() }))
}
fn run_layiso(s: &mut Session, p: &Value) -> Result<Value> {
let keep = obj_layers(s, p)?;
if keep.is_empty() {
return Err(bad("layiso", "select objects on the layers to isolate"));
}
let d = s.doc_mut()?;
prev_push(d);
d.header.set_str("CADCRAFT_LAYISO", &serde_json::to_string(&d.layers).unwrap_or_default());
for l in &mut d.layers {
if !keep.iter().any(|k| k.eq_ignore_ascii_case(&l.name)) {
l.on = false;
}
}
if let Some(k) = keep.first() {
d.header.set_str("CLAYER", k);
}
Ok(json!({ "message": format!("Layer(s) {} have been isolated.", keep.join(", ")) }))
}
fn run_layuniso(s: &mut Session, _p: &Value) -> Result<Value> {
let d = s.doc_mut()?;
let snap = d.header.str("CADCRAFT_LAYISO", "");
let layers: Vec<Layer> = serde_json::from_str(&snap).map_err(|_| bad("layuniso", "No isolated layers."))?;
d.layers = layers;
ok()
}
fn set_obj_layers(s: &mut Session, p: &Value, f: fn(&mut Layer)) -> Result<Value> {
let ls = obj_layers(s, p)?;
let d = s.doc_mut()?;
let cur = d.header.str("CLAYER", "0");
prev_push(d);
for n in &ls {
if let Some(l) = d.layer_mut(n) {
let was_frozen = l.frozen;
f(l);
if l.frozen && !was_frozen && l.name.eq_ignore_ascii_case(&cur) {
l.frozen = false;
}
}
}
s.set_selection(Vec::new());
Ok(json!({ "layers": ls }))
}
fn all_layers(s: &mut Session, f: fn(&mut Layer)) -> Result<Value> {
let d = s.doc_mut()?;
prev_push(d);
d.layers.iter_mut().for_each(f);
ok()
}
fn run_state_save(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "name").ok_or_else(|| bad("layerstate.save", "`name` is required"))?.to_string();
let d = s.doc_mut()?;
let layers = d.layers.clone();
d.layer_states.retain(|st| st.name != name);
d.layer_states.push(cadcraft_doc::LayerState { name, layers });
ok()
}
fn run_state_restore(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "name").ok_or_else(|| bad("layerstate.restore", "`name` is required"))?;
let d = s.doc_mut()?;
let st = d.layer_states.iter().find(|st| st.name == name).cloned().ok_or_else(|| bad("layerstate.restore", "no such layer state"))?;
for saved in st.layers {
if let Some(l) = d.layer_mut(&saved.name) {
*l = saved;
}
}
ok()
}
fn run_state_list(s: &mut Session, _p: &Value) -> Result<Value> {
let d = s.doc()?;
let states: Vec<Value> = d.layer_states.iter().map(|st| json!({ "name": st.name, "layers": st.layers.len() })).collect();
let msg = if states.is_empty() {
"No saved layer states.".to_string()
} else {
d.layer_states.iter().map(|st| st.name.clone()).collect::<Vec<_>>().join(", ")
};
Ok(json!({ "states": states, "message": msg }))
}
fn run_state_delete(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "name").ok_or_else(|| bad("layerstate.delete", "`name` is required"))?;
let d = s.doc_mut()?;
if !d.layer_states.iter().any(|st| st.name == name) {
return Err(bad("layerstate.delete", "no such layer state"));
}
d.layer_states.retain(|st| st.name != name);
ok()
}
fn run_state_rename(s: &mut Session, p: &Value) -> Result<Value> {
let from = str_param(p, "from").ok_or_else(|| bad("layerstate.rename", "`from` is required"))?;
let to = str_param(p, "to").map(str::trim).ok_or_else(|| bad("layerstate.rename", "`to` is required"))?.to_string();
if !valid_name(&to) {
return Err(bad("layerstate.rename", "invalid name"));
}
let d = s.doc_mut()?;
if d.layer_states.iter().any(|st| st.name == to) {
return Err(bad("layerstate.rename", format!("layer state `{to}` already exists")));
}
let st = d.layer_states.iter_mut().find(|st| st.name == from).ok_or_else(|| bad("layerstate.rename", "no such layer state"))?;
st.name = to;
ok()
}
// LAYMRG and LAYDEL.
mod merge;
#[cfg(test)]
mod tests {
use serde_json::json;
use crate::Session;
#[test]
fn layer_states_save_list_restore_delete() {
let mut s = Session::new();
s.execute("layer.new", &json!({ "name": "A", "color": 1 })).unwrap();
s.execute("layerstate.save", &json!({ "name": "base" })).unwrap();
s.execute("layer.set", &json!({ "name": "A", "on": false, "color": 3 })).unwrap();
let l = s.execute("layerstate.list", &json!({})).unwrap();
assert_eq!(l["states"][0]["name"], "base");
s.execute("layerstate.restore", &json!({ "name": "base" })).unwrap();
let a = s.doc().unwrap().layer("A").unwrap().clone();
assert!(a.on);
assert_eq!(a.color, cadcraft_color::Color::Index(1));
s.execute("layerstate.rename", &json!({ "from": "base", "to": "start" })).unwrap();
assert!(s.execute("layerstate.delete", &json!({ "name": "base" })).is_err());
s.execute("layerstate.delete", &json!({ "name": "start" })).unwrap();
assert!(s.execute("layerstate.list", &json!({})).unwrap()["states"].as_array().unwrap().is_empty());
// `used` flags in the layer list.
s.execute("line", &json!({ "points": [[0, 0], [1, 1]] })).unwrap();
let v = s.execute("layer", &json!({})).unwrap();
let used: Vec<bool> = v["layers"].as_array().unwrap().iter().map(|l| l["used"].as_bool().unwrap()).collect();
assert!(used.contains(&true) && used.contains(&false));
}
#[test]
fn rejected_layer_set_changes_nothing() {
let mut s = Session::new();
// Layer 0 is current; freezing it is rejected and must not apply the colour either.
let before = s.doc().unwrap().layer("0").unwrap().clone();
let undo = s.state().unwrap().undo.len();
assert!(s.execute("layer.set", &json!({ "name": "0", "frozen": true, "color": 3 })).is_err());
assert_eq!(s.doc().unwrap().layer("0").unwrap(), &before);
assert_eq!(s.state().unwrap().undo.len(), undo, "no undo step for a rejected call");
// Same for a rejected rename.
assert!(s.execute("layer.set", &json!({ "name": "0", "newName": "X", "color": 3 })).is_err());
assert_eq!(s.doc().unwrap().layer("0").unwrap(), &before);
assert_eq!(s.state().unwrap().undo.len(), undo, "no undo step for a rejected call");
// A valid colour-only edit of the current layer still applies.
s.execute("layer.set", &json!({ "name": "0", "color": 3 })).unwrap();
assert_ne!(s.doc().unwrap().layer("0").unwrap(), &before);
}
#[test]
fn delete_and_purge_keep_layers_used_by_block_definitions() {
let mut s = Session::new();
s.execute("layer.new", &json!({ "name": "A", "color": 1 })).unwrap();
s.execute("layer.current", &json!({ "name": "A" })).unwrap();
let l = s.execute("line", &json!({ "points": [[0, 0], [1, 1]] })).unwrap()["handles"][0].as_str().unwrap().to_string();
s.execute("block", &json!({ "name": "B", "base": [0, 0], "handles": [l], "keep": "delete" })).unwrap();
s.execute("layer.current", &json!({ "name": "0" })).unwrap();
s.execute("insert", &json!({ "name": "B", "at": [5, 5] })).unwrap();
assert!(s.execute("layer.delete", &json!({ "name": "A" })).is_err());
s.execute("purge", &json!({})).unwrap();
assert!(s.doc().unwrap().layer("A").is_some());
}
#[test]
fn rename_repoints_paper_space_and_block_entities() {
let mut s = Session::new();
s.execute("layer.new", &json!({ "name": "A", "color": "red", "current": true })).unwrap();
s.execute("line", &json!({ "points": [[0, 0], [1, 1]] })).unwrap();
s.execute("selectall", &json!({})).unwrap();
s.execute("block", &json!({ "name": "Part", "base": [0, 0], "keep": "delete" })).unwrap();
s.execute("layout.set", &json!({ "name": "Layout1" })).unwrap();
s.execute("line", &json!({ "points": [[0, 0], [1, 1]] })).unwrap();
s.execute("layer.set", &json!({ "name": "A", "newName": "B" })).unwrap();
let d = s.doc().unwrap();
let on_layer = |name: &str| {
let paper = d.layouts.iter().flat_map(|l| l.entities.iter());
let blocks = d.blocks.values().flat_map(|b| b.entities.iter());
paper.chain(blocks).filter(|e| e.common.layer == name).count()
};
assert_eq!(on_layer("A"), 0);
assert_eq!(on_layer("B"), 2);
}
}