diff --git a/crates/io/src/dxf_read.rs b/crates/io/src/dxf_read.rs index 4c797ed9e..4c19a1184 100644 --- a/crates/io/src/dxf_read.rs +++ b/crates/io/src/dxf_read.rs @@ -249,6 +249,130 @@ fn entity(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKind)> { } /// 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 { + let mut verts = Vec::new(); + let mut cur: Option = 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 +} + +/// An ATTDEF, whose multiline text is embedded MTEXT after its own groups. +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), + } +} + 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) { @@ -260,138 +384,16 @@ fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_left: &mut usi "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" => { - 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); - } - EntityKind::Arc(a) - } - "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); - } - EntityKind::Ellipse(e) - } - "LWPOLYLINE" => { - let mut verts = Vec::new(); - let mut cur: Option = 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)? - } - "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(); - } - EntityKind::Spline(sp) - } + "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" => { - // 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); - } - EntityKind::AttDef(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), - }) - } + "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 @@ -1109,6 +1111,91 @@ fn entity_extras(kind: &str, tags: &[Tag], h: Handle, k: &mut EntityKind, rx: &m /// 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. +fn polyline_entity(recs: &[(String, Vec)], i: &mut usize, t: &T) -> Option<(Common, EntityKind, Vec)> { + 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 = 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 = 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. +fn insert_attribs(recs: &[(String, Vec)], i: &mut usize, ins_layer: &str) -> Vec { + 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 +} + fn parse_entities( recs: &[(String, Vec)], d: &mut Drawing, @@ -1134,92 +1221,17 @@ fn parse_entities( i += 1; match kind.as_str() { "POLYLINE" => { - 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 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 = 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 Some(first) = fs.next() else { - *skipped = skipped.saturating_add(1); - continue; - }; - 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, - }) + 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 mut attribs = Vec::new(); let ins_layer = kindent.as_ref().map(|(c, _)| c.layer.clone()).unwrap_or_default(); - 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; - } - } + let attribs = insert_attribs(recs, &mut i, &ins_layer); if let Some((_, EntityKind::Insert(ins))) = kindent.as_mut() { ins.attribs = attribs; } @@ -1688,6 +1700,13 @@ pub fn read(bytes: &[u8]) -> Result { read_budgeted(bytes, 4_000_000) } +/// A block definition read from BLOCKS: name, base point, description, entities (owner, paper +/// flag, entity) and block flags. +type BlockDef = (String, Vec3, String, Vec<(Option, bool, Entity)>, i64); + +/// A layout read from OBJECTS: name, tab order, paper block handle and page setup. +type LayoutDef = (String, u32, String, PageSetup); + /// Import DXF sections while sharing one materialized-table-cell budget across blocks and entity sections. fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result { let tags = cadcraft_dxf::parse(bytes).map_err(|e| IoError::Format(e.to_string()))?; @@ -1700,8 +1719,8 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result { // A file without $INSUNITS declares no units (#1209); only new drawings take the template's. d.header.set_i64("INSUNITS", 0); let mut rx = Rx { pstyles: plot_style_names(&secs), ..Rx::default() }; - let mut blocks: Vec<(String, Vec3, String, Vec<(Option, bool, Entity)>, i64)> = Vec::new(); - let mut layout_objs: Vec<(String, u32, String, PageSetup)> = Vec::new(); + let mut blocks: Vec = Vec::new(); + let mut layout_objs: Vec = Vec::new(); let mut entities = Vec::new(); let mut objs = Objects::default(); // Every handle another program's file already uses is taken before anything gets a new one: @@ -1721,73 +1740,109 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result { match s.name.as_str() { "HEADER" => header(&s.tags, &mut d), "TABLES" => tables(&s.tags, &mut d, &mut rx), - "BLOCKS" => { - let recs = records(&s.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 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, &mut d, &mut rx, &mut n, &mut 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(&mut skipped, "BLOCKS", n); - } + "BLOCKS" => blocks.extend(read_blocks(&s.tags, &mut d, &mut rx, &mut table_cells_left, &mut skipped)), // A damaged file may split its entities over several ENTITIES sections (#865). "ENTITIES" => { let mut n = 0; entities.extend(parse_entities(&records(&s.tags), &mut d, &mut rx, &mut n, &mut table_cells_left)); count_skipped(&mut skipped, "ENTITIES", n); } - "OBJECTS" => { - for (k, tg) in records(&s.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))); - } - } - } + "OBJECTS" => read_objects(&s.tags, &mut objs, &mut layout_objs), _ => {} } } // A file CADCraft wrote names its own next handle (#495): its table records and objects // are numbered above it and rebuilt on save, so following $HANDSEED would drift every save. let own_seed = objs.xrecord_text(crate::dxf_ext::HANDSEED_KEY).and_then(|t| Handle::parse_hex(t.trim())); - // Layouts. + let (paper_block_for, first_layout) = setup_layouts(&mut d, &mut layout_objs, &rx); + place_blocks(&mut d, blocks, &mut rx, &paper_block_for, &first_layout); + place_entities(&mut d, entities, &rx, &paper_block_for, &first_layout); + apply_sortents(&mut d, &objs.sortents, &rx.brs, &paper_block_for, &first_layout); + objs.apply(&mut d, &rx); + crate::dxf_image::resolve(&secs, &mut d); + crate::dxf_mline::resolve(&secs, &mut d); + // Keep handles unique against the header's seed. + let seed = d.header.str("HANDSEED", ""); + if let Some(h) = own_seed.or_else(|| Handle::parse_hex(&seed)) { + d.bump_handseed(Handle(h.0.saturating_sub(1))); + } + if d.layer("0").is_none() { + d.layers.insert(0, Layer::default()); + } + crate::dxf_raw::read(&secs, &mut d); + d.skipped_records = skipped; + Ok(d) +} + +/// Read the BLOCKS section: the xref paths of external blocks, then each block's entities. +fn read_blocks( + tags: &[Tag], + d: &mut Drawing, + rx: &mut Rx, + table_cells_left: &mut usize, + skipped: &mut std::collections::BTreeMap, +) -> Vec { + 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`. +fn read_objects(tags: &[Tag], objs: &mut Objects, layout_objs: &mut Vec) { + 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. +fn setup_layouts(d: &mut Drawing, layout_objs: &mut [LayoutDef], rx: &Rx) -> (HashMap, String) { let mut paper_block_for: HashMap = HashMap::new(); layout_objs.sort_by_key(|l| l.1); - for (name, order, br, page) in &layout_objs { + for (name, order, br, page) in layout_objs.iter() { if name.eq_ignore_ascii_case("Model") { continue; } @@ -1806,7 +1861,12 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result { 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()); - // Blocks: model/paper space blocks hold entities in newer files. + (paper_block_for, first_layout) +} + +/// Place the BLOCKS entities: model and paper space blocks go to their space, the rest become +/// block definitions. +fn place_blocks(d: &mut Drawing, blocks: Vec, rx: &mut Rx, paper_block_for: &HashMap, first_layout: &str) { for (name, base, description, ents, xref) in blocks { let up = name.to_ascii_uppercase(); if up == "*MODEL_SPACE" { @@ -1817,7 +1877,7 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result { continue; } if up.starts_with("*PAPER_SPACE") { - let lname = paper_block_for.get(&up).cloned().unwrap_or_else(|| first_layout.clone()); + 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); @@ -1857,13 +1917,22 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result { } d.blocks.insert(name, std::sync::Arc::new(b)); } - // ENTITIES section: model space, or paper space when flagged (67=1). +} + +/// Place the ENTITIES section: model space, or paper space when flagged (67=1). +fn place_entities( + d: &mut Drawing, + entities: Vec<(Option, bool, Entity)>, + rx: &Rx, + paper_block_for: &HashMap, + 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.clone()); + 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); } @@ -1871,21 +1940,6 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result { d.model.push(e); } } - apply_sortents(&mut d, &objs.sortents, &rx.brs, &paper_block_for, &first_layout); - objs.apply(&mut d, &rx); - crate::dxf_image::resolve(&secs, &mut d); - crate::dxf_mline::resolve(&secs, &mut d); - // Keep handles unique against the header's seed. - let seed = d.header.str("HANDSEED", ""); - if let Some(h) = own_seed.or_else(|| Handle::parse_hex(&seed)) { - d.bump_handseed(Handle(h.0.saturating_sub(1))); - } - if d.layer("0").is_none() { - d.layers.insert(0, Layer::default()); - } - crate::dxf_raw::read(&secs, &mut d); - d.skipped_records = skipped; - Ok(d) } fn count_skipped(skipped: &mut std::collections::BTreeMap, section: &str, n: usize) { @@ -2054,13 +2108,7 @@ impl Objects { Some(text) } - 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); - } + 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 }; @@ -2070,6 +2118,9 @@ impl Objects { 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 @@ -2090,6 +2141,61 @@ impl Objects { } }); } + } + + 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 = 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), + } + } + } + + 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; @@ -2113,50 +2219,14 @@ impl Objects { 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; } - // 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), - } - } + 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); } } - // 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 = 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), - } - } + self.apply_groups(d); // Standard associativity, for dimensions without CADCraft's exact links. for tags in &self.dimassocs { dimassoc(d, tags); diff --git a/crates/io/src/pdf.rs b/crates/io/src/pdf.rs index a4d9f5ea7..3f35233a8 100644 --- a/crates/io/src/pdf.rs +++ b/crates/io/src/pdf.rs @@ -17,7 +17,7 @@ use std::fmt::Write as _; use cadcraft_color::Rgb; use cadcraft_doc::{Drawing, Handle, PageSetup, Space}; use cadcraft_geom::{Bounds2, Vec2}; -use cadcraft_render::{Kind, Sheet, clip, paper}; +use cadcraft_render::{DPrim, Kind, Sheet, clip, paper}; use serde_json::Value; use craft_pdf::write::backend as kr; @@ -808,7 +808,7 @@ fn krilla_failure(e: KrillaError) -> Failure { /// as text instead: the glyph at font size 1 under its glyph matrix (the matrix axes are one /// em). fn marks(s: &mut Surface<'_>, p: &SheetPage, groups: &Groups, fonts: &mut fonts::Fonts) -> std::result::Result<(), Failure> { - let (list, map) = (&p.list, &p.map); + let list = &p.list; let texts = fonts::texts(list); let white = Rgb(255, 255, 255); let mut batch: Option<(StrokeStyle, PathBuilder)> = None; @@ -829,100 +829,126 @@ fn marks(s: &mut Surface<'_>, p: &SheetPage, groups: &Groups, fonts: &mut fonts: let t = if prim.kind == Kind::Mask { 0 } else { prim.transparency.min(cadcraft_render::MAX_TRANSPARENCY) }; let w = f64::from(prim.lw) * PT_PER_MM; let w = if w.is_finite() && w > 0.0 { w.min(100.0) } else { 0.0 }; + let m = Mark { rgb, t, w }; match prim.kind { - Kind::Polyline | Kind::Infinite { .. } => { - let pts: Vec = match prim.kind { - Kind::Infinite { ray } => match (raw.first(), raw.get(1)) { - (Some(base), Some(dir)) => { - let a = map.pt(*base); - let dir_pt = map.pt(*base + *dir) - a; - match clip::clip_infinite(a, dir_pt, ray, &p.clip_pt) { - Some((x, y)) => vec![x, y], - None => continue, - } - } - _ => continue, - }, - _ => raw.iter().map(|q| map.pt(*q)).collect(), - }; - if pts.len() < 2 { - continue; - } - // With a vertex out of range, keep the pieces inside the plot area, widened so - // a wide stroke just outside it still shows its edge. - let runs = if pts.iter().all(|q| coord(*q).is_some()) { vec![pts] } else { clip::clip_polyline(&pts, &p.clip_pt.expand(w + 1.0)) }; - let style = StrokeStyle { rgb, width: w as f32, transparency: t }; - if t > 0 || batch.as_ref().is_some_and(|(b, _)| *b != style) { - flush(s, &mut batch); - } - for run in runs { - let Some(pts) = run.iter().map(|q| coord(*q)).collect::>>() else { continue }; - // The writer leaves out a path without extent, so a zero-length line is - // painted as the round-cap dot its stroke would make. - if let Some(&(x, y)) = pts.first() - && pts.iter().all(|q| *q == (x, y)) - { - flush(s, &mut batch); - let d = if w > 0.0 { w as f32 } else { HAIRLINE_DOT_PT }; - if let Some(path) = circle_path(x, y, d / 2.0) { - fill_path(s, &path, rgb, t); - } - continue; - } - let (_, pb) = batch.get_or_insert_with(|| (style, PathBuilder::new())); - for (j, (x, y)) in pts.iter().enumerate() { - if j == 0 { - pb.move_to(*x, *y); - } else { - pb.line_to(*x, *y); - } - } - } - } + Kind::Polyline | Kind::Infinite { .. } => polyline(s, &mut batch, p, prim, raw, m), Kind::Tris | Kind::Mask => { flush(s, &mut batch); // A wipeout's mask is paper white. - let rgb = if prim.kind == Kind::Mask { white } else { rgb }; - if let Some(text) = u32::try_from(i).ok().and_then(|i| texts.get(&i)) - && let Some(font) = fonts.font(&text.glyph.face) - { - glyph(s, font, text, map, rgb, t); - continue; - } - let mapped: Vec = raw.iter().map(|q| map.pt(*q)).collect(); - // With a vertex out of range, keep the parts inside the plot area. - let tris = if mapped.iter().all(|q| coord(*q).is_some()) { mapped } else { clip::clip_triangles(&mapped, &p.clip_pt) }; - let mut pb = PathBuilder::new(); - for tri in tris.as_chunks::<3>().0 { - let Some(v) = tri.iter().map(|q| coord(*q)).collect::>>() else { continue }; - for (j, (x, y)) in v.iter().enumerate() { - if j == 0 { - pb.move_to(*x, *y); - } else { - pb.line_to(*x, *y); - } - } - pb.close(); - } - if let Some(path) = pb.finish() { - fill_path(s, &path, rgb, t); - } - } - Kind::Point => { - let Some(q) = raw.first() else { continue }; - flush(s, &mut batch); - let half = (w.max(0.5)) / 2.0; - let c = map.pt(*q); - if let Some(path) = rect_path(c - Vec2::new(half, half), c + Vec2::new(half, half)) { - fill_path(s, &path, rgb, t); + let m = if prim.kind == Kind::Mask { Mark { rgb: white, ..m } } else { m }; + let text_font = + u32::try_from(i).ok().and_then(|i| texts.get(&i).copied()).and_then(|text| fonts.font(&text.glyph.face).map(|font| (text, font))); + match text_font { + Some((text, font)) => glyph(s, font, text, &p.map, m.rgb, m.t), + None => fill_tris(s, p, raw, m.rgb, m.t), } } + Kind::Point => point(s, &mut batch, &p.map, raw, m), } } flush(s, &mut batch); layer_changed(s, layer, None) } +/// How a primitive is painted: colour, transparency (percent) and lineweight (points). +#[derive(Clone, Copy)] +struct Mark { + rgb: Rgb, + t: u8, + w: f64, +} + +/// Stroke a polyline or infinite line, clipped to the plot area, into the open batch. +fn polyline(s: &mut Surface<'_>, batch: &mut Option<(StrokeStyle, PathBuilder)>, p: &SheetPage, prim: &DPrim, raw: &[Vec2], m: Mark) { + let Some(pts) = polyline_points(prim, raw, &p.map, &p.clip_pt) else { return }; + if pts.len() < 2 { + return; + } + // With a vertex out of range, keep the pieces inside the plot area, widened so + // a wide stroke just outside it still shows its edge. + let runs = if pts.iter().all(|q| coord(*q).is_some()) { vec![pts] } else { clip::clip_polyline(&pts, &p.clip_pt.expand(m.w + 1.0)) }; + let style = StrokeStyle { rgb: m.rgb, width: m.w as f32, transparency: m.t }; + if m.t > 0 || batch.as_ref().is_some_and(|(b, _)| *b != style) { + flush(s, batch); + } + for run in runs { + stroke_run(s, batch, &run, style, m); + } +} + +/// The page points of a polyline, or of an infinite line clipped to the plot area; `None` when +/// there is nothing to draw. +fn polyline_points(prim: &DPrim, raw: &[Vec2], map: &Map, clip_pt: &Bounds2) -> Option> { + match prim.kind { + Kind::Infinite { ray } => { + let (Some(base), Some(dir)) = (raw.first(), raw.get(1)) else { return None }; + let a = map.pt(*base); + let dir_pt = map.pt(*base + *dir) - a; + let (x, y) = clip::clip_infinite(a, dir_pt, ray, clip_pt)?; + Some(vec![x, y]) + } + _ => Some(raw.iter().map(|q| map.pt(*q)).collect()), + } +} + +/// Add one run of a polyline to the batch, or paint it as a dot when it has no extent. +fn stroke_run(s: &mut Surface<'_>, batch: &mut Option<(StrokeStyle, PathBuilder)>, run: &[Vec2], style: StrokeStyle, m: Mark) { + let Some(pts) = run.iter().map(|q| coord(*q)).collect::>>() else { return }; + // The writer leaves out a path without extent, so a zero-length line is + // painted as the round-cap dot its stroke would make. + if let Some(&(x, y)) = pts.first() + && pts.iter().all(|q| *q == (x, y)) + { + flush(s, batch); + let d = if m.w > 0.0 { m.w as f32 } else { HAIRLINE_DOT_PT }; + if let Some(path) = circle_path(x, y, d / 2.0) { + fill_path(s, &path, m.rgb, m.t); + } + return; + } + let (_, pb) = batch.get_or_insert_with(|| (style, PathBuilder::new())); + for (j, (x, y)) in pts.iter().enumerate() { + if j == 0 { + pb.move_to(*x, *y); + } else { + pb.line_to(*x, *y); + } + } +} + +/// Fill a set of triangles, clipped to the plot area. +fn fill_tris(s: &mut Surface<'_>, p: &SheetPage, raw: &[Vec2], rgb: Rgb, t: u8) { + let mapped: Vec = raw.iter().map(|q| p.map.pt(*q)).collect(); + // With a vertex out of range, keep the parts inside the plot area. + let tris = if mapped.iter().all(|q| coord(*q).is_some()) { mapped } else { clip::clip_triangles(&mapped, &p.clip_pt) }; + let mut pb = PathBuilder::new(); + for tri in tris.as_chunks::<3>().0 { + let Some(v) = tri.iter().map(|q| coord(*q)).collect::>>() else { continue }; + for (j, (x, y)) in v.iter().enumerate() { + if j == 0 { + pb.move_to(*x, *y); + } else { + pb.line_to(*x, *y); + } + } + pb.close(); + } + if let Some(path) = pb.finish() { + fill_path(s, &path, rgb, t); + } +} + +/// Paint a point marker: a small square at the point. +fn point(s: &mut Surface<'_>, batch: &mut Option<(StrokeStyle, PathBuilder)>, map: &Map, raw: &[Vec2], m: Mark) { + let Some(q) = raw.first() else { return }; + flush(s, batch); + let half = (m.w.max(0.5)) / 2.0; + let c = map.pt(*q); + if let Some(path) = rect_path(c - Vec2::new(half, half), c + Vec2::new(half, half)) { + fill_path(s, &path, m.rgb, m.t); + } +} + /// Write one glyph as text in its embedded font: font size 1, placed by the glyph's matrix (one /// em along each axis). A glyph whose matrix is out of range or collapses is left out. fn glyph(s: &mut Surface<'_>, font: kr::text::Font, t: &cadcraft_render::PlotText, map: &Map, rgb: Rgb, transparency: u8) {