diff --git a/crates/doc/src/entity.rs b/crates/doc/src/entity.rs index 0d1f5eb5f..089904109 100644 --- a/crates/doc/src/entity.rs +++ b/crates/doc/src/entity.rs @@ -9,130 +9,6 @@ use serde::{Deserialize, Serialize}; mod mline; pub use mline::*; -/// A database handle (DXF group 5), unique per drawing. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] -pub struct Handle(pub u64); - -impl Handle { - pub fn hex(self) -> String { - format!("{:X}", self.0) - } - pub fn parse_hex(s: &str) -> Option { - let s = s.trim(); - if !s.bytes().all(|b| b.is_ascii_hexdigit()) { - return None; - } - u64::from_str_radix(s, 16).ok().map(Handle) - } -} - -/// Lineweight in hundredths of a millimetre, or a logical value. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum Lineweight { - #[default] - ByLayer, - ByBlock, - Default, - /// 0..=211 (hundredths of mm). - Mm100(u16), -} - -impl Lineweight { - /// The standard lineweight values (mm × 100). - pub const STANDARD: [u16; 24] = [0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158, 200, 211]; - pub fn to_dxf(self) -> i16 { - match self { - Lineweight::ByLayer => -1, - Lineweight::ByBlock => -2, - Lineweight::Default => -3, - Lineweight::Mm100(v) => v.min(211) as i16, - } - } - pub fn from_dxf(v: i16) -> Lineweight { - match v { - -2 => Lineweight::ByBlock, - -3 => Lineweight::Default, - 0..=211 => Lineweight::Mm100(v as u16), - _ => Lineweight::ByLayer, - } - } - pub fn name(self) -> String { - match self { - Lineweight::ByLayer => "ByLayer".into(), - Lineweight::ByBlock => "ByBlock".into(), - Lineweight::Default => "Default".into(), - Lineweight::Mm100(v) => format!("{:.2} mm", f64::from(v) / 100.0), - } - } -} - -/// Transparency: ByLayer, ByBlock, or 0..=90 percent. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum Transparency { - #[default] - ByLayer, - ByBlock, - Percent(u8), -} - -impl Transparency { - /// "ByLayer", "ByBlock" or the percentage ("50"). - pub fn name(&self) -> String { - match self { - Transparency::ByLayer => "ByLayer".into(), - Transparency::ByBlock => "ByBlock".into(), - Transparency::Percent(p) => p.to_string(), - } - } -} - -/// Properties every entity has. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(default, rename_all = "camelCase")] -pub struct Common { - pub layer: String, - pub color: Color, - pub linetype: String, - pub lineweight: Lineweight, - pub ltscale: f64, - pub transparency: Transparency, - pub visible: bool, - pub thickness: f64, - pub extrusion: Vec3, - /// Named plot style: `ByLayer`, `ByBlock` or a style of the layout's named (`.stb`) plot - /// style table. - pub plot_style: String, - /// The annotation scales an annotative object supports (empty: not annotative). - #[serde(skip_serializing_if = "Vec::is_empty")] - pub anno_scales: Vec, - /// Other applications' extended data (xdata) as read from the drawing file: whole - /// applications, each from its `1001` group on. It is part of the object, so a copy of the - /// object (COPY, MIRROR, ARRAY, paste) carries it, as in AutoCAD. - #[serde(skip_serializing_if = "Vec::is_empty")] - pub xdata: Vec, -} - -impl Default for Common { - fn default() -> Self { - Common { - layer: "0".into(), - color: Color::ByLayer, - linetype: "ByLayer".into(), - lineweight: Lineweight::ByLayer, - ltscale: 1.0, - transparency: Transparency::ByLayer, - visible: true, - thickness: 0.0, - extrusion: Vec3::Z, - plot_style: "ByLayer".into(), - anno_scales: Vec::new(), - xdata: Vec::new(), - } - } -} - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Entity { pub handle: Handle, @@ -146,108 +22,6 @@ impl Entity { Entity { handle, common: Common::default(), kind } } } - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Line { - pub a: Vec3, - pub b: Vec3, -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Point { - pub p: Vec3, - #[serde(default)] - pub angle: f64, -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Circle { - pub center: Vec3, - pub radius: f64, -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Arc { - pub center: Vec3, - pub radius: f64, - /// Radians, CCW. - pub start: f64, - pub end: f64, -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Ellipse { - pub center: Vec3, - pub major: Vec3, - pub ratio: f64, - pub start: f64, - pub end: f64, -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct LwPolyline { - pub vertices: Vec, - pub closed: bool, - #[serde(default)] - pub const_width: f64, - #[serde(default)] - pub elevation: f64, - /// Polyline linetype generation (PLINEGEN). - #[serde(default)] - pub plinegen: bool, -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Polyline3d { - pub points: Vec, - pub closed: bool, -} -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum HAlign { - #[default] - Left, - Center, - Right, - Aligned, - Middle, - Fit, -} -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum VAlign { - #[default] - Baseline, - Bottom, - Middle, - Top, -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Text { - pub insert: Vec3, - #[serde(default)] - pub align_pt: Option, - pub height: f64, - pub value: String, - #[serde(default)] - pub rotation: f64, - #[serde(default = "one")] - pub width_factor: f64, - #[serde(default)] - pub oblique: f64, - #[serde(default = "standard")] - pub style: String, - #[serde(default)] - pub halign: HAlign, - #[serde(default)] - pub valign: VAlign, -} -impl Text { - /// The point the text is placed by, as the Insertion snap and the grip show it: the alignment - /// point (DXF group 11) of justified text, the start point (group 10) of left-baseline, - /// aligned and fit text. - pub fn justify_point(&self) -> Vec2 { - match (self.halign, self.valign) { - (HAlign::Left, VAlign::Baseline) | (HAlign::Aligned | HAlign::Fit, _) => self.insert.xy(), - _ => self.align_pt.unwrap_or(self.insert).xy(), - } - } -} fn one() -> f64 { 1.0 } @@ -257,720 +31,27 @@ fn standard() -> String { mod helix; pub use helix::*; -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MText { - pub insert: Vec3, - pub height: f64, - /// Reference rectangle width (0 = no wrapping). - #[serde(default)] - pub width: f64, - /// 1..=9: TL TC TR ML MC MR BL BC BR. - #[serde(default = "attach_tl")] - pub attach: u8, - #[serde(default)] - pub rotation: f64, - #[serde(default = "standard")] - pub style: String, - /// Raw MTEXT contents with inline codes (\P, {\fArial|b1;...}, \S1/2; …). - pub contents: String, - #[serde(default = "one")] - pub line_spacing: f64, - /// Line spacing style (DXF 73): Exactly (`true`) keeps `line_spacing` whatever the character - /// heights; At least (`false`, the default) lets taller characters push lines apart. - #[serde(default)] - pub line_spacing_exact: bool, - /// Columns the text flows through, each `width` wide (DXF 75/76/78/79/48/49/50); `None`: - /// one column. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub columns: Option, -} -fn attach_tl() -> u8 { - 1 -} - -/// Static columns: a fixed number of columns of one height. Dynamic columns: as many columns -/// as the text needs, of one height (auto height) or each its own (manual heights). -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum MTextColumnType { - #[default] - Static, - Dynamic, -} - -/// MTEXT columns (MTEXT Columns option, DXF Reference MTEXT groups 75–79, 48–50). -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(default, rename_all = "camelCase")] -pub struct MTextColumns { - #[serde(rename = "type")] - pub kind: MTextColumnType, - /// Number of columns (static columns: the columns there are). - pub count: u16, - /// Space between two columns. - pub gutter: f64, - /// Height of every column (static, and dynamic with auto height). - pub height: f64, - /// Dynamic columns: one height for all columns (`height`) instead of `heights`. - pub auto_height: bool, - /// Dynamic columns with manual heights: the height of each column. - #[serde(skip_serializing_if = "Vec::is_empty")] - pub heights: Vec, - /// Columns run right to left. - pub flow_reversed: bool, -} - -impl MTextColumns { - /// The width of `count` columns of `column_width` and their gutters. - pub fn total_width(&self, column_width: f64) -> f64 { - let n = f64::from(self.count.max(1)); - column_width * n + self.gutter.max(0.0) * (n - 1.0) - } - - /// The tallest column. - pub fn max_height(&self) -> f64 { - self.heights.iter().copied().fold(self.height, f64::max).max(0.0) - } - - /// Scale every length (SCALE, block insertion). - pub fn scale(&mut self, s: f64) { - self.gutter *= s; - self.height *= s; - self.heights.iter_mut().for_each(|h| *h *= s); - } -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Attrib { - pub tag: String, - pub text: Text, - #[serde(default)] - pub invisible: bool, - #[serde(default)] - pub constant: bool, - #[serde(default)] - pub prompt: String, - /// An attribute's own properties; each one left unset follows the attribute definition. - #[serde(default, skip_serializing_if = "AttribProps::is_empty")] - pub props: AttribProps, - /// A multiline attribute (DXF attribute type 2, or 4 for a definition): its text as - /// multiline text. `text.value` holds the plain text, which value edits change. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub multiline: Option, - /// Its other modes: Verify, Preset and Lock position. - #[serde(default, skip_serializing_if = "AttribModes::is_default")] - pub modes: AttribModes, -} - -/// An attribute's modes beyond invisible and constant. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AttribModes { - /// Verify: the value is asked for twice when the block is inserted (DXF flag 4). - #[serde(default)] - pub verify: bool, - /// Preset: the default value is used without asking (DXF flag 8). - #[serde(default)] - pub preset: bool, - /// Lock position: the attribute can't be moved on its own within a reference (DXF group - /// 280 after the flags). A reference's attributes take it from their definitions. - #[serde(default)] - pub lock_position: bool, -} - -impl AttribModes { - pub fn is_default(&self) -> bool { - *self == Self::default() - } - /// The modes of a reference's attribute made from a definition with these: AutoCAD keeps - /// only Lock position there. - pub fn for_reference(self) -> Self { - AttribModes { lock_position: self.lock_position, ..Self::default() } - } -} - -/// The multiline text of a multiline attribute. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MultilineAttrib { - pub mtext: MText, - /// The plain text `mtext`'s contents read as: an attribute whose value differs was edited - /// since, and its value replaces the contents. - pub read_as: String, -} - -impl MultilineAttrib { - /// This text placed by `m` (a block reference's transform, for its attribute made from a - /// definition). - pub fn transformed(&self, m: &Mat3) -> MultilineAttrib { - let mut k = EntityKind::MText(self.mtext.clone()); - k.transform(m); - let mtext = if let EntityKind::MText(t) = k { t } else { self.mtext.clone() }; - MultilineAttrib { mtext, read_as: self.read_as.clone() } - } -} - -impl Attrib { - /// Move a multiline attribute's text by `m` along with the attribute (nothing for others). - pub fn transform_multiline(&mut self, m: &Mat3) { - if let Some(ml) = &mut self.multiline { - *ml = ml.transformed(m); - } - } - - /// A multiline attribute's text as drawn and saved: its multiline text, with the contents - /// replaced by the value when the value was edited since it was read. - pub fn multiline_text(&self) -> Option { - let m = self.multiline.as_ref()?; - let mut t = m.mtext.clone(); - if self.text.value != m.read_as { - t.contents = mtext_literal(&self.text.value); - } - Some(t) - } -} - -/// `s` as MTEXT contents that read back as `s`: backslashes and braces escaped, line breaks as -/// paragraph breaks. -pub fn mtext_literal(s: &str) -> String { - let mut out = String::with_capacity(s.len()); - for c in s.chars() { - match c { - '\\' => out.push_str("\\\\"), - '{' => out.push_str("\\{"), - '}' => out.push_str("\\}"), - '\n' => out.push_str("\\P"), - '\r' => {} - c => out.push(c), - } - } - out -} - -/// Properties set on one attribute of a block reference (an ATTRIB's layer, colour, linetype and -/// lineweight), overriding those of its definition. -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(default, rename_all = "camelCase")] -pub struct AttribProps { - #[serde(skip_serializing_if = "Option::is_none")] - pub layer: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub color: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub linetype: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub lineweight: Option, -} - -impl AttribProps { - /// Nothing set: the attribute is drawn with its definition's properties. - pub fn is_empty(&self) -> bool { - *self == AttribProps::default() - } - /// `base` (the definition's properties) with the ones set here in place. - pub fn over(&self, base: &Common) -> Common { - Common { - layer: self.layer.clone().unwrap_or_else(|| base.layer.clone()), - color: self.color.unwrap_or(base.color), - linetype: self.linetype.clone().unwrap_or_else(|| base.linetype.clone()), - lineweight: self.lineweight.unwrap_or(base.lineweight), - ..base.clone() - } - } -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Insert { - pub block: String, - pub insert: Vec3, - #[serde(default = "unit_scale")] - pub scale: Vec3, - #[serde(default)] - pub rotation: f64, - #[serde(default)] - pub attribs: Vec, - #[serde(default = "one_u")] - pub cols: u32, - #[serde(default = "one_u")] - pub rows: u32, - #[serde(default)] - pub col_spacing: f64, - #[serde(default)] - pub row_spacing: f64, -} -fn unit_scale() -> Vec3 { - Vec3::new(1.0, 1.0, 1.0) -} -fn one_u() -> u32 { - 1 -} -impl Insert { - /// Block-to-world transform (2D part). - pub fn transform(&self, base: Vec2) -> Mat3 { - Mat3::translate(self.insert.xy()) - .then_before(Mat3::rotate(self.rotation)) - .then_before(Mat3::scale(self.scale.x, self.scale.y)) - .then_before(Mat3::translate(-base)) - } -} -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum DimKind { - /// Rotated linear dimension (horizontal = 0, vertical = 90°). - Linear { - rotation: f64, - }, - Aligned, - Angular, - Angular3P, - Diameter, - Radius, - Ordinate { - x_type: bool, - }, - ArcLength, -} -/// A dimension. Definition points follow DXF: `defpt` (10: dimension line point), -/// `p13`/`p14` (extension origins), `p15`/`p16` (arc/angle points), `text_mid` (11). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Dimension { - pub kind: DimKind, - pub defpt: Vec3, - pub text_mid: Vec3, - #[serde(default)] - pub p13: Vec3, - #[serde(default)] - pub p14: Vec3, - #[serde(default)] - pub p15: Vec3, - #[serde(default)] - pub p16: Vec3, - /// "" = measured value, "<>" inside = measurement placeholder, " " = suppressed. - #[serde(default)] - pub text: String, - #[serde(default = "standard")] - pub style: String, - #[serde(default)] - pub measurement: f64, - #[serde(default)] - pub text_rotation: f64, - /// User moved the text (text_mid is authoritative). - #[serde(default)] - pub user_text_pos: bool, - /// Anonymous block with the rendered geometry (from files); regenerated when edited. - #[serde(default)] - pub block: Option, - /// Per-dimension style overrides (DIMOVERRIDE): DimStyle field names (camelCase) → values, - /// merged over the named style. - #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")] - pub overrides: serde_json::Map, - /// Associativity: which definition points follow which objects. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub assoc: Vec, - /// Oblique extension lines, jogs, breaks and inspection frames. - #[serde(default, skip_serializing_if = "DimMarks::is_empty")] - pub marks: DimMarks, -} - -/// What dimension edits add to a dimension's drawing: DIMEDIT Oblique, DIMJOGLINE, DIMJOGGED, -/// DIMBREAK and DIMINSPECT. -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(default, rename_all = "camelCase")] -pub struct DimMarks { - /// Angle (radians) of the extension lines of a linear or aligned dimension; None = - /// perpendicular to the dimension line. - #[serde(skip_serializing_if = "Option::is_none")] - pub oblique: Option, - /// A jog on the dimension line of a linear or aligned dimension, at this point (DIMJOGLINE). - #[serde(skip_serializing_if = "Option::is_none")] - pub jog_line: Option, - /// A jogged radius dimension (DIMJOGGED). - #[serde(skip_serializing_if = "Option::is_none")] - pub jogged: Option, - /// Gaps in the dimension and extension lines (DIMBREAK). - #[serde(skip_serializing_if = "Vec::is_empty")] - pub breaks: Vec, - /// Inspection frame, label and rate (DIMINSPECT). - #[serde(skip_serializing_if = "Option::is_none")] - pub inspect: Option, -} - -impl DimMarks { - pub fn is_empty(&self) -> bool { - *self == DimMarks::default() - } - - /// Transform the points and angles (part of [`EntityKind::transform`]). - fn transform(&mut self, t3: &dyn Fn(&mut Vec3), rot: f64, s: f64, mirror: bool) { - if let Some(a) = &mut self.oblique { - *a = cadcraft_geom::norm_angle(if mirror { rot - (*a - rot) } else { *a + rot }); - } - if let Some(j) = &mut self.jog_line { - t3(j); - } - if let Some(j) = &mut self.jogged { - t3(&mut j.center); - t3(&mut j.jog); - } - for b in &mut self.breaks { - t3(&mut b.at); - b.size *= s; - } - } -} - -/// The definition of a jogged radius dimension: the dimension line starts at an overriding -/// centre and jogs on its way to the arc. -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(default, rename_all = "camelCase")] -pub struct JoggedRadius { - /// Where the dimension line starts instead of the true centre. - pub center: Vec3, - /// The middle of the jog. - pub jog: Vec3, -} - -/// A gap in a dimension's lines: everything within `size / 2` of `at` is left out. -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(default, rename_all = "camelCase")] -pub struct DimBreak { - pub at: Vec3, - pub size: f64, - /// Made by DIMBREAK Auto (recomputed by the next Auto) rather than picked by hand. - pub auto: bool, -} - -/// An inspection dimension (DIMINSPECT). -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(default, rename_all = "camelCase")] -pub struct Inspection { - /// Frame shape: 0 round ends, 1 angular ends, 2 none. - pub shape: u8, - /// Text left of the value ("" = none). - pub label: String, - /// Inspection rate right of the value ("" = none). - pub rate: String, -} - -/// A dimension definition point that follows an object (associative dimensions). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DimAssoc { - /// Definition point: "defpt", "p13", "p14", "p15" or "p16". - pub point: String, - /// The object it is attached to. - pub handle: Handle, - pub snap: AssocSnap, -} - -/// Where on the object an associative definition point sits. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum AssocSnap { - /// Start point of a line/arc (or polyline vertex 0). - Start, - /// End point of a line/arc. - End, - Mid, - /// Centre of a circle/arc. - Center, - /// On a circle/arc at a fixed angle (radians) from its centre. - OnCircle { - angle: f64, - }, - /// Intersection of this (line) object with another line. - Intersection { - other: Handle, - }, - /// Vertex `index` of a polyline. - Vertex { - index: usize, - }, -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Leader { - pub vertices: Vec, - #[serde(default = "yes")] - pub arrow: bool, - #[serde(default)] - pub spline: bool, - #[serde(default = "standard")] - pub style: String, -} fn yes() -> bool { true } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MLeader { - pub leaders: Vec>, - pub landing: Vec3, - pub dogleg: f64, - pub text: Option, - #[serde(default = "standard")] - pub style: String, - #[serde(default = "one")] - pub arrow_size: f64, - /// Leader lines are smooth curves through their points (leader type Spline). An empty - /// `leaders` is a multileader without a leader line (leader type None). - #[serde(default)] - pub spline: bool, - /// Block content in place of the text: a block reference placed at the landing. - #[serde(default)] - pub block: Option, -} - -impl MLeader { - /// Each leader line as drawn: through its points to the landing, smoothed for spline leaders. - pub fn leader_paths(&self) -> Vec> { - self.leaders - .iter() - .map(|l| { - let pts: Vec = l.iter().map(|v| v.xy()).chain(std::iter::once(self.landing.xy())).collect(); - if self.spline && pts.len() > 2 { GSpline::from_fit_points(&pts).tessellate(1e-3) } else { pts } - }) - .collect() - } -} -/// A hatch boundary loop: closed polyline with bulges. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct HatchLoop { - pub vertices: Vec, - #[serde(default)] - pub outer: bool, - /// The objects the loop was made from (DXF group 97 count, 330 handles): an associative - /// hatch re-fills the loop from them when they change. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub sources: Vec, -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Hatch { - pub pattern: String, - pub solid: bool, - pub loops: Vec, - #[serde(default = "one")] - pub scale: f64, - #[serde(default)] - pub angle: f64, - #[serde(default)] - pub associative: bool, - /// 0 normal (odd parity), 1 outer, 2 ignore. - #[serde(default)] - pub style: u8, - #[serde(default)] - pub elevation: f64, - /// Gradient fill: name + second colour. - #[serde(default)] - pub gradient: Option, - #[serde(default)] - pub origin: Vec2, - #[serde(default)] - pub background: Option, - /// The pattern's line families as a file defined them, for a pattern that isn't in the - /// standard library (unit scale and angle, like the library's; empty otherwise). - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub pattern_lines: Vec, -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Gradient { - pub name: String, - pub color1: Color, - pub color2: Color, - #[serde(default)] - pub angle: f64, - #[serde(default)] - pub centered: bool, -} -/// SOLID / TRACE: up to four corners (DXF order 1,2,4,3 when drawn). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Solid { - pub corners: [Vec3; 4], -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Face3d { - pub corners: [Vec3; 4], - #[serde(default)] - pub hidden_edges: u8, -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct RayLine { - pub base: Vec3, - pub dir: Vec3, -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Viewport { - pub center: Vec3, - pub width: f64, - pub height: f64, - pub view_center: Vec2, - pub view_height: f64, - #[serde(default)] - pub id: u32, - #[serde(default)] - pub locked: bool, - #[serde(default)] - pub frozen_layers: Vec, - /// Per-viewport layer colour overrides (VP Color in the Layer Properties Manager). - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub layer_colors: Vec<(String, Color)>, - /// The paper-space entity whose outline clips the viewport (a closed polyline, circle, - /// ellipse or spline; DXF group 340). `None`: the rectangle `center`/`width`/`height`. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub clip: Option, - /// Display on (MVIEW ON/OFF). An off viewport shows its border but no model space. - #[serde(default = "yes")] - pub on: bool, -} -impl Default for Viewport { - fn default() -> Self { - Viewport { - center: Vec3::ZERO, - width: 1.0, - height: 1.0, - view_center: Vec2::ZERO, - view_height: 1.0, - id: 0, - locked: false, - frozen_layers: Vec::new(), - layer_colors: Vec::new(), - clip: None, - on: true, - } - } -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Image { - pub insert: Vec3, - /// One pixel's width (U) and height (V) vectors in drawing units. - pub u: Vec3, - pub v: Vec3, - /// Image size in pixels. - pub size: Vec2, - /// The image file (its IMAGEDEF object's path). - pub path: String, - /// The image definition's name (its key in the drawing's image dictionary). - #[serde(default, skip_serializing_if = "String::is_empty")] - pub name: String, - /// Clip boundary in pixel coordinates (origin at the top-left corner of the image, y down): - /// two opposite corners of a rectangle, or a polygon's vertices. Empty: the whole image. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub clip: Vec, - /// The clip boundary is applied. - #[serde(default)] - pub clipping: bool, - /// Display flags: 1 show the image, 2 show it when not aligned with the screen, 4 use the - /// clip boundary, 8 transparency on. - #[serde(default = "image_display")] - pub display: u16, - /// Brightness, contrast and fade, 0..=100. - #[serde(default = "image_fifty")] - pub brightness: u8, - #[serde(default = "image_fifty")] - pub contrast: u8, - #[serde(default)] - pub fade: u8, - /// The definition's default size of one pixel and its resolution unit (0 none, 2 cm, 5 inch). - #[serde(default = "image_pixel")] - pub pixel_size: Vec2, - #[serde(default)] - pub resolution_units: u8, -} -fn image_display() -> u16 { - 7 -} -fn image_fifty() -> u8 { - 50 -} -fn image_pixel() -> Vec2 { - Vec2::new(1.0, 1.0) -} -impl Default for Image { - fn default() -> Self { - Image { - insert: Vec3::ZERO, - u: Vec3::new(1.0, 0.0, 0.0), - v: Vec3::new(0.0, 1.0, 0.0), - size: Vec2::new(1.0, 1.0), - path: String::new(), - name: String::new(), - clip: Vec::new(), - clipping: false, - display: image_display(), - brightness: image_fifty(), - contrast: image_fifty(), - fade: 0, - pixel_size: image_pixel(), - resolution_units: 0, - } - } -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Wipeout { - pub boundary: Vec, -} -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TableCell { - pub text: String, - #[serde(default)] - pub merged: Option<(u32, u32)>, - /// The cell's own cell style; empty: its row's. - #[serde(default, skip_serializing_if = "String::is_empty")] - pub style: String, - /// A formula (`=Sum(A1:B3)`); `text` holds its value. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub formula: Option, - /// The cell's own text height (DXF 140 in the cell record), before the table's scale; - /// `None`: its cell style's. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub text_height: Option, -} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Table { - pub insert: Vec3, - pub col_widths: Vec, - pub row_heights: Vec, - pub cells: Vec>, - #[serde(default = "standard")] - pub style: String, - #[serde(default = "default_text_h")] - pub text_height: f64, - /// First row is a title (merged across, larger text). - #[serde(default)] - pub title: bool, - /// The row after the title holds column headers. - #[serde(default)] - pub header: bool, - /// The cell style of each row; missing or empty: by position (Title, Header, Data). - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub row_styles: Vec, - /// Table breaking (TABLEBREAK): the table drawn in several parts. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub breaks: Option, - /// The CSV file the cells come from (DATALINK). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub data_link: Option, -} -fn default_text_h() -> f64 { - 0.18 -} mod region; pub use region::*; -/// A raw DXF group: kept for round-tripping entities we do not model. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct RawTag { - pub code: i32, - pub value: String, -} +mod block; +mod common; +mod curves; +mod dim; +mod fill; +mod layout; +mod text; + +pub use block::{Attrib, AttribModes, AttribProps, Insert, MultilineAttrib, mtext_literal}; +pub use common::{Common, Handle, Lineweight, RawTag, Transparency}; +pub use curves::{Arc, Circle, Ellipse, Face3d, Line, LwPolyline, Point, Polyline3d, RayLine, Solid}; +pub use dim::{AssocSnap, DimAssoc, DimBreak, DimKind, DimMarks, Dimension, Inspection, JoggedRadius, Leader, MLeader}; +pub use fill::{Gradient, Hatch, HatchLoop, Wipeout}; +pub use layout::{Image, Table, TableCell, Viewport}; +pub use text::{HAlign, MText, MTextColumnType, MTextColumns, Text, VAlign}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Unknown { pub dxf_type: String, diff --git a/crates/doc/src/entity/block.rs b/crates/doc/src/entity/block.rs new file mode 100644 index 000000000..166afee27 --- /dev/null +++ b/crates/doc/src/entity/block.rs @@ -0,0 +1,182 @@ +use super::common::{Common, Lineweight}; +use super::text::{MText, Text}; +use super::*; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Attrib { + pub tag: String, + pub text: Text, + #[serde(default)] + pub invisible: bool, + #[serde(default)] + pub constant: bool, + #[serde(default)] + pub prompt: String, + /// An attribute's own properties; each one left unset follows the attribute definition. + #[serde(default, skip_serializing_if = "AttribProps::is_empty")] + pub props: AttribProps, + /// A multiline attribute (DXF attribute type 2, or 4 for a definition): its text as + /// multiline text. `text.value` holds the plain text, which value edits change. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub multiline: Option, + /// Its other modes: Verify, Preset and Lock position. + #[serde(default, skip_serializing_if = "AttribModes::is_default")] + pub modes: AttribModes, +} + +/// An attribute's modes beyond invisible and constant. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AttribModes { + /// Verify: the value is asked for twice when the block is inserted (DXF flag 4). + #[serde(default)] + pub verify: bool, + /// Preset: the default value is used without asking (DXF flag 8). + #[serde(default)] + pub preset: bool, + /// Lock position: the attribute can't be moved on its own within a reference (DXF group + /// 280 after the flags). A reference's attributes take it from their definitions. + #[serde(default)] + pub lock_position: bool, +} + +impl AttribModes { + pub fn is_default(&self) -> bool { + *self == Self::default() + } + /// The modes of a reference's attribute made from a definition with these: AutoCAD keeps + /// only Lock position there. + pub fn for_reference(self) -> Self { + AttribModes { lock_position: self.lock_position, ..Self::default() } + } +} + +/// The multiline text of a multiline attribute. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MultilineAttrib { + pub mtext: MText, + /// The plain text `mtext`'s contents read as: an attribute whose value differs was edited + /// since, and its value replaces the contents. + pub read_as: String, +} + +impl MultilineAttrib { + /// This text placed by `m` (a block reference's transform, for its attribute made from a + /// definition). + pub fn transformed(&self, m: &Mat3) -> MultilineAttrib { + let mut k = EntityKind::MText(self.mtext.clone()); + k.transform(m); + let mtext = if let EntityKind::MText(t) = k { t } else { self.mtext.clone() }; + MultilineAttrib { mtext, read_as: self.read_as.clone() } + } +} + +impl Attrib { + /// Move a multiline attribute's text by `m` along with the attribute (nothing for others). + pub fn transform_multiline(&mut self, m: &Mat3) { + if let Some(ml) = &mut self.multiline { + *ml = ml.transformed(m); + } + } + + /// A multiline attribute's text as drawn and saved: its multiline text, with the contents + /// replaced by the value when the value was edited since it was read. + pub fn multiline_text(&self) -> Option { + let m = self.multiline.as_ref()?; + let mut t = m.mtext.clone(); + if self.text.value != m.read_as { + t.contents = mtext_literal(&self.text.value); + } + Some(t) + } +} + +/// `s` as MTEXT contents that read back as `s`: backslashes and braces escaped, line breaks as +/// paragraph breaks. +pub fn mtext_literal(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + match c { + '\\' => out.push_str("\\\\"), + '{' => out.push_str("\\{"), + '}' => out.push_str("\\}"), + '\n' => out.push_str("\\P"), + '\r' => {} + c => out.push(c), + } + } + out +} + +/// Properties set on one attribute of a block reference (an ATTRIB's layer, colour, linetype and +/// lineweight), overriding those of its definition. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct AttribProps { + #[serde(skip_serializing_if = "Option::is_none")] + pub layer: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub color: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub linetype: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub lineweight: Option, +} + +impl AttribProps { + /// Nothing set: the attribute is drawn with its definition's properties. + pub fn is_empty(&self) -> bool { + *self == AttribProps::default() + } + /// `base` (the definition's properties) with the ones set here in place. + pub fn over(&self, base: &Common) -> Common { + Common { + layer: self.layer.clone().unwrap_or_else(|| base.layer.clone()), + color: self.color.unwrap_or(base.color), + linetype: self.linetype.clone().unwrap_or_else(|| base.linetype.clone()), + lineweight: self.lineweight.unwrap_or(base.lineweight), + ..base.clone() + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Insert { + pub block: String, + pub insert: Vec3, + #[serde(default = "unit_scale")] + pub scale: Vec3, + #[serde(default)] + pub rotation: f64, + #[serde(default)] + pub attribs: Vec, + #[serde(default = "one_u")] + pub cols: u32, + #[serde(default = "one_u")] + pub rows: u32, + #[serde(default)] + pub col_spacing: f64, + #[serde(default)] + pub row_spacing: f64, +} + +fn unit_scale() -> Vec3 { + Vec3::new(1.0, 1.0, 1.0) +} + +fn one_u() -> u32 { + 1 +} + +impl Insert { + /// Block-to-world transform (2D part). + pub fn transform(&self, base: Vec2) -> Mat3 { + Mat3::translate(self.insert.xy()) + .then_before(Mat3::rotate(self.rotation)) + .then_before(Mat3::scale(self.scale.x, self.scale.y)) + .then_before(Mat3::translate(-base)) + } +} diff --git a/crates/doc/src/entity/common.rs b/crates/doc/src/entity/common.rs new file mode 100644 index 000000000..6f7867ab3 --- /dev/null +++ b/crates/doc/src/entity/common.rs @@ -0,0 +1,132 @@ +use super::*; + +/// A database handle (DXF group 5), unique per drawing. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub struct Handle(pub u64); + +impl Handle { + pub fn hex(self) -> String { + format!("{:X}", self.0) + } + pub fn parse_hex(s: &str) -> Option { + let s = s.trim(); + if !s.bytes().all(|b| b.is_ascii_hexdigit()) { + return None; + } + u64::from_str_radix(s, 16).ok().map(Handle) + } +} + +/// Lineweight in hundredths of a millimetre, or a logical value. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum Lineweight { + #[default] + ByLayer, + ByBlock, + Default, + /// 0..=211 (hundredths of mm). + Mm100(u16), +} + +impl Lineweight { + /// The standard lineweight values (mm × 100). + pub const STANDARD: [u16; 24] = [0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158, 200, 211]; + pub fn to_dxf(self) -> i16 { + match self { + Lineweight::ByLayer => -1, + Lineweight::ByBlock => -2, + Lineweight::Default => -3, + Lineweight::Mm100(v) => v.min(211) as i16, + } + } + pub fn from_dxf(v: i16) -> Lineweight { + match v { + -2 => Lineweight::ByBlock, + -3 => Lineweight::Default, + 0..=211 => Lineweight::Mm100(v as u16), + _ => Lineweight::ByLayer, + } + } + pub fn name(self) -> String { + match self { + Lineweight::ByLayer => "ByLayer".into(), + Lineweight::ByBlock => "ByBlock".into(), + Lineweight::Default => "Default".into(), + Lineweight::Mm100(v) => format!("{:.2} mm", f64::from(v) / 100.0), + } + } +} + +/// Transparency: ByLayer, ByBlock, or 0..=90 percent. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum Transparency { + #[default] + ByLayer, + ByBlock, + Percent(u8), +} + +impl Transparency { + /// "ByLayer", "ByBlock" or the percentage ("50"). + pub fn name(&self) -> String { + match self { + Transparency::ByLayer => "ByLayer".into(), + Transparency::ByBlock => "ByBlock".into(), + Transparency::Percent(p) => p.to_string(), + } + } +} + +/// Properties every entity has. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct Common { + pub layer: String, + pub color: Color, + pub linetype: String, + pub lineweight: Lineweight, + pub ltscale: f64, + pub transparency: Transparency, + pub visible: bool, + pub thickness: f64, + pub extrusion: Vec3, + /// Named plot style: `ByLayer`, `ByBlock` or a style of the layout's named (`.stb`) plot + /// style table. + pub plot_style: String, + /// The annotation scales an annotative object supports (empty: not annotative). + #[serde(skip_serializing_if = "Vec::is_empty")] + pub anno_scales: Vec, + /// Other applications' extended data (xdata) as read from the drawing file: whole + /// applications, each from its `1001` group on. It is part of the object, so a copy of the + /// object (COPY, MIRROR, ARRAY, paste) carries it, as in AutoCAD. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub xdata: Vec, +} + +impl Default for Common { + fn default() -> Self { + Common { + layer: "0".into(), + color: Color::ByLayer, + linetype: "ByLayer".into(), + lineweight: Lineweight::ByLayer, + ltscale: 1.0, + transparency: Transparency::ByLayer, + visible: true, + thickness: 0.0, + extrusion: Vec3::Z, + plot_style: "ByLayer".into(), + anno_scales: Vec::new(), + xdata: Vec::new(), + } + } +} + +/// A raw DXF group: kept for round-tripping entities we do not model. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RawTag { + pub code: i32, + pub value: String, +} diff --git a/crates/doc/src/entity/curves.rs b/crates/doc/src/entity/curves.rs new file mode 100644 index 000000000..770b22559 --- /dev/null +++ b/crates/doc/src/entity/curves.rs @@ -0,0 +1,76 @@ +use super::*; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Line { + pub a: Vec3, + pub b: Vec3, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Point { + pub p: Vec3, + #[serde(default)] + pub angle: f64, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Circle { + pub center: Vec3, + pub radius: f64, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Arc { + pub center: Vec3, + pub radius: f64, + /// Radians, CCW. + pub start: f64, + pub end: f64, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Ellipse { + pub center: Vec3, + pub major: Vec3, + pub ratio: f64, + pub start: f64, + pub end: f64, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct LwPolyline { + pub vertices: Vec, + pub closed: bool, + #[serde(default)] + pub const_width: f64, + #[serde(default)] + pub elevation: f64, + /// Polyline linetype generation (PLINEGEN). + #[serde(default)] + pub plinegen: bool, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Polyline3d { + pub points: Vec, + pub closed: bool, +} + +/// SOLID / TRACE: up to four corners (DXF order 1,2,4,3 when drawn). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Solid { + pub corners: [Vec3; 4], +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Face3d { + pub corners: [Vec3; 4], + #[serde(default)] + pub hidden_edges: u8, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RayLine { + pub base: Vec3, + pub dir: Vec3, +} diff --git a/crates/doc/src/entity/dim.rs b/crates/doc/src/entity/dim.rs new file mode 100644 index 000000000..396e74c6f --- /dev/null +++ b/crates/doc/src/entity/dim.rs @@ -0,0 +1,226 @@ +use super::block::Insert; +use super::common::Handle; +use super::text::MText; +use super::*; + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum DimKind { + /// Rotated linear dimension (horizontal = 0, vertical = 90°). + Linear { + rotation: f64, + }, + Aligned, + Angular, + Angular3P, + Diameter, + Radius, + Ordinate { + x_type: bool, + }, + ArcLength, +} + +/// A dimension. Definition points follow DXF: `defpt` (10: dimension line point), +/// `p13`/`p14` (extension origins), `p15`/`p16` (arc/angle points), `text_mid` (11). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Dimension { + pub kind: DimKind, + pub defpt: Vec3, + pub text_mid: Vec3, + #[serde(default)] + pub p13: Vec3, + #[serde(default)] + pub p14: Vec3, + #[serde(default)] + pub p15: Vec3, + #[serde(default)] + pub p16: Vec3, + /// "" = measured value, "<>" inside = measurement placeholder, " " = suppressed. + #[serde(default)] + pub text: String, + #[serde(default = "standard")] + pub style: String, + #[serde(default)] + pub measurement: f64, + #[serde(default)] + pub text_rotation: f64, + /// User moved the text (text_mid is authoritative). + #[serde(default)] + pub user_text_pos: bool, + /// Anonymous block with the rendered geometry (from files); regenerated when edited. + #[serde(default)] + pub block: Option, + /// Per-dimension style overrides (DIMOVERRIDE): DimStyle field names (camelCase) → values, + /// merged over the named style. + #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")] + pub overrides: serde_json::Map, + /// Associativity: which definition points follow which objects. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub assoc: Vec, + /// Oblique extension lines, jogs, breaks and inspection frames. + #[serde(default, skip_serializing_if = "DimMarks::is_empty")] + pub marks: DimMarks, +} + +/// What dimension edits add to a dimension's drawing: DIMEDIT Oblique, DIMJOGLINE, DIMJOGGED, +/// DIMBREAK and DIMINSPECT. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct DimMarks { + /// Angle (radians) of the extension lines of a linear or aligned dimension; None = + /// perpendicular to the dimension line. + #[serde(skip_serializing_if = "Option::is_none")] + pub oblique: Option, + /// A jog on the dimension line of a linear or aligned dimension, at this point (DIMJOGLINE). + #[serde(skip_serializing_if = "Option::is_none")] + pub jog_line: Option, + /// A jogged radius dimension (DIMJOGGED). + #[serde(skip_serializing_if = "Option::is_none")] + pub jogged: Option, + /// Gaps in the dimension and extension lines (DIMBREAK). + #[serde(skip_serializing_if = "Vec::is_empty")] + pub breaks: Vec, + /// Inspection frame, label and rate (DIMINSPECT). + #[serde(skip_serializing_if = "Option::is_none")] + pub inspect: Option, +} + +impl DimMarks { + pub fn is_empty(&self) -> bool { + *self == DimMarks::default() + } + + /// Transform the points and angles (part of [`EntityKind::transform`]). + pub(super) fn transform(&mut self, t3: &dyn Fn(&mut Vec3), rot: f64, s: f64, mirror: bool) { + if let Some(a) = &mut self.oblique { + *a = cadcraft_geom::norm_angle(if mirror { rot - (*a - rot) } else { *a + rot }); + } + if let Some(j) = &mut self.jog_line { + t3(j); + } + if let Some(j) = &mut self.jogged { + t3(&mut j.center); + t3(&mut j.jog); + } + for b in &mut self.breaks { + t3(&mut b.at); + b.size *= s; + } + } +} + +/// The definition of a jogged radius dimension: the dimension line starts at an overriding +/// centre and jogs on its way to the arc. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct JoggedRadius { + /// Where the dimension line starts instead of the true centre. + pub center: Vec3, + /// The middle of the jog. + pub jog: Vec3, +} + +/// A gap in a dimension's lines: everything within `size / 2` of `at` is left out. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct DimBreak { + pub at: Vec3, + pub size: f64, + /// Made by DIMBREAK Auto (recomputed by the next Auto) rather than picked by hand. + pub auto: bool, +} + +/// An inspection dimension (DIMINSPECT). +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct Inspection { + /// Frame shape: 0 round ends, 1 angular ends, 2 none. + pub shape: u8, + /// Text left of the value ("" = none). + pub label: String, + /// Inspection rate right of the value ("" = none). + pub rate: String, +} + +/// A dimension definition point that follows an object (associative dimensions). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DimAssoc { + /// Definition point: "defpt", "p13", "p14", "p15" or "p16". + pub point: String, + /// The object it is attached to. + pub handle: Handle, + pub snap: AssocSnap, +} + +/// Where on the object an associative definition point sits. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum AssocSnap { + /// Start point of a line/arc (or polyline vertex 0). + Start, + /// End point of a line/arc. + End, + Mid, + /// Centre of a circle/arc. + Center, + /// On a circle/arc at a fixed angle (radians) from its centre. + OnCircle { + angle: f64, + }, + /// Intersection of this (line) object with another line. + Intersection { + other: Handle, + }, + /// Vertex `index` of a polyline. + Vertex { + index: usize, + }, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Leader { + pub vertices: Vec, + #[serde(default = "yes")] + pub arrow: bool, + #[serde(default)] + pub spline: bool, + #[serde(default = "standard")] + pub style: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MLeader { + pub leaders: Vec>, + pub landing: Vec3, + pub dogleg: f64, + pub text: Option, + #[serde(default = "standard")] + pub style: String, + #[serde(default = "one")] + pub arrow_size: f64, + /// Leader lines are smooth curves through their points (leader type Spline). An empty + /// `leaders` is a multileader without a leader line (leader type None). + #[serde(default)] + pub spline: bool, + /// Block content in place of the text: a block reference placed at the landing. + #[serde(default)] + pub block: Option, +} + +impl MLeader { + /// Each leader line as drawn: through its points to the landing, smoothed for spline leaders. + pub fn leader_paths(&self) -> Vec> { + self.leaders + .iter() + .map(|l| { + let pts: Vec = l.iter().map(|v| v.xy()).chain(std::iter::once(self.landing.xy())).collect(); + if self.spline && pts.len() > 2 { GSpline::from_fit_points(&pts).tessellate(1e-3) } else { pts } + }) + .collect() + } +} diff --git a/crates/doc/src/entity/fill.rs b/crates/doc/src/entity/fill.rs new file mode 100644 index 000000000..1b69108a0 --- /dev/null +++ b/crates/doc/src/entity/fill.rs @@ -0,0 +1,61 @@ +use super::common::Handle; +use super::*; + +/// A hatch boundary loop: closed polyline with bulges. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct HatchLoop { + pub vertices: Vec, + #[serde(default)] + pub outer: bool, + /// The objects the loop was made from (DXF group 97 count, 330 handles): an associative + /// hatch re-fills the loop from them when they change. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub sources: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Hatch { + pub pattern: String, + pub solid: bool, + pub loops: Vec, + #[serde(default = "one")] + pub scale: f64, + #[serde(default)] + pub angle: f64, + #[serde(default)] + pub associative: bool, + /// 0 normal (odd parity), 1 outer, 2 ignore. + #[serde(default)] + pub style: u8, + #[serde(default)] + pub elevation: f64, + /// Gradient fill: name + second colour. + #[serde(default)] + pub gradient: Option, + #[serde(default)] + pub origin: Vec2, + #[serde(default)] + pub background: Option, + /// The pattern's line families as a file defined them, for a pattern that isn't in the + /// standard library (unit scale and angle, like the library's; empty otherwise). + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub pattern_lines: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Gradient { + pub name: String, + pub color1: Color, + pub color2: Color, + #[serde(default)] + pub angle: f64, + #[serde(default)] + pub centered: bool, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Wipeout { + pub boundary: Vec, +} diff --git a/crates/doc/src/entity/layout.rs b/crates/doc/src/entity/layout.rs new file mode 100644 index 000000000..7ebe49eee --- /dev/null +++ b/crates/doc/src/entity/layout.rs @@ -0,0 +1,168 @@ +use super::common::Handle; +use super::*; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Viewport { + pub center: Vec3, + pub width: f64, + pub height: f64, + pub view_center: Vec2, + pub view_height: f64, + #[serde(default)] + pub id: u32, + #[serde(default)] + pub locked: bool, + #[serde(default)] + pub frozen_layers: Vec, + /// Per-viewport layer colour overrides (VP Color in the Layer Properties Manager). + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub layer_colors: Vec<(String, Color)>, + /// The paper-space entity whose outline clips the viewport (a closed polyline, circle, + /// ellipse or spline; DXF group 340). `None`: the rectangle `center`/`width`/`height`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub clip: Option, + /// Display on (MVIEW ON/OFF). An off viewport shows its border but no model space. + #[serde(default = "yes")] + pub on: bool, +} + +impl Default for Viewport { + fn default() -> Self { + Viewport { + center: Vec3::ZERO, + width: 1.0, + height: 1.0, + view_center: Vec2::ZERO, + view_height: 1.0, + id: 0, + locked: false, + frozen_layers: Vec::new(), + layer_colors: Vec::new(), + clip: None, + on: true, + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Image { + pub insert: Vec3, + /// One pixel's width (U) and height (V) vectors in drawing units. + pub u: Vec3, + pub v: Vec3, + /// Image size in pixels. + pub size: Vec2, + /// The image file (its IMAGEDEF object's path). + pub path: String, + /// The image definition's name (its key in the drawing's image dictionary). + #[serde(default, skip_serializing_if = "String::is_empty")] + pub name: String, + /// Clip boundary in pixel coordinates (origin at the top-left corner of the image, y down): + /// two opposite corners of a rectangle, or a polygon's vertices. Empty: the whole image. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub clip: Vec, + /// The clip boundary is applied. + #[serde(default)] + pub clipping: bool, + /// Display flags: 1 show the image, 2 show it when not aligned with the screen, 4 use the + /// clip boundary, 8 transparency on. + #[serde(default = "image_display")] + pub display: u16, + /// Brightness, contrast and fade, 0..=100. + #[serde(default = "image_fifty")] + pub brightness: u8, + #[serde(default = "image_fifty")] + pub contrast: u8, + #[serde(default)] + pub fade: u8, + /// The definition's default size of one pixel and its resolution unit (0 none, 2 cm, 5 inch). + #[serde(default = "image_pixel")] + pub pixel_size: Vec2, + #[serde(default)] + pub resolution_units: u8, +} + +fn image_display() -> u16 { + 7 +} + +fn image_fifty() -> u8 { + 50 +} + +fn image_pixel() -> Vec2 { + Vec2::new(1.0, 1.0) +} + +impl Default for Image { + fn default() -> Self { + Image { + insert: Vec3::ZERO, + u: Vec3::new(1.0, 0.0, 0.0), + v: Vec3::new(0.0, 1.0, 0.0), + size: Vec2::new(1.0, 1.0), + path: String::new(), + name: String::new(), + clip: Vec::new(), + clipping: false, + display: image_display(), + brightness: image_fifty(), + contrast: image_fifty(), + fade: 0, + pixel_size: image_pixel(), + resolution_units: 0, + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TableCell { + pub text: String, + #[serde(default)] + pub merged: Option<(u32, u32)>, + /// The cell's own cell style; empty: its row's. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub style: String, + /// A formula (`=Sum(A1:B3)`); `text` holds its value. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub formula: Option, + /// The cell's own text height (DXF 140 in the cell record), before the table's scale; + /// `None`: its cell style's. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub text_height: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Table { + pub insert: Vec3, + pub col_widths: Vec, + pub row_heights: Vec, + pub cells: Vec>, + #[serde(default = "standard")] + pub style: String, + #[serde(default = "default_text_h")] + pub text_height: f64, + /// First row is a title (merged across, larger text). + #[serde(default)] + pub title: bool, + /// The row after the title holds column headers. + #[serde(default)] + pub header: bool, + /// The cell style of each row; missing or empty: by position (Title, Header, Data). + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub row_styles: Vec, + /// Table breaking (TABLEBREAK): the table drawn in several parts. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub breaks: Option, + /// The CSV file the cells come from (DATALINK). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub data_link: Option, +} + +fn default_text_h() -> f64 { + 0.18 +} diff --git a/crates/doc/src/entity/text.rs b/crates/doc/src/entity/text.rs new file mode 100644 index 000000000..a4eff0427 --- /dev/null +++ b/crates/doc/src/entity/text.rs @@ -0,0 +1,141 @@ +use super::*; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum HAlign { + #[default] + Left, + Center, + Right, + Aligned, + Middle, + Fit, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum VAlign { + #[default] + Baseline, + Bottom, + Middle, + Top, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Text { + pub insert: Vec3, + #[serde(default)] + pub align_pt: Option, + pub height: f64, + pub value: String, + #[serde(default)] + pub rotation: f64, + #[serde(default = "one")] + pub width_factor: f64, + #[serde(default)] + pub oblique: f64, + #[serde(default = "standard")] + pub style: String, + #[serde(default)] + pub halign: HAlign, + #[serde(default)] + pub valign: VAlign, +} + +impl Text { + /// The point the text is placed by, as the Insertion snap and the grip show it: the alignment + /// point (DXF group 11) of justified text, the start point (group 10) of left-baseline, + /// aligned and fit text. + pub fn justify_point(&self) -> Vec2 { + match (self.halign, self.valign) { + (HAlign::Left, VAlign::Baseline) | (HAlign::Aligned | HAlign::Fit, _) => self.insert.xy(), + _ => self.align_pt.unwrap_or(self.insert).xy(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MText { + pub insert: Vec3, + pub height: f64, + /// Reference rectangle width (0 = no wrapping). + #[serde(default)] + pub width: f64, + /// 1..=9: TL TC TR ML MC MR BL BC BR. + #[serde(default = "attach_tl")] + pub attach: u8, + #[serde(default)] + pub rotation: f64, + #[serde(default = "standard")] + pub style: String, + /// Raw MTEXT contents with inline codes (\P, {\fArial|b1;...}, \S1/2; …). + pub contents: String, + #[serde(default = "one")] + pub line_spacing: f64, + /// Line spacing style (DXF 73): Exactly (`true`) keeps `line_spacing` whatever the character + /// heights; At least (`false`, the default) lets taller characters push lines apart. + #[serde(default)] + pub line_spacing_exact: bool, + /// Columns the text flows through, each `width` wide (DXF 75/76/78/79/48/49/50); `None`: + /// one column. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub columns: Option, +} + +fn attach_tl() -> u8 { + 1 +} + +/// Static columns: a fixed number of columns of one height. Dynamic columns: as many columns +/// as the text needs, of one height (auto height) or each its own (manual heights). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum MTextColumnType { + #[default] + Static, + Dynamic, +} + +/// MTEXT columns (MTEXT Columns option, DXF Reference MTEXT groups 75–79, 48–50). +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct MTextColumns { + #[serde(rename = "type")] + pub kind: MTextColumnType, + /// Number of columns (static columns: the columns there are). + pub count: u16, + /// Space between two columns. + pub gutter: f64, + /// Height of every column (static, and dynamic with auto height). + pub height: f64, + /// Dynamic columns: one height for all columns (`height`) instead of `heights`. + pub auto_height: bool, + /// Dynamic columns with manual heights: the height of each column. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub heights: Vec, + /// Columns run right to left. + pub flow_reversed: bool, +} + +impl MTextColumns { + /// The width of `count` columns of `column_width` and their gutters. + pub fn total_width(&self, column_width: f64) -> f64 { + let n = f64::from(self.count.max(1)); + column_width * n + self.gutter.max(0.0) * (n - 1.0) + } + + /// The tallest column. + pub fn max_height(&self) -> f64 { + self.heights.iter().copied().fold(self.height, f64::max).max(0.0) + } + + /// Scale every length (SCALE, block insertion). + pub fn scale(&mut self, s: f64) { + self.gutter *= s; + self.height *= s; + self.heights.iter_mut().for_each(|h| *h *= s); + } +}