cadcraft/crates/render/src/lib.rs

1266 lines
53 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! CADCraft rendering.
//!
//! [`build`] turns a drawing space into a [`DisplayList`]: world-space polylines, filled
//! triangles and points, each tagged with its top-level entity handle and resolved colour.
//! Linetypes, hatches, text, block references and dimensions are expanded here. The UI uploads
//! the list to the GPU; [`raster`] draws it on the CPU (PNG export, plot preview, tests).
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
pub mod clip;
mod dim;
mod fill;
mod hatch;
mod linetype;
mod linetype_text;
mod mline;
pub mod paper;
pub mod pen;
mod plotstyle;
mod point;
pub mod raster;
mod tolerance;
pub mod units;
mod wide;
use cadcraft_color::{Color, Rgb};
use cadcraft_doc::{Drawing, Entity, EntityKind, Handle, Lineweight, Prim, Space};
use cadcraft_geom::{Bounds2, Mat3, Polyline, Vec2};
use pen::Pen;
pub use dim::{ArrowGeom, Arrowhead, DimGeometry, DimText, LineRole, arrowhead, dimension_geometry, dimension_geometry_with, format_linear_value};
pub use fill::triangulate_evenodd;
pub use paper::{PAPER_SIZES, PaperSize, Sheet, paper_size, sheet};
pub use tolerance::{gdt_points, tolerance_in};
/// Handle carried by model-space geometry drawn inside a paper-space viewport (it belongs to no
/// single paper-space entity, so selection highlighting never matches it).
pub const VIEWPORT_CONTENT: Handle = Handle(0);
/// [`DisplayList::prim_layers`] of a primitive whose layer isn't in the layer table.
pub const NO_LAYER: u32 = u32::MAX;
/// What a display-list primitive draws.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Kind {
/// `verts[start..start+len]` as a connected polyline.
Polyline,
/// `tris[start..start+len]`, three vertices per triangle.
Tris,
/// A wipeout: triangles laid out like [`Kind::Tris`], filled with the background colour
/// (paper white when plotting, the canvas colour on screen) so they hide what was drawn
/// before them.
Mask,
/// A single point marker at `verts[start]`.
Point,
/// An infinite line or ray: `verts[start]` = base, `verts[start+1]` = unit direction.
Infinite { ray: bool },
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DPrim {
pub handle: Handle,
pub color: Rgb,
/// Colour 7 (directly, by layer or by block): drawn white on a dark background and black on
/// a light one, see [`DPrim::display_rgb`]. Other colours keep their RGB.
pub aci7: bool,
/// Lineweight in mm (0 = thinnest).
pub lw: f32,
pub kind: Kind,
pub start: u32,
pub len: u32,
}
#[derive(Clone, Debug, Default)]
pub struct DisplayList {
pub prims: Vec<DPrim>,
pub verts: Vec<Vec2>,
pub tris: Vec<Vec2>,
/// Plots ([`build_plot`]): for each primitive, the index in [`Drawing::layers`] of the layer
/// it is drawn on (block contents on layer 0: the block reference's; [`NO_LAYER`] when the
/// layer isn't in the table), for PDF layers. Empty for other lists.
pub prim_layers: Vec<u32>,
/// Plots: outline-font glyphs that a plotter can write as text instead of drawing the
/// filled outline primitive they stand for ([`PlotText::prim`]), in drawing order.
pub texts: Vec<PlotText>,
pub bounds: Bounds2,
/// The paper sheet when the list was built for a layout (paper space); the UI draws the
/// white sheet, its shadow and the printable-area outline from it.
pub sheet: Option<Sheet>,
/// Block contents expanded while building (see [`MAX_BLOCK_EXPANSION`]).
pub expanded: usize,
/// Plot style end, join and fill style per primitive (parallel to `prims`; missing entries
/// are "use object"), turned into geometry by [`pen::apply`].
pub pens: Vec<Pen>,
}
/// A glyph of plotted text: the font glyph and, in world coordinates, where it sits. It is
/// drawn by primitive [`PlotText::prim`] (a filled outline in the primitive's colour); output
/// with real text writes the glyph instead of that primitive.
#[derive(Clone, Debug, PartialEq)]
pub struct PlotText {
/// The filled outline primitive (index in [`DisplayList::prims`]); [`PlotText::NONE`] when
/// the glyph has none in the list (clipped by a viewport).
pub prim: u32,
/// Triangle vertices of the outline when it was drawn, and their bounding box (a clipped
/// outline has others).
pub verts: u32,
pub bounds: Bounds2,
/// The glyph; its origin and em axes are in world coordinates.
pub glyph: cadcraft_fonts::TextGlyph,
}
impl PlotText {
pub const NONE: u32 = u32::MAX;
/// [`PlotText::prim`] of a glyph cut by a viewport edge.
pub const CLIPPED: u32 = u32::MAX - 1;
}
impl DPrim {
/// The colour to draw with on `background`.
pub fn display_rgb(&self, background: Rgb) -> Rgb {
cadcraft_color::display_rgb(self.color, self.aci7, background)
}
}
/// A resolved colour: its RGB, whether it is colour 7, and its index colour (0 = a true
/// colour), which picks the style of a colour-dependent plot style table.
#[derive(Clone, Copy, Debug, PartialEq)]
struct Ink {
rgb: Rgb,
aci7: bool,
aci: u8,
}
/// Resolve `c` like [`Color::resolve`], noting whether it comes out as colour 7.
fn ink(c: Color, layer: Color, block: Color) -> Ink {
// ByLayer / ByBlock left unresolved (layer or block colour itself logical) mean colour 7.
let is7 = |c: Color| matches!(c, Color::ByLayer | Color::ByBlock | Color::Index(7));
let aci7 = match c {
Color::ByLayer => is7(layer),
Color::ByBlock => is7(block),
c => c == Color::Index(7),
};
let aci = match c {
Color::ByLayer => layer,
Color::ByBlock => block,
c => c,
};
let aci = match aci {
Color::Index(i) => i,
Color::True(_) => 0,
Color::ByLayer | Color::ByBlock => 7,
};
Ink { rgb: c.resolve(layer, block), aci7, aci }
}
impl DisplayList {
pub fn segment_count(&self) -> usize {
self.prims.iter().filter(|p| p.kind == Kind::Polyline).map(|p| p.len.saturating_sub(1) as usize).sum()
}
/// The polyline points of a primitive.
pub fn points(&self, p: &DPrim) -> &[Vec2] {
match p.kind {
Kind::Tris | Kind::Mask => self.tris.get(p.start as usize..(p.start + p.len) as usize).unwrap_or(&[]),
_ => self.verts.get(p.start as usize..(p.start + p.len) as usize).unwrap_or(&[]),
}
}
}
/// Build options.
#[derive(Clone, Debug)]
pub struct Options {
/// Chord tolerance in world units (about half a pixel at the current zoom).
pub tolerance: f64,
/// Dashes shorter than this (world units) are drawn as continuous lines.
pub min_dash: f64,
/// Draw text (off for very coarse previews).
pub text: bool,
/// Highlighted (selected) handles are not special here; the canvas overlays them.
pub fill: bool,
pub lineweights: bool,
/// Height of the visible area in world units of the space being built. Relative point
/// sizes (`PDSIZE` <= 0) are a percentage of it; 0 = the height of the space's extents.
pub view_height: f64,
/// Plot style table to draw with. When `None`, [`build`] of a layout whose page setup has
/// "Display plot styles" on uses the layout's table; [`build_plot`] uses none.
pub plot_style_table: Option<std::sync::Arc<cadcraft_doc::PlotStyleTable>>,
/// The area being looked at (world units of the space being built), for on-screen builds:
/// hatch patterns are only generated inside it. `None` = everywhere (plots, exports, picks).
pub view: Option<Bounds2>,
}
impl Default for Options {
fn default() -> Self {
Options { tolerance: 0.001, min_dash: 0.0, text: true, fill: true, lineweights: false, view_height: 0.0, plot_style_table: None, view: None }
}
}
/// The largest axis scale of a transform's linear part (its largest singular value): a length
/// `l` in the transform's own units measures at most `l * max_scale` after it, in any direction.
/// A uniform scale gives its factor, where [`Mat3::scale_factor`] gives the mean of the axes.
fn max_scale(m: &Mat3) -> f64 {
let (a, b, c, d) = (m.a, m.b, m.c, m.d);
let e = (a * a + b * b + c * c + d * d) / 2.0;
let f = (((a * a + b * b - c * c - d * d) / 2.0).powi(2) + (a * c + b * d).powi(2)).sqrt();
let s = (e + f).max(0.0).sqrt();
if s.is_finite() { s } else { 0.0 }
}
/// Graphics state inherited through block references.
#[derive(Clone)]
struct Ctx<'a> {
d: &'a Drawing,
xf: Mat3,
block_color: Color,
block_layer: Option<String>,
block_lw: Lineweight,
block_ltype: String,
depth: usize,
top: Handle,
/// Layers frozen in the viewport being drawn.
frozen: &'a [String],
/// Per-viewport layer colour overrides of the viewport being drawn.
vp_colors: &'a [(String, Color)],
/// Extra linetype scale for model space seen through a viewport: 1 / viewport scale when
/// `PSLTSCALE` is on (dashes keep their paper-space length at any viewport scale), else 1.
lt_factor: f64,
/// Named plot style (table index) of the block reference being expanded, for `ByBlock`.
block_pstyle: Option<usize>,
}
impl Ctx<'_> {
/// A layer's colour, honouring the viewport's override.
fn layer_color(&self, name: &str) -> Option<Color> {
if let Some((_, c)) = self.vp_colors.iter().find(|(n, _)| n.eq_ignore_ascii_case(name)) {
return Some(*c);
}
self.d.layer(name).map(|l| l.color)
}
}
struct Builder<'a> {
list: DisplayList,
opts: &'a Options,
/// Plotting: skip layers marked "do not plot".
plotting: bool,
/// The plot style table drawn with.
styles: Option<&'a plotstyle::Styler>,
/// Named plot style (table index) of the entity being drawn.
named: Option<usize>,
}
/// Upper bound on the block contents drawn per display list (each block reference, MINSERT copy
/// and entity drawn inside a block counts one). Nested, self-referencing or arrayed block
/// references in a hostile file would otherwise expand exponentially (`MAX_BLOCK_DEPTH` only
/// bounds the depth); past the limit the remaining block contents are not drawn.
pub const MAX_BLOCK_EXPANSION: usize = 2_000_000;
impl Builder<'_> {
/// `color` and `lw` as the plot style table prints them.
fn styled(&self, color: Ink, lw: f32) -> (Ink, f32) {
match self.styles {
Some(s) => s.apply(color, self.named, lw, self.opts.lineweights),
None => (color, lw),
}
}
/// Record the end, join and fill style `color` prints with for the primitive pushed next.
fn note_pen(&mut self, color: Ink) {
let Some(s) = self.styles else { return };
let pen = s.pen(color, self.named);
if !pen.is_object() {
let i = self.list.prims.len();
self.list.pens.resize(i, Pen::default());
self.list.pens.push(pen);
}
}
fn polyline(&mut self, ctx: &Ctx, color: Ink, lw: f32, pts: &[Vec2]) {
if pts.len() < 2 {
return;
}
self.note_pen(color);
let (color, lw) = self.styled(color, lw);
let start = self.list.verts.len() as u32;
for p in pts {
let q = ctx.xf.apply(*p);
self.list.bounds.add(q);
self.list.verts.push(q);
}
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw, kind: Kind::Polyline, start, len: pts.len() as u32 });
}
fn tris(&mut self, ctx: &Ctx, color: Ink, tris: &[Vec2]) {
if tris.len() < 3 {
return;
}
self.note_pen(color);
let (color, _) = self.styled(color, 0.0);
let start = self.list.tris.len() as u32;
for p in tris {
let q = ctx.xf.apply(*p);
self.list.bounds.add(q);
self.list.tris.push(q);
}
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw: 0.0, kind: Kind::Tris, start, len: tris.len() as u32 });
}
/// Shaped text: strokes as polylines; TrueType glyphs filled (TEXTFILL) or outlined.
fn shaped(&mut self, ctx: &Ctx, color: Ink, lw: f32, sh: &cadcraft_fonts::Shaped) {
for s in &sh.strokes {
self.polyline(ctx, color, lw, s);
}
if sh.glyphs.is_empty() {
return;
}
let fill = self.opts.fill && ctx.d.header.i64("TEXTFILL", 1) != 0;
// Plots note which filled outlines are font glyphs (see `DisplayList::texts`).
let mut text = if self.plotting && fill { sh.text.iter().peekable() } else { [].iter().peekable() };
for (i, g) in sh.glyphs.iter().enumerate() {
if fill {
let tris = fill::triangulate_evenodd(g);
let at = self.list.prims.len();
self.tris(ctx, color, &tris);
while text.next_if(|t| t.outline < i).is_some() {}
if let Some(t) = text.next_if(|t| t.outline == i)
&& self.list.prims.len() > at
&& let (Ok(prim), Ok(verts)) = (u32::try_from(at), u32::try_from(tris.len()))
{
let xf = &ctx.xf;
let glyph = cadcraft_fonts::TextGlyph { origin: xf.apply(t.origin), x: xf.apply_vec(t.x), y: xf.apply_vec(t.y), ..t.clone() };
let bounds = self.list.prims.get(at).map(|p| Bounds2::from_points(self.list.points(p).iter().copied())).unwrap_or(Bounds2::EMPTY);
self.list.texts.push(PlotText { prim, verts, bounds, glyph });
}
} else {
for c in g {
self.polyline(ctx, color, lw, c);
}
}
}
}
fn point(&mut self, ctx: &Ctx, color: Ink, p: Vec2) {
let (color, _) = self.styled(color, 0.0);
let q = ctx.xf.apply(p);
self.list.bounds.add(q);
let start = self.list.verts.len() as u32;
self.list.verts.push(q);
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw: 0.0, kind: Kind::Point, start, len: 1 });
}
fn infinite(&mut self, ctx: &Ctx, color: Ink, base: Vec2, dir: Vec2, ray: bool) {
let (color, _) = self.styled(color, 0.0);
let start = self.list.verts.len() as u32;
self.list.verts.push(ctx.xf.apply(base));
self.list.verts.push(ctx.xf.apply_vec(dir).normalized());
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw: 0.0, kind: Kind::Infinite { ray }, start, len: 2 });
}
/// Take one unit of the block-expansion budget; false once it is spent.
fn expand(&mut self) -> bool {
self.list.expanded = self.list.expanded.saturating_add(1);
self.list.expanded <= MAX_BLOCK_EXPANSION
}
}
/// Build the display list for a space. For a layout (paper space) this includes the model-space
/// geometry seen through each viewport, clipped to the viewport, and [`DisplayList::sheet`].
pub fn build(d: &Drawing, space: &Space, opts: &Options) -> DisplayList {
build_space(d, space, opts, false)
}
/// Like [`build`] but for plotting: layers marked "do not plot" (such as `Defpoints`) are left out.
pub fn build_plot(d: &Drawing, space: &Space, opts: &Options) -> DisplayList {
build_space(d, space, opts, true)
}
fn top_ctx<'a>(d: &'a Drawing, xf: Mat3, top: Handle, frozen: &'a [String], vp_colors: &'a [(String, Color)]) -> Ctx<'a> {
Ctx {
d,
xf,
block_color: Color::Index(7),
block_layer: None,
block_lw: Lineweight::Default,
block_ltype: "Continuous".into(),
depth: 0,
top,
frozen,
vp_colors,
lt_factor: 1.0,
block_pstyle: None,
}
}
fn build_space(d: &Drawing, space: &Space, opts: &Options, plotting: bool) -> DisplayList {
let opts = &with_point_view(d, space, opts);
let table = opts.plot_style_table.clone().or_else(|| match space {
Space::Paper(n) if !plotting => d
.layout(n)
.filter(|l| l.page.show_plot_styles)
.and_then(|l| cadcraft_doc::plot_style_table(d, &l.page.plot_style_table))
.map(std::sync::Arc::new),
_ => None,
});
let styler = table.map(|t| plotstyle::Styler::new(d, t));
let mut b = Builder { list: DisplayList::default(), opts, plotting, styles: styler.as_ref(), named: None };
if let Space::Paper(name) = space {
b.list.sheet = paper::sheet(d, name);
}
if let Some(store) = d.space(space) {
let paper = matches!(space, Space::Paper(_));
let mut viewports = 0usize;
for e in store.iter() {
if paper && let EntityKind::Viewport(vp) = &e.kind {
if viewports < MAX_VIEWPORTS {
viewports += 1;
viewport(&mut b, d, e, vp);
if plotting {
take_viewport_layers(&mut b.list);
}
}
continue;
}
entity(&mut b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
}
}
// A layout is drawn in paper units, so its pens become geometry now.
if let Some(sheet) = b.list.sheet {
pen::apply(&mut b.list, sheet.unit_mm);
}
b.list
}
/// `opts` with a view height for relative point sizes: the extents height when none is given
/// and points draw as figures sized relative to the view.
fn with_point_view(d: &Drawing, space: &Space, opts: &Options) -> Options {
let mut o = opts.clone();
let relative = d.header.f64("PDSIZE", 0.0) <= 0.0 && point::sized(d.header.i64("PDMODE", 0));
if !(o.view_height.is_finite() && o.view_height > 0.0) && relative {
let ext = d.extents(space);
o.view_height = if ext.is_empty() { 0.0 } else { ext.height().max(ext.width() * 1e-3) };
}
o
}
/// Upper bound on viewports drawn per layout (hostile files).
const MAX_VIEWPORTS: usize = 256;
/// A paper-space viewport: its border (as an ordinary entity) and model space seen through it.
/// Viewport id 1 is the paper-space view itself and is not drawn. A clipped viewport shows
/// model space through its clip boundary's outline (the boundary entity draws itself, so the
/// rectangle is not drawn); a viewport turned off shows only its border.
fn viewport(b: &mut Builder, d: &Drawing, e: &Entity, vp: &cadcraft_doc::Viewport) {
if vp.id == 1 || !e.common.visible || d.is_hidden_object(e.handle) {
return;
}
// The viewport's layer frozen hides its contents too (layer off hides only the border).
if d.layer(&e.common.layer).is_some_and(|l| l.frozen) {
return;
}
let poly = viewport_clip(d, e.handle, vp);
// Border (on the viewport's layer; layer off hides only the border).
if poly.is_none() {
entity(b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
}
if b.list.expanded >= MAX_BLOCK_EXPANSION || !vp.on {
return;
}
let center = vp.center.xy();
let ok = |v: f64| v.is_finite() && v > 0.0;
if !(ok(vp.width) && ok(vp.height) && ok(vp.view_height) && center.is_finite() && vp.view_center.is_finite()) {
return;
}
let s = vp.height / vp.view_height;
if !ok(s) {
return;
}
let half = Vec2::new(vp.width / 2.0, vp.height / 2.0);
let rect = match &poly {
Some(p) => p.bounds(),
None => Bounds2::new(center - half, center + half),
};
let xf = Mat3::translate(center).then_before(Mat3::scale(s, s)).then_before(Mat3::translate(-vp.view_center));
let mhalf = Vec2::new(rect.width(), rect.height()) / (2.0 * s);
let mcenter = vp.view_center + (rect.center() - center) / s;
// Model window with slack: entity bounds are approximate (text, dimensions).
let win = Bounds2::new(mcenter - mhalf, mcenter + mhalf).expand(mhalf.x.max(mhalf.y) * 0.1);
let mut sub = Builder { list: DisplayList::default(), opts: b.opts, plotting: b.plotting, styles: b.styles, named: None };
// PSLTSCALE on: model-space dashes are scaled so they measure the same on paper as in
// paper space, whatever the viewport scale.
let lt_factor = if d.header.i64("PSLTSCALE", 1) != 0 { 1.0 / s } else { 1.0 };
for me in d.model.iter() {
if !matches!(me.kind, EntityKind::Ray(_) | EntityKind::XLine(_) | EntityKind::Viewport(_)) {
let eb = cadcraft_doc::entity_bounds(d, me, 0);
if !eb.is_empty() && !eb.intersects(&win) {
continue;
}
}
if matches!(me.kind, EntityKind::Viewport(_)) {
continue;
}
entity(&mut sub, &Ctx { lt_factor, ..top_ctx(d, xf, VIEWPORT_CONTENT, &vp.frozen_layers, &vp.layer_colors) }, me);
// Plotted text glyphs move to the layout's list; their outlines carry the glyph's
// index there in their handle's high half until clipped (see `take_viewport_layers`).
for t in std::mem::take(&mut sub.list.texts) {
let k = b.list.texts.len() as u64 + 1;
if let Some(p) = sub.list.prims.get_mut(t.prim as usize)
&& k < u64::from(u32::MAX)
{
p.handle = Handle(p.handle.0 | (k << 32));
b.list.texts.push(PlotText { prim: PlotText::NONE, ..t });
}
}
}
// The contents are in paper units now: draw their pens before clipping.
pen::apply(&mut sub.list, paper::paper_unit_mm(d));
b.list.expanded = b.list.expanded.saturating_add(sub.list.expanded);
match &poly {
Some(p) => append_clipped(&mut b.list, &sub.list, &Clip::Poly(p)),
None => append_clipped(&mut b.list, &sub.list, &Clip::Rect(rect)),
}
}
/// The clip region of a clipped viewport: the outline of its clip boundary entity, when that
/// is a closed shape in the drawing. `None` for a rectangular viewport (or a dangling,
/// unusable or self-referencing boundary, which falls back to the rectangle).
pub fn viewport_clip(d: &Drawing, handle: Handle, vp: &cadcraft_doc::Viewport) -> Option<clip::PolyClip> {
let h = vp.clip.filter(|h| *h != handle)?;
let be = d.entity(h)?;
clip::PolyClip::new(&clip::boundary_polygon(&be.kind)?)
}
/// What viewport contents are clipped to.
enum Clip<'a> {
/// The viewport rectangle (the fast path).
Rect(Bounds2),
/// A clip boundary's outline.
Poly(&'a clip::PolyClip),
}
/// Append `src` to `dst`, clipped to `clip`.
fn append_clipped(dst: &mut DisplayList, src: &DisplayList, clip: &Clip) {
for p in &src.prims {
let pts = src.points(p);
match (p.kind, clip) {
(Kind::Polyline, Clip::Rect(rect)) => {
for piece in clip::clip_polyline(pts, rect) {
push_raw(dst, p, Kind::Polyline, &piece);
}
}
(Kind::Polyline, Clip::Poly(c)) => {
for piece in c.clip_polyline(pts) {
push_raw(dst, p, Kind::Polyline, &piece);
}
}
(Kind::Tris | Kind::Mask, Clip::Rect(rect)) => {
let t = clip::clip_triangles(pts, rect);
push_raw(dst, p, p.kind, &t);
}
(Kind::Tris | Kind::Mask, Clip::Poly(c)) => {
let t = c.clip_triangles(pts);
push_raw(dst, p, p.kind, &t);
}
(Kind::Point, _) => {
let inside = |q: Vec2| match clip {
Clip::Rect(rect) => rect.contains(q),
Clip::Poly(c) => c.contains(q),
};
if let Some(q) = pts.first()
&& inside(*q)
{
push_raw(dst, p, Kind::Point, &[*q]);
}
}
(Kind::Infinite { ray }, Clip::Rect(rect)) => {
if let (Some(base), Some(dir)) = (pts.first(), pts.get(1))
&& let Some((a, c)) = clip::clip_infinite(*base, *dir, ray, rect)
{
push_raw(dst, p, Kind::Polyline, &[a, c]);
}
}
(Kind::Infinite { ray }, Clip::Poly(c)) => {
if let (Some(base), Some(dir)) = (pts.first(), pts.get(1)) {
for piece in c.clip_infinite(*base, *dir, ray) {
push_raw(dst, p, Kind::Polyline, &piece);
}
}
}
}
}
}
fn push_raw(dst: &mut DisplayList, p: &DPrim, kind: Kind, pts: &[Vec2]) {
let min = match kind {
Kind::Polyline => 2,
Kind::Tris | Kind::Mask => 3,
_ => 1,
};
if pts.len() < min {
return;
}
let tris = matches!(kind, Kind::Tris | Kind::Mask);
let start = if tris { dst.tris.len() } else { dst.verts.len() } as u32;
for q in pts {
dst.bounds.add(*q);
}
if tris {
dst.tris.extend_from_slice(pts);
} else {
dst.verts.extend_from_slice(pts);
}
dst.prims.push(DPrim { handle: p.handle, color: p.color, aci7: p.aci7, lw: p.lw, kind, start, len: pts.len() as u32 });
}
/// Build the display list for a set of loose entities (previews, rubber bands).
pub fn build_entities<'a, I: IntoIterator<Item = &'a Entity>>(d: &Drawing, ents: I, opts: &Options) -> DisplayList {
let ents: Vec<&Entity> = ents.into_iter().collect();
// Only previews holding points need the model extents for relative point sizes.
let with_view;
let opts = if ents.iter().any(|e| matches!(e.kind, EntityKind::Point(_))) {
with_view = with_point_view(d, &Space::Model, opts);
&with_view
} else {
opts
};
let mut b = Builder { list: DisplayList::default(), opts, plotting: false, styles: None, named: None };
for e in ents {
entity(&mut b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
}
b.list
}
/// The layer `e` is drawn on, as an index in the layer table: layer 0 inside a block is the
/// block reference's layer.
fn entity_layer(ctx: &Ctx, e: &Entity) -> u32 {
let name = if e.common.layer == "0" { ctx.block_layer.as_deref().unwrap_or("0") } else { e.common.layer.as_str() };
ctx.d.layers.iter().position(|l| l.name.eq_ignore_ascii_case(name)).and_then(|i| u32::try_from(i).ok()).unwrap_or(NO_LAYER)
}
/// After a viewport is appended to a plot: the layers its contents carry in their handles
/// (see [`entity`]) go to [`DisplayList::prim_layers`], and the handles back to
/// [`VIEWPORT_CONTENT`]. The viewport's border was tagged when it was drawn.
///
/// Text glyphs seen through the viewport (marked in the handles' high half) keep their outline
/// primitive when it came through the clip whole; a glyph cut by the viewport edge stays
/// geometry.
fn take_viewport_layers(list: &mut DisplayList) {
let start = list.prim_layers.len();
for i in start..list.prims.len() {
let Some(p) = list.prims.get(i).copied() else { break };
list.prim_layers.push(u32::try_from(p.handle.0 & 0xFFFF_FFFF).unwrap_or(NO_LAYER));
let bounds = Bounds2::from_points(list.points(&p).iter().copied());
if let Some(t) = (p.handle.0 >> 32).checked_sub(1).and_then(|k| list.texts.get_mut(k as usize)) {
let same = |a: Vec2, b: Vec2| a.dist(b) <= 1e-9 * (1.0 + a.len());
let whole = t.prim == PlotText::NONE
&& p.kind == Kind::Tris
&& p.len == t.verts
&& same(bounds.min, t.bounds.min)
&& same(bounds.max, t.bounds.max);
// A second piece (or a cut one) means the clip changed the glyph.
t.prim = if whole { u32::try_from(i).unwrap_or(PlotText::NONE) } else { PlotText::CLIPPED };
}
if let Some(p) = list.prims.get_mut(i) {
p.handle = VIEWPORT_CONTENT;
}
}
}
fn resolve(ctx: &Ctx, e: &Entity, plotting: bool) -> (Ink, f32, Option<cadcraft_doc::Linetype>, f64, bool) {
resolve_common(ctx, &e.common, plotting)
}
/// [`resolve`] for an object's properties `c`.
fn resolve_common(ctx: &Ctx, c: &cadcraft_doc::Common, plotting: bool) -> (Ink, f32, Option<cadcraft_doc::Linetype>, f64, bool) {
let d = ctx.d;
// Layer "0" inside a block takes the insert's layer.
let layer_name = if c.layer == "0" { ctx.block_layer.as_deref().unwrap_or("0") } else { c.layer.as_str() };
let layer = d.layer(layer_name);
let visible =
c.visible && layer.is_none_or(|l| l.visible() && (!plotting || l.plot)) && !ctx.frozen.iter().any(|f| f.eq_ignore_ascii_case(layer_name));
let layer_color = ctx.layer_color(layer_name).unwrap_or(Color::Index(7));
let rgb = ink(c.color, layer_color, ctx.block_color);
let lw = match c.lineweight {
Lineweight::ByLayer => layer.map(|l| l.lineweight).unwrap_or(Lineweight::Default),
Lineweight::ByBlock => ctx.block_lw,
x => x,
};
let lw_mm = match lw {
Lineweight::Mm100(v) => f32::from(v) / 100.0,
_ => default_lineweight(d),
};
let lt_name = match c.linetype.to_ascii_lowercase().as_str() {
"bylayer" => layer.map(|l| l.linetype.clone()).unwrap_or_else(|| "Continuous".into()),
"byblock" => ctx.block_ltype.clone(),
_ => c.linetype.clone(),
};
let lt = d.linetype(&lt_name).filter(|l| !l.pattern.is_empty()).cloned();
let scale = d.header.f64("LTSCALE", 1.0) * c.ltscale * ctx.lt_factor;
(rgb, lw_mm, lt, scale, visible)
}
/// The width in mm that the "Default" lineweight stands for: `LWDEFAULT` (hundredths of a mm,
/// 0 to 211), 0.25 mm when it is unset or out of range.
fn default_lineweight(d: &Drawing) -> f32 {
match d.header.i64("LWDEFAULT", 25) {
v @ 0..=211 => v as f32 / 100.0,
_ => 0.25,
}
}
fn entity(b: &mut Builder, ctx: &Ctx, e: &Entity) {
// Hidden by ISOLATEOBJECTS / HIDEOBJECTS (and so in viewports and plots too).
if ctx.d.is_hidden_object(e.handle) {
return;
}
// The entity's named plot style, for everything it draws (block contents set their own).
let saved = b.named;
b.named = b.styles.and_then(|s| s.named_index(ctx, &e.common));
entity_parts(b, ctx, e);
b.named = saved;
if b.plotting && b.list.prim_layers.len() < b.list.prims.len() {
// What it drew that its block contents didn't claim is on its own layer.
let layer = entity_layer(ctx, e);
let from = b.list.prim_layers.len();
b.list.prim_layers.resize(b.list.prims.len(), layer);
if ctx.top == VIEWPORT_CONTENT {
// Model space seen through a viewport is clipped into the layout's list; the layer
// travels in the handle until then (see [`take_viewport_layers`]).
for p in b.list.prims.iter_mut().skip(from) {
p.handle = Handle(u64::from(layer));
}
}
}
}
fn entity_parts(b: &mut Builder, ctx: &Ctx, e: &Entity) {
// A top-level annotative object draws at the view's annotation scale: a viewport's scale for
// model space seen through it, else the drawing's (CANNOSCALE in model space, 1:1 on paper).
if ctx.depth == 0 && cadcraft_doc::is_annotative(e) {
let mode = if ctx.top == VIEWPORT_CONTENT {
cadcraft_doc::anno_draw(e, 1.0 / ctx.xf.scale_factor().max(1e-12), ctx.d.anno_all_visible())
} else {
ctx.d.anno_draw(e)
};
match mode {
cadcraft_doc::AnnoDraw::Hidden => return,
cadcraft_doc::AnnoDraw::Scaled(f) => {
let mut scaled = cadcraft_doc::anno_scaled(e, f);
scaled.common.anno_scales.clear();
return entity_parts(b, ctx, &scaled);
}
cadcraft_doc::AnnoDraw::Plain => {}
}
}
let (rgb, lw, lt, ltscale, visible) = resolve(ctx, e, b.plotting);
if !visible {
return;
}
// A plot style's linetype replaces the object's.
let lt = match b.styles.and_then(|s| s.linetype(rgb, b.named)) {
Some(over) => over,
None => lt,
};
let tol = b.opts.tolerance / ctx.xf.scale_factor().max(1e-12);
let lw = if b.opts.lineweights { lw } else { 0.0 };
// Stroke a polyline with the entity's linetype.
let stroke = |b: &mut Builder, pts: &[Vec2]| match &lt {
Some(lt) => {
let min = b.opts.min_dash / ctx.xf.scale_factor().max(1e-12);
for dash in linetype::apply(pts, lt, ltscale, min) {
if dash.len() == 1 {
if let Some(p) = dash.first() {
b.point(ctx, rgb, *p);
}
} else {
b.polyline(ctx, rgb, lw, &dash);
}
}
// Text embedded in the linetype (shapes need SHX files and are not drawn).
if b.opts.text {
for sh in linetype_text::texts(ctx.d, pts, lt, ltscale, min) {
b.shaped(ctx, rgb, lw, &sh);
}
}
}
None => b.polyline(ctx, rgb, lw, pts),
};
match &e.kind {
EntityKind::Text(t) => {
if !b.opts.text {
return;
}
let (sh, _) = place_text_entity(ctx.d, t, &t.value);
b.shaped(ctx, rgb, lw, &sh);
}
EntityKind::AttDef(a) => {
let (sh, _) = place_text_entity(ctx.d, &a.text, &a.tag);
b.shaped(ctx, rgb, lw, &sh);
}
EntityKind::MText(t) => {
if !b.opts.text {
return;
}
mtext(b, ctx, t, rgb, lw);
}
EntityKind::Insert(ins) => insert(b, ctx, e, ins, rgb),
EntityKind::Dimension(dm) => {
if let Some(blk) = dm.block.as_ref().and_then(|n| ctx.d.block(n))
&& ctx.depth < cadcraft_doc::MAX_BLOCK_DEPTH
{
let sub = Ctx { block_pstyle: b.named, ..sub_ctx(ctx, e, Mat3::IDENTITY) };
for be in blk.entities.iter() {
if !b.expand() {
break;
}
entity(b, &sub, be);
}
} else {
dimension(b, ctx, e, dm, lw, lt.as_ref(), ltscale);
}
}
EntityKind::Hatch(h) => hatch::draw(b, ctx, e, h, rgb, lw, tol),
EntityKind::MLine(m) => mline::draw(b, ctx, e, m, rgb, lw, tol),
EntityKind::LwPolyline(p) if p.const_width > 0.0 || p.vertices.iter().any(|v| v.start_width > 0.0 || v.end_width > 0.0) => {
if b.opts.fill && ctx.d.header.i64("FILLMODE", 1) != 0 {
match &lt {
// Dashed: each dash a filled piece of the band; dots a line across it.
Some(lt) => {
let min = b.opts.min_dash / ctx.xf.scale_factor().max(1e-12);
let (tris, ticks) = wide::dashed(p, tol, lt, ltscale, min);
b.tris(ctx, rgb, &tris);
for t in &ticks {
match t.as_slice() {
[q] => b.point(ctx, rgb, *q),
_ => b.polyline(ctx, rgb, lw, t),
}
}
}
None => b.tris(ctx, rgb, &fill::wide_polyline(p, tol)),
}
} else {
let pl = Polyline { vertices: p.vertices.clone(), closed: p.closed };
stroke(b, &pl.tessellate(tol));
}
}
EntityKind::LwPolyline(p) => {
let pl = Polyline { vertices: p.vertices.clone(), closed: p.closed };
if p.plinegen || lt.is_none() {
stroke(b, &pl.tessellate(tol));
} else {
for s in pl.segments() {
let mut pts = Vec::new();
s.tessellate(tol, &mut pts);
stroke(b, &pts);
}
}
}
EntityKind::Solid(s) | EntityKind::Trace(s) => {
let c = &s.corners;
let q = [c[0].xy(), c[1].xy(), c[3].xy(), c[2].xy()];
if b.opts.fill && ctx.d.header.i64("FILLMODE", 1) != 0 {
b.tris(ctx, rgb, &[q[0], q[1], q[2], q[0], q[2], q[3]]);
} else {
b.polyline(ctx, rgb, lw, &[q[0], q[1], q[2], q[3], q[0]]);
}
}
EntityKind::Wipeout(w) => {
// Hide what was drawn before it (draw order), then the frame per WIPEOUTFRAME:
// 0 = none, 1 = shown and plotted, 2 = shown but not plotted.
if b.opts.fill {
let n = b.list.prims.len();
b.tris(ctx, rgb, &fill::triangulate_evenodd(std::slice::from_ref(&w.boundary)));
if let Some(p) = b.list.prims.get_mut(n) {
p.kind = Kind::Mask;
}
}
let frame = match ctx.d.header.i64("WIPEOUTFRAME", 1) {
0 => false,
2 => !b.plotting,
_ => true,
};
if frame {
let mut pts = w.boundary.clone();
if let Some(f) = pts.first().copied() {
pts.push(f);
}
b.polyline(ctx, rgb, lw, &pts);
}
}
EntityKind::Image(i) => {
let o = i.insert.xy();
let u = i.u.xy() * i.size.x;
let v = i.v.xy() * i.size.y;
b.polyline(ctx, rgb, lw, &[o, o + u, o + u + v, o + v, o]);
b.polyline(ctx, rgb, lw, &[o, o + u + v]);
b.polyline(ctx, rgb, lw, &[o + u, o + v]);
}
EntityKind::Table(t) => table(b, ctx, t, rgb, lw),
EntityKind::Tolerance(t) => tolerance::draw(b, ctx, e, t, lw, tol),
EntityKind::Leader(l) => {
let pts: Vec<Vec2> = l.vertices.iter().map(|v| v.xy()).collect();
stroke(b, &pts);
if l.arrow
&& let (Some(a), Some(n)) = (pts.first(), pts.get(1))
{
let st = ctx.d.dim_style(&l.style);
// DIMASZ × DIMSCALE of the leader's style (the drawing's DIMSCALE when it is 0).
let size = st.map(|s| s.arrow_size).unwrap_or(0.18) * leader_scale(ctx.d, st);
// DIMLDRBLK ("" = closed filled).
let kind = dim::Arrowhead::parse(st.map(|s| s.leader_arrow_block.as_str()).unwrap_or(""));
let g = dim::arrowhead(kind, *a, (*a - *n).normalized(), size);
for l in &g.lines {
b.polyline(ctx, rgb, lw, l);
}
b.tris(ctx, rgb, &g.tris);
}
}
EntityKind::MLeader(m) => {
// Straight or spline leader lines; block content draws like a block reference.
for pts in m.leader_paths() {
b.polyline(ctx, rgb, lw, &pts);
if let (Some(a), Some(n)) = (pts.first(), pts.get(1)) {
b.tris(ctx, rgb, &dim::arrow(*a, (*a - *n).normalized(), m.arrow_size));
}
}
if let Some(t) = &m.text {
let land = m.landing.xy();
let dir = if t.insert.x >= land.x { 1.0 } else { -1.0 };
b.polyline(ctx, rgb, lw, &[land, land + Vec2::new(m.dogleg * dir, 0.0)]);
mtext(b, ctx, t, rgb, lw);
}
if let Some(ins) = &m.block {
if m.dogleg > 0.0 && !m.leaders.is_empty() {
let land = m.landing.xy();
let dir = if ins.insert.x >= land.x { 1.0 } else { -1.0 };
b.polyline(ctx, rgb, lw, &[land, land + Vec2::new(m.dogleg * dir, 0.0)]);
}
insert(b, ctx, e, ins, rgb);
}
}
EntityKind::Point(p) => {
// Relative sizes follow the view: undo the block / viewport scale.
let view = b.opts.view_height / ctx.xf.scale_factor().max(1e-12);
let size = point::size(ctx.d.header.f64("PDSIZE", 0.0), view);
let fig = point::figure(ctx.d.header.i64("PDMODE", 0), size, p.p.xy(), p.angle, tol);
if fig.dot {
b.point(ctx, rgb, p.p.xy());
}
for l in &fig.lines {
b.polyline(ctx, rgb, lw, l);
}
}
kind => {
for prim in kind.prims() {
match prim {
Prim::Seg(s) => {
let mut pts = Vec::new();
s.tessellate(tol, &mut pts);
stroke(b, &pts);
}
Prim::Circle(c) => {
let mut pts = Vec::new();
c.tessellate(tol, &mut pts);
stroke(b, &pts);
}
Prim::Ellipse(el) => {
let mut pts = Vec::new();
el.tessellate(tol, &mut pts);
stroke(b, &pts);
}
Prim::Spline(s) => stroke(b, &s.tessellate(tol)),
Prim::Point(p) => b.point(ctx, rgb, p),
Prim::Infinite { base, dir, ray } => b.infinite(ctx, rgb, base, dir, ray),
Prim::Fill(pts) => {
let tris = fill::triangulate_evenodd(&[pts]);
b.tris(ctx, rgb, &tris);
}
}
}
}
}
}
/// The text style of an entity (Standard defaults when missing).
fn text_style(d: &Drawing, name: &str) -> cadcraft_doc::TextStyle {
d.text_style(name).cloned().unwrap_or_default()
}
/// Shape a TEXT / ATTRIB / ATTDEF with its style's font and generation flags.
pub fn place_text_entity(d: &Drawing, t: &cadcraft_doc::Text, value: &str) -> (cadcraft_fonts::Shaped, Bounds2) {
let ts = text_style(d, &t.style);
let font = cadcraft_fonts::TextFont::resolve(&ts.font);
let (h, v) = align(t.halign, t.valign);
let p = cadcraft_fonts::TextParams {
insert: t.insert.xy(),
align_pt: t.align_pt.map(|p| p.xy()),
height: t.height,
rotation: t.rotation,
width_factor: t.width_factor,
oblique: t.oblique,
h,
v,
backwards: ts.backwards,
upside_down: ts.upside_down,
};
cadcraft_fonts::place(&font, value, &p)
}
/// Lay out an MTEXT with its style's font.
pub fn layout_mtext_entity(d: &Drawing, t: &cadcraft_doc::MText) -> cadcraft_fonts::MTextLayout {
let ts = text_style(d, &t.style);
let params = cadcraft_fonts::MTextParams {
insert: t.insert.xy(),
height: t.height,
width: t.width,
attach: t.attach,
rotation: t.rotation,
line_spacing: t.line_spacing,
font: cadcraft_fonts::TextFont::resolve(&ts.font),
width_factor: ts.width_factor,
oblique: ts.oblique,
columns: t.columns.as_ref().map(|c| cadcraft_fonts::MTextColumnSpec {
dynamic: c.kind == cadcraft_doc::MTextColumnType::Dynamic,
count: usize::from(c.count.max(1)),
gutter: c.gutter,
height: c.height,
heights: if c.kind == cadcraft_doc::MTextColumnType::Dynamic && !c.auto_height { c.heights.clone() } else { Vec::new() },
reversed: c.flow_reversed,
}),
};
cadcraft_fonts::layout_mtext_with(&t.contents, &params)
}
fn mtext_color(c: Option<cadcraft_fonts::MTextColor>, rgb: Ink) -> Ink {
match c {
Some(cadcraft_fonts::MTextColor::Aci(i)) => {
u8::try_from(i).ok().filter(|i| *i > 0).map(|i| ink(Color::Index(i), Color::Index(7), Color::Index(7))).unwrap_or(rgb)
}
Some(cadcraft_fonts::MTextColor::Rgb(v)) => {
Ink { rgb: Rgb(((v >> 16) & 0xFF) as u8, ((v >> 8) & 0xFF) as u8, (v & 0xFF) as u8), aci7: false, aci: 0 }
}
None => rgb,
}
}
fn mtext(b: &mut Builder, ctx: &Ctx, t: &cadcraft_doc::MText, rgb: Ink, lw: f32) {
let l = layout_mtext_entity(ctx.d, t);
for p in &l.pieces {
b.shaped(ctx, mtext_color(p.color, rgb), lw, &p.shaped);
}
}
/// The font settings of a dimension style's text style.
pub fn dim_text(d: &Drawing, st: &cadcraft_doc::DimStyle) -> DimText {
let ts = text_style(d, &st.text_style);
DimText { font: cadcraft_fonts::TextFont::resolve(&ts.font), fixed_height: ts.height, width_factor: ts.width_factor, oblique: ts.oblique }
}
/// Geometry of a dimension in a drawing: its style, overrides and text font.
pub fn dimension_in(d: &Drawing, dm: &cadcraft_doc::Dimension) -> DimGeometry {
let style = d.dim_style(&dm.style).cloned().unwrap_or_default();
let st = style.with_overrides(&dm.overrides);
dim::dimension_geometry_with(dm, &style, d.header.f64("DIMSCALE", 1.0), &dim_text(d, &st))
}
/// Draw a polyline with a linetype (None: continuous).
fn stroke_with(b: &mut Builder, ctx: &Ctx, rgb: Ink, lw: f32, lt: Option<&cadcraft_doc::Linetype>, ltscale: f64, pts: &[Vec2]) {
match lt {
Some(lt) => {
let min = b.opts.min_dash / ctx.xf.scale_factor().max(1e-12);
for dash in linetype::apply(pts, lt, ltscale, min) {
match dash.as_slice() {
[p] => b.point(ctx, rgb, *p),
_ => b.polyline(ctx, rgb, lw, &dash),
}
}
}
None => b.polyline(ctx, rgb, lw, pts),
}
}
/// `lw` / `lt` are the dimension's own lineweight and linetype (ByBlock in DIMLWD, DIMLWE,
/// DIMLTYPE, DIMLTEX1 and DIMLTEX2).
fn dimension(b: &mut Builder, ctx: &Ctx, e: &Entity, dm: &cadcraft_doc::Dimension, lw: f32, lt: Option<&cadcraft_doc::Linetype>, ltscale: f64) {
let d = ctx.d;
let style = d.dim_style(&dm.style).cloned().unwrap_or_default().with_overrides(&dm.overrides);
let g = dimension_in(d, dm);
// DIMCLRD / DIMCLRE / DIMCLRT: ByBlock = the dimension's own colour.
let layer_name = if e.common.layer == "0" { ctx.block_layer.as_deref().unwrap_or("0") } else { e.common.layer.as_str() };
let layer_color = ctx.layer_color(layer_name).unwrap_or(Color::Index(7));
let own = match e.common.color {
Color::ByLayer => layer_color,
Color::ByBlock => ctx.block_color,
c => c,
};
let dim_rgb = ink(style.dim_line_color, layer_color, own);
let ext_rgb = ink(style.ext_line_color, layer_color, own);
let txt_rgb = ink(style.text_color, layer_color, own);
let layer = d.layer(layer_name);
let weight = |v: i16| -> f32 {
if !b.opts.lineweights {
return 0.0;
}
match Lineweight::from_dxf(v) {
Lineweight::ByBlock => lw,
Lineweight::Mm100(x) => f32::from(x) / 100.0,
Lineweight::ByLayer => match layer.map(|l| l.lineweight) {
Some(Lineweight::Mm100(x)) => f32::from(x) / 100.0,
_ => default_lineweight(d),
},
Lineweight::Default => default_lineweight(d),
}
};
let ltype = |name: &str| -> Option<cadcraft_doc::Linetype> {
match name.trim().to_ascii_lowercase().as_str() {
"" | "byblock" => lt.cloned(),
"bylayer" => layer.and_then(|l| d.linetype(&l.linetype)).filter(|l| !l.pattern.is_empty()).cloned(),
_ => d.linetype(name.trim()).filter(|l| !l.pattern.is_empty()).cloned(),
}
};
let (dim_lw, ext_lw) = (weight(style.dim_lineweight), weight(style.ext_lineweight));
let (dim_lt, ext1_lt, ext2_lt) = (ltype(&style.dim_linetype), ltype(&style.ext_linetype1), ltype(&style.ext_linetype2));
for (i, (l, role)) in g.lines.iter().zip(g.line_roles.iter().chain(std::iter::repeat(&LineRole::Dim))).enumerate() {
let (rgb, w, t) = match role {
LineRole::Ext if g.ext2_lines.contains(&i) => (ext_rgb, ext_lw, ext2_lt.as_ref()),
LineRole::Ext => (ext_rgb, ext_lw, ext1_lt.as_ref()),
LineRole::Dim => (dim_rgb, dim_lw, dim_lt.as_ref()),
};
stroke_with(b, ctx, rgb, w, t, ltscale, l);
}
for t in &g.fills {
b.tris(ctx, dim_rgb, t);
}
if b.opts.text {
let sh = cadcraft_fonts::Shaped { strokes: g.text.clone(), glyphs: g.text_glyphs.clone(), width: 0.0, text: g.text_runs.clone() };
b.shaped(ctx, txt_rgb, lw, &sh);
}
}
mod table;
pub use table::{MAX_LAYOUT_CELLS, TableFill, TableGeom, TableLine, TableText, table_covered, table_geometry, table_style_of};
fn table(b: &mut Builder, ctx: &Ctx, t: &cadcraft_doc::Table, rgb: Ink, lw: f32) {
table::draw(b, ctx, t, rgb, lw);
}
fn align(h: cadcraft_doc::HAlign, v: cadcraft_doc::VAlign) -> (cadcraft_fonts::Align, cadcraft_fonts::VAlign) {
use cadcraft_fonts::{Align as A, VAlign as V};
let ha = match h {
cadcraft_doc::HAlign::Left => A::Left,
cadcraft_doc::HAlign::Center => A::Center,
cadcraft_doc::HAlign::Right => A::Right,
cadcraft_doc::HAlign::Aligned => A::Aligned,
cadcraft_doc::HAlign::Middle => A::Middle,
cadcraft_doc::HAlign::Fit => A::Fit,
};
let va = match v {
cadcraft_doc::VAlign::Baseline => V::Baseline,
cadcraft_doc::VAlign::Bottom => V::Bottom,
cadcraft_doc::VAlign::Middle => V::Middle,
cadcraft_doc::VAlign::Top => V::Top,
};
(ha, va)
}
fn sub_ctx<'a>(ctx: &Ctx<'a>, e: &Entity, m: Mat3) -> Ctx<'a> {
let layer = if e.common.layer == "0" { ctx.block_layer.clone() } else { Some(e.common.layer.clone()) };
let color = match e.common.color {
Color::ByBlock => ctx.block_color,
Color::ByLayer => ctx.layer_color(layer.as_deref().unwrap_or("0")).unwrap_or(Color::Index(7)),
c => c,
};
let lw = match e.common.lineweight {
Lineweight::ByBlock => ctx.block_lw,
Lineweight::ByLayer => ctx.d.layer(layer.as_deref().unwrap_or("0")).map(|l| l.lineweight).unwrap_or(Lineweight::Default),
x => x,
};
let lt = match e.common.linetype.to_ascii_lowercase().as_str() {
"byblock" => ctx.block_ltype.clone(),
"bylayer" => ctx.d.layer(layer.as_deref().unwrap_or("0")).map(|l| l.linetype.clone()).unwrap_or_else(|| "Continuous".into()),
_ => e.common.linetype.clone(),
};
Ctx {
d: ctx.d,
xf: ctx.xf.then_before(m),
block_color: color,
block_layer: layer,
block_lw: lw,
block_ltype: lt,
depth: ctx.depth + 1,
top: ctx.top,
frozen: ctx.frozen,
vp_colors: ctx.vp_colors,
lt_factor: ctx.lt_factor,
block_pstyle: ctx.block_pstyle,
}
}
fn insert(b: &mut Builder, ctx: &Ctx, e: &Entity, ins: &cadcraft_doc::Insert, rgb: Ink) {
if ctx.depth >= cadcraft_doc::MAX_BLOCK_DEPTH {
return;
}
if let Some(blk) = ctx.d.block(&ins.block) {
let cols = ins.cols.clamp(1, 10_000);
let rows = ins.rows.clamp(1, 10_000);
'copies: for r in 0..rows {
for c in 0..cols {
if !b.expand() {
break 'copies;
}
let off = Vec2::new(ins.col_spacing * f64::from(c), ins.row_spacing * f64::from(r)).rotate(ins.rotation);
let m = Mat3::translate(off).then_before(ins.transform(blk.base.xy()));
let sub = Ctx { block_pstyle: b.named, ..sub_ctx(ctx, e, m) };
for be in blk.entities.iter() {
// Constant/visible attribute definitions inside blocks are not drawn; attribs are.
if matches!(be.kind, EntityKind::AttDef(_)) {
continue;
}
if !b.expand() {
break 'copies;
}
entity(b, &sub, be);
}
}
}
}
if b.opts.text {
// ATTMODE (ATTDISP): 0 shows no attributes, 1 the visible ones, 2 all of them.
let attmode = ctx.d.header.i64("ATTMODE", 1);
for a in &ins.attribs {
if attmode == 0 || (a.invisible && attmode != 2) {
continue;
}
let (sh, _) = place_text_entity(ctx.d, &a.text, &a.text.value);
// Properties of the attribute's definition (as AutoCAD copies them to the attribute
// on insertion), except those set on the attribute itself: ByBlock = the reference's,
// layer 0 = the reference's layer.
let def = ctx.d.block(&ins.block).and_then(|blk| blk.entities.iter().find(|be| attdef_tag(be, &a.tag)));
let (ink, lw, shown) = match (def, a.props.is_empty()) {
(None, true) => (rgb, resolve(ctx, e, b.plotting).1, true),
(def, _) => {
let c = a.props.over(&def.map(|d| d.common.clone()).unwrap_or_else(by_block));
let (ink, lw, _, _, shown) = resolve_common(&sub_ctx(ctx, e, Mat3::IDENTITY), &c, b.plotting);
(ink, lw, shown)
}
};
if shown {
b.shaped(ctx, ink, if b.opts.lineweights { lw } else { 0.0 }, &sh);
}
}
}
}
/// Properties that follow the block reference's (an attribute without a definition).
fn by_block() -> cadcraft_doc::Common {
cadcraft_doc::Common { color: Color::ByBlock, linetype: "ByBlock".into(), lineweight: Lineweight::ByBlock, ..cadcraft_doc::Common::default() }
}
/// The attribute definition with tag `tag` (any case).
fn attdef_tag(e: &Entity, tag: &str) -> bool {
matches!(&e.kind, EntityKind::AttDef(d) if d.tag.eq_ignore_ascii_case(tag))
}
/// The overall scale of a leader drawn with dimension style `st`: its DIMSCALE, else the
/// drawing's.
fn leader_scale(d: &Drawing, st: Option<&cadcraft_doc::DimStyle>) -> f64 {
let dimscale = d.header.f64("DIMSCALE", 1.0);
st.map(|s| s.effective_scale(dimscale)).unwrap_or_else(|| cadcraft_doc::DimStyle::default().effective_scale(dimscale))
}
#[cfg(test)]
mod tests;