mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-15 06:46:45 -04:00
Dynamic blocks: read parameters and actions from AutoCAD files (#838)
* Dynamic blocks: read parameters and actions from AutoCAD files A dynamic block's evaluation graph (kept as foreign data since #528) is read into a typed model, `cadcraft_doc::dynblock`, without changing the kept data: parameters (linear with value sets, point, visibility states, lookup, base point; polar, XY, rotation, alignment and flip by geometry) and actions (stretch with its frame and moved vertices, lookup tables; move, scale, rotate, flip, array, polar stretch by kind), each with its graph id and the parameters it follows. - `blocks.dynamic {name}` returns the model as JSON (handles as hex); `blocks.list` marks dynamic blocks. - Counts and values come from the file: capped by the groups present, non-finite numbers and missing members skipped. - Tests: a synthetic DOOR block (visibility, linear list, stretch, lookup), its survival through a save, damaged data, and the engine commands. Co-Authored-By: Claude Opus 5.5 <[email protected]> * DWG: dynamic block lookup parameters keep their name and lookup action AutoCAD writes a lookup parameter's subclass marker as AcDbBlockLookUpParameter; the DWG bridge reads AcDbBlockLookupParameter and matches it exactly, so the parameter's name (303) and lookup action (94) were lost in DWG files and AutoCAD's lookups stopped working (a visibility change no longer resized the block). The bridge DXF now uses its spelling. Found by driving AutoCAD on CADCraft's output. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Dynamic blocks: a work budget on the record cursor, so counts promising missing groups can't make nested loops rescan a hostile record (with regression test) Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Cody Vogt <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
8 files changed
+1172
-5
No files matched your search
@@ -0,0 +1,510 @@
|
||||
//! Dynamic blocks made in another program, read from the data CADCraft keeps for them.
|
||||
//!
|
||||
//! A dynamic block's behaviour lives in objects CADCraft does not write itself and keeps as
|
||||
//! [`Foreign`](crate::foreign::Foreign) data: the block's BLOCK_RECORD has an extension dictionary
|
||||
//! whose `ACAD_ENHANCEDBLOCK` entry is an evaluation graph (`ACAD_EVALUATION_GRAPH`), and the
|
||||
//! graph lists the block's *parameters* (a distance, a point, visibility states, a lookup…) and
|
||||
//! *actions* (what changing a parameter does to the block's objects: stretch, move, look up…).
|
||||
//! [`dynamic_block`] reads them into the model below without changing the kept data, so a drawing
|
||||
//! saved again still carries them exactly as read.
|
||||
//!
|
||||
//! Every count and index here comes from the file: they are capped by the data actually present
|
||||
//! and unknown or damaged objects are skipped, never trusted.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use cadcraft_geom::{Vec2, Vec3};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::foreign::ForeignObject;
|
||||
use crate::{Drawing, Handle, RawTag};
|
||||
|
||||
/// Most objects one evaluation graph is read with.
|
||||
const MAX_NODES: usize = 10_000;
|
||||
|
||||
/// Handles as hex strings in JSON, as commands report them.
|
||||
fn hex<S: serde::Serializer>(h: &Handle, s: S) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_str(&h.hex())
|
||||
}
|
||||
fn hexes<S: serde::Serializer>(hs: &[Handle], s: S) -> Result<S::Ok, S::Error> {
|
||||
s.collect_seq(hs.iter().map(|h| h.hex()))
|
||||
}
|
||||
fn hex_vertices<S: serde::Serializer>(v: &[(Handle, Vec<i32>)], s: S) -> Result<S::Ok, S::Error> {
|
||||
s.collect_seq(v.iter().map(|(h, i)| (h.hex(), i)))
|
||||
}
|
||||
|
||||
/// A dynamic block's parameters and actions.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DynamicBlock {
|
||||
pub parameters: Vec<Parameter>,
|
||||
pub actions: Vec<Action>,
|
||||
/// Graph members of types not modelled here (grips, constraints…), by DXF type.
|
||||
pub other: Vec<String>,
|
||||
}
|
||||
|
||||
/// One parameter: a property a reference to the block can change.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Parameter {
|
||||
/// Its id in the evaluation graph (actions and lookups refer to parameters by it).
|
||||
pub node: i32,
|
||||
#[serde(serialize_with = "hex")]
|
||||
pub handle: Handle,
|
||||
/// The name the block editor shows ("Linear", "Visibility State").
|
||||
pub element: String,
|
||||
/// The parameter's properties appear in a reference's Properties palette.
|
||||
pub show_properties: bool,
|
||||
pub kind: ParameterKind,
|
||||
}
|
||||
|
||||
/// What a parameter is, with its definition geometry in block coordinates.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase")]
|
||||
pub enum ParameterKind {
|
||||
/// A distance between two points (Distance1).
|
||||
Linear { base: Vec3, end: Vec3, label: String, description: String, values: ValueSet },
|
||||
/// A distance and an angle from a base point.
|
||||
Polar { base: Vec3, end: Vec3 },
|
||||
/// Horizontal and vertical distances from a base point.
|
||||
Xy { base: Vec3, end: Vec3 },
|
||||
/// An angle about a base point.
|
||||
Rotation { base: Vec3, end: Vec3 },
|
||||
/// Aligns the reference with nearby geometry.
|
||||
Alignment { base: Vec3, end: Vec3 },
|
||||
/// Mirrors the objects about a reflection line.
|
||||
Flip { base: Vec3, end: Vec3 },
|
||||
/// A position (Position1).
|
||||
Point { at: Vec3, label: String, description: String },
|
||||
/// Named sets of visible objects.
|
||||
Visibility {
|
||||
at: Vec3,
|
||||
label: String,
|
||||
description: String,
|
||||
#[serde(serialize_with = "hexes")]
|
||||
controlled: Vec<Handle>,
|
||||
states: Vec<VisibilityState>,
|
||||
},
|
||||
/// A named value chosen from a lookup table.
|
||||
Lookup { at: Vec3, label: String, description: String },
|
||||
/// Moves the block's base point.
|
||||
BasePoint { at: Vec3 },
|
||||
/// A parameter type not modelled yet.
|
||||
Other { dxf_type: String },
|
||||
}
|
||||
|
||||
impl ParameterKind {
|
||||
/// The property name a reference shows ("Distance1", "Visibility1"), when it has one.
|
||||
pub fn label(&self) -> Option<&str> {
|
||||
match self {
|
||||
ParameterKind::Linear { label, .. }
|
||||
| ParameterKind::Point { label, .. }
|
||||
| ParameterKind::Visibility { label, .. }
|
||||
| ParameterKind::Lookup { label, .. } => Some(label),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One visibility state: its name and the objects shown in it (the parameter's other controlled
|
||||
/// objects are hidden).
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisibilityState {
|
||||
pub name: String,
|
||||
#[serde(serialize_with = "hexes")]
|
||||
pub visible: Vec<Handle>,
|
||||
/// The parameters and actions active in this state (their objects).
|
||||
#[serde(serialize_with = "hexes")]
|
||||
pub active: Vec<Handle>,
|
||||
}
|
||||
|
||||
/// The values a distance or angle may take.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ValueSet {
|
||||
/// Which of the limits below apply (see the methods).
|
||||
pub flags: i32,
|
||||
pub minimum: f64,
|
||||
pub maximum: f64,
|
||||
pub increment: f64,
|
||||
/// The allowed values, for a list.
|
||||
pub list: Vec<f64>,
|
||||
}
|
||||
|
||||
impl ValueSet {
|
||||
pub fn has_minimum(&self) -> bool {
|
||||
self.flags & 1 != 0
|
||||
}
|
||||
pub fn has_maximum(&self) -> bool {
|
||||
self.flags & 2 != 0
|
||||
}
|
||||
pub fn has_increment(&self) -> bool {
|
||||
self.flags & 4 != 0
|
||||
}
|
||||
pub fn is_list(&self) -> bool {
|
||||
self.flags & 8 != 0
|
||||
}
|
||||
}
|
||||
|
||||
/// One action: what changing parameters does to the block's objects.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Action {
|
||||
pub node: i32,
|
||||
#[serde(serialize_with = "hex")]
|
||||
pub handle: Handle,
|
||||
/// Its name in the block editor ("Stretch", "Lookup1").
|
||||
pub name: String,
|
||||
/// Where the block editor shows it.
|
||||
pub at: Vec3,
|
||||
/// The objects it acts on.
|
||||
#[serde(serialize_with = "hexes")]
|
||||
pub selection: Vec<Handle>,
|
||||
/// Graph ids of the parameters it follows, with the parameter value each connection reads
|
||||
/// ("EndXDelta").
|
||||
pub connections: Vec<(i32, String)>,
|
||||
pub kind: ActionKind,
|
||||
}
|
||||
|
||||
/// What an action does.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase")]
|
||||
pub enum ActionKind {
|
||||
/// Moves the vertices inside `frame` of the objects listed (all of an object not listed).
|
||||
Stretch {
|
||||
frame: Vec<Vec2>,
|
||||
#[serde(serialize_with = "hex_vertices")]
|
||||
vertices: Vec<(Handle, Vec<i32>)>,
|
||||
multiplier: f64,
|
||||
angle_offset: f64,
|
||||
},
|
||||
/// A table: each row sets the listed parameter values together.
|
||||
Lookup {
|
||||
columns: Vec<LookupColumn>,
|
||||
rows: Vec<Vec<String>>,
|
||||
},
|
||||
Move,
|
||||
Scale,
|
||||
Rotate,
|
||||
Flip,
|
||||
Array,
|
||||
PolarStretch,
|
||||
/// An action type not modelled yet.
|
||||
Other {
|
||||
dxf_type: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// A lookup table column: the parameter value it reads or sets.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LookupColumn {
|
||||
/// The parameter's graph id.
|
||||
pub node: i32,
|
||||
/// The value it stands for ("VisibilityState", "lookupString").
|
||||
pub property: String,
|
||||
/// A lookup property (shown on the reference) rather than an input.
|
||||
pub lookup: bool,
|
||||
/// What the reference shows when no row matches ("Custom").
|
||||
pub unmatched: String,
|
||||
}
|
||||
|
||||
/// The groups of an object split at its subclass markers (100): `(marker, groups after it)`.
|
||||
fn sections(tags: &[RawTag]) -> Vec<(&str, &[RawTag])> {
|
||||
let mut out: Vec<(&str, &[RawTag])> = Vec::new();
|
||||
let mut start = 0;
|
||||
let mut name = "";
|
||||
for (i, t) in tags.iter().enumerate() {
|
||||
if t.code == 100 {
|
||||
out.push((name, tags.get(start..i).unwrap_or_default()));
|
||||
name = t.value.as_str();
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
out.push((name, tags.get(start..).unwrap_or_default()));
|
||||
out
|
||||
}
|
||||
|
||||
/// The groups after subclass marker `marker`, empty when the object has none.
|
||||
fn section<'a>(secs: &[(&str, &'a [RawTag])], marker: &str) -> &'a [RawTag] {
|
||||
secs.iter().find(|(n, _)| n.eq_ignore_ascii_case(marker)).map_or(&[], |(_, t)| t)
|
||||
}
|
||||
|
||||
/// Reads groups in order, as an object writes them.
|
||||
struct Cursor<'a> {
|
||||
tags: &'a [RawTag],
|
||||
at: usize,
|
||||
/// Groups scanned so far. A missing group neither moves the cursor nor ends the loops that
|
||||
/// ask for it, so a hostile record could make nested counts rescan it again and again; past
|
||||
/// `budget` every lookup fails at once and every count is zero.
|
||||
work: usize,
|
||||
budget: usize,
|
||||
}
|
||||
|
||||
impl<'a> Cursor<'a> {
|
||||
fn new(tags: &'a [RawTag]) -> Self {
|
||||
Cursor { tags, at: 0, work: 0, budget: tags.len().saturating_mul(16).saturating_add(1024) }
|
||||
}
|
||||
/// The next group with `code` from here on (the cursor moves past it).
|
||||
fn next(&mut self, code: i32) -> Option<&'a str> {
|
||||
if self.work >= self.budget {
|
||||
return None;
|
||||
}
|
||||
let rest = self.tags.get(self.at..)?;
|
||||
let Some(rel) = rest.iter().position(|t| t.code == code) else {
|
||||
self.work = self.work.saturating_add(rest.len().max(1));
|
||||
return None;
|
||||
};
|
||||
self.work = self.work.saturating_add(rel + 1);
|
||||
self.at += rel + 1;
|
||||
self.tags.get(self.at - 1).map(|t| t.value.as_str())
|
||||
}
|
||||
fn string(&mut self, code: i32) -> String {
|
||||
self.next(code).unwrap_or_default().to_string()
|
||||
}
|
||||
fn int(&mut self, code: i32) -> i32 {
|
||||
self.next(code).and_then(|v| v.trim().parse().ok()).unwrap_or(0)
|
||||
}
|
||||
fn real(&mut self, code: i32) -> f64 {
|
||||
self.next(code).and_then(|v| v.trim().parse::<f64>().ok()).filter(|v| v.is_finite()).unwrap_or(0.0)
|
||||
}
|
||||
/// A count read from the file, capped by the groups left (each item needs at least one).
|
||||
fn count(&mut self, code: i32) -> usize {
|
||||
let n = usize::try_from(self.int(code)).unwrap_or(0).min(self.tags.len().saturating_sub(self.at));
|
||||
if self.work >= self.budget { 0 } else { n }
|
||||
}
|
||||
fn handle(&mut self, code: i32) -> Option<Handle> {
|
||||
self.next(code).and_then(Handle::parse_hex)
|
||||
}
|
||||
fn handles(&mut self, code: i32, n: usize) -> Vec<Handle> {
|
||||
(0..n).filter_map(|_| self.handle(code)).collect()
|
||||
}
|
||||
/// A point written with codes `x`, `x + 10`, `x + 20`.
|
||||
fn point(&mut self, x: i32) -> Vec3 {
|
||||
let px = self.real(x);
|
||||
let py = self.real(x + 10);
|
||||
let at = self.at;
|
||||
// Z is optional: only take it when it is the very next group.
|
||||
let pz = match self.tags.get(at) {
|
||||
Some(t) if t.code == x + 20 => self.real(x + 20),
|
||||
_ => 0.0,
|
||||
};
|
||||
Vec3::new(px, py, pz)
|
||||
}
|
||||
}
|
||||
|
||||
/// The objects `xdict`'s `ACAD_ENHANCEDBLOCK` evaluation graph lists, when the dictionary has one.
|
||||
fn graph_members<'a>(objects: &HashMap<Handle, &'a ForeignObject>, xdict: Handle) -> Option<Vec<&'a ForeignObject>> {
|
||||
let dict = objects.get(&xdict)?;
|
||||
let mut graph = None;
|
||||
let mut tags = dict.tags.iter();
|
||||
while let Some(t) = tags.next() {
|
||||
if t.code == 3 && t.value.eq_ignore_ascii_case("ACAD_ENHANCEDBLOCK") {
|
||||
graph = tags.next().filter(|t| t.code == 350 || t.code == 360).and_then(|t| Handle::parse_hex(&t.value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let graph = objects.get(&graph?).filter(|o| o.kind.eq_ignore_ascii_case("ACAD_EVALUATION_GRAPH"))?;
|
||||
let members = section(§ions(&graph.tags), "AcDbEvalGraph").iter().filter(|t| t.code == 360).filter_map(|t| Handle::parse_hex(&t.value));
|
||||
Some(members.take(MAX_NODES).filter_map(|h| objects.get(&h).copied()).collect())
|
||||
}
|
||||
|
||||
/// The dynamic block data of block `name` (any case; `*Model_Space` for a drawing that is a
|
||||
/// block's own drawing), or `None` when it has none.
|
||||
pub fn dynamic_block(d: &Drawing, name: &str) -> Option<DynamicBlock> {
|
||||
let xdict = d.foreign.records.get(&("BLOCK_RECORD".to_string(), name.trim().to_ascii_uppercase()))?.xdict?;
|
||||
let objects: HashMap<Handle, &ForeignObject> = d.foreign.objects.iter().map(|o| (o.handle, o)).collect();
|
||||
let mut out = DynamicBlock::default();
|
||||
for o in graph_members(&objects, xdict)? {
|
||||
let secs = sections(&o.tags);
|
||||
let node = Cursor::new(section(&secs, "AcDbEvalExpr")).int(90);
|
||||
let element = Cursor::new(section(&secs, "AcDbBlockElement")).string(300);
|
||||
let kind = o.kind.to_ascii_uppercase();
|
||||
if kind.ends_with("PARAMETER") {
|
||||
let show_properties = Cursor::new(section(&secs, "AcDbBlockParameter")).int(280) != 0;
|
||||
out.parameters.push(Parameter { node, handle: o.handle, element, show_properties, kind: parameter(&kind, &secs) });
|
||||
} else if kind.ends_with("ACTION") {
|
||||
out.actions.push(action(o.handle, node, element, &kind, &secs));
|
||||
} else {
|
||||
out.other.push(o.kind.clone());
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn parameter(kind: &str, secs: &[(&str, &[RawTag])]) -> ParameterKind {
|
||||
let two = || {
|
||||
let mut c = Cursor::new(section(secs, "AcDbBlock2PtParameter"));
|
||||
(c.point(1010), c.point(1011))
|
||||
};
|
||||
let one = || Cursor::new(section(secs, "AcDbBlock1PtParameter")).point(1010);
|
||||
match kind {
|
||||
"BLOCKLINEARPARAMETER" => {
|
||||
let (base, end) = two();
|
||||
let mut c = Cursor::new(section(secs, "AcDbBlockLinearParameter"));
|
||||
let label = c.string(305);
|
||||
let description = c.string(306);
|
||||
ParameterKind::Linear { base, end, label, description, values: value_set(&mut c) }
|
||||
}
|
||||
"BLOCKPOINTPARAMETER" => {
|
||||
let mut c = Cursor::new(section(secs, "AcDbBlockPointParameter"));
|
||||
ParameterKind::Point { at: one(), label: c.string(303), description: c.string(304) }
|
||||
}
|
||||
"BLOCKVISIBILITYPARAMETER" => visibility(one(), section(secs, "AcDbBlockVisibilityParameter")),
|
||||
"BLOCKLOOKUPPARAMETER" => {
|
||||
let mut c = Cursor::new(section(secs, "AcDbBlockLookupParameter"));
|
||||
ParameterKind::Lookup { at: one(), label: c.string(303), description: c.string(304) }
|
||||
}
|
||||
"BLOCKBASEPOINTPARAMETER" => ParameterKind::BasePoint { at: one() },
|
||||
"BLOCKPOLARPARAMETER" | "BLOCKXYPARAMETER" | "BLOCKROTATIONPARAMETER" | "BLOCKALIGNMENTPARAMETER" | "BLOCKFLIPPARAMETER" => {
|
||||
let (base, end) = two();
|
||||
match kind {
|
||||
"BLOCKPOLARPARAMETER" => ParameterKind::Polar { base, end },
|
||||
"BLOCKXYPARAMETER" => ParameterKind::Xy { base, end },
|
||||
"BLOCKROTATIONPARAMETER" => ParameterKind::Rotation { base, end },
|
||||
"BLOCKALIGNMENTPARAMETER" => ParameterKind::Alignment { base, end },
|
||||
_ => ParameterKind::Flip { base, end },
|
||||
}
|
||||
}
|
||||
other => ParameterKind::Other { dxf_type: other.to_string() },
|
||||
}
|
||||
}
|
||||
|
||||
/// A value set after a parameter's name and description: description (307), flags (96),
|
||||
/// minimum, maximum and increment (141–143), then the listed values (175 count, 144 each).
|
||||
fn value_set(c: &mut Cursor) -> ValueSet {
|
||||
c.next(307);
|
||||
let flags = c.int(96);
|
||||
let (minimum, maximum, increment) = (c.real(141), c.real(142), c.real(143));
|
||||
let n = c.count(175);
|
||||
let list = (0..n).filter_map(|_| c.next(144).and_then(|v| v.trim().parse::<f64>().ok()).filter(|v| v.is_finite())).collect();
|
||||
ValueSet { flags, minimum, maximum, increment, list }
|
||||
}
|
||||
|
||||
/// A visibility parameter: its name and description (301, 302), the objects it controls (93
|
||||
/// count, 331 each), then each state (92 count): name (303), visible objects (94, 332) and the
|
||||
/// parameters and actions active in it (95, 333).
|
||||
fn visibility(at: Vec3, tags: &[RawTag]) -> ParameterKind {
|
||||
let mut c = Cursor::new(tags);
|
||||
let label = c.string(301);
|
||||
let description = c.string(302);
|
||||
let n = c.count(93);
|
||||
let controlled = c.handles(331, n);
|
||||
let states = (0..c.count(92))
|
||||
.map(|_| {
|
||||
let name = c.string(303);
|
||||
let n = c.count(94);
|
||||
let visible = c.handles(332, n);
|
||||
let n = c.count(95);
|
||||
VisibilityState { name, visible, active: c.handles(333, n) }
|
||||
})
|
||||
.collect();
|
||||
ParameterKind::Visibility { at, label, description, controlled, states }
|
||||
}
|
||||
|
||||
fn action(handle: Handle, node: i32, name: String, kind: &str, secs: &[(&str, &[RawTag])]) -> Action {
|
||||
let mut c = Cursor::new(section(secs, "AcDbBlockAction"));
|
||||
c.next(70);
|
||||
let n = c.count(71);
|
||||
let selection = c.handles(330, n);
|
||||
let at = c.point(1010);
|
||||
let (connections, kind) = match kind {
|
||||
"BLOCKSTRETCHACTION" => stretch(section(secs, "AcDbBlockStretchAction")),
|
||||
"BLOCKLOOKUPACTION" => (Vec::new(), lookup(section(secs, "AcDbBlockLookupAction"))),
|
||||
"BLOCKMOVEACTION" => (two_connections(section(secs, "AcDbBlockMoveAction")), ActionKind::Move),
|
||||
"BLOCKSCALEACTION" => (Vec::new(), ActionKind::Scale),
|
||||
"BLOCKROTATEACTION" => (Vec::new(), ActionKind::Rotate),
|
||||
"BLOCKFLIPACTION" => (Vec::new(), ActionKind::Flip),
|
||||
"BLOCKARRAYACTION" => (Vec::new(), ActionKind::Array),
|
||||
"BLOCKPOLARSTRETCHACTION" => (Vec::new(), ActionKind::PolarStretch),
|
||||
other => (Vec::new(), ActionKind::Other { dxf_type: other.to_string() }),
|
||||
};
|
||||
Action { node, handle, name, at, selection, connections, kind }
|
||||
}
|
||||
|
||||
/// Two connections: parameter ids (92, 93) and the values they read (301, 302).
|
||||
fn two_connections(tags: &[RawTag]) -> Vec<(i32, String)> {
|
||||
let mut c = Cursor::new(tags);
|
||||
let (a, b) = (c.int(92), c.int(93));
|
||||
let (na, nb) = (c.string(301), c.string(302));
|
||||
vec![(a, na), (b, nb)]
|
||||
}
|
||||
|
||||
/// A stretch action: its two connections, the frame (72 count, 1011/1021 points), the objects
|
||||
/// whose vertices it moves (73 count; each a 331 handle, a 74 count and 94 vertex indices), then
|
||||
/// the distance multiplier (140) and angle offset (141).
|
||||
fn stretch(tags: &[RawTag]) -> (Vec<(i32, String)>, ActionKind) {
|
||||
let connections = two_connections(tags);
|
||||
let mut c = Cursor::new(tags);
|
||||
let n = c.count(72);
|
||||
let frame = (0..n)
|
||||
.map(|_| {
|
||||
let x = c.real(1011);
|
||||
Vec2::new(x, c.real(1021))
|
||||
})
|
||||
.collect();
|
||||
let objects = c.count(73);
|
||||
let vertices = (0..objects)
|
||||
.filter_map(|_| {
|
||||
let h = c.handle(331)?;
|
||||
let n = c.count(74);
|
||||
Some((h, (0..n).map(|_| c.int(94)).collect()))
|
||||
})
|
||||
.collect();
|
||||
c.next(75);
|
||||
let (multiplier, angle_offset) = (c.real(140), c.real(141));
|
||||
(connections, ActionKind::Stretch { frame, vertices, multiplier, angle_offset })
|
||||
}
|
||||
|
||||
/// A lookup table: rows (92) and columns (93), the cells row by row (302, after a 301 header),
|
||||
/// then each column: unmatched name (303 then 305), parameter id (94), lookup flag (282) and the
|
||||
/// parameter value it stands for (304).
|
||||
fn lookup(tags: &[RawTag]) -> ActionKind {
|
||||
let mut c = Cursor::new(tags);
|
||||
let rows = c.count(92);
|
||||
let cols = c.count(93);
|
||||
c.next(301);
|
||||
let cells = rows.saturating_mul(cols).min(tags.len());
|
||||
let flat: Vec<String> = (0..cells).map(|_| c.string(302)).collect();
|
||||
let rows = flat.chunks(cols.max(1)).map(<[String]>::to_vec).collect();
|
||||
let columns = (0..cols)
|
||||
.map(|_| {
|
||||
c.next(303);
|
||||
let node = c.int(94);
|
||||
let lookup = c.int(282) != 0;
|
||||
let unmatched = c.string(305);
|
||||
let property = c.string(304);
|
||||
LookupColumn { node, property, lookup, unmatched }
|
||||
})
|
||||
.collect();
|
||||
ActionKind::Lookup { columns, rows }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tag(code: i32, value: &str) -> RawTag {
|
||||
RawTag { code, value: value.to_string() }
|
||||
}
|
||||
|
||||
/// Counts that promise groups the record doesn't have: each nested loop would rescan the
|
||||
/// record for them. The work budget keeps this linear in the record's size.
|
||||
#[test]
|
||||
fn hostile_counts_with_missing_groups_stay_linear() {
|
||||
let n = 20_000;
|
||||
let mut tags = vec![tag(93, "0"), tag(92, &n.to_string())];
|
||||
tags.extend((0..n).map(|_| tag(94, &n.to_string())));
|
||||
let ParameterKind::Visibility { states, .. } = visibility(Vec3::default(), &tags) else {
|
||||
panic!("a visibility parameter")
|
||||
};
|
||||
assert!(states.len() <= n && states.iter().all(|s| s.visible.is_empty()));
|
||||
|
||||
let mut tags = vec![tag(73, &n.to_string())];
|
||||
tags.extend((0..n).flat_map(|_| [tag(331, "1F"), tag(74, &n.to_string())]));
|
||||
let (_, ActionKind::Stretch { vertices, .. }) = stretch(&tags) else { panic!("a stretch action") };
|
||||
let ints: usize = vertices.iter().map(|(_, v)| v.len()).sum();
|
||||
assert!(ints <= 16 * tags.len() + 1024, "{ints}");
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ pub use store::EntityStore;
|
||||
pub use tables::*;
|
||||
|
||||
/// File data CADCraft doesn't model, kept for saving (#528).
|
||||
pub mod dynblock;
|
||||
pub mod foreign;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
@@ -43,6 +43,10 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
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("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("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")
|
||||
@@ -326,9 +330,22 @@ fn run_base(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
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<_>>() }),
|
||||
)
|
||||
let block = |b: &Block| {
|
||||
let dynamic = cadcraft_doc::dynblock::dynamic_block(d, &b.name).is_some();
|
||||
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()))
|
||||
}
|
||||
|
||||
// ---------------- interactive ----------------
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
//! `blocks.dynamic` and `blocks.list` report a dynamic block's parameters and actions as read
|
||||
//! from another program's file (kept foreign data, built by hand here).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::doc::foreign::{Foreign, ForeignExtra, ForeignObject};
|
||||
use cadcraft_engine::doc::{Handle, RawTag};
|
||||
use serde_json::json;
|
||||
|
||||
fn obj(handle: u64, kind: &str, tags: &[(i32, &str)]) -> ForeignObject {
|
||||
ForeignObject { handle: Handle(handle), kind: kind.into(), tags: tags.iter().map(|(c, v)| RawTag { code: *c, value: (*v).into() }).collect() }
|
||||
}
|
||||
|
||||
/// A session whose block "Door" has a visibility parameter with two states.
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
let l = s.execute("line", &json!({ "points": [[0, 0], [1, 0]] })).unwrap()["handles"][0].clone();
|
||||
s.execute("block", &json!({ "name": "Door", "base": [0, 0], "handles": [l] })).unwrap();
|
||||
let line = s.doc().unwrap().block("Door").unwrap().entities.handles()[0].hex();
|
||||
let mut f = Foreign::default();
|
||||
f.records.insert(("BLOCK_RECORD".into(), "DOOR".into()), ForeignExtra { xdict: Some(Handle(0xF00)), ..Default::default() });
|
||||
f.objects.push(obj(0xF00, "DICTIONARY", &[(100, "AcDbDictionary"), (3, "ACAD_ENHANCEDBLOCK"), (360, "F01")]));
|
||||
f.objects.push(obj(0xF01, "ACAD_EVALUATION_GRAPH", &[(100, "AcDbEvalGraph"), (91, "0"), (95, "1"), (360, "F02")]));
|
||||
f.objects.push(obj(
|
||||
0xF02,
|
||||
"BLOCKVISIBILITYPARAMETER",
|
||||
&[
|
||||
(100, "AcDbEvalExpr"),
|
||||
(90, "1"),
|
||||
(100, "AcDbBlockElement"),
|
||||
(300, "Visibility State"),
|
||||
(100, "AcDbBlockParameter"),
|
||||
(280, "1"),
|
||||
(100, "AcDbBlock1PtParameter"),
|
||||
(1010, "2"),
|
||||
(1020, "3"),
|
||||
(100, "AcDbBlockVisibilityParameter"),
|
||||
(301, "Visibility1"),
|
||||
(302, ""),
|
||||
(93, "1"),
|
||||
(331, &line),
|
||||
(92, "2"),
|
||||
(303, "Open"),
|
||||
(94, "1"),
|
||||
(332, &line),
|
||||
(95, "0"),
|
||||
(303, "Closed"),
|
||||
(94, "0"),
|
||||
(95, "0"),
|
||||
],
|
||||
));
|
||||
let mut d = s.doc().unwrap().clone();
|
||||
d.foreign = Arc::new(f);
|
||||
s.open_drawing(d, "dynamic", None);
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_block_data_is_reported() {
|
||||
let mut s = session();
|
||||
let list = s.execute("blocks.list", &json!({})).unwrap();
|
||||
let flags: Vec<_> = list["blocks"].as_array().unwrap().iter().map(|b| (b["name"].as_str().unwrap().to_string(), b["dynamic"].clone())).collect();
|
||||
assert!(flags.contains(&("Door".into(), json!(true))), "{flags:?}");
|
||||
|
||||
let m = s.execute("blocks.dynamic", &json!({ "name": "DOOR" })).unwrap();
|
||||
let p = &m["parameters"][0];
|
||||
assert_eq!((p["node"].clone(), p["element"].clone(), p["showProperties"].clone()), (json!(1), json!("Visibility State"), json!(true)));
|
||||
assert_eq!(p["kind"]["type"], "visibility");
|
||||
assert_eq!(p["kind"]["label"], "Visibility1");
|
||||
assert_eq!(p["kind"]["at"], json!({ "x": 2.0, "y": 3.0, "z": 0.0 }));
|
||||
let states: Vec<_> =
|
||||
p["kind"]["states"].as_array().unwrap().iter().map(|st| (st["name"].clone(), st["visible"].as_array().unwrap().len())).collect();
|
||||
assert_eq!(states, [(json!("Open"), 1), (json!("Closed"), 0)]);
|
||||
assert!(p["kind"]["controlled"][0].is_string(), "handles are hex strings");
|
||||
assert_eq!(m["actions"], json!([]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocks_without_dynamic_data_and_bad_names() {
|
||||
let mut s = session();
|
||||
let l = s.execute("line", &json!({ "points": [[0, 0], [1, 1]] })).unwrap()["handles"][0].clone();
|
||||
s.execute("block", &json!({ "name": "Window", "base": [0, 0], "handles": [l] })).unwrap();
|
||||
assert_eq!(s.execute("blocks.dynamic", &json!({ "name": "Window" })).unwrap(), json!(null));
|
||||
assert_eq!(s.execute("blocks.dynamic", &json!({ "name": "*Model_Space" })).unwrap(), json!(null));
|
||||
for p in [json!({}), json!({ "name": 3 }), json!({ "name": "Nope" }), json!(null)] {
|
||||
assert!(s.execute("blocks.dynamic", &p).is_err(), "{p}");
|
||||
}
|
||||
}
|
||||
@@ -1506,7 +1506,21 @@ pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
|
||||
/// The R2000 DXF the DWG bridge converts to a `dwg` version DWG: its entity types are the DWG
|
||||
/// version's (MULTILEADER from 2007).
|
||||
pub(crate) fn dwg_bridge(d: &Drawing, dwg: Version) -> String {
|
||||
cadcraft_dxf::write_ascii(&tags_for(d, Version::R2000, dwg))
|
||||
let mut tags = tags_for(d, Version::R2000, dwg);
|
||||
bridge_markers(&mut tags);
|
||||
cadcraft_dxf::write_ascii(&tags)
|
||||
}
|
||||
|
||||
/// Subclass markers the DWG bridge knows by another spelling (it matches them exactly):
|
||||
/// AutoCAD writes a dynamic block lookup parameter's as `AcDbBlockLookUpParameter`, the bridge
|
||||
/// reads `AcDbBlockLookupParameter`; otherwise the parameter loses its name and its lookup
|
||||
/// action in the DWG, and AutoCAD's lookups stop working.
|
||||
fn bridge_markers(tags: &mut [Tag]) {
|
||||
for t in tags.iter_mut().filter(|t| t.code == 100) {
|
||||
if t.str().trim().eq_ignore_ascii_case("AcDbBlockLookupParameter") {
|
||||
*t = Tag::s(100, "AcDbBlockLookupParameter");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The drawing's DXF tags, declaring `version` in `$ACADVER`, with what `target` (the version the
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
//! Dynamic blocks from another program: their parameters and actions are read into
|
||||
//! `cadcraft_doc::dynblock`, and survive a save unchanged.
|
||||
//!
|
||||
//! The fixture is synthetic, written for this test: a DOOR block whose BLOCK_RECORD carries an
|
||||
//! evaluation graph with a visibility parameter (Open, Closed), a linear parameter (Width, a list
|
||||
//! of values), a stretch action on it, a lookup parameter and its lookup table, and a grip.
|
||||
|
||||
use cadcraft_doc::dynblock::{ActionKind, DynamicBlock, ParameterKind, dynamic_block};
|
||||
use cadcraft_doc::{Handle, RawTag};
|
||||
use cadcraft_io::{read_dxf, write_dxf};
|
||||
|
||||
/// DXF text from lines of `code value` (the value may be empty).
|
||||
fn dxf(src: &str) -> String {
|
||||
let mut out = String::new();
|
||||
for line in src.lines().map(str::trim).filter(|l| !l.is_empty()) {
|
||||
let (code, value) = line.split_once(' ').unwrap_or((line, ""));
|
||||
out.push_str(&format!("{code}\n{value}\n"));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
const FIXTURE: &str = "
|
||||
0 SECTION
|
||||
2 HEADER
|
||||
9 $ACADVER
|
||||
1 AC1032
|
||||
9 $HANDSEED
|
||||
5 200
|
||||
0 ENDSEC
|
||||
0 SECTION
|
||||
2 TABLES
|
||||
0 TABLE
|
||||
2 BLOCK_RECORD
|
||||
5 1
|
||||
330 0
|
||||
100 AcDbSymbolTable
|
||||
70 3
|
||||
0 BLOCK_RECORD
|
||||
5 1F
|
||||
330 1
|
||||
100 AcDbSymbolTableRecord
|
||||
100 AcDbBlockTableRecord
|
||||
2 *Model_Space
|
||||
340 0
|
||||
0 BLOCK_RECORD
|
||||
5 1E
|
||||
330 1
|
||||
100 AcDbSymbolTableRecord
|
||||
100 AcDbBlockTableRecord
|
||||
2 *Paper_Space
|
||||
340 0
|
||||
0 BLOCK_RECORD
|
||||
5 1D
|
||||
102 {ACAD_XDICTIONARY
|
||||
360 60
|
||||
102 }
|
||||
330 1
|
||||
100 AcDbSymbolTableRecord
|
||||
100 AcDbBlockTableRecord
|
||||
2 Door
|
||||
340 0
|
||||
70 0
|
||||
280 1
|
||||
281 1
|
||||
0 ENDTAB
|
||||
0 ENDSEC
|
||||
0 SECTION
|
||||
2 BLOCKS
|
||||
0 BLOCK
|
||||
5 20
|
||||
330 1F
|
||||
100 AcDbEntity
|
||||
8 0
|
||||
100 AcDbBlockBegin
|
||||
2 *Model_Space
|
||||
70 0
|
||||
10 0
|
||||
20 0
|
||||
30 0
|
||||
3 *Model_Space
|
||||
1
|
||||
0 ENDBLK
|
||||
5 21
|
||||
330 1F
|
||||
100 AcDbEntity
|
||||
8 0
|
||||
100 AcDbBlockEnd
|
||||
0 BLOCK
|
||||
5 24
|
||||
330 1D
|
||||
100 AcDbEntity
|
||||
8 0
|
||||
100 AcDbBlockBegin
|
||||
2 Door
|
||||
70 0
|
||||
10 0
|
||||
20 0
|
||||
30 0
|
||||
3 Door
|
||||
1
|
||||
0 LINE
|
||||
5 30
|
||||
330 1D
|
||||
100 AcDbEntity
|
||||
8 0
|
||||
100 AcDbLine
|
||||
10 0
|
||||
20 0
|
||||
30 0
|
||||
11 36
|
||||
21 0
|
||||
31 0
|
||||
0 LINE
|
||||
5 31
|
||||
330 1D
|
||||
100 AcDbEntity
|
||||
8 0
|
||||
100 AcDbLine
|
||||
10 0
|
||||
20 0
|
||||
30 0
|
||||
11 0
|
||||
21 36
|
||||
31 0
|
||||
0 LINE
|
||||
5 32
|
||||
330 1D
|
||||
100 AcDbEntity
|
||||
8 0
|
||||
100 AcDbLine
|
||||
10 0
|
||||
20 0
|
||||
30 0
|
||||
11 25
|
||||
21 25
|
||||
31 0
|
||||
0 ENDBLK
|
||||
5 25
|
||||
330 1D
|
||||
100 AcDbEntity
|
||||
8 0
|
||||
100 AcDbBlockEnd
|
||||
0 ENDSEC
|
||||
0 SECTION
|
||||
2 ENTITIES
|
||||
0 ENDSEC
|
||||
0 SECTION
|
||||
2 OBJECTS
|
||||
0 DICTIONARY
|
||||
5 C
|
||||
330 0
|
||||
100 AcDbDictionary
|
||||
281 1
|
||||
0 DICTIONARY
|
||||
5 60
|
||||
330 1D
|
||||
100 AcDbDictionary
|
||||
280 1
|
||||
281 1
|
||||
3 ACAD_ENHANCEDBLOCK
|
||||
360 61
|
||||
0 ACAD_EVALUATION_GRAPH
|
||||
5 61
|
||||
102 {ACAD_REACTORS
|
||||
330 60
|
||||
102 }
|
||||
330 60
|
||||
100 AcDbEvalGraph
|
||||
96 6
|
||||
97 6
|
||||
91 0
|
||||
93 32
|
||||
95 1
|
||||
360 62
|
||||
92 -1
|
||||
92 -1
|
||||
92 -1
|
||||
92 -1
|
||||
91 1
|
||||
93 32
|
||||
95 2
|
||||
360 63
|
||||
92 -1
|
||||
92 -1
|
||||
92 -1
|
||||
92 -1
|
||||
91 2
|
||||
93 32
|
||||
95 3
|
||||
360 64
|
||||
92 -1
|
||||
92 -1
|
||||
92 -1
|
||||
92 -1
|
||||
91 3
|
||||
93 32
|
||||
95 4
|
||||
360 65
|
||||
92 -1
|
||||
92 -1
|
||||
92 -1
|
||||
92 -1
|
||||
91 4
|
||||
93 32
|
||||
95 5
|
||||
360 66
|
||||
92 -1
|
||||
92 -1
|
||||
92 -1
|
||||
92 -1
|
||||
91 5
|
||||
93 32
|
||||
95 6
|
||||
360 67
|
||||
92 -1
|
||||
92 -1
|
||||
92 -1
|
||||
92 -1
|
||||
0 BLOCKVISIBILITYPARAMETER
|
||||
5 62
|
||||
330 61
|
||||
100 AcDbEvalExpr
|
||||
90 1
|
||||
98 33
|
||||
99 232
|
||||
100 AcDbBlockElement
|
||||
300 Visibility State
|
||||
98 33
|
||||
99 232
|
||||
1071 0
|
||||
100 AcDbBlockParameter
|
||||
280 1
|
||||
281 0
|
||||
100 AcDbBlock1PtParameter
|
||||
1010 18
|
||||
1020 40
|
||||
1030 0
|
||||
93 0
|
||||
170 0
|
||||
171 0
|
||||
100 AcDbBlockVisibilityParameter
|
||||
281 1
|
||||
301 Visibility1
|
||||
302 Door swing shown or not
|
||||
91 0
|
||||
93 2
|
||||
331 31
|
||||
331 32
|
||||
92 2
|
||||
303 Open
|
||||
94 2
|
||||
332 31
|
||||
332 32
|
||||
95 1
|
||||
333 63
|
||||
303 Closed
|
||||
94 0
|
||||
95 0
|
||||
0 BLOCKLINEARPARAMETER
|
||||
5 63
|
||||
330 61
|
||||
100 AcDbEvalExpr
|
||||
90 2
|
||||
98 33
|
||||
99 232
|
||||
100 AcDbBlockElement
|
||||
300 Linear
|
||||
98 33
|
||||
99 232
|
||||
1071 0
|
||||
100 AcDbBlockParameter
|
||||
280 1
|
||||
281 0
|
||||
100 AcDbBlock2PtParameter
|
||||
1010 0
|
||||
1020 -4
|
||||
1030 0
|
||||
1011 36
|
||||
1021 -4
|
||||
1031 0
|
||||
170 4
|
||||
91 0
|
||||
91 0
|
||||
91 0
|
||||
91 0
|
||||
171 0
|
||||
172 0
|
||||
173 0
|
||||
174 0
|
||||
177 0
|
||||
100 AcDbBlockLinearParameter
|
||||
305 Width
|
||||
306 Door leaf width
|
||||
140 0
|
||||
307
|
||||
96 8
|
||||
141 0
|
||||
142 0
|
||||
143 0
|
||||
175 3
|
||||
144 30
|
||||
144 32
|
||||
144 36
|
||||
0 BLOCKSTRETCHACTION
|
||||
5 64
|
||||
330 61
|
||||
100 AcDbEvalExpr
|
||||
90 3
|
||||
98 33
|
||||
99 232
|
||||
100 AcDbBlockElement
|
||||
300 Stretch
|
||||
98 33
|
||||
99 232
|
||||
1071 0
|
||||
100 AcDbBlockAction
|
||||
70 0
|
||||
71 1
|
||||
330 30
|
||||
1010 40
|
||||
1020 -8
|
||||
1030 0
|
||||
100 AcDbBlockStretchAction
|
||||
92 2
|
||||
93 2
|
||||
301 EndXDelta
|
||||
302 EndYDelta
|
||||
72 2
|
||||
1011 34
|
||||
1021 -2
|
||||
1011 38
|
||||
1021 2
|
||||
73 1
|
||||
331 30
|
||||
74 1
|
||||
94 1
|
||||
75 0
|
||||
140 1
|
||||
141 0
|
||||
280 0
|
||||
0 BLOCKLOOKUPPARAMETER
|
||||
5 65
|
||||
330 61
|
||||
100 AcDbEvalExpr
|
||||
90 4
|
||||
98 33
|
||||
99 232
|
||||
100 AcDbBlockElement
|
||||
300 Lookup
|
||||
98 33
|
||||
99 232
|
||||
1071 0
|
||||
100 AcDbBlockParameter
|
||||
280 1
|
||||
281 0
|
||||
100 AcDbBlock1PtParameter
|
||||
1010 -6
|
||||
1020 40
|
||||
1030 0
|
||||
93 0
|
||||
170 0
|
||||
171 0
|
||||
100 AcDbBlockLookUpParameter
|
||||
94 5
|
||||
303 Size
|
||||
304 Standard door sizes
|
||||
0 BLOCKLOOKUPACTION
|
||||
5 66
|
||||
330 61
|
||||
100 AcDbEvalExpr
|
||||
90 5
|
||||
98 33
|
||||
99 232
|
||||
100 AcDbBlockElement
|
||||
300 Lookup1
|
||||
98 33
|
||||
99 232
|
||||
1071 0
|
||||
100 AcDbBlockAction
|
||||
70 0
|
||||
71 0
|
||||
1010 -4
|
||||
1020 38
|
||||
1030 0
|
||||
100 AcDbBlockLookupAction
|
||||
92 2
|
||||
93 2
|
||||
301
|
||||
302 30
|
||||
302 Narrow
|
||||
302 36
|
||||
302 Wide
|
||||
303
|
||||
94 2
|
||||
95 1
|
||||
96 0
|
||||
282 0
|
||||
305 Custom
|
||||
281 0
|
||||
304 UpdatedDistance
|
||||
303
|
||||
94 4
|
||||
95 1
|
||||
96 0
|
||||
282 1
|
||||
305 Custom
|
||||
281 1
|
||||
304 lookupString
|
||||
280 1
|
||||
0 BLOCKFLIPGRIP
|
||||
5 67
|
||||
330 61
|
||||
100 AcDbEvalExpr
|
||||
90 6
|
||||
98 33
|
||||
99 232
|
||||
100 AcDbBlockElement
|
||||
300 Grip
|
||||
98 33
|
||||
99 232
|
||||
1071 0
|
||||
0 ENDSEC
|
||||
0 EOF
|
||||
";
|
||||
|
||||
fn h(hex: &str) -> Handle {
|
||||
Handle::parse_hex(hex).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dynamic_block_reads_its_parameters_and_actions() {
|
||||
let d = read_dxf(dxf(FIXTURE).as_bytes()).unwrap();
|
||||
assert_eq!(d.block("DOOR").unwrap().entities.len(), 3);
|
||||
let m = dynamic_block(&d, "door").expect("Door is a dynamic block");
|
||||
assert!(dynamic_block(&d, "*Model_Space").is_none(), "model space has no graph here");
|
||||
|
||||
let labels: Vec<_> = m.parameters.iter().map(|p| p.kind.label().unwrap_or("-")).collect();
|
||||
assert_eq!(labels, ["Visibility1", "Width", "Size"]);
|
||||
let ParameterKind::Visibility { states, controlled, description, .. } = &m.parameters[0].kind else { panic!("{:?}", m.parameters[0]) };
|
||||
assert_eq!(description, "Door swing shown or not");
|
||||
assert_eq!(controlled, &[h("31"), h("32")]);
|
||||
let names: Vec<_> = states.iter().map(|s| (s.name.as_str(), s.visible.len(), s.active.len())).collect();
|
||||
assert_eq!(names, [("Open", 2, 1), ("Closed", 0, 0)]);
|
||||
|
||||
let p = &m.parameters[1];
|
||||
assert_eq!((p.node, p.element.as_str(), p.show_properties), (2, "Linear", true));
|
||||
let ParameterKind::Linear { base, end, values, description, .. } = &p.kind else { panic!("{p:?}") };
|
||||
assert_eq!((base.x, base.y, end.x, end.y), (0.0, -4.0, 36.0, -4.0));
|
||||
assert_eq!(description, "Door leaf width");
|
||||
assert!(values.is_list() && !values.has_minimum());
|
||||
assert_eq!(values.list, [30.0, 32.0, 36.0]);
|
||||
|
||||
let names: Vec<_> = m.actions.iter().map(|a| a.name.as_str()).collect();
|
||||
assert_eq!(names, ["Stretch", "Lookup1"]);
|
||||
let a = &m.actions[0];
|
||||
assert_eq!(a.selection, [h("30")]);
|
||||
assert_eq!(a.connections, [(2, "EndXDelta".to_string()), (2, "EndYDelta".to_string())]);
|
||||
let ActionKind::Stretch { frame, vertices, multiplier, .. } = &a.kind else { panic!("{a:?}") };
|
||||
assert_eq!(frame.len(), 2);
|
||||
assert_eq!((frame[0].x, frame[1].y), (34.0, 2.0));
|
||||
assert_eq!(vertices, &[(h("30"), vec![1])]);
|
||||
assert_eq!(*multiplier, 1.0);
|
||||
|
||||
let ActionKind::Lookup { columns, rows } = &m.actions[1].kind else { panic!("{:?}", m.actions[1]) };
|
||||
assert_eq!(rows, &[vec!["30".to_string(), "Narrow".to_string()], vec!["36".to_string(), "Wide".to_string()]]);
|
||||
let cols: Vec<_> = columns.iter().map(|c| (c.node, c.property.as_str(), c.lookup, c.unmatched.as_str())).collect();
|
||||
assert_eq!(cols, [(2, "UpdatedDistance", false, "Custom"), (4, "lookupString", true, "Custom")]);
|
||||
|
||||
assert_eq!(m.other, ["BLOCKFLIPGRIP"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_saved_drawing_keeps_the_dynamic_block_as_read() {
|
||||
let d = read_dxf(dxf(FIXTURE).as_bytes()).unwrap();
|
||||
let before = dynamic_block(&d, "Door").unwrap();
|
||||
let again = read_dxf(write_dxf(&d).as_bytes()).unwrap();
|
||||
// The objects may get new handles on save; the block's objects keep theirs, and the graph's
|
||||
// references between its members follow the new ones.
|
||||
assert_eq!(by_node(dynamic_block(&again, "Door").unwrap()), by_node(before.clone()));
|
||||
// And through DWG: the lookup parameter (AutoCAD writes its marker AcDbBlockLookUpParameter)
|
||||
// keeps its name and lookup action.
|
||||
let dwg = cadcraft_io::write(&d, "x.dwg").unwrap();
|
||||
let back = cadcraft_io::read(&dwg, "x.dwg").unwrap();
|
||||
assert_eq!(by_node(dynamic_block(&back, "Door").unwrap()), by_node(before));
|
||||
}
|
||||
|
||||
/// The model with each parameter and action object named by its graph id instead of its handle.
|
||||
fn by_node(mut m: DynamicBlock) -> DynamicBlock {
|
||||
let ids: std::collections::HashMap<Handle, i32> =
|
||||
m.parameters.iter().map(|p| (p.handle, p.node)).chain(m.actions.iter().map(|a| (a.handle, a.node))).collect();
|
||||
let id = |h: &Handle| Handle(ids.get(h).copied().unwrap_or(-1) as u64);
|
||||
for p in &mut m.parameters {
|
||||
p.handle = id(&p.handle);
|
||||
if let ParameterKind::Visibility { states, .. } = &mut p.kind {
|
||||
for s in states {
|
||||
s.active = s.active.iter().map(id).collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
for a in &mut m.actions {
|
||||
a.handle = id(&a.handle);
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn damaged_dynamic_block_data_is_skipped_not_trusted() {
|
||||
// Counts far beyond the groups present, values that aren't numbers, a graph member that
|
||||
// doesn't exist and a dictionary entry with no graph: reading must not panic or hang.
|
||||
let src = FIXTURE
|
||||
.replace("93 2\n331 31", "93 999999999\n331 31")
|
||||
.replace("92 2\n303 Open", "92 -7\n303 Open")
|
||||
.replace("175 3", "175 2147483647")
|
||||
.replace("144 32", "144 nan")
|
||||
.replace("72 2\n1011 34", "72 99999\n1011 34")
|
||||
.replace("92 2\n93 2\n301\n", "92 1000000\n93 1000000\n301\n")
|
||||
.replace("360 67", "360 FFFF");
|
||||
let d = read_dxf(dxf(&src).as_bytes()).unwrap();
|
||||
let m = dynamic_block(&d, "Door").unwrap();
|
||||
assert_eq!(m.parameters.len(), 3);
|
||||
let ParameterKind::Linear { values, .. } = &m.parameters[1].kind else { panic!() };
|
||||
assert!(values.list.len() <= 3 && values.list.iter().all(|v| v.is_finite()), "{:?}", values.list);
|
||||
assert!(m.other.is_empty(), "a member that doesn't exist is skipped");
|
||||
|
||||
let tags = |v: &[(i32, &str)]| v.iter().map(|(c, s)| RawTag { code: *c, value: (*s).into() }).collect::<Vec<_>>();
|
||||
let mut d = read_dxf(dxf(FIXTURE).as_bytes()).unwrap();
|
||||
let f = std::sync::Arc::make_mut(&mut d.foreign);
|
||||
if let Some(dict) = f.objects.iter_mut().find(|o| o.handle == h("60")) {
|
||||
dict.tags = tags(&[(100, "AcDbDictionary"), (3, "ACAD_ENHANCEDBLOCK")]);
|
||||
}
|
||||
assert!(dynamic_block(&d, "Door").is_none(), "no graph handle");
|
||||
}
|
||||
@@ -1282,3 +1282,4 @@
|
||||
Origin: {x}, {y} Початок: {x}, {y}
|
||||
Sheet Books to PDF... Книги аркушів у PDF...
|
||||
Not a number or distance: {text} Не число й не відстань: {text}
|
||||
Dynamic Block Data Дані динамічного блока
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# File-format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (VIEWPORT frozen layers as group 341; code pages, #505; native MULTILEADER, #459; foreign data kept, #528; PDF text, layers and sheets, #567, #571; templates and user files, #552; plotter devices, #564) · **Target:** Autodesk AutoCAD 2027
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (dynamic block data read into a model) · **Target:** Autodesk AutoCAD 2027
|
||||
|
||||
Every format AutoCAD 2027 reads or writes, with CADCraft's support, fidelity and tests. Work
|
||||
items are in [gaps.md](gaps.md#file-formats); the weighted score feeds
|
||||
@@ -93,6 +93,8 @@ early (#175), but the throughput problem remains. **DWG ≈ 30% read, ≈ 15% wr
|
||||
| Objects: GROUP, MLINESTYLE, MLEADERSTYLE, IMAGEDEF, SCALE | yes | yes (groups #417, scale list #468) | |
|
||||
| Objects: VISUALSTYLE, PLOTSETTINGS, materials, field objects and other unmodelled objects | kept raw (#528) | written back as read | not editable |
|
||||
| Foreign xdata, extension dictionaries, reactors, unmodelled objects, colour books | kept raw (#528); an entity's xdata is part of the entity, so COPY, MIRROR, ARRAY and paste keep it on the copies (EXPLODE pieces don't) | written back, handles remapped, references to deleted objects dropped | through DWG: xdata on table records and blocks, entity colour books and reactors, custom-class objects lost (acadrust) |
|
||||
| Objects: dynamic block data (ACAD_EVALUATION_GRAPH, BLOCK…PARAMETER, BLOCK…ACTION, grips) | read into a model (`dynblock`, `blocks.dynamic`): parameters, visibility states, value sets, stretch and lookup actions; kept raw (#528) | written back as read | not applied to references or editable yet; DWG open and save keep it too (checked with an AutoCAD dynamic block) |
|
||||
| Foreign xdata, extension dictionaries, reactors, unmodelled objects, colour books | kept raw (#528) | written back, handles remapped, references to deleted objects dropped | through DWG: xdata on table records and blocks, entity colour books and reactors, custom-class objects lost (acadrust) |
|
||||
| Handles and owners | entity handles kept; others regenerated | consistent | stable across CADCraft re-saves (#495); other programs' table and object handles renumbered once |
|
||||
|
||||
**DXF ≈ 50% read, ≈ 30% write** (estimated; weights: common 2D entities 50%, annotation objects
|
||||
@@ -137,6 +139,7 @@ our output in AutoCAD black-box.
|
||||
| 2026-10-12 | minor | DXF/DWG save: the Model layout's plot settings are written as CADCraft plots model space, drawing extents scaled to fit (PLOTSETTINGS 74 = 1, 75 = 0), instead of plot type 5 "Layout information", which other programs' Plot dialogs can't use for model space |
|
||||
| 2026-10-12 | minor | VIEWPORT frozen layers also written and read as AutoCAD's group 341 layer handles, so third-party viewers honour per-viewport freezing in DXF (DWG converter gap noted) |
|
||||
| 2026-10-12 | minor | HATCH: associative hatches inside blocks keep their source objects; boundaries carry `{ACAD_REACTORS}` |
|
||||
| 2026-10-12 | minor | Dynamic block data row: parameters and actions read into a model (`blocks.dynamic`); kept by DXF and DWG save |
|
||||
| 2026-10-11 | minor | Rows refreshed for merged work: code pages and `\M+` (#505), MULTILEADER (#459), IMAGE, MLINE/HELIX/REGION/TOLERANCE, VPORT/VIEW/UCS tables, modelled and raw objects, foreign data (#528), DWT templates and user `.lin`/`.pat`/`.pgp` files (#552), PDF output (#567, #571), plotter devices (#564), data extraction (#433, #534); weighted percentages not re-measured |
|
||||
| 2026-10-11 | minor | DXF (ASCII and binary) and DWG saved at 2000–2018, 2018 by default (#421) |
|
||||
| 2026-10-10 | minor | WIPEOUT and gradients written since #147/#162; other save losses remain (#201, #204, #208, #209, #213, #223) |
|
||||
|
||||
Reference in new issue
Block a user