mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-29 05:16:49 -06:00
Squash of PR #486 by @vangel76. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
590 lines
23 KiB
Rust
590 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::*;
|
|
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)
|
|
}
|
|
}
|