cadcraft/crates/engine/src/cmd/blocks.rs

754 lines
31 KiB
Rust

//! Blocks: BLOCK, INSERT, ATTDEF, ATTEDIT (programmatic), WBLOCK, BASE, PURGE, block listing.
mod purge;
use std::sync::Arc;
use cadcraft_doc::{Attrib, Block, Entity, EntityKind, HAlign, Handle, Insert, Text, VAlign};
use cadcraft_geom::{Mat3, Vec2, Vec3};
use serde_json::{Value, json};
use super::helpers::v3;
use super::machines::{SelOutcome, SelectPhase, number};
use super::*;
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
mod attedit;
pub fn specs() -> Vec<CommandSpec> {
vec![
CommandSpec::new("block", "Make...", run_block)
.menu(&["Draw", "Block", "Make..."])
.alias(&["b", "bmake"])
.params("{name, base: [x,y], handles?, keep?: \"convert\"|\"retain\"|\"delete\" (default \"convert\"), description?}")
.interactive(|_| Ok(Box::new(BlockM::default()))),
// -BLOCK erases the selected objects once the block is defined.
CommandSpec::new("-block", "Create Block", run_dash_block)
.alias(&["-b"])
.params("{name, base: [x,y], handles?, keep?: \"delete\"|\"retain\"|\"convert\" (default \"delete\"), description?}")
.interactive(|_| Ok(Box::new(BlockM { dash: true, ..BlockM::default() }))),
CommandSpec::new("insert", "Block...", run_insert)
.menu(&["Insert", "Block..."])
.alias(&["i", "-insert", "ddinsert"])
.params("{name (\"*NAME\": exploded), file?: DXF/DWG path (defines or redefines block `name`, default the file name, from its model space; base point $INSBASE), at: [x,y], scale?: number | [x, y], rotation? (degrees), basePoint?: [x,y] (block coordinates), attribs?: {TAG: value}, explode?: bool}")
.interactive(|_| Ok(Box::new(InsertM::default()))),
CommandSpec::new("attdef", "Define Attributes...", run_attdef)
.menu(&["Draw", "Block", "Define Attributes..."])
.alias(&["att", "-attdef"])
.params("{tag, prompt?, default?, at, height?, invisible?}")
.interactive(|_| Ok(Box::new(AttdefM::default()))),
attedit::attedit_spec(),
attedit::dash_attedit_spec(),
CommandSpec::new("wblock", "Write Block", run_wblock).alias(&["w"]).params("{path, name? | handles?, base?}"),
CommandSpec::new("base", "Base", run_base).menu(&["Draw", "Block", "Base"]).params("{at: [x,y]}"),
CommandSpec::new("purge", "Purge", purge::run)
.alias(&["pu", "-purge"])
.params("{type?: all|blocks|dimstyles|groups|layers|linetypes|mleaderstyles|tablestyles|textstyles|zerolength|emptytext (default all: every named type), names?: \"A*,B\" (default *)} → purged")
.interactive(|_| Ok(Box::new(purge::PurgeM::default()))),
CommandSpec::new("blocks.list", "List Blocks", run_list).enabled(has_doc).noundo(),
CommandSpec::new("battman", "Block Attribute Manager...", run_battman)
.menu(&["Modify", "Object", "Attribute", "Block Attribute Manager..."])
.params("{name}")
.noundo(),
attribs::attdisp_spec(),
attribs::attsync_spec(),
]
}
fn valid_block_name(n: &str) -> bool {
!n.trim().is_empty() && n.len() <= 255 && !n.starts_with('*') && !n.chars().any(|c| "<>/\\\":;?*|,=`".contains(c))
}
/// The block an entity references, when it is a block reference.
fn referenced_block(e: &Entity) -> Option<String> {
if let EntityKind::Insert(i) = &e.kind { Some(i.block.clone()) } else { None }
}
/// Whether any of `ents` refers to block `name`, directly or through nested block references.
fn references_block(d: &cadcraft_doc::Drawing, ents: &[Entity], name: &str) -> bool {
let mut todo: Vec<String> = ents.iter().filter_map(referenced_block).collect();
let mut seen = std::collections::HashSet::new();
while let Some(b) = todo.pop() {
if b.eq_ignore_ascii_case(name) {
return true;
}
if seen.insert(b.to_ascii_uppercase())
&& let Some(blk) = d.block(&b)
{
todo.extend(blk.entities.iter().filter_map(|e| referenced_block(e)));
}
}
false
}
/// Create a block from entities. Returns the insert handle when converting.
pub(crate) fn make_block(s: &mut Session, name: &str, base: Vec2, hs: &[Handle], keep: &str, description: &str) -> Result<Option<Handle>> {
if !valid_block_name(name) {
return Err(bad("block", "invalid block name"));
}
let d = s.doc()?;
let ents: Vec<Entity> = hs.iter().filter_map(|h| d.entity(*h).map(|e| (**e).clone())).collect();
if ents.is_empty() {
return Err(bad("block", "no objects selected"));
}
if references_block(d, &ents, name) {
return Err(bad("block", format!("Block \"{name}\" references itself.")));
}
// Block names are case-insensitive: redefining keeps the existing definition's name.
let name = d.blocks.keys().find(|k| k.eq_ignore_ascii_case(name)).cloned().unwrap_or_else(|| name.to_string());
let name = name.as_str();
let mut b = Block::new(name);
b.base = v3(base);
b.description = description.to_string();
let doc = s.doc_mut()?;
for mut e in ents {
e.handle = doc.new_handle();
b.entities.push(e);
}
doc.blocks.insert(name.to_string(), Arc::new(b));
if keep != "retain" {
for h in hs {
doc.remove_entity(*h);
}
}
s.set_selection(Vec::new());
if keep == "convert" {
let h = insert(s, name, base, 1.0, 0.0, &serde_json::Map::new())?;
return Ok(Some(h));
}
Ok(None)
}
/// Insert a block reference, filling attributes from `values` (else their defaults).
pub(crate) fn insert(s: &mut Session, name: &str, at: Vec2, scale: f64, rotation: f64, values: &serde_json::Map<String, Value>) -> Result<Handle> {
insert_with(s, name, at, Vec2::new(scale, scale), rotation, None, values)
}
/// The block reference INSERT places: X/Y scale factors, and an optional base point (in block
/// coordinates) that lands on `at` instead of the block's own base point.
fn insert_entity(blk: &Block, at: Vec2, scale: Vec2, rotation: f64, base: Option<Vec2>) -> Insert {
let mut ins = Insert {
block: blk.name.clone(),
insert: v3(at),
scale: Vec3::new(scale.x, scale.y, scale.x),
rotation,
attribs: Vec::new(),
cols: 1,
rows: 1,
col_spacing: 0.0,
row_spacing: 0.0,
};
if let Some(b) = base {
let landed = ins.transform(blk.base.xy()).apply(b);
ins.insert = v3(at + (at - landed));
}
ins
}
/// [`insert`] with separate X/Y scale factors and an optional base point (see [`insert_entity`]).
pub(crate) fn insert_with(
s: &mut Session,
name: &str,
at: Vec2,
scale: Vec2,
rotation: f64,
base: Option<Vec2>,
values: &serde_json::Map<String, Value>,
) -> Result<Handle> {
let blk = s.doc()?.block(name).cloned().ok_or_else(|| EngineError::Other(format!("Block \"{name}\" not found.")))?;
let mut ins = insert_entity(&blk, at, scale, rotation, base);
let m = ins.transform(blk.base.xy());
for e in blk.entities.iter() {
if let EntityKind::AttDef(ad) = &e.kind {
if ad.constant {
continue;
}
let mut t = ad.text.clone();
let mut k = EntityKind::Text(t.clone());
k.transform(&m);
if let EntityKind::Text(tt) = k {
t = tt;
}
t.value = attedit::value_for(values, &ad.tag).unwrap_or_else(|| ad.text.value.clone());
ins.attribs.push(Attrib { tag: ad.tag.clone(), text: t, invisible: ad.invisible, constant: false, prompt: String::new() });
}
}
s.add_entity(EntityKind::Insert(ins))
}
fn run_block(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "name").ok_or_else(|| bad("block", "`name` is required"))?.to_string();
let base = point_param(p, "base").unwrap_or(Vec2::ZERO);
let hs = targets(s, p)?;
let keep = str_param(p, "keep").unwrap_or("convert").to_string();
let r = make_block(s, &name, base, &hs, &keep, str_param(p, "description").unwrap_or(""))?;
Ok(json!({ "block": name, "insert": r.map(|h| h.hex()) }))
}
fn run_insert(s: &mut Session, p: &Value) -> Result<Value> {
let p = &insert_file::json_params(s, p)?;
let name = str_param(p, "name").ok_or_else(|| bad("insert", "`name` is required"))?.to_string();
let at = point_req("insert", p, "at")?;
// `scale`: one factor, or [x, y] for different X and Y scale factors.
let scale = match p.get("scale") {
Some(v) if v.is_array() => point_value(v).ok_or_else(|| bad("insert", "`scale` must be a number or [x, y]"))?,
_ => {
let f = f64_or(p, "scale", 1.0);
Vec2::new(f, f)
}
};
if scale.x == 0.0 || scale.y == 0.0 {
return Err(bad("insert", "scale cannot be 0"));
}
let rot = f64_or(p, "rotation", 0.0).to_radians();
let vals = p.get("attribs").and_then(Value::as_object).cloned().unwrap_or_default();
let h = insert_with(s, &name, at, scale, rot, point_param(p, "basePoint"), &vals)?;
if bool_or(p, "explode", false) {
return s.execute("explode", &json!({ "handles": [h.hex()] }));
}
Ok(attedit::with_unknown_tags(s, h, &vals, json!({ "handle": h.hex() })))
}
fn attdef_kind(s: &Session, tag: &str, prompt: &str, default: &str, at: Vec2, height: f64, invisible: bool) -> EntityKind {
let style = s.doc().map(|d| d.header.str("TEXTSTYLE", "Standard")).unwrap_or_else(|_| "Standard".into());
EntityKind::AttDef(Attrib {
tag: tag.to_ascii_uppercase(),
text: Text {
insert: v3(at),
align_pt: None,
height,
value: default.into(),
rotation: 0.0,
width_factor: 1.0,
oblique: 0.0,
style,
halign: HAlign::Left,
valign: VAlign::Baseline,
},
invisible,
constant: false,
prompt: prompt.into(),
})
}
fn run_attdef(s: &mut Session, p: &Value) -> Result<Value> {
let tag = str_param(p, "tag").ok_or_else(|| bad("attdef", "`tag` is required"))?;
if tag.contains(' ') || tag.is_empty() {
return Err(bad("attdef", "tag must be non-empty without spaces"));
}
let at = point_req("attdef", p, "at")?;
let h = f64_or(p, "height", s.doc()?.header.f64("TEXTSIZE", 0.2));
let k = attdef_kind(s, tag, str_param(p, "prompt").unwrap_or(tag), str_param(p, "default").unwrap_or(""), at, h, bool_or(p, "invisible", false));
Ok(json!({ "handle": s.add_entity(k)?.hex() }))
}
fn run_wblock(s: &mut Session, p: &Value) -> Result<Value> {
let path = str_param(p, "path").ok_or_else(|| bad("wblock", "`path` is required"))?.to_string();
let src = s.doc()?;
let mut d = cadcraft_doc::Drawing::new_imperial();
d.layers = src.layers.clone();
d.linetypes = src.linetypes.clone();
d.text_styles = src.text_styles.clone();
d.dim_styles = src.dim_styles.clone();
d.blocks = src.blocks.clone();
// The base point becomes the written drawing's origin, its insertion base: by default 0,0 for
// objects and the block's base point for a block (`base` is then in block coordinates).
let base = point_param(p, "base");
d.header.set("INSBASE", cadcraft_doc::HVal::Point(Vec3::ZERO));
let m = Mat3::translate(-base.unwrap_or(Vec2::ZERO));
let ents: Vec<Entity> = if let Some(name) = str_param(p, "name") {
let b = src.block(name).ok_or_else(|| bad("wblock", format!("no block `{name}`")))?;
let m = Mat3::translate(-base.unwrap_or(b.base.xy()));
b.entities
.iter()
.map(|e| {
let mut e = (**e).clone();
e.kind.transform(&m);
e
})
.collect()
} else {
targets(s, p)?
.iter()
.filter_map(|h| src.entity(*h))
.map(|e| {
let mut e = (**e).clone();
e.kind.transform(&m);
e
})
.collect()
};
for mut e in ents {
e.handle = d.new_handle();
d.model.push(e);
}
let hooks = super::file::io().ok_or_else(|| bad("wblock", "file formats are not available in this build"))?;
let bytes = (hooks.write)(&d, &path).map_err(|e| bad("wblock", e))?;
#[cfg(not(target_arch = "wasm32"))]
std::fs::write(&path, &bytes).map_err(|e| bad("wblock", format!("{path}: {e}")))?;
Ok(json!({ "path": path, "entities": d.model.len(), "bytes": bytes.len() }))
}
fn run_base(s: &mut Session, p: &Value) -> Result<Value> {
let at = point_req("base", p, "at")?;
s.doc_mut()?.header.set("INSBASE", cadcraft_doc::HVal::Point(v3(at)));
ok()
}
fn run_list(s: &mut Session, _p: &Value) -> Result<Value> {
let d = s.doc()?;
Ok(
json!({ "blocks": d.blocks.values().filter(|b| !b.anonymous).map(|b| json!({"name": b.name, "entities": b.entities.len(), "base": [b.base.x, b.base.y], "description": b.description})).collect::<Vec<_>>() }),
)
}
fn run_battman(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "name").ok_or_else(|| bad("battman", "`name` is required"))?;
let b = s.doc()?.block(name).cloned().ok_or_else(|| bad("battman", "no such block"))?;
let atts: Vec<Value> = b
.entities
.iter()
.filter_map(|e| {
if let EntityKind::AttDef(a) = &e.kind {
Some(json!({"tag": a.tag, "prompt": a.prompt, "default": a.text.value, "invisible": a.invisible}))
} else {
None
}
})
.collect();
Ok(json!({ "block": name, "attributes": atts }))
}
// ---------------- interactive ----------------
/// -BLOCK's JSON form: BLOCK with the selection erased unless `keep` says otherwise.
fn run_dash_block(s: &mut Session, p: &Value) -> Result<Value> {
let mut p = p.clone();
if let Some(o) = p.as_object_mut() {
o.entry("keep").or_insert_with(|| json!("delete"));
}
run_block(s, &p)
}
/// BLOCK / -BLOCK at the command line. BLOCK converts the selection to a reference to the new
/// block (the dialog's default); -BLOCK erases it.
#[derive(Default)]
struct BlockM {
/// -BLOCK: erase the selection.
dash: bool,
/// A name already in use, waiting for the answer to "Redefine it?".
existing: Option<String>,
name: Option<String>,
base: Option<Vec2>,
sel: SelectPhase,
}
impl Interactive for BlockM {
fn name(&self) -> &'static str {
"BLOCK"
}
fn prompt(&self, _s: &Session) -> Prompt {
match (&self.name, self.base) {
(None, _) => match &self.existing {
Some(n) => Prompt::new(format!("Block \"{n}\" already exists. Redefine it?"), super::curves::KW).kw(&["Yes", "No"]).default("N"),
None => Prompt::new("Enter block name", Accept::TEXT).kw(&["?"]),
},
(Some(_), None) => Prompt::new("Specify insertion base point", Accept::POINT).kw(&["Annotative"]),
_ => self.sel.prompt(),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
match (&self.name, self.base) {
(None, _) if self.existing.is_some() => {
// Yes redefines the block; No (the default) asks for another name.
let yes = matches!(&i, Input::Keyword(k) | Input::Text(k) if k.trim().to_ascii_lowercase().starts_with('y'));
self.name = self.existing.take().filter(|_| yes);
Ok(Step::Continue)
}
(None, _) => {
if let Input::Text(t) = i {
let n = t.trim().to_string();
if n == "?" {
let names: Vec<String> = s.doc()?.blocks.values().filter(|b| !b.anonymous).map(|b| b.name.clone()).collect();
s.echo(format!("Defined blocks: {}", if names.is_empty() { "(none)".into() } else { names.join(", ") }));
} else if !valid_block_name(&n) {
s.echo("Invalid block name.");
} else if s.doc()?.block(&n).is_some() {
self.existing = Some(n);
} else {
self.name = Some(n);
}
}
Ok(Step::Continue)
}
(Some(_), None) => {
if let Input::Point(p) = i {
self.base = Some(p);
self.sel = SelectPhase::begin(s);
if self.sel.done {
let picked = self.sel.picked.clone();
return self.finish(s, &picked);
}
}
Ok(Step::Continue)
}
_ => match self.sel.feed(s, &i)? {
SelOutcome::More => Ok(Step::Continue),
SelOutcome::Empty => Ok(Step::Cancel),
SelOutcome::Done(hs) => self.finish(s, &hs),
},
}
}
}
impl BlockM {
fn finish(&mut self, s: &mut Session, hs: &[Handle]) -> Result<Step> {
let (Some(name), Some(base)) = (self.name.clone(), self.base) else { return Ok(Step::Cancel) };
// A definition that can't be made (e.g. one that would reference itself) ends the command.
let keep = if self.dash { "delete" } else { "convert" };
if let Err(e) = make_block(s, &name, base, hs, keep, "") {
s.echo(e.to_string());
return Ok(Step::Done);
}
s.echo(format!("Block \"{name}\" defined with {} object(s).", hs.len()));
Ok(Step::Done)
}
}
mod insert_file;
/// Where the command-line INSERT is in its prompt sequence.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
enum InsStage {
#[default]
Name,
/// A file whose block already exists: redefine it from the file?
Redefine,
Point,
/// Basepoint: a new base point, picked on the block shown at its definition position.
BasePoint,
/// Scale preset before the insertion point.
PresetScale,
/// Rotate preset before the insertion point.
PresetRotation,
XScale,
Corner,
YScale,
Rotation,
Attribs,
}
#[derive(Default)]
struct InsertM {
stage: InsStage,
name: Option<String>,
at: Option<Vec2>,
base: Option<Vec2>,
x_scale: Option<f64>,
scale: Option<Vec2>,
rotation: Option<f64>,
attdefs: Vec<(String, String, String)>,
values: serde_json::Map<String, Value>,
/// `*name`: insert the block's objects (one uniform scale factor, no attribute values).
explode: bool,
/// The file waiting for the Redefine answer.
file: Option<insert_file::FileRef>,
}
impl InsertM {
/// After the insertion point or a scale/rotation answer: the next prompt still needed.
fn advance(&mut self, s: &mut Session) -> Result<Step> {
self.stage = if self.scale.is_none() {
if self.explode { InsStage::PresetScale } else { InsStage::XScale }
} else if self.rotation.is_none() {
InsStage::Rotation
} else {
InsStage::Attribs
};
if self.stage == InsStage::Attribs
&& self.values.len() >= self.attdefs.len()
&& let (Some(n), Some(a), Some(sc), Some(r)) = (self.name.clone(), self.at, self.scale, self.rotation)
{
let h = insert_with(s, &n, a, sc, r, self.base, &self.values)?;
if self.explode {
insert_file::explode_insert(s, h)?;
}
return Ok(Step::Done);
}
Ok(Step::Continue)
}
/// Block `name` chosen: collect its attribute prompts and ask for the insertion point.
fn choose(&mut self, s: &mut Session, name: &str) -> Result<()> {
let Some(b) = s.doc()?.block(name).cloned() else {
s.echo(format!("Block \"{name}\" not found."));
return Ok(());
};
self.attdefs = if self.explode {
Vec::new()
} else {
b.entities
.iter()
.filter_map(|e| {
if let EntityKind::AttDef(a) = &e.kind {
(!a.constant).then(|| (a.tag.clone(), a.prompt.clone(), a.text.value.clone()))
} else {
None
}
})
.collect()
};
s.doc_mut()?.header.set_str("INSNAME", &b.name);
self.name = Some(b.name.clone());
self.stage = InsStage::Point;
Ok(())
}
/// Define the block from a file, then choose it; a file that can't be read is reported.
fn choose_file(&mut self, s: &mut Session, f: &insert_file::FileRef) -> Result<()> {
match f.define(s) {
Ok(name) => self.choose(s, &name),
Err(e) => {
s.echo(e.to_string());
Ok(())
}
}
}
/// Scale factors from an opposite corner: the X and Y distances from the insertion point.
fn corner(a: Vec2, p: Vec2) -> Result<Vec2> {
let d = p - a;
if d.x.abs() < 1e-12 || d.y.abs() < 1e-12 {
return Err(EngineError::Other("The corner must not be level with the insertion point.".into()));
}
Ok(d)
}
}
fn nonzero(t: &str) -> Result<f64> {
number(t).filter(|v| *v != 0.0).ok_or_else(|| EngineError::Other("Requires a non-zero number.".into()))
}
impl Interactive for InsertM {
fn name(&self) -> &'static str {
"INSERT"
}
fn prompt(&self, s: &Session) -> Prompt {
let at = self.at.unwrap_or_default();
match self.stage {
InsStage::Name => {
let last = s.doc().map(|d| d.header.str("INSNAME", "")).unwrap_or_default();
let p = Prompt::new("Enter block name", Accept::TEXT).kw(&["?"]);
if last.is_empty() { p } else { p.default(last) }
}
InsStage::Redefine => {
let name = self.file.as_ref().map(|f| f.name.clone()).unwrap_or_default();
Prompt::new(format!("Block \"{name}\" already exists. Redefine it?"), super::curves::KW).kw(&["Yes", "No"]).default("N")
}
InsStage::Point => Prompt::new("Specify insertion point", Accept::POINT).kw(&["Basepoint", "Scale", "Rotate"]),
InsStage::BasePoint => Prompt::new("Specify base point", Accept::POINT),
InsStage::PresetScale => Prompt::new("Specify scale factor for XYZ axes", Accept::NUMBER).default("1"),
InsStage::PresetRotation => Prompt::new("Specify rotation angle", Accept::NUMBER).default("0"),
InsStage::XScale => {
Prompt::new("Enter X scale factor, specify opposite corner", Accept::POINT_OR_NUMBER).kw(&["Corner"]).default("1").base(at)
}
InsStage::Corner => Prompt::new("Specify opposite corner", Accept::POINT).base(at),
InsStage::YScale => Prompt::new("Enter Y scale factor", Accept::NUMBER).default("use X scale factor"),
InsStage::Rotation => Prompt::new("Specify rotation angle", Accept::POINT_OR_NUMBER).default("0").base(at),
InsStage::Attribs => {
let (_, prompt, default) = self.attdefs.get(self.values.len()).cloned().unwrap_or_default();
Prompt::new(if prompt.is_empty() { "Enter attribute value".into() } else { prompt }, Accept::TEXT).default(default)
}
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
match self.stage {
InsStage::Name => {
let n = match i {
Input::Text(t) => t.trim().to_string(),
Input::Enter => s.doc()?.header.str("INSNAME", ""),
_ => return Ok(Step::Continue),
};
if n == "?" {
let names: Vec<String> = s.doc()?.blocks.values().filter(|b| !b.anonymous).map(|b| b.name.clone()).collect();
s.echo(format!("Defined blocks: {}", names.join(", ")));
return Ok(Step::Continue);
}
// `*name`: insert exploded.
self.explode = n.starts_with('*');
let n = n.strip_prefix('*').map_or(n.as_str(), str::trim).to_string();
match insert_file::file_ref(s, &n) {
Some(f) if f.ask_redefine(s) => {
self.file = Some(f);
self.stage = InsStage::Redefine;
}
Some(f) => self.choose_file(s, &f)?,
None => self.choose(s, &n)?,
}
}
InsStage::Redefine => {
// Yes defines the block from the file; No (the default) inserts the existing block.
let Some(f) = self.file.take() else { return Ok(Step::Continue) };
let yes = matches!(&i, Input::Keyword(k) | Input::Text(k) if k.trim().to_ascii_lowercase().starts_with('y'));
if yes {
self.choose_file(s, &f)?;
} else {
self.choose(s, &f.name)?;
}
}
InsStage::Point => match i {
Input::Point(p) => {
self.at = Some(p);
return self.advance(s);
}
Input::Keyword(k) if k == "Basepoint" => self.stage = InsStage::BasePoint,
Input::Keyword(k) if k == "Scale" => self.stage = InsStage::PresetScale,
Input::Keyword(k) if k == "Rotate" => self.stage = InsStage::PresetRotation,
_ => {}
},
InsStage::BasePoint => {
if let Input::Point(p) = i {
self.base = Some(p);
self.stage = InsStage::Point;
}
}
InsStage::PresetScale => {
let f = match i {
Input::Text(t) => nonzero(&t)?,
Input::Enter => 1.0,
_ => return Ok(Step::Continue),
};
self.scale = Some(Vec2::new(f, f));
// Exploded insertion asks its one scale factor after the insertion point.
if self.at.is_some() {
return self.advance(s);
}
self.stage = InsStage::Point;
}
InsStage::PresetRotation => {
self.rotation = Some(match i {
Input::Text(t) => s.angle_settings().direction(&t).ok_or_else(|| EngineError::Other("Requires an angle.".into()))?,
Input::Enter => 0.0,
_ => return Ok(Step::Continue),
});
self.stage = InsStage::Point;
}
InsStage::XScale => match i {
Input::Keyword(k) if k == "Corner" => self.stage = InsStage::Corner,
Input::Point(p) => {
self.scale = Some(Self::corner(self.at.unwrap_or_default(), p)?);
return self.advance(s);
}
Input::Text(t) => {
self.x_scale = Some(nonzero(&t)?);
self.stage = InsStage::YScale;
}
Input::Enter => {
self.x_scale = Some(1.0);
self.stage = InsStage::YScale;
}
_ => {}
},
InsStage::Corner => {
if let Input::Point(p) = i {
self.scale = Some(Self::corner(self.at.unwrap_or_default(), p)?);
return self.advance(s);
}
}
InsStage::YScale => {
let x = self.x_scale.unwrap_or(1.0);
let y = match i {
Input::Text(t) => nonzero(&t)?,
Input::Enter => x,
_ => return Ok(Step::Continue),
};
self.scale = Some(Vec2::new(x, y));
return self.advance(s);
}
InsStage::Rotation => {
let a = self.at.unwrap_or_default();
self.rotation = Some(match i {
Input::Point(p) => a.angle_to(p),
Input::Text(t) => s.angle_settings().direction(&t).ok_or_else(|| EngineError::Other("Requires an angle.".into()))?,
_ => 0.0,
});
return self.advance(s);
}
InsStage::Attribs => {
let (tag, _, default) = self.attdefs.get(self.values.len()).cloned().unwrap_or_default();
let v = match i {
Input::Text(t) => t,
_ => default,
};
self.values.insert(tag, Value::String(v));
return self.advance(s);
}
}
Ok(Step::Continue)
}
fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
let (Some(n), Ok(d)) = (&self.name, s.doc()) else { return Vec::new() };
let Some(b) = d.block(n) else { return Vec::new() };
// Picking a new base point: the block sits at its definition position.
if self.stage == InsStage::BasePoint {
return vec![EntityKind::Insert(insert_entity(b, b.base.xy(), Vec2::new(1.0, 1.0), 0.0, None))];
}
let at = self.at.unwrap_or(c);
let scale = self.scale.unwrap_or_else(|| match (self.stage, self.x_scale) {
(InsStage::XScale | InsStage::Corner, _) => Self::corner(at, c).unwrap_or(Vec2::new(1.0, 1.0)),
(_, Some(x)) => Vec2::new(x, x),
_ => Vec2::new(1.0, 1.0),
});
let rot = self.rotation.unwrap_or(if self.stage == InsStage::Rotation { at.angle_to(c) } else { 0.0 });
vec![EntityKind::Insert(insert_entity(b, at, scale, rot, self.base))]
}
}
#[derive(Default)]
struct AttdefM {
tag: Option<String>,
prompt: Option<String>,
default: Option<String>,
}
impl Interactive for AttdefM {
fn name(&self) -> &'static str {
"ATTDEF"
}
fn prompt(&self, _s: &Session) -> Prompt {
match (&self.tag, &self.prompt, &self.default) {
(None, ..) => Prompt::new("Enter attribute tag name", Accept::TEXT),
(Some(_), None, _) => Prompt::new("Enter attribute prompt", Accept::TEXT),
(Some(_), Some(_), None) => Prompt::new("Enter default attribute value", Accept::TEXT),
_ => Prompt::new("Specify start point of text", Accept::POINT),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
let text = match &i {
Input::Text(t) => Some(t.clone()),
Input::Enter => Some(String::new()),
_ => None,
};
match (&self.tag, &self.prompt, &self.default) {
(None, ..) => {
if let Some(t) = text.filter(|t| !t.trim().is_empty() && !t.contains(' ')) {
self.tag = Some(t.to_ascii_uppercase());
}
}
(Some(_), None, _) => self.prompt = text,
(Some(_), Some(_), None) => self.default = text,
(Some(tag), Some(pr), Some(def)) => {
if let Input::Point(p) = i {
let h = s.doc()?.header.f64("TEXTSIZE", 0.2);
let k = attdef_kind(s, tag, pr, def, p, h, false);
s.add_entity(k)?;
return Ok(Step::Done);
}
}
}
Ok(Step::Continue)
}
}
mod attribs;