mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-11 16:32:22 -06:00
* refactor: replace wildcard imports with explicit ones (S2208, batch_00) * refactor: replace wildcard imports with explicit ones (S2208, batch_00) Explicit lists now resolve the names each file uses, and the test module keeps its glob so it still sees the parent's items.
591 lines
23 KiB
Rust
591 lines
23 KiB
Rust
//! Annotative scaling: the drawing's scale list (SCALELISTEDIT), the scales annotative objects
|
|
//! support (OBJECTSCALE, Add/Delete Current Scale, ANNORESET, ANNOUPDATE), which annotative
|
|
//! objects show (ANNOALLVISIBLE) and the current annotation scale (CANNOSCALE with
|
|
//! ANNOAUTOSCALE).
|
|
|
|
use cadcraft_doc::{AnnoScale, Drawing, EntityKind, Handle, MAX_SCALES, is_annotative};
|
|
use serde_json::{Value, json};
|
|
|
|
use super::machines::{SelOutcome, SelectPhase, SelectRun};
|
|
use super::{CommandSpec, bad, f64_req, targets};
|
|
|
|
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
|
|
|
|
pub fn specs() -> Vec<CommandSpec> {
|
|
vec![
|
|
CommandSpec::new("scalelistedit", "Scale List...", run_scalelistedit)
|
|
.menu(&["Format", "Scale List..."])
|
|
.alias(&["-scalelistedit"])
|
|
.params(
|
|
"{add?: {name, ratio: \"paper:drawing\" (e.g. \"1:50\") | paperUnits + drawingUnits}, delete?: name | [names], reset?: true | \"metric\" | \"imperial\"} → scales, current (no parameters: list only)",
|
|
)
|
|
.interactive(|_| Ok(Box::new(ScaleListM::default()))),
|
|
CommandSpec::new("objectscale", "Add/Delete Scales...", run_objectscale)
|
|
.menu(&["Modify", "Annotative Object Scale", "Add/Delete Scales..."])
|
|
.alias(&["-objectscale"])
|
|
.params("{handles?, add?: name | [names], delete?: name | [names]} (scales of the drawing's list; an object keeps at least one) → changed, objects")
|
|
.interactive(|_| Ok(Box::new(ObjectScaleM::default()))),
|
|
CommandSpec::new("aiobjectscaleadd", "Add Current Scale", run_add_current)
|
|
.menu(&["Modify", "Annotative Object Scale", "Add Current Scale"])
|
|
.params("{handles?} (adds the current annotation scale to the annotative objects)")
|
|
.interactive(|_| Ok(Box::new(SelectRun::new("aiobjectscaleadd", "AIOBJECTSCALEADD")))),
|
|
CommandSpec::new("aiobjectscaleremove", "Delete Current Scale", run_remove_current)
|
|
.menu(&["Modify", "Annotative Object Scale", "Delete Current Scale"])
|
|
.params("{handles?} (removes the current annotation scale from the annotative objects; an object keeps at least one)")
|
|
.interactive(|_| Ok(Box::new(SelectRun::new("aiobjectscaleremove", "AIOBJECTSCALEREMOVE")))),
|
|
CommandSpec::new("annoreset", "Synchronize Multiple-scale Positions", run_annoreset)
|
|
.menu(&["Modify", "Annotative Object Scale", "Synchronize Multiple-scale Positions"])
|
|
.params("{handles?} (every scale of an annotative object shares one position)")
|
|
.interactive(|_| Ok(Box::new(SelectRun::new("annoreset", "ANNORESET")))),
|
|
CommandSpec::new("annoupdate", "Annotative Update", run_annoupdate)
|
|
.params("{handles?} (makes text, dimensions, multileaders and block references annotative or not as their style or block is)")
|
|
.interactive(|_| Ok(Box::new(SelectRun::new("annoupdate", "ANNOUPDATE")))),
|
|
CommandSpec::new("annovisible.current", "Current Scale Only", |s, _| set_all_visible(s, Some(false)))
|
|
.menu(&["View", "Show Annotative Objects", "Current Scale Only"])
|
|
.params("{} (ANNOALLVISIBLE 0: annotative objects show only at scales they support)"),
|
|
CommandSpec::new("annovisible.all", "All Scales", |s, _| set_all_visible(s, Some(true)))
|
|
.menu(&["View", "Show Annotative Objects", "All Scales"])
|
|
.params("{} (ANNOALLVISIBLE 1: every annotative object shows)"),
|
|
CommandSpec::new("annovisible.toggle", "Annotation Visibility", |s, _| set_all_visible(s, None)).params("{} (toggles ANNOALLVISIBLE)"),
|
|
]
|
|
}
|
|
|
|
/// The most objects one command call reports scales for.
|
|
const MAX_REPORTED: usize = 1000;
|
|
|
|
fn scale_json(sc: &AnnoScale) -> Value {
|
|
json!({ "name": sc.name, "paperUnits": sc.paper_units, "drawingUnits": sc.drawing_units })
|
|
}
|
|
|
|
fn list_json(d: &Drawing) -> Value {
|
|
json!({ "scales": d.scales.iter().map(scale_json).collect::<Vec<_>>(), "current": d.current_anno_scale().name })
|
|
}
|
|
|
|
/// Scale names given as one string or an array of strings.
|
|
fn names(cmd: &str, v: Option<&Value>) -> Result<Vec<String>> {
|
|
match v {
|
|
None | Some(Value::Null) => Ok(Vec::new()),
|
|
Some(Value::String(s)) => Ok(vec![s.trim().to_string()]),
|
|
Some(Value::Array(a)) if a.len() <= MAX_SCALES => {
|
|
a.iter().map(|v| v.as_str().map(|s| s.trim().to_string()).ok_or_else(|| bad(cmd, "scale names must be strings"))).collect()
|
|
}
|
|
Some(_) => Err(bad(cmd, "expected a scale name or an array of scale names")),
|
|
}
|
|
}
|
|
|
|
fn find_scale(d: &Drawing, cmd: &str, name: &str) -> Result<AnnoScale> {
|
|
d.anno_scale(name).cloned().ok_or_else(|| bad(cmd, format!("`{name}` is not in the scale list")))
|
|
}
|
|
|
|
/// A scale ratio typed as `paper:drawing` or `paper=drawing`, each side a number or distance
|
|
/// (`1:50`, `2:1`, `1/4":1'`).
|
|
pub(crate) fn parse_ratio(t: &str) -> Option<(f64, f64)> {
|
|
let (a, b) = t.split_once(':').or_else(|| t.split_once('='))?;
|
|
let ok = |v: f64| v.is_finite() && v > 0.0;
|
|
let a = crate::units::parse_distance(a).filter(|v| ok(*v))?;
|
|
let b = crate::units::parse_distance(b).filter(|v| ok(*v))?;
|
|
Some((a, b))
|
|
}
|
|
|
|
/// Make `name` the current annotation scale (CANNOSCALE). With ANNOAUTOSCALE on, the annotative
|
|
/// objects of model space on layers that are on, thawed and unlocked get the scale too.
|
|
pub(crate) fn set_current(s: &mut Session, name: &str) -> Result<()> {
|
|
let sc = find_scale(s.doc()?, "setvar", name)?;
|
|
let auto = s.settings.annoautoscale;
|
|
let d = s.doc_mut()?;
|
|
d.header.set_str("CANNOSCALE", &sc.name);
|
|
if auto {
|
|
let usable = |e: &cadcraft_doc::Entity| d.layer(&e.common.layer).is_none_or(|l| l.visible() && !l.locked);
|
|
let hs: Vec<Handle> = d.model.iter().filter(|e| is_annotative(e) && usable(e)).map(|e| e.handle).collect();
|
|
for h in hs {
|
|
add_scale(d, h, &sc);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Add `sc` to annotative object `h`; whether it changed.
|
|
fn add_scale(d: &mut Drawing, h: Handle, sc: &AnnoScale) -> bool {
|
|
let mut changed = false;
|
|
let _ = d.modify_entity(h, |e| {
|
|
if is_annotative(e) && !e.common.supports_scale(&sc.name) && e.common.anno_scales.len() < MAX_SCALES {
|
|
e.common.anno_scales.push(sc.clone());
|
|
changed = true;
|
|
}
|
|
});
|
|
changed
|
|
}
|
|
|
|
/// What removing a scale from an object did.
|
|
#[derive(PartialEq)]
|
|
enum Removed {
|
|
Yes,
|
|
/// It is the object's only scale.
|
|
Last,
|
|
No,
|
|
}
|
|
|
|
fn remove_scale(d: &mut Drawing, h: Handle, name: &str) -> Removed {
|
|
let mut r = Removed::No;
|
|
let _ = d.modify_entity(h, |e| {
|
|
if !is_annotative(e) || !e.common.supports_scale(name) {
|
|
return;
|
|
}
|
|
if e.common.anno_scales.len() < 2 {
|
|
r = Removed::Last;
|
|
return;
|
|
}
|
|
e.common.anno_scales.retain(|s| !s.name.eq_ignore_ascii_case(name));
|
|
r = Removed::Yes;
|
|
});
|
|
r
|
|
}
|
|
|
|
/// The annotative objects among the targets (an error when nothing is selected).
|
|
fn annotative_targets(s: &Session, p: &Value, cmd: &str) -> Result<(Vec<Handle>, usize)> {
|
|
let hs = targets(s, p)?;
|
|
if hs.is_empty() {
|
|
return Err(bad(cmd, "nothing selected"));
|
|
}
|
|
let d = s.doc()?;
|
|
let total = hs.len();
|
|
let anno: Vec<Handle> = hs.into_iter().filter(|h| d.entity(*h).is_some_and(|e| is_annotative(e))).collect();
|
|
Ok((anno, total))
|
|
}
|
|
|
|
fn objects_json(d: &Drawing, hs: &[Handle]) -> Value {
|
|
json!(
|
|
hs.iter()
|
|
.take(MAX_REPORTED)
|
|
.filter_map(|h| d.entity(*h))
|
|
.map(|e| json!({ "handle": e.handle.hex(), "scales": e.common.anno_scales.iter().map(|s| s.name.clone()).collect::<Vec<_>>() }))
|
|
.collect::<Vec<_>>()
|
|
)
|
|
}
|
|
|
|
fn run_objectscale(s: &mut Session, p: &Value) -> Result<Value> {
|
|
const CMD: &str = "objectscale";
|
|
let (hs, total) = annotative_targets(s, p, CMD)?;
|
|
let d = s.doc()?;
|
|
let add = names(CMD, p.get("add"))?.iter().map(|n| find_scale(d, CMD, n)).collect::<Result<Vec<_>>>()?;
|
|
let del = names(CMD, p.get("delete"))?;
|
|
let d = s.doc_mut()?;
|
|
let (mut changed, mut last) = (0usize, 0usize);
|
|
for h in &hs {
|
|
let mut any = false;
|
|
for sc in &add {
|
|
any |= add_scale(d, *h, sc);
|
|
}
|
|
for n in &del {
|
|
match remove_scale(d, *h, n) {
|
|
Removed::Yes => any = true,
|
|
Removed::Last => last += 1,
|
|
Removed::No => {}
|
|
}
|
|
}
|
|
changed += usize::from(any);
|
|
}
|
|
let mut msg = format!("{changed} of {total} object(s) changed");
|
|
if hs.len() < total {
|
|
msg.push_str(&format!(", {} not annotative", total - hs.len()));
|
|
}
|
|
if last > 0 {
|
|
msg.push_str(&format!(", {last} scale(s) kept as an object's only scale"));
|
|
}
|
|
Ok(json!({ "changed": changed, "annotative": hs.len(), "objects": objects_json(d, &hs), "message": msg }))
|
|
}
|
|
|
|
fn run_add_current(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cur = s.doc()?.current_anno_scale();
|
|
run_objectscale(s, &with_handles(s, p, json!({ "add": cur.name })))
|
|
}
|
|
|
|
fn run_remove_current(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cur = s.doc()?.current_anno_scale();
|
|
run_objectscale(s, &with_handles(s, p, json!({ "delete": cur.name })))
|
|
}
|
|
|
|
/// `extra` with the targets of `p` as `handles`.
|
|
fn with_handles(s: &Session, p: &Value, mut extra: Value) -> Value {
|
|
let hs = targets(s, p).unwrap_or_default();
|
|
if let Some(o) = extra.as_object_mut() {
|
|
o.insert("handles".into(), json!(hs.iter().map(|h| h.hex()).collect::<Vec<_>>()));
|
|
}
|
|
extra
|
|
}
|
|
|
|
fn run_annoreset(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let (hs, _) = annotative_targets(s, p, "annoreset")?;
|
|
// One position serves every scale of an object, so they are always in step.
|
|
Ok(json!({ "objects": hs.len(), "message": format!("{} annotative object(s) synchronized", hs.len()) }))
|
|
}
|
|
|
|
fn run_annoupdate(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let hs = targets(s, p)?;
|
|
if hs.is_empty() {
|
|
return Err(bad("annoupdate", "nothing selected"));
|
|
}
|
|
let d = s.doc()?;
|
|
let cur = d.current_anno_scale();
|
|
// Hatches have no style: HPANNOTATIVE only sets new ones.
|
|
let plan: Vec<(Handle, bool)> = hs
|
|
.iter()
|
|
.filter_map(|h| d.entity(*h))
|
|
.filter(|e| cadcraft_doc::can_be_annotative(&e.kind) && !matches!(e.kind, EntityKind::Hatch(_)))
|
|
.map(|e| (e.handle, d.annotative_by_style(&e.kind)))
|
|
.filter(|(h, want)| d.entity(*h).is_some_and(|e| is_annotative(e) != *want))
|
|
.collect();
|
|
let d = s.doc_mut()?;
|
|
for (h, want) in &plan {
|
|
d.modify_entity(*h, |e| cadcraft_doc::set_annotative(e, *want, &cur))?;
|
|
}
|
|
Ok(json!({ "updated": plan.len(), "message": format!("{} object(s) updated", plan.len()) }))
|
|
}
|
|
|
|
fn set_all_visible(s: &mut Session, on: Option<bool>) -> Result<Value> {
|
|
let d = s.doc_mut()?;
|
|
let on = on.unwrap_or(!d.anno_all_visible());
|
|
d.header.set_i64("ANNOALLVISIBLE", i64::from(on));
|
|
Ok(json!({ "annoallvisible": i64::from(on) }))
|
|
}
|
|
|
|
// ---------------- the scale list ----------------
|
|
|
|
/// Scales objects use, and the current one: they stay when the list is reset and can't be
|
|
/// deleted.
|
|
fn in_use(d: &Drawing) -> Vec<AnnoScale> {
|
|
let mut v = vec![d.current_anno_scale()];
|
|
let ents = d.model.iter().chain(d.layouts.iter().flat_map(|l| l.entities.iter()));
|
|
for e in ents.filter(|e| is_annotative(e)) {
|
|
for sc in &e.common.anno_scales {
|
|
if v.len() < MAX_SCALES && !v.iter().any(|u| u.name.eq_ignore_ascii_case(&sc.name)) {
|
|
v.push(sc.clone());
|
|
}
|
|
}
|
|
}
|
|
v
|
|
}
|
|
|
|
fn add_to_list(d: &mut Drawing, sc: AnnoScale) -> Result<()> {
|
|
const CMD: &str = "scalelistedit";
|
|
let name = sc.name.trim();
|
|
if name.is_empty() || name.chars().count() > 255 {
|
|
return Err(bad(CMD, "a scale name has 1 to 255 characters"));
|
|
}
|
|
if !sc.is_valid() {
|
|
return Err(bad(CMD, "paper and drawing units must be positive"));
|
|
}
|
|
if d.anno_scale(name).is_some() {
|
|
return Err(bad(CMD, format!("a scale named `{name}` already exists")));
|
|
}
|
|
if d.scales.len() >= MAX_SCALES {
|
|
return Err(bad(CMD, "the scale list is full"));
|
|
}
|
|
d.scales.push(AnnoScale { name: name.to_string(), ..sc });
|
|
Ok(())
|
|
}
|
|
|
|
fn delete_from_list(d: &mut Drawing, name: &str) -> Result<()> {
|
|
const CMD: &str = "scalelistedit";
|
|
let sc = find_scale(d, CMD, name)?;
|
|
if d.current_anno_scale().name.eq_ignore_ascii_case(&sc.name) {
|
|
return Err(bad(CMD, format!("`{}` is the current annotation scale", sc.name)));
|
|
}
|
|
if in_use(d).iter().any(|u| u.name.eq_ignore_ascii_case(&sc.name)) {
|
|
return Err(bad(CMD, format!("`{}` is used by annotative objects", sc.name)));
|
|
}
|
|
d.scales.retain(|s| !s.name.eq_ignore_ascii_case(&sc.name));
|
|
Ok(())
|
|
}
|
|
|
|
/// Replace the list with the default metric or imperial one, keeping scales in use.
|
|
fn reset_list(d: &mut Drawing, metric: bool) {
|
|
let keep = in_use(d);
|
|
let mut list = if metric { cadcraft_doc::metric_scales() } else { cadcraft_doc::imperial_scales() };
|
|
for sc in keep {
|
|
if !list.iter().any(|l| l.name.eq_ignore_ascii_case(&sc.name)) {
|
|
list.push(sc);
|
|
}
|
|
}
|
|
d.scales = list;
|
|
}
|
|
|
|
fn run_scalelistedit(s: &mut Session, p: &Value) -> Result<Value> {
|
|
const CMD: &str = "scalelistedit";
|
|
let metric = s.doc()?.header.i64("MEASUREMENT", 0) == 1;
|
|
let reset = match p.get("reset") {
|
|
None | Some(Value::Null) | Some(Value::Bool(false)) => None,
|
|
Some(Value::Bool(true)) => Some(metric),
|
|
Some(Value::String(t)) if t.eq_ignore_ascii_case("metric") => Some(true),
|
|
Some(Value::String(t)) if t.eq_ignore_ascii_case("imperial") => Some(false),
|
|
Some(_) => return Err(bad(CMD, "`reset` is true, \"metric\" or \"imperial\"")),
|
|
};
|
|
let del = names(CMD, p.get("delete"))?;
|
|
let add = match p.get("add") {
|
|
None | Some(Value::Null) => None,
|
|
Some(a) => {
|
|
let name = a.get("name").and_then(Value::as_str).ok_or_else(|| bad(CMD, "`add.name` is required"))?;
|
|
let (pu, du) = match a.get("ratio").and_then(Value::as_str) {
|
|
Some(r) => parse_ratio(r).ok_or_else(|| bad(CMD, format!("`{r}` is not a ratio such as 1:50")))?,
|
|
None => (f64_req(CMD, a, "paperUnits")?, f64_req(CMD, a, "drawingUnits")?),
|
|
};
|
|
Some(AnnoScale::new(name, pu, du))
|
|
}
|
|
};
|
|
let d = s.doc_mut()?;
|
|
if let Some(m) = reset {
|
|
reset_list(d, m);
|
|
}
|
|
for n in &del {
|
|
delete_from_list(d, n)?;
|
|
}
|
|
if let Some(sc) = add {
|
|
add_to_list(d, sc)?;
|
|
}
|
|
Ok(list_json(d))
|
|
}
|
|
|
|
fn echo_list(s: &mut Session) {
|
|
let lines: Vec<String> = s
|
|
.doc()
|
|
.map(|d| {
|
|
d.scales
|
|
.iter()
|
|
.map(|sc| format!(" {} ({} paper = {} drawing)", sc.name, fmt_units(sc.paper_units), fmt_units(sc.drawing_units)))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
for l in lines {
|
|
s.echo(l);
|
|
}
|
|
}
|
|
|
|
fn fmt_units(v: f64) -> String {
|
|
let r = (v * 1e6).round() / 1e6;
|
|
if r.fract() == 0.0 { format!("{r:.0}") } else { format!("{r}") }
|
|
}
|
|
|
|
#[derive(Default)]
|
|
enum ListStage {
|
|
#[default]
|
|
Option,
|
|
AddName,
|
|
AddRatio(String),
|
|
Delete,
|
|
Reset,
|
|
}
|
|
|
|
/// -SCALELISTEDIT: "Enter option [?/Add/Delete/Reset/Exit]", repeated until Exit or Enter.
|
|
#[derive(Default)]
|
|
struct ScaleListM {
|
|
stage: ListStage,
|
|
}
|
|
|
|
impl Interactive for ScaleListM {
|
|
fn name(&self) -> &'static str {
|
|
"SCALELISTEDIT"
|
|
}
|
|
fn prompt(&self, _s: &Session) -> Prompt {
|
|
match &self.stage {
|
|
ListStage::Option => Prompt::new("Enter option", Accept::TEXT).kw(&["?", "Add", "Delete", "Reset", "Exit"]).default("Exit"),
|
|
ListStage::AddName => Prompt::new("Enter name for new scale", Accept::TEXT),
|
|
ListStage::AddRatio(_) => Prompt::new("Enter scale ratio (paper units:drawing units)", Accept::TEXT).default("1:1"),
|
|
ListStage::Delete => Prompt::new("Enter name of scale to delete", Accept::TEXT).kw(&["?"]),
|
|
ListStage::Reset => Prompt::new("Reset the scale list to the defaults?", Accept::TEXT).kw(&["Yes", "No"]).default("No"),
|
|
}
|
|
}
|
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
|
let text = match i {
|
|
Input::Cancel => return Ok(Step::Cancel),
|
|
Input::Enter => String::new(),
|
|
Input::Text(t) | Input::Keyword(t) => t.trim().to_string(),
|
|
_ => return Ok(Step::Continue),
|
|
};
|
|
let stage = std::mem::take(&mut self.stage);
|
|
let result = match stage {
|
|
ListStage::Option => match text.to_ascii_lowercase().as_str() {
|
|
"" | "e" | "exit" => return Ok(Step::Done),
|
|
"?" => {
|
|
echo_list(s);
|
|
Ok(())
|
|
}
|
|
"a" | "add" => {
|
|
self.stage = ListStage::AddName;
|
|
Ok(())
|
|
}
|
|
"d" | "delete" => {
|
|
self.stage = ListStage::Delete;
|
|
Ok(())
|
|
}
|
|
"r" | "reset" => {
|
|
self.stage = ListStage::Reset;
|
|
Ok(())
|
|
}
|
|
_ => {
|
|
s.echo("Invalid option keyword.");
|
|
Ok(())
|
|
}
|
|
},
|
|
ListStage::AddName if text.is_empty() => Ok(()),
|
|
ListStage::AddName => {
|
|
if s.doc()?.anno_scale(&text).is_some() {
|
|
s.echo(format!("A scale named \"{text}\" already exists."));
|
|
} else {
|
|
self.stage = ListStage::AddRatio(text);
|
|
}
|
|
Ok(())
|
|
}
|
|
ListStage::AddRatio(name) => {
|
|
let t = if text.is_empty() { "1:1" } else { text.as_str() };
|
|
match parse_ratio(t) {
|
|
Some((pu, du)) => add_to_list(s.doc_mut()?, AnnoScale::new(&name, pu, du)),
|
|
None => {
|
|
s.echo("Enter a ratio such as 1:50.");
|
|
self.stage = ListStage::AddRatio(name);
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
ListStage::Delete => match text.as_str() {
|
|
"" => Ok(()),
|
|
"?" => {
|
|
echo_list(s);
|
|
self.stage = ListStage::Delete;
|
|
Ok(())
|
|
}
|
|
n => delete_from_list(s.doc_mut()?, n),
|
|
},
|
|
ListStage::Reset => {
|
|
if matches!(text.to_ascii_lowercase().as_str(), "y" | "yes") {
|
|
let metric = s.doc()?.header.i64("MEASUREMENT", 0) == 1;
|
|
reset_list(s.doc_mut()?, metric);
|
|
}
|
|
Ok(())
|
|
}
|
|
};
|
|
if let Err(e) = result {
|
|
s.echo(e.to_string());
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Clone, Copy, PartialEq)]
|
|
enum ObjStage {
|
|
#[default]
|
|
Select,
|
|
Option,
|
|
Add,
|
|
Delete,
|
|
}
|
|
|
|
/// -OBJECTSCALE: select annotative objects, then add or delete scales by name until Enter.
|
|
#[derive(Default)]
|
|
struct ObjectScaleM {
|
|
sel: SelectPhase,
|
|
stage: ObjStage,
|
|
hs: Vec<Handle>,
|
|
}
|
|
|
|
impl ObjectScaleM {
|
|
/// Keep the annotative objects of the selection; false when there are none.
|
|
fn take(&mut self, s: &mut Session, picked: Vec<Handle>) -> Result<bool> {
|
|
let d = s.doc()?;
|
|
self.hs = picked.into_iter().filter(|h| d.entity(*h).is_some_and(|e| is_annotative(e))).collect();
|
|
if self.hs.is_empty() {
|
|
s.echo("No annotative objects selected.");
|
|
return Ok(false);
|
|
}
|
|
self.stage = ObjStage::Option;
|
|
Ok(true)
|
|
}
|
|
fn apply(&self, s: &mut Session, key: &str, name: &str) {
|
|
let mut p = json!({ "handles": self.hs.iter().map(|h| h.hex()).collect::<Vec<_>>() });
|
|
if let Some(o) = p.as_object_mut() {
|
|
o.insert(key.to_string(), json!(name));
|
|
}
|
|
match run_objectscale(s, &p) {
|
|
Ok(r) => {
|
|
if let Some(m) = r.get("message").and_then(Value::as_str) {
|
|
s.echo(m.to_string());
|
|
}
|
|
}
|
|
Err(e) => s.echo(e.to_string()),
|
|
}
|
|
}
|
|
fn echo_supported(&self, s: &mut Session) {
|
|
let Ok(d) = s.doc() else { return };
|
|
let mut names: Vec<String> = Vec::new();
|
|
for e in self.hs.iter().filter_map(|h| d.entity(*h)) {
|
|
for sc in &e.common.anno_scales {
|
|
if names.len() < MAX_SCALES && !names.iter().any(|n| n.eq_ignore_ascii_case(&sc.name)) {
|
|
names.push(sc.name.clone());
|
|
}
|
|
}
|
|
}
|
|
s.echo(format!("Scales of the selected objects: {}", names.join(", ")));
|
|
}
|
|
}
|
|
|
|
impl Interactive for ObjectScaleM {
|
|
fn name(&self) -> &'static str {
|
|
"OBJECTSCALE"
|
|
}
|
|
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
|
self.sel = SelectPhase::begin(s);
|
|
if self.sel.done {
|
|
let picked = self.sel.picked.clone();
|
|
if !self.take(s, picked)? {
|
|
return Ok(Step::Done);
|
|
}
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
fn prompt(&self, s: &Session) -> Prompt {
|
|
let cur = s.doc().map(|d| d.current_anno_scale().name).unwrap_or_else(|_| "1:1".into());
|
|
match self.stage {
|
|
ObjStage::Select => Prompt::new(if self.sel.removing { "Remove objects" } else { "Select annotative objects" }, Accept::SELECT),
|
|
ObjStage::Option => Prompt::new("Enter an option", Accept::TEXT).kw(&["Add", "Delete", "?"]),
|
|
ObjStage::Add => Prompt::new("Enter named scale to add", Accept::TEXT).kw(&["?"]).default(cur),
|
|
ObjStage::Delete => Prompt::new("Enter named scale to delete", Accept::TEXT).kw(&["?"]).default(cur),
|
|
}
|
|
}
|
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
|
if i == Input::Cancel {
|
|
return Ok(Step::Cancel);
|
|
}
|
|
if self.stage == ObjStage::Select {
|
|
return match self.sel.feed(s, &i)? {
|
|
SelOutcome::More => Ok(Step::Continue),
|
|
SelOutcome::Empty => Ok(Step::Done),
|
|
SelOutcome::Done(hs) => Ok(if self.take(s, hs)? { Step::Continue } else { Step::Done }),
|
|
};
|
|
}
|
|
let text = match i {
|
|
Input::Enter => String::new(),
|
|
Input::Text(t) | Input::Keyword(t) => t.trim().to_string(),
|
|
_ => return Ok(Step::Continue),
|
|
};
|
|
match self.stage {
|
|
ObjStage::Option => match text.to_ascii_lowercase().as_str() {
|
|
"" => return Ok(Step::Done),
|
|
"a" | "add" => self.stage = ObjStage::Add,
|
|
"d" | "delete" => self.stage = ObjStage::Delete,
|
|
"?" => self.echo_supported(s),
|
|
_ => s.echo("Invalid option keyword."),
|
|
},
|
|
stage @ (ObjStage::Add | ObjStage::Delete) => {
|
|
if text == "?" {
|
|
echo_list(s);
|
|
return Ok(Step::Continue);
|
|
}
|
|
let name = if text.is_empty() { s.doc()?.current_anno_scale().name } else { text };
|
|
self.apply(s, if stage == ObjStage::Add { "add" } else { "delete" }, &name);
|
|
self.stage = ObjStage::Option;
|
|
}
|
|
ObjStage::Select => {}
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
}
|