mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-26 20:06:50 -06:00
refactor(io): move dxf_read.rs items to dxf_read/
This commit is contained in:
+18
-2378
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
use super::*;
|
||||
|
||||
/// Value lookup helpers over an entity's tags.
|
||||
pub(super) struct T<'a>(pub(super) &'a [Tag]);
|
||||
|
||||
impl T<'_> {
|
||||
pub(super) fn f(&self, code: i32) -> Option<f64> {
|
||||
self.0.iter().find(|t| t.code == code).map(Tag::f64)
|
||||
}
|
||||
pub(super) fn fd(&self, code: i32, d: f64) -> f64 {
|
||||
self.f(code).unwrap_or(d)
|
||||
}
|
||||
pub(super) fn i(&self, code: i32) -> Option<i64> {
|
||||
self.0.iter().find(|t| t.code == code).map(Tag::i64)
|
||||
}
|
||||
pub(super) fn s(&self, code: i32) -> Option<String> {
|
||||
self.0.iter().find(|t| t.code == code).map(Tag::str)
|
||||
}
|
||||
pub(super) fn p(&self, code: i32) -> Vec3 {
|
||||
Vec3::new(self.fd(code, 0.0), self.fd(code + 10, 0.0), self.fd(code + 20, 0.0))
|
||||
}
|
||||
pub(super) fn all_f(&self, code: i32) -> Vec<f64> {
|
||||
self.0.iter().filter(|t| t.code == code).map(Tag::f64).collect()
|
||||
}
|
||||
pub(super) fn pts(&self, code: i32) -> Vec<Vec2> {
|
||||
let xs = self.all_f(code);
|
||||
let ys = self.all_f(code + 10);
|
||||
xs.into_iter().zip(ys).map(|(x, y)| Vec2::new(x, y)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// An entity's group 440: `0x01` in the top byte is ByBlock, `0x02` a fixed alpha (mapped to a
|
||||
/// percentage as for layers), anything else ByLayer.
|
||||
fn transparency_440(v: i64) -> Transparency {
|
||||
match (v >> 24) & 0xff {
|
||||
1 => Transparency::ByBlock,
|
||||
2 => {
|
||||
let alpha = u32::try_from(v & 0xff).unwrap_or(255);
|
||||
let opaque = (alpha * 100 + 127) / 255;
|
||||
Transparency::Percent(u8::try_from(100u32.saturating_sub(opaque).min(90)).unwrap_or(90))
|
||||
}
|
||||
_ => Transparency::ByLayer,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn common(t: &T) -> Common {
|
||||
let mut c = Common { layer: t.s(8).unwrap_or_else(|| "0".into()), linetype: t.s(6).unwrap_or_else(|| "ByLayer".into()), ..Common::default() };
|
||||
if let Some(v) = t.i(62) {
|
||||
c.color = Color::from_aci(v as i16);
|
||||
if v < 0 {
|
||||
c.color = Color::ByLayer;
|
||||
c.visible = true;
|
||||
}
|
||||
}
|
||||
if let Some(tc) = t.i(420) {
|
||||
c.color = Color::True(Rgb::from_u32(tc as u32));
|
||||
}
|
||||
if let Some(lw) = t.i(370) {
|
||||
c.lineweight = Lineweight::from_dxf(lw as i16);
|
||||
}
|
||||
if let Some(s) = t.f(48) {
|
||||
c.ltscale = if s > 0.0 { s } else { 1.0 };
|
||||
}
|
||||
if t.i(60) == Some(1) {
|
||||
c.visible = false;
|
||||
}
|
||||
if let Some(v) = t.i(440) {
|
||||
c.transparency = transparency_440(v);
|
||||
}
|
||||
c.thickness = t.fd(39, 0.0);
|
||||
c.extrusion = Vec3::new(t.fd(210, 0.0), t.fd(220, 0.0), t.fd(230, 1.0));
|
||||
c
|
||||
}
|
||||
|
||||
/// Apply the OCS → WCS transform for 2D entities with a non-default extrusion.
|
||||
pub(super) fn ocs(c: &Common, p: Vec3) -> Vec3 {
|
||||
ocs_n(c.extrusion, p)
|
||||
}
|
||||
|
||||
pub(super) fn ocs_n(n: Vec3, p: Vec3) -> Vec3 {
|
||||
if (n.z - 1.0).abs() < 1e-12 && n.x.abs() < 1e-12 && n.y.abs() < 1e-12 { p } else { cadcraft_geom::Mat4::ocs(n).apply(p) }
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
use super::common::T;
|
||||
use super::*;
|
||||
|
||||
/// Associativity from a DIMASSOC object (files from other writers). The dimension kind decides
|
||||
/// which definition points the references name, in the order AutoCAD writes them: `p13`/`p14`
|
||||
/// for linear, aligned and arc-length dimensions, both ends of both lines (`p13`..`p16`) for
|
||||
/// angular dimensions, the three points of an angular-3-point dimension, `p13` for ordinate
|
||||
/// dimensions, and for radial or diametric dimensions the single circle reference
|
||||
/// ([`radial_links`]). CADCraft's own xdata links, when present, win.
|
||||
pub(super) fn dimassoc(d: &mut Drawing, tags: &[Tag]) {
|
||||
let start = tags.iter().position(|x| x.code == 100 && x.str() == "AcDbDimAssoc").unwrap_or(0);
|
||||
let tail = tags.get(start..).unwrap_or(&[]);
|
||||
let Some(dim_h) = tail.iter().find(|x| x.code == 330).and_then(|x| Handle::parse_hex(&x.str())) else { return };
|
||||
let Some(EntityKind::Dimension(dm)) = d.entity(dim_h).map(|e| &e.kind) else { return };
|
||||
if !dm.assoc.is_empty() {
|
||||
return;
|
||||
}
|
||||
// A radial or diametric dimension names its circle once, by its centre.
|
||||
if matches!(dm.kind, DimKind::Radius | DimKind::Diameter) {
|
||||
let links = radial_links(d, dm, tail);
|
||||
if !links.is_empty() {
|
||||
let _ = d.modify_entity(dim_h, |e| {
|
||||
if let EntityKind::Dimension(dm) = &mut e.kind {
|
||||
dm.assoc = links;
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
let slots: &[&str] = match dm.kind {
|
||||
DimKind::Linear { .. } | DimKind::Aligned => &["p13", "p14"],
|
||||
DimKind::Angular => &["p13", "p14", "p15", "p16"],
|
||||
DimKind::Angular3P => &["p13", "p14", "p15"],
|
||||
DimKind::ArcLength => &["p13", "p14"],
|
||||
DimKind::Ordinate { .. } => &["p13"],
|
||||
DimKind::Radius | DimKind::Diameter => return,
|
||||
};
|
||||
let flags = T(tail).i(90).unwrap_or(0);
|
||||
let used: Vec<&str> = slots.iter().enumerate().filter(|(i, _)| flags & (1 << i) != 0).map(|(_, s)| *s).collect();
|
||||
// Point references start at each `1 AcDbOsnapPointRef`.
|
||||
let starts: Vec<usize> = tail.iter().enumerate().filter(|(_, x)| x.code == 1).map(|(i, _)| i).take(8).collect();
|
||||
let mut links = Vec::new();
|
||||
for (k, s) in starts.iter().enumerate() {
|
||||
let Some(point) = used.get(k) else { break };
|
||||
let end = starts.get(k + 1).copied().unwrap_or(tail.len());
|
||||
let r = T(tail.get(s + 1..end).unwrap_or(&[]));
|
||||
let Some(main) = r.s(331).and_then(|h| Handle::parse_hex(&h)) else { continue };
|
||||
let other = r.s(332).and_then(|h| Handle::parse_hex(&h));
|
||||
let at = r.p(10).xy();
|
||||
// A polyline's segment (subentity type 73 = 2, edge; 91 its 1-based index).
|
||||
let segment = r.i(91).filter(|k| *k >= 1 && r.i(73) == Some(2));
|
||||
if let Some(snap) = infer_snap(d, r.i(72).unwrap_or(0), main, other, at, segment) {
|
||||
links.push(DimAssoc { point: (*point).to_string(), handle: main, snap });
|
||||
}
|
||||
}
|
||||
if !links.is_empty() {
|
||||
let _ = d.modify_entity(dim_h, |e| {
|
||||
if let EntityKind::Dimension(dm) = &mut e.kind {
|
||||
dm.assoc = links;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Associations of a radial or diametric dimension from its one standard reference: the circle
|
||||
/// or arc it measures, by its centre (osnap 3). The definition points are then on that circle,
|
||||
/// so the dimension follows its radius or diameter.
|
||||
fn radial_links(d: &Drawing, dm: &Dimension, tail: &[Tag]) -> Vec<DimAssoc> {
|
||||
let starts: Vec<usize> = tail.iter().enumerate().filter(|(_, x)| x.code == 1 && x.str() == "AcDbOsnapPointRef").map(|(i, _)| i).take(8).collect();
|
||||
for (k, s) in starts.iter().enumerate() {
|
||||
let end = starts.get(k + 1).copied().unwrap_or(tail.len());
|
||||
let r = T(tail.get(s + 1..end).unwrap_or(&[]));
|
||||
if !matches!(r.i(72), Some(3 | 4)) {
|
||||
continue;
|
||||
}
|
||||
let Some(handle) = r.s(331).and_then(|h| Handle::parse_hex(&h)) else { continue };
|
||||
let Some(centre) = circle_centre(d, handle) else { continue };
|
||||
let on = |point: &str, p: Vec3| {
|
||||
let angle = centre.angle_to(p.xy());
|
||||
angle.is_finite().then(|| DimAssoc { point: point.into(), handle, snap: AssocSnap::OnCircle { angle } })
|
||||
};
|
||||
let links = match dm.kind {
|
||||
DimKind::Radius => on("p15", dm.p15).map(|p15| vec![DimAssoc { point: "defpt".into(), handle, snap: AssocSnap::Center }, p15]),
|
||||
DimKind::Diameter => [on("defpt", dm.defpt), on("p15", dm.p15)].into_iter().collect::<Option<Vec<_>>>(),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(links) = links {
|
||||
return links;
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// The planar centre of a circle or arc, or `None` for any other entity: the point a radial or
|
||||
/// diametric dimension's single DIMASSOC reference names.
|
||||
fn circle_centre(d: &Drawing, h: Handle) -> Option<Vec2> {
|
||||
match &d.entity(h)?.kind {
|
||||
EntityKind::Circle(c) => Some(c.center.xy()),
|
||||
EntityKind::Arc(a) => Some(a.center.xy()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A CADCraft snap from a DIMASSOC object snap type and point, and for a polyline the segment
|
||||
/// the point is an end of: AutoCAD writes no usable point there (10/20 are 0, #1049), the segment
|
||||
/// and the osnap (1 its end, 13 its start) name the vertex. For a line or arc the osnap alone
|
||||
/// says which end: 13 is the start and 1 the end, whatever the stored point (the point is
|
||||
/// 0,0,0 for curves, and choosing by distance gave the wrong end for arcs). A point entity has
|
||||
/// only one position, so both codes mean it.
|
||||
fn infer_snap(d: &Drawing, osnap: i64, main: Handle, other: Option<Handle>, at: Vec2, segment: Option<i64>) -> Option<AssocSnap> {
|
||||
let e = d.entity(main)?;
|
||||
if let (EntityKind::LwPolyline(p), Some(k), 1 | 13) = (&e.kind, segment, osnap) {
|
||||
let n = p.vertices.len();
|
||||
let segments = if p.closed { n } else { n.saturating_sub(1) };
|
||||
let k = usize::try_from(k).ok().filter(|k| *k <= segments)?;
|
||||
let index = if osnap == 13 { k - 1 } else { k % n };
|
||||
return Some(AssocSnap::Vertex { index });
|
||||
}
|
||||
Some(match osnap {
|
||||
1 | 13 => match &e.kind {
|
||||
// AutoCAD names a curve's start with 13 and its end with 1 (the stored point is 0,0,0).
|
||||
// Other writers use 1 for either end of a line with the point stored: take the nearer.
|
||||
EntityKind::Line(l) => {
|
||||
if osnap == 13 || (at != Vec2::ZERO && l.a.xy().dist(at) <= l.b.xy().dist(at)) {
|
||||
AssocSnap::Start
|
||||
} else {
|
||||
AssocSnap::End
|
||||
}
|
||||
}
|
||||
EntityKind::Arc(_) => {
|
||||
if osnap == 13 {
|
||||
AssocSnap::Start
|
||||
} else {
|
||||
AssocSnap::End
|
||||
}
|
||||
}
|
||||
EntityKind::LwPolyline(p) => {
|
||||
let index = p.vertices.iter().enumerate().min_by(|x, y| x.1.p.dist(at).total_cmp(&y.1.p.dist(at))).map(|(i, _)| i)?;
|
||||
AssocSnap::Vertex { index }
|
||||
}
|
||||
EntityKind::Point(_) => AssocSnap::Start,
|
||||
_ => return None,
|
||||
},
|
||||
2 => AssocSnap::Mid,
|
||||
3 | 4 => AssocSnap::Center,
|
||||
5 | 10 => match &e.kind {
|
||||
EntityKind::Circle(Circle { center, .. }) | EntityKind::Arc(Arc { center, .. }) => {
|
||||
let v = at - center.xy();
|
||||
AssocSnap::OnCircle { angle: v.y.atan2(v.x) }
|
||||
}
|
||||
_ => return None,
|
||||
},
|
||||
6 => AssocSnap::Intersection { other: other.filter(|o| d.entity(*o).is_some())? },
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
use super::common::T;
|
||||
use super::entities::{entity_extras, entity_plot_style, entity_with_table_budget, insert_attribs, polyline_entity};
|
||||
use super::objects::{MAX_OBJECTS, Objects, page_setup};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn parse_entities(
|
||||
recs: &[(String, Vec<Tag>)],
|
||||
d: &mut Drawing,
|
||||
rx: &mut Rx,
|
||||
skipped: &mut usize,
|
||||
table_cells_left: &mut usize,
|
||||
) -> Vec<(Option<String>, bool, Entity)> {
|
||||
let mut out = Vec::new();
|
||||
let mut i = 0;
|
||||
while let Some((kind, tags)) = recs.get(i) {
|
||||
let t = T(tags);
|
||||
let owner = crate::dxf_ext::owner(tags);
|
||||
let paper = t.i(67) == Some(1);
|
||||
let handle = t.s(5).and_then(|h| Handle::parse_hex(&h));
|
||||
let mut kindent = entity_with_table_budget(kind, tags, table_cells_left);
|
||||
if let Some((c, _)) = kindent.as_mut()
|
||||
&& let Some(n) = entity_plot_style(tags, rx)
|
||||
{
|
||||
c.plot_style = n;
|
||||
}
|
||||
// More entities from this record (the faces of a polyface or polygon mesh).
|
||||
let mut extra: Vec<EntityKind> = Vec::new();
|
||||
i += 1;
|
||||
match kind.as_str() {
|
||||
"POLYLINE" => {
|
||||
let Some((c, k, more)) = polyline_entity(recs, &mut i, &t) else {
|
||||
*skipped = skipped.saturating_add(1);
|
||||
continue;
|
||||
};
|
||||
extra = more;
|
||||
kindent = Some((c, k));
|
||||
}
|
||||
"INSERT" => {
|
||||
if t.i(66) == Some(1) {
|
||||
let ins_layer = kindent.as_ref().map(|(c, _)| c.layer.clone()).unwrap_or_default();
|
||||
let attribs = insert_attribs(recs, &mut i, &ins_layer);
|
||||
if let Some((_, EntityKind::Insert(ins))) = kindent.as_mut() {
|
||||
ins.attribs = attribs;
|
||||
}
|
||||
}
|
||||
}
|
||||
// A VERTEX or ATTRIB with no POLYLINE or INSERT before it; a stray SEQEND holds nothing.
|
||||
"VERTEX" | "ATTRIB" => {
|
||||
*skipped = skipped.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
"SEQEND" => continue,
|
||||
_ => {}
|
||||
}
|
||||
if let Some((c, mut k)) = kindent {
|
||||
let h = match handle {
|
||||
Some(h) => {
|
||||
d.bump_handseed(h);
|
||||
h
|
||||
}
|
||||
None => d.new_handle(),
|
||||
};
|
||||
entity_extras(kind, tags, h, &mut k, rx);
|
||||
let more: Vec<_> =
|
||||
extra.into_iter().map(|f| (owner.clone(), paper, Entity { handle: d.new_handle(), common: c.clone(), kind: f })).collect();
|
||||
out.push((owner, paper, Entity { handle: h, common: c, kind: k }));
|
||||
out.extend(more);
|
||||
} else if kind == "HATCH" {
|
||||
// Without boundary data. The other records `entity` returns nothing for are read
|
||||
// elsewhere (the writer's paper viewport, the loops of a region).
|
||||
*skipped = skipped.saturating_add(1);
|
||||
}
|
||||
}
|
||||
// Assign fresh handles to duplicates (hostile files).
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for (_, _, e) in &mut out {
|
||||
if !seen.insert(e.handle) {
|
||||
e.handle = d.new_handle();
|
||||
seen.insert(e.handle);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Read the BLOCKS section: the xref paths of external blocks, then each block's entities.
|
||||
pub(super) fn read_blocks(
|
||||
tags: &[Tag],
|
||||
d: &mut Drawing,
|
||||
rx: &mut Rx,
|
||||
table_cells_left: &mut usize,
|
||||
skipped: &mut std::collections::BTreeMap<String, usize>,
|
||||
) -> Vec<BlockDef> {
|
||||
let recs = records(tags);
|
||||
for (k, tg) in &recs {
|
||||
let t = T(tg);
|
||||
if k == "BLOCK" && t.i(70).unwrap_or(0) & 4 != 0 {
|
||||
rx.xref_paths.insert(t.s(2).unwrap_or_default().to_ascii_uppercase(), t.s(1).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
let mut blocks = Vec::new();
|
||||
let mut n = 0;
|
||||
let mut i = 0;
|
||||
while let Some((k, tg)) = recs.get(i) {
|
||||
if k == "BLOCK" {
|
||||
let t = T(tg);
|
||||
let name = t.s(2).unwrap_or_default();
|
||||
let base = t.p(10);
|
||||
let description = t.s(4).unwrap_or_default();
|
||||
// Block flags: 4 xref, 8 overlay.
|
||||
let xref = t.i(70).unwrap_or(0) & 12;
|
||||
let mut j = i + 1;
|
||||
while recs.get(j).is_some_and(|(k, _)| k != "ENDBLK") {
|
||||
j += 1;
|
||||
}
|
||||
let body = recs.get(i + 1..j).unwrap_or(&[]);
|
||||
let ents = parse_entities(body, d, rx, &mut n, table_cells_left);
|
||||
blocks.push((name, base, description, ents, xref));
|
||||
i = j + 1;
|
||||
} else {
|
||||
// A record outside any BLOCK ... ENDBLK pair.
|
||||
if k != "ENDBLK" {
|
||||
n = n.saturating_add(1);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
count_skipped(skipped, "BLOCKS", n);
|
||||
blocks
|
||||
}
|
||||
|
||||
/// Read the OBJECTS section into `objs`, and each LAYOUT into `layout_objs`.
|
||||
pub(super) fn read_objects(tags: &[Tag], objs: &mut Objects, layout_objs: &mut Vec<LayoutDef>) {
|
||||
for (k, tg) in records(tags) {
|
||||
objs.add(&k, &tg);
|
||||
if k == "LAYOUT" {
|
||||
// Fields after the AcDbLayout marker (AcDbPlotSettings also has a code 1).
|
||||
let start = tg.iter().position(|x| x.code == 100 && x.str() == "AcDbLayout").unwrap_or(0);
|
||||
let tail = tg.get(start..).unwrap_or(&[]);
|
||||
let t = T(tail);
|
||||
// LAYOUT has two 330s: owner dict and the block record; take the last.
|
||||
let br = tg.iter().rfind(|x| x.code == 330).map(Tag::str).unwrap_or_default();
|
||||
let plot = T(tg.get(..start).unwrap_or(&[]));
|
||||
layout_objs.push((t.s(1).unwrap_or_default(), t.i(71).unwrap_or(0) as u32, br.to_ascii_uppercase(), page_setup(&plot)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the drawing's layouts, and return the layout each paper-space block belongs to, and the
|
||||
/// layout that takes paper-space entities without an owner.
|
||||
pub(super) fn setup_layouts(d: &mut Drawing, layout_objs: &mut [LayoutDef], rx: &Rx) -> (HashMap<String, String>, String) {
|
||||
let mut paper_block_for: HashMap<String, String> = HashMap::new();
|
||||
layout_objs.sort_by_key(|l| l.1);
|
||||
for (name, order, br, page) in layout_objs.iter() {
|
||||
if name.eq_ignore_ascii_case("Model") {
|
||||
continue;
|
||||
}
|
||||
d.layouts.push(Layout { page: page.clone(), ..Layout::new(name, *order) });
|
||||
if let Some(bname) = rx.brs.get(br) {
|
||||
paper_block_for.insert(bname.to_ascii_uppercase(), name.clone());
|
||||
}
|
||||
}
|
||||
if d.layouts.is_empty() {
|
||||
d.layouts.push(Layout::new("Layout1", 1));
|
||||
paper_block_for.insert("*PAPER_SPACE".into(), "Layout1".into());
|
||||
}
|
||||
// The layout in `*Paper_Space` is the active one, kept when it isn't the first tab (the first
|
||||
// is active by default); paper-space entities without an owner go there.
|
||||
let active = paper_block_for.get("*PAPER_SPACE").cloned();
|
||||
let first_tab = d.layouts.iter().min_by_key(|l| l.tab_order).map(|l| l.name.clone());
|
||||
d.active_layout = active.clone().filter(|a| Some(a) != first_tab.as_ref());
|
||||
let first_layout = active.or_else(|| d.layouts.first().map(|l| l.name.clone())).unwrap_or_else(|| "Layout1".into());
|
||||
(paper_block_for, first_layout)
|
||||
}
|
||||
|
||||
/// Place the BLOCKS entities: model and paper space blocks go to their space, the rest become
|
||||
/// block definitions.
|
||||
pub(super) fn place_blocks(d: &mut Drawing, blocks: Vec<BlockDef>, rx: &mut Rx, paper_block_for: &HashMap<String, String>, first_layout: &str) {
|
||||
for (name, base, description, ents, xref) in blocks {
|
||||
let up = name.to_ascii_uppercase();
|
||||
if up == "*MODEL_SPACE" {
|
||||
for (_, _, e) in ents {
|
||||
d.ensure_layer(&e.common.layer);
|
||||
d.model.push(e);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if up.starts_with("*PAPER_SPACE") {
|
||||
let lname = paper_block_for.get(&up).cloned().unwrap_or_else(|| first_layout.to_string());
|
||||
if let Some(l) = d.layouts.iter_mut().find(|l| l.name == lname) {
|
||||
for (_, _, e) in ents {
|
||||
l.entities.push(e);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Generated geometry we rebuild on save: table blocks, and standard arrowhead blocks
|
||||
// nothing inserts whose content is the one CADCraft writes. An arrowhead block with other
|
||||
// content (AutoCAD's `_Dot`, a user's redefinition) is kept, so dimensions keep it.
|
||||
let generated_arrow = || {
|
||||
let own: Vec<&Entity> = ents.iter().map(|(_, _, e)| e).collect();
|
||||
crate::dxf_ext::is_standard_arrow(&name) && !rx.inserted.contains(&up) && crate::dxf_write::is_generated_arrow(&name, &own)
|
||||
};
|
||||
if rx.table_blocks.contains(&up) || generated_arrow() {
|
||||
continue;
|
||||
}
|
||||
let mut b = Block::new(&name);
|
||||
b.base = base;
|
||||
b.description = description;
|
||||
if let Some(&(units, explodable)) = rx.block_props.get(&up) {
|
||||
(b.units, b.explodable) = (units, explodable);
|
||||
}
|
||||
if xref & 4 != 0 {
|
||||
b.xref_path = Some(String::new());
|
||||
b.xref_overlay = xref & 8 != 0;
|
||||
} else {
|
||||
for (_, _, e) in ents {
|
||||
b.entities.push(e);
|
||||
}
|
||||
}
|
||||
// Array parameters, with the block names the items use (other programs may rename
|
||||
// anonymous blocks); dropped when the items no longer match them.
|
||||
b.array = rx.block_arrays.get(&up).cloned().and_then(|mut a| a.sync_names(b.entities.iter().map(|e| e.as_ref())).then_some(a));
|
||||
if let Some(path) = rx.xref_paths.remove(&up) {
|
||||
b.xref_path = Some(path);
|
||||
}
|
||||
d.blocks.insert(name, std::sync::Arc::new(b));
|
||||
}
|
||||
}
|
||||
|
||||
/// Place the ENTITIES section: model space, or paper space when flagged (67=1).
|
||||
pub(super) fn place_entities(
|
||||
d: &mut Drawing,
|
||||
entities: Vec<(Option<String>, bool, Entity)>,
|
||||
rx: &Rx,
|
||||
paper_block_for: &HashMap<String, String>,
|
||||
first_layout: &str,
|
||||
) {
|
||||
for (owner, paper, e) in entities {
|
||||
d.ensure_layer(&e.common.layer);
|
||||
let owner_name = owner.and_then(|o| rx.brs.get(&o.trim().to_ascii_uppercase()).cloned()).map(|n| n.to_ascii_uppercase());
|
||||
let to_paper = paper || owner_name.as_deref().is_some_and(|n| n.starts_with("*PAPER_SPACE"));
|
||||
if to_paper {
|
||||
let lname = owner_name.and_then(|n| paper_block_for.get(&n).cloned()).unwrap_or_else(|| first_layout.to_string());
|
||||
if let Some(l) = d.layouts.iter_mut().find(|l| l.name == lname) {
|
||||
l.entities.push(e);
|
||||
}
|
||||
} else {
|
||||
d.model.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn count_skipped(skipped: &mut std::collections::BTreeMap<String, usize>, section: &str, n: usize) {
|
||||
if n > 0 {
|
||||
let c = skipped.entry(section.to_string()).or_insert(0);
|
||||
*c = c.saturating_add(n);
|
||||
}
|
||||
}
|
||||
|
||||
/// Records between sections, which [`sections`] passes over: everything after an ENDSEC up to the
|
||||
/// next SECTION. Stray ENDSECs and the EOF hold nothing and don't count.
|
||||
pub(super) fn outside_sections(tags: &[Tag]) -> usize {
|
||||
let mut inside = false;
|
||||
let mut n = 0usize;
|
||||
for t in tags.iter().filter(|t| t.code == 0) {
|
||||
match t.str().as_str() {
|
||||
"SECTION" => inside = true,
|
||||
"ENDSEC" => inside = false,
|
||||
"EOF" => {}
|
||||
_ if !inside => n = n.saturating_add(1),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// A SORTENTSTABLE (DXF Reference, OBJECTS section): after its AcDbSortentsTable marker, the
|
||||
/// owning block record (330), then pairs of an entity handle (331) and the sort handle (5) it is
|
||||
/// drawn by.
|
||||
pub(super) fn sortents_table(tags: &[Tag]) -> Option<(String, HashMap<Handle, u64>)> {
|
||||
let start = tags.iter().position(|t| t.code == 100 && t.str() == "AcDbSortentsTable")?;
|
||||
let tail = tags.get(start + 1..)?;
|
||||
let owner = tail.iter().find(|t| t.code == 330)?.str().trim().to_ascii_uppercase();
|
||||
let mut map = HashMap::new();
|
||||
let mut ent: Option<Handle> = None;
|
||||
for t in tail {
|
||||
match t.code {
|
||||
331 => ent = Handle::parse_hex(&t.str()),
|
||||
5 => {
|
||||
if let (Some(e), Some(k)) = (ent.take(), Handle::parse_hex(&t.str()))
|
||||
&& map.len() < MAX_OBJECTS
|
||||
{
|
||||
map.insert(e, k.0);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(!map.is_empty()).then_some((owner, map))
|
||||
}
|
||||
|
||||
/// Draw each space with a SORTENTSTABLE in sort-handle order: an entity listed in the table by
|
||||
/// its sort handle, any other by its own handle.
|
||||
pub(super) fn apply_sortents(
|
||||
d: &mut Drawing,
|
||||
tables: &[(String, HashMap<Handle, u64>)],
|
||||
brs: &HashMap<String, String>,
|
||||
paper_for: &HashMap<String, String>,
|
||||
first_layout: &str,
|
||||
) {
|
||||
for (owner, map) in tables {
|
||||
let Some(name) = brs.get(owner) else { continue };
|
||||
let up = name.to_ascii_uppercase();
|
||||
let key = |e: &Entity| map.get(&e.handle).copied().unwrap_or(e.handle.0);
|
||||
if up == "*MODEL_SPACE" {
|
||||
d.model.sort_by_key(key);
|
||||
} else if up.starts_with("*PAPER_SPACE") {
|
||||
let lname = paper_for.get(&up).map(String::as_str).unwrap_or(first_layout);
|
||||
if let Some(l) = d.layouts.iter_mut().find(|l| l.name == lname) {
|
||||
l.entities.sort_by_key(key);
|
||||
}
|
||||
} else if let Some(b) = d.blocks.get_mut(name.as_str()) {
|
||||
std::sync::Arc::make_mut(b).entities.sort_by_key(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,651 @@
|
||||
use super::common::{T, common, ocs};
|
||||
use super::hatch::hatch;
|
||||
use super::text::{attdef, attrib_modes, attrib_props, mtext_from, multiline_attrib, split_embedded, text_from};
|
||||
use super::*;
|
||||
|
||||
/// Parse one entity record. POLYLINE/INSERT followers are handled by the caller.
|
||||
pub(super) fn entity(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKind)> {
|
||||
entity_with_table_budget(kind, tags, &mut 4_000_000)
|
||||
}
|
||||
|
||||
/// Parse one entity while charging materialized table cells to the caller's shared import budget.
|
||||
/// An ARC, with its angles run the other way in a mirrored OCS.
|
||||
fn arc(c: &Common, t: &T) -> Arc {
|
||||
let mut a = Arc { center: ocs(c, t.p(10)), radius: t.fd(40, 1.0).abs(), start: t.fd(50, 0.0).to_radians(), end: t.fd(51, 360.0).to_radians() };
|
||||
if c.extrusion.z < 0.0 {
|
||||
// Mirrored OCS: angles run the other way.
|
||||
let (s, e) = (std::f64::consts::PI - a.end, std::f64::consts::PI - a.start);
|
||||
a.start = cadcraft_geom::norm_angle(s);
|
||||
a.end = cadcraft_geom::norm_angle(e);
|
||||
}
|
||||
a
|
||||
}
|
||||
|
||||
fn ellipse(c: &Common, t: &T) -> Ellipse {
|
||||
let mut e = Ellipse {
|
||||
center: t.p(10),
|
||||
major: t.p(11),
|
||||
ratio: t.fd(40, 1.0).clamp(1e-9, 1.0),
|
||||
start: t.fd(41, 0.0),
|
||||
end: t.fd(42, std::f64::consts::TAU),
|
||||
};
|
||||
if c.extrusion.z < 0.0 && (cadcraft_geom::ccw_sweep(e.start, e.end) - std::f64::consts::TAU).abs() > 1e-9 {
|
||||
// Centre and major axis are WCS, but the minor axis is extrusion × major: with (0,0,-1)
|
||||
// it points the other way, so parameter t lands where -t does in the XY plane. A full
|
||||
// ellipse is symmetric and stays as it is.
|
||||
let (s, e2) = (-e.end, -e.start);
|
||||
e.start = cadcraft_geom::norm_angle(s);
|
||||
e.end = cadcraft_geom::norm_angle(e2);
|
||||
}
|
||||
e
|
||||
}
|
||||
|
||||
/// A LWPOLYLINE, or the region CADCraft wrote as one.
|
||||
fn lwpolyline(tags: &[Tag], c: &Common, t: &T) -> Option<EntityKind> {
|
||||
let mut verts = Vec::new();
|
||||
let mut cur: Option<PolyVertex> = None;
|
||||
for tg in tags {
|
||||
match tg.code {
|
||||
10 => {
|
||||
if let Some(v) = cur.take() {
|
||||
verts.push(v);
|
||||
}
|
||||
cur = Some(PolyVertex { p: Vec2::new(tg.f64(), 0.0), ..Default::default() });
|
||||
}
|
||||
20 => {
|
||||
if let Some(v) = cur.as_mut() {
|
||||
v.p.y = tg.f64();
|
||||
}
|
||||
}
|
||||
40 => {
|
||||
if let Some(v) = cur.as_mut() {
|
||||
v.start_width = tg.f64();
|
||||
}
|
||||
}
|
||||
41 => {
|
||||
if let Some(v) = cur.as_mut() {
|
||||
v.end_width = tg.f64();
|
||||
}
|
||||
}
|
||||
42 => {
|
||||
if let Some(v) = cur.as_mut() {
|
||||
v.bulge = tg.f64();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
verts.extend(cur);
|
||||
let flags = t.i(70).unwrap_or(0);
|
||||
if c.extrusion.z < 0.0 {
|
||||
for v in &mut verts {
|
||||
v.p.x = -v.p.x;
|
||||
v.bulge = -v.bulge;
|
||||
}
|
||||
}
|
||||
let pl = LwPolyline { vertices: verts, closed: flags & 1 != 0, const_width: t.fd(43, 0.0), elevation: t.fd(38, 0.0), plinegen: flags & 128 != 0 };
|
||||
// A region CADCraft wrote as polylines (`dxf_region`).
|
||||
crate::dxf_region::from_polyline(tags, pl)
|
||||
}
|
||||
|
||||
fn spline(t: &T) -> Spline {
|
||||
let degree = t.i(71).unwrap_or(3).clamp(1, 10) as usize;
|
||||
let knots = t.all_f(40);
|
||||
let control = t.pts(10);
|
||||
let weights = t.all_f(41);
|
||||
let fit = t.pts(11);
|
||||
let tangent = |c: i32| t.f(c).map(|_| t.p(c).xy());
|
||||
let fit_opts =
|
||||
cadcraft_geom::FitOptions { knots: Default::default(), start_tangent: tangent(12), end_tangent: tangent(13), tolerance: t.fd(44, 0.0) }
|
||||
.sanitized();
|
||||
let closed = t.i(70).unwrap_or(0) & 1 != 0;
|
||||
let mut sp = Spline { degree, knots, control, weights, fit: fit.clone(), closed, fit_opts };
|
||||
if !sp.is_valid() && fit.len() >= 2 {
|
||||
sp = Spline::fit_with(&fit, sp.closed, fit_opts);
|
||||
} else if !sp.is_valid() && sp.control.len() >= 2 {
|
||||
sp = Spline::from_control(sp.control.clone(), degree);
|
||||
} else if fit.len() >= 3 {
|
||||
// DXF keeps the knots, not how they were spaced: recover it for later refits.
|
||||
sp.fit_opts.knots = sp.infer_knot_param();
|
||||
}
|
||||
sp
|
||||
}
|
||||
|
||||
pub(super) fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_left: &mut usize) -> Option<(Common, EntityKind)> {
|
||||
// The paper-space viewport our writer adds (dxf_write::paper_view) stays implicit.
|
||||
if kind == "VIEWPORT" && crate::dxf_ext::is_paper_view(tags) {
|
||||
return None;
|
||||
}
|
||||
let t = T(tags);
|
||||
let c = common(&t);
|
||||
let k = match kind {
|
||||
"LINE" => EntityKind::Line(Line { a: t.p(10), b: t.p(11) }),
|
||||
"POINT" => EntityKind::Point(Point { p: t.p(10), angle: t.fd(50, 0.0).to_radians() }),
|
||||
"CIRCLE" => EntityKind::Circle(Circle { center: ocs(&c, t.p(10)), radius: t.fd(40, 1.0).abs() }),
|
||||
"ARC" => EntityKind::Arc(arc(&c, &t)),
|
||||
"ELLIPSE" => EntityKind::Ellipse(ellipse(&c, &t)),
|
||||
"LWPOLYLINE" => lwpolyline(tags, &c, &t)?,
|
||||
"SPLINE" => EntityKind::Spline(spline(&t)),
|
||||
"HELIX" => EntityKind::Helix(helix(tags)),
|
||||
"RAY" => EntityKind::Ray(RayLine { base: t.p(10), dir: t.p(11) }),
|
||||
"XLINE" => EntityKind::XLine(RayLine { base: t.p(10), dir: t.p(11) }),
|
||||
"TEXT" => EntityKind::Text(text_from(&t)),
|
||||
"MTEXT" => EntityKind::MText(mtext_from(tags, &c)),
|
||||
"ATTDEF" => EntityKind::AttDef(attdef(tags, &c)),
|
||||
"INSERT" => {
|
||||
// A mirrored block reference comes with extrusion (0,0,-1): its insertion point, scale,
|
||||
// rotation and column spacing are in that OCS, where x runs the other way. In the WCS that
|
||||
// is the mirrored point, a negative x scale, the opposite rotation and a negative column
|
||||
// spacing. Noise of up to 1e-3 in the extrusion's x/y moves the projection by ~1e-7 of the
|
||||
// size, so it still counts as mirrored. A truly tilted extrusion (block plane not parallel
|
||||
// to XY) needs the full 3D block transform, which Insert cannot hold; it is left as read.
|
||||
let n = c.extrusion;
|
||||
let mirrored = n.z < 0.0 && n.x.hypot(n.y) <= 1e-3 * n.z.abs();
|
||||
let mut p = t.p(10);
|
||||
let mut scale = Vec3::new(t.fd(41, 1.0), t.fd(42, 1.0), t.fd(43, 1.0));
|
||||
let mut rotation = t.fd(50, 0.0).to_radians();
|
||||
let mut col_spacing = t.fd(44, 0.0);
|
||||
if mirrored {
|
||||
p = ocs(&c, p);
|
||||
scale.x = -scale.x;
|
||||
rotation = -rotation;
|
||||
col_spacing = -col_spacing;
|
||||
}
|
||||
EntityKind::Insert(Insert {
|
||||
block: t.s(2).unwrap_or_default(),
|
||||
insert: p,
|
||||
scale,
|
||||
rotation,
|
||||
attribs: Vec::new(),
|
||||
cols: t.i(70).unwrap_or(1).clamp(1, 10_000) as u32,
|
||||
rows: t.i(71).unwrap_or(1).clamp(1, 10_000) as u32,
|
||||
col_spacing,
|
||||
row_spacing: t.fd(45, 0.0),
|
||||
})
|
||||
}
|
||||
"DIMENSION" => {
|
||||
let ty = t.i(70).unwrap_or(0) & 0x0f;
|
||||
let kind = match ty {
|
||||
0 => DimKind::Linear { rotation: t.fd(50, 0.0).to_radians() },
|
||||
1 => DimKind::Aligned,
|
||||
2 => DimKind::Angular,
|
||||
3 => DimKind::Diameter,
|
||||
4 => DimKind::Radius,
|
||||
5 => DimKind::Angular3P,
|
||||
6 => DimKind::Ordinate { x_type: t.i(70).unwrap_or(0) & 64 != 0 },
|
||||
8 => DimKind::ArcLength,
|
||||
_ => DimKind::Aligned,
|
||||
};
|
||||
EntityKind::Dimension(Dimension {
|
||||
kind,
|
||||
defpt: t.p(10),
|
||||
// The text midpoint is the one OCS point of a DIMENSION.
|
||||
text_mid: if c.extrusion.z < 0.0 { ocs(&c, t.p(11)) } else { t.p(11) },
|
||||
p13: t.p(13),
|
||||
p14: t.p(14),
|
||||
p15: t.p(15),
|
||||
p16: t.p(16),
|
||||
text: t.s(1).unwrap_or_default(),
|
||||
style: t.s(3).unwrap_or_else(|| "Standard".into()),
|
||||
measurement: t.fd(42, 0.0),
|
||||
text_rotation: t.fd(53, 0.0).to_radians(),
|
||||
user_text_pos: t.i(70).unwrap_or(0) & 128 != 0,
|
||||
block: t.s(2).filter(|s| !s.is_empty()),
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
})
|
||||
}
|
||||
"LEADER" => EntityKind::Leader(Leader {
|
||||
vertices: t.pts(10).into_iter().map(|p| p.to3(0.0)).collect(),
|
||||
arrow: t.i(71).unwrap_or(1) != 0,
|
||||
spline: t.i(72).unwrap_or(0) != 0,
|
||||
style: t.s(3).unwrap_or_else(|| "Standard".into()),
|
||||
}),
|
||||
// Names behind handles follow in `entity_extras` and `Objects::apply`.
|
||||
"MULTILEADER" | "MLEADER" => EntityKind::MLeader(crate::dxf_mleader::read(tags)),
|
||||
"SOLID" | "TRACE" => {
|
||||
let corners = [t.p(10), t.p(11), t.p(12), if t.f(13).is_some() { t.p(13) } else { t.p(12) }];
|
||||
let corners = corners.map(|p| ocs(&c, p));
|
||||
if kind == "SOLID" { EntityKind::Solid(Solid { corners }) } else { EntityKind::Trace(Solid { corners }) }
|
||||
}
|
||||
"MLINE" => EntityKind::MLine(crate::dxf_mline::read_entity(tags)),
|
||||
"3DFACE" => EntityKind::Face3d(Face3d { corners: [t.p(10), t.p(11), t.p(12), t.p(13)], hidden_edges: t.i(70).unwrap_or(0) as u8 }),
|
||||
"HATCH" => {
|
||||
let mut k = hatch(tags, &t)?;
|
||||
if c.extrusion.z < 0.0
|
||||
&& let EntityKind::Hatch(h) = &mut k
|
||||
{
|
||||
// Boundary in the mirrored OCS, like LWPOLYLINE; the pattern direction mirrors too.
|
||||
for v in h.loops.iter_mut().flat_map(|l| l.vertices.iter_mut()) {
|
||||
v.p.x = -v.p.x;
|
||||
v.bulge = -v.bulge;
|
||||
}
|
||||
h.angle = cadcraft_geom::norm_angle(std::f64::consts::PI - h.angle);
|
||||
}
|
||||
k
|
||||
}
|
||||
"ACAD_TABLE" => EntityKind::Table(acad_table(tags, table_cells_left)),
|
||||
"VIEWPORT" => EntityKind::Viewport(Viewport {
|
||||
center: t.p(10),
|
||||
width: t.fd(40, 1.0),
|
||||
height: t.fd(41, 1.0),
|
||||
view_center: Vec2::new(t.fd(12, 0.0), t.fd(22, 0.0)),
|
||||
view_height: t.fd(45, 1.0),
|
||||
id: t.i(69).unwrap_or(0) as u32,
|
||||
locked: t.i(90).unwrap_or(0) & 16384 != 0,
|
||||
frozen_layers: crate::dxf_ext::read_frozen(tags),
|
||||
layer_colors: crate::dxf_ext::read_layer_colors(tags),
|
||||
// Status flags: 65536 = non-rectangular clipping (by the 340 entity), 131072 = off.
|
||||
clip: t.s(340).and_then(|h| Handle::parse_hex(&h)).filter(|h| h.0 != 0 && t.i(90).unwrap_or(0) & 65536 != 0),
|
||||
on: t.i(90).unwrap_or(0) & 131072 == 0,
|
||||
}),
|
||||
"WIPEOUT" => {
|
||||
let o = t.p(10);
|
||||
let u = t.p(11);
|
||||
let v = t.p(12);
|
||||
// Clip vertices are in pixel space: origin at the image's top-left corner, y pointing down.
|
||||
let pts: Vec<Vec2> = t.pts(14).into_iter().map(|q| o.xy() + u.xy() * (q.x + 0.5) + v.xy() * (0.5 - q.y)).collect();
|
||||
EntityKind::Wipeout(Wipeout { boundary: pts })
|
||||
}
|
||||
// The file path comes from the IMAGEDEF object (`dxf_image::resolve`).
|
||||
"IMAGE" => EntityKind::Image(crate::dxf_image::read_entity(tags)),
|
||||
"TOLERANCE" => {
|
||||
let dir = t.p(11);
|
||||
let dir = if dir.xy().is_finite() && dir.xy().len() > 1e-12 { dir } else { Vec3::new(1.0, 0.0, 0.0) };
|
||||
EntityKind::Tolerance(Tolerance {
|
||||
insert: t.p(10),
|
||||
dir,
|
||||
style: t.s(3).unwrap_or_else(|| "Standard".into()),
|
||||
text: t.s(1).unwrap_or_default().chars().take(MAX_FRAME_CHARS).collect(),
|
||||
})
|
||||
}
|
||||
other => EntityKind::Unknown(Unknown {
|
||||
dxf_type: other.to_string(),
|
||||
tags: tags.iter().map(|tg| RawTag { code: tg.code, value: tg.str() }).collect(),
|
||||
}),
|
||||
};
|
||||
Some((c, k))
|
||||
}
|
||||
|
||||
/// An ACAD_TABLE entity: sizes, cell texts and merges (DXF Reference), plus CADCraft's
|
||||
/// title/header flags, text height and style name from xdata when present.
|
||||
pub(super) fn acad_table(tags: &[Tag], table_cells_left: &mut usize) -> Table {
|
||||
const MAX_CELLS: usize = 1_000_000;
|
||||
let start = tags.iter().position(|x| x.code == 100 && x.str() == "AcDbTable").unwrap_or(0);
|
||||
let tail = tags.get(start..).unwrap_or(&[]);
|
||||
let first_cell = tail.iter().position(|x| x.code == 171).unwrap_or(tail.len());
|
||||
let head = T(tail.get(..first_cell).unwrap_or(&[]));
|
||||
let mut rows = usize::try_from(head.i(91).unwrap_or(0)).unwrap_or(0).min(10_000);
|
||||
let mut cols = usize::try_from(head.i(92).unwrap_or(0)).unwrap_or(0).min(10_000);
|
||||
// Large declared sizes must be backed by cell records (no allocation from a bare count).
|
||||
let n_cells = tail.iter().filter(|x| x.code == 171).count();
|
||||
let declared = rows.saturating_mul(cols);
|
||||
if declared > MAX_CELLS || (declared > 10_000 && n_cells < declared) {
|
||||
rows = 0;
|
||||
cols = 0;
|
||||
}
|
||||
// Charge before any cell vector is materialized, including discarded tables.
|
||||
let count = rows.saturating_mul(cols);
|
||||
match table_cells_left.checked_sub(count) {
|
||||
Some(left) => *table_cells_left = left,
|
||||
None => return Table { insert: T(tags).p(10), ..Table::default() },
|
||||
}
|
||||
let size = |v: f64| if v.is_finite() && v > 0.0 { v } else { 0.27 };
|
||||
let mut row_heights: Vec<f64> = head.all_f(141).into_iter().take(rows).map(size).collect();
|
||||
row_heights.resize(rows, 0.27);
|
||||
let mut col_widths: Vec<f64> = head.all_f(142).into_iter().take(cols).map(size).collect();
|
||||
col_widths.resize(cols, 2.5);
|
||||
// Cells, row-major, each starting at a 171 group; xdata ends the last one.
|
||||
let body = tail.get(first_cell..).unwrap_or(&[]);
|
||||
let body = body.get(..body.iter().position(|x| x.code >= 1000).unwrap_or(body.len())).unwrap_or(&[]);
|
||||
let mut flat: Vec<TableCell> = Vec::new();
|
||||
let mut i = 0;
|
||||
while let Some(t0) = body.get(i) {
|
||||
if flat.len() >= rows * cols {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
if t0.code != 171 {
|
||||
continue;
|
||||
}
|
||||
let end = body.get(i..).and_then(|r| r.iter().position(|x| x.code == 171)).map(|p| i + p).unwrap_or(body.len());
|
||||
let cell = body.get(i..end).unwrap_or(&[]);
|
||||
i = end;
|
||||
let c = T(cell);
|
||||
let mut text: String = cell.iter().filter(|x| x.code == 2 || x.code == 3).map(Tag::str).collect();
|
||||
text.push_str(&c.s(1).unwrap_or_default());
|
||||
let span = |code: i32| c.i(code).and_then(|v| u32::try_from(v).ok());
|
||||
// A merge starts at the cell whose span (rows 176, columns 175) is more than one cell.
|
||||
// Its own merged flag (173) varies by writer: CADCraft sets it, while AutoCAD-style
|
||||
// writers (acadrust 0.6.4) set it only on the cells the merge covers, whose span is 0.
|
||||
let merged = match (span(176), span(175)) {
|
||||
(Some(rs), Some(cs)) if rs >= 1 && cs >= 1 && (rs, cs) != (1, 1) => Some((rs, cs)),
|
||||
_ => None,
|
||||
};
|
||||
// The cell's own text height: 140 outside the value block (301 … 304), where it is a number.
|
||||
let mut in_value = false;
|
||||
let mut text_height = None;
|
||||
for x in cell {
|
||||
match x.code {
|
||||
301 => in_value = true,
|
||||
304 => in_value = false,
|
||||
140 if !in_value => text_height = Some(x.f64()).filter(|h| h.is_finite() && *h > 0.0 && *h < 1e9),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
flat.push(TableCell { text, merged, text_height, ..TableCell::default() });
|
||||
}
|
||||
flat.resize(rows * cols, TableCell::default());
|
||||
let cells: Vec<Vec<TableCell>> = if cols == 0 { vec![Vec::new(); rows] } else { flat.chunks(cols).map(<[TableCell]>::to_vec).collect() };
|
||||
let x = crate::dxf_ext::xdata(tags, crate::dxf_ext::APP);
|
||||
let ours = x.iter().position(|t| t.code == 1000 && t.str() == "TABLE").map(|p| x.get(p + 1..).unwrap_or(&[]));
|
||||
let (title, header, text_height, style) = match ours {
|
||||
Some(o) => {
|
||||
let flags: Vec<i64> = o.iter().filter(|t| t.code == 1070).map(Tag::i64).collect();
|
||||
let h = o.iter().find(|t| t.code == 1040).map(Tag::f64).filter(|h| h.is_finite() && *h > 0.0).unwrap_or(0.18);
|
||||
let style = o.iter().find(|t| t.code == 1000).map(Tag::str).unwrap_or_else(|| "Standard".into());
|
||||
(flags.first().is_some_and(|v| *v != 0), flags.get(1).is_some_and(|v| *v != 0), h, style)
|
||||
}
|
||||
// Other writers: a first row merged across every column reads as a title.
|
||||
// Their text height is resolved to the table style's once the styles are read (`apply`).
|
||||
None => (cols > 1 && cells.first().and_then(|r| r.first()).and_then(|c| c.merged) == Some((1, cols as u32)), false, 0.0, "Standard".into()),
|
||||
};
|
||||
let mut t = Table { insert: T(tags).p(10), col_widths, row_heights, cells, style, text_height, title, header, ..Table::default() };
|
||||
crate::dxf_table::read_table_xdata(tags, &mut t);
|
||||
t
|
||||
}
|
||||
|
||||
/// The plot style names of a file: the entries of the `ACAD_PLOTSTYLENAME` dictionary of the
|
||||
/// named object dictionary (the first object), by placeholder handle.
|
||||
pub(super) fn plot_style_names(secs: &[cadcraft_dxf::Section]) -> HashMap<String, String> {
|
||||
let mut out = HashMap::new();
|
||||
let Some(objects) = secs.iter().find(|s| s.name == "OBJECTS") else { return out };
|
||||
let recs = records(&objects.tags);
|
||||
let entries = |tags: &[Tag]| {
|
||||
let mut v: Vec<(String, String)> = Vec::new();
|
||||
let mut name: Option<String> = None;
|
||||
for t in tags {
|
||||
match t.code {
|
||||
3 => name = Some(t.str()),
|
||||
350 | 360 => {
|
||||
if let Some(n) = name.take() {
|
||||
v.push((n, t.str().trim().to_ascii_uppercase()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
v
|
||||
};
|
||||
let Some((_, root)) = recs.iter().find(|(k, _)| k == "DICTIONARY") else { return out };
|
||||
let Some((_, dict)) = entries(root).into_iter().find(|(n, _)| n.eq_ignore_ascii_case(crate::dxf_ext::PLOT_STYLE_NAMES_KEY)) else {
|
||||
return out;
|
||||
};
|
||||
let is_dict = |k: &str| k == "DICTIONARY" || k == "ACDBDICTIONARYWDFLT";
|
||||
let Some((_, tags)) = recs.iter().find(|(k, tg)| is_dict(k) && T(tg).s(5).is_some_and(|h| h.trim().eq_ignore_ascii_case(&dict))) else {
|
||||
return out;
|
||||
};
|
||||
for (n, h) in entries(tags).into_iter().take(crate::dxf_ext::MAX_PLOT_STYLE_NAMES) {
|
||||
if !n.trim().is_empty() {
|
||||
out.insert(h, n.chars().take(255).collect());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The named plot style an entity record points at (group 390 among its AcDbEntity groups).
|
||||
pub(super) fn entity_plot_style(tags: &[Tag], rx: &Rx) -> Option<String> {
|
||||
let mut in_entity = false;
|
||||
for t in tags {
|
||||
if t.code == 100 {
|
||||
if in_entity {
|
||||
return None;
|
||||
}
|
||||
in_entity = t.str() == "AcDbEntity";
|
||||
} else if t.code == 390 && in_entity {
|
||||
return rx.pstyles.get(&t.str().trim().to_ascii_uppercase()).cloned();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Dimension overrides and associativity, table references: data of an entity record that
|
||||
/// needs the file's handle maps.
|
||||
pub(super) fn entity_extras(kind: &str, tags: &[Tag], h: Handle, k: &mut EntityKind, rx: &mut Rx) {
|
||||
match (kind, k) {
|
||||
("DIMENSION", EntityKind::Dimension(dm)) => {
|
||||
dm.overrides = crate::dxf_ext::read_dstyle(tags, &rx.styles, &rx.brs, &rx.ltypes);
|
||||
dm.assoc = crate::dxf_ext::read_assoc(tags);
|
||||
if let Some(center) = crate::dxf_ext::read_arclen(tags) {
|
||||
dm.kind = DimKind::ArcLength;
|
||||
dm.p15 = center;
|
||||
}
|
||||
dm.marks = crate::dxf_ext::read_marks(tags);
|
||||
// Group 52: the extension lines' angle relative to the dimension line (DIMEDIT Oblique).
|
||||
let base = match dm.kind {
|
||||
DimKind::Linear { rotation } => Some(rotation),
|
||||
DimKind::Aligned => Some((dm.p14.xy() - dm.p13.xy()).angle()),
|
||||
_ => None,
|
||||
};
|
||||
if let (Some(base), Some(o)) = (base, T(tags).f(52).filter(|o| o.is_finite() && o.rem_euclid(180.0) != 0.0)) {
|
||||
dm.marks.oblique = Some(cadcraft_geom::norm_angle(base + o.to_radians()));
|
||||
}
|
||||
}
|
||||
("ACAD_TABLE", EntityKind::Table(_)) => {
|
||||
let t = T(tags);
|
||||
if let Some(b) = t.s(2) {
|
||||
rx.table_blocks.insert(b.to_ascii_uppercase());
|
||||
}
|
||||
let ours = crate::dxf_ext::xdata(tags, crate::dxf_ext::APP).iter().any(|t| t.code == 1000 && t.str() == "TABLE");
|
||||
if !ours {
|
||||
rx.table_style_fix.push((h, t.s(342).map(|sh| sh.trim().to_ascii_uppercase()).unwrap_or_default()));
|
||||
}
|
||||
}
|
||||
("INSERT", EntityKind::Insert(ins)) => {
|
||||
rx.inserted.insert(ins.block.to_ascii_uppercase());
|
||||
}
|
||||
("MULTILEADER" | "MLEADER", EntityKind::MLeader(m)) => {
|
||||
let pending = crate::dxf_mleader::resolve(tags, m, &rx.styles, &rx.brs);
|
||||
if let Some(b) = &m.block {
|
||||
rx.inserted.insert(b.block.to_ascii_uppercase());
|
||||
}
|
||||
rx.mleader_fix.push((h, pending));
|
||||
}
|
||||
// CADCraft's VPFROZEN xdata wins; otherwise AutoCAD's group 341 layer handles.
|
||||
("VIEWPORT", EntityKind::Viewport(v)) if v.frozen_layers.is_empty() => {
|
||||
v.frozen_layers = tags
|
||||
.iter()
|
||||
.take_while(|t| t.code < 1000)
|
||||
.filter(|t| t.code == 341)
|
||||
.filter_map(|t| rx.layers.get(&t.str().trim().to_ascii_uppercase()).cloned())
|
||||
.collect();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a run of entity records (ENTITIES section or a block's body) into entities. Records that
|
||||
/// can't be placed are counted in `skipped`.
|
||||
/// The entity of a POLYLINE, with its mesh faces (advancing `i` past its VERTEX records). `None`
|
||||
/// for a mesh without faces.
|
||||
pub(super) fn polyline_entity(recs: &[(String, Vec<Tag>)], i: &mut usize, t: &T) -> Option<(Common, EntityKind, Vec<EntityKind>)> {
|
||||
let flags = t.i(70).unwrap_or(0);
|
||||
let mut pts: Vec<(Vec3, f64, f64, f64)> = Vec::new();
|
||||
// Polyface face records (VERTEX 70 bit 128 without 64): vertex indices 71–74.
|
||||
let mut faces: Vec<[i64; 4]> = Vec::new();
|
||||
while let Some((k2, t2)) = recs.get(*i) {
|
||||
if k2 == "VERTEX" {
|
||||
let tt = T(t2);
|
||||
let vflags = tt.i(70).unwrap_or(0);
|
||||
if vflags & 128 != 0 && vflags & 64 == 0 {
|
||||
faces.push([71, 72, 73, 74].map(|c| tt.i(c).unwrap_or(0)));
|
||||
} else if vflags & 16 == 0 {
|
||||
pts.push((tt.p(10), tt.fd(42, 0.0), tt.fd(40, 0.0), tt.fd(41, 0.0)));
|
||||
}
|
||||
*i += 1;
|
||||
} else {
|
||||
if k2 == "SEQEND" {
|
||||
*i += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
let c = common(t);
|
||||
let mut extra: Vec<EntityKind> = Vec::new();
|
||||
let k = if flags & (16 | 64) != 0 {
|
||||
// Polyface (64) and polygon (16) meshes become 3D faces; the first takes the
|
||||
// polyline's place.
|
||||
let verts: Vec<Vec3> = pts.iter().map(|p| p.0).collect();
|
||||
let mut fs = if flags & 64 != 0 { polyface_faces(&verts, &faces) } else { mesh_faces(&verts, t, flags) }.into_iter().map(EntityKind::Face3d);
|
||||
let first = fs.next()?;
|
||||
extra.extend(fs);
|
||||
first
|
||||
} else if flags & 8 != 0 {
|
||||
EntityKind::Polyline3d(Polyline3d { points: pts.iter().map(|p| p.0).collect(), closed: flags & 1 != 0 })
|
||||
} else {
|
||||
// A 2D polyline's vertices are in its OCS: mirrored like LWPOLYLINE.
|
||||
let m = if c.extrusion.z < 0.0 { -1.0 } else { 1.0 };
|
||||
EntityKind::LwPolyline(LwPolyline {
|
||||
vertices: pts.iter().map(|p| PolyVertex { p: Vec2::new(m * p.0.x, p.0.y), bulge: m * p.1, start_width: p.2, end_width: p.3 }).collect(),
|
||||
closed: flags & 1 != 0,
|
||||
const_width: t.fd(40, 0.0).min(t.fd(41, 0.0)).max(0.0),
|
||||
elevation: t.p(10).z,
|
||||
plinegen: flags & 128 != 0,
|
||||
})
|
||||
};
|
||||
Some((c, k, extra))
|
||||
}
|
||||
|
||||
/// The ATTRIB records after an INSERT, advancing `i` past them and their SEQEND.
|
||||
pub(super) fn insert_attribs(recs: &[(String, Vec<Tag>)], i: &mut usize, ins_layer: &str) -> Vec<Attrib> {
|
||||
let mut attribs = Vec::new();
|
||||
while let Some((k2, t2)) = recs.get(*i) {
|
||||
if k2 == "ATTRIB" {
|
||||
let tt = T(t2);
|
||||
// A multiline attribute has its text as embedded MTEXT after its own groups.
|
||||
let (own, embedded) = split_embedded(t2);
|
||||
let multiline = multiline_attrib(embedded, &common(&tt));
|
||||
let own = T(own);
|
||||
let mut text = text_from(&own);
|
||||
if let Some(m) = &multiline {
|
||||
text.value.clone_from(&m.read_as);
|
||||
}
|
||||
attribs.push(Attrib {
|
||||
tag: own.s(2).unwrap_or_default(),
|
||||
text,
|
||||
invisible: own.i(70).unwrap_or(0) & 1 != 0,
|
||||
constant: false,
|
||||
prompt: String::new(),
|
||||
props: attrib_props(&tt, ins_layer),
|
||||
multiline,
|
||||
modes: attrib_modes(own.0),
|
||||
});
|
||||
*i += 1;
|
||||
} else {
|
||||
if k2 == "SEQEND" {
|
||||
*i += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
attribs
|
||||
}
|
||||
|
||||
/// Faces of a polyface mesh: each face record names three or four vertices by 1-based index;
|
||||
/// a negative index hides the edge that starts at that vertex. Faces with fewer vertices or
|
||||
/// an index outside the vertex list are skipped.
|
||||
fn polyface_faces(verts: &[Vec3], faces: &[[i64; 4]]) -> Vec<Face3d> {
|
||||
let mut out = Vec::new();
|
||||
for f in faces {
|
||||
let mut corners: Vec<Vec3> = Vec::with_capacity(4);
|
||||
let mut hidden = 0u8;
|
||||
let mut ok = true;
|
||||
for &ix in f.iter().filter(|&&ix| ix != 0) {
|
||||
let at = usize::try_from(ix.unsigned_abs()).ok().and_then(|n| n.checked_sub(1));
|
||||
let Some(p) = at.and_then(|n| verts.get(n)) else {
|
||||
ok = false;
|
||||
break;
|
||||
};
|
||||
if ix < 0 {
|
||||
hidden |= 1 << corners.len();
|
||||
}
|
||||
corners.push(*p);
|
||||
}
|
||||
match (ok, corners.as_slice()) {
|
||||
// A triangle's closing edge is the 3DFACE's fourth (the third is degenerate).
|
||||
(true, [a, b, c]) => out.push(Face3d { corners: [*a, *b, *c, *c], hidden_edges: hidden | if hidden & 4 != 0 { 8 } else { 0 } }),
|
||||
(true, [a, b, c, d]) => out.push(Face3d { corners: [*a, *b, *c, *d], hidden_edges: hidden }),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Quads of an M×N polygon mesh (71/72, or the smoothed 73/74 when the vertex count matches
|
||||
/// those), closed in M by bit 1 and in N by bit 32. Vertices run row by row.
|
||||
fn mesh_faces(verts: &[Vec3], t: &T, flags: i64) -> Vec<Face3d> {
|
||||
let dims = |a: i32, b: i32| Some((usize::try_from(t.i(a)?).ok()?, usize::try_from(t.i(b)?).ok()?));
|
||||
let fits = |(m, n): (usize, usize)| m >= 2 && n >= 2 && m.checked_mul(n).is_some_and(|mn| mn <= verts.len());
|
||||
let exact = |(m, n): (usize, usize)| fits((m, n)) && m * n == verts.len();
|
||||
let Some((m, n)) = [dims(71, 72), dims(73, 74)].into_iter().flatten().find(|d| exact(*d)).or_else(|| dims(71, 72).filter(|d| fits(*d))) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let at = |i: usize, j: usize| verts.get((i % m) * n + j % n).copied();
|
||||
let rows = if flags & 1 != 0 { m } else { m - 1 };
|
||||
let cols = if flags & 32 != 0 { n } else { n - 1 };
|
||||
let mut out = Vec::new();
|
||||
for i in 0..rows {
|
||||
for j in 0..cols {
|
||||
if let (Some(a), Some(b), Some(c), Some(d)) = (at(i, j), at(i, j + 1), at(i + 1, j + 1), at(i + 1, j)) {
|
||||
out.push(Face3d { corners: [a, b, c, d], hidden_edges: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A HELIX (DXF Reference): the axis base point, start point, axis, radius, turns, turn height,
|
||||
/// handedness and constraint after the AcDbHelix marker; the top radius is how far the end of its
|
||||
/// spline (AcDbSpline: the last control point, else fit point) lies from the axis.
|
||||
fn helix(tags: &[Tag]) -> Helix {
|
||||
let hs = tags.iter().position(|t| t.code == 100 && t.str() == "AcDbHelix").unwrap_or(tags.len());
|
||||
let ss = tags.iter().position(|t| t.code == 100 && t.str() == "AcDbSpline").unwrap_or(0).min(hs);
|
||||
let spline = tags.get(ss..hs).unwrap_or(&[]);
|
||||
let t = T(tags.get(hs..).unwrap_or(&[]));
|
||||
// The last point of the spline (3D) with group `code`.
|
||||
let last = |code: i32| {
|
||||
let i = spline.iter().rposition(|g| g.code == code)?;
|
||||
let f = |c: i32| spline.get(i..).and_then(|r| r.iter().find(|g| g.code == c)).map(Tag::f64).unwrap_or(0.0);
|
||||
Some(Vec3::new(f(code), f(code + 10), f(code + 20)))
|
||||
};
|
||||
let center = t.p(10);
|
||||
let start = t.p(11);
|
||||
let axis = t.p(12);
|
||||
let axis = if axis.is_finite() && axis.len() > 1e-12 { axis.normalized() } else { Vec3::Z };
|
||||
// Distance from the axis.
|
||||
let radius = |p: Vec3| {
|
||||
let v = Vec3::new(p.x - center.x, p.y - center.y, p.z - center.z);
|
||||
let along = v.dot(axis);
|
||||
Vec3::new(v.x - axis.x * along, v.y - axis.y * along, v.z - axis.z * along).len()
|
||||
};
|
||||
let base_radius = t.f(40).filter(|r| r.is_finite() && *r >= 0.0).unwrap_or_else(|| radius(start));
|
||||
let turns = t.fd(41, 1.0);
|
||||
let end = last(10).or_else(|| last(11));
|
||||
let height = match (t.f(42), end) {
|
||||
(Some(th), _) if th.is_finite() => th * turns,
|
||||
(_, Some(e)) => e.z - center.z,
|
||||
_ => 0.0,
|
||||
};
|
||||
Helix {
|
||||
center,
|
||||
base_radius,
|
||||
top_radius: end.map(radius).unwrap_or(base_radius),
|
||||
height,
|
||||
turns,
|
||||
start_angle: center.xy().angle_to(start.xy()),
|
||||
ccw: t.i(290).unwrap_or(1) != 0,
|
||||
constrain: HelixConstrain::from_dxf(t.i(280).unwrap_or(2)),
|
||||
}
|
||||
.sanitized()
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
use super::common::T;
|
||||
use super::*;
|
||||
|
||||
pub(super) fn hatch(tags: &[Tag], t: &T) -> Option<EntityKind> {
|
||||
let (origin, background) = hatch_xdata(tags);
|
||||
let pattern = t.s(2).unwrap_or_else(|| "SOLID".into());
|
||||
let solid = t.i(70).unwrap_or(0) == 1;
|
||||
let n_loops = t.i(91).unwrap_or(0).clamp(0, 100_000) as usize;
|
||||
let mut loops = Vec::new();
|
||||
// Walk the boundary data after the 91 tag.
|
||||
let start = tags.iter().position(|x| x.code == 91).map(|i| i + 1)?;
|
||||
let mut i = start;
|
||||
let get = |i: usize| tags.get(i);
|
||||
for _ in 0..n_loops {
|
||||
// 92: loop type flags.
|
||||
while get(i).is_some_and(|x| x.code != 92) {
|
||||
i += 1;
|
||||
}
|
||||
let flags = get(i)?.i64();
|
||||
i += 1;
|
||||
let outer = flags & 1 != 0;
|
||||
let mut verts: Vec<PolyVertex> = Vec::new();
|
||||
if flags & 2 != 0 {
|
||||
// Polyline boundary: 72 has-bulge, 73 closed, 93 count, then 10/20/42.
|
||||
let has_bulge = get(i).filter(|x| x.code == 72).map(Tag::i64).unwrap_or(0) != 0;
|
||||
while get(i).is_some_and(|x| x.code != 93) {
|
||||
i += 1;
|
||||
}
|
||||
let n = get(i)?.i64().clamp(0, 1_000_000) as usize;
|
||||
i += 1;
|
||||
for _ in 0..n {
|
||||
let x = get(i).filter(|t| t.code == 10)?.f64();
|
||||
let y = get(i + 1).filter(|t| t.code == 20)?.f64();
|
||||
i += 2;
|
||||
let mut b = 0.0;
|
||||
if has_bulge && get(i).is_some_and(|t| t.code == 42) {
|
||||
b = get(i)?.f64();
|
||||
i += 1;
|
||||
}
|
||||
verts.push(PolyVertex::with_bulge(Vec2::new(x, y), b));
|
||||
}
|
||||
} else {
|
||||
// Edge boundary: 93 edge count, each 72 edge type.
|
||||
while get(i).is_some_and(|x| x.code != 93) {
|
||||
i += 1;
|
||||
}
|
||||
let n = get(i)?.i64().clamp(0, 1_000_000) as usize;
|
||||
i += 1;
|
||||
for _ in 0..n {
|
||||
while get(i).is_some_and(|x| x.code != 72) {
|
||||
i += 1;
|
||||
}
|
||||
let ty = get(i)?.i64();
|
||||
i += 1;
|
||||
let f = |i: usize| get(i).map(Tag::f64).unwrap_or(0.0);
|
||||
match ty {
|
||||
1 => {
|
||||
let a = Vec2::new(f(i), f(i + 1));
|
||||
let b = Vec2::new(f(i + 2), f(i + 3));
|
||||
i += 4;
|
||||
if verts.last().is_none_or(|l| !l.p.near(a, 1e-9)) {
|
||||
verts.push(PolyVertex::new(a));
|
||||
}
|
||||
verts.push(PolyVertex::new(b));
|
||||
}
|
||||
2 => {
|
||||
let c = Vec2::new(f(i), f(i + 1));
|
||||
let r = f(i + 2);
|
||||
let sa = f(i + 3).to_radians();
|
||||
let ea = f(i + 4).to_radians();
|
||||
let ccw = get(i + 5).map(Tag::i64).unwrap_or(1) != 0;
|
||||
i += 6;
|
||||
let (s, e) = if ccw { (sa, ea) } else { (-sa, -ea) };
|
||||
let p0 = Vec2::polar(c, r, s);
|
||||
let p1 = Vec2::polar(c, r, e);
|
||||
let sweep = if ccw { cadcraft_geom::ccw_sweep(s, e) } else { -cadcraft_geom::ccw_sweep(e, s) };
|
||||
if verts.last().is_none_or(|l| !l.p.near(p0, 1e-9)) {
|
||||
verts.push(PolyVertex::new(p0));
|
||||
}
|
||||
// A full circle (0 to 360) starts and ends on the same point, which one
|
||||
// bulge can't span: it goes in as two half circles.
|
||||
let full = (sweep.abs() - std::f64::consts::TAU).abs() < 1e-9;
|
||||
let bulge = cadcraft_geom::arc_to_bulge(if full { sweep / 2.0 } else { sweep });
|
||||
if let Some(l) = verts.last_mut() {
|
||||
l.bulge = bulge;
|
||||
}
|
||||
if full {
|
||||
verts.push(PolyVertex::with_bulge(Vec2::polar(c, r, s + sweep / 2.0), bulge));
|
||||
}
|
||||
verts.push(PolyVertex::new(p1));
|
||||
}
|
||||
3 => {
|
||||
// Elliptic arc: approximate with points.
|
||||
let c = Vec2::new(f(i), f(i + 1));
|
||||
let m = Vec2::new(f(i + 2), f(i + 3));
|
||||
let ratio = f(i + 4);
|
||||
let sa = f(i + 5).to_radians();
|
||||
let ea = f(i + 6).to_radians();
|
||||
let ccw = get(i + 7).filter(|t| t.code == 73).map(Tag::i64).unwrap_or(1) != 0;
|
||||
i += 8;
|
||||
// A clockwise edge stores its parameters mirrored, as an arc edge does: it
|
||||
// runs from -start clockwise to -end, the ccw sweep from -end to -start
|
||||
// walked backwards.
|
||||
let (s, e) = if ccw { (sa, ea) } else { (-ea, -sa) };
|
||||
let e = cadcraft_geom::Ellipse { center: c, major: m, ratio, start: s, end: e };
|
||||
let mut pts = Vec::new();
|
||||
e.tessellate(m.len() * 1e-3, &mut pts);
|
||||
if !ccw {
|
||||
pts.reverse();
|
||||
}
|
||||
verts.extend(pts.into_iter().map(PolyVertex::new));
|
||||
}
|
||||
4 => {
|
||||
// Spline edge: 94 degree, 73 rational, 74 periodic, 95 knot count, 96
|
||||
// control point count, then the 40 knots, the 10/20 control points (each
|
||||
// with a 42 weight when rational), and 97 fit point count with 11/21 fit
|
||||
// points and 12/22, 13/23 end tangents (R2010+).
|
||||
let (mut degree, mut nk, mut nc) = (3, 0, 0);
|
||||
while let Some(x) = get(i).filter(|x| matches!(x.code, 94 | 73 | 74 | 95 | 96)) {
|
||||
let n = x.i64().clamp(0, 100_000) as usize;
|
||||
match x.code {
|
||||
94 => degree = n.clamp(1, 10), // same cap as the SPLINE reader
|
||||
95 => nk = n,
|
||||
96 => nc = n,
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
let mut knots = Vec::new();
|
||||
while knots.len() < nk && get(i).is_some_and(|x| x.code == 40) {
|
||||
knots.push(f(i));
|
||||
i += 1;
|
||||
}
|
||||
let (mut control, mut weights) = (Vec::new(), Vec::new());
|
||||
while control.len() < nc && get(i).is_some_and(|x| x.code == 10) {
|
||||
control.push(Vec2::new(f(i), f(i + 1)));
|
||||
i += 2;
|
||||
if get(i).is_some_and(|t| t.code == 42) {
|
||||
weights.push(f(i));
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
let mut fit = Vec::new();
|
||||
if get(i).is_some_and(|x| x.code == 97) {
|
||||
let n = get(i).map(Tag::i64).unwrap_or(0).clamp(0, 100_000) as usize;
|
||||
i += 1;
|
||||
while fit.len() < n && get(i).is_some_and(|x| x.code == 11) {
|
||||
fit.push(Vec2::new(f(i), f(i + 1)));
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
if weights.len() != control.len() {
|
||||
weights.clear();
|
||||
}
|
||||
let sp = Spline { degree, knots, control, weights, ..Spline::default() };
|
||||
let sp = if sp.is_valid() {
|
||||
sp
|
||||
} else if sp.control.is_empty() && fit.len() > 1 {
|
||||
Spline::from_fit_points(&fit)
|
||||
} else {
|
||||
Spline::from_control(sp.control, degree)
|
||||
};
|
||||
let pts = sp.tessellate(1e-3);
|
||||
let skip = usize::from(verts.last().zip(pts.first()).is_some_and(|(l, p)| l.p.near(*p, 1e-9)));
|
||||
verts.extend(pts.into_iter().skip(skip).map(PolyVertex::new));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if verts.len() > 1 && verts.first().map(|f| f.p).zip(verts.last().map(|l| l.p)).is_some_and(|(a, b)| a.near(b, 1e-9)) {
|
||||
verts.pop();
|
||||
}
|
||||
// 97: number of source boundary objects, then their handles (330). A spline edge's fit
|
||||
// data count is a 97 too, followed by its fit points instead.
|
||||
let mut sources = Vec::new();
|
||||
for _ in 0..2 {
|
||||
while get(i).is_some_and(|x| !matches!(x.code, 97 | 92 | 75)) {
|
||||
i += 1;
|
||||
}
|
||||
if get(i).is_none_or(|x| x.code != 97) {
|
||||
break;
|
||||
}
|
||||
let n = get(i).map(Tag::i64).unwrap_or(0).clamp(0, MAX_HATCH_SOURCES as i64) as usize;
|
||||
i += 1;
|
||||
if get(i).is_some_and(|x| !matches!(x.code, 330 | 92 | 75)) {
|
||||
continue;
|
||||
}
|
||||
while sources.len() < n
|
||||
&& let Some(x) = get(i).filter(|x| x.code == 330)
|
||||
{
|
||||
sources.extend(Handle::parse_hex(&x.str()).filter(|h| h.0 != 0));
|
||||
i += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
loops.push(HatchLoop { vertices: verts, outer, sources });
|
||||
}
|
||||
let solid = solid || pattern.eq_ignore_ascii_case("SOLID");
|
||||
let (scale, angle) = (t.fd(41, 1.0), t.fd(52, 0.0).to_radians());
|
||||
// Keep the file's definition lines even for a name the library knows: the library's versions
|
||||
// of some patterns (AR-CONC) only approximate AutoCAD's, and drawing them at the file's scale
|
||||
// could be hundreds of times denser than the hatch the author saw.
|
||||
let pattern_lines = if solid { Vec::new() } else { pattern_definition(tags, scale, angle, origin) };
|
||||
Some(EntityKind::Hatch(Hatch {
|
||||
pattern: pattern.to_ascii_uppercase(),
|
||||
solid,
|
||||
loops,
|
||||
scale,
|
||||
angle,
|
||||
associative: t.i(71).unwrap_or(0) != 0,
|
||||
style: t.i(75).unwrap_or(0) as u8,
|
||||
// The first 30 is the elevation point's z (boundary points are 2D).
|
||||
elevation: t.f(30).filter(|z| z.is_finite()).unwrap_or(0.0),
|
||||
gradient: gradient(tags),
|
||||
origin,
|
||||
background,
|
||||
pattern_lines,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Upper bound on the source boundary objects of one HATCH loop.
|
||||
const MAX_HATCH_SOURCES: usize = 4096;
|
||||
|
||||
/// Upper bounds on a HATCH's pattern definition lines and on the dashes of one line.
|
||||
const MAX_PATTERN_LINES: usize = 1024;
|
||||
|
||||
const MAX_PATTERN_DASHES: usize = 256;
|
||||
|
||||
/// The pattern definition lines of a HATCH (groups 78, then per line 53 angle, 43/44 base
|
||||
/// point, 45/46 offset, 79 dash count, 49 dash lengths). A file holds them as drawn: scaled,
|
||||
/// rotated with the hatch and the offset as a world vector. They are kept the library's way
|
||||
/// (unit scale, no hatch rotation, base point relative to the hatch origin, offset as shift
|
||||
/// along the line and spacing), which the writer and the renderer turn back. Lines with
|
||||
/// non-finite values are skipped.
|
||||
fn pattern_definition(tags: &[Tag], scale: f64, angle: f64, origin: Vec2) -> Vec<cadcraft_doc::library::PatternLine> {
|
||||
let Some(at) = tags.iter().position(|x| x.code == 78) else { return Vec::new() };
|
||||
let scale = if scale.is_finite() && scale > 1e-9 { scale } else { 1.0 };
|
||||
let angle = if angle.is_finite() { angle } else { 0.0 };
|
||||
// Raw lines: angle (degrees), base, offset, dashes.
|
||||
let mut raw: Vec<(f64, Vec2, Vec2, Vec<f64>)> = Vec::new();
|
||||
for x in tags.iter().skip(at + 1) {
|
||||
let cur = raw.last_mut();
|
||||
match (x.code, cur) {
|
||||
(53, _) => {
|
||||
if raw.len() >= MAX_PATTERN_LINES {
|
||||
break;
|
||||
}
|
||||
raw.push((x.f64(), Vec2::ZERO, Vec2::ZERO, Vec::new()));
|
||||
}
|
||||
(43, Some(l)) => l.1.x = x.f64(),
|
||||
(44, Some(l)) => l.1.y = x.f64(),
|
||||
(45, Some(l)) => l.2.x = x.f64(),
|
||||
(46, Some(l)) => l.2.y = x.f64(),
|
||||
(79, Some(_)) => {}
|
||||
(49, Some(l)) => {
|
||||
if l.3.len() < MAX_PATTERN_DASHES {
|
||||
l.3.push(x.f64());
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
raw.into_iter()
|
||||
.filter(|(a, b, o, d)| a.is_finite() && b.is_finite() && o.is_finite() && d.iter().all(|v| v.is_finite()))
|
||||
.map(|(a, base, offset, dashes)| {
|
||||
let local = (base - origin).rotate(-angle) * (1.0 / scale);
|
||||
let delta = offset.rotate(-a.to_radians()) * (1.0 / scale);
|
||||
cadcraft_doc::library::PatternLine {
|
||||
angle: a - angle.to_degrees(),
|
||||
origin: (local.x, local.y),
|
||||
delta: (delta.x, delta.y),
|
||||
dashes: dashes.into_iter().map(|d| d / scale).collect(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Hatch origin and background colour from `CADCRAFT` xdata: `1000 HATCH`, `1011` origin,
|
||||
/// `1000` background colour name (empty for none). Defaults when absent.
|
||||
fn hatch_xdata(tags: &[Tag]) -> (Vec2, Option<Color>) {
|
||||
let x = crate::dxf_ext::xdata(tags, crate::dxf_ext::APP);
|
||||
let Some(at) = x.windows(2).position(|w| matches!(w, [m, o] if m.code == 1000 && m.str() == "HATCH" && o.code == 1011)) else {
|
||||
return (Vec2::ZERO, None);
|
||||
};
|
||||
let val = |k: usize, code: i32| x.get(at + k).filter(|t| t.code == code);
|
||||
let coord = |k: usize, code: i32| val(k, code).map(Tag::f64).filter(|v| v.is_finite()).unwrap_or(0.0);
|
||||
let origin = Vec2::new(coord(1, 1011), coord(2, 1021));
|
||||
(origin, val(4, 1000).and_then(|t| Color::parse(&t.str())))
|
||||
}
|
||||
|
||||
/// Gradient values missing from a file fall back to the GRADIENT command's defaults.
|
||||
fn default_gradient() -> Gradient {
|
||||
Gradient { name: "LINEAR".into(), color1: Color::Index(5), color2: Color::Index(7), angle: 0.0, centered: true }
|
||||
}
|
||||
|
||||
/// A hatch's gradient fill: the native groups when present (another program may have edited
|
||||
/// them), otherwise CADCraft's `GRADIENT` xdata (kept by R2000 files and DWG conversions).
|
||||
fn gradient(tags: &[Tag]) -> Option<Gradient> {
|
||||
if tags.iter().any(|x| x.code == 450) { native_gradient(tags) } else { xdata_gradient(tags) }
|
||||
}
|
||||
|
||||
/// `CADCRAFT` xdata: `1000 GRADIENT`, then name, 1040 angle, 1070 centered, colour 1 and 2 names.
|
||||
fn xdata_gradient(tags: &[Tag]) -> Option<Gradient> {
|
||||
let x = crate::dxf_ext::xdata(tags, crate::dxf_ext::APP);
|
||||
let at = x.iter().position(|t| t.code == 1000 && t.str() == "GRADIENT")?;
|
||||
let o = x.get(at + 1..).unwrap_or(&[]);
|
||||
let d = default_gradient();
|
||||
let strs: Vec<String> = o.iter().filter(|t| t.code == 1000).take(3).map(Tag::str).collect();
|
||||
let color = |i: usize, d: Color| strs.get(i).and_then(|s| Color::parse(s)).unwrap_or(d);
|
||||
Some(Gradient {
|
||||
name: strs.first().cloned().unwrap_or(d.name),
|
||||
color1: color(1, d.color1),
|
||||
color2: color(2, d.color2),
|
||||
angle: o.iter().find(|t| t.code == 1040).map(Tag::f64).filter(|a| a.is_finite()).unwrap_or(d.angle),
|
||||
centered: o.iter().find(|t| t.code == 1070).map_or(d.centered, |t| t.i64() != 0),
|
||||
})
|
||||
}
|
||||
|
||||
/// DXF Reference HATCH groups 450–470, when 450 says gradient. Each 463 starts a colour record
|
||||
/// whose 63 (ACI) and 421 (true colour) groups follow.
|
||||
fn native_gradient(tags: &[Tag]) -> Option<Gradient> {
|
||||
let start = tags.iter().position(|x| x.code == 450)?;
|
||||
if tags.get(start)?.i64() != 1 {
|
||||
return None;
|
||||
}
|
||||
let mut g = default_gradient();
|
||||
// Per colour record: (ACI, true colour); a true colour wins over its ACI fallback.
|
||||
let mut colors: [(Option<Color>, Option<Rgb>); 2] = [(None, None); 2];
|
||||
let mut slot: Option<usize> = None;
|
||||
for x in tags.iter().skip(start + 1).take_while(|x| x.code != 1001) {
|
||||
let rec = slot.and_then(|s| colors.get_mut(s));
|
||||
match x.code {
|
||||
460 => g.angle = Some(x.f64()).filter(|a| a.is_finite()).unwrap_or(0.0),
|
||||
461 => g.centered = x.f64().abs() < 1e-9,
|
||||
463 => slot = Some(slot.map_or(0, |s| s.saturating_add(1))),
|
||||
63 => {
|
||||
if let Some(r) = rec {
|
||||
r.0 = Some(Color::from_aci(i16::try_from(x.i64()).unwrap_or(256)));
|
||||
}
|
||||
}
|
||||
421 => {
|
||||
if let Some(r) = rec {
|
||||
r.1 = Some(Rgb::from_u32(u32::try_from(x.i64() & 0xff_ffff).unwrap_or(0)));
|
||||
}
|
||||
}
|
||||
470 => g.name = x.str(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let [c1, c2] = colors;
|
||||
let pick = |(aci, rgb): (Option<Color>, Option<Rgb>), d: Color| rgb.map(Color::True).or(aci).unwrap_or(d);
|
||||
g.color1 = pick(c1, g.color1);
|
||||
g.color2 = pick(c2, g.color2);
|
||||
Some(g)
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
use super::common::T;
|
||||
use super::dimassoc::dimassoc;
|
||||
use super::document::sortents_table;
|
||||
use super::*;
|
||||
|
||||
/// Non-graphical objects applied once every entity is placed.
|
||||
#[derive(Default)]
|
||||
pub(super) struct Objects {
|
||||
/// Dictionary entry handle (upper case) → entry name.
|
||||
names: HashMap<String, String>,
|
||||
/// SCALE objects (the annotation scale list), in file order.
|
||||
scales: Vec<cadcraft_doc::AnnoScale>,
|
||||
/// XRECORD handle (upper case) → its groups.
|
||||
xrecords: HashMap<String, Vec<Tag>>,
|
||||
/// TABLESTYLE objects: (handle, groups).
|
||||
table_styles: Vec<(String, Vec<Tag>)>,
|
||||
/// DIMASSOC objects.
|
||||
dimassocs: Vec<Vec<Tag>>,
|
||||
/// MLEADERSTYLE objects: (handle, groups).
|
||||
mleader_styles: Vec<(String, Vec<Tag>)>,
|
||||
/// GROUP objects: (handle, groups).
|
||||
groups: Vec<(String, Vec<Tag>)>,
|
||||
/// Saved paper-space views of layouts: (layout name, view).
|
||||
layout_views: Vec<(String, (Vec2, f64))>,
|
||||
/// SORTENTSTABLE objects: (owning block record handle, entity handle → sort handle).
|
||||
pub(super) sortents: Vec<(String, HashMap<Handle, u64>)>,
|
||||
/// WIPEOUTVARIABLES: the drawing's WIPEOUTFRAME (0 = frames hidden, 1 = shown).
|
||||
wipeout_frame: Option<i64>,
|
||||
}
|
||||
|
||||
pub(super) const MAX_OBJECTS: usize = 1_000_000;
|
||||
|
||||
impl Objects {
|
||||
pub(super) fn add(&mut self, kind: &str, tags: &[Tag]) {
|
||||
let h = T(tags).s(5).map(|h| h.trim().to_ascii_uppercase()).unwrap_or_default();
|
||||
match kind {
|
||||
"SCALE" if self.scales.len() < cadcraft_doc::MAX_SCALES => {
|
||||
if let Some(s) = crate::dxf_ext::read_scale(tags)
|
||||
&& !self.scales.iter().any(|x| x.name.eq_ignore_ascii_case(&s.name))
|
||||
{
|
||||
self.scales.push(s);
|
||||
}
|
||||
}
|
||||
"DICTIONARY" => {
|
||||
let mut name: Option<String> = None;
|
||||
for t in tags {
|
||||
match t.code {
|
||||
3 => name = Some(t.str()),
|
||||
350 | 360 => {
|
||||
if let Some(n) = name.take()
|
||||
&& self.names.len() < MAX_OBJECTS
|
||||
{
|
||||
self.names.insert(t.str().trim().to_ascii_uppercase(), n);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
"XRECORD" if self.xrecords.len() < MAX_OBJECTS => {
|
||||
self.xrecords.insert(h, tags.to_vec());
|
||||
}
|
||||
"TABLESTYLE" if self.table_styles.len() < MAX_OBJECTS => self.table_styles.push((h, tags.to_vec())),
|
||||
"DIMASSOC" if self.dimassocs.len() < MAX_OBJECTS => self.dimassocs.push(tags.to_vec()),
|
||||
"MLEADERSTYLE" if self.mleader_styles.len() < MAX_OBJECTS => self.mleader_styles.push((h, tags.to_vec())),
|
||||
"GROUP" if self.groups.len() < MAX_OBJECTS => self.groups.push((h, tags.to_vec())),
|
||||
"WIPEOUTVARIABLES" => self.wipeout_frame = T(tags).i(70).filter(|v| (0..=2).contains(v)),
|
||||
"SORTENTSTABLE" if self.sortents.len() < MAX_OBJECTS => {
|
||||
if let Some(t) = sortents_table(tags) {
|
||||
self.sortents.push(t);
|
||||
}
|
||||
}
|
||||
"LAYOUT" if self.layout_views.len() < MAX_OBJECTS => {
|
||||
let start = tags.iter().position(|x| x.code == 100 && x.str() == "AcDbLayout").unwrap_or(0);
|
||||
if let (Some(name), Some(view)) = (T(tags.get(start..).unwrap_or(&[])).s(1), layout_view(tags)) {
|
||||
self.layout_views.push((name, view));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The text of the CADCraft XRECORD stored under `key` in a dictionary (its 1/3 strings).
|
||||
pub(super) fn xrecord_text(&self, key: &str) -> Option<String> {
|
||||
let h = self.names.iter().find(|(_, n)| n.as_str() == key).map(|(h, _)| h)?;
|
||||
let tags = self.xrecords.get(h)?;
|
||||
let mut text = String::new();
|
||||
for t in tags.iter().filter(|t| t.code == 1 || t.code == 3) {
|
||||
if text.len() > crate::dxf_ext::MAX_PAYLOAD {
|
||||
break;
|
||||
}
|
||||
text.push_str(&t.str());
|
||||
}
|
||||
Some(text)
|
||||
}
|
||||
|
||||
fn apply_table_styles(&self, d: &mut Drawing) {
|
||||
// Table styles (named by their ACAD_TABLESTYLE dictionary entries).
|
||||
for (h, tags) in &self.table_styles {
|
||||
let Some(name) = self.names.get(h).filter(|n| !n.is_empty()) else { continue };
|
||||
let st = crate::dxf_table::read_table_style(name, tags);
|
||||
match d.table_styles.iter_mut().find(|s| s.name.eq_ignore_ascii_case(name)) {
|
||||
Some(x) => *x = st,
|
||||
None => d.table_styles.push(st),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_table_style_fixes(&self, d: &mut Drawing, rx: &Rx) {
|
||||
for (h, sh) in &rx.table_style_fix {
|
||||
let name = self.names.get(sh).cloned();
|
||||
// A table from another writer has no scale of its own: its text height is its
|
||||
// style's, so every cell style height applies as it is.
|
||||
let height = {
|
||||
let style = name.as_deref().unwrap_or("Standard");
|
||||
d.table_styles
|
||||
.iter()
|
||||
.find(|s| s.name.eq_ignore_ascii_case(style))
|
||||
.map_or(cadcraft_doc::TableStyle::default().text_height, |s| s.text_height)
|
||||
};
|
||||
let _ = d.modify_entity(*h, |e| {
|
||||
if let EntityKind::Table(t) = &mut e.kind {
|
||||
if let Some(name) = name {
|
||||
t.style = name;
|
||||
}
|
||||
t.text_height = height;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_mleader_styles(&self, d: &mut Drawing, rx: &Rx) {
|
||||
// Multileader styles (named by their ACAD_MLEADERSTYLE dictionary entries).
|
||||
for (h, tags) in &self.mleader_styles {
|
||||
let Some(name) = self.names.get(h).filter(|n| !n.is_empty()) else { continue };
|
||||
let t = T(tags);
|
||||
let def = MLeaderStyle::default();
|
||||
let num = |code: i32, d: f64, min: f64| t.f(code).filter(|v| v.is_finite() && *v >= min).unwrap_or(d);
|
||||
let text_style = t.s(342).and_then(|sh| rx.styles.get(&sh.trim().to_ascii_uppercase()).cloned()).filter(|n| !n.is_empty());
|
||||
let st = MLeaderStyle {
|
||||
name: name.clone(),
|
||||
arrow_size: num(44, def.arrow_size, 0.0),
|
||||
text_height: t.f(45).filter(|v| v.is_finite() && *v > 0.0).unwrap_or(def.text_height),
|
||||
landing_gap: num(42, def.landing_gap, 0.0),
|
||||
dogleg: num(43, def.dogleg, 0.0),
|
||||
text_style: text_style.unwrap_or(def.text_style),
|
||||
annotative: t.i(296).unwrap_or(0) != 0,
|
||||
};
|
||||
match d.mleader_styles.iter_mut().find(|s| s.name.eq_ignore_ascii_case(name)) {
|
||||
Some(x) => *x = st,
|
||||
None => d.mleader_styles.push(st),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_groups(&self, d: &mut Drawing) {
|
||||
// Groups (named by their ACAD_GROUP dictionary entries); members that exist.
|
||||
for (h, tags) in &self.groups {
|
||||
let Some(name) = self.names.get(h).filter(|n| !n.is_empty()) else { continue };
|
||||
let t = T(tags);
|
||||
let members: Vec<Handle> = tags
|
||||
.iter()
|
||||
.filter(|x| x.code == 340)
|
||||
.filter_map(|x| Handle::parse_hex(&x.str()))
|
||||
.filter(|m| d.entity(*m).is_some())
|
||||
.take(MAX_OBJECTS)
|
||||
.collect();
|
||||
let g = Group { name: name.clone(), description: t.s(300).unwrap_or_default(), selectable: t.i(71).unwrap_or(1) != 0, members };
|
||||
match d.groups.iter_mut().find(|x| x.name.eq_ignore_ascii_case(name)) {
|
||||
Some(x) => *x = g,
|
||||
None => d.groups.push(g),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn apply(self, d: &mut Drawing, rx: &Rx) {
|
||||
if let Some(v) = self.wipeout_frame {
|
||||
d.header.set_i64("WIPEOUTFRAME", v);
|
||||
}
|
||||
if let Some(style) = self.xrecord_text(crate::dxf_ext::MLEADER_STYLE_KEY).filter(|s| !s.trim().is_empty()) {
|
||||
d.header.set_str("CMLEADERSTYLE", &style);
|
||||
}
|
||||
self.apply_table_styles(d);
|
||||
self.apply_table_style_fixes(d, rx);
|
||||
// Parametric constraints and parameters.
|
||||
if let Some((c, p)) = self.xrecord_text(crate::dxf_ext::CONSTRAINTS_KEY).and_then(|t| crate::dxf_ext::parse_constraints(&t)) {
|
||||
d.constraints = c;
|
||||
d.parametric = p;
|
||||
}
|
||||
// The annotation scale list (a metric drawing without one gets the metric defaults),
|
||||
// then CADCraft's annotative scaling record.
|
||||
if !self.scales.is_empty() {
|
||||
d.scales = self.scales.clone();
|
||||
} else if d.header.i64("MEASUREMENT", 0) == 1 {
|
||||
d.scales = cadcraft_doc::metric_scales();
|
||||
}
|
||||
if let Some(p) = self.xrecord_text(crate::dxf_ext::ANNOTATION_KEY).and_then(|t| crate::dxf_ext::parse_anno(&t)) {
|
||||
crate::dxf_ext::apply_anno(d, p);
|
||||
}
|
||||
// Saved layer states.
|
||||
if let Some(states) = self.xrecord_text(crate::dxf_ext::LAYER_STATES_KEY).and_then(|t| crate::dxf_ext::parse_layer_states(&t)) {
|
||||
d.layer_states = states;
|
||||
}
|
||||
// The drawing's plot style tables.
|
||||
if let Some(tables) = self.xrecord_text(crate::dxf_ext::PLOT_STYLES_KEY).and_then(|t| crate::dxf_ext::parse_plot_styles(&t)) {
|
||||
d.plot_style_tables = tables;
|
||||
}
|
||||
self.apply_mleader_styles(d, rx);
|
||||
crate::dxf_mleader::apply(d, &rx.mleader_fix, &self.names);
|
||||
for (name, view) in &self.layout_views {
|
||||
if let Some(l) = d.layouts.iter_mut().find(|l| l.name == *name) {
|
||||
l.view = Some(*view);
|
||||
}
|
||||
}
|
||||
self.apply_groups(d);
|
||||
// Standard associativity, for dimensions without CADCraft's exact links.
|
||||
for tags in &self.dimassocs {
|
||||
dimassoc(d, tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Page setup from a LAYOUT object's AcDbPlotSettings fields (sizes and margins in mm).
|
||||
pub(super) fn page_setup(t: &T) -> PageSetup {
|
||||
let mut p = PageSetup::default();
|
||||
let mm = |code: i32, d: f64| {
|
||||
let v = t.fd(code, d);
|
||||
if v.is_finite() && (0.0..=100_000.0).contains(&v) { v } else { d }
|
||||
};
|
||||
let (w, h) = (mm(44, 0.0), mm(45, 0.0));
|
||||
if w > 0.0 && h > 0.0 {
|
||||
p.width_mm = w.min(h);
|
||||
p.height_mm = w.max(h);
|
||||
}
|
||||
p.margins_mm = [mm(40, p.margins_mm[0]), mm(41, p.margins_mm[1]), mm(42, p.margins_mm[2]), mm(43, p.margins_mm[3])];
|
||||
// Plot paper units 1: millimetres. 0 (inches) is left to MEASUREMENT: earlier CADCraft
|
||||
// versions wrote 0 for every layout.
|
||||
p.paper_mm = t.i(72) == Some(1);
|
||||
// Plot rotation 1 or 3 turns the media a quarter turn; media defined wider than tall is
|
||||
// already landscape.
|
||||
let rotated = matches!(t.i(73), Some(1 | 3));
|
||||
if w > 0.0 && h > 0.0 {
|
||||
p.landscape = rotated != (w > h);
|
||||
} else if t.i(73).is_some() {
|
||||
p.landscape = rotated;
|
||||
}
|
||||
if let Some(name) = t.s(4).filter(|n| !n.is_empty()) {
|
||||
p.paper = name.replace('_', " ");
|
||||
}
|
||||
if let Some(dev) = t.s(2).map(|s| s.trim().to_string()) {
|
||||
p.device = if dev.is_empty() || dev.eq_ignore_ascii_case("none_device") { "None".into() } else { dev };
|
||||
}
|
||||
// Plot layout flags: 2 display plot styles, 4 centred, 16 standard scale (75 = 0: scaled
|
||||
// to fit), 128 lineweights.
|
||||
if let Some(flags) = t.i(70) {
|
||||
p.show_plot_styles = flags & 2 != 0;
|
||||
p.center = flags & 4 != 0;
|
||||
p.lineweights = flags & 128 != 0;
|
||||
p.scale_to_fit = flags & 16 != 0 && t.i(75) == Some(0);
|
||||
}
|
||||
// Custom scale: 142 paper units per 143 drawing units; 147 is the scale factor.
|
||||
let pos = |v: Option<f64>| v.filter(|v| v.is_finite() && *v > 0.0);
|
||||
let ratio = pos(t.f(142)).zip(pos(t.f(143))).map(|(n, d)| n / d);
|
||||
if let Some(s) = pos(ratio).or_else(|| pos(t.f(147))) {
|
||||
p.scale = s;
|
||||
}
|
||||
p.plot_area = match t.i(74) {
|
||||
Some(0) => "display",
|
||||
Some(1) => "extents",
|
||||
Some(2) => "limits",
|
||||
Some(4) => "window",
|
||||
_ => "layout",
|
||||
}
|
||||
.into();
|
||||
p.plot_window = [t.fd(48, 0.0), t.fd(49, 0.0), t.fd(140, 0.0), t.fd(141, 0.0)].map(|v| if v.is_finite() { v } else { 0.0 });
|
||||
p.plot_style_table = t.s(7).unwrap_or_default();
|
||||
p
|
||||
}
|
||||
|
||||
/// A layout's saved paper-space view (centre, height) from CADCraft xdata.
|
||||
fn layout_view(tags: &[Tag]) -> Option<(Vec2, f64)> {
|
||||
let list = crate::dxf_ext::xdata_list(crate::dxf_ext::xdata(tags, crate::dxf_ext::APP), "PSVIEW");
|
||||
let g = |c: i32| list.iter().find(|t| t.code == c).map(Tag::f64).filter(|v| v.is_finite());
|
||||
Some((Vec2::new(g(1010)?, g(1020)?), g(1040)?))
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
use super::common::T;
|
||||
use super::objects::MAX_OBJECTS;
|
||||
use super::*;
|
||||
|
||||
pub(super) fn header(tags: &[Tag], d: &mut Drawing) {
|
||||
let mut i = 0;
|
||||
while let Some(t) = tags.get(i) {
|
||||
if t.code == 9 {
|
||||
let name = t.str().trim_start_matches('$').to_ascii_uppercase();
|
||||
i += 1;
|
||||
let mut vals: Vec<&Tag> = Vec::new();
|
||||
while let Some(v) = tags.get(i) {
|
||||
if v.code == 9 {
|
||||
break;
|
||||
}
|
||||
vals.push(v);
|
||||
i += 1;
|
||||
}
|
||||
// Legacy files keep spacing in HEADER; modern *ACTIVE VPORT entries override it.
|
||||
if matches!(name.as_str(), "SNAPUNIT" | "GRIDUNIT") {
|
||||
let (Some(x), Some(y)) = (vals.iter().find(|t| t.code == 10), vals.iter().find(|t| t.code == 20)) else { continue };
|
||||
let p = Vec2::new(x.f64(), y.f64());
|
||||
if p.is_finite() && if name == "GRIDUNIT" { p.x >= 0.0 && p.y >= 0.0 } else { p.x > 0.0 && p.y > 0.0 } {
|
||||
d.header.set(&name, HVal::Point(p.to3(0.0)));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let hv = match vals.as_slice() {
|
||||
[x, y, z] if x.code == 10 => HVal::Point(Vec3::new(x.f64(), y.f64(), z.f64())),
|
||||
[x, y] if x.code == 10 => HVal::Point(Vec3::new(x.f64(), y.f64(), 0.0)),
|
||||
[v] => match &v.value {
|
||||
cadcraft_dxf::Value::Real(r) => HVal::Real(*r),
|
||||
cadcraft_dxf::Value::Int(n) => HVal::Int(*n),
|
||||
cadcraft_dxf::Value::Bool(b) => HVal::Int(i64::from(*b)),
|
||||
other => HVal::Str(other.as_str()),
|
||||
},
|
||||
_ => continue,
|
||||
};
|
||||
// $DIMDSEP holds a character code; the dimension style holds the character.
|
||||
let hv = match hv {
|
||||
HVal::Int(n) if name == "DIMDSEP" => match u32::try_from(n).ok().and_then(char::from_u32).filter(|c| !c.is_control()) {
|
||||
Some(c) => HVal::Str(c.to_string()),
|
||||
None => continue,
|
||||
},
|
||||
// $DIMJOGANG is in radians; the variable (like the style) holds degrees.
|
||||
HVal::Real(r) if name == "DIMJOGANG" => HVal::Real((r.to_degrees() * 1e9).round() / 1e9),
|
||||
// $ANGBASE is in degrees (a drawing measured from north has 90.0); the variable
|
||||
// holds radians.
|
||||
HVal::Real(r) if name == "ANGBASE" => HVal::Real(r.to_radians()),
|
||||
hv => hv,
|
||||
};
|
||||
d.header.set(&name, hv);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// True when a style record carries `AcadAnnotative` xdata with the annotative flag set.
|
||||
fn annotative(tags: &[Tag]) -> bool {
|
||||
let list = crate::dxf_ext::xdata_list(crate::dxf_ext::xdata(tags, "AcadAnnotative"), "AnnotativeData");
|
||||
list.iter().filter(|t| t.code == 1070).nth(1).is_some_and(|t| t.i64() != 0)
|
||||
}
|
||||
|
||||
/// A text style's font: CADCraft's own font name from `CADCRAFT` xdata (`1000 FONT`, name)
|
||||
/// while the font file is still the `txt` written for other readers; the TrueType family of the
|
||||
/// style's `ACAD` xdata (`1000` typeface) when it has no font file (how AutoCAD stores TrueType
|
||||
/// styles); otherwise the file.
|
||||
fn style_font(tags: &[Tag], file: String) -> String {
|
||||
if file.trim().is_empty()
|
||||
&& let Some(face) =
|
||||
crate::dxf_ext::xdata(tags, "ACAD").iter().find(|t| t.code == 1000).map(|t| t.str().trim().to_string()).filter(|f| !f.is_empty())
|
||||
{
|
||||
return face;
|
||||
}
|
||||
let x = crate::dxf_ext::xdata(tags, crate::dxf_ext::APP);
|
||||
let own = x.windows(2).find_map(|w| match w {
|
||||
[m, n] if m.code == 1000 && m.str() == "FONT" && n.code == 1000 => Some(n.str()),
|
||||
_ => None,
|
||||
});
|
||||
let stem = file.trim().to_ascii_lowercase();
|
||||
match own {
|
||||
Some(name) if !name.trim().is_empty() && (stem == "txt" || stem == "txt.shx") => name,
|
||||
_ => file,
|
||||
}
|
||||
}
|
||||
|
||||
/// Load symbol tables and resolve handle references, indexing layer names for case-insensitive updates.
|
||||
pub(super) fn tables(tags: &[Tag], d: &mut Drawing, rx: &mut Rx) {
|
||||
let recs = records(tags);
|
||||
// STYLE handle → name, or the shape file of an unnamed shape style (linetype elements).
|
||||
let ltype_styles: HashMap<String, String> = recs
|
||||
.iter()
|
||||
.filter(|(k, _)| k == "STYLE")
|
||||
.filter_map(|(_, tg)| {
|
||||
let t = T(tg);
|
||||
let name = t.s(2).filter(|n| !n.trim().is_empty());
|
||||
Some((t.s(5)?.trim().to_ascii_uppercase(), name.or_else(|| t.s(3))?))
|
||||
})
|
||||
.collect();
|
||||
// Handle maps first: DIMSTYLE records refer to text styles and arrow blocks by handle.
|
||||
for (kind, tg) in &recs {
|
||||
let t = T(tg);
|
||||
let (Some(h), Some(n)) = (t.s(5), t.s(2)) else { continue };
|
||||
match kind.as_str() {
|
||||
"STYLE" => {
|
||||
rx.styles.insert(h.trim().to_ascii_uppercase(), n);
|
||||
}
|
||||
"BLOCK_RECORD" => {
|
||||
let units = t.i(70).and_then(|u| u8::try_from(u).ok()).unwrap_or(0);
|
||||
rx.block_props.insert(n.to_ascii_uppercase(), (units, t.i(280).unwrap_or(1) != 0));
|
||||
if let Some(a) = crate::dxf_ext::read_array(tg) {
|
||||
rx.block_arrays.insert(n.to_ascii_uppercase(), a);
|
||||
}
|
||||
rx.brs.insert(h.trim().to_ascii_uppercase(), n);
|
||||
}
|
||||
"LTYPE" => {
|
||||
rx.ltypes.insert(h.trim().to_ascii_uppercase(), n);
|
||||
}
|
||||
"LAYER" => {
|
||||
rx.layers.insert(h.trim().to_ascii_uppercase(), n);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut active_vport = false;
|
||||
// Index of layers already appended, by lower-cased name, so each LAYER record updates or
|
||||
// appends in O(1) instead of scanning the growing layer list — otherwise many distinct layer
|
||||
// names make import quadratic (CWE-407).
|
||||
let mut layer_index: HashMap<String, usize> = d.layers.iter().enumerate().map(|(i, l)| (l.name.to_ascii_lowercase(), i)).collect();
|
||||
for (kind, tg) in recs {
|
||||
let t = T(&tg);
|
||||
match kind.as_str() {
|
||||
"LAYER" => {
|
||||
let name = t.s(2).unwrap_or_default();
|
||||
if name.is_empty() || d.layers.len() >= MAX_OBJECTS {
|
||||
continue;
|
||||
}
|
||||
let aci = t.i(62).unwrap_or(7);
|
||||
let flags = t.i(70).unwrap_or(0);
|
||||
let mut l = Layer {
|
||||
name: name.clone(),
|
||||
color: Color::Index(aci.unsigned_abs().clamp(1, 255) as u8),
|
||||
linetype: t.s(6).unwrap_or_else(|| "Continuous".into()),
|
||||
lineweight: t.i(370).map(|v| Lineweight::from_dxf(v as i16)).unwrap_or(Lineweight::Default),
|
||||
on: aci >= 0,
|
||||
frozen: flags & 1 != 0,
|
||||
locked: flags & 4 != 0,
|
||||
vp_freeze_new: flags & 2 != 0,
|
||||
plot: t.i(290).unwrap_or(1) != 0,
|
||||
..Layer::default()
|
||||
};
|
||||
if let Some(tc) = t.i(420) {
|
||||
l.color = Color::True(Rgb::from_u32(tc as u32));
|
||||
}
|
||||
if let Some(n) = t.s(390).and_then(|h| rx.pstyles.get(&h.trim().to_ascii_uppercase())) {
|
||||
l.plot_style = n.clone();
|
||||
}
|
||||
let alpha = crate::dxf_ext::xdata(&tg, crate::dxf_ext::LAYER_TRANSPARENCY_APP).iter().find(|x| x.code == 1071);
|
||||
if let Some(pct) = alpha.and_then(|x| crate::dxf_ext::transparency_from_dxf(x.i64())) {
|
||||
l.transparency = pct;
|
||||
}
|
||||
if let Some(desc) = crate::dxf_ext::xdata(&tg, crate::dxf_ext::LAYER_DESCRIPTION_APP).iter().filter(|x| x.code == 1000).nth(1) {
|
||||
l.description = desc.str();
|
||||
}
|
||||
match layer_index.get(&name.to_ascii_lowercase()) {
|
||||
Some(&i) => {
|
||||
if let Some(x) = d.layers.get_mut(i) {
|
||||
*x = l;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
layer_index.insert(name.to_ascii_lowercase(), d.layers.len());
|
||||
d.layers.push(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
"LTYPE" => {
|
||||
let name = t.s(2).unwrap_or_default();
|
||||
if name.is_empty() || d.linetype(&name).is_some() || d.linetypes.len() >= MAX_OBJECTS {
|
||||
continue;
|
||||
}
|
||||
let pattern = ltype_pattern(&tg, <ype_styles);
|
||||
d.linetypes.push(Linetype { name, description: t.s(3).unwrap_or_default(), pattern });
|
||||
}
|
||||
"STYLE" => {
|
||||
let name = t.s(2).unwrap_or_default();
|
||||
if name.is_empty() || d.text_styles.len() >= MAX_OBJECTS {
|
||||
continue;
|
||||
}
|
||||
// 70 bit 4: vertical; 71 bits 2/4: backwards / upside down.
|
||||
let gen_flags = t.i(71).unwrap_or(0);
|
||||
let st = TextStyle {
|
||||
name: name.clone(),
|
||||
font: style_font(&tg, t.s(3).unwrap_or_default()),
|
||||
big_font: t.s(4).unwrap_or_default(),
|
||||
height: t.fd(40, 0.0),
|
||||
width_factor: t.fd(41, 1.0),
|
||||
oblique: t.fd(50, 0.0).to_radians(),
|
||||
backwards: gen_flags & 2 != 0,
|
||||
upside_down: gen_flags & 4 != 0,
|
||||
vertical: t.i(70).unwrap_or(0) & 4 != 0,
|
||||
annotative: annotative(&tg),
|
||||
};
|
||||
match d.text_styles.iter_mut().find(|s| s.name.eq_ignore_ascii_case(&name)) {
|
||||
Some(x) => *x = st,
|
||||
None => d.text_styles.push(st),
|
||||
}
|
||||
}
|
||||
// The first *ACTIVE viewport holds the current snap/grid settings.
|
||||
"VPORT" if !active_vport && t.s(2).is_some_and(|n| n.trim().eq_ignore_ascii_case("*ACTIVE")) => {
|
||||
active_vport = true;
|
||||
vport_record(d, &t);
|
||||
for (name, code) in [("SNAPUNIT", 14), ("GRIDUNIT", 15)] {
|
||||
if let (Some(x), Some(y)) = (t.f(code), t.f(code + 10)) {
|
||||
let p = Vec2::new(x, y);
|
||||
if p.is_finite() && if name == "GRIDUNIT" { p.x >= 0.0 && p.y >= 0.0 } else { p.x > 0.0 && p.y > 0.0 } {
|
||||
d.header.set(name, HVal::Point(p.to3(0.0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let (Some(x), Some(y)) = (t.f(13), t.f(23))
|
||||
&& x.is_finite()
|
||||
&& y.is_finite()
|
||||
{
|
||||
d.header.set("SNAPBASE", HVal::Point(Vec3::new(x, y, 0.0)));
|
||||
}
|
||||
if let Some(a) = t.f(50).filter(|a| a.is_finite()) {
|
||||
d.header.set_f64("SNAPANG", a);
|
||||
}
|
||||
}
|
||||
"VIEW" => {
|
||||
let name = t.s(2).unwrap_or_default();
|
||||
let (h, w) = (t.fd(40, 0.0), t.fd(41, 0.0));
|
||||
if name.is_empty() || !(h.is_finite() && w.is_finite() && h > 0.0 && w > 0.0) || d.views.len() >= MAX_OBJECTS {
|
||||
continue;
|
||||
}
|
||||
let center = t.p(10).xy();
|
||||
if !(center.x.is_finite() && center.y.is_finite()) {
|
||||
continue;
|
||||
}
|
||||
let x = crate::dxf_ext::xdata(&tg, crate::dxf_ext::APP);
|
||||
let field = |key: &str| {
|
||||
x.iter().position(|t| t.code == 1000 && t.str() == key).and_then(|i| x.get(i + 1)).filter(|t| t.code == 1000).map(Tag::str)
|
||||
};
|
||||
// 70 bit 1: a paper space view; its layout is CADCraft xdata.
|
||||
let layout = (t.i(70).unwrap_or(0) & 1 != 0).then(|| field("LAYOUT").unwrap_or_default());
|
||||
let twist = t.f(50).filter(|a| a.is_finite()).map_or(0.0, |a| a.rem_euclid(360.0).to_radians());
|
||||
let v = NamedView { name: name.clone(), center, height: h, width: w, layer_state: field("LAYERSTATE"), twist, layout };
|
||||
match d.views.iter_mut().find(|x| x.name.eq_ignore_ascii_case(&name)) {
|
||||
Some(x) => *x = v,
|
||||
None => d.views.push(v),
|
||||
}
|
||||
}
|
||||
"UCS" => {
|
||||
let name = t.s(2).unwrap_or_default();
|
||||
let (origin, x_axis, y_axis) = (t.p(10), t.p(11), t.p(12));
|
||||
let finite = [origin, x_axis, y_axis].iter().all(|p| p.x.is_finite() && p.y.is_finite() && p.z.is_finite());
|
||||
if name.is_empty() || !finite || d.ucss.len() >= MAX_OBJECTS {
|
||||
continue;
|
||||
}
|
||||
let u = Ucs { name: name.clone(), origin, x_axis, y_axis };
|
||||
match d.ucss.iter_mut().find(|x| x.name.eq_ignore_ascii_case(&name)) {
|
||||
Some(x) => *x = u,
|
||||
None => d.ucss.push(u),
|
||||
}
|
||||
}
|
||||
"DIMSTYLE" => {
|
||||
let name = t.s(2).unwrap_or_default();
|
||||
if name.is_empty() || d.dim_styles.len() >= MAX_OBJECTS {
|
||||
continue;
|
||||
}
|
||||
// Every variable with a DIMSTYLE group code; values the style can't take are
|
||||
// left at their defaults.
|
||||
let mut fields = serde_json::Map::new();
|
||||
// The first 70 is the record's flags; a later one is DIMTFILLCLR.
|
||||
let flags = tg.iter().position(|x| x.code == 70);
|
||||
for (i, tag) in tg.iter().enumerate().take_while(|(_, x)| x.code < 1000) {
|
||||
if Some(i) == flags {
|
||||
continue;
|
||||
}
|
||||
let Some((field, kind)) = crate::dxf_ext::dim_field(tag.code) else { continue };
|
||||
if let Some(v) = crate::dxf_ext::decode(kind, tag, &rx.styles, &rx.brs, &rx.ltypes) {
|
||||
fields.insert(field.to_string(), v);
|
||||
}
|
||||
}
|
||||
fields.extend(crate::dxf_ext::read_newer_dimvars(&tg, &rx.styles, &rx.brs, &rx.ltypes));
|
||||
let mut st = DimStyle { name: name.clone(), annotative: annotative(&tg), ..DimStyle::default() };
|
||||
st.apply_fields(&fields);
|
||||
match d.dim_styles.iter_mut().find(|s| s.name.eq_ignore_ascii_case(&name)) {
|
||||
Some(x) => *x = st,
|
||||
None => d.dim_styles.push(st),
|
||||
}
|
||||
}
|
||||
"VPORT" => vport_record(d, &t),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// One *Active viewport is no tiling: the view is the editor's (it zooms to the extents).
|
||||
d.vports.retain(|c| c.tiles.len() > usize::from(c.is_active()));
|
||||
}
|
||||
|
||||
/// Most tiles a viewport configuration keeps.
|
||||
const MAX_VPORT_TILES: usize = 64;
|
||||
|
||||
/// A VPORT record: one tile of the configuration it names (`*ACTIVE` the one shown). Corners
|
||||
/// are fractions of the drawing area; unusable tiles are skipped.
|
||||
fn vport_record(d: &mut Drawing, t: &T) {
|
||||
let name = t.s(2).map(|n| n.trim().to_string()).unwrap_or_default();
|
||||
let (min, max, center) = (t.p(10).xy(), t.p(11).xy(), t.p(12).xy());
|
||||
let height = t.fd(40, 0.0);
|
||||
let unit = |v: f64| v.is_finite() && (-1e-9..=1.0 + 1e-9).contains(&v);
|
||||
let ok = [min.x, min.y, max.x, max.y].into_iter().all(unit)
|
||||
&& max.x > min.x + 1e-6
|
||||
&& max.y > min.y + 1e-6
|
||||
&& center.is_finite()
|
||||
&& height.is_finite()
|
||||
&& height > 0.0;
|
||||
if name.is_empty() || !ok {
|
||||
return;
|
||||
}
|
||||
let aspect = t.f(41).filter(|a| a.is_finite() && *a > 0.0).unwrap_or(0.0);
|
||||
let clamp = |p: Vec2| Vec2::new(p.x.clamp(0.0, 1.0), p.y.clamp(0.0, 1.0));
|
||||
let tile = VportTile { min: clamp(min), max: clamp(max), center, height, aspect };
|
||||
let name = if name.eq_ignore_ascii_case(VportConfig::ACTIVE) { VportConfig::ACTIVE.to_string() } else { name };
|
||||
let room = d.vports.len() < MAX_OBJECTS;
|
||||
match d.vports.iter_mut().find(|c| c.name.eq_ignore_ascii_case(&name)) {
|
||||
Some(c) if c.tiles.len() < MAX_VPORT_TILES => c.tiles.push(tile),
|
||||
Some(_) => {}
|
||||
None if room => d.vports.push(VportConfig { name, tiles: vec![tile] }),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The dash elements of an LTYPE record with their embedded text and shapes (DXF Reference,
|
||||
/// LTYPE): each `49` length starts an element; `74` type flags (1 absolute rotation, 2 text,
|
||||
/// 4 shape), `75` shape number, `340` STYLE, `46` scale, `50` rotation (radians), `44`/`45`
|
||||
/// offset and `9` text follow it.
|
||||
fn ltype_pattern(tags: &[Tag], styles: &HashMap<String, String>) -> Vec<DashElement> {
|
||||
let mut out: Vec<DashElement> = Vec::new();
|
||||
for t in tags.iter().take_while(|t| t.code < 1000) {
|
||||
if t.code == 49 {
|
||||
out.push(DashElement::dash(t.f64()));
|
||||
continue;
|
||||
}
|
||||
let Some(el) = out.last_mut() else { continue };
|
||||
let complex = el.text.is_some() || el.shape.is_some();
|
||||
let v = t.f64();
|
||||
match t.code {
|
||||
74 => {
|
||||
let flags = t.i64();
|
||||
el.absolute = flags & 1 != 0;
|
||||
if flags & 2 != 0 {
|
||||
el.text = Some(String::new());
|
||||
} else if flags & 4 != 0 {
|
||||
el.shape = Some(0);
|
||||
}
|
||||
}
|
||||
75 if el.shape.is_some() => el.shape = Some(u16::try_from(t.i64()).unwrap_or(0)),
|
||||
340 if complex => el.style = styles.get(&t.str().trim().to_ascii_uppercase()).filter(|n| !n.trim().is_empty()).cloned(),
|
||||
9 if el.text.is_some() => el.text = Some(t.str()),
|
||||
46 if complex && v.is_finite() => el.scale = v,
|
||||
50 if complex && v.is_finite() => el.rotation = v,
|
||||
44 if complex && v.is_finite() => el.offset.x = v,
|
||||
45 if complex && v.is_finite() => el.offset.y = v,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
use super::common::{T, common, ocs_n};
|
||||
use super::*;
|
||||
|
||||
/// The properties an ATTRIB carries itself: those it has groups for. Its layer counts only when
|
||||
/// it differs from its block reference's (an attribute defined on layer 0 goes on the reference's
|
||||
/// layer, as it is drawn anyway).
|
||||
pub(super) fn attrib_props(t: &T, insert_layer: &str) -> AttribProps {
|
||||
let c = common(t);
|
||||
AttribProps {
|
||||
layer: t.s(8).filter(|l| !l.is_empty() && !l.eq_ignore_ascii_case(insert_layer)),
|
||||
color: (t.i(62).is_some() || t.i(420).is_some()).then_some(c.color),
|
||||
linetype: t.s(6).filter(|l| !l.is_empty()),
|
||||
lineweight: t.i(370).is_some().then_some(c.lineweight),
|
||||
}
|
||||
}
|
||||
|
||||
/// An attribute's modes from its own groups: flags (70) 4 verify and 8 preset, and Lock
|
||||
/// position, the 280 after its tag in its subclass (a 280 before the tag is the subclass version).
|
||||
pub(super) fn attrib_modes(own: &[Tag]) -> AttribModes {
|
||||
let flags = own.iter().find(|t| t.code == 70).map(Tag::i64).unwrap_or(0);
|
||||
let sub = own.iter().position(|t| t.code == 100 && t.str().trim().starts_with("AcDbAttribute")).unwrap_or(0);
|
||||
let rest = own.get(sub..).unwrap_or_default();
|
||||
let lock_position = rest.iter().position(|t| t.code == 2).is_some_and(|at| rest.iter().skip(at + 1).any(|t| t.code == 280 && t.i64() != 0));
|
||||
AttribModes { verify: flags & 4 != 0, preset: flags & 8 != 0, lock_position }
|
||||
}
|
||||
|
||||
/// An ATTRIB's or ATTDEF's groups split at its embedded multiline text (`101 Embedded Object`):
|
||||
/// the attribute's own groups and, for a multiline attribute, the MTEXT groups after the marker.
|
||||
pub(super) fn split_embedded(tags: &[Tag]) -> (&[Tag], Option<&[Tag]>) {
|
||||
match tags.iter().position(|t| t.code == 101) {
|
||||
Some(i) => (tags.get(..i).unwrap_or(tags), tags.get(i + 1..)),
|
||||
None => (tags, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// A multiline attribute's text, from the MTEXT groups embedded in it.
|
||||
pub(super) fn multiline_attrib(embedded: Option<&[Tag]>, c: &Common) -> Option<MultilineAttrib> {
|
||||
let mtext = mtext_from(embedded.filter(|e| !e.is_empty())?, c);
|
||||
let read_as = cadcraft_fonts::plain_mtext(&mtext.contents);
|
||||
Some(MultilineAttrib { mtext, read_as })
|
||||
}
|
||||
|
||||
/// Multiline text from MTEXT groups (an MTEXT entity's, or a multiline attribute's embedded ones).
|
||||
pub(super) fn mtext_from(tags: &[Tag], c: &Common) -> MText {
|
||||
let t = T(tags);
|
||||
// Contents: code 3 chunks followed by code 1.
|
||||
let mut contents = String::new();
|
||||
for tg in tags.iter().filter(|x| x.code == 3) {
|
||||
contents.push_str(&tg.str());
|
||||
}
|
||||
contents.push_str(&t.s(1).unwrap_or_default());
|
||||
let contents = mtext_dxf::decode_contents(&contents);
|
||||
let columns = mtext_dxf::read_columns(tags);
|
||||
// The direction comes from the MTEXT's own groups only: in an R2018+ embedded object
|
||||
// (group 101), 11 is the insertion point, not the X-axis direction (#667, as in #834).
|
||||
let own = T(mtext_dxf::before_columns(tags));
|
||||
let mut rotation = match (own.f(11), own.f(21)) {
|
||||
(Some(x), Some(y)) if x != 0.0 || y != 0.0 => Vec2::new(x, y).angle(),
|
||||
_ => own.fd(50, 0.0).to_radians(),
|
||||
};
|
||||
let mut attach = t.i(71).unwrap_or(1).clamp(1, 9) as u8;
|
||||
if c.extrusion.z < 0.0 {
|
||||
// Insertion point and direction are WCS, but the line height runs along extrusion ×
|
||||
// direction: under (0,0,-1) the glyphs are mirrored. Drawn readable (MIRRTEXT = 0) with
|
||||
// the same footprint, the text turns round and left/right attachment swap.
|
||||
rotation = cadcraft_geom::norm_angle(rotation + std::f64::consts::PI);
|
||||
attach = match attach {
|
||||
1 | 4 | 7 => attach + 2,
|
||||
3 | 6 | 9 => attach - 2,
|
||||
a => a,
|
||||
};
|
||||
}
|
||||
MText {
|
||||
insert: t.p(10),
|
||||
height: t.fd(40, 0.2).max(1e-9),
|
||||
width: match &columns {
|
||||
Some((_, w)) if t.fd(41, 0.0) <= 0.0 => *w,
|
||||
_ => t.fd(41, 0.0).max(0.0),
|
||||
},
|
||||
attach,
|
||||
rotation,
|
||||
style: t.s(7).unwrap_or_else(|| "Standard".into()),
|
||||
contents,
|
||||
line_spacing: t.fd(44, 1.0),
|
||||
line_spacing_exact: t.i(73) == Some(2),
|
||||
columns: columns.map(|(c, _)| c),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn text_from(t: &T) -> Text {
|
||||
let h = match t.i(72).unwrap_or(0) {
|
||||
1 => HAlign::Center,
|
||||
2 => HAlign::Right,
|
||||
3 => HAlign::Aligned,
|
||||
4 => HAlign::Middle,
|
||||
5 => HAlign::Fit,
|
||||
_ => HAlign::Left,
|
||||
};
|
||||
let v = match t.i(73).or_else(|| t.i(74)).unwrap_or(0) {
|
||||
1 => VAlign::Bottom,
|
||||
2 => VAlign::Middle,
|
||||
3 => VAlign::Top,
|
||||
_ => VAlign::Baseline,
|
||||
};
|
||||
let align = if t.f(11).is_some() { Some(t.p(11)) } else { None };
|
||||
let mut text = Text {
|
||||
insert: t.p(10),
|
||||
align_pt: if h == HAlign::Left && v == VAlign::Baseline { None } else { align },
|
||||
height: t.fd(40, 0.2).max(1e-9),
|
||||
value: t.s(1).unwrap_or_default(),
|
||||
rotation: t.fd(50, 0.0).to_radians(),
|
||||
width_factor: t.fd(41, 1.0),
|
||||
oblique: t.fd(51, 0.0).to_radians(),
|
||||
style: t.s(7).unwrap_or_else(|| "Standard".into()),
|
||||
halign: h,
|
||||
valign: v,
|
||||
};
|
||||
let n = Vec3::new(t.fd(210, 0.0), t.fd(220, 0.0), t.fd(230, 1.0));
|
||||
if n.z < 0.0 {
|
||||
mirror_text(&mut text, align.map(|a| ocs_n(n, a)), ocs_n(n, t.p(10)));
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
/// TEXT / ATTRIB / ATTDEF with extrusion (0,0,-1): the points are OCS and the text runs from its
|
||||
/// insertion point towards -x in the WCS, with mirrored glyphs. CADCraft draws text readable
|
||||
/// (MIRRTEXT = 0), so it keeps the mirrored footprint: the direction turns to -rotation and
|
||||
/// left/right justification swap ends (start ↔ end point).
|
||||
fn mirror_text(t: &mut Text, align: Option<Vec3>, insert: Vec3) {
|
||||
t.rotation = cadcraft_geom::norm_angle(-t.rotation);
|
||||
let baseline_left = |t: &Text, p: Vec3| if t.valign == VAlign::Baseline { None } else { Some(p) };
|
||||
match t.halign {
|
||||
HAlign::Left => {
|
||||
// TL/ML/BL are placed by their alignment point, baseline-left text by its start point.
|
||||
t.halign = HAlign::Right;
|
||||
t.insert = insert;
|
||||
t.align_pt = Some(if t.valign == VAlign::Baseline { insert } else { align.unwrap_or(insert) });
|
||||
}
|
||||
HAlign::Right => {
|
||||
t.halign = HAlign::Left;
|
||||
t.insert = align.unwrap_or(insert);
|
||||
t.align_pt = baseline_left(t, t.insert);
|
||||
}
|
||||
HAlign::Aligned | HAlign::Fit => {
|
||||
t.insert = align.unwrap_or(insert);
|
||||
t.align_pt = Some(insert);
|
||||
}
|
||||
HAlign::Center | HAlign::Middle => {
|
||||
t.insert = insert;
|
||||
t.align_pt = align;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An ATTDEF, whose multiline text is embedded MTEXT after its own groups.
|
||||
pub(super) fn attdef(tags: &[Tag], c: &Common) -> Attrib {
|
||||
// A multiline definition has its text as embedded MTEXT after its own groups.
|
||||
let (own, embedded) = split_embedded(tags);
|
||||
let multiline = multiline_attrib(embedded, c);
|
||||
let own = T(own);
|
||||
let mut text = text_from(&own);
|
||||
if let Some(m) = &multiline {
|
||||
text.value.clone_from(&m.read_as);
|
||||
}
|
||||
Attrib {
|
||||
tag: own.s(2).unwrap_or_default(),
|
||||
text,
|
||||
invisible: own.i(70).unwrap_or(0) & 1 != 0,
|
||||
constant: own.i(70).unwrap_or(0) & 2 != 0,
|
||||
prompt: own.s(3).unwrap_or_default(),
|
||||
props: Default::default(),
|
||||
multiline,
|
||||
modes: attrib_modes(own.0),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user