mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
craft-dxf: ACAD_TABLE and TABLESTYLE (read) (#994)
* craft-dxf: model, the drawing above the tags (header, tables, blocks, layouts) Document::read indexes a tag list into header variables, table records, blocks with their entities, the ENTITIES records (a POLYLINE with its vertices, an INSERT with its attributes) and the objects, each a slice of the tag list. The typed table records, LAYOUT and DICTIONARY are made on request. Also the colour index palette, colour, lineweight and transparency values, extended data lookup and the object coordinate system. Merged from CADCraft's cadcraft-io dxf_read (5a6aa9b6) and VectorCraft's vectorcraft-cad import (566f7208). Neither app's document types appear. * craft-dxf: model entities (geometry, text, MTEXT columns, hatches, dimensions) Entity::read types an entity record: the properties every entity has, and LINE, POINT, CIRCLE, ARC, ELLIPSE, LWPOLYLINE, POLYLINE with its vertices (2D, 3D, mesh, polyface), SPLINE, RAY, XLINE, TEXT, MTEXT with columns, ATTDEF, INSERT with its attributes (multiline ones included), DIMENSION, LEADER, SOLID, TRACE, 3DFACE, HATCH (polyline and edge loops, pattern lines, seeds, gradient), VIEWPORT, IMAGE, WIPEOUT and TOLERANCE. Other types stay readable from the record's tags. model::text has the caret encoding of strings, both ways. Merged from CADCraft's cadcraft-io dxf_read (5a6aa9b6) and VectorCraft's vectorcraft-cad import (566f7208). * craft-dxf: a hatch spline edge without fit data leaves the loop's source count Before R2010 a spline edge has no 97 of its own, so the 97 after it counts the loop's source entities. It was read as the edge's fit point count and the sources were lost. * craft-dxf: model::write, a drawing as tags (R12 to 2018) Drawing::tags writes the header, classes, the nine symbol tables, blocks, entities and (R2000 and later) the dictionaries, plot style placeholder and layouts around the layers, linetypes, styles, dimension styles, blocks and entities it is given, in the model's own types. entity_tags writes one entity for a caller with its own file. Handles given with the content are kept; the rest are numbered after them. The file skeleton is VectorCraft's vectorcraft-cad writer.rs (566f7208); the MTEXT column groups are CADCraft's dxf_read/mtext_dxf.rs column_tags (5a6aa9b6). * craft-dxf: model objects (page setup, groups, image definitions, draw order, clip filters, DIMASSOC, scales, multiline styles), HELIX and dimension overrides model::objects types the OBJECTS records both readers and CADCraft's passes use: the AcDbPlotSettings groups of a LAYOUT, GROUP, IMAGEDEF, SORTENTSTABLE, SPATIAL_FILTER, DIMASSOC, SCALE and MLINESTYLE, and dim_overrides reads the DSTYLE list of a dimension's ACAD extended data. Document gains objects_of and dictionary_names (the name a dictionary lists an object under). HELIX is an entity kind, read and written. From CADCraft's cadcraft-io dxf_read/objects.rs, document.rs, dimassoc.rs, entities.rs, dxf_mline.rs, dxf_clip.rs and dxf_image.rs (5a6aa9b6). * craft-dxf: an attribute's vertical alignment is group 74, not the field length in 73 Group 73 of an ATTRIB or ATTDEF is its field length. A writer that emits it turned every attribute into another vertical alignment, since 73 was read first and 74 only in its absence. Ported from storytold/cadcraft#1591. * README: keep main's crate list after the merge * craft-dxf: read entity groups written after the entity's own subclass marker Some writers leave the AcDbEntity subclass without a layer and put its groups right after the entity's own marker. Every entity has a layer, so its absence marks that layout, and the run of AcDbEntity codes that starts after the marker is read too. The run ends at the first other code, so the entity's own groups that reuse a code (an MTEXT's 48) are not taken for it. Ported from storytold/cadcraft#2877. * craft-dxf: an attribute's justification is read from its text groups, not its multiline header A multiline ATTRIB or ATTDEF writes a header in its AcDbAttribute subclass whose 72 and 11 are not the justification and alignment point. A left-justified attribute writes no 72 of its own, so the header's was read and the attribute came out centred. The text is now read from the groups up to the attribute's subclass and, after it, from all but 71, 72, 73 and 11. Ported from storytold/cadcraft#2313. * craft-dxf: MLINE, read and written EntityKind::MLine holds the style name and handle, scale, justification, flags, start point and each vertex with its direction, miter and the 41 and 42 parameter lists of each element. Reading the 41s and 42s into the list their 74 or 75 started keeps them apart. From CADCraft's cadcraft-io dxf_mline.rs read_entity and entity_tags (5a6aa9b6). * craft-dxf: MULTILEADER and MLEADERSTYLE, read model::mleader reads a MULTILEADER by level (the entity's groups, its CONTEXT_DATA block, its leader roots and their leader lines, which reuse the same codes) into MLeader, keeping the entity's and the context's copies of a value apart, and MLEADERSTYLE into MLeaderStyle. Roots, lines, vertices and attribute values are capped as CADCraft caps them. The writer leaves a multileader out and counts it, as it does any record it has no groups for. From CADCraft's cadcraft-io dxf_mleader.rs parse (5a6aa9b6). * craft-dxf: ACAD_TABLE and TABLESTYLE, read model::table reads an ACAD_TABLE's size, row heights, column widths and its cells (text, merge span, merged flag, text height outside the value block, alignment, text style, block) into Table, and a TABLESTYLE's margins, suppressed rows and its data, header and title cell types with their six borders into TableStyle. A size past 10,000 a side or 1,000,000 cells, or with no rows or columns, keeps nothing, and only cells the file holds are kept. The writer leaves a table out and counts it. From CADCraft's cadcraft-io dxf_read/entities.rs acad_table and dxf_table.rs read_table_style (5a6aa9b6). --------- Co-authored-by: Oliver Hagmann <268387159+ohagmann@users.noreply.github.com> Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com> Co-authored-by: Francesco BV <francesco.esposito@bvitalia.net>
This commit is contained in:
5 files changed
+339
-5
No files matched your search
@@ -22,6 +22,7 @@ mod entity;
|
||||
mod hatch;
|
||||
pub mod mleader;
|
||||
pub mod objects;
|
||||
pub mod table;
|
||||
mod tables;
|
||||
pub mod text;
|
||||
pub mod write;
|
||||
|
||||
@@ -532,6 +532,8 @@ pub enum EntityKind {
|
||||
MLine(MLine),
|
||||
/// Read only: [`super::write`] leaves it out (pass the record as raw tags).
|
||||
MLeader(super::mleader::MLeader),
|
||||
/// Read only: [`super::write`] leaves it out (pass the record as raw tags).
|
||||
Table(super::table::Table),
|
||||
Ray {
|
||||
base: Point,
|
||||
direction: Point,
|
||||
@@ -800,6 +802,7 @@ impl Entity {
|
||||
EntityKind::MLine(mline(own.0, &own))
|
||||
}
|
||||
"MULTILEADER" | "MLEADER" => EntityKind::MLeader(super::mleader::MLeader::read(e.tags)),
|
||||
"ACAD_TABLE" => EntityKind::Table(super::table::Table::read(e.tags)),
|
||||
"RAY" => EntityKind::Ray { base: line(&g).0, direction: line(&g).1 },
|
||||
"XLINE" => EntityKind::XLine { base: line(&g).0, direction: line(&g).1 },
|
||||
"TEXT" => EntityKind::Text(Text::read(&g)),
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
//! ACAD_TABLE and TABLESTYLE.
|
||||
//!
|
||||
//! An ACAD_TABLE's own groups (after `AcDbTable`) give its size, its row heights and column
|
||||
//! widths, then one run of groups per cell, each started by a `171` (the cell type), row by row.
|
||||
//! The table is drawn by its anonymous `*T` block (group 2), which a reader can show instead.
|
||||
//!
|
||||
//! From CADCraft's `cadcraft-io` (`dxf_read/entities.rs` `acad_table` and `dxf_table.rs`
|
||||
//! `read_table_style`, 5a6aa9b6).
|
||||
|
||||
use super::document::Record;
|
||||
use super::{Color, Groups, Lineweight, Point};
|
||||
use crate::Tag;
|
||||
|
||||
/// Most rows, and most columns, a table keeps.
|
||||
pub const MAX_SIDE: usize = 10_000;
|
||||
/// Most cells a table keeps.
|
||||
pub const MAX_CELLS: usize = 1_000_000;
|
||||
|
||||
/// A cell of an ACAD_TABLE.
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub struct TableCell {
|
||||
/// Group 171: 1 text, 2 a block.
|
||||
pub cell_type: i64,
|
||||
/// Groups 2 and 3 in order, then 1: the text as written.
|
||||
pub text: String,
|
||||
/// Groups 176 (rows) and 175 (columns): the span of a merge that starts here.
|
||||
pub span: Option<(u32, u32)>,
|
||||
/// Group 173 is not 0: the cell is part of a merge. Writers differ on whether the first cell
|
||||
/// of a merge has it.
|
||||
pub merged: bool,
|
||||
/// Group 140 outside the value block (`301` ... `304`, where 140 is a number): the cell's
|
||||
/// own text height.
|
||||
pub text_height: Option<f64>,
|
||||
/// Group 170: the alignment, 1–9.
|
||||
pub alignment: Option<i64>,
|
||||
/// Group 7: the cell's text style name.
|
||||
pub text_style: Option<String>,
|
||||
/// Group 340: the block of a block cell.
|
||||
pub block: Option<u64>,
|
||||
}
|
||||
|
||||
/// An ACAD_TABLE.
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub struct Table {
|
||||
/// Group 2: the anonymous block that draws the table.
|
||||
pub block: Option<String>,
|
||||
/// Group 342: the TABLESTYLE.
|
||||
pub style: Option<u64>,
|
||||
/// Group 10.
|
||||
pub insert: Point,
|
||||
/// Group 11: the horizontal direction.
|
||||
pub direction: Point,
|
||||
/// Groups 91 and 92: the rows and columns, as the file states them.
|
||||
pub rows: usize,
|
||||
pub columns: usize,
|
||||
/// Groups 141 and 142, as many as there are rows and columns at most.
|
||||
pub row_heights: Vec<f64>,
|
||||
pub column_widths: Vec<f64>,
|
||||
/// The cells there are, row by row: at most `rows * columns` of them.
|
||||
pub cells: Vec<TableCell>,
|
||||
}
|
||||
|
||||
impl Table {
|
||||
/// The cell at `row`, `column`, when the file has it.
|
||||
pub fn cell(&self, row: usize, column: usize) -> Option<&TableCell> {
|
||||
self.cells.get(row.checked_mul(self.columns)?.checked_add(column)?)
|
||||
}
|
||||
|
||||
/// The typed form of an ACAD_TABLE record. A table larger than [`MAX_SIDE`] a side or
|
||||
/// [`MAX_CELLS`] in all, or with no rows or no columns, keeps neither: its size reads as 0
|
||||
/// by 0, with no cells, row heights or column widths. A file states its size, and only cells
|
||||
/// it holds are kept, so a large declared size costs nothing.
|
||||
pub fn read(tags: &[Tag]) -> Table {
|
||||
let all = Groups(tags);
|
||||
let start = tags.iter().position(|t| t.code == 100 && t.str_ref().is_some_and(|s| s.trim() == "AcDbTable")).unwrap_or(0);
|
||||
let tail = tags.get(start..).unwrap_or(&[]);
|
||||
let first_cell = tail.iter().position(|t| t.code == 171).unwrap_or(tail.len());
|
||||
let head = Groups(tail.get(..first_cell).unwrap_or(&[]));
|
||||
let side = |code: i32| usize::try_from(head.i_or(code, 0)).ok().filter(|n| *n <= MAX_SIDE).unwrap_or(0);
|
||||
let (mut rows, mut columns) = (side(91), side(92));
|
||||
let cells_max = rows.saturating_mul(columns);
|
||||
if cells_max == 0 || cells_max > MAX_CELLS {
|
||||
(rows, columns) = (0, 0);
|
||||
}
|
||||
let mut t = Table {
|
||||
block: all.name(2),
|
||||
style: all.handle(342).filter(|h| *h != 0),
|
||||
insert: all.point(10),
|
||||
direction: all.point_opt(11).unwrap_or([1.0, 0.0, 0.0]),
|
||||
rows,
|
||||
columns,
|
||||
row_heights: head.0.iter().filter(|t| t.code == 141).take(rows).map(Tag::f64).collect(),
|
||||
column_widths: head.0.iter().filter(|t| t.code == 142).take(columns).map(Tag::f64).collect(),
|
||||
cells: Vec::new(),
|
||||
};
|
||||
// Cells, row by row, each from a 171 group; extended data ends the last one.
|
||||
let body = tail.get(first_cell..).unwrap_or(&[]);
|
||||
let body = body.get(..body.iter().position(|t| t.code >= 1000).unwrap_or(body.len())).unwrap_or(&[]);
|
||||
let starts: Vec<usize> = body.iter().enumerate().filter(|(_, t)| t.code == 171).map(|(i, _)| i).take(rows * columns).collect();
|
||||
for (k, s) in starts.iter().enumerate() {
|
||||
let end = starts
|
||||
.get(k + 1)
|
||||
.copied()
|
||||
.unwrap_or_else(|| body.get(s + 1..).and_then(|r| r.iter().position(|t| t.code == 171)).map_or(body.len(), |p| s + 1 + p));
|
||||
let cell = body.get(*s..end).unwrap_or(&[]);
|
||||
t.cells.push(read_cell(cell));
|
||||
}
|
||||
t
|
||||
}
|
||||
}
|
||||
|
||||
fn read_cell(cell: &[Tag]) -> TableCell {
|
||||
let g = Groups(cell);
|
||||
let mut text: String = cell.iter().filter(|t| t.code == 2 || t.code == 3).map(Tag::str).collect();
|
||||
text.push_str(&g.s(1).unwrap_or_default());
|
||||
let span = |code: i32| g.i(code).and_then(|v| u32::try_from(v).ok());
|
||||
// The cell's own text height: 140 outside the value block, where it is a number.
|
||||
let mut in_value = false;
|
||||
let mut text_height = None;
|
||||
for t in cell {
|
||||
match t.code {
|
||||
301 => in_value = true,
|
||||
304 => in_value = false,
|
||||
140 if !in_value && text_height.is_none() => text_height = Some(t.f64()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
TableCell {
|
||||
cell_type: g.i_or(171, 1),
|
||||
text,
|
||||
span: span(176).zip(span(175)),
|
||||
merged: g.i_or(173, 0) != 0,
|
||||
text_height,
|
||||
alignment: g.i(170),
|
||||
text_style: g.name(7),
|
||||
block: g.handle(340).filter(|h| *h != 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// One edge of a table cell style: top, right, bottom, left, inside horizontal, inside vertical.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Border {
|
||||
/// Groups 274 to 279.
|
||||
pub lineweight: Option<Lineweight>,
|
||||
/// Groups 284 to 289 (shown unless 0).
|
||||
pub visible: Option<bool>,
|
||||
/// Groups 64 to 69.
|
||||
pub color: Option<Color>,
|
||||
}
|
||||
|
||||
/// The groups of one cell type of a TABLESTYLE (data, header, title, in that order).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CellTypeStyle {
|
||||
/// Group 7: the text style name.
|
||||
pub text_style: String,
|
||||
/// Group 140.
|
||||
pub text_height: Option<f64>,
|
||||
/// Group 170: the alignment, 1–9.
|
||||
pub alignment: Option<i64>,
|
||||
/// Group 62.
|
||||
pub text_color: Option<Color>,
|
||||
/// Group 283 is not 0 and group 63: the fill colour.
|
||||
pub fill: Option<Color>,
|
||||
pub borders: [Border; 6],
|
||||
}
|
||||
|
||||
/// A TABLESTYLE.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct TableStyle {
|
||||
pub handle: Option<u64>,
|
||||
/// Group 3.
|
||||
pub description: String,
|
||||
/// Group 70: the flow direction (0 down, 1 up).
|
||||
pub flow: i64,
|
||||
/// Groups 40 and 41: the horizontal and vertical cell margins.
|
||||
pub margin_h: Option<f64>,
|
||||
pub margin_v: Option<f64>,
|
||||
/// Groups 280 and 281: the title and the header row are suppressed (not 0).
|
||||
pub title_suppressed: bool,
|
||||
pub header_suppressed: bool,
|
||||
/// Data, header and title, as many as the record has.
|
||||
pub cell_types: Vec<CellTypeStyle>,
|
||||
}
|
||||
|
||||
impl TableStyle {
|
||||
pub fn read(r: &Record<'_>) -> TableStyle {
|
||||
let start = r.tags.iter().position(|t| t.code == 100 && t.str_ref().is_some_and(|s| s.trim() == "AcDbTableStyle")).unwrap_or(0);
|
||||
let body = r.tags.get(start..).unwrap_or(&[]);
|
||||
let body = body.get(..body.iter().position(|t| t.code >= 1000).unwrap_or(body.len())).unwrap_or(&[]);
|
||||
// Groups up to the first cell type (code 7) are the style's own.
|
||||
let head_end = body.iter().position(|t| t.code == 7).unwrap_or(body.len());
|
||||
let head = Groups(body.get(..head_end).unwrap_or(&[]));
|
||||
let starts: Vec<usize> = body.iter().enumerate().filter(|(_, t)| t.code == 7).map(|(i, _)| i).take(3).collect();
|
||||
let cell_types = starts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(k, s)| {
|
||||
let end = starts
|
||||
.get(k + 1)
|
||||
.copied()
|
||||
.unwrap_or_else(|| body.get(s + 1..).and_then(|x| x.iter().position(|t| t.code == 7)).map_or(body.len(), |p| s + 1 + p));
|
||||
let g = Groups(body.get(*s..end).unwrap_or(&[]));
|
||||
let border = |k: i32| Border {
|
||||
lineweight: g.i(274 + k).map(Lineweight::from_dxf),
|
||||
visible: g.i(284 + k).map(|v| v != 0),
|
||||
color: g.i(64 + k).map(Color::from_index),
|
||||
};
|
||||
CellTypeStyle {
|
||||
text_style: g.s(7).unwrap_or_default().trim().to_string(),
|
||||
text_height: g.f(140),
|
||||
alignment: g.i(170),
|
||||
text_color: g.i(62).map(Color::from_index),
|
||||
fill: (g.i_or(283, 0) != 0).then(|| g.i(63).map_or(Color::Index(7), Color::from_index)),
|
||||
borders: [0, 1, 2, 3, 4, 5].map(border),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
TableStyle {
|
||||
handle: r.handle(),
|
||||
description: head.s(3).unwrap_or_default(),
|
||||
flow: head.i_or(70, 0),
|
||||
margin_h: head.f(40),
|
||||
margin_v: head.f(41),
|
||||
title_suppressed: head.i_or(280, 0) != 0,
|
||||
header_suppressed: head.i_or(281, 0) != 0,
|
||||
cell_types,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -748,7 +748,7 @@ impl W {
|
||||
}
|
||||
|
||||
/// The groups of an entity after its common properties; `false` for a kind that has none
|
||||
/// here ([`EntityKind::MLeader`] and [`EntityKind::Other`]).
|
||||
/// here ([`EntityKind::MLeader`], [`EntityKind::Table`] and [`EntityKind::Other`]).
|
||||
fn geometry(&mut self, kind: &EntityKind, c: &Common) -> bool {
|
||||
match kind {
|
||||
EntityKind::Line { start, end } => {
|
||||
@@ -884,7 +884,7 @@ impl W {
|
||||
}
|
||||
self.p(11, *direction);
|
||||
}
|
||||
EntityKind::MLeader(_) | EntityKind::Other => return false,
|
||||
EntityKind::MLeader(_) | EntityKind::Table(_) | EntityKind::Other => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -932,7 +932,7 @@ fn kind_name(kind: &EntityKind) -> Option<&'static str> {
|
||||
EntityKind::Image(_) => "IMAGE",
|
||||
EntityKind::Wipeout(_) => "WIPEOUT",
|
||||
EntityKind::Tolerance { .. } => "TOLERANCE",
|
||||
EntityKind::MLeader(_) | EntityKind::Other => return None,
|
||||
EntityKind::MLeader(_) | EntityKind::Table(_) | EntityKind::Other => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1051,7 +1051,7 @@ impl Ids {
|
||||
/// The tags of one entity (with its vertices or attributes and their SEQEND) for `format`, owned
|
||||
/// by `owner`: for a caller that writes its own file around them. Its handle is the entity's, or
|
||||
/// one from `next_handle`; followers get handles from `next_handle` too, which is moved past
|
||||
/// those used. Empty for [`EntityKind::MLeader`] and [`EntityKind::Other`].
|
||||
/// those used. Empty for [`EntityKind::MLeader`], [`EntityKind::Table`] and [`EntityKind::Other`].
|
||||
pub fn entity_tags(e: &Entity, format: Format, owner: u64, next_handle: &mut u64) -> Vec<Tag> {
|
||||
let mut w = W { v: format, t: Vec::new() };
|
||||
let mut ids = Ids { used: HashSet::new(), next: *next_handle, floor: 0 };
|
||||
@@ -1139,7 +1139,7 @@ const PAPER: &str = "*Paper_Space";
|
||||
|
||||
impl Drawing<'_> {
|
||||
/// The whole file as tags, ending with `0 EOF`, and how many entities were left out
|
||||
/// ([`EntityKind::MLeader`] and [`EntityKind::Other`], which it has no groups for: pass
|
||||
/// ([`EntityKind::MLeader`], [`EntityKind::Table`] and [`EntityKind::Other`], which it has no groups for: pass
|
||||
/// those in `raw_entities`).
|
||||
pub fn tags(&self) -> (Vec<Tag>, usize) {
|
||||
let v = self.format;
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
//! ACAD_TABLE and TABLESTYLE.
|
||||
|
||||
use craft_dxf::model::table::{MAX_SIDE, Table, TableStyle};
|
||||
use craft_dxf::model::{Color, Document, Entity, EntityKind, Lineweight, entity_records, records};
|
||||
use craft_dxf::{Tag, parse};
|
||||
|
||||
fn tags(text: &str) -> Vec<Tag> {
|
||||
let lines: Vec<&str> = text.split(['\n', '|']).map(str::trim).filter(|l| !l.is_empty()).map(|l| if l == "~" { "" } else { l }).collect();
|
||||
parse((lines.join("\n") + "\n").as_bytes()).expect("the test drawing parses")
|
||||
}
|
||||
|
||||
const TABLE: &str = "0|ACAD_TABLE|5|90|100|AcDbEntity|8|0|100|AcDbBlockReference|2|*T1|10|1.0|20|2.0|30|0.0|100|AcDbTable|280|0|342|5A|343|*T1|11|1.0|21|0.0|31|0.0
|
||||
90|22|91|2|92|2|93|0|94|0|95|0|96|0|141|0.5|141|0.7|142|3.0|142|4.0
|
||||
171|1|172|0|173|0|174|0|175|2|176|1|91|0|178|0|145|0.0|92|0|301|CELL_VALUE|93|6|90|4|1|Title|94|0|140|99.0|300|~|302|Title|304|ACVALUE_END|140|2.5|170|5|7|Bold
|
||||
171|1|172|0|173|1|174|0|175|0|176|0|91|0|178|0
|
||||
171|1|172|0|173|0|174|0|175|1|176|1|2|first-|3|part-|1|last
|
||||
171|2|172|0|173|0|174|0|175|1|176|1|340|B1
|
||||
1001|CADCRAFT|1000|TABLE";
|
||||
|
||||
fn read(text: &str) -> Table {
|
||||
let t = tags(text);
|
||||
let (e, _) = entity_records(&records(&t));
|
||||
let EntityKind::Table(table) = Entity::read(&e[0]).kind else { panic!("a table") };
|
||||
table
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_table_and_its_cells() {
|
||||
let t = read(TABLE);
|
||||
assert_eq!((t.block.as_deref(), t.style, t.insert, t.direction), (Some("*T1"), Some(0x5A), [1.0, 2.0, 0.0], [1.0, 0.0, 0.0]));
|
||||
assert_eq!((t.rows, t.columns, t.row_heights.as_slice(), t.column_widths.as_slice()), (2, 2, [0.5, 0.7].as_slice(), [3.0, 4.0].as_slice()));
|
||||
assert_eq!(t.cells.len(), 4);
|
||||
let title = t.cell(0, 0).expect("a cell");
|
||||
// The 140 inside the value block is a number, not the height; the extended data ends the last cell.
|
||||
assert_eq!(
|
||||
(title.text.as_str(), title.span, title.merged, title.text_height, title.alignment, title.text_style.as_deref()),
|
||||
("Title", Some((1, 2)), false, Some(2.5), Some(5), Some("Bold"))
|
||||
);
|
||||
assert_eq!((t.cell(0, 1).map(|c| c.merged), t.cell(0, 1).map(|c| c.span)), (Some(true), Some(Some((0, 0)))));
|
||||
assert_eq!(t.cell(1, 0).map(|c| c.text.as_str()), Some("first-part-last"));
|
||||
let block = t.cell(1, 1).expect("a cell");
|
||||
assert_eq!((block.cell_type, block.block, block.text.as_str()), (2, Some(0xB1), ""));
|
||||
assert_eq!((t.cell(2, 0), t.cell(usize::MAX, 2)), (None, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_size_a_table_cannot_have_keeps_nothing() {
|
||||
for (r, c) in [(0, 5), (5, 0), (MAX_SIDE + 1, 1), (2000, 2000)] {
|
||||
let t = read(&format!("0|ACAD_TABLE|100|AcDbTable|91|{r}|92|{c}|141|1.0|142|1.0|171|1|1|x"));
|
||||
assert_eq!((t.rows, t.columns, t.cells.len(), t.row_heights.len(), t.column_widths.len()), (0, 0, 0, 0, 0), "{r} x {c}");
|
||||
}
|
||||
// A large size the file backs with no cells costs nothing.
|
||||
let t = read("0|ACAD_TABLE|100|AcDbTable|91|1000|92|1000|171|1|1|only");
|
||||
assert_eq!((t.rows, t.columns, t.cells.len()), (1000, 1000, 1));
|
||||
// More cells than the size are left out.
|
||||
let t = read("0|ACAD_TABLE|100|AcDbTable|91|1|92|1|171|1|1|a|171|1|1|b");
|
||||
assert_eq!(t.cells.len(), 1);
|
||||
assert_eq!(read("0|ACAD_TABLE|91|-3|92|2").rows, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_table_style() {
|
||||
let t = tags(
|
||||
"0|SECTION|2|OBJECTS|0|TABLESTYLE|5|5A|100|AcDbTableStyle|3|Plain|70|0|71|0|40|0.06|41|0.07|280|0|281|1
|
||||
7|Standard|140|0.18|170|5|62|1|63|7|283|1|90|512|91|1|274|-2|275|50|284|1|285|0|64|2|65|0
|
||||
7|Bold|140|0.25|170|2|62|0|283|0
|
||||
7|Title|140|0.3
|
||||
1001|CADCRAFT|1000|CELLSTYLES|0|ENDSEC|0|EOF",
|
||||
);
|
||||
let d = Document::read(&t);
|
||||
let s = TableStyle::read(d.objects_of("TABLESTYLE").next().expect("the style"));
|
||||
assert_eq!(
|
||||
(s.handle, s.description.as_str(), s.flow, s.margin_h, s.margin_v, s.title_suppressed, s.header_suppressed),
|
||||
(Some(0x5A), "Plain", 0, Some(0.06), Some(0.07), false, true)
|
||||
);
|
||||
assert_eq!(s.cell_types.len(), 3);
|
||||
let data = &s.cell_types[0];
|
||||
assert_eq!(
|
||||
(data.text_style.as_str(), data.text_height, data.alignment, data.text_color, data.fill),
|
||||
("Standard", Some(0.18), Some(5), Some(Color::Index(1)), Some(Color::Index(7)))
|
||||
);
|
||||
assert_eq!(
|
||||
(data.borders[0].lineweight, data.borders[1].lineweight, data.borders[0].visible, data.borders[1].visible),
|
||||
(Some(Lineweight::ByBlock), Some(Lineweight::Hundredths(50)), Some(true), Some(false))
|
||||
);
|
||||
assert_eq!((data.borders[0].color, data.borders[1].color, data.borders[2].color), (Some(Color::Index(2)), Some(Color::ByBlock), None));
|
||||
let header = &s.cell_types[1];
|
||||
assert_eq!((header.text_style.as_str(), header.fill, header.text_color), ("Bold", None, Some(Color::ByBlock)));
|
||||
assert_eq!(s.cell_types[2].text_height, Some(0.3));
|
||||
let bare = tags("0|SECTION|2|OBJECTS|0|TABLESTYLE|0|ENDSEC|0|EOF");
|
||||
let empty = Document::read(&bare);
|
||||
assert!(TableStyle::read(empty.objects_of("TABLESTYLE").next().expect("the style")).cell_types.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_writer_leaves_a_table_out() {
|
||||
let t = tags(TABLE);
|
||||
let e = Entity::read(&entity_records(&records(&t)).0[0]);
|
||||
let (_, skipped) = craft_dxf::model::write::Drawing { entities: vec![e], ..Default::default() }.tags();
|
||||
assert_eq!(skipped, 1);
|
||||
}
|
||||
Reference in new issue
Block a user