Files
cadcraft/crates/io/src/dxf_write.rs
T
Valentin KleinandClaude Opus 5.5 a395ec3d2d SPLINE: Knots, start/end Tangency, toLerance and Object (part of #408) (#413)
SPLINE's Knots, Object, start/end Tangency and toLerance options printed
"not available yet". Fit-point splines now take a knot parametrisation
(chord, square root, uniform), end tangents (clamped cubic with derivative
constraints) and a fit tolerance (least-squares approximation with the
fewest control points that stay within it). The spline keeps these options
so grip and SPLINEDIT edits refit the same way; DXF writes the tolerance
(44) and tangents (12/13) and recovers the parametrisation from the knots.
Object turns 2D/3D polylines with straight segments (PEDIT Spline output)
into splines through their vertices.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-10 19:19:57 -04:00

2103 lines
77 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Drawing → DXF (AC1015 / R2000 structure, readable by every DXF consumer since 2000).
use std::collections::HashMap;
use cadcraft_color::Color;
use cadcraft_doc::*;
use cadcraft_dxf::Tag;
use cadcraft_geom::{Bounds2, Vec2, Vec3};
use crate::dxf_ext::{self, DimVal, K};
struct W {
t: Vec<Tag>,
next: u64,
}
impl W {
fn s(&mut self, c: i32, v: impl Into<String>) {
self.t.push(Tag::s(c, v));
}
fn f(&mut self, c: i32, v: f64) {
self.t.push(Tag::f(c, v));
}
fn i(&mut self, c: i32, v: i64) {
self.t.push(Tag::i(c, v));
}
fn p(&mut self, c: i32, p: Vec3) {
self.f(c, p.x);
self.f(c + 10, p.y);
self.f(c + 20, p.z);
}
fn p2(&mut self, c: i32, p: Vec2) {
self.f(c, p.x);
self.f(c + 10, p.y);
}
fn h(&mut self) -> String {
let h = format!("{:X}", self.next);
self.next += 1;
h
}
/// A `102 {NAME` … `102 }` group of handles.
fn group(&mut self, name: &str, code: i32, handles: &[&str]) {
self.s(102, format!("{{{name}"));
for h in handles {
self.s(code, *h);
}
self.s(102, "}");
}
/// A dimension variable as a group (DIMSTYLE records) or as an xdata value (DSTYLE).
fn dimval(&mut self, code: i32, v: DimVal, xdata: bool) {
match v {
DimVal::Real(x) => self.f(if xdata { 1040 } else { code }, x),
DimVal::Int(i) => self.i(if xdata { 1070 } else { code }, i),
DimVal::Str(t) => {
if xdata || !t.is_empty() {
self.s(if xdata { 1000 } else { code }, t);
}
}
DimVal::Handle(h) => {
if xdata {
self.s(1005, h);
} else if h != "0" {
self.s(code, h);
}
}
}
}
fn xdata(&mut self, tags: Vec<Tag>) {
self.t.extend(tags);
}
}
/// The `DIMASSOC` object of an associative dimension (stored in the dimension's extension
/// dictionary under `ACAD_DIMASSOC`).
struct AssocObj {
xdict: String,
handle: String,
/// Associativity flag (bit per point reference).
flags: i64,
refs: Vec<OsnapRef>,
}
/// An `AcDbOsnapPointRef` of a DIMASSOC object.
struct OsnapRef {
osnap: i64,
main: Handle,
param: f64,
point: Vec3,
other: Option<Handle>,
}
/// Handles and names the entity writer needs from the rest of the file.
#[derive(Default)]
struct Ctx {
/// First viewport of each layout that has no paper-space viewport (id 1) → the layout's index;
/// [`paper_view`] writes that layout's paper-space viewport ahead of it.
paper_views: HashMap<Handle, usize>,
/// Dimension → anonymous `*D` block name.
dim_blocks: HashMap<Handle, String>,
/// Upper-case text style name → STYLE record handle.
styles: HashMap<String, String>,
/// Upper-case arrow name (as stored in styles/overrides) → BLOCK_RECORD handle.
arrows: HashMap<String, String>,
/// Associative dimension → its DIMASSOC object.
assoc: HashMap<Handle, AssocObj>,
/// Table → (anonymous `*T` block name, BLOCK_RECORD handle).
tables: HashMap<Handle, (String, String)>,
/// Table style name → TABLESTYLE handle (the first is the fallback).
table_styles: Vec<(String, String)>,
/// Raster images' IMAGEDEF, IMAGEDEF_REACTOR and dictionary handles.
images: crate::dxf_image::Plan,
}
impl Ctx {
fn style(&self, name: &str) -> Option<String> {
self.styles.get(&name.to_ascii_uppercase()).cloned()
}
fn arrow(&self, name: &str) -> Option<String> {
self.arrows.get(&name.trim().to_ascii_uppercase()).cloned()
}
fn table_style(&self, name: &str) -> String {
self.table_styles
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(name))
.or_else(|| self.table_styles.first())
.map(|(_, h)| h.clone())
.unwrap_or_else(|| "0".into())
}
/// A dimension variable encoded with this file's handles; integers outside the 16-bit
/// range of their groups are dropped.
fn encode(&self, kind: K, v: &serde_json::Value) -> Option<DimVal> {
dxf_ext::encode(kind, v, &|n| self.style(n), &|n| self.arrow(n)).filter(|x| !matches!(x, DimVal::Int(i) if i16::try_from(*i).is_err()))
}
}
/// `AcadAnnotative` xdata marking an annotative style.
fn annotative_xdata(w: &mut W) {
w.s(1001, "AcadAnnotative");
w.s(1000, "AnnotativeData");
w.s(1002, "{");
w.i(1070, 1);
w.i(1070, 1);
w.s(1002, "}");
}
/// Override (`ACAD` DSTYLE) and associativity (`CADCRAFT` ASSOC) xdata of a dimension.
fn dim_xdata(w: &mut W, dm: &Dimension, cx: &Ctx) {
let mut ov = W { t: Vec::new(), next: 0 };
let mut sah = false;
for (k, v) in &dm.overrides {
let Some(field) = DimStyle::field_name(k) else { continue };
let Some((code, kind)) = dxf_ext::dim_code(field) else { continue };
let Some(val) = dxf_ext::canonical(field, v) else { continue };
let Some(enc) = cx.encode(kind, &val) else { continue };
sah |= matches!(code, 343 | 344);
ov.i(1070, i64::from(code));
ov.dimval(code, enc, true);
}
if sah {
ov.i(1070, i64::from(dxf_ext::DIMSAH));
ov.i(1070, 1);
}
if !ov.t.is_empty() {
w.s(1001, "ACAD");
w.s(1000, "DSTYLE");
w.s(1002, "{");
w.xdata(ov.t);
w.s(1002, "}");
}
let mut ours = Vec::new();
if matches!(dm.kind, DimKind::ArcLength) {
ours.extend(dxf_ext::arclen_xdata(dm.p15));
}
if !dm.assoc.is_empty() {
ours.extend(dxf_ext::assoc_xdata(&dm.assoc));
}
if !ours.is_empty() {
w.s(1001, dxf_ext::APP);
w.xdata(ours);
}
}
/// Header variables we write, with their group codes (DXF Reference, HEADER section).
/// `P2`/`P3` = 2D/3D point.
const HEADER_VARS: &[(&str, i32)] = &[
("INSBASE", -3),
("LIMMIN", -2),
("LIMMAX", -2),
("ORTHOMODE", 70),
("FILLMODE", 70),
("MIRRTEXT", 70),
("LTSCALE", 40),
("ATTMODE", 70),
("TEXTSIZE", 40),
("TRACEWID", 40),
("TEXTSTYLE", 7),
("CLAYER", 8),
("CELTYPE", 6),
("CECOLOR", 62),
("CELTSCALE", 40),
("DIMSCALE", 40),
("DIMSTYLE", 2),
("LUNITS", 70),
("LUPREC", 70),
("AUNITS", 70),
("AUPREC", 70),
("ELEVATION", 40),
("THICKNESS", 40),
("FILLETRAD", 40),
("CHAMFERA", 40),
("CHAMFERB", 40),
("ANGBASE", 50),
("ANGDIR", 70),
("PDMODE", 70),
("PDSIZE", 40),
("PLINEWID", 40),
("CELWEIGHT", 370),
("LWDISPLAY", 290),
("INSUNITS", 70),
("MEASUREMENT", 70),
("PSLTSCALE", 70),
];
fn header_vars(w: &mut W, d: &Drawing) {
for (name, code) in HEADER_VARS {
let Some(v) = d.header.get(name) else { continue };
match (*code, v) {
(-2 | -3, HVal::Point(p)) => {
w.s(9, format!("${name}"));
w.f(10, p.x);
w.f(20, p.y);
if *code == -3 {
w.f(30, p.z);
}
}
(-2 | -3, _) => {}
(c @ (40 | 50), v) => {
if let Some(f) = v.as_f64() {
w.s(9, format!("${name}"));
w.f(c, f);
}
}
(c @ (62 | 70 | 290 | 370), v) => {
if let Some(i) = v.as_i64() {
w.s(9, format!("${name}"));
w.i(c, i);
}
}
(c, v) => {
if let Some(t) = v.as_str() {
w.s(9, format!("${name}"));
w.s(c, t);
}
}
}
}
}
/// An entity's group 440 (DXF Reference): ByBlock is `0x01000000`, a fixed transparency its alpha
/// with the "by alpha" flag `0x02000000` (the alpha mapping layers use). ByLayer writes nothing.
fn transparency_440(t: Transparency) -> Option<i64> {
match t {
Transparency::ByLayer => None,
Transparency::ByBlock => Some(0x0100_0000),
Transparency::Percent(p) => Some(i64::from(0x0200_0000 | ((100 - u32::from(p.min(90))) * 255 / 100))),
}
}
fn common(w: &mut W, e: &Entity, owner: &str, paper: bool, subclass: &str) {
common_x(w, e, owner, paper, subclass, None);
}
/// Common entity groups; `assoc` adds the reactor and extension dictionary of a DIMASSOC.
fn common_x(w: &mut W, e: &Entity, owner: &str, paper: bool, subclass: &str, assoc: Option<&AssocObj>) {
w.s(5, e.handle.hex());
if let Some(a) = assoc {
w.group("ACAD_REACTORS", 330, &[&a.handle]);
w.group("ACAD_XDICTIONARY", 360, &[&a.xdict]);
}
w.s(330, owner);
w.s(100, "AcDbEntity");
if paper {
w.i(67, 1);
}
layer_and_props(w, &e.common, None);
if (e.common.ltscale - 1.0).abs() > 1e-12 {
w.f(48, e.common.ltscale);
}
if !e.common.visible {
w.i(60, 1);
}
if let Some(v) = transparency_440(e.common.transparency) {
w.i(440, v);
}
if !subclass.is_empty() {
w.s(100, subclass);
}
}
/// Layer, linetype, colour and lineweight groups (8, 6, 62/420, 370). ByLayer is left out, unless
/// it replaces something else in `base` (an attribute's definition).
fn layer_and_props(w: &mut W, c: &Common, base: Option<&Common>) {
let write = |by_layer: bool, base_by_layer: fn(&Common) -> bool| !by_layer || base.is_some_and(|b| !base_by_layer(b));
w.s(8, &c.layer);
if write(c.linetype.eq_ignore_ascii_case("bylayer"), |b| b.linetype.eq_ignore_ascii_case("bylayer")) {
w.s(6, &c.linetype);
}
match c.color {
Color::ByLayer if write(true, |b| b.color == Color::ByLayer) => w.i(62, 256),
Color::ByLayer => {}
Color::True(rgb) => {
w.i(62, i64::from(cadcraft_color::nearest_aci(rgb)));
w.i(420, i64::from(rgb.to_u32()));
}
c => w.i(62, i64::from(c.to_aci())),
}
if write(c.lineweight == Lineweight::ByLayer, |b| b.lineweight == Lineweight::ByLayer) {
w.i(370, i64::from(c.lineweight.to_dxf()));
}
}
/// The properties attribute `a` of block reference `e` (of `ins`) is drawn with: its own, else its
/// definition's; an attribute defined on layer 0 is on the reference's layer. Also the
/// definition's properties.
fn attrib_common(d: &Drawing, e: &Entity, ins: &Insert, a: &Attrib) -> (Common, Common) {
let def = d
.block(&ins.block)
.and_then(|b| b.entities.iter().find(|be| matches!(&be.kind, EntityKind::AttDef(ad) if ad.tag.eq_ignore_ascii_case(&a.tag))));
let base = def.map(|de| de.common.clone()).unwrap_or_default();
let mut c = a.props.over(&base);
if c.layer == "0" {
c.layer.clone_from(&e.common.layer);
}
(c, base)
}
/// ATTRIB/ATTDEF flags (group 70): 1 invisible, 2 constant.
fn attrib_flags(a: &Attrib) -> i64 {
i64::from(a.invisible) | if a.constant { 2 } else { 0 }
}
fn text_tags(w: &mut W, t: &Text, attrib_tag: Option<(&str, i64)>, attdef_prompt: Option<&str>) {
w.p(10, t.insert);
w.f(40, t.height);
w.s(1, &t.value);
if t.rotation != 0.0 {
w.f(50, t.rotation.to_degrees());
}
if (t.width_factor - 1.0).abs() > 1e-12 {
w.f(41, t.width_factor);
}
if t.oblique != 0.0 {
w.f(51, t.oblique.to_degrees());
}
w.s(7, &t.style);
let h = match t.halign {
HAlign::Left => 0,
HAlign::Center => 1,
HAlign::Right => 2,
HAlign::Aligned => 3,
HAlign::Middle => 4,
HAlign::Fit => 5,
};
let v = match t.valign {
VAlign::Baseline => 0,
VAlign::Bottom => 1,
VAlign::Middle => 2,
VAlign::Top => 3,
};
if h != 0 {
w.i(72, h);
}
if let Some(a) = t.align_pt {
w.p(11, a);
}
match attrib_tag {
Some((tag, flags)) => {
w.s(100, if attdef_prompt.is_some() { "AcDbAttributeDefinition" } else { "AcDbAttribute" });
if let Some(prompt) = attdef_prompt {
w.s(3, prompt);
}
w.s(2, tag);
w.i(70, flags);
if v != 0 {
w.i(74, v);
}
}
None => {
w.s(100, "AcDbText");
if v != 0 {
w.i(73, v);
}
}
}
}
fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx) {
match &e.kind {
EntityKind::Line(l) => {
w.s(0, "LINE");
common(w, e, owner, paper, "AcDbLine");
thickness(w, e);
w.p(10, l.a);
w.p(11, l.b);
}
EntityKind::Point(p) => {
w.s(0, "POINT");
common(w, e, owner, paper, "AcDbPoint");
w.p(10, p.p);
thickness(w, e);
if p.angle != 0.0 {
w.f(50, p.angle.to_degrees());
}
}
EntityKind::Circle(c) => {
w.s(0, "CIRCLE");
common(w, e, owner, paper, "AcDbCircle");
thickness(w, e);
w.p(10, c.center);
w.f(40, c.radius);
}
EntityKind::Arc(a) => {
w.s(0, "ARC");
common(w, e, owner, paper, "AcDbCircle");
thickness(w, e);
w.p(10, a.center);
w.f(40, a.radius);
w.s(100, "AcDbArc");
w.f(50, a.start.to_degrees());
w.f(51, a.end.to_degrees());
}
EntityKind::Ellipse(el) => {
w.s(0, "ELLIPSE");
common(w, e, owner, paper, "AcDbEllipse");
w.p(10, el.center);
w.p(11, el.major);
w.f(40, el.ratio);
w.f(41, el.start);
w.f(42, el.end);
}
EntityKind::LwPolyline(p) => {
w.s(0, "LWPOLYLINE");
common(w, e, owner, paper, "AcDbPolyline");
w.i(90, p.vertices.len() as i64);
w.i(70, i64::from(p.closed) | if p.plinegen { 128 } else { 0 });
if p.const_width > 0.0 {
w.f(43, p.const_width);
}
if p.elevation != 0.0 {
w.f(38, p.elevation);
}
thickness(w, e);
for v in &p.vertices {
w.p2(10, v.p);
if v.start_width != 0.0 || v.end_width != 0.0 {
w.f(40, v.start_width);
w.f(41, v.end_width);
}
if v.bulge != 0.0 {
w.f(42, v.bulge);
}
}
}
EntityKind::Polyline3d(p) => {
w.s(0, "POLYLINE");
common(w, e, owner, paper, "AcDb3dPolyline");
w.i(66, 1);
w.p(10, Vec3::ZERO);
thickness(w, e);
w.i(70, 8 | i64::from(p.closed));
for q in &p.points {
let vh = w.h();
w.s(0, "VERTEX");
w.s(5, vh);
w.s(330, e.handle.hex());
w.s(100, "AcDbEntity");
w.s(8, &e.common.layer);
w.s(100, "AcDbVertex");
w.s(100, "AcDb3dPolylineVertex");
w.p(10, *q);
w.i(70, 32);
}
let sh = w.h();
w.s(0, "SEQEND");
w.s(5, sh);
w.s(330, e.handle.hex());
w.s(100, "AcDbEntity");
w.s(8, &e.common.layer);
}
EntityKind::Spline(s) => {
w.s(0, "SPLINE");
common(w, e, owner, paper, "AcDbSpline");
let flags = 8 | i64::from(s.closed) | if s.is_periodic() { 2 } else { 0 } | if s.weights.is_empty() { 0 } else { 4 };
w.i(70, flags);
w.i(71, s.degree as i64);
w.i(72, s.knots.len() as i64);
w.i(73, s.control.len() as i64);
w.i(74, s.fit.len() as i64);
w.f(42, 1e-10);
w.f(43, 1e-10);
// Fit data: tolerance and end tangents (the knot parametrisation is implied by the knots).
if !s.fit.is_empty() {
w.f(44, s.fit_opts.tolerance);
if let Some(t) = s.fit_opts.start_tangent {
w.p(12, t.to3(0.0));
}
if let Some(t) = s.fit_opts.end_tangent {
w.p(13, t.to3(0.0));
}
}
for k in &s.knots {
w.f(40, *k);
}
for (i, c) in s.control.iter().enumerate() {
w.p(10, c.to3(0.0));
if let Some(wt) = s.weights.get(i) {
w.f(41, *wt);
}
}
for f in &s.fit {
w.p(11, f.to3(0.0));
}
}
EntityKind::Ray(r) | EntityKind::XLine(r) => {
let ray = matches!(e.kind, EntityKind::Ray(_));
w.s(0, if ray { "RAY" } else { "XLINE" });
common(w, e, owner, paper, if ray { "AcDbRay" } else { "AcDbXline" });
w.p(10, r.base);
w.p(11, r.dir);
}
EntityKind::Text(t) => {
w.s(0, "TEXT");
common(w, e, owner, paper, "AcDbText");
thickness(w, e);
text_tags(w, t, None, None);
}
EntityKind::AttDef(a) => {
w.s(0, "ATTDEF");
common(w, e, owner, paper, "AcDbText");
text_tags(w, &a.text, Some((&a.tag, attrib_flags(a))), Some(&a.prompt));
}
EntityKind::MText(t) => {
w.s(0, "MTEXT");
common(w, e, owner, paper, "AcDbMText");
w.p(10, t.insert);
w.f(40, t.height);
w.f(41, t.width);
w.i(71, i64::from(t.attach));
w.i(72, 1);
// Long contents go in 250-char 3-chunks, the remainder in 1.
let chars: Vec<char> = t.contents.chars().collect();
let chunks: Vec<String> = chars.chunks(250).map(|c| c.iter().collect()).collect();
let n = chunks.len();
for (i, c) in chunks.iter().enumerate() {
w.s(if i + 1 == n { 1 } else { 3 }, c.clone());
}
if n == 0 {
w.s(1, "");
}
w.s(7, &t.style);
if t.rotation != 0.0 {
w.f(50, t.rotation.to_degrees());
}
// Line spacing style: 1 = at least, 2 = exactly.
w.i(73, if t.line_spacing_exact { 2 } else { 1 });
w.f(44, t.line_spacing);
}
EntityKind::Insert(i) => {
w.s(0, "INSERT");
common(w, e, owner, paper, "AcDbBlockReference");
if !i.attribs.is_empty() {
w.i(66, 1);
}
w.s(2, &i.block);
w.p(10, i.insert);
w.f(41, i.scale.x);
w.f(42, i.scale.y);
w.f(43, i.scale.z);
w.f(50, i.rotation.to_degrees());
if i.cols > 1 || i.rows > 1 {
w.i(70, i64::from(i.cols));
w.i(71, i64::from(i.rows));
w.f(44, i.col_spacing);
w.f(45, i.row_spacing);
}
if !i.attribs.is_empty() {
for a in &i.attribs {
let ah = w.h();
w.s(0, "ATTRIB");
w.s(5, ah);
w.s(330, e.handle.hex());
w.s(100, "AcDbEntity");
let (c, base) = attrib_common(d, e, i, a);
layer_and_props(w, &c, Some(&base));
w.s(100, "AcDbText");
text_tags(w, &a.text, Some((&a.tag, attrib_flags(a))), None);
}
let sh = w.h();
w.s(0, "SEQEND");
w.s(5, sh);
w.s(330, e.handle.hex());
w.s(100, "AcDbEntity");
w.s(8, &e.common.layer);
}
}
EntityKind::Dimension(dm) => {
w.s(0, "DIMENSION");
common_x(w, e, owner, paper, "AcDbDimension", cx.assoc.get(&e.handle));
let bname = cx.dim_blocks.get(&e.handle).cloned().or_else(|| dm.block.clone()).unwrap_or_default();
w.s(2, bname);
w.p(10, dm.defpt);
let style = d.dim_style(&dm.style).cloned().unwrap_or_default();
let g = cadcraft_render::dimension_geometry(dm, &style, d.header.f64("DIMSCALE", 1.0));
w.p(11, if dm.user_text_pos { dm.text_mid } else { g.text_pos.to3(0.0) });
let ty: i64 = match dm.kind {
DimKind::Linear { .. } => 0,
DimKind::Aligned => 1,
DimKind::Angular => 2,
DimKind::Diameter => 3,
DimKind::Radius => 4,
DimKind::Angular3P => 5,
DimKind::Ordinate { x_type } => 6 | if x_type { 64 } else { 0 },
// Written as aligned, marked by CADCraft xdata (see `dim_xdata`).
DimKind::ArcLength => 1,
};
w.i(70, ty | 32 | if dm.user_text_pos { 128 } else { 0 });
if !dm.text.is_empty() {
w.s(1, &dm.text);
}
w.s(3, &dm.style);
if dm.text_rotation != 0.0 {
w.f(53, dm.text_rotation.to_degrees());
}
match dm.kind {
DimKind::Linear { rotation } => {
w.s(100, "AcDbAlignedDimension");
w.p(13, dm.p13);
w.p(14, dm.p14);
w.f(50, rotation.to_degrees());
w.s(100, "AcDbRotatedDimension");
}
DimKind::Aligned | DimKind::ArcLength => {
w.s(100, "AcDbAlignedDimension");
w.p(13, dm.p13);
w.p(14, dm.p14);
}
DimKind::Radius => {
w.s(100, "AcDbRadialDimension");
w.p(15, dm.p15);
w.f(40, 0.0);
}
DimKind::Diameter => {
w.s(100, "AcDbDiametricDimension");
w.p(15, dm.p15);
w.f(40, 0.0);
}
DimKind::Angular => {
w.s(100, "AcDb2LineAngularDimension");
w.p(13, dm.p13);
w.p(14, dm.p14);
w.p(15, dm.p15);
w.p(16, dm.p16);
}
DimKind::Angular3P => {
w.s(100, "AcDb3PointAngularDimension");
w.p(13, dm.p13);
w.p(14, dm.p14);
w.p(15, dm.p15);
}
DimKind::Ordinate { .. } => {
w.s(100, "AcDbOrdinateDimension");
w.p(13, dm.p13);
w.p(14, dm.p14);
}
}
dim_xdata(w, dm, cx);
}
EntityKind::Table(t) => {
// Tables without a generated block (none expected) are not written.
let Some((bname, brh)) = cx.tables.get(&e.handle) else { return };
w.s(0, "ACAD_TABLE");
common(w, e, owner, paper, "AcDbBlockReference");
w.s(2, bname);
w.p(10, t.insert);
w.s(100, "AcDbTable");
w.i(280, 0);
w.s(342, cx.table_style(&t.style));
w.s(343, brh);
w.p(11, Vec3::new(1.0, 0.0, 0.0));
w.i(90, 0);
let (rows, cols) = table_size(t);
w.i(91, rows as i64);
w.i(92, cols as i64);
for c in [93, 94, 95, 96] {
w.i(c, 0);
}
for h in t.row_heights.iter().take(rows) {
w.f(141, *h);
}
for c in t.col_widths.iter().take(cols) {
w.f(142, *c);
}
let cov = cadcraft_render::table_covered(t);
let span = |v: u32| i64::from(v.min(i16::MAX as u32));
for r in 0..rows {
for c in 0..cols {
let cell = t.cells.get(r).and_then(|x| x.get(c));
let covered = cov.get(r).and_then(|x| x.get(c)).copied().unwrap_or(false);
let merge = cell.and_then(|x| x.merged).filter(|&(a, b)| a >= 1 && b >= 1 && (a, b) != (1, 1));
w.i(171, 1);
w.i(172, 0);
w.i(173, i64::from(covered || merge.is_some()));
w.i(174, 0);
let (rs, cs) = if covered { (0, 0) } else { merge.map(|(a, b)| (span(a), span(b))).unwrap_or((1, 1)) };
w.i(175, cs);
w.i(176, rs);
w.i(91, 0);
w.i(178, 0);
w.f(145, 0.0);
// Long text: 250-character code-2 chunks, the rest in code 1.
let chars: Vec<char> = cell.map(|x| x.text.chars().collect()).unwrap_or_default();
let chunks: Vec<String> = chars.chunks(250).map(|c| c.iter().collect()).collect();
let n = chunks.len();
for (i, ch) in chunks.into_iter().enumerate() {
w.s(if i + 1 == n { 1 } else { 2 }, ch);
}
if n == 0 {
w.s(1, "");
}
}
}
w.s(1001, dxf_ext::APP);
w.s(1000, "TABLE");
w.i(1070, i64::from(t.title));
w.i(1070, i64::from(t.header));
w.f(1040, t.text_height);
w.s(1000, &t.style);
}
EntityKind::MLeader(m) => {
// Written as plain LEADER + MTEXT entities (MULTILEADER objects aren't supported
// yet), so every reader shows the annotation.
for arm in m.leaders.iter().take(64) {
let mut vertices = arm.clone();
vertices.push(m.landing);
let le = Entity {
handle: Handle(u64::from_str_radix(&w.h(), 16).unwrap_or(0)),
common: e.common.clone(),
kind: EntityKind::Leader(cadcraft_doc::Leader { vertices, arrow: true, spline: false, style: m.style.clone() }),
};
entity(w, d, &le, owner, paper, cx);
}
if let Some(t) = &m.text {
let te = Entity {
handle: Handle(u64::from_str_radix(&w.h(), 16).unwrap_or(0)),
common: e.common.clone(),
kind: EntityKind::MText(t.clone()),
};
entity(w, d, &te, owner, paper, cx);
}
}
EntityKind::Leader(l) => {
w.s(0, "LEADER");
common(w, e, owner, paper, "AcDbLeader");
w.s(3, &l.style);
w.i(71, i64::from(l.arrow));
w.i(72, i64::from(l.spline));
w.i(76, l.vertices.len() as i64);
for v in &l.vertices {
w.p(10, *v);
}
}
EntityKind::Image(i) => {
// Every image has its IMAGEDEF (planned in `write`); the fallback never happens.
let Some((def, reactor)) = cx.images.by_entity.get(&e.handle) else { return };
w.s(0, "IMAGE");
common(w, e, owner, paper, "AcDbRasterImage");
w.t.extend(crate::dxf_image::entity_tags(i, def, reactor));
}
EntityKind::Solid(s) | EntityKind::Trace(s) => {
let solid = matches!(e.kind, EntityKind::Solid(_));
w.s(0, if solid { "SOLID" } else { "TRACE" });
common(w, e, owner, paper, "AcDbTrace");
w.p(10, s.corners[0]);
w.p(11, s.corners[1]);
w.p(12, s.corners[2]);
w.p(13, s.corners[3]);
thickness(w, e);
}
EntityKind::Face3d(f) => {
w.s(0, "3DFACE");
common(w, e, owner, paper, "AcDbFace");
w.p(10, f.corners[0]);
w.p(11, f.corners[1]);
w.p(12, f.corners[2]);
w.p(13, f.corners[3]);
w.i(70, i64::from(f.hidden_edges));
}
EntityKind::Hatch(h) => {
w.s(0, "HATCH");
common(w, e, owner, paper, "AcDbHatch");
w.p(10, Vec3::new(0.0, 0.0, h.elevation));
w.p(210, Vec3::Z);
w.s(2, &h.pattern);
w.i(70, i64::from(h.solid));
w.i(71, i64::from(h.associative));
w.i(91, h.loops.len() as i64);
for l in &h.loops {
w.i(92, 2 | if l.outer { 1 } else { 0 });
let has_bulge = l.vertices.iter().any(|v| v.bulge != 0.0);
w.i(72, i64::from(has_bulge));
w.i(73, 1);
w.i(93, l.vertices.len() as i64);
for v in &l.vertices {
w.p2(10, v.p);
if has_bulge {
w.f(42, v.bulge);
}
}
w.i(97, 0);
}
w.i(75, i64::from(h.style));
w.i(76, 1);
if !h.solid {
w.f(52, h.angle.to_degrees());
w.f(41, h.scale);
w.i(77, 0);
let pat = cadcraft_doc::library::pattern(&h.pattern);
// A pattern the library doesn't define keeps the lines read from its file.
let lines = pat.map(|p| p.lines).unwrap_or_else(|| h.pattern_lines.clone());
w.i(78, lines.len() as i64);
for pl in lines {
let ang = pl.angle.to_radians() + h.angle;
w.f(53, ang.to_degrees());
let o = h.origin + Vec2::new(pl.origin.0, pl.origin.1).rotate(h.angle) * h.scale;
w.f(43, o.x);
w.f(44, o.y);
let dl = Vec2::new(pl.delta.0, pl.delta.1).rotate(ang) * h.scale;
w.f(45, dl.x);
w.f(46, dl.y);
w.i(79, pl.dashes.len() as i64);
for dsh in pl.dashes {
w.f(49, dsh * h.scale);
}
}
}
w.i(98, 0);
if let Some(g) = &h.gradient {
gradient(w, g);
}
hatch_xdata(w, h);
}
EntityKind::Viewport(v) => {
paper_view(w, d, e, owner, cx);
w.s(0, "VIEWPORT");
common(w, e, owner, paper, "AcDbViewport");
w.p(10, v.center);
w.f(40, v.width);
w.f(41, v.height);
// Status 68: a positive value is "on" (the stacking order; 1 is the active viewport).
w.i(68, i64::from(v.id.clamp(1, 32767)));
w.i(69, i64::from(v.id));
w.p2(12, v.view_center);
w.f(45, v.view_height);
// Status flags: 16384 = display locked, 32768 = always set (readers take it as "on").
w.i(90, 32768 | if v.locked { 16384 } else { 0 });
// One CADCraft xdata group holds both lists: readers stop at the first group of an app.
if !v.frozen_layers.is_empty() || !v.layer_colors.is_empty() {
w.s(1001, dxf_ext::APP);
if !v.frozen_layers.is_empty() {
w.xdata(dxf_ext::frozen_xdata(&v.frozen_layers));
}
if !v.layer_colors.is_empty() {
w.xdata(dxf_ext::layer_colors_xdata(&v.layer_colors));
}
}
}
EntityKind::Wipeout(wo) => {
// A 1x1 image spanning the boundary's extents. Clip vertices are in pixel space, whose origin is
// the image's top-left corner with y pointing down: the reader maps each vertex back through
// insert + u * (x + 0.5) + v * (0.5 - y).
// Polygonal clip boundaries are closed, so the first vertex is repeated when needed.
let b = Bounds2::from_points(wo.boundary.iter().copied());
let o = if b.is_empty() { Vec2::ZERO } else { b.min };
let (sx, sy) = (if b.width() > 0.0 { b.width() } else { 1.0 }, if b.height() > 0.0 { b.height() } else { 1.0 });
w.s(0, "WIPEOUT");
common(w, e, owner, paper, "AcDbWipeout");
w.i(90, 0);
w.p(10, Vec3::new(o.x, o.y, 0.0));
w.p(11, Vec3::new(sx, 0.0, 0.0));
w.p(12, Vec3::new(0.0, sy, 0.0));
w.p2(13, Vec2::new(1.0, 1.0));
// Display flags 7 = show image, show unaligned, use clipping boundary; clipping on; default brightness/contrast/fade.
w.i(70, 7);
w.i(280, 1);
w.i(281, 50);
w.i(282, 50);
w.i(283, 0);
w.i(71, 2);
let mut pts = wo.boundary.clone();
if let (Some(first), Some(last)) = (pts.first().copied(), pts.last().copied())
&& first != last
{
pts.push(first);
}
w.i(91, pts.len() as i64);
for q in pts {
w.p2(14, Vec2::new((q.x - o.x) / sx - 0.5, 0.5 - (q.y - o.y) / sy));
}
}
// Entity types CADCraft doesn't model aren't written; the save command reports them.
EntityKind::Unknown(_) => {}
}
}
/// A hatch's gradient fill (DXF Reference, HATCH group codes 450–470): two-colour gradient
/// with its rotation (radians), shift (0 = centered, 1 = shifted) and per-colour 463 records
/// carrying ACI (63) and, for true colours, the RGB value (421). Those groups belong to R2004+
/// files, so the same gradient also travels as `CADCRAFT` xdata, which survives this R2000
/// file's conversion to DWG (where the native fields don't exist).
fn gradient(w: &mut W, g: &Gradient) {
w.i(450, 1);
w.i(451, 0);
w.f(460, g.angle);
w.f(461, if g.centered { 0.0 } else { 1.0 });
w.i(452, 0);
w.f(462, 0.0);
w.i(453, 2);
for (k, c) in [(0.0, g.color1), (1.0, g.color2)] {
w.f(463, k);
match c {
Color::True(rgb) => {
w.i(63, i64::from(cadcraft_color::nearest_aci(rgb)));
w.i(421, i64::from(rgb.to_u32()));
}
c => w.i(63, i64::from(c.to_aci())),
}
}
w.s(470, &g.name);
w.s(1001, dxf_ext::APP);
w.s(1000, "GRADIENT");
// A 1000 group holds at most 255 bytes; cut at a character boundary.
let cut = g.name.char_indices().map(|(i, c)| i + c.len_utf8()).take_while(|end| *end <= 255).last().unwrap_or(0);
w.s(1000, g.name.get(..cut).unwrap_or_default());
w.f(1040, g.angle);
w.i(1070, i64::from(g.centered));
w.s(1000, g.color1.name());
w.s(1000, g.color2.name());
}
/// Group 39 (thickness) of the entities whose DXF Reference record has one; only when set.
fn thickness(w: &mut W, e: &Entity) {
if e.common.thickness != 0.0 {
w.f(39, e.common.thickness);
}
}
/// Hatch origin and background colour as `CADCRAFT` xdata: `1000 HATCH`, the origin as a
/// world position (`1011`, moved with the hatch by other programs) and the background colour
/// name (`1000`, empty for none). The DXF Reference has no HATCH groups for either: other
/// readers get the origin through the pattern lines' base points (43/44), the background isn't
/// carried. Appended to the gradient's `CADCRAFT` xdata when there is one.
fn hatch_xdata(w: &mut W, h: &Hatch) {
if h.origin == Vec2::ZERO && h.background.is_none() {
return;
}
if h.gradient.is_none() {
w.s(1001, dxf_ext::APP);
}
w.s(1000, "HATCH");
w.p(1011, h.origin.to3(0.0));
w.s(1000, h.background.map(Color::name).unwrap_or_default());
}
/// Anonymous dimension blocks (`*D1`…) with the rendered geometry, as consumers expect.
fn dim_block_entities(d: &Drawing, dm: &Dimension, layer: &str) -> Vec<Entity> {
// The dimension as drawn: its overrides, DIMSCALE (0 = the drawing's) and text font.
let style = d.dim_style(&dm.style).cloned().unwrap_or_default().with_overrides(&dm.overrides);
let g = cadcraft_render::dimension_in(d, dm);
let mut out = Vec::new();
let c = Common { layer: layer.into(), color: Color::ByBlock, ..Common::default() };
for l in &g.lines {
for seg in l.windows(2) {
if let [a, b] = seg {
out.push(Entity { handle: Handle(0), common: c.clone(), kind: EntityKind::Line(Line { a: a.to3(0.0), b: b.to3(0.0) }) });
}
}
}
for t in &g.fills {
if let [a, b, cc] = t.as_slice() {
out.push(Entity {
handle: Handle(0),
common: c.clone(),
kind: EntityKind::Solid(Solid { corners: [a.to3(0.0), b.to3(0.0), cc.to3(0.0), cc.to3(0.0)] }),
});
}
}
if !g.value.is_empty() {
out.push(Entity {
handle: Handle(0),
common: c,
kind: EntityKind::MText(MText {
insert: g.text_pos.to3(0.0),
height: g.text_height,
width: 0.0,
attach: 5,
rotation: g.text_angle,
style: style.text_style.clone(),
contents: g.value.clone(),
line_spacing: 1.0,
line_spacing_exact: false,
}),
});
}
out
}
/// Rows and columns written for a table (bounded like the renderer).
fn table_size(t: &Table) -> (usize, usize) {
(t.row_heights.len().min(10_000), t.col_widths.len().min(10_000))
}
/// The anonymous `*T` block of a table: cell borders and texts, for readers that only draw
/// the block.
fn table_block_entities(d: &Drawing, t: &Table, layer: &str) -> Vec<Entity> {
let (rows, cols) = table_size(t);
let mut xs = vec![0.0];
for w in t.col_widths.iter().take(cols) {
xs.push(xs.last().copied().unwrap_or(0.0) + w);
}
let mut ys = vec![0.0];
for h in t.row_heights.iter().take(rows) {
ys.push(ys.last().copied().unwrap_or(0.0) + h);
}
let x_at = |c: usize| xs.get(c).copied().unwrap_or(0.0);
let y_at = |r: usize| ys.get(r).copied().unwrap_or(0.0);
let p = |x: f64, y: f64| Vec3::new(x, -y, 0.0);
let cov = cadcraft_render::table_covered(t);
let margin = d.table_styles.iter().find(|s| s.name.eq_ignore_ascii_case(&t.style)).map(|s| s.margin).unwrap_or(0.06).max(0.0);
let c = Common { layer: layer.into(), color: Color::ByBlock, ..Common::default() };
let mut out = Vec::new();
let mut line = |a: Vec3, b: Vec3| out.push(Entity { handle: Handle(0), common: c.clone(), kind: EntityKind::Line(Line { a, b }) });
let mut texts = Vec::new();
for r in 0..rows {
for col in 0..cols {
if cov.get(r).and_then(|v| v.get(col)).copied().unwrap_or(false) {
continue;
}
let cell = t.cells.get(r).and_then(|row| row.get(col));
let (rs, cs) = cell.and_then(|x| x.merged).map(|(a, b)| (a.max(1) as usize, b.max(1) as usize)).unwrap_or((1, 1));
let (r2, c2) = ((r + rs).min(rows), (col + cs).min(cols));
let (x0, x1, y0, y1) = (x_at(col), x_at(c2), y_at(r), y_at(r2));
line(p(x0, y1), p(x1, y1));
line(p(x1, y1), p(x1, y0));
if r == 0 {
line(p(x0, y0), p(x1, y0));
}
if col == 0 {
line(p(x0, y0), p(x0, y1));
}
let Some(cell) = cell.filter(|x| !x.text.is_empty()) else { continue };
let title = t.title && r == 0;
let header = t.header && r == usize::from(t.title);
let (insert, attach) = if title || header { (p((x0 + x1) / 2.0, (y0 + y1) / 2.0), 5) } else { (p(x0 + margin, (y0 + y1) / 2.0), 4) };
texts.push(Entity {
handle: Handle(0),
common: c.clone(),
kind: EntityKind::MText(MText {
insert,
height: if title { t.text_height * 1.4 } else { t.text_height },
width: 0.0,
attach,
rotation: 0.0,
style: "Standard".into(),
contents: cell.text.clone(),
line_spacing: 1.0,
line_spacing_exact: false,
}),
});
}
}
out.extend(texts);
out
}
/// Our own geometry for an arrowhead block: unit size, tip at the origin, pointing along +X.
fn arrow_block_entities(kind: cadcraft_render::Arrowhead) -> Vec<Entity> {
let g = cadcraft_render::arrowhead(kind, Vec2::ZERO, Vec2::X, 1.0);
let c = Common { layer: "0".into(), color: Color::ByBlock, ..Common::default() };
let mut out = Vec::new();
for l in &g.lines {
for seg in l.windows(2) {
if let [a, b] = seg {
out.push(Entity { handle: Handle(0), common: c.clone(), kind: EntityKind::Line(Line { a: a.to3(0.0), b: b.to3(0.0) }) });
}
}
}
for t in g.tris.chunks(3) {
if let [a, b, cc] = t {
out.push(Entity {
handle: Handle(0),
common: c.clone(),
kind: EntityKind::Solid(Solid { corners: [a.to3(0.0), b.to3(0.0), cc.to3(0.0), cc.to3(0.0)] }),
});
}
}
out
}
/// Standard DIMASSOC point references of an associative dimension: the extension-line
/// origins of linear/aligned dimensions and the feature point of ordinate dimensions, linked
/// to objects that exist. CADCraft's exact links travel in xdata as well.
fn std_assoc_refs(d: &Drawing, dm: &Dimension) -> (i64, Vec<OsnapRef>) {
let slots: &[&str] = match dm.kind {
DimKind::Linear { .. } | DimKind::Aligned => &["p13", "p14"],
DimKind::Ordinate { .. } => &["p13"],
_ => &[],
};
let mut flags = 0;
let mut refs = Vec::new();
for (slot, name) in slots.iter().enumerate() {
let Some(a) = dm.assoc.iter().find(|a| a.point == *name) else { continue };
let Some(ent) = d.entity(a.handle) else { continue };
let point = if *name == "p13" { dm.p13 } else { dm.p14 };
let (osnap, param, other) = match &a.snap {
AssocSnap::Start | AssocSnap::End | AssocSnap::Vertex { .. } => (1, 0.0, None),
AssocSnap::Mid => (2, 0.0, None),
AssocSnap::Center => (if matches!(ent.kind, EntityKind::Point(_)) { 4 } else { 3 }, 0.0, None),
AssocSnap::OnCircle { angle } => (10, *angle, None),
AssocSnap::Intersection { other } => {
if d.entity(*other).is_none() {
continue;
}
(6, 0.0, Some(*other))
}
};
flags |= 1 << slot;
refs.push(OsnapRef { osnap, main: a.handle, param, point, other });
}
(flags, refs)
}
/// Layouts with viewports but without the paper-space viewport (id 1, the sheet itself), keyed
/// by their first viewport. AutoCAD and DWG take a layout's first viewport as its paper-space
/// viewport (DWG does not store viewport ids; readers number them in order), so a layout made in
/// CADCraft, whose paper-space viewport is implicit, gets one written ahead of its viewports.
fn paper_views(d: &Drawing) -> HashMap<Handle, usize> {
let mut out = HashMap::new();
'layouts: for (i, l) in d.layouts.iter().enumerate() {
let mut first = None;
for e in l.entities.iter() {
if let EntityKind::Viewport(v) = &e.kind {
if v.id == 1 {
continue 'layouts;
}
first.get_or_insert(e.handle);
}
}
if let Some(h) = first {
out.insert(h, i);
}
}
out
}
/// The paper-space viewport (id 1) of the layout whose first viewport is `e`, if it needs one
/// ([`paper_views`]): the whole sheet, viewed 1:1. Marked with CADCraft xdata so that our reader
/// leaves it implicit again.
fn paper_view(w: &mut W, d: &Drawing, e: &Entity, owner: &str, cx: &Ctx) {
let Some(l) = cx.paper_views.get(&e.handle).and_then(|i| d.layouts.get(*i)) else { return };
let size = cadcraft_render::paper::sheet(d, &l.name).map(|s| s.size).unwrap_or(Vec2::new(12.0, 9.0));
let center = size * 0.5;
let h = w.h();
w.s(0, "VIEWPORT");
w.s(5, h);
w.s(330, owner);
w.s(100, "AcDbEntity");
w.i(67, 1);
w.s(8, "0");
w.s(100, "AcDbViewport");
w.p(10, center.to3(0.0));
w.f(40, size.x);
w.f(41, size.y);
w.i(68, 1);
w.i(69, 1);
w.p2(12, center);
w.f(45, size.y);
w.i(90, 32768);
w.s(1001, dxf_ext::APP);
w.s(1000, dxf_ext::PAPER_VIEW);
}
fn table_head(w: &mut W, name: &str, count: usize) -> String {
let h = w.h();
w.s(0, "TABLE");
w.s(2, name);
w.s(5, h.clone());
w.s(330, "0");
w.s(100, "AcDbSymbolTable");
w.i(70, count as i64);
h
}
fn record_head(w: &mut W, kind: &str, owner: &str, subclass: &str) -> String {
let h = w.h();
w.s(0, kind);
w.s(if kind == "DIMSTYLE" { 105 } else { 5 }, h.clone());
w.s(330, owner);
w.s(100, "AcDbSymbolTableRecord");
w.s(100, subclass);
h
}
/// Write a drawing as ASCII DXF.
pub fn write(d: &Drawing) -> String {
// Structural handles start above every entity handle.
let mut w = W { t: Vec::new(), next: d.handseed.max(0x100) + 0x1000 };
// Pre-allocate handles for block records.
let mut paper_layouts: Vec<&Layout> = d.layouts.iter().collect();
paper_layouts.sort_by_key(|l| l.tab_order);
let ms_br = w.h();
let ps_brs: Vec<(String, String, &Layout)> = paper_layouts
.iter()
.enumerate()
.map(|(i, l)| (if i == 0 { "*Paper_Space".to_string() } else { format!("*Paper_Space{}", i - 1) }, String::new(), *l))
.collect();
let ps_brs: Vec<(String, String, &Layout)> = ps_brs.into_iter().map(|(n, _, l)| (n, w.h(), l)).collect();
let mut user_blocks: Vec<(String, String, &Block)> = d.blocks.values().map(|b| (b.name.clone(), String::new(), b.as_ref())).collect();
user_blocks.iter_mut().for_each(|b| b.1 = format!("{:X}", 0));
let user_blocks: Vec<(String, String, &Block)> = user_blocks.into_iter().map(|(n, _, b)| (n, w.h(), b)).collect();
let mut cx = Ctx { paper_views: paper_views(d), ..Ctx::default() };
// Dimension blocks.
let mut dim_defs: Vec<(String, String, Vec<Entity>)> = Vec::new(); // filled below
let mut n = 1;
let all_spaces: Vec<&EntityStore> = std::iter::once(&d.model).chain(d.layouts.iter().map(|l| &l.entities)).collect();
for st in &all_spaces {
for e in st.iter() {
if let EntityKind::Dimension(dm) = &e.kind {
let mut name = format!("*D{n}");
while d.block(&name).is_some() {
n += 1;
name = format!("*D{n}");
}
n += 1;
let ents = dim_block_entities(d, dm, &e.common.layer);
let brh = w.h();
cx.dim_blocks.insert(e.handle, name.clone());
dim_defs.push((name, brh, ents));
}
}
}
// Every entity, including block contents (for arrows, tables and overrides).
let every: Vec<&Entity> = all_spaces
.iter()
.flat_map(|s| s.iter().map(|e| e.as_ref()))
.chain(d.blocks.values().flat_map(|b| b.entities.iter().map(|e| e.as_ref())))
.collect();
// Arrowhead blocks: user blocks by name, otherwise generated from our own geometry.
let mut arrow_names: Vec<String> = Vec::new();
for s in &d.dim_styles {
arrow_names.extend([s.arrow_block.clone(), s.arrow_block1.clone(), s.arrow_block2.clone()]);
}
for e in &every {
if let EntityKind::Dimension(dm) = &e.kind {
for (k, v) in &dm.overrides {
let Some(f) = DimStyle::field_name(k).filter(|f| f.starts_with("arrowBlock")) else { continue };
if let Some(s) = dxf_ext::canonical(f, v).as_ref().and_then(|v| v.as_str()) {
arrow_names.push(s.to_string());
}
}
}
}
let mut arrow_defs: Vec<(String, String, Vec<Entity>)> = Vec::new();
for name in arrow_names {
let key = name.trim().to_ascii_uppercase();
if key.is_empty() || cx.arrows.contains_key(&key) {
continue;
}
if let Some((_, h, _)) = user_blocks.iter().find(|(n, _, _)| n.eq_ignore_ascii_case(name.trim())) {
cx.arrows.insert(key, h.clone());
continue;
}
let Some((bname, kind)) = dxf_ext::arrow_block_name(&name) else { continue };
let existing = arrow_defs.iter().find(|(n, _, _)| n.eq_ignore_ascii_case(&bname)).map(|(_, h, _)| h.clone());
let existing = existing.or_else(|| user_blocks.iter().find(|(n, _, _)| n.eq_ignore_ascii_case(&bname)).map(|(_, h, _)| h.clone()));
let h = match existing {
Some(h) => h,
None => {
let h = w.h();
arrow_defs.push((bname, h.clone(), arrow_block_entities(kind)));
h
}
};
cx.arrows.insert(key, h);
}
// Table blocks (`*T`).
let mut table_defs: Vec<(String, String, Vec<Entity>)> = Vec::new();
let mut tn = 1;
for e in &every {
if let EntityKind::Table(t) = &e.kind {
if cx.tables.contains_key(&e.handle) {
continue;
}
let mut name = format!("*T{tn}");
while d.block(&name).is_some() {
tn += 1;
name = format!("*T{tn}");
}
tn += 1;
let brh = w.h();
cx.tables.insert(e.handle, (name.clone(), brh.clone()));
table_defs.push((name, brh, table_block_entities(d, t, &e.common.layer)));
}
}
// Image definitions (IMAGEDEF objects) of raster images anywhere in the drawing.
cx.images = crate::dxf_image::plan(every.iter().copied(), || w.h());
// DIMASSOC objects of associative dimensions in model and paper space.
for st in &all_spaces {
for e in st.iter() {
if let EntityKind::Dimension(dm) = &e.kind {
let (flags, refs) = std_assoc_refs(d, dm);
if !refs.is_empty() {
cx.assoc.insert(e.handle, AssocObj { xdict: w.h(), handle: w.h(), flags, refs });
}
}
}
}
let default_table_style = [TableStyle::default()];
let table_styles: &[TableStyle] = if d.table_styles.is_empty() { &default_table_style } else { &d.table_styles };
cx.table_styles = table_styles.iter().map(|s| (s.name.clone(), w.h())).collect();
let root_dict = w.h();
let group_dict = w.h();
let layout_dict = w.h();
let table_style_dict = w.h();
// CADCraft data in XRECORDs of the named object dictionary: (key, handle, text chunks).
let mut xrecords: Vec<(&str, String, Vec<String>)> = Vec::new();
if let Some(chunks) = dxf_ext::constraint_chunks(&d.constraints, &d.parametric) {
xrecords.push((dxf_ext::CONSTRAINTS_KEY, w.h(), chunks));
}
if let Some(chunks) = dxf_ext::layer_state_chunks(&d.layer_states) {
xrecords.push((dxf_ext::LAYER_STATES_KEY, w.h(), chunks));
}
let model_layout = w.h();
let layout_handles: Vec<String> = ps_brs.iter().map(|_| w.h()).collect();
// ---------------- HEADER ----------------
w.s(0, "SECTION");
w.s(2, "HEADER");
w.s(9, "$ACADVER");
w.s(1, "AC1015");
w.s(9, "$DWGCODEPAGE");
w.s(3, "ANSI_1252");
let ext = d.extents(&Space::Model);
if !ext.is_empty() {
w.s(9, "$EXTMIN");
w.p(10, ext.min.to3(0.0));
w.s(9, "$EXTMAX");
w.p(10, ext.max.to3(0.0));
}
header_vars(&mut w, d);
// The current multileader style (an R2007 variable; older readers skip it).
if let Some(name) = d.header.get("CMLEADERSTYLE").and_then(HVal::as_str) {
w.s(9, "$CMLEADERSTYLE");
w.s(2, name);
}
let seed_pos = w.t.len();
w.s(9, "$HANDSEED");
w.s(5, "0");
w.s(0, "ENDSEC");
// ---------------- CLASSES ----------------
w.s(0, "SECTION");
w.s(2, "CLASSES");
let mut classes = vec![("TABLESTYLE", "AcDbTableStyle", 4095, false), ("MLEADERSTYLE", "AcDbMLeaderStyle", 4095, false)];
if !cx.tables.is_empty() {
classes.push(("ACAD_TABLE", "AcDbTable", 1025, true));
}
if !cx.assoc.is_empty() {
classes.push(("DIMASSOC", "AcDbDimAssoc", 0, false));
}
if every.iter().any(|e| matches!(e.kind, EntityKind::Wipeout(_))) {
classes.push(("WIPEOUT", "AcDbWipeout", 127, true));
}
if !cx.images.is_empty() {
classes.push(("IMAGE", "AcDbRasterImage", 127, true));
classes.push(("IMAGEDEF", "AcDbRasterImageDef", 0, false));
classes.push(("IMAGEDEF_REACTOR", "AcDbRasterImageDefReactor", 1, false));
classes.push(("RASTERVARIABLES", "AcDbRasterVariables", 0, false));
}
for (dxf_name, cpp, proxy, is_entity) in classes {
w.s(0, "CLASS");
w.s(1, dxf_name);
w.s(2, cpp);
w.s(3, "ObjectDBX Classes");
w.i(90, proxy);
w.i(280, 0);
w.i(281, i64::from(is_entity));
}
w.s(0, "ENDSEC");
// ---------------- TABLES ----------------
w.s(0, "SECTION");
w.s(2, "TABLES");
// VPORT
let th = table_head(&mut w, "VPORT", 1);
record_head(&mut w, "VPORT", &th, "AcDbViewportTableRecord");
w.s(2, "*Active");
w.i(70, 0);
w.p2(10, Vec2::ZERO);
w.p2(11, Vec2::new(1.0, 1.0));
w.p2(12, if ext.is_empty() { Vec2::new(6.0, 4.5) } else { ext.center() });
// Snap grid origin and rotation (SNAPBASE, SNAPANG in degrees): R13+ files keep them here.
w.p2(13, d.header.point("SNAPBASE").map(|p| p.xy()).filter(|p| p.is_finite()).unwrap_or(Vec2::ZERO));
w.p2(14, Vec2::new(0.5, 0.5));
w.p2(15, Vec2::new(0.5, 0.5));
w.p(16, Vec3::Z);
w.p(17, Vec3::ZERO);
w.f(40, if ext.is_empty() { 9.0 } else { (ext.height() * 1.1).max(1e-6) });
w.f(41, 1.6);
w.f(42, 50.0);
w.f(50, d.header.f64("SNAPANG", 0.0));
w.s(0, "ENDTAB");
// LTYPE. Embedded text and shapes point at STYLE records (340), which get their handles
// in the STYLE table below: (tag position, style name, is a shape) are filled in there.
let mut ltype_handles: HashMap<String, String> = HashMap::new();
let mut ltype_style_refs: Vec<(usize, Option<String>, bool)> = Vec::new();
let th = table_head(&mut w, "LTYPE", d.linetypes.len());
for lt in &d.linetypes {
let h = record_head(&mut w, "LTYPE", &th, "AcDbLinetypeTableRecord");
ltype_handles.entry(lt.name.to_ascii_uppercase()).or_insert(h);
w.s(2, &lt.name);
w.i(70, 0);
w.s(3, &lt.description);
w.i(72, 65);
w.i(73, lt.pattern.len() as i64);
w.f(40, lt.pattern_length());
for el in &lt.pattern {
w.f(49, el.length);
// 74: 2 = text, 4 = shape, + 1 when the rotation is absolute (DXF Reference, LTYPE).
let (kind, shape) = match (&el.text, el.shape) {
(Some(_), _) => (2, 0),
(None, Some(n)) => (4, n),
(None, None) => (0, 0),
};
if kind == 0 {
w.i(74, 0);
continue;
}
w.i(74, kind | i64::from(el.absolute));
w.i(75, i64::from(shape));
ltype_style_refs.push((w.t.len(), el.style.clone(), kind == 4));
w.s(340, "0");
w.f(46, el.scale);
w.f(50, el.rotation);
w.f(44, el.offset.x);
w.f(45, el.offset.y);
if let Some(t) = &el.text {
w.s(9, t.clone());
}
}
}
w.s(0, "ENDTAB");
// LAYER
let th = table_head(&mut w, "LAYER", d.layers.len());
for l in &d.layers {
record_head(&mut w, "LAYER", &th, "AcDbLayerTableRecord");
w.s(2, &l.name);
w.i(70, i64::from(l.frozen) | if l.vp_freeze_new { 2 } else { 0 } | if l.locked { 4 } else { 0 });
let aci = match l.color {
Color::True(rgb) => i64::from(cadcraft_color::nearest_aci(rgb)),
c => i64::from(c.to_aci()).clamp(1, 255),
};
w.i(62, if l.on { aci } else { -aci });
if let Color::True(rgb) = l.color {
w.i(420, i64::from(rgb.to_u32()));
}
w.s(6, &l.linetype);
if !l.plot {
w.i(290, 0);
}
w.i(370, i64::from(l.lineweight.to_dxf()));
if l.transparency > 0 {
w.s(1001, dxf_ext::LAYER_TRANSPARENCY_APP);
w.i(1071, dxf_ext::transparency_to_dxf(l.transparency));
}
if !l.description.is_empty() {
// The first string is the layer standard's name, unused here.
w.s(1001, dxf_ext::LAYER_DESCRIPTION_APP);
w.s(1000, "");
w.s(1000, dxf_ext::xdata_str(&l.description));
}
}
w.s(0, "ENDTAB");
// STYLE. Linetype shapes from a shape file that isn't a text style get an unnamed shape
// style record (70 bit 1) naming the file.
let mut shape_files: Vec<String> = Vec::new();
for (_, style, shape) in &ltype_style_refs {
let Some(f) = style.as_deref().map(str::trim).filter(|f| *shape && !f.is_empty() && d.text_style(f).is_none()) else { continue };
if !shape_files.iter().any(|x| x.eq_ignore_ascii_case(f)) {
shape_files.push(f.to_string());
}
}
let th = table_head(&mut w, "STYLE", d.text_styles.len() + shape_files.len());
for s in &d.text_styles {
let h = record_head(&mut w, "STYLE", &th, "AcDbTextStyleTableRecord");
cx.styles.entry(s.name.to_ascii_uppercase()).or_insert(h);
w.s(2, &s.name);
// 70 bit 4: vertical; 71 bits 2/4: backwards / upside down.
w.i(70, if s.vertical { 4 } else { 0 });
w.f(40, s.height);
w.f(41, s.width_factor);
w.f(50, s.oblique.to_degrees());
w.i(71, if s.backwards { 2 } else { 0 } | if s.upside_down { 4 } else { 0 });
w.f(42, 0.2);
// The built-in font goes out as `txt`, which every reader has; CADCraft's own name
// travels in `CADCRAFT` xdata (`1000 FONT`, name) so the style reopens with it.
let file = if s.font == cadcraft_fonts_name() { "txt" } else { s.font.as_str() };
w.s(3, file);
w.s(4, &s.big_font);
if s.annotative {
annotative_xdata(&mut w);
}
if file != s.font {
w.s(1001, dxf_ext::APP);
w.s(1000, "FONT");
w.s(1000, dxf_ext::xdata_str(&s.font));
}
}
let mut shape_handles: HashMap<String, String> = HashMap::new();
for f in &shape_files {
let h = record_head(&mut w, "STYLE", &th, "AcDbTextStyleTableRecord");
shape_handles.insert(f.to_ascii_uppercase(), h);
w.s(2, "");
w.i(70, 1);
w.f(40, 0.0);
w.f(41, 1.0);
w.f(50, 0.0);
w.i(71, 0);
w.f(42, 0.2);
w.s(3, f.clone());
w.s(4, "");
}
w.s(0, "ENDTAB");
// Linetype text and shapes without a known style use Standard (else the first style).
let fallback_style = cx.style("Standard").or_else(|| d.text_styles.first().and_then(|s| cx.style(&s.name))).unwrap_or_else(|| "0".into());
for (pos, style, shape) in ltype_style_refs {
let name = style.as_deref().map(str::trim).unwrap_or_default();
let h = shape.then(|| shape_handles.get(&name.to_ascii_uppercase()).cloned()).flatten().or_else(|| cx.style(name));
if let Some(t) = w.t.get_mut(pos) {
*t = Tag::s(340, h.unwrap_or_else(|| fallback_style.clone()));
}
}
// VIEW: named views (DXF Reference, VIEW). A view's layer state is CADCraft xdata.
let th = table_head(&mut w, "VIEW", d.views.len());
for v in &d.views {
record_head(&mut w, "VIEW", &th, "AcDbViewTableRecord");
w.s(2, &v.name);
w.i(70, 0);
w.f(40, v.height);
w.p2(10, v.center);
w.f(41, v.width);
w.p(11, Vec3::Z);
w.p(12, Vec3::ZERO);
w.f(42, 50.0);
w.f(43, 0.0);
w.f(44, 0.0);
w.f(50, 0.0);
w.i(71, 0);
w.i(281, 0);
w.i(72, 0);
if let Some(ls) = &v.layer_state {
w.s(1001, dxf_ext::APP);
w.s(1000, "LAYERSTATE");
w.s(1000, dxf_ext::xdata_str(ls));
}
}
w.s(0, "ENDTAB");
// UCS: named user coordinate systems (DXF Reference, UCS).
let th = table_head(&mut w, "UCS", d.ucss.len());
for u in &d.ucss {
record_head(&mut w, "UCS", &th, "AcDbUCSTableRecord");
w.s(2, &u.name);
w.i(70, 0);
w.p(10, u.origin);
w.p(11, u.x_axis);
w.p(12, u.y_axis);
w.i(79, 0);
w.f(146, 0.0);
}
w.s(0, "ENDTAB");
let apps = ["ACAD", dxf_ext::APP, "AcadAnnotative", dxf_ext::LAYER_TRANSPARENCY_APP, dxf_ext::LAYER_DESCRIPTION_APP];
let th = table_head(&mut w, "APPID", apps.len());
for app in apps {
record_head(&mut w, "APPID", &th, "AcDbRegAppTableRecord");
w.s(2, app);
w.i(70, 0);
}
w.s(0, "ENDTAB");
// DIMSTYLE
let th = {
let h = w.h();
w.s(0, "TABLE");
w.s(2, "DIMSTYLE");
w.s(5, h.clone());
w.s(330, "0");
w.s(100, "AcDbSymbolTable");
w.i(70, d.dim_styles.len() as i64);
w.s(100, "AcDbDimStyleTable");
w.i(71, 0);
h
};
for s in &d.dim_styles {
record_head(&mut w, "DIMSTYLE", &th, "AcDbDimStyleTableRecord");
w.s(2, &s.name);
w.i(70, 0);
let v = serde_json::to_value(s).unwrap_or_default();
for (field, code, kind) in dxf_ext::DIM_CODES {
if let Some(enc) = v.get(*field).and_then(|x| cx.encode(*kind, x)) {
w.dimval(*code, enc, false);
}
}
if !s.arrow_block1.trim().is_empty() || !s.arrow_block2.trim().is_empty() {
w.i(dxf_ext::DIMSAH, 1);
}
if s.annotative {
annotative_xdata(&mut w);
}
}
w.s(0, "ENDTAB");
// BLOCK_RECORD
let th = table_head(&mut w, "BLOCK_RECORD", 1 + ps_brs.len() + user_blocks.len() + dim_defs.len() + arrow_defs.len() + table_defs.len());
let br_rec = |w: &mut W, h: &str, name: &str, layout: Option<&str>, blk: Option<&Block>| {
w.s(0, "BLOCK_RECORD");
w.s(5, h);
w.s(330, th.clone());
w.s(100, "AcDbSymbolTableRecord");
w.s(100, "AcDbBlockTableRecord");
w.s(2, name);
w.s(340, layout.unwrap_or("0"));
// User blocks: insertion units (70) and explodability (280).
if let Some(b) = blk {
w.i(70, i64::from(b.units));
w.i(280, i64::from(b.explodable));
}
};
br_rec(&mut w, &ms_br, "*Model_Space", Some(&model_layout), None);
for (i, (n, h, _)) in ps_brs.iter().enumerate() {
br_rec(&mut w, h, n, layout_handles.get(i).map(String::as_str), None);
}
for (n, h, b) in &user_blocks {
br_rec(&mut w, h, n, None, Some(b));
}
for (n, h, _) in dim_defs.iter().chain(&arrow_defs).chain(&table_defs) {
br_rec(&mut w, h, n, None, None);
}
w.s(0, "ENDTAB");
w.s(0, "ENDSEC");
// ---------------- BLOCKS ----------------
w.s(0, "SECTION");
w.s(2, "BLOCKS");
let block = |w: &mut W, name: &str, desc: &str, brh: &str, base: Vec3, flags: i64, ents: &mut dyn Iterator<Item = &Entity>, paper: bool| {
let bh = w.h();
w.s(0, "BLOCK");
w.s(5, bh);
w.s(330, brh);
w.s(100, "AcDbEntity");
if paper {
w.i(67, 1);
}
w.s(8, "0");
w.s(100, "AcDbBlockBegin");
w.s(2, name);
w.i(70, flags);
w.p(10, base);
w.s(3, name);
w.s(1, "");
if !desc.is_empty() {
w.s(4, desc);
}
for e in ents {
entity(w, d, e, brh, false, &cx);
}
let eh = w.h();
w.s(0, "ENDBLK");
w.s(5, eh);
w.s(330, brh);
w.s(100, "AcDbEntity");
if paper {
w.i(67, 1);
}
w.s(8, "0");
w.s(100, "AcDbBlockEnd");
};
block(&mut w, "*Model_Space", "", &ms_br, Vec3::ZERO, 0, &mut std::iter::empty(), false);
for (i, (n, h, l)) in ps_brs.iter().enumerate() {
if i == 0 {
block(&mut w, n, "", h, Vec3::ZERO, 0, &mut std::iter::empty(), true);
} else {
block(&mut w, n, "", h, Vec3::ZERO, 0, &mut l.entities.iter().map(|e| e.as_ref()), true);
}
}
for (n, h, b) in &user_blocks {
block(&mut w, n, &b.description, h, b.base, if b.anonymous { 1 } else { 0 }, &mut b.entities.iter().map(|e| e.as_ref()), false);
}
// Generated geometry entities (dimension, arrowhead and table blocks) need handles.
for (_, _, ents) in dim_defs.iter_mut().chain(arrow_defs.iter_mut()).chain(table_defs.iter_mut()) {
for e in ents.iter_mut() {
e.handle = Handle(w.next);
w.next += 1;
}
}
for (n, h, ents) in &dim_defs {
block(&mut w, n, "", h, Vec3::ZERO, 1, &mut ents.iter(), false);
}
for (n, h, ents) in &arrow_defs {
block(&mut w, n, "", h, Vec3::ZERO, 0, &mut ents.iter(), false);
}
for (n, h, ents) in &table_defs {
block(&mut w, n, "", h, Vec3::ZERO, 1, &mut ents.iter(), false);
}
w.s(0, "ENDSEC");
// ---------------- ENTITIES ----------------
w.s(0, "SECTION");
w.s(2, "ENTITIES");
for e in d.model.iter() {
entity(&mut w, d, e, &ms_br, false, &cx);
}
if let Some((_, h, l)) = ps_brs.first() {
for e in l.entities.iter() {
entity(&mut w, d, e, h, true, &cx);
}
}
w.s(0, "ENDSEC");
// ---------------- OBJECTS ----------------
// Groups (members that still exist) and multileader styles.
let group_owner = group_dict.clone();
let written: std::collections::HashSet<String> = w.t.iter().filter(|t| t.code == 5).map(Tag::str).collect();
let groups: Vec<(&Group, String)> = d.groups.iter().filter(|g| !g.name.is_empty()).map(|g| (g, w.h())).collect();
let default_mleader_style = [MLeaderStyle::default()];
let mleader_styles: &[MLeaderStyle] = if d.mleader_styles.is_empty() { &default_mleader_style } else { &d.mleader_styles };
let mleader_dict = w.h();
let mleader_handles: Vec<String> = mleader_styles.iter().map(|_| w.h()).collect();
w.s(0, "SECTION");
w.s(2, "OBJECTS");
w.s(0, "DICTIONARY");
w.s(5, root_dict.clone());
w.s(330, "0");
w.s(100, "AcDbDictionary");
w.i(281, 1);
w.s(3, "ACAD_GROUP");
w.s(350, group_dict.clone());
if !cx.images.is_empty() {
w.s(3, "ACAD_IMAGE_DICT");
w.s(350, cx.images.dict.clone());
w.s(3, "ACAD_IMAGE_VARS");
w.s(350, cx.images.vars.clone());
}
w.s(3, "ACAD_LAYOUT");
w.s(350, layout_dict.clone());
w.s(3, "ACAD_MLEADERSTYLE");
w.s(350, mleader_dict.clone());
w.s(3, "ACAD_TABLESTYLE");
w.s(350, table_style_dict.clone());
for (key, h, _) in &xrecords {
w.s(3, *key);
w.s(350, h);
}
w.s(0, "DICTIONARY");
w.s(5, group_dict);
w.s(330, root_dict.clone());
w.s(100, "AcDbDictionary");
w.i(281, 1);
for (g, h) in &groups {
w.s(3, &g.name);
w.s(350, h);
}
for (g, h) in &groups {
group_obj(&mut w, g, h, &group_owner, &written);
}
// Multileader styles.
w.s(0, "DICTIONARY");
w.s(5, mleader_dict.clone());
w.s(330, root_dict.clone());
w.s(100, "AcDbDictionary");
w.i(281, 1);
for (s, h) in mleader_styles.iter().zip(&mleader_handles) {
w.s(3, &s.name);
w.s(350, h);
}
let byblock_ltype = ltype_handles.get("BYBLOCK").cloned();
for (s, h) in mleader_styles.iter().zip(&mleader_handles) {
mleader_style_obj(&mut w, s, h, &mleader_dict, byblock_ltype.as_deref(), cx.style(&s.text_style));
}
// Table styles.
w.s(0, "DICTIONARY");
w.s(5, table_style_dict.clone());
w.s(330, root_dict.clone());
w.s(100, "AcDbDictionary");
w.i(281, 1);
for (name, h) in &cx.table_styles {
w.s(3, name);
w.s(350, h);
}
for (s, (_, h)) in table_styles.iter().zip(&cx.table_styles) {
table_style_obj(&mut w, s, h, &table_style_dict);
}
// Parametric constraints and parameters, saved layer states (CADCraft data).
for (_, h, chunks) in &xrecords {
w.s(0, "XRECORD");
w.s(5, h);
w.group("ACAD_REACTORS", 330, &[&root_dict]);
w.s(330, root_dict.clone());
w.s(100, "AcDbXrecord");
w.i(280, 1);
for c in chunks {
w.s(1, c.clone());
}
}
// Dimension associativity: extension dictionary + DIMASSOC per dimension.
let mut assoc: Vec<(&Handle, &AssocObj)> = cx.assoc.iter().collect();
assoc.sort_by_key(|(h, _)| **h);
for (dim, a) in assoc {
let dh = dim.hex();
w.s(0, "DICTIONARY");
w.s(5, a.xdict.clone());
w.group("ACAD_REACTORS", 330, &[&dh]);
w.s(330, dh.clone());
w.s(100, "AcDbDictionary");
w.i(280, 1);
w.i(281, 1);
w.s(3, "ACAD_DIMASSOC");
w.s(360, a.handle.clone());
w.s(0, "DIMASSOC");
w.s(5, a.handle.clone());
w.group("ACAD_REACTORS", 330, &[&a.xdict]);
w.s(330, a.xdict.clone());
w.s(100, "AcDbDimAssoc");
w.s(330, dh);
w.i(90, a.flags);
w.i(70, 0);
w.i(71, 0);
for r in &a.refs {
w.s(1, "AcDbOsnapPointRef");
w.i(72, r.osnap);
w.s(331, r.main.hex());
w.i(73, 0);
w.i(91, 0);
w.s(301, "");
w.f(40, r.param);
w.p(10, r.point);
if let Some(o) = r.other {
w.s(332, o.hex());
w.i(74, 0);
w.i(92, 0);
w.s(302, "");
}
w.i(75, 0);
}
}
w.s(0, "DICTIONARY");
w.s(5, layout_dict.clone());
w.s(330, root_dict.clone());
w.s(100, "AcDbDictionary");
w.i(281, 1);
w.s(3, "Model");
w.s(350, model_layout.clone());
for (i, (_, _, l)) in ps_brs.iter().enumerate() {
w.s(3, &l.name);
w.s(350, layout_handles.get(i).cloned().unwrap_or_default());
}
let layout_obj = |w: &mut W, h: &str, name: &str, tab: i64, brh: &str, flags: i64, page: &PageSetup, view: Option<(Vec2, f64)>| {
// Plot scale: a standard scale when fitting or 1:1, otherwise custom (142 paper units
// per 143 drawing units).
let scale = if page.scale.is_finite() && page.scale > 0.0 { page.scale } else { 1.0 };
let standard = page.scale_to_fit || scale == 1.0;
let plot_flags = 512
| 32
| if standard { 16 } else { 0 }
| if page.lineweights { 128 } else { 0 }
| if page.center { 4 } else { 0 }
| if page.show_plot_styles { 2 } else { 0 };
let plot_type = match page.plot_area.as_str() {
"display" => 0,
"extents" => 1,
"limits" => 2,
"window" => 4,
_ => 5,
};
w.s(0, "LAYOUT");
w.s(5, h);
w.s(330, layout_dict.clone());
w.s(100, "AcDbPlotSettings");
w.s(1, "");
let device = page.device.trim();
w.s(2, if device.is_empty() || device.eq_ignore_ascii_case("None") { "none_device" } else { device });
w.s(4, page.paper.replace(' ', "_"));
w.s(6, "");
for (c, v) in [
(40, page.margins_mm[0]),
(41, page.margins_mm[1]),
(42, page.margins_mm[2]),
(43, page.margins_mm[3]),
(44, page.width_mm),
(45, page.height_mm),
(46, 0.0),
(47, 0.0),
(48, 0.0),
(49, 0.0),
(140, 0.0),
(141, 0.0),
(142, scale),
(143, 1.0),
] {
w.f(c, v);
}
w.i(70, plot_flags);
w.i(72, 0);
w.i(73, i64::from(page.landscape));
w.i(74, plot_type);
w.s(7, &page.plot_style_table);
w.i(75, if page.scale_to_fit { 0 } else { 16 });
w.f(147, scale);
w.f(148, 0.0);
w.f(149, 0.0);
w.s(100, "AcDbLayout");
w.s(1, name);
w.i(70, flags);
w.i(71, tab);
w.p2(10, Vec2::ZERO);
w.p2(11, Vec2::new(12.0, 9.0));
w.p(12, Vec3::ZERO);
w.p(14, Vec3::ZERO);
w.p(15, Vec3::ZERO);
w.f(146, 0.0);
w.p(13, Vec3::ZERO);
w.p(16, Vec3::new(1.0, 0.0, 0.0));
w.p(17, Vec3::new(0.0, 1.0, 0.0));
w.i(76, 0);
w.s(330, brh);
// The saved paper-space view (centre, height) as CADCraft xdata.
if let Some((c, height)) = view.filter(|(c, v)| c.x.is_finite() && c.y.is_finite() && v.is_finite()) {
w.s(1001, dxf_ext::APP);
w.s(1000, "PSVIEW");
w.s(1002, "{");
w.p(1010, c.to3(0.0));
w.f(1040, height);
w.s(1002, "}");
}
};
layout_obj(&mut w, &model_layout, "Model", 0, &ms_br, 1, &PageSetup::default(), None);
for (i, (_, brh, l)) in ps_brs.iter().enumerate() {
let h = layout_handles.get(i).cloned().unwrap_or_default();
layout_obj(&mut w, &h, &l.name, i64::from(l.tab_order.max(1)), brh, 1, &l.page, l.view);
}
// Raster image definitions.
w.t.extend(crate::dxf_image::objects(&cx.images, &root_dict));
w.s(0, "ENDSEC");
w.s(0, "EOF");
let seed = format!("{:X}", w.next);
if let Some(t) = w.t.get_mut(seed_pos + 1) {
*t = Tag::s(5, seed);
}
mark_xrefs(&mut w.t, d);
cadcraft_dxf::write_ascii(&w.t)
}
/// Xref blocks (DXF Reference, BLOCK): bit 4 of group 70 and the external drawing's path in
/// group 1, set on the written BLOCK records of blocks that have an `xref_path`.
fn mark_xrefs(tags: &mut [Tag], d: &Drawing) {
let xrefs: HashMap<String, &str> = d.blocks.values().filter_map(|b| Some((b.name.to_ascii_uppercase(), b.xref_path.as_deref()?))).collect();
if xrefs.is_empty() {
return;
}
// Inside a BLOCK record: the path once its name (2) says it is an xref.
let mut block: Option<Option<&str>> = None;
for t in tags.iter_mut() {
match (t.code, block) {
(0, _) => block = (t.str() == "BLOCK").then_some(None),
(2, Some(None)) => block = Some(xrefs.get(&t.str().to_ascii_uppercase()).copied()),
(70, Some(Some(_))) => *t = Tag::i(70, t.i64() | 4),
(1, Some(Some(p))) => *t = Tag::s(1, p),
_ => {}
}
}
}
/// A TABLESTYLE object (DXF Reference, OBJECTS: TABLESTYLE): margins, title/header
/// suppression and per row type (data, column header, title) text and border settings.
fn table_style_obj(w: &mut W, s: &TableStyle, h: &str, dict: &str) {
w.s(0, "TABLESTYLE");
w.s(5, h);
w.group("ACAD_REACTORS", 330, &[dict]);
w.s(330, dict);
w.s(100, "AcDbTableStyle");
w.s(3, "");
w.i(70, 0);
w.i(71, 0);
w.f(40, s.margin);
w.f(41, s.margin);
w.i(280, i64::from(!s.title));
w.i(281, i64::from(!s.header));
// Data (middle left), column header and title (middle centre).
for (height, align) in [(s.text_height, 4), (s.text_height, 5), (s.text_height * 1.4, 5)] {
w.s(7, "Standard");
w.f(140, height);
w.i(170, align);
w.i(62, 0);
w.i(63, 7);
w.i(283, 0);
for c in 274..=279 {
w.i(c, -2);
}
for c in 284..=289 {
w.i(c, 1);
}
for c in 64..=69 {
w.i(c, 0);
}
}
}
/// A GROUP object (DXF Reference, OBJECTS: GROUP): description, unnamed (`*A…`) and selectable
/// flags, and the member entities that were written to this file.
fn group_obj(w: &mut W, g: &Group, h: &str, dict: &str, written: &std::collections::HashSet<String>) {
w.s(0, "GROUP");
w.s(5, h);
w.group("ACAD_REACTORS", 330, &[dict]);
w.s(330, dict);
w.s(100, "AcDbGroup");
w.s(300, &g.description);
w.i(70, i64::from(g.name.starts_with('*')));
w.i(71, i64::from(g.selectable));
for m in g.members.iter().map(|m| m.hex()).filter(|m| written.contains(m)) {
w.s(340, m);
}
}
/// A MLEADERSTYLE object (DXF Reference, OBJECTS: MLEADERSTYLE): MText content and straight
/// ByBlock leaders with the style's landing gap, dogleg length, arrow size, text style and
/// text height.
fn mleader_style_obj(w: &mut W, s: &MLeaderStyle, h: &str, dict: &str, byblock_ltype: Option<&str>, text_style: Option<String>) {
// ByBlock as a 32-bit colour value (0xC1000000).
const BYBLOCK: i64 = -1_056_964_608;
w.s(0, "MLEADERSTYLE");
w.s(5, h);
w.group("ACAD_REACTORS", 330, &[dict]);
w.s(330, dict);
w.s(100, "AcDbMLeaderStyle");
w.i(179, 2);
w.i(170, 2);
w.i(171, 1);
w.i(172, 0);
w.i(90, 2);
w.f(40, 0.0);
w.f(41, 0.0);
w.i(173, 1);
w.i(91, BYBLOCK);
if let Some(lt) = byblock_ltype {
w.s(340, lt);
}
w.i(92, -2);
w.i(290, 1);
w.f(42, s.landing_gap);
w.i(291, 1);
w.f(43, s.dogleg);
w.s(3, "");
w.f(44, s.arrow_size);
w.s(300, "");
if let Some(ts) = text_style {
w.s(342, ts);
}
w.i(174, 1);
w.i(178, 1);
w.i(175, 1);
w.i(176, 0);
w.i(93, BYBLOCK);
w.f(45, s.text_height);
w.i(292, 0);
w.i(297, 0);
w.f(46, 4.0);
w.i(94, BYBLOCK);
for c in [47, 49, 140] {
w.f(c, 1.0);
}
w.i(293, 1);
w.f(141, 0.0);
w.i(294, 1);
w.i(177, 0);
w.f(142, 1.0);
w.i(295, 0);
w.i(296, 0);
w.f(143, 0.125);
w.i(271, 0);
w.i(272, 9);
w.i(273, 9);
}
fn cadcraft_fonts_name() -> &'static str {
"CADCraft Stroke"
}