mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-10 12:30:16 -06:00
1106 lines
48 KiB
Rust
1106 lines
48 KiB
Rust
//! Blocks: BLOCK, INSERT, ATTDEF, ATTEDIT (programmatic), WBLOCK, BASE, PURGE, block listing, the
|
|
//! Block Editor.
|
|
|
|
pub(crate) mod bedit;
|
|
pub(in crate::cmd) mod purge;
|
|
|
|
use std::sync::Arc;
|
|
|
|
use cadcraft_doc::{Attrib, Block, Entity, EntityKind, HAlign, Handle, Insert, Text, VAlign};
|
|
use cadcraft_geom::{Vec2, Vec3};
|
|
use serde_json::{Value, json};
|
|
|
|
use super::helpers::v3;
|
|
use super::machines::{SelOutcome, SelectPhase, number};
|
|
use super::{CommandSpec, bad, bool_or, f64_or, find_command, ok, point_param, point_req, point_value, str_param, targets};
|
|
|
|
use super::has_doc;
|
|
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
|
|
|
|
mod attdef;
|
|
mod attedit;
|
|
mod burst;
|
|
mod description;
|
|
mod dyngrip;
|
|
mod extract;
|
|
pub(crate) mod refedit;
|
|
mod wblock;
|
|
mod xclip;
|
|
|
|
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? (up to 255 characters, one line), annotative?}")
|
|
.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? (up to 255 characters, one line)}")
|
|
.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("minsert", "Multiple Insert", run_minsert)
|
|
.params("{name, file?: DXF/DWG path (as INSERT), at: [x,y], scale?: number | [x, y], rotation? (degrees), basePoint?: [x,y], attribs?: {TAG: value}, rows?: 1..32767 (default 1), columns?: 1..32767 (default 1), rowSpacing?, columnSpacing? (along the reference's own axes, not scaled with it)}")
|
|
.interactive(|_| Ok(Box::new(InsertM { minsert: true, ..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(attdef::AttdefM::default()))),
|
|
attedit::attedit_spec(),
|
|
attedit::dash_attedit_spec(),
|
|
wblock::wblock_spec(),
|
|
wblock::dash_wblock_spec(),
|
|
CommandSpec::new("base", "Base", run_base).menu(&["Draw", "Block", "Base"]).params("{at: [x,y]}"),
|
|
CommandSpec::new("blocks.dynamic", "Dynamic Block Data", run_dynamic)
|
|
.params("{name (a block, or \"*Model_Space\" for a drawing that is a dynamic block's own)} → {parameters, actions, other} | null (not a dynamic block)")
|
|
.enabled(has_doc)
|
|
.noundo(),
|
|
CommandSpec::new("blocks.reference", "Dynamic Block Reference", run_reference)
|
|
.params("{handle (a block reference)} → {definition, values (by parameter graph id), properties: [{name, value, choices}]} | null (not a dynamic block reference)")
|
|
.enabled(has_doc)
|
|
.noundo(),
|
|
CommandSpec::new("blocks.reference.set", "Set Dynamic Property", run_reference_set)
|
|
.params("{handle (a dynamic block reference), name (a custom property as Properties names it: a visibility state, a lookup, a distance or \"Position1 X\"), value (lengths in drawing units)} → {properties}")
|
|
.enabled(has_doc),
|
|
CommandSpec::new("blocks.reference.grips", "Dynamic Block Grips", dyngrip::run_grips)
|
|
.params("{handle} → [{node, part: base|end|at, shape: linear|point|list, at: [x,y], dir: [x,y], property, choices?}] (empty: not a dynamic block reference)")
|
|
.enabled(has_doc)
|
|
.noundo(),
|
|
CommandSpec::new("blocks.reference.grip", "Drag Dynamic Grip", dyngrip::run_grip)
|
|
.params("{handle, node (the parameter's graph id), part: base|end|at, to: [x,y] (drawing coordinates)} → {properties}: a distance's arrow changes the distance (snapped to its values), a position's square moves it")
|
|
.enabled(has_doc),
|
|
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(),
|
|
battman::battman_spec(),
|
|
attribs::attdisp_spec(),
|
|
attribs::attsync_spec(),
|
|
burst::burst_spec(),
|
|
blockreplace::blockreplace_spec(),
|
|
extract::dataextraction_spec(),
|
|
extract::eattext_spec(),
|
|
description::description_spec(),
|
|
]
|
|
.into_iter()
|
|
.chain(attribs::attdisp_menu_specs())
|
|
.chain(xref::specs())
|
|
.chain(refedit::specs())
|
|
.chain(xclip::specs())
|
|
.chain(bedit::specs())
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) 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 draws, if any: a block reference's block, a dimension's block or a
|
|
/// multileader's block content.
|
|
fn referenced_block(e: &Entity) -> Option<String> {
|
|
match &e.kind {
|
|
EntityKind::Insert(i) => Some(i.block.clone()),
|
|
EntityKind::Dimension(dm) => dm.block.clone(),
|
|
EntityKind::MLeader(m) => m.block.as_ref().map(|b| b.block.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Whether any of `ents` refers to block `name`, directly or through nested block references,
|
|
/// dimension blocks and multileader block content.
|
|
pub(crate) 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
|
|
}
|
|
|
|
/// What BLOCK does with the selected objects once the block is defined (the dialog's Convert to
|
|
/// block, Retain and Delete).
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(crate) enum Keep {
|
|
/// Replace the objects by a reference to the new block.
|
|
Convert,
|
|
/// Leave the objects in the drawing.
|
|
Retain,
|
|
/// Erase the objects.
|
|
Delete,
|
|
}
|
|
|
|
impl Keep {
|
|
/// The `keep` parameter's documented values, in any case; `None` for anything else.
|
|
pub(crate) fn parse(value: &str) -> Option<Keep> {
|
|
match value.trim().to_ascii_lowercase().as_str() {
|
|
"convert" => Some(Keep::Convert),
|
|
"retain" => Some(Keep::Retain),
|
|
"delete" => Some(Keep::Delete),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The `keep` parameter of `cmd`, `default` when absent. A value that is not documented is an
|
|
/// error (issue #935: a misspelt value used to erase the objects without converting them).
|
|
fn keep_param(cmd: &str, p: &Value, default: Keep) -> Result<Keep> {
|
|
match str_param(p, "keep") {
|
|
None => Ok(default),
|
|
Some(v) => Keep::parse(v).ok_or_else(|| bad(cmd, format!("`keep` must be \"convert\", \"retain\" or \"delete\", not {v:?}"))),
|
|
}
|
|
}
|
|
|
|
/// 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: Keep, 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.")));
|
|
}
|
|
if d.refedit.as_ref().is_some_and(|r| r.block.eq_ignore_ascii_case(name)) {
|
|
return Err(bad("block", format!("Block \"{name}\" is being edited in place; finish with REFCLOSE first.")));
|
|
}
|
|
// 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));
|
|
match keep {
|
|
Keep::Retain => {}
|
|
Keep::Convert => {
|
|
for h in hs {
|
|
doc.remove_entity(*h);
|
|
}
|
|
}
|
|
// As after ERASE, OOPS brings them back.
|
|
Keep::Delete => {
|
|
super::modify::erase_kept(s, hs)?;
|
|
}
|
|
}
|
|
s.set_selection(Vec::new());
|
|
if keep == 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/Z 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: Vec3, rotation: f64, base: Option<Vec2>) -> Insert {
|
|
let mut ins = Insert {
|
|
block: blk.name.clone(),
|
|
insert: v3(at),
|
|
scale,
|
|
rotation,
|
|
attribs: Vec::new(),
|
|
cols: 1,
|
|
rows: 1,
|
|
col_spacing: 0.0,
|
|
row_spacing: 0.0,
|
|
clip: None,
|
|
};
|
|
if let Some(b) = base {
|
|
let landed = ins.transform(blk.base.xy()).apply(b);
|
|
ins.insert = v3(at + (at - landed));
|
|
}
|
|
ins
|
|
}
|
|
|
|
/// MINSERT's rectangular array of a block reference: rows and columns, and the distances between
|
|
/// them along the reference's rotated axes (not scaled with it). One row and one column is a plain
|
|
/// reference.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
struct Grid {
|
|
rows: u32,
|
|
cols: u32,
|
|
row_spacing: f64,
|
|
col_spacing: f64,
|
|
}
|
|
|
|
impl Grid {
|
|
const ONE: Grid = Grid { rows: 1, cols: 1, row_spacing: 0.0, col_spacing: 0.0 };
|
|
/// The most rows or columns MINSERT takes, as AutoCAD 2024 asks for them.
|
|
const MAX: u32 = 32767;
|
|
|
|
/// A distance is kept only along an axis with more than one copy: AutoCAD 2024 asks for no
|
|
/// other and stores 0 for it (one row of three columns: 44 = 20, 45 = 0).
|
|
fn new(rows: u32, cols: u32, row_spacing: f64, col_spacing: f64) -> Grid {
|
|
Grid { rows, cols, row_spacing: if rows > 1 { row_spacing } else { 0.0 }, col_spacing: if cols > 1 { col_spacing } else { 0.0 } }
|
|
}
|
|
|
|
fn apply(self, ins: &mut Insert) {
|
|
(ins.rows, ins.cols, ins.row_spacing, ins.col_spacing) = (self.rows, self.cols, self.row_spacing, self.col_spacing);
|
|
}
|
|
}
|
|
|
|
/// [`insert`] with separate X/Y scale factors (Z as X) 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> {
|
|
insert_scaled(s, name, at, Vec3::new(scale.x, scale.y, scale.x), rotation, base, values, Grid::ONE)
|
|
}
|
|
|
|
/// [`insert_with`] with a Z scale factor of its own, and MINSERT's grid. A grid has the one set
|
|
/// of attributes (AutoCAD 2024 asks for their values once).
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn insert_scaled(
|
|
s: &mut Session,
|
|
name: &str,
|
|
at: Vec2,
|
|
scale: Vec3,
|
|
rotation: f64,
|
|
base: Option<Vec2>,
|
|
values: &serde_json::Map<String, Value>,
|
|
grid: Grid,
|
|
) -> 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);
|
|
grid.apply(&mut ins);
|
|
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(),
|
|
props: Default::default(),
|
|
multiline: ad.multiline.as_ref().map(|ml| ml.transformed(&m)),
|
|
modes: ad.modes.for_reference(),
|
|
});
|
|
}
|
|
}
|
|
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 = keep_param("block", p, Keep::Convert)?;
|
|
let description = str_param(p, "description").unwrap_or("");
|
|
description::check_text("block", description)?;
|
|
let r = make_block(s, &name, base, &hs, keep, description)?;
|
|
// An annotative block: its references (the converted selection too) are annotative.
|
|
if bool_or(p, "annotative", false) {
|
|
let d = s.doc_mut()?;
|
|
let cur = d.current_anno_scale();
|
|
if let Some(b) = d.blocks.iter_mut().find(|(k, _)| k.eq_ignore_ascii_case(&name)).map(|(_, b)| b) {
|
|
Arc::make_mut(b).annotative = true;
|
|
}
|
|
if let Some(h) = r {
|
|
d.modify_entity(h, |e| cadcraft_doc::set_annotative(e, true, &cur))?;
|
|
}
|
|
}
|
|
Ok(json!({ "block": name, "insert": r.map(|h| h.hex()) }))
|
|
}
|
|
|
|
fn run_insert(s: &mut Session, p: &Value) -> Result<Value> {
|
|
insert_json(s, p, "insert", Grid::ONE)
|
|
}
|
|
|
|
/// MINSERT's JSON form: INSERT's parameters and the grid.
|
|
fn run_minsert(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let count = |key: &str| -> Result<u32> {
|
|
match p.get(key) {
|
|
None | Some(Value::Null) => Ok(1),
|
|
Some(v) => v
|
|
.as_f64()
|
|
.filter(|n| n.fract() == 0.0 && *n >= 1.0 && *n <= f64::from(Grid::MAX))
|
|
.map(|n| n as u32)
|
|
.ok_or_else(|| bad("minsert", format!("`{key}` must be an integer between 1 and {}", Grid::MAX))),
|
|
}
|
|
};
|
|
// A distance given must be a number: `"15"` is refused, not taken as 0.
|
|
let spacing = |key: &str| -> Result<f64> {
|
|
match p.get(key) {
|
|
None | Some(Value::Null) => Ok(0.0),
|
|
Some(v) => v.as_f64().filter(|x| x.is_finite()).ok_or_else(|| bad("minsert", format!("`{key}` must be a number"))),
|
|
}
|
|
};
|
|
let grid = Grid::new(count("rows")?, count("columns")?, spacing("rowSpacing")?, spacing("columnSpacing")?);
|
|
insert_json(s, p, "minsert", grid)
|
|
}
|
|
|
|
/// INSERT's and MINSERT's JSON form.
|
|
fn insert_json(s: &mut Session, p: &Value, cmd: &str, grid: Grid) -> Result<Value> {
|
|
let p = &insert_file::json_params(s, p)?;
|
|
let name = str_param(p, "name").ok_or_else(|| bad(cmd, "`name` is required"))?.to_string();
|
|
let at = point_req(cmd, 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(cmd, "`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(cmd, "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_scaled(s, &name, at, Vec3::new(scale.x, scale.y, scale.x), rot, point_param(p, "basePoint"), &vals, grid)?;
|
|
if cmd == "insert" && bool_or(p, "explode", false) {
|
|
// The EXPLODE command body, not `execute`: INSERT records the one undo step.
|
|
let explode = find_command("explode").ok_or_else(|| EngineError::UnknownCommand("explode".into()))?;
|
|
return (explode.run)(s, &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(),
|
|
props: Default::default(),
|
|
multiline: None,
|
|
modes: Default::default(),
|
|
})
|
|
}
|
|
|
|
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_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()?;
|
|
// One object index for the whole listing: rebuilding it per block multiplies the block count
|
|
// by the drawing's kept object count (CWE-407).
|
|
let objects = cadcraft_doc::dynblock::ObjectIndex::new(d);
|
|
let block = |b: &Block| {
|
|
let dynamic = cadcraft_doc::dynblock::is_dynamic_block_in(d, &objects, &b.name);
|
|
json!({"name": b.name, "entities": b.entities.len(), "base": [b.base.x, b.base.y], "description": b.description, "dynamic": dynamic})
|
|
};
|
|
Ok(json!({ "blocks": d.blocks.values().filter(|b| !b.anonymous).map(|b| block(b)).collect::<Vec<_>>() }))
|
|
}
|
|
|
|
/// A dynamic block's parameters and actions as read from the file (null for a block that has
|
|
/// none).
|
|
fn run_dynamic(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let name = str_param(p, "name").ok_or_else(|| bad("blocks.dynamic", "`name` is required"))?;
|
|
let d = s.doc()?;
|
|
if d.block(name).is_none() && !name.trim().eq_ignore_ascii_case("*Model_Space") {
|
|
return Err(bad("blocks.dynamic", format!("no block `{name}`")));
|
|
}
|
|
serde_json::to_value(cadcraft_doc::dynblock::dynamic_block(d, name)).map_err(|e| bad("blocks.dynamic", e.to_string()))
|
|
}
|
|
|
|
/// The dynamic block a block reference shows, the values it was given and its custom properties
|
|
/// (null for any other reference).
|
|
fn run_reference(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let h = super::curves::handle_param(p, "handle").ok_or_else(|| bad("blocks.reference", "`handle` is required"))?;
|
|
let d = s.doc()?;
|
|
match d.entity(h).map(|e| &e.kind) {
|
|
Some(EntityKind::Insert(_)) => {}
|
|
Some(_) => return Err(bad("blocks.reference", "`handle` is not a block reference")),
|
|
None => return Err(bad("blocks.reference", format!("no object {}", h.hex()))),
|
|
}
|
|
let (Some(r), Some((_, properties))) = (cadcraft_doc::dynblock::reference(d, h), cadcraft_doc::dynblock::custom_properties(d, h)) else {
|
|
return Ok(Value::Null);
|
|
};
|
|
Ok(json!({ "definition": r.definition, "values": r.values, "properties": properties }))
|
|
}
|
|
|
|
/// Change a custom property of a dynamic block reference (one undo step).
|
|
fn run_reference_set(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cmd = "blocks.reference.set";
|
|
let h = super::curves::handle_param(p, "handle").ok_or_else(|| bad(cmd, "`handle` is required"))?;
|
|
let name = str_param(p, "name").ok_or_else(|| bad(cmd, "`name` is required"))?;
|
|
let value = match p.get("value") {
|
|
Some(Value::String(v)) => v.clone(),
|
|
Some(Value::Number(n)) => n.to_string(),
|
|
_ => return Err(bad(cmd, "`value` is required")),
|
|
};
|
|
if !matches!(s.doc()?.entity(h).map(|e| &e.kind), Some(EntityKind::Insert(_))) {
|
|
return Err(bad(cmd, "`handle` is not a block reference"));
|
|
}
|
|
if super::curves::is_locked(s, h) {
|
|
return Err(bad(cmd, "the reference is on a locked layer"));
|
|
}
|
|
let mut d = s.doc()?.clone();
|
|
cadcraft_doc::dynblock::set_property(&mut d, h, name, &value).map_err(|e| bad(cmd, e))?;
|
|
let properties = cadcraft_doc::dynblock::custom_properties(&d, h).map(|(_, p)| p).unwrap_or_default();
|
|
*s.doc_mut()? = d;
|
|
Ok(json!({ "properties": properties }))
|
|
}
|
|
|
|
// ---------------- 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 { Keep::Delete } else { Keep::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;
|
|
pub(crate) mod xref;
|
|
|
|
/// 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 dropped where it was (at the cursor).
|
|
BasePoint,
|
|
/// Scale preset before the insertion point.
|
|
PresetScale,
|
|
/// X, Y or Z scale preset (0, 1, 2) before the insertion point: that axis alone.
|
|
PresetAxis(usize),
|
|
/// Rotate preset before the insertion point.
|
|
PresetRotation,
|
|
XScale,
|
|
Corner,
|
|
YScale,
|
|
/// XYZ: the Z scale factor, after X and Y.
|
|
ZScale,
|
|
Rotation,
|
|
/// MINSERT: the number of rows, then of columns.
|
|
Rows,
|
|
Cols,
|
|
/// MINSERT: the distance between rows, or a unit cell's first corner.
|
|
RowSpacing,
|
|
/// MINSERT: the unit cell's opposite corner (its width between columns, its height between
|
|
/// rows).
|
|
Cell(Vec2),
|
|
ColSpacing,
|
|
Attribs,
|
|
/// A Verify attribute asked again.
|
|
Verify,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct InsertM {
|
|
stage: InsStage,
|
|
name: Option<String>,
|
|
at: Option<Vec2>,
|
|
base: Option<Vec2>,
|
|
x_scale: Option<f64>,
|
|
scale: Option<Vec2>,
|
|
/// The Z scale factor when given (a preset or XYZ); otherwise the X factor's size. AutoCAD
|
|
/// keeps a Z factor typed at XYZ's Z prompt as given, and stores the size of any other.
|
|
z_scale: Option<f64>,
|
|
/// X/Y/Z presets given so far.
|
|
axes: [Option<f64>; 3],
|
|
/// XYZ at the X scale prompt: X, Y and Z are asked in turn.
|
|
xyz: bool,
|
|
/// Where the block was dropped to pick a Basepoint on it.
|
|
dropped: Option<Vec2>,
|
|
rotation: Option<f64>,
|
|
/// The attributes asked for: tag, prompt, default value, and Verify (asked again).
|
|
attdefs: Vec<(String, String, String, bool)>,
|
|
values: serde_json::Map<String, Value>,
|
|
/// Verify attributes asked again so far.
|
|
verified: usize,
|
|
/// `*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>,
|
|
/// MINSERT: after the rotation, the grid's rows and columns and the distances between them.
|
|
minsert: bool,
|
|
rows: Option<u32>,
|
|
cols: Option<u32>,
|
|
row_spacing: Option<f64>,
|
|
col_spacing: Option<f64>,
|
|
}
|
|
|
|
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 if let Some(g) = self.grid_stage() {
|
|
g
|
|
} else {
|
|
InsStage::Attribs
|
|
};
|
|
if self.stage == InsStage::Attribs && self.values.is_empty() && !self.attdefs.is_empty() {
|
|
s.echo("Enter attribute values");
|
|
}
|
|
// Then the Verify attributes are asked again, with the values typed.
|
|
if self.stage == InsStage::Attribs && self.values.len() >= self.attdefs.len() && self.verified < self.verifying().len() {
|
|
if self.verified == 0 {
|
|
s.echo("Verify attribute values");
|
|
}
|
|
self.stage = InsStage::Verify;
|
|
return Ok(Step::Continue);
|
|
}
|
|
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_scaled(s, &n, a, self.scale3(sc), r, self.base, &self.values, self.grid())?;
|
|
if self.explode {
|
|
insert_file::explode_insert(s, h)?;
|
|
}
|
|
return Ok(Step::Done);
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
|
|
/// MINSERT's next grid prompt, if any is left (as AutoCAD 2024 asks them): the rows, the
|
|
/// columns, then the distance between rows when there is more than one row and between
|
|
/// columns when there is more than one column. A unit cell gives both distances.
|
|
fn grid_stage(&self) -> Option<InsStage> {
|
|
if !self.minsert {
|
|
return None;
|
|
}
|
|
let (Some(rows), Some(cols)) = (self.rows, self.cols) else {
|
|
return Some(if self.rows.is_none() { InsStage::Rows } else { InsStage::Cols });
|
|
};
|
|
if rows > 1 && self.row_spacing.is_none() {
|
|
Some(InsStage::RowSpacing)
|
|
} else if cols > 1 && self.col_spacing.is_none() {
|
|
Some(InsStage::ColSpacing)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// The grid answered so far (one copy for INSERT).
|
|
fn grid(&self) -> Grid {
|
|
Grid::new(self.rows.unwrap_or(1), self.cols.unwrap_or(1), self.row_spacing.unwrap_or(0.0), self.col_spacing.unwrap_or(0.0))
|
|
}
|
|
|
|
/// The Verify attributes asked for.
|
|
fn verifying(&self) -> Vec<&(String, String, String, bool)> {
|
|
self.attdefs.iter().filter(|a| a.3).collect()
|
|
}
|
|
|
|
/// Block `name` chosen: collect its attribute prompts and ask for the insertion point. Constant
|
|
/// and Preset attributes take their values without asking, and so do all of them with ATTREQ 0.
|
|
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 || !s.settings.attreq {
|
|
Vec::new()
|
|
} else {
|
|
b.entities
|
|
.iter()
|
|
.filter_map(|e| {
|
|
if let EntityKind::AttDef(a) = &e.kind {
|
|
(!a.constant && !a.modes.preset).then(|| (a.tag.clone(), a.prompt.clone(), a.text.value.clone(), a.modes.verify))
|
|
} 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(())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// X/Y scale factors with Z: as given, else the X factor's size (AutoCAD 2024: X -1 with Y
|
|
/// as X stores Z +1).
|
|
/// X and Y from a corner: XYZ still asks Z, which defaults to the size of X.
|
|
fn corner_scale(&mut self, s: &mut Session, sc: Vec2) -> Result<Step> {
|
|
self.scale = Some(sc);
|
|
if self.xyz {
|
|
self.x_scale = Some(sc.x);
|
|
self.stage = InsStage::ZScale;
|
|
return Ok(Step::Continue);
|
|
}
|
|
self.advance(s)
|
|
}
|
|
|
|
fn scale3(&self, sc: Vec2) -> Vec3 {
|
|
Vec3::new(sc.x, sc.y, self.z_scale.unwrap_or(sc.x.abs()))
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
}
|
|
|
|
/// A distance typed at MINSERT's grid prompts: any finite number, negative or 0 too.
|
|
fn distance(t: &str) -> Result<f64> {
|
|
number(t).filter(|v| v.is_finite()).ok_or_else(|| EngineError::Other("Requires a distance.".into()))
|
|
}
|
|
|
|
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 {
|
|
if self.minsert { "MINSERT" } else { "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", "X", "Y", "Z", "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::PresetAxis(i) => Prompt::new(format!("Specify {} scale factor", ["X", "Y", "Z"][i.min(2)]), Accept::NUMBER).default("1"),
|
|
InsStage::PresetRotation => Prompt::new("Specify rotation angle", Accept::NUMBER).default("0"),
|
|
InsStage::XScale if self.xyz => Prompt::new("Specify X scale factor", Accept::POINT_OR_NUMBER).kw(&["Corner"]).default("1").base(at),
|
|
InsStage::XScale => {
|
|
Prompt::new("Enter X scale factor, specify opposite corner", Accept::POINT_OR_NUMBER).kw(&["Corner", "XYZ"]).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::ZScale => Prompt::new("Specify Z scale factor or", Accept::NUMBER).default("use X scale factor"),
|
|
InsStage::Rotation => Prompt::new("Specify rotation angle", Accept::POINT_OR_NUMBER).default("0").base(at),
|
|
InsStage::Rows => Prompt::new("Enter number of rows (---)", Accept::NUMBER).default("1"),
|
|
InsStage::Cols => Prompt::new("Enter number of columns (|||)", Accept::NUMBER).default("1"),
|
|
InsStage::RowSpacing => Prompt::new("Enter distance between rows or specify unit cell (---)", Accept::POINT_OR_NUMBER),
|
|
InsStage::Cell(a) => Prompt::new("Specify opposite corner", Accept::POINT).corner(a),
|
|
InsStage::ColSpacing => Prompt::new("Specify distance between columns (|||)", Accept::NUMBER),
|
|
InsStage::Attribs => {
|
|
let (_, prompt, default, _) = self.attdefs.get(self.values.len()).cloned().unwrap_or_default();
|
|
let p = Prompt::new(if prompt.is_empty() { "Enter attribute value".into() } else { prompt }, Accept::TEXT);
|
|
// No default shown when there is none ("Value:", AutoCAD 2024), not "<>".
|
|
if default.is_empty() { p } else { p.default(default) }
|
|
}
|
|
InsStage::Verify => {
|
|
let (tag, prompt) = self.verifying().get(self.verified).map(|a| (a.0.clone(), a.1.clone())).unwrap_or_default();
|
|
let typed = self.values.get(&tag).and_then(Value::as_str).unwrap_or_default().to_string();
|
|
Prompt::new(if prompt.is_empty() { "Enter attribute value".into() } else { prompt }, Accept::TEXT).default(typed)
|
|
}
|
|
}
|
|
}
|
|
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 (INSERT only: a grid has no exploded form).
|
|
self.explode = !self.minsert && n.starts_with('*');
|
|
let n = if self.explode { n.strip_prefix('*').map_or(n.as_str(), str::trim).to_string() } else { n };
|
|
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" => {
|
|
// AutoCAD drops the block where it is being dragged, at the cursor (the origin
|
|
// in a script: 2024 inserted (15, 5) for base point (612350.678, 2398770.432)
|
|
// and insertion point (612365.678, 2398775.432)).
|
|
self.dropped = Some(s.cursor);
|
|
self.stage = InsStage::BasePoint;
|
|
}
|
|
Input::Keyword(k) if k == "Scale" => self.stage = InsStage::PresetScale,
|
|
Input::Keyword(k) if k == "X" => self.stage = InsStage::PresetAxis(0),
|
|
Input::Keyword(k) if k == "Y" => self.stage = InsStage::PresetAxis(1),
|
|
Input::Keyword(k) if k == "Z" => self.stage = InsStage::PresetAxis(2),
|
|
Input::Keyword(k) if k == "Rotate" => self.stage = InsStage::PresetRotation,
|
|
_ => {}
|
|
},
|
|
InsStage::BasePoint => {
|
|
if let Input::Point(p) = i {
|
|
// From the dropped block to its own coordinates.
|
|
let def = s.doc()?.block(self.name.as_deref().unwrap_or_default()).map_or(Vec2::ZERO, |b| b.base.xy());
|
|
self.base = Some(def + (p - self.dropped.unwrap_or(def)));
|
|
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));
|
|
self.z_scale = Some(f.abs());
|
|
// An axis preset after it changes that axis alone (Scale -3 then X 2: 2/-3/3).
|
|
self.axes = [Some(f), Some(f), Some(f.abs())];
|
|
// Exploded insertion asks its one scale factor after the insertion point.
|
|
if self.at.is_some() {
|
|
return self.advance(s);
|
|
}
|
|
self.stage = InsStage::Point;
|
|
}
|
|
// One axis preset: the others are 1, and no scale factors are asked after the point.
|
|
InsStage::PresetAxis(ax) => {
|
|
let f = match i {
|
|
Input::Text(t) => nonzero(&t)?,
|
|
Input::Enter => 1.0,
|
|
_ => return Ok(Step::Continue),
|
|
};
|
|
if let Some(a) = self.axes.get_mut(ax) {
|
|
*a = Some(f);
|
|
}
|
|
let [x, y, z] = self.axes.map(|a| a.unwrap_or(1.0));
|
|
self.scale = Some(Vec2::new(x, y));
|
|
self.z_scale = Some(z.abs());
|
|
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::Keyword(k) if k == "XYZ" => self.xyz = true,
|
|
Input::Point(p) => {
|
|
let sc = Self::corner(self.at.unwrap_or_default(), p)?;
|
|
return self.corner_scale(s, sc);
|
|
}
|
|
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 {
|
|
let sc = Self::corner(self.at.unwrap_or_default(), p)?;
|
|
return self.corner_scale(s, sc);
|
|
}
|
|
}
|
|
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));
|
|
if self.xyz {
|
|
self.stage = InsStage::ZScale;
|
|
return Ok(Step::Continue);
|
|
}
|
|
return self.advance(s);
|
|
}
|
|
InsStage::ZScale => {
|
|
self.z_scale = Some(match i {
|
|
Input::Text(t) => nonzero(&t)?,
|
|
Input::Enter => self.x_scale.unwrap_or(1.0).abs(),
|
|
_ => return Ok(Step::Continue),
|
|
});
|
|
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::Rows | InsStage::Cols => {
|
|
let n = match i {
|
|
Input::Text(t) => t
|
|
.trim()
|
|
.parse::<f64>()
|
|
.ok()
|
|
.filter(|v| v.fract() == 0.0 && *v >= 1.0 && *v <= f64::from(Grid::MAX))
|
|
.map(|v| v as u32)
|
|
.ok_or_else(|| EngineError::Other(format!("Requires an integer between 1 and {}.", Grid::MAX)))?,
|
|
Input::Enter => 1,
|
|
_ => return Ok(Step::Continue),
|
|
};
|
|
if self.stage == InsStage::Rows {
|
|
self.rows = Some(n);
|
|
} else {
|
|
self.cols = Some(n);
|
|
}
|
|
return self.advance(s);
|
|
}
|
|
InsStage::RowSpacing => match i {
|
|
Input::Point(a) => self.stage = InsStage::Cell(a),
|
|
Input::Text(t) => {
|
|
self.row_spacing = Some(distance(&t)?);
|
|
return self.advance(s);
|
|
}
|
|
_ => {}
|
|
},
|
|
InsStage::Cell(a) => {
|
|
if let Input::Point(b) = i {
|
|
(self.col_spacing, self.row_spacing) = (Some(b.x - a.x), Some(b.y - a.y));
|
|
return self.advance(s);
|
|
}
|
|
}
|
|
InsStage::ColSpacing => {
|
|
if let Input::Text(t) = i {
|
|
self.col_spacing = Some(distance(&t)?);
|
|
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);
|
|
}
|
|
// Enter keeps the value typed.
|
|
InsStage::Verify => {
|
|
let tag = self.verifying().get(self.verified).map(|a| a.0.clone()).unwrap_or_default();
|
|
match i {
|
|
Input::Text(t) => {
|
|
self.values.insert(tag, Value::String(t));
|
|
}
|
|
Input::Enter => {}
|
|
// A click or anything else is no answer: the value is asked for again.
|
|
_ => return Ok(Step::Continue),
|
|
}
|
|
self.verified += 1;
|
|
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, self.dropped.unwrap_or(b.base.xy()), Vec3::new(1.0, 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 });
|
|
let mut ins = insert_entity(b, at, self.scale3(scale), rot, self.base);
|
|
self.grid().apply(&mut ins);
|
|
vec![EntityKind::Insert(ins)]
|
|
}
|
|
}
|
|
|
|
mod attribs;
|
|
mod battman;
|
|
mod blockreplace;
|