mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-30 05:46:50 -06:00
- A dimension on a layer with a viewport colour override now takes the override inside that viewport (dimension and extension lines, arrows, text), like every other object. - A LEADER's arrowhead is DIMASZ x DIMSCALE of the leader's dimension style; the drawing's DIMSCALE only applies when the style's is 0 (as for dimensions), and both 0 no longer gives a zero-size arrow. - Attributes are drawn with the properties of their attribute definition, which AutoCAD copies to the attribute on insertion: ByLayer = the definition's layer (layer 0 = the block reference's layer), ByBlock = the block reference's colour and lineweight, explicit colours and lineweights as set. An attribute on an off or frozen layer is hidden. Attributes without a definition keep the block reference's colour and now its lineweight too, instead of 0. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1008 lines
40 KiB
Rust
1008 lines
40 KiB
Rust
//! 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;
|
||
pub mod paper;
|
||
mod point;
|
||
pub mod raster;
|
||
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};
|
||
|
||
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};
|
||
|
||
/// 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);
|
||
|
||
/// 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>,
|
||
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,
|
||
}
|
||
|
||
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 and whether it is colour 7.
|
||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||
struct Ink {
|
||
rgb: Rgb,
|
||
aci7: bool,
|
||
}
|
||
|
||
/// 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),
|
||
};
|
||
Ink { rgb: c.resolve(layer, block), aci7 }
|
||
}
|
||
|
||
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,
|
||
}
|
||
|
||
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 }
|
||
}
|
||
}
|
||
|
||
/// 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,
|
||
}
|
||
|
||
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,
|
||
}
|
||
|
||
/// 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<'_> {
|
||
fn polyline(&mut self, ctx: &Ctx, color: Ink, lw: f32, pts: &[Vec2]) {
|
||
if pts.len() < 2 {
|
||
return;
|
||
}
|
||
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;
|
||
}
|
||
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;
|
||
for g in &sh.glyphs {
|
||
if fill {
|
||
let tris = fill::triangulate_evenodd(g);
|
||
self.tris(ctx, color, &tris);
|
||
} else {
|
||
for c in g {
|
||
self.polyline(ctx, color, lw, c);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
fn point(&mut self, ctx: &Ctx, color: Ink, p: Vec2) {
|
||
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 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,
|
||
}
|
||
}
|
||
|
||
fn build_space(d: &Drawing, space: &Space, opts: &Options, plotting: bool) -> DisplayList {
|
||
let opts = &with_point_view(d, space, opts);
|
||
let mut b = Builder { list: DisplayList::default(), opts, plotting };
|
||
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);
|
||
}
|
||
continue;
|
||
}
|
||
entity(&mut b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
|
||
}
|
||
}
|
||
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.
|
||
fn viewport(b: &mut Builder, d: &Drawing, e: &Entity, vp: &cadcraft_doc::Viewport) {
|
||
if vp.id == 1 || !e.common.visible {
|
||
return;
|
||
}
|
||
// Border (on the viewport's layer; layer off hides only the border).
|
||
entity(b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
|
||
if b.list.expanded >= MAX_BLOCK_EXPANSION {
|
||
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 = 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 = half / s;
|
||
// Model window with slack: entity bounds are approximate (text, dimensions).
|
||
let win = Bounds2::new(vp.view_center - mhalf, vp.view_center + mhalf).expand(mhalf.x.max(mhalf.y) * 0.1);
|
||
let mut sub = Builder { list: DisplayList::default(), opts: b.opts, plotting: b.plotting };
|
||
// 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);
|
||
}
|
||
b.list.expanded = b.list.expanded.saturating_add(sub.list.expanded);
|
||
append_clipped(&mut b.list, &sub.list, &rect);
|
||
}
|
||
|
||
/// Append `src` to `dst`, clipped to `rect`.
|
||
fn append_clipped(dst: &mut DisplayList, src: &DisplayList, rect: &Bounds2) {
|
||
for p in &src.prims {
|
||
let pts = src.points(p);
|
||
match p.kind {
|
||
Kind::Polyline => {
|
||
for piece in clip::clip_polyline(pts, rect) {
|
||
push_raw(dst, p, Kind::Polyline, &piece);
|
||
}
|
||
}
|
||
Kind::Tris | Kind::Mask => {
|
||
let t = clip::clip_triangles(pts, rect);
|
||
push_raw(dst, p, p.kind, &t);
|
||
}
|
||
Kind::Point => {
|
||
if let Some(q) = pts.first()
|
||
&& rect.contains(*q)
|
||
{
|
||
push_raw(dst, p, Kind::Point, &[*q]);
|
||
}
|
||
}
|
||
Kind::Infinite { ray } => {
|
||
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]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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 };
|
||
for e in ents {
|
||
entity(&mut b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
|
||
}
|
||
b.list
|
||
}
|
||
|
||
fn resolve(ctx: &Ctx, e: &Entity, 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 e.common.layer == "0" { ctx.block_layer.as_deref().unwrap_or("0") } else { e.common.layer.as_str() };
|
||
let layer = d.layer(layer_name);
|
||
let visible = e.common.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(e.common.color, layer_color, ctx.block_color);
|
||
let lw = match e.common.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 e.common.linetype.to_ascii_lowercase().as_str() {
|
||
"bylayer" => layer.map(|l| l.linetype.clone()).unwrap_or_else(|| "Continuous".into()),
|
||
"byblock" => ctx.block_ltype.clone(),
|
||
_ => e.common.linetype.clone(),
|
||
};
|
||
let lt = d.linetype(<_name).filter(|l| !l.pattern.is_empty()).cloned();
|
||
let scale = d.header.f64("LTSCALE", 1.0) * e.common.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) {
|
||
let (rgb, lw, lt, ltscale, visible) = resolve(ctx, e, b.plotting);
|
||
if !visible {
|
||
return;
|
||
}
|
||
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 < {
|
||
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 = 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);
|
||
}
|
||
}
|
||
EntityKind::Hatch(h) => {
|
||
if h.solid || h.pattern.eq_ignore_ascii_case("SOLID") || h.gradient.is_some() {
|
||
if b.opts.fill {
|
||
let loops: Vec<Vec<Vec2>> =
|
||
h.loops.iter().map(|l| Polyline { vertices: l.vertices.clone(), closed: true }.tessellate(tol)).collect();
|
||
let tris = fill::triangulate_evenodd(&loops);
|
||
let c = match &h.gradient {
|
||
Some(g) => ink(g.color1, Color::Index(7), ctx.block_color),
|
||
None => rgb,
|
||
};
|
||
b.tris(ctx, c, &tris);
|
||
}
|
||
} else {
|
||
for seg in hatch::pattern_lines(h, tol) {
|
||
if seg.len() == 1 {
|
||
if let Some(p) = seg.first() {
|
||
b.point(ctx, rgb, *p);
|
||
}
|
||
} else {
|
||
b.polyline(ctx, rgb, lw, &seg);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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 < {
|
||
// 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::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);
|
||
let kind = dim::Arrowhead::parse(st.map(|s| s.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) => {
|
||
for l in &m.leaders {
|
||
let pts: Vec<Vec2> = l.iter().map(|v| v.xy()).chain(std::iter::once(m.landing.xy())).collect();
|
||
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);
|
||
}
|
||
}
|
||
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,
|
||
};
|
||
cadcraft_fonts::layout_mtext_with(&t.contents, ¶ms)
|
||
}
|
||
|
||
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 }
|
||
}
|
||
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))
|
||
}
|
||
|
||
fn dimension(b: &mut Builder, ctx: &Ctx, e: &Entity, dm: &cadcraft_doc::Dimension, lw: f32) {
|
||
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);
|
||
for (l, role) in g.lines.iter().zip(g.line_roles.iter().chain(std::iter::repeat(&LineRole::Dim))) {
|
||
b.polyline(ctx, if *role == LineRole::Ext { ext_rgb } else { dim_rgb }, lw, 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 };
|
||
b.shaped(ctx, txt_rgb, lw, &sh);
|
||
}
|
||
}
|
||
|
||
/// Covered cells of a table (inside another cell's merge, not its anchor).
|
||
pub fn table_covered(t: &cadcraft_doc::Table) -> Vec<Vec<bool>> {
|
||
let rows = t.row_heights.len().min(10_000);
|
||
let cols = t.col_widths.len().min(10_000);
|
||
let mut cov = vec![vec![false; cols]; rows];
|
||
for (r, row) in t.cells.iter().enumerate().take(rows) {
|
||
for (c, cell) in row.iter().enumerate().take(cols) {
|
||
if let Some((rs, cs)) = cell.merged {
|
||
for rr in r..(r + rs.max(1) as usize).min(rows) {
|
||
for cc in c..(c + cs.max(1) as usize).min(cols) {
|
||
if (rr, cc) != (r, c)
|
||
&& let Some(x) = cov.get_mut(rr).and_then(|v| v.get_mut(cc))
|
||
{
|
||
*x = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
cov
|
||
}
|
||
|
||
fn table(b: &mut Builder, ctx: &Ctx, t: &cadcraft_doc::Table, rgb: Ink, lw: f32) {
|
||
let o = t.insert.xy();
|
||
let rows = t.row_heights.len().min(10_000);
|
||
let cols = t.col_widths.len().min(10_000);
|
||
let xs: Vec<f64> = std::iter::once(0.0)
|
||
.chain(t.col_widths.iter().take(cols).scan(0.0, |a, w| {
|
||
*a += w;
|
||
Some(*a)
|
||
}))
|
||
.collect();
|
||
let ys: Vec<f64> = std::iter::once(0.0)
|
||
.chain(t.row_heights.iter().take(rows).scan(0.0, |a, h| {
|
||
*a += h;
|
||
Some(*a)
|
||
}))
|
||
.collect();
|
||
let cov = table_covered(t);
|
||
let style = ctx.d.table_styles.iter().find(|s| s.name.eq_ignore_ascii_case(&t.style)).cloned().unwrap_or_default();
|
||
let ts = text_style(ctx.d, "Standard");
|
||
let font = cadcraft_fonts::TextFont::resolve(&ts.font);
|
||
let x_at = |c: usize| xs.get(c).copied().unwrap_or(0.0);
|
||
let y_at = |r: usize| ys.get(r).copied().unwrap_or(0.0);
|
||
for r in 0..rows {
|
||
for c in 0..cols {
|
||
if cov.get(r).and_then(|v| v.get(c)).copied().unwrap_or(false) {
|
||
continue;
|
||
}
|
||
let cell = t.cells.get(r).and_then(|row| row.get(c));
|
||
let (rs, cs) = cell.and_then(|x| x.merged).map(|(a, b)| (a.max(1) as usize, b.max(1) as usize)).unwrap_or((1, 1));
|
||
let (r2, c2) = ((r + rs).min(rows), (c + cs).min(cols));
|
||
let (x0, x1, y0, y1) = (x_at(c), x_at(c2), y_at(r), y_at(r2));
|
||
let p = |x: f64, y: f64| o + Vec2::new(x, -y);
|
||
// Each cell draws its bottom and right edges; the outer top/left come from row 0 / col 0.
|
||
b.polyline(ctx, rgb, lw, &[p(x0, y1), p(x1, y1), p(x1, y0)]);
|
||
if r == 0 {
|
||
b.polyline(ctx, rgb, lw, &[p(x0, y0), p(x1, y0)]);
|
||
}
|
||
if c == 0 {
|
||
b.polyline(ctx, rgb, lw, &[p(x0, y0), p(x0, y1)]);
|
||
}
|
||
let Some(cell) = cell.filter(|x| !x.text.is_empty() && b.opts.text) else { continue };
|
||
let title = t.title && r == 0;
|
||
let header = t.header && r == usize::from(t.title);
|
||
let h = if title { t.text_height * 1.4 } else { t.text_height };
|
||
// Titles and headers are centred; data sits middle-left inside the cell margin.
|
||
let params = if title || header {
|
||
cadcraft_fonts::TextParams::centered(p((x0 + x1) / 2.0, (y0 + y1) / 2.0), h, 0.0)
|
||
} else {
|
||
let at = p(x0 + style.margin.max(0.0), (y0 + y1) / 2.0);
|
||
cadcraft_fonts::TextParams {
|
||
align_pt: Some(at),
|
||
h: cadcraft_fonts::Align::Left,
|
||
v: cadcraft_fonts::VAlign::Middle,
|
||
..cadcraft_fonts::TextParams::new(at, h)
|
||
}
|
||
};
|
||
let (sh, _) = cadcraft_fonts::place(&font, &cell.text, ¶ms);
|
||
b.shaped(ctx, rgb, lw, &sh);
|
||
}
|
||
}
|
||
}
|
||
|
||
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,
|
||
}
|
||
}
|
||
|
||
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 = 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 {
|
||
for a in &ins.attribs {
|
||
if a.invisible {
|
||
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): 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 {
|
||
Some(def) => {
|
||
let (ink, lw, _, _, shown) = resolve(&sub_ctx(ctx, e, Mat3::IDENTITY), def, b.plotting);
|
||
(ink, lw, shown)
|
||
}
|
||
None => (rgb, resolve(ctx, e, b.plotting).1, true),
|
||
};
|
||
if shown {
|
||
b.shaped(ctx, ink, if b.opts.lineweights { lw } else { 0.0 }, &sh);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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;
|