mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 10:56:51 -06:00
* AutoCAD interop: justified text, DIMPOST, current dimension style, *D block colours What AutoCAD takes from CADCraft's DWG/DXF as it is: - Justified TEXT/ATTRIB/ATTDEF (#1050): group 10 is now where the baseline starts, from the alignment point and the text's width, not the alignment point itself; AutoCAD drew centred and right-aligned text from DWG starting at its alignment point. A stored point close to CADCraft's own (AutoCAD's, exact for its fonts) is kept. - DIMPOST (#1053) is written even when empty: the DWG bridge defaulted a missing one to "<>", and AutoCAD then dropped the "R" of radius dimensions when it regenerated them. - The current dimension style (#1051): the DXF header now carries its DIM* variables (AutoCAD draws new and updated dimensions from them); the DWG bridge points $DIMSTYLE at the style by name (its handle was renumbered, so the writer fell back to Standard), and reading a DWG gives the current dimension and text styles their names back. - *D blocks (#1052): lines, arrows, extension lines and text take the style's DIMCLRD/DIMCLRE/DIMCLRT instead of ByBlock. #1054 (group 42 of a rotated linear dimension) was already fixed on main by #855; acad_interop.rs keeps it covered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test: angular dimensions store their angle in radians (group 42) Three-point (60°, 174°) and two-line (120°) angular dimensions, read back from the DWG as DXF. Already right on main since #855; this guards it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Justified text made here: Left Middle and Left Bottom write their baseline start too text_insert kept a stored insertion point that was within 0.1 x an estimated width + 0.5 x height of CADCraft's baseline start, taking it for AutoCAD's own. The width was estimated from the alignment point's offset, which for Left Middle is only h/2 (vertical), so a text made here, whose stored point is the alignment point, passed and kept it: AutoCAD drew it half a height high (Left Bottom: a third). Reported with a test by @lee-cyrille-maskell. A stored point equal to the alignment point is now always stale. Otherwise the offset is split along and across the baseline: along it the same slack as before (a tenth of the text's width plus half its height, as fonts and their substitutes differ most in width), across it a fifth of the height. Multiline attribute values keep the distance-only check, since their lines are placed by the embedded MTEXT. On 12 drawings made in AutoCAD (1,055 justified texts) the insertion points written are the same as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2797 lines
110 KiB
Rust
2797 lines
110 KiB
Rust
//! Drawing → DXF (AC1015 / R2000 structure, readable by every DXF consumer since 2000). Later
|
||
//! versions keep that structure (the groups they add are optional, DXF Reference) and differ in
|
||
//! `$ACADVER`, from 2004 in the newer DIMSTYLE groups, and from 2007 in UTF-8 text (the caller
|
||
//! picks the text encoding) and MULTILEADER entities (older files get LEADER + MTEXT).
|
||
|
||
use std::collections::HashMap;
|
||
|
||
use cadcraft_color::Color;
|
||
use cadcraft_doc::*;
|
||
use cadcraft_dxf::{Tag, Version};
|
||
use cadcraft_geom::{Bounds2, Vec2, Vec3};
|
||
|
||
use crate::dxf_ext::{self, DimVal, K};
|
||
|
||
struct W {
|
||
t: Vec<Tag>,
|
||
next: u64,
|
||
/// Upper-case plot style name → its `ACDBPLACEHOLDER` handle (group 390 of layers and objects).
|
||
pstyles: HashMap<String, String>,
|
||
/// Upper-case layer name → its LAYER table record handle (group 341 of viewports).
|
||
layers: HashMap<String, String>,
|
||
/// Persistent reactors of boundary objects: each source object of an associative hatch →
|
||
/// the hatches (groups `{ACAD_REACTORS 330 …}`), from [`hatch_reactors`].
|
||
reactors: HashMap<Handle, Vec<String>>,
|
||
}
|
||
|
||
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 = self.next.saturating_add(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 {
|
||
/// Multiline styles: MLINESTYLE handles and their dictionary.
|
||
mlines: crate::dxf_mline::Plan,
|
||
/// 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>,
|
||
/// Upper-case linetype name → LTYPE record handle (DIMLTYPE, DIMLTEX1, DIMLTEX2).
|
||
ltypes: 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)>,
|
||
/// Multileader style name → MLEADERSTYLE handle (the first is the fallback).
|
||
mleader_styles: Vec<(String, String)>,
|
||
/// Upper-case block name → BLOCK_RECORD handle (multileader block content).
|
||
block_records: HashMap<String, String>,
|
||
/// Raster images' IMAGEDEF, IMAGEDEF_REACTOR and dictionary handles.
|
||
images: crate::dxf_image::Plan,
|
||
/// The version the file ends up in (the DWG version for a DWG save): it decides which
|
||
/// entity types exist (MULTILEADER from 2007).
|
||
target: Version,
|
||
}
|
||
|
||
impl Ctx {
|
||
/// Whether multileaders are written as MULTILEADER (AutoCAD 2007 / AC1021 and later);
|
||
/// older files get LEADER + MTEXT/INSERT.
|
||
fn native_mleader(&self) -> bool {
|
||
self.target >= Version::R2007
|
||
}
|
||
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::named(&self.table_styles, name)
|
||
}
|
||
fn mleader_style(&self, name: &str) -> String {
|
||
Self::named(&self.mleader_styles, name)
|
||
}
|
||
/// The handle of the style `name` in `styles`, or of the first one.
|
||
fn named(styles: &[(String, String)], name: &str) -> String {
|
||
styles.iter().find(|(n, _)| n.eq_ignore_ascii_case(name)).or_else(|| 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> {
|
||
let ltype = |n: &str| self.ltypes.get(&n.trim().to_ascii_uppercase()).cloned();
|
||
dxf_ext::encode(kind, v, &|n| self.style(n), &|n| self.arrow(n), <ype)
|
||
.filter(|x| !matches!(x, DimVal::Int(i) if i16::try_from(*i).is_err()))
|
||
}
|
||
}
|
||
|
||
/// The persistent reactors of boundary objects: each source object of an associative hatch in
|
||
/// the same space or block → the hatches it bounds (AutoCAD links the two both ways; without the
|
||
/// reactor the hatch no longer follows its boundary there).
|
||
fn hatch_reactors(d: &Drawing) -> HashMap<Handle, Vec<String>> {
|
||
let mut out: HashMap<Handle, Vec<String>> = HashMap::new();
|
||
let stores = std::iter::once(&d.model).chain(d.layouts.iter().map(|l| &l.entities)).chain(d.blocks.values().map(|b| &b.entities));
|
||
for st in stores {
|
||
for e in st.iter() {
|
||
let EntityKind::Hatch(h) = &e.kind else { continue };
|
||
if !h.associative {
|
||
continue;
|
||
}
|
||
for s in h.loops.iter().flat_map(|l| &l.sources) {
|
||
if *s != e.handle && st.contains(*s) {
|
||
let r = out.entry(*s).or_default();
|
||
let own = e.handle.hex();
|
||
if !r.contains(&own) {
|
||
r.push(own);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// `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, pstyles: HashMap::new(), layers: HashMap::new(), reactors: HashMap::new() };
|
||
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));
|
||
}
|
||
ours.extend(dxf_ext::marks_xdata(&dm.marks));
|
||
if !ours.is_empty() {
|
||
w.s(1001, dxf_ext::APP);
|
||
w.xdata(ours);
|
||
}
|
||
}
|
||
|
||
/// Group 52 of a linear or aligned dimension with oblique extension lines: their angle less the
|
||
/// dimension line's (`base`), in degrees.
|
||
fn oblique_52(w: &mut W, dm: &Dimension, base: f64) {
|
||
if let Some(a) = dm.marks.oblique.filter(|a| a.is_finite()) {
|
||
w.f(52, cadcraft_geom::norm_angle(a - base).to_degrees());
|
||
}
|
||
}
|
||
|
||
/// 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),
|
||
("SNAPUNIT", -2),
|
||
("GRIDUNIT", -2),
|
||
("ORTHOMODE", 70),
|
||
("FILLMODE", 70),
|
||
("MIRRTEXT", 70),
|
||
("LTSCALE", 40),
|
||
("ATTMODE", 70),
|
||
("TEXTSIZE", 40),
|
||
("TRACEWID", 40),
|
||
("CMLSTYLE", 2),
|
||
("CMLJUST", 70),
|
||
("CMLSCALE", 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),
|
||
];
|
||
|
||
/// The current dimension style's variables as header variables (`$DIMTXT`, `$DIMDEC`…), after
|
||
/// `$DIMSTYLE`: AutoCAD draws new and updated dimensions with the header's DIM* values, which
|
||
/// otherwise stay its defaults (0.18 text, 4 decimals) whatever the current style says (#1051).
|
||
/// `$DIMSCALE` is a header variable of its own.
|
||
fn dim_header_vars(w: &mut W, d: &Drawing) {
|
||
let name = d.header.str("DIMSTYLE", "Standard");
|
||
let Some(style) = d.dim_styles.iter().find(|s| s.name.eq_ignore_ascii_case(&name)) else { return };
|
||
let v = serde_json::to_value(style).unwrap_or_default();
|
||
let none = |_: &str| None;
|
||
for (var, field) in cadcraft_doc::DIMVARS {
|
||
if *var == "DIMSCALE" {
|
||
continue;
|
||
}
|
||
let (Some(value), Some((_, _, kind))) = (v.get(*field), dxf_ext::DIM_CODES.iter().find(|(f, _, _)| f == field)) else { continue };
|
||
match kind {
|
||
// Names, not handles, in the header.
|
||
K::TextStyle | K::Block => {
|
||
let Some(n) = value.as_str() else { continue };
|
||
if *kind == K::TextStyle && n.trim().is_empty() {
|
||
continue;
|
||
}
|
||
w.s(9, format!("${var}"));
|
||
w.s(if *kind == K::TextStyle { 7 } else { 1 }, n);
|
||
}
|
||
K::Linetype => {}
|
||
_ => match dxf_ext::encode(*kind, value, &none, &none, &none) {
|
||
Some(DimVal::Real(x)) => {
|
||
w.s(9, format!("${var}"));
|
||
w.f(40, x);
|
||
}
|
||
Some(DimVal::Int(i)) if i16::try_from(i).is_ok() => {
|
||
w.s(9, format!("${var}"));
|
||
w.i(70, i);
|
||
}
|
||
Some(DimVal::Str(t)) => {
|
||
w.s(9, format!("${var}"));
|
||
w.s(1, t);
|
||
}
|
||
_ => {}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
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)) => {
|
||
let p = match *name {
|
||
"SNAPUNIT" => d.header.snap_unit().to3(0.0),
|
||
"GRIDUNIT" => d.header.grid_unit().to3(0.0),
|
||
_ => *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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
dim_header_vars(w, d);
|
||
}
|
||
|
||
/// 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());
|
||
let mut reactors: Vec<String> = assoc.map(|a| a.handle.clone()).into_iter().collect();
|
||
reactors.extend(w.reactors.get(&e.handle).into_iter().flatten().cloned());
|
||
if !reactors.is_empty() {
|
||
let rs: Vec<&str> = reactors.iter().map(String::as_str).collect();
|
||
w.group("ACAD_REACTORS", 330, &rs);
|
||
}
|
||
if let Some(a) = assoc {
|
||
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);
|
||
}
|
||
// A named plot style (not ByLayer/ByBlock): its placeholder object.
|
||
if let Some(h) = w.pstyles.get(&e.common.plot_style.trim().to_ascii_uppercase()).cloned() {
|
||
w.s(390, h);
|
||
}
|
||
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)
|
||
}
|
||
|
||
/// A multiline attribute's groups after its own (DXF 2007 and later): attribute type 2 (ATTRIB)
|
||
/// or 4 (ATTDEF), its alignment point, then its text as embedded MTEXT (`101 Embedded Object`).
|
||
/// Only what differs from unrotated text goes in the embedded object, as AutoCAD writes it.
|
||
fn multiline_tags(w: &mut W, a: &Attrib, attdef: bool) {
|
||
let Some(m) = a.multiline_text() else { return };
|
||
w.i(71, if attdef { 4 } else { 2 });
|
||
w.i(72, if attdef { 0 } else { 1 });
|
||
w.p(11, a.text.align_pt.unwrap_or(a.text.insert));
|
||
w.s(101, "Embedded Object");
|
||
w.p(10, m.insert);
|
||
w.f(40, m.height);
|
||
w.f(41, m.width);
|
||
w.f(46, 0.0);
|
||
w.i(71, i64::from(m.attach));
|
||
w.i(72, 5);
|
||
let chars: Vec<char> = crate::dxf_read::mtext_dxf::encode_contents(&m.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, &m.style);
|
||
if m.rotation != 0.0 {
|
||
w.p(11, Vec3::new(m.rotation.cos(), m.rotation.sin(), 0.0));
|
||
}
|
||
w.i(73, if m.line_spacing_exact { 2 } else { 1 });
|
||
w.f(44, m.line_spacing);
|
||
}
|
||
|
||
/// ATTRIB/ATTDEF flags (group 70): 1 invisible, 2 constant, 4 verify, 8 preset.
|
||
fn attrib_flags(a: &Attrib) -> i64 {
|
||
i64::from(a.invisible) | if a.constant { 2 } else { 0 } | if a.modes.verify { 4 } else { 0 } | if a.modes.preset { 8 } else { 0 }
|
||
}
|
||
|
||
/// A TEXT's groups, or an attribute's (`attrib`, in a file of version `target`) with its
|
||
/// subclass: the subclass version (`280 0`, 2010 and later), prompt, tag, flags and alignment,
|
||
/// then Lock position (`280`, 2007 and later; readers take the 280 after the tag as it).
|
||
/// The insertion point (group 10) to write for `t`. A justified text is placed by its alignment
|
||
/// point (group 11), and its insertion point is the start of its baseline, which AutoCAD reads
|
||
/// from DWG as is (it recomputes it only for DXF): writing the alignment point there drew centred
|
||
/// and right-aligned text from the wrong end in AutoCAD (#1050).
|
||
///
|
||
/// The stored point is kept when it is close to where CADCraft's font puts the baseline start:
|
||
/// then it is AutoCAD's own (exact for its fonts). It is replaced when it is clearly stale: the
|
||
/// alignment point itself (text made here) or a text whose length changed since. Close means
|
||
/// along the baseline within a tenth of the text's width plus half its height (AutoCAD's fonts,
|
||
/// or the ones CADCraft substitutes for them, differ most in width, and most on short texts) and
|
||
/// across it within a fifth of its height: Left Middle and Left Bottom text made here, whose
|
||
/// alignment point is only half or a third of a height from the baseline start, must not pass
|
||
/// for AutoCAD's.
|
||
fn text_insert(d: &Drawing, t: &Text) -> Vec3 {
|
||
let computed = computed_text_insert(d, t);
|
||
if t.align_pt.is_some_and(|a| a.xy() == t.insert.xy()) {
|
||
return computed;
|
||
}
|
||
let h = t.height.abs();
|
||
let off = (t.insert.xy() - computed.xy()).rotate(-t.rotation);
|
||
let close = if t.value.contains('\n') {
|
||
// A multiline attribute's lines are placed by its embedded MTEXT: a one-line baseline
|
||
// start says little about them, so only the distance counts.
|
||
let width = t.align_pt.map_or(0.0, |a| 2.0 * a.xy().dist(computed.xy()));
|
||
off.len() <= 0.1 * width + 0.5 * h
|
||
} else {
|
||
let width = cadcraft_fonts::line_width(&t.value, h, t.width_factor.abs());
|
||
off.x.abs() <= 0.1 * width + 0.5 * h && off.y.abs() <= 0.2 * h
|
||
};
|
||
if close { t.insert } else { computed }
|
||
}
|
||
|
||
fn computed_text_insert(d: &Drawing, t: &Text) -> Vec3 {
|
||
let justified = !matches!((t.halign, t.valign), (HAlign::Left, VAlign::Baseline) | (HAlign::Aligned | HAlign::Fit, _));
|
||
let Some(a) = t.align_pt.filter(|_| justified) else { return t.insert };
|
||
let ts = d.text_style(&t.style).cloned().unwrap_or_default();
|
||
let (h, v) = cadcraft_render::text_align(t.halign, t.valign);
|
||
let p = cadcraft_fonts::TextParams {
|
||
insert: t.insert.xy(),
|
||
align_pt: Some(a.xy()),
|
||
height: t.height,
|
||
rotation: t.rotation,
|
||
width_factor: t.width_factor,
|
||
oblique: t.oblique,
|
||
h,
|
||
v,
|
||
backwards: ts.backwards,
|
||
upside_down: ts.upside_down,
|
||
};
|
||
let q = cadcraft_fonts::baseline_start(&cadcraft_fonts::TextFont::resolve(&ts.font), &t.value, &p);
|
||
if q.is_finite() { Vec3::new(q.x, q.y, a.z) } else { t.insert }
|
||
}
|
||
|
||
fn text_tags(w: &mut W, d: &Drawing, t: &Text, attrib: Option<(&Attrib, Version)>, attdef_prompt: Option<&str>) {
|
||
w.p(10, text_insert(d, t));
|
||
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 {
|
||
Some((a, target)) => {
|
||
w.s(100, if attdef_prompt.is_some() { "AcDbAttributeDefinition" } else { "AcDbAttribute" });
|
||
if target >= Version::R2010 {
|
||
w.i(280, 0);
|
||
}
|
||
if let Some(prompt) = attdef_prompt {
|
||
w.s(3, prompt);
|
||
}
|
||
w.s(2, &a.tag);
|
||
w.i(70, attrib_flags(a));
|
||
if v != 0 {
|
||
w.i(74, v);
|
||
}
|
||
if target >= Version::R2007 {
|
||
w.i(280, i64::from(a.modes.lock_position));
|
||
}
|
||
}
|
||
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::MLine(m) => {
|
||
w.s(0, "MLINE");
|
||
common(w, e, owner, paper, "AcDbMline");
|
||
w.t.extend(crate::dxf_mline::entity_tags(m, &cx.mlines.handle(&m.style)));
|
||
}
|
||
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::Helix(h) => {
|
||
// A cubic spline through points on the helix, then the helix data (DXF Reference).
|
||
w.s(0, "HELIX");
|
||
common(w, e, owner, paper, "AcDbSpline");
|
||
let (s, z) = h.spline3();
|
||
w.i(70, 0);
|
||
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, 0);
|
||
w.f(42, 1e-10);
|
||
w.f(43, 1e-10);
|
||
for k in &s.knots {
|
||
w.f(40, *k);
|
||
}
|
||
for (c, z) in s.control.iter().zip(&z) {
|
||
w.p(10, c.to3(*z));
|
||
}
|
||
w.s(100, "AcDbHelix");
|
||
w.i(90, 29);
|
||
w.i(91, 63);
|
||
w.p(10, h.center);
|
||
w.p(11, h.start_point());
|
||
w.p(12, Vec3::Z);
|
||
w.f(40, h.base_radius);
|
||
w.f(41, h.turns);
|
||
w.f(42, h.turn_height());
|
||
w.i(290, i64::from(h.ccw));
|
||
w.i(280, h.constrain.to_dxf());
|
||
}
|
||
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, d, t, None, None);
|
||
}
|
||
// Closed polylines, one per loop (`dxf_region`): REGION's own data is ACIS.
|
||
EntityKind::Region(r) => {
|
||
for (i, tags) in crate::dxf_region::polylines(r).into_iter().enumerate() {
|
||
w.s(0, "LWPOLYLINE");
|
||
if i == 0 {
|
||
common(w, e, owner, paper, "AcDbPolyline");
|
||
} else {
|
||
// The other loops get handles of their own (only the handle and properties are written).
|
||
let h = Handle::parse_hex(&w.h()).unwrap_or(e.handle);
|
||
let part = Entity { handle: h, common: e.common.clone(), kind: EntityKind::Point(Point { p: Vec3::ZERO, angle: 0.0 }) };
|
||
common(w, &part, owner, paper, "AcDbPolyline");
|
||
}
|
||
w.t.extend(tags);
|
||
}
|
||
}
|
||
EntityKind::AttDef(a) => {
|
||
w.s(0, "ATTDEF");
|
||
common(w, e, owner, paper, "AcDbText");
|
||
text_tags(w, d, &a.text, Some((a, cx.target)), Some(&a.prompt));
|
||
if cx.target >= Version::R2007 {
|
||
multiline_tags(w, a, true);
|
||
}
|
||
}
|
||
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> = crate::dxf_read::mtext_dxf::encode_contents(&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);
|
||
w.t.extend(crate::dxf_read::mtext_dxf::column_tags(t));
|
||
}
|
||
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));
|
||
// Hidden with its definition (a dynamic block's visibility state hides it).
|
||
if !base.visible {
|
||
w.i(60, 1);
|
||
}
|
||
w.s(100, "AcDbText");
|
||
text_tags(w, d, &a.text, Some((a, cx.target)), None);
|
||
if cx.target >= Version::R2007 {
|
||
multiline_tags(w, a, false);
|
||
}
|
||
}
|
||
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 });
|
||
// The actual measurement (read-only in DXF): AutoLISP's entget shows it.
|
||
w.f(42, g.measurement);
|
||
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());
|
||
oblique_52(w, dm, rotation);
|
||
w.s(100, "AcDbRotatedDimension");
|
||
}
|
||
DimKind::Aligned | DimKind::ArcLength => {
|
||
w.s(100, "AcDbAlignedDimension");
|
||
w.p(13, dm.p13);
|
||
w.p(14, dm.p14);
|
||
if matches!(dm.kind, DimKind::Aligned) {
|
||
oblique_52(w, dm, (dm.p14.xy() - dm.p13.xy()).angle());
|
||
}
|
||
}
|
||
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);
|
||
if let Some(h) = cell.and_then(|x| x.text_height).filter(|h| h.is_finite() && *h > 0.0) {
|
||
w.f(140, h);
|
||
}
|
||
// 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);
|
||
for c in crate::dxf_table::table_xdata(t) {
|
||
w.s(1000, c);
|
||
}
|
||
}
|
||
EntityKind::MLeader(m) if !cx.native_mleader() => mleader_fallback(w, d, e, m, owner, paper, cx),
|
||
EntityKind::MLeader(m) => {
|
||
w.s(0, "MULTILEADER");
|
||
common(w, e, owner, paper, "AcDbMLeader");
|
||
let refs = crate::dxf_mleader::Refs {
|
||
style: cx.mleader_style(&m.style),
|
||
text_style: m.text.as_ref().and_then(|t| cx.style(&t.style)),
|
||
block: m.block.as_ref().and_then(|b| cx.block_records.get(&b.block.to_ascii_uppercase()).cloned()),
|
||
};
|
||
w.t.extend(crate::dxf_mleader::entity_tags(d, m, &refs));
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
// Source boundary objects of an associative hatch (the ones still beside it, in
|
||
// its space or block, which carry the hatch as a reactor).
|
||
let own = e.handle.hex();
|
||
let sources: Vec<String> =
|
||
l.sources.iter().filter(|s| h.associative && w.reactors.get(*s).is_some_and(|r| r.contains(&own))).map(|s| s.hex()).collect();
|
||
w.i(97, sources.len() as i64);
|
||
for s in &sources {
|
||
w.s(330, s);
|
||
}
|
||
}
|
||
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);
|
||
// The lines read from the file, else the library's pattern of that name.
|
||
let lines = if h.pattern_lines.is_empty() {
|
||
cadcraft_doc::library::pattern_for(d, &h.pattern).map(|p| p.lines).unwrap_or_default()
|
||
} 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), 0 off.
|
||
w.i(68, if v.on { i64::from(v.id.clamp(1, 32767)) } else { 0 });
|
||
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, 65536 = clipped by the
|
||
// 340 entity, 131072 = off.
|
||
let clip = v.clip.filter(|h| *h != e.handle && d.entity(*h).is_some());
|
||
let flags = 32768 | if v.locked { 16384 } else { 0 } | if clip.is_some() { 65536 } else { 0 } | if v.on { 0 } else { 131072 };
|
||
w.i(90, flags);
|
||
if let Some(h) = clip {
|
||
w.s(340, h.hex());
|
||
}
|
||
// AutoCAD's frozen-layer list: one soft pointer per layer, to its LAYER record. Other
|
||
// programs read only this; CADCraft prefers its own xdata below when present.
|
||
for name in &v.frozen_layers {
|
||
if let Some(h) = w.layers.get(&name.to_ascii_uppercase()).cloned() {
|
||
w.s(341, h);
|
||
}
|
||
}
|
||
// 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));
|
||
}
|
||
}
|
||
EntityKind::Tolerance(t) => {
|
||
w.s(0, "TOLERANCE");
|
||
common(w, e, owner, paper, "AcDbFcf");
|
||
w.s(3, &t.style);
|
||
w.p(10, t.insert);
|
||
w.s(1, &t.text);
|
||
w.p(11, t.dir);
|
||
}
|
||
// Entity types CADCraft doesn't model aren't written; the save command reports them.
|
||
EntityKind::Unknown(_) => {}
|
||
}
|
||
}
|
||
|
||
/// `$DWGCODEPAGE`: before 2007 the code page read from the file when CADCraft knows it (the text
|
||
/// is ASCII with `\U+XXXX` escapes, right in any code page, but the file keeps its declared one);
|
||
/// otherwise, and from 2007 (UTF-8 text), `ANSI_1252`.
|
||
fn code_page(d: &Drawing, version: Version) -> String {
|
||
let read = d.header.get("DWGCODEPAGE").and_then(HVal::as_str).map(str::trim).unwrap_or_default();
|
||
let known = read.is_ascii() && read.len() <= 32 && cadcraft_dxf::decode_code_page(b"", read).is_some();
|
||
if version < Version::R2007 && known { read.to_string() } else { "ANSI_1252".into() }
|
||
}
|
||
|
||
/// A multileader for a file older than 2007 (no MULTILEADER): a LEADER per leader line, ending
|
||
/// at the landing, plus its MTEXT or its block's INSERT, with the multileader's properties.
|
||
fn mleader_fallback(w: &mut W, d: &Drawing, e: &Entity, m: &MLeader, owner: &str, paper: bool, cx: &Ctx) {
|
||
let part = |w: &mut W, kind: EntityKind| Entity { handle: Handle::parse_hex(&w.h()).unwrap_or(Handle(0)), common: e.common.clone(), kind };
|
||
let style = d.header.str("DIMSTYLE", "Standard");
|
||
for arm in m.leaders.iter().take(64) {
|
||
let mut vertices = arm.clone();
|
||
vertices.push(m.landing);
|
||
let le = part(w, EntityKind::Leader(Leader { vertices, arrow: true, spline: m.spline, style: style.clone() }));
|
||
entity(w, d, &le, owner, paper, cx);
|
||
}
|
||
if let Some(t) = &m.text {
|
||
let te = part(w, EntityKind::MText(t.clone()));
|
||
entity(w, d, &te, owner, paper, cx);
|
||
}
|
||
if let Some(ins) = &m.block {
|
||
let ie = part(w, EntityKind::Insert(ins.clone()));
|
||
entity(w, d, &ie, owner, paper, cx);
|
||
}
|
||
}
|
||
|
||
/// 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());
|
||
}
|
||
|
||
/// What happens to the anonymous `*D` blocks read from a file (#495): a dimension that still
|
||
/// names its block regenerates its geometry under that name, keeping the block's entity
|
||
/// handles, and `*D` blocks nothing else references are not written again.
|
||
struct DimBlockPlan {
|
||
/// Dimension (model or paper space) → the read block whose name and handles it reuses.
|
||
reuse: HashMap<Handle, std::sync::Arc<Block>>,
|
||
/// Upper-case names of `*D` blocks not written as ordinary blocks.
|
||
skip: std::collections::HashSet<String>,
|
||
}
|
||
|
||
fn is_dim_block(name: &str) -> bool {
|
||
name.get(..2).is_some_and(|p| p.eq_ignore_ascii_case("*D"))
|
||
}
|
||
|
||
fn dim_block_plan(d: &Drawing) -> DimBlockPlan {
|
||
let spaces: Vec<&EntityStore> = std::iter::once(&d.model).chain(d.layouts.iter().map(|l| &l.entities)).collect();
|
||
// Block names other objects use: inserts and multileader contents anywhere, and dimensions
|
||
// inside block definitions (their blocks are written as read).
|
||
let mut used = std::collections::HashSet::new();
|
||
let note = |e: &Entity, dims: bool, used: &mut std::collections::HashSet<String>| match &e.kind {
|
||
EntityKind::Insert(i) => {
|
||
used.insert(i.block.to_ascii_uppercase());
|
||
}
|
||
EntityKind::MLeader(m) => used.extend(m.block.as_ref().map(|b| b.block.to_ascii_uppercase())),
|
||
EntityKind::Dimension(dm) if dims => used.extend(dm.block.as_ref().map(|n| n.to_ascii_uppercase())),
|
||
_ => {}
|
||
};
|
||
for e in spaces.iter().flat_map(|s| s.iter()) {
|
||
note(e, false, &mut used);
|
||
}
|
||
for e in d.blocks.values().flat_map(|b| b.entities.iter()) {
|
||
note(e, true, &mut used);
|
||
}
|
||
let skip = d.blocks.keys().filter(|n| is_dim_block(n)).map(|n| n.to_ascii_uppercase()).filter(|n| !used.contains(n)).collect();
|
||
let mut plan = DimBlockPlan { reuse: HashMap::new(), skip };
|
||
let mut claimed = std::collections::HashSet::new();
|
||
for e in spaces.iter().flat_map(|s| s.iter()) {
|
||
let EntityKind::Dimension(dm) = &e.kind else { continue };
|
||
let Some(b) = dm.block.as_deref().filter(|n| is_dim_block(n)).and_then(|n| d.block(n)) else { continue };
|
||
let key = b.name.to_ascii_uppercase();
|
||
if plan.skip.contains(&key) && claimed.insert(key) {
|
||
plan.reuse.insert(e.handle, b.clone());
|
||
}
|
||
}
|
||
plan
|
||
}
|
||
|
||
/// Give regenerated dimension geometry the handles of the block it replaces, in order; the rest
|
||
/// keep handle 0 and get fresh ones.
|
||
fn reuse_handles(ents: &mut [Entity], old: &Block, taken: &mut std::collections::HashSet<Handle>) {
|
||
let mut handles = old.entities.iter().map(|e| e.handle).filter(|h| h.0 != 0);
|
||
for e in ents.iter_mut() {
|
||
match handles.by_ref().find(|h| taken.insert(*h)) {
|
||
Some(h) => e.handle = h,
|
||
None => break,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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();
|
||
// Each part in its style colour (DIMCLRD, DIMCLRE, DIMCLRT), ByBlock when the style says so,
|
||
// as AutoCAD writes them: readers that draw the block drew every part ByBlock, i.e. in the
|
||
// dimension's own colour (#1052).
|
||
let in_color = |color: Color| Common { layer: layer.into(), color, ..Common::default() };
|
||
let (dim_c, ext_c, text_c) = (in_color(style.dim_line_color), in_color(style.ext_line_color), in_color(style.text_color));
|
||
for (i, l) in g.lines.iter().enumerate() {
|
||
let c = match g.line_roles.get(i) {
|
||
Some(cadcraft_render::LineRole::Ext) => &ext_c,
|
||
_ => &dim_c,
|
||
};
|
||
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: dim_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: text_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,
|
||
columns: None,
|
||
}),
|
||
});
|
||
}
|
||
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 fills, borders and texts as its cell styles draw
|
||
/// them, for readers that only draw the block.
|
||
fn table_block_entities(d: &Drawing, t: &Table, layer: &str) -> Vec<Entity> {
|
||
let g = cadcraft_render::table_geometry(d, t);
|
||
let c = Common { layer: layer.into(), color: Color::ByBlock, ..Common::default() };
|
||
let mut out = Vec::new();
|
||
for f in &g.fills {
|
||
let [a, b, cc, dd] = f.corners.map(|p| p.to3(0.0));
|
||
// SOLID corners go 1, 2, 4, 3.
|
||
out.push(Entity {
|
||
handle: Handle(0),
|
||
common: Common { color: f.color, ..c.clone() },
|
||
kind: EntityKind::Solid(Solid { corners: [a, b, dd, cc] }),
|
||
});
|
||
}
|
||
for l in &g.lines {
|
||
out.push(Entity {
|
||
handle: Handle(0),
|
||
common: Common { color: l.color, lineweight: l.lineweight, ..c.clone() },
|
||
kind: EntityKind::Line(Line { a: l.a.to3(0.0), b: l.b.to3(0.0) }),
|
||
});
|
||
}
|
||
let mut texts = Vec::new();
|
||
for cell in &g.texts {
|
||
if !cell.text.is_empty() {
|
||
let style = if d.text_style(&cell.style).is_some() { cell.style.clone() } else { "Standard".into() };
|
||
texts.push(Entity {
|
||
handle: Handle(0),
|
||
common: Common { color: cell.color, ..c.clone() },
|
||
kind: EntityKind::MText(MText {
|
||
insert: cell.at.to3(0.0),
|
||
height: cell.height,
|
||
width: 0.0,
|
||
attach: cell.attach,
|
||
rotation: 0.0,
|
||
style,
|
||
contents: cell.text.clone(),
|
||
line_spacing: 1.0,
|
||
line_spacing_exact: false,
|
||
columns: None,
|
||
}),
|
||
});
|
||
}
|
||
}
|
||
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);
|
||
}
|
||
|
||
/// The VPORT table: the `*Active` model space tiles (one viewport on the extents when there is no
|
||
/// tiled configuration) and the saved configurations, each tile a record, the current one first.
|
||
fn vport_table(w: &mut W, d: &Drawing, ext: Bounds2) {
|
||
let single = VportTile {
|
||
min: Vec2::ZERO,
|
||
max: Vec2::new(1.0, 1.0),
|
||
center: if ext.is_empty() { Vec2::new(6.0, 4.5) } else { ext.center() },
|
||
height: if ext.is_empty() { 9.0 } else { (ext.height() * 1.1).max(1e-6) },
|
||
aspect: 1.6,
|
||
};
|
||
let tiled = d.vports.iter().find(|c| c.is_active() && c.tiles.len() > 1);
|
||
let mut records: Vec<(&str, &VportTile)> = match tiled {
|
||
Some(c) => c.tiles.iter().map(|t| (VportConfig::ACTIVE, t)).collect(),
|
||
None => vec![(VportConfig::ACTIVE, &single)],
|
||
};
|
||
records.extend(d.vports.iter().filter(|c| !c.is_active() && !c.name.is_empty()).flat_map(|c| c.tiles.iter().map(|t| (c.name.as_str(), t))));
|
||
let th = table_head(w, "VPORT", records.len());
|
||
let finite = |p: Vec2, or: Vec2| if p.is_finite() { p } else { or };
|
||
for (name, t) in records {
|
||
record_head(w, "VPORT", &th, "AcDbViewportTableRecord");
|
||
w.s(2, name);
|
||
w.i(70, 0);
|
||
w.p2(10, finite(t.min, Vec2::ZERO));
|
||
w.p2(11, finite(t.max, Vec2::new(1.0, 1.0)));
|
||
w.p2(12, finite(t.center, Vec2::ZERO));
|
||
// 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, d.header.snap_unit());
|
||
w.p2(15, d.header.grid_unit());
|
||
w.p(16, Vec3::Z);
|
||
w.p(17, Vec3::ZERO);
|
||
w.f(40, if t.height.is_finite() && t.height > 0.0 { t.height } else { 1.0 });
|
||
w.f(41, if t.aspect.is_finite() && t.aspect > 0.0 { t.aspect } else { 1.0 });
|
||
w.f(42, 50.0);
|
||
w.f(50, d.header.f64("SNAPANG", 0.0));
|
||
}
|
||
w.s(0, "ENDTAB");
|
||
}
|
||
|
||
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.
|
||
/// The drawing as R2000 ASCII DXF.
|
||
pub fn write(d: &Drawing) -> String {
|
||
cadcraft_dxf::write_ascii(&tags(d, Version::R2000))
|
||
}
|
||
|
||
/// The drawing's DXF tags, declaring `version` in `$ACADVER`.
|
||
pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
|
||
tags_for(d, version, version)
|
||
}
|
||
|
||
/// 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 {
|
||
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
|
||
/// file ends up in: `version`, or the DWG version the bridge writes) has.
|
||
fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
|
||
// An in-place block edit in progress is not written: the drawing as before it.
|
||
let committed = d.committed();
|
||
let d: &Drawing = &committed;
|
||
// Structural handles start above every entity handle.
|
||
let mut w = W {
|
||
t: Vec::new(),
|
||
next: d.handseed.max(0x100).saturating_add(0x1000),
|
||
pstyles: HashMap::new(),
|
||
layers: HashMap::new(),
|
||
reactors: hatch_reactors(d),
|
||
};
|
||
// 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();
|
||
// Xrefs are saved as references: without their objects and the symbols that came with them.
|
||
let saved_view = crate::dxf_xref::saved_view(d);
|
||
let d = saved_view.as_ref().unwrap_or(d);
|
||
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 dim_plan = dim_block_plan(d);
|
||
let mut user_blocks: Vec<(String, String, &Block)> = d
|
||
.blocks
|
||
.values()
|
||
.filter(|b| !dim_plan.skip.contains(&b.name.to_ascii_uppercase()))
|
||
.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), target, ..Ctx::default() };
|
||
cx.block_records = user_blocks.iter().map(|(n, h, _)| (n.to_ascii_uppercase(), h.clone())).collect();
|
||
// Dimension blocks.
|
||
let mut dim_defs: Vec<(String, String, Vec<Entity>)> = Vec::new(); // filled below
|
||
let mut n = 1;
|
||
let mut reused_handles = std::collections::HashSet::new();
|
||
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 reused = dim_plan.reuse.get(&e.handle);
|
||
let ents: Vec<Entity>;
|
||
let name = match reused {
|
||
// A dimension still linked to the block it was read with hasn't changed since
|
||
// (edits drop the link, and the screen draws that block): the block is written
|
||
// as read, so other programs show it as they drew it.
|
||
Some(b) if !b.entities.is_empty() => {
|
||
ents = b.entities.iter().map(|x| x.as_ref().clone()).collect();
|
||
reused_handles.extend(ents.iter().map(|x| x.handle).filter(|h| h.0 != 0));
|
||
b.name.clone()
|
||
}
|
||
Some(b) => {
|
||
let mut fresh = dim_block_entities(d, dm, &e.common.layer);
|
||
reuse_handles(&mut fresh, b, &mut reused_handles);
|
||
ents = fresh;
|
||
b.name.clone()
|
||
}
|
||
None => {
|
||
ents = dim_block_entities(d, dm, &e.common.layer);
|
||
let mut name = format!("*D{n}");
|
||
while d.block(&name).is_some() {
|
||
n += 1;
|
||
name = format!("*D{n}");
|
||
}
|
||
n += 1;
|
||
name
|
||
}
|
||
};
|
||
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(), s.leader_arrow_block.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") || *f == "leaderArrowBlock") 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());
|
||
cx.mlines = crate::dxf_mline::plan(d, 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 default_mleader_style = [MLeaderStyle::default()];
|
||
let mleader_styles: &[MLeaderStyle] = if d.mleader_styles.is_empty() { &default_mleader_style } else { &d.mleader_styles };
|
||
cx.mleader_styles = mleader_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();
|
||
// The drawing's own next handle: table records and objects are numbered above it, so
|
||
// reading it back (instead of $HANDSEED) keeps their handles across saves (#495).
|
||
xrecords.push((dxf_ext::HANDSEED_KEY, w.h(), vec![format!("{:X}", d.handseed)]));
|
||
if let Some(name) = d.header.get("CMLEADERSTYLE").and_then(HVal::as_str) {
|
||
xrecords.push((dxf_ext::MLEADER_STYLE_KEY, w.h(), vec![name.to_string()]));
|
||
}
|
||
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));
|
||
}
|
||
if let Some(chunks) = dxf_ext::plot_style_chunks(&d.plot_style_tables) {
|
||
xrecords.push((dxf_ext::PLOT_STYLES_KEY, w.h(), chunks));
|
||
}
|
||
if let Some(chunks) = dxf_ext::anno_chunks(d) {
|
||
xrecords.push((dxf_ext::ANNOTATION_KEY, w.h(), chunks));
|
||
}
|
||
// The annotation scale list: the ACAD_SCALELIST dictionary of SCALE objects.
|
||
let scale_dict = w.h();
|
||
let scales: Vec<(&cadcraft_doc::AnnoScale, String)> = d.scales.iter().filter(|s| s.is_valid()).map(|s| (s, w.h())).collect();
|
||
// Plot style names used by layers and objects (Normal always), each a placeholder object.
|
||
let pstyle_dict = w.h();
|
||
let mut pstyle_names: Vec<String> = vec![cadcraft_doc::NORMAL_STYLE.to_string()];
|
||
let named = d.layers.iter().map(|l| l.plot_style.as_str()).chain(every.iter().map(|e| e.common.plot_style.as_str()));
|
||
for n in named {
|
||
let n = n.trim();
|
||
let logical = n.is_empty() || n.eq_ignore_ascii_case("bylayer") || n.eq_ignore_ascii_case("byblock");
|
||
if !logical && pstyle_names.len() < dxf_ext::MAX_PLOT_STYLE_NAMES && !pstyle_names.iter().any(|x| x.eq_ignore_ascii_case(n)) {
|
||
pstyle_names.push(n.to_string());
|
||
}
|
||
}
|
||
let pstyles: Vec<(String, String)> = pstyle_names.into_iter().map(|n| (n, w.h())).collect();
|
||
w.pstyles = pstyles.iter().map(|(n, h)| (n.to_ascii_uppercase(), h.clone())).collect();
|
||
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, version.code());
|
||
w.s(9, "$DWGCODEPAGE");
|
||
w.s(3, code_page(d, version));
|
||
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);
|
||
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),
|
||
("ACDBDICTIONARYWDFLT", "AcDbDictionaryWithDefault", 0, false),
|
||
("ACDBPLACEHOLDER", "AcDbPlaceHolder", 0, false),
|
||
];
|
||
if !cx.tables.is_empty() {
|
||
classes.push(("ACAD_TABLE", "AcDbTable", 1025, true));
|
||
}
|
||
if every.iter().any(|e| matches!(e.kind, EntityKind::Helix(_))) {
|
||
classes.push(("HELIX", "AcDbHelix", 4095, true));
|
||
}
|
||
if !cx.assoc.is_empty() {
|
||
classes.push(("DIMASSOC", "AcDbDimAssoc", 0, false));
|
||
}
|
||
if cx.native_mleader() && every.iter().any(|e| matches!(e.kind, EntityKind::MLeader(_))) {
|
||
classes.push(("MULTILEADER", "AcDbMLeader", 1025, true));
|
||
}
|
||
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));
|
||
}
|
||
classes.push(("SCALE", "AcDbScale", 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_table(&mut w, d, ext);
|
||
// 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.clone());
|
||
cx.ltypes.entry(lt.name.to_ascii_uppercase()).or_insert(h);
|
||
w.s(2, <.name);
|
||
w.i(70, if crate::dxf_xref::dependent(<.name) { 48 } else { 0 });
|
||
w.s(3, <.description);
|
||
w.i(72, 65);
|
||
w.i(73, lt.pattern.len() as i64);
|
||
w.f(40, lt.pattern_length());
|
||
for el in <.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 {
|
||
let lh = record_head(&mut w, "LAYER", &th, "AcDbLayerTableRecord");
|
||
w.layers.insert(l.name.to_ascii_uppercase(), lh);
|
||
w.s(2, &l.name);
|
||
// 16 | 32: an xref's layer (`xref|name`), externally dependent and resolved.
|
||
let xref = if crate::dxf_xref::dependent(&l.name) { 48 } else { 0 };
|
||
w.i(70, i64::from(l.frozen) | if l.vp_freeze_new { 2 } else { 0 } | if l.locked { 4 } else { 0 } | xref);
|
||
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()));
|
||
let normal = w.pstyles.get(&cadcraft_doc::NORMAL_STYLE.to_ascii_uppercase()).cloned();
|
||
if let Some(h) = w.pstyles.get(&l.plot_style.trim().to_ascii_uppercase()).cloned().or(normal) {
|
||
w.s(390, h);
|
||
}
|
||
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 <ype_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, i64::from(v.layout.is_some()));
|
||
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, if v.twist.is_finite() { v.twist.to_degrees() } else { 0.0 });
|
||
w.i(71, 0);
|
||
w.i(281, 0);
|
||
w.i(72, 0);
|
||
let xdata = [("LAYERSTATE", v.layer_state.as_deref()), ("LAYOUT", v.layout.as_deref().filter(|l| !l.is_empty()))];
|
||
if xdata.iter().any(|(_, x)| x.is_some()) {
|
||
w.s(1001, dxf_ext::APP);
|
||
for (key, value) in xdata {
|
||
if let Some(value) = value {
|
||
w.s(1000, key);
|
||
w.s(1000, dxf_ext::xdata_str(value));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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();
|
||
let defaults = serde_json::to_value(DimStyle::default()).unwrap_or_default();
|
||
let mut newer = W { t: Vec::new(), next: 0, pstyles: HashMap::new(), layers: HashMap::new(), reactors: HashMap::new() };
|
||
for (field, code, kind) in dxf_ext::DIM_CODES {
|
||
let Some(enc) = v.get(*field).and_then(|x| cx.encode(*kind, x)) else { continue };
|
||
// DIMPOST is written even when empty: a reader that defaults a missing one to "<>"
|
||
// (the DWG bridge's) would otherwise give radius and diameter dimensions no "R" or
|
||
// "Ø" in AutoCAD (#1053).
|
||
if *code == dxf_ext::DIMPOST
|
||
&& let DimVal::Str(t) = &enc
|
||
&& t.is_empty()
|
||
{
|
||
w.s(*code, "");
|
||
continue;
|
||
}
|
||
if !dxf_ext::NEWER_CODES.contains(code) {
|
||
w.dimval(*code, enc, false);
|
||
continue;
|
||
}
|
||
// Table groups where the file version has them, else xdata.
|
||
if version >= dxf_ext::NEWER_SINCE {
|
||
w.dimval(*code, enc, false);
|
||
} else if v.get(*field) != defaults.get(*field) {
|
||
newer.i(1070, i64::from(*code));
|
||
newer.dimval(*code, enc, true);
|
||
}
|
||
}
|
||
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);
|
||
}
|
||
if !newer.t.is_empty() {
|
||
w.s(1001, dxf_ext::APP);
|
||
w.s(1000, dxf_ext::DIMVARS_LIST);
|
||
w.s(1002, "{");
|
||
w.xdata(newer.t);
|
||
w.s(1002, "}");
|
||
}
|
||
}
|
||
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));
|
||
if let Some(a) = &b.array {
|
||
w.s(1001, dxf_ext::APP);
|
||
w.xdata(dxf_ext::array_xdata(a));
|
||
}
|
||
}
|
||
};
|
||
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().filter(|e| e.handle.0 == 0) {
|
||
e.handle = Handle(w.next);
|
||
w.next = w.next.saturating_add(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 mleader_dict = w.h();
|
||
let mleader_handles: Vec<String> = cx.mleader_styles.iter().map(|(_, h)| h.clone()).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_MLINESTYLE");
|
||
w.s(350, cx.mlines.dict.clone());
|
||
w.s(3, dxf_ext::PLOT_STYLE_NAMES_KEY);
|
||
w.s(350, pstyle_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(3, "ACAD_SCALELIST");
|
||
w.s(350, scale_dict.clone());
|
||
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));
|
||
}
|
||
// Multiline styles.
|
||
w.t.extend(crate::dxf_mline::objects(&cx.mlines, &root_dict));
|
||
// 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, d, s, h, &table_style_dict);
|
||
}
|
||
// Plot style names: a dictionary with a default (Normal) of placeholder objects.
|
||
w.s(0, "ACDBDICTIONARYWDFLT");
|
||
w.s(5, pstyle_dict.clone());
|
||
w.group("ACAD_REACTORS", 330, &[&root_dict]);
|
||
w.s(330, root_dict.clone());
|
||
w.s(100, "AcDbDictionary");
|
||
w.i(281, 1);
|
||
for (n, h) in &pstyles {
|
||
w.s(3, n);
|
||
w.s(350, h);
|
||
}
|
||
w.s(100, "AcDbDictionaryWithDefault");
|
||
w.s(340, pstyles.first().map(|(_, h)| h.clone()).unwrap_or_default());
|
||
for (_, h) in &pstyles {
|
||
w.s(0, "ACDBPLACEHOLDER");
|
||
w.s(5, h);
|
||
w.group("ACAD_REACTORS", 330, &[&pstyle_dict]);
|
||
w.s(330, pstyle_dict.clone());
|
||
}
|
||
// Parametric constraints and parameters, saved layer states, plot style tables (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());
|
||
}
|
||
}
|
||
scale_list(&mut w, &scales, &scale_dict, &root_dict);
|
||
// 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, "}");
|
||
}
|
||
};
|
||
// Model space has no page setup of its own: write what PLOT does for it, the drawing extents
|
||
// fitted to the paper (plot type 1, scaled to fit). The default page's "layout" area is
|
||
// plot type 5, "Layout information" (DXF Reference, PLOTSETTINGS group 74), which means
|
||
// nothing for model space: another program's Plot dialog then shows no plot area or scale.
|
||
let model_page = PageSetup { plot_area: "extents".into(), scale_to_fit: true, ..PageSetup::default() };
|
||
layout_obj(&mut w, &model_layout, "Model", 0, &ms_br, 1, &model_page, 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);
|
||
}
|
||
crate::dxf_raw::finish(&mut w.t, d);
|
||
crate::dxf_raw::data_store(&mut w.t, d, target >= Version::R2013);
|
||
add_class_instance_counts(&mut w.t);
|
||
mark_xrefs(&mut w.t, d);
|
||
w.t
|
||
}
|
||
|
||
/// Write DXF Reference group 91 on every CLASS record. Count records after `dxf_raw::finish`
|
||
/// restores foreign classes and objects so every class gets its actual instance count.
|
||
fn add_class_instance_counts(tags: &mut Vec<Tag>) {
|
||
let mut counts: HashMap<String, i64> = HashMap::new();
|
||
for t in tags.iter().filter(|t| t.code == 0) {
|
||
let kind = t.str();
|
||
if kind != "CLASS" {
|
||
*counts.entry(kind.to_ascii_uppercase()).or_default() += 1;
|
||
}
|
||
}
|
||
|
||
let mut start = 0;
|
||
while start < tags.len() {
|
||
if tags.get(start).is_none_or(|t| t.code != 0 || t.str() != "CLASS") {
|
||
start += 1;
|
||
continue;
|
||
}
|
||
let end = tags[start + 1..].iter().position(|t| t.code == 0).map_or(tags.len(), |i| start + 1 + i);
|
||
let name = tags[start + 1..end].iter().find(|t| t.code == 1).map(Tag::str).unwrap_or_default().to_ascii_uppercase();
|
||
let count = counts.get(&name).copied().unwrap_or(0);
|
||
let inserted = if let Some(i) = tags[start + 1..end].iter().position(|t| t.code == 91) {
|
||
tags[start + 1 + i] = Tag::i(91, count);
|
||
false
|
||
} else if let Some(i) = tags[start + 1..end].iter().position(|t| t.code == 90) {
|
||
tags.insert(start + 2 + i, Tag::i(91, count));
|
||
true
|
||
} else {
|
||
false
|
||
};
|
||
// Inserting the group shifts the next record's start by one.
|
||
start = end + usize::from(inserted);
|
||
}
|
||
}
|
||
|
||
/// 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) {
|
||
// Overlays add bit 8.
|
||
let xrefs: HashMap<String, (&str, i64)> = d
|
||
.blocks
|
||
.values()
|
||
.filter_map(|b| Some((b.name.to_ascii_uppercase(), (b.xref_path.as_deref()?, if b.xref_overlay { 12 } else { 4 }))))
|
||
.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, i64)>> = 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((_, flags)))) => *t = Tag::i(70, t.i64() | flags),
|
||
(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, d: &Drawing, 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, column header and title cell styles; CADCraft's complete cell styles in xdata.
|
||
for kind in crate::dxf_table::CELL_TYPES {
|
||
let cs = s.cell_style(kind);
|
||
let style = if d.text_style(&cs.text_style).is_some() { cs.text_style.clone() } else { "Standard".to_string() };
|
||
w.t.extend(crate::dxf_table::cell_type_tags(&cs, &style));
|
||
}
|
||
let x = crate::dxf_table::cell_styles_xdata(s);
|
||
if !x.is_empty() {
|
||
w.s(1001, dxf_ext::APP);
|
||
for c in x {
|
||
w.s(1000, c);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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, i64::from(s.annotative));
|
||
w.f(143, 0.125);
|
||
w.i(271, 0);
|
||
w.i(272, 9);
|
||
w.i(273, 9);
|
||
}
|
||
|
||
/// The ACAD_SCALELIST dictionary and its SCALE objects (DXF Reference, OBJECTS section): name
|
||
/// (300), paper units (140), drawing units (141), unit scale flag (290).
|
||
fn scale_list(w: &mut W, scales: &[(&cadcraft_doc::AnnoScale, String)], dict: &str, root: &str) {
|
||
w.s(0, "DICTIONARY");
|
||
w.s(5, dict);
|
||
w.group("ACAD_REACTORS", 330, &[root]);
|
||
w.s(330, root);
|
||
w.s(100, "AcDbDictionary");
|
||
w.i(281, 1);
|
||
for (i, (_, h)) in scales.iter().enumerate() {
|
||
w.s(3, format!("S{i}"));
|
||
w.s(350, h.clone());
|
||
}
|
||
for (s, h) in scales {
|
||
w.s(0, "SCALE");
|
||
w.s(5, h.clone());
|
||
w.group("ACAD_REACTORS", 330, &[dict]);
|
||
w.s(330, dict);
|
||
w.s(100, "AcDbScale");
|
||
w.i(70, 0);
|
||
w.s(300, dxf_ext::xdata_str(&s.name));
|
||
w.f(140, s.paper_units);
|
||
w.f(141, s.drawing_units);
|
||
w.i(290, i64::from(cadcraft_doc::same_factor(s.factor(), 1.0)));
|
||
}
|
||
}
|
||
|
||
fn cadcraft_fonts_name() -> &'static str {
|
||
"CADCraft Stroke"
|
||
}
|
||
|
||
/// One entity's DXF groups, as the writer stores them, without its `0` group: its type name
|
||
/// and the groups of its record (an entity written as several records, a POLYLINE and its
|
||
/// VERTEXes, gives its first). Handles of objects outside the entity (owner, styles) are left
|
||
/// out. AutoLISP's `entget` is built on it.
|
||
pub fn entity_groups(d: &Drawing, e: &Entity) -> (String, Vec<Tag>) {
|
||
let mut w = W {
|
||
t: Vec::new(),
|
||
next: d.handseed.max(0x100).saturating_add(0x1000),
|
||
pstyles: HashMap::new(),
|
||
layers: HashMap::new(),
|
||
reactors: HashMap::new(),
|
||
};
|
||
let cx = Ctx { target: Version::R2018, ..Ctx::default() };
|
||
let paper = d.space_of(e.handle).is_some_and(|s| !matches!(s, cadcraft_doc::Space::Model));
|
||
entity(&mut w, d, e, "0", paper, &cx);
|
||
let mut tags = w.t.into_iter();
|
||
let kind = tags.next().filter(|t| t.code == 0).map(|t| t.value.as_str()).unwrap_or_default();
|
||
(kind, tags.take_while(|t| t.code != 0).filter(|t| t.code != 330).collect())
|
||
}
|