use super::blocks::{insert, sub_ctx}; use super::dim_draw::{MAX_FILL_SOLIDS, MAX_FILL_TEXTS, block_text_box, dim_text_fill, dimension, dimension_in, solid_covers, starts_text_marks}; use super::display::{Kind, NO_LAYER, VIEWPORT_CONTENT}; use super::props::{resolve, transparency}; use super::text_entity::{mtext, place_text_entity}; use super::{ Builder, Ctx, DimGeometry, Drawing, Entity, EntityKind, Handle, Ink, Mat3, Polyline, Prim, Vec2, dim, fill, hatch, linetype, linetype_text, mline, point, table, tolerance, wide, }; /// 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) } pub(crate) fn entity(b: &mut Builder, ctx: &Ctx, e: &Entity) { // Hidden by ISOLATEOBJECTS / HIDEOBJECTS (and so in viewports and plots too). A cancelled // build stops here (block contents come through here too). if ctx.d.is_hidden_object(e.handle) || b.opts.cancelled() { return; } // The entity's named plot style and transparency (set by `entity_parts`), for everything it // draws (block contents set their own). let (saved, saved_transparency) = (b.named, b.transparency); b.named = b.styles.and_then(|s| s.named_index(ctx, &e.common)); entity_parts(b, ctx, e); b.named = saved; b.transparency = saved_transparency; 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, layer_transparency) = resolve(ctx, e, b.plotting); if !visible { return; } if b.opts.transparency { b.transparency = transparency(e.common.transparency, layer_transparency, ctx.block_transparency); } // 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.as_ref().filter(|_| b.opts.linetypes) { 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 upright; let t = if e.common.matches_layout_orientation() && t.rotation != 0.0 { upright = cadcraft_doc::Text { rotation: 0.0, ..t.clone() }; &upright } else { t }; 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; } if e.common.matches_layout_orientation() && t.rotation != 0.0 { mtext(b, ctx, &cadcraft_doc::MText { rotation: 0.0, ..t.clone() }, rgb, lw); } else { mtext(b, ctx, t, rgb, lw); } } EntityKind::Insert(ins) => insert(b, ctx, e, ins, rgb), EntityKind::Dimension(dm) => draw_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 { // The GPU canvas has no MSAA: a band narrower than a pixel lights only where it // covers pixel centres, so whole sides vanish at some zooms (#1434). On screen the // centre line is drawn too, as a hairline (wide bands are drawn without lineweight), // so a thin band still shows as a one-pixel line. Always, not below a threshold: // the list serves a whole band of zooms. Plots and exports draw the band alone, and // a transparent band gets none (its alpha would add up along the centre). let hairline = !b.plotting && b.opts.min_dash > 0.0 && b.transparency == 0; match lt.as_ref().filter(|_| b.opts.linetypes) { // 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, centres) = wide::dashed(p, tol, lt, ltscale, min); b.tris(ctx, rgb, &tris); if hairline { for c in ¢res { b.hairline(ctx, rgb, c); } } 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)); if hairline { let pl = Polyline { vertices: p.vertices.clone(), closed: p.closed }; b.hairline(ctx, rgb, &pl.tessellate(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 !b.opts.linetypes || 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) => draw_wipeout(b, ctx, w, rgb, lw), EntityKind::Image(i) => draw_image(b, ctx, i, rgb, lw), 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 = 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) => draw_mleader(b, ctx, e, m, 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); } } } } } } fn draw_mleader(b: &mut Builder, ctx: &Ctx, e: &Entity, m: &cadcraft_doc::MLeader, rgb: Ink, lw: f32) { // 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); } } fn draw_image(b: &mut Builder, ctx: &Ctx, i: &cadcraft_doc::Image, rgb: Ink, lw: f32) { let o = i.insert.xy(); let u = i.u.xy() * i.size.x; let v = i.v.xy() * i.size.y; // Display flag 1: show the image. Without pixels (file not found, not loaded yet, hidden) // the rectangle is crossed out. let raster = if i.display & 1 != 0 { ctx.d.rasters.get(&i.path) } else { None }; if let Some(r) = raster { b.image(ctx, rgb, i, r); } b.polyline(ctx, rgb, lw, &[o, o + u, o + u + v, o + v, o]); if raster.is_none() { b.polyline(ctx, rgb, lw, &[o, o + u + v]); b.polyline(ctx, rgb, lw, &[o + u, o + v]); } } fn draw_wipeout(b: &mut Builder, ctx: &Ctx, w: &cadcraft_doc::Wipeout, rgb: Ink, lw: f32) { // 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 { b.polyline(ctx, rgb, lw, &w.closed_boundary()); } } fn draw_dimension(b: &mut Builder, ctx: &Ctx, e: &Entity, dm: &cadcraft_doc::Dimension, lw: f32, lt: Option<&cadcraft_doc::Linetype>, ltscale: f64) { if let Some(blk) = dm.block.as_ref().and_then(|n| ctx.d.block(n)) && ctx.depth < cadcraft_doc::MAX_BLOCK_DEPTH { // DXF group 12: a moved dimension keeps its `*D` block where it was and the offset in // the insertion point, so the block draws translated by it (#2634). let off = dm.insertion_point.xy(); let sub = Ctx { block_pstyle: b.named, block_transparency: b.transparency, ..sub_ctx(ctx, e, Mat3::translate(off)) }; // DIMTFILL: the block's MTEXT carries no background mask here, so a background // around each of the block's texts is drawn before the block's first text or first // line of the text's marks, except for a text that a SOLID of the block covers. let style = ctx.d.dim_style(&dm.style).cloned().unwrap_or_default().with_overrides(&dm.overrides); let g = if style.text_fill != 0 { dimension_in(ctx.d, dm) } else { DimGeometry::default() }; let gap = style.text_gap.abs() * style.effective_scale(ctx.d.header.f64("DIMSCALE", 1.0)); let boxes: Vec> = if g.text_fill.is_empty() { Vec::new() } else { let solids: Vec<&Entity> = blk.entities.iter().map(|be| &**be).filter(|be| matches!(be.kind, EntityKind::Solid(_))).take(MAX_FILL_SOLIDS).collect(); // A text's own box (no gap): a SOLID holding all its corners is its background. let covered = |be: &Entity| block_text_box(ctx.d, be, 0.0).is_some_and(|t| solids.iter().any(|s| t.iter().all(|p| solid_covers(s, *p)))); blk.entities .iter() .filter(|be| matches!(be.kind, EntityKind::MText(_) | EntityKind::Text(_))) .take(MAX_FILL_TEXTS) .filter(|be| !covered(be)) .filter_map(|be| block_text_box(ctx.d, be, gap)) .map(|mut corners| { corners.iter_mut().for_each(|c| *c += off); corners }) .collect() }; let mut fill_pending = !boxes.is_empty(); for be in blk.entities.iter() { if !b.expand() { break; } if fill_pending && starts_text_marks(be, &g) { for corners in &boxes { dim_text_fill(b, ctx, e, &style, corners); } fill_pending = false; } entity(b, &sub, be); } } else { dimension(b, ctx, e, dm, lw, lt, ltscale); } } /// Draw a polyline with a linetype (None: continuous). pub(crate) fn stroke_with(b: &mut Builder, ctx: &Ctx, rgb: Ink, lw: f32, lt: Option<&cadcraft_doc::Linetype>, ltscale: f64, pts: &[Vec2]) { match lt.filter(|_| b.opts.linetypes) { 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), } } fn table(b: &mut Builder, ctx: &Ctx, t: &cadcraft_doc::Table, rgb: Ink, lw: f32) { table::draw(b, ctx, t, rgb, lw); } /// 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)) }