mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
Squash of PR #638 by @STE1200. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
135c928fea
commit
244feb6e89
6 files changed
+514
-87
No files matched your search
@@ -7,6 +7,7 @@
|
||||
//! cadcraft-cli commands [FILTER] command catalog (JSON)
|
||||
//! cadcraft-cli mcp [--connect HOST:PORT] MCP server on stdio (headless or bridged to the app)
|
||||
//! cadcraft-cli perf [N] timing table on a synthetic N-entity drawing
|
||||
//! cadcraft-cli perf FILE canvas display-list build of a drawing, zoomed in step by step
|
||||
//! cadcraft-cli sample (bracket|floorplan) OUT write a built-in sample drawing
|
||||
//! ```
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
@@ -60,7 +61,7 @@ const USAGE: &str = "usage:
|
||||
cadcraft-cli run [FILE | --sample | --metric] [--script TEXT] [--script-file F.scr] [--cmd 'id {json}']... [--save OUT.dxf] [--export OUT.(png|svg|pdf)] [FRAMING]
|
||||
cadcraft-cli commands [FILTER]
|
||||
cadcraft-cli mcp [--connect HOST:PORT]
|
||||
cadcraft-cli perf [N]
|
||||
cadcraft-cli perf [N | FILE]
|
||||
cadcraft-cli sample (bracket|floorplan) OUT.(dxf|dwg|svg|png)
|
||||
cadcraft-cli --version
|
||||
|
||||
@@ -393,11 +394,55 @@ fn synthetic(n: usize, side: f64) -> Result<cadcraft_engine::doc::Drawing, Strin
|
||||
Ok(dr)
|
||||
}
|
||||
|
||||
/// `perf FILE`: the display list built as the canvas builds it (a 1600 x 1000 px view at the
|
||||
/// drawing's centre: tolerance, dash level of detail and view window from the zoom band), at
|
||||
/// zoom extents and zoomed in 4x at a time, next to the full-detail build plots use.
|
||||
fn perf_file(path: &str) -> Result<(), String> {
|
||||
use cadcraft_engine::geom::{Bounds2, Vec2};
|
||||
use cadcraft_engine::render::{Options, build};
|
||||
use std::time::Instant;
|
||||
let ms = |t: Instant| t.elapsed().as_secs_f64() * 1000.0;
|
||||
let mut s = Session::empty();
|
||||
let t = Instant::now();
|
||||
open(&mut s, path)?;
|
||||
let open_ms = ms(t);
|
||||
let st = s.state().map_err(|e| e.to_string())?;
|
||||
let (d, space) = (&st.doc, &st.space);
|
||||
let ext = d.extents(space);
|
||||
let size = ext.width().max(ext.height());
|
||||
if ext.is_empty() || !(size.is_finite() && size > 0.0) {
|
||||
return Err("perf: the drawing is empty".into());
|
||||
}
|
||||
outln!("open {open_ms:.0} ms, {} entities, extents {:.1} x {:.1}", d.entity_count(), ext.width(), ext.height());
|
||||
outln!("{:>8} {:>10} {:>10}", "zoom", "build ms", "prims");
|
||||
let mut finest = Options::default();
|
||||
for k in 0..5 {
|
||||
let zoom = 4f64.powi(k);
|
||||
let h = size / zoom;
|
||||
let pixel = 2f64.powi((h / 1000.0).max(1e-300).log2().floor() as i32);
|
||||
let half = Vec2::new(h * 0.8, h * 0.5);
|
||||
let visible = Bounds2::new(ext.center() - half, ext.center() + half);
|
||||
let screen = Options { tolerance: pixel * 0.5, min_dash: pixel * 2.0, view: Some(visible.expand(h * 0.8)), ..Default::default() };
|
||||
let t = Instant::now();
|
||||
let n = build(d, space, &screen).prims.len();
|
||||
outln!("{:>7}x {:>10.1} {n:>10}", zoom, ms(t));
|
||||
finest = screen;
|
||||
}
|
||||
// Plots: every pattern line, everywhere (last, so its large list doesn't weigh on the above).
|
||||
let t = Instant::now();
|
||||
let n = build(d, space, &Options { min_dash: 0.0, view: None, ..finest }).prims.len();
|
||||
outln!("{:>8} {:>10.1} {n:>10}", "full", ms(t));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn perf(args: &[String]) -> Result<(), String> {
|
||||
use cadcraft_engine::doc::Space;
|
||||
use cadcraft_engine::geom::{Bounds2, Vec2};
|
||||
use cadcraft_engine::{select, snap};
|
||||
use std::time::Instant;
|
||||
if let Some(path) = args.first().filter(|a| a.parse::<usize>().is_err()) {
|
||||
return perf_file(path);
|
||||
}
|
||||
let n: usize = match args.first() {
|
||||
Some(a) => a.parse().map_err(|_| format!("perf: bad entity count {a}"))?,
|
||||
None => 200_000,
|
||||
|
||||
+10
-1
@@ -301,7 +301,16 @@ fn sheet(d: &Drawing, space: &Space, o: &PdfOptions) -> Result<SheetPage> {
|
||||
Space::Model => true,
|
||||
});
|
||||
let plot_style_table = cadcraft_doc::plot_style_table(d, &page.plot_style_table).map(std::sync::Arc::new);
|
||||
let ropts = cadcraft_render::Options { tolerance: 0.001, min_dash: 0.0, text: true, fill: true, lineweights, view_height: 0.0, plot_style_table };
|
||||
let ropts = cadcraft_render::Options {
|
||||
tolerance: 0.001,
|
||||
min_dash: 0.0,
|
||||
text: true,
|
||||
fill: true,
|
||||
lineweights,
|
||||
view_height: 0.0,
|
||||
plot_style_table,
|
||||
view: None,
|
||||
};
|
||||
let k = unit_mm * PT_PER_MM;
|
||||
let window = o.window.filter(|w| matches!(space, Space::Model) && !w.is_empty());
|
||||
let fit = o.fit.unwrap_or(window.is_some() || matches!(space, Space::Model));
|
||||
|
||||
+194
-72
@@ -1,10 +1,16 @@
|
||||
//! Hatch drawing: island detection picks the boundary loops that take part, then a solid,
|
||||
//! gradient or background fill (even-odd triangles) and the pattern lines (each pattern line
|
||||
//! family swept across the boundary, even-odd clipped, then dashed).
|
||||
//!
|
||||
//! On-screen builds get a level of detail (`Options::min_dash` > 0 is two pixels, `Options::view`
|
||||
//! the area being looked at): a pattern whose lines run together on screen is drawn as a solid
|
||||
//! fill of its boundary ([`reads_solid`]), a dash or dot repeat under two pixels as a continuous
|
||||
//! line (as linetypes already are), and nothing is generated outside the view. Plots, exports and
|
||||
//! picks pass `min_dash = 0` and `view = None` and get every line.
|
||||
|
||||
use super::{Builder, Ctx, Ink, fill, ink};
|
||||
use super::{Builder, Ctx, Ink, fill, ink, max_scale};
|
||||
use cadcraft_color::{Color, Rgb};
|
||||
use cadcraft_doc::library::pattern_for;
|
||||
use cadcraft_doc::library::{PatternLine, pattern_for};
|
||||
use cadcraft_doc::{Drawing, Entity, Gradient, Hatch};
|
||||
use cadcraft_geom::{Bounds2, Polyline, Vec2, point_in_polygon};
|
||||
|
||||
@@ -14,7 +20,12 @@ pub(super) fn draw(b: &mut Builder, ctx: &Ctx, e: &Entity, h: &Hatch, rgb: Ink,
|
||||
// Gradient and background colours given ByLayer follow the hatch's own layer.
|
||||
let layer = 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).unwrap_or(Color::Index(7));
|
||||
if h.solid || h.pattern.eq_ignore_ascii_case("SOLID") || h.gradient.is_some() {
|
||||
// Level of detail: a pixel in the hatch's own units, along its most stretched axis (so lines
|
||||
// that show apart in any direction are never merged); 0 = full detail.
|
||||
let stretch = max_scale(&ctx.xf);
|
||||
let px = if stretch > 1e-12 { b.opts.min_dash / 2.0 / stretch } else { 0.0 };
|
||||
let dense = b.opts.fill && reads_solid(ctx.d, h, px);
|
||||
if h.solid || h.pattern.eq_ignore_ascii_case("SOLID") || h.gradient.is_some() || dense {
|
||||
if b.opts.fill {
|
||||
let tris = fill::triangulate_evenodd(&loops);
|
||||
match &h.gradient {
|
||||
@@ -29,7 +40,12 @@ pub(super) fn draw(b: &mut Builder, ctx: &Ctx, e: &Entity, h: &Hatch, rgb: Ink,
|
||||
{
|
||||
b.tris(ctx, ink(bg, layer_color, ctx.block_color), &fill::triangulate_evenodd(&loops));
|
||||
}
|
||||
for seg in pattern_lines(ctx.d, h, &loops) {
|
||||
// The area being looked at, in the hatch's units.
|
||||
let view = b.opts.view.and_then(|v| {
|
||||
let inv = ctx.xf.inverse()?;
|
||||
Some(Bounds2::from_points(v.corners().iter().map(|p| inv.apply(*p))))
|
||||
});
|
||||
for seg in pattern_lines(ctx.d, h, &loops, px, view.as_ref()) {
|
||||
if seg.len() == 1 {
|
||||
if let Some(p) = seg.first() {
|
||||
b.point(ctx, rgb, *p);
|
||||
@@ -208,21 +224,86 @@ impl Field {
|
||||
|
||||
/// Upper bound on the line families drawn from a pattern definition read from a file.
|
||||
const MAX_PATTERN_LINES: usize = 1024;
|
||||
/// Upper bound on the runs of one hatch (AutoCAD's HPMAXLINES default), checked as runs are made
|
||||
/// (a hostile or mis-scaled pattern can't exhaust memory). A view-limited build makes at most the
|
||||
/// runs of a full build, so it never leaves out runs that a full build draws.
|
||||
const MAX_RUNS: usize = 1_000_000;
|
||||
/// Upper bound on the lines one family sweeps over the whole hatch; past it the family is left
|
||||
/// out (a full-detail build of a pattern far too fine for its area).
|
||||
const MAX_FAMILY_LINES: i64 = 20_000;
|
||||
/// The same bound when the sweep is limited to a view: the count then follows the view's size
|
||||
/// on screen, and lines closer than a pixel already read as solid, so only very large displays
|
||||
/// come near it.
|
||||
const MAX_VIEW_FAMILY_LINES: i64 = 100_000;
|
||||
/// Line families closer than this on screen (pixels) run together: the hatch reads as solid.
|
||||
pub const SOLID_SPACING_PX: f64 = 1.0;
|
||||
/// Average marks per pixel from which a pattern reads as solid (scattered marks leave about
|
||||
/// e^-2 ≈ 14% of the pixels empty there).
|
||||
pub const SOLID_COVERAGE: f64 = 2.0;
|
||||
|
||||
/// Upper bound on line runs drawn for one hatch (AutoCAD's HPMAXLINES default): a pattern far
|
||||
/// too dense for its area stops here instead of exhausting memory.
|
||||
const MAX_HATCH_SEGMENTS: usize = 1_000_000;
|
||||
|
||||
/// Hatch line runs in world coordinates (single-point runs are dots) for the fill `loops`.
|
||||
pub fn pattern_lines(d: &Drawing, h: &Hatch, loops: &[Vec<Vec2>]) -> Vec<Vec<Vec2>> {
|
||||
// The lines the file defined (as its author's CAD drew them), else the library's pattern of
|
||||
// that name, else ANSI31.
|
||||
let lines = if h.pattern_lines.is_empty() {
|
||||
/// The line families a hatch draws with: the lines its file defined (as its author's CAD drew
|
||||
/// them), else the library's pattern of that name as drawn in `d`, else ANSI31.
|
||||
fn lines_of(d: &Drawing, h: &Hatch) -> Vec<PatternLine> {
|
||||
if h.pattern_lines.is_empty() {
|
||||
pattern_for(d, &h.pattern).or_else(|| pattern_for(d, "ANSI31")).map(|p| p.lines).unwrap_or_default()
|
||||
} else {
|
||||
h.pattern_lines.iter().take(MAX_PATTERN_LINES).cloned().collect()
|
||||
};
|
||||
let scale = if h.scale.is_finite() && h.scale > 1e-9 { h.scale } else { 1.0 };
|
||||
}
|
||||
}
|
||||
|
||||
/// The hatch's pattern scale (1 when unusable).
|
||||
fn scale_of(h: &Hatch) -> f64 {
|
||||
if h.scale.is_finite() && h.scale > 1e-9 { h.scale } else { 1.0 }
|
||||
}
|
||||
|
||||
/// True when, at `px` hatch units per pixel, the pattern covers its area on screen, so drawing
|
||||
/// it line by line would fill (nearly) every pixel anyway. Each line, dash or dot is drawn at
|
||||
/// least a pixel wide, so that is the case when
|
||||
/// - some line family has its lines closer than [`SOLID_SPACING_PX`] and no gap along them a
|
||||
/// pixel or longer (continuous lines, or a repeat that reads as a continuous line), or
|
||||
/// - the families together put on average [`SOLID_COVERAGE`] or more marks on every pixel
|
||||
/// (dense dot or dash patterns made of several families).
|
||||
///
|
||||
/// A sparse dot or dash pattern keeps its look. `px <= 0` (full detail) is never solid.
|
||||
pub fn reads_solid(d: &Drawing, h: &Hatch, px: f64) -> bool {
|
||||
if !(px.is_finite() && px > 0.0) {
|
||||
return false;
|
||||
}
|
||||
let scale = scale_of(h);
|
||||
let mut coverage = 0.0;
|
||||
for f in lines_of(d, h) {
|
||||
let spacing = (f.delta.1 * scale).abs();
|
||||
if !(spacing.is_finite() && spacing > 0.0) {
|
||||
continue;
|
||||
}
|
||||
let pat_len: f64 = f.dashes.iter().map(|d| (d * scale).abs()).sum();
|
||||
// The share of a line inked on screen (a dash or dot is at least a pixel long there).
|
||||
let ink: f64 = f.dashes.iter().filter(|d| **d >= 0.0).map(|d| (d * scale).max(px)).sum();
|
||||
let inked = if f.dashes.is_empty() || (ink > 0.0 && pat_len < 2.0 * px) {
|
||||
1.0
|
||||
} else if pat_len > 0.0 {
|
||||
(ink / pat_len).min(1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let gap = f.dashes.iter().filter(|d| **d < 0.0).map(|d| (d * scale).abs()).fold(0.0, f64::max);
|
||||
let draws = f.dashes.is_empty() || ink > 0.0;
|
||||
if spacing < SOLID_SPACING_PX * px && draws && (inked >= 1.0 || gap < px) {
|
||||
return true;
|
||||
}
|
||||
coverage += inked * px / spacing;
|
||||
}
|
||||
coverage.is_finite() && coverage >= SOLID_COVERAGE
|
||||
}
|
||||
|
||||
/// Hatch line runs in world coordinates (single-point runs are dots) for the fill `loops`.
|
||||
/// `px`: a pixel in the hatch's units for the level of detail (a dash or dot repeat under two
|
||||
/// pixels is one continuous run); 0 = full detail, as plots, exports and picks use. `view`: only
|
||||
/// the part of the hatch inside it is generated; `None` = all of it.
|
||||
pub fn pattern_lines(d: &Drawing, h: &Hatch, loops: &[Vec<Vec2>], px: f64, view: Option<&Bounds2>) -> Vec<Vec<Vec2>> {
|
||||
let lines = lines_of(d, h);
|
||||
let scale = scale_of(h);
|
||||
let px = if px.is_finite() && px > 0.0 { px } else { 0.0 };
|
||||
let mut out = Vec::new();
|
||||
for fam in &lines {
|
||||
let ang = fam.angle.to_radians() + h.angle;
|
||||
@@ -233,77 +314,118 @@ pub fn pattern_lines(d: &Drawing, h: &Hatch, loops: &[Vec<Vec2>]) -> Vec<Vec<Vec
|
||||
continue;
|
||||
}
|
||||
let dashes: Vec<f64> = fam.dashes.iter().map(|d| d * scale).collect();
|
||||
family(loops, origin, ang, delta, spacing, &dashes, &mut out);
|
||||
if out.len() >= MAX_HATCH_SEGMENTS {
|
||||
Family { origin, ang, delta, spacing, dashes: &dashes }.sweep(loops, px, view, &mut out);
|
||||
if out.len() >= MAX_RUNS {
|
||||
break;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn family(loops: &[Vec<Vec2>], origin: Vec2, ang: f64, delta: Vec2, spacing: f64, dashes: &[f64], out: &mut Vec<Vec<Vec2>>) {
|
||||
// Work in a frame where the family's lines are horizontal.
|
||||
let to_local = |p: Vec2| (p - origin).rotate(-ang);
|
||||
let to_world = |p: Vec2| origin + p.rotate(ang);
|
||||
let local: Vec<Vec<Vec2>> = loops.iter().map(|l| l.iter().map(|p| to_local(*p)).collect()).collect();
|
||||
let bb = Bounds2::from_points(local.iter().flatten().copied());
|
||||
if bb.is_empty() {
|
||||
return;
|
||||
/// One pattern line family, scaled and placed: parallel lines `spacing` apart through `origin`
|
||||
/// at angle `ang`, each shifted along itself by `delta`'s along-line part from the last.
|
||||
struct Family<'a> {
|
||||
origin: Vec2,
|
||||
ang: f64,
|
||||
delta: Vec2,
|
||||
spacing: f64,
|
||||
dashes: &'a [f64],
|
||||
}
|
||||
|
||||
impl Family<'_> {
|
||||
/// Into the frame where the family's lines are horizontal.
|
||||
fn to_local(&self, p: Vec2) -> Vec2 {
|
||||
(p - self.origin).rotate(-self.ang)
|
||||
}
|
||||
let shift_per_line = delta.rotate(-ang).x;
|
||||
let k0 = (bb.min.y / spacing).floor() as i64;
|
||||
let k1 = (bb.max.y / spacing).ceil() as i64;
|
||||
if k1.saturating_sub(k0) > 20_000 {
|
||||
return;
|
||||
fn to_world(&self, p: Vec2) -> Vec2 {
|
||||
self.origin + p.rotate(self.ang)
|
||||
}
|
||||
let pat_len: f64 = dashes.iter().map(|d| d.abs()).sum();
|
||||
for k in k0..=k1 {
|
||||
if out.len() >= MAX_HATCH_SEGMENTS {
|
||||
|
||||
fn sweep(&self, loops: &[Vec<Vec2>], px: f64, view: Option<&Bounds2>, out: &mut Vec<Vec<Vec2>>) {
|
||||
let local: Vec<Vec<Vec2>> = loops.iter().map(|l| l.iter().map(|p| self.to_local(*p)).collect()).collect();
|
||||
let bb = Bounds2::from_points(local.iter().flatten().copied());
|
||||
if bb.is_empty() {
|
||||
return;
|
||||
}
|
||||
let y = k as f64 * spacing;
|
||||
let mut xs: Vec<f64> = Vec::new();
|
||||
for l in &local {
|
||||
let n = l.len();
|
||||
for i in 0..n {
|
||||
let (Some(a), Some(b)) = (l.get(i), l.get((i + 1) % n)) else { continue };
|
||||
if (a.y > y) != (b.y > y) {
|
||||
xs.push(a.x + (b.x - a.x) * (y - a.y) / (b.y - a.y));
|
||||
// The lines and spans swept: the boundary's extent, cut to the view's (its corners in
|
||||
// this frame) when there is one.
|
||||
let (mut lo, mut hi) = (bb.min, bb.max);
|
||||
if let Some(v) = view {
|
||||
let vb = Bounds2::from_points(v.corners().iter().map(|p| self.to_local(*p)));
|
||||
if vb.is_empty() || !vb.intersects(&bb) {
|
||||
return;
|
||||
}
|
||||
lo = Vec2::new(lo.x.max(vb.min.x), lo.y.max(vb.min.y));
|
||||
hi = Vec2::new(hi.x.min(vb.max.x), hi.y.min(vb.max.y));
|
||||
}
|
||||
let shift_per_line = self.delta.rotate(-self.ang).x;
|
||||
let k0 = (lo.y / self.spacing).floor() as i64;
|
||||
let k1 = (hi.y / self.spacing).ceil() as i64;
|
||||
let max_lines = if view.is_some() { MAX_VIEW_FAMILY_LINES } else { MAX_FAMILY_LINES };
|
||||
if k1.saturating_sub(k0) > max_lines {
|
||||
return;
|
||||
}
|
||||
let pat_len: f64 = self.dashes.iter().map(|d| d.abs()).sum();
|
||||
// Level of detail: a whole repeat under two pixels reads as a continuous line (when the
|
||||
// family draws anything at all).
|
||||
let draws = self.dashes.iter().any(|d| *d >= 0.0);
|
||||
let solid_line = self.dashes.is_empty() || pat_len < 1e-12 || (px > 0.0 && pat_len < 2.0 * px && draws);
|
||||
for k in k0..=k1 {
|
||||
if out.len() >= MAX_RUNS {
|
||||
return;
|
||||
}
|
||||
let y = k as f64 * self.spacing;
|
||||
let mut xs: Vec<f64> = Vec::new();
|
||||
for l in &local {
|
||||
let n = l.len();
|
||||
for i in 0..n {
|
||||
let (Some(a), Some(b)) = (l.get(i), l.get((i + 1) % n)) else { continue };
|
||||
if (a.y > y) != (b.y > y) {
|
||||
xs.push(a.x + (b.x - a.x) * (y - a.y) / (b.y - a.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
xs.sort_by(f64::total_cmp);
|
||||
let offset = shift_per_line * k as f64;
|
||||
for span in xs.chunks(2) {
|
||||
let [x0, x1] = span else { continue };
|
||||
let (x0, x1) = if view.is_some() { (x0.max(lo.x), x1.min(hi.x)) } else { (*x0, *x1) };
|
||||
if view.is_some() && x1 <= x0 {
|
||||
continue;
|
||||
}
|
||||
if solid_line {
|
||||
out.push(vec![self.to_world(Vec2::new(x0, y)), self.to_world(Vec2::new(x1, y))]);
|
||||
} else {
|
||||
self.dash(x0, x1, y, offset, pat_len, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
xs.sort_by(f64::total_cmp);
|
||||
let offset = shift_per_line * k as f64;
|
||||
for span in xs.chunks(2) {
|
||||
let [x0, x1] = span else { continue };
|
||||
if dashes.is_empty() || pat_len < 1e-12 {
|
||||
out.push(vec![to_world(Vec2::new(*x0, y)), to_world(Vec2::new(*x1, y))]);
|
||||
continue;
|
||||
}
|
||||
// Dash phase anchored at x = offset.
|
||||
let start_rep = ((x0 - offset) / pat_len).floor();
|
||||
let mut x = offset + start_rep * pat_len;
|
||||
let mut guard = 0;
|
||||
'outer: while x < *x1 {
|
||||
for d in dashes {
|
||||
guard += 1;
|
||||
if guard > 100_000 || out.len() >= MAX_HATCH_SEGMENTS {
|
||||
break 'outer;
|
||||
}
|
||||
let len = d.abs();
|
||||
if *d > 0.0 {
|
||||
let a = x.max(*x0);
|
||||
let b = (x + len).min(*x1);
|
||||
if b > a {
|
||||
out.push(vec![to_world(Vec2::new(a, y)), to_world(Vec2::new(b, y))]);
|
||||
}
|
||||
} else if *d == 0.0 && x >= *x0 && x <= *x1 {
|
||||
out.push(vec![to_world(Vec2::new(x, y))]);
|
||||
}
|
||||
x += len;
|
||||
if x >= *x1 {
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
/// The dashes and dots of the span `x0..x1` of the line at `y`, phase anchored at x = `offset`.
|
||||
fn dash(&self, x0: f64, x1: f64, y: f64, offset: f64, pat_len: f64, out: &mut Vec<Vec<Vec2>>) {
|
||||
let start_rep = ((x0 - offset) / pat_len).floor();
|
||||
let mut x = offset + start_rep * pat_len;
|
||||
let mut guard = 0;
|
||||
'outer: while x < x1 {
|
||||
for d in self.dashes {
|
||||
guard += 1;
|
||||
if guard > 100_000 || out.len() >= MAX_RUNS {
|
||||
break 'outer;
|
||||
}
|
||||
let len = d.abs();
|
||||
if *d > 0.0 {
|
||||
let a = x.max(x0);
|
||||
let b = (x + len).min(x1);
|
||||
if b > a {
|
||||
out.push(vec![self.to_world(Vec2::new(a, y)), self.to_world(Vec2::new(b, y))]);
|
||||
}
|
||||
} else if *d == 0.0 && x >= x0 && x <= x1 {
|
||||
out.push(vec![self.to_world(Vec2::new(x, y))]);
|
||||
}
|
||||
x += len;
|
||||
if x >= x1 {
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,14 +185,28 @@ pub struct Options {
|
||||
/// 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 }
|
||||
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> {
|
||||
|
||||
@@ -385,3 +385,201 @@ fn table_title_is_centred_and_merged() {
|
||||
let b = Bounds2::from_points(text);
|
||||
assert!((b.center().x - 3.0).abs() < 0.05, "{b:?}");
|
||||
}
|
||||
|
||||
// ---------- hatch level of detail (on-screen builds, issue #544) ----------
|
||||
|
||||
fn pattern_hatch(pattern: &str, scale: f64, (x0, y0, x1, y1): (f64, f64, f64, f64)) -> Hatch {
|
||||
let sq = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)].map(|(x, y)| PolyVertex::new(Vec2::new(x, y)));
|
||||
Hatch {
|
||||
pattern: pattern.into(),
|
||||
solid: false,
|
||||
loops: vec![HatchLoop { vertices: sq.to_vec(), outer: true, sources: Vec::new() }],
|
||||
scale,
|
||||
angle: 0.0,
|
||||
associative: false,
|
||||
style: 0,
|
||||
elevation: 0.0,
|
||||
gradient: None,
|
||||
origin: Vec2::ZERO,
|
||||
background: None,
|
||||
pattern_lines: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn drawing_with(h: Hatch) -> Drawing {
|
||||
let mut d = Drawing::new_imperial();
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Hatch(h)).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
fn loops_of(h: &Hatch) -> Vec<Vec<Vec2>> {
|
||||
crate::hatch::fill_loops(h, 0.001)
|
||||
}
|
||||
|
||||
/// On-screen options for a view where one pixel is `px` drawing units.
|
||||
fn screen(px: f64, view: Option<Bounds2>) -> Options {
|
||||
Options { tolerance: px / 2.0, min_dash: px * 2.0, view, ..Default::default() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dense_hatch_on_screen_is_one_solid_fill() {
|
||||
// ANSI31 at scale 0.01: lines 0.00125 apart over a 4x4 square. On a 1000 px view of it a
|
||||
// pixel is 0.004, so the lines run together: one solid fill. Plots keep every line.
|
||||
let d = drawing_with(pattern_hatch("ANSI31", 0.01, (0.0, 0.0, 4.0, 4.0)));
|
||||
let plot = build(&d, &Space::Model, &Options::default());
|
||||
assert!(plot.prims.len() > 4000, "plots keep every line: {}", plot.prims.len());
|
||||
let l = build(&d, &Space::Model, &screen(0.004, None));
|
||||
assert_eq!(l.prims.len(), 1);
|
||||
assert_eq!(l.prims[0].kind, Kind::Tris);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sparse_dots_keep_their_look_when_lines_are_close() {
|
||||
// Dot rows half a pixel apart but dots five pixels apart along them: not solid colour.
|
||||
let fam = |dashes: &[f64]| cadcraft_doc::library::PatternLine { angle: 0.0, origin: (0.0, 0.0), delta: (0.0, 0.5), dashes: dashes.to_vec() };
|
||||
let sparse = Hatch { pattern_lines: vec![fam(&[0.0, -5.0])], ..pattern_hatch("MYDOTS", 1.0, (0.0, 0.0, 40.0, 40.0)) };
|
||||
assert!(!crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &sparse, 1.0));
|
||||
// Dots closer than a pixel along rows closer than a pixel: solid.
|
||||
let dense = Hatch { pattern_lines: vec![fam(&[0.0, -0.5])], ..sparse.clone() };
|
||||
assert!(crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &dense, 1.0));
|
||||
// A family with only gaps draws nothing, so it never reads as solid.
|
||||
let blank = Hatch { pattern_lines: vec![fam(&[-0.5])], ..sparse.clone() };
|
||||
assert!(!crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &blank, 1.0));
|
||||
assert!(crate::hatch::pattern_lines(&cadcraft_doc::Drawing::new_imperial(), &blank, &loops_of(&blank), 1.0, None).is_empty());
|
||||
// Full detail is never solid.
|
||||
assert!(!crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &dense, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scattered_dot_pattern_reads_solid_once_dots_fill_the_pixels() {
|
||||
// AR-SAND at scale 3/16 (the library pattern is drawn 16x coarser since #643, so this is the
|
||||
// geometry of the old scale 3): three dot families, no single one dense enough on its own at
|
||||
// 0.5 per pixel, but together about four dots on every pixel. Zoomed in 4x the dots stand apart.
|
||||
let h = pattern_hatch("AR-SAND", 3.0 / 16.0, (0.0, 0.0, 100.0, 100.0));
|
||||
assert!(crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &h, 0.5));
|
||||
assert!(!crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &h, 0.125));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_zoom_pattern_is_unchanged() {
|
||||
// Lines and dash repeats a few pixels long or more: the level of detail changes nothing.
|
||||
for (pattern, px) in [("ANSI31", 0.05), ("ANSI33", 0.05), ("AR-CONC", 0.02), ("AR-SAND", 0.01), ("DOTS", 0.01)] {
|
||||
let h = pattern_hatch(pattern, 1.0, (0.0, 0.0, 6.0, 4.0));
|
||||
let loops = loops_of(&h);
|
||||
let full = crate::hatch::pattern_lines(&cadcraft_doc::Drawing::new_imperial(), &h, &loops, 0.0, None);
|
||||
assert!(!full.is_empty(), "{pattern}");
|
||||
assert!(!crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &h, px), "{pattern}");
|
||||
assert_eq!(crate::hatch::pattern_lines(&cadcraft_doc::Drawing::new_imperial(), &h, &loops, px, None), full, "{pattern}");
|
||||
// A view covering the whole hatch changes nothing either.
|
||||
let all = Bounds2::new(Vec2::new(-1.0, -1.0), Vec2::new(7.0, 5.0));
|
||||
assert_eq!(crate::hatch::pattern_lines(&cadcraft_doc::Drawing::new_imperial(), &h, &loops, px, Some(&all)), full, "{pattern}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dash_repeat_under_two_pixels_is_one_run() {
|
||||
// ANSI33's dashed family repeats every 0.1875: at 0.1 per pixel it reads as a continuous line,
|
||||
// drawn as one run per span covering the same length as the full-detail span.
|
||||
let h = pattern_hatch("ANSI33", 1.0, (0.0, 0.0, 6.0, 4.0));
|
||||
let loops = loops_of(&h);
|
||||
let full = crate::hatch::pattern_lines(&cadcraft_doc::Drawing::new_imperial(), &h, &loops, 0.0, None);
|
||||
let lod = crate::hatch::pattern_lines(&cadcraft_doc::Drawing::new_imperial(), &h, &loops, 0.1, None);
|
||||
assert!(lod.len() < full.len(), "{} vs {}", lod.len(), full.len());
|
||||
assert!(lod.iter().all(|r| r.len() == 2));
|
||||
// The continuous family: identical runs in both.
|
||||
let solid_runs = |v: &[Vec<Vec2>]| v.iter().filter(|r| r.len() == 2).count();
|
||||
assert!(solid_runs(&lod) > 0 && solid_runs(&full) > solid_runs(&lod));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hatch_pattern_only_inside_the_view() {
|
||||
// A 40x40 ANSI31 hatch looked at through a 2x2 view in a corner, at full pixel detail.
|
||||
let d = drawing_with(pattern_hatch("ANSI31", 1.0, (0.0, 0.0, 40.0, 40.0)));
|
||||
let full = build(&d, &Space::Model, &Options::default());
|
||||
let far = Some(Bounds2::new(Vec2::new(100.0, 100.0), Vec2::new(110.0, 110.0)));
|
||||
assert_eq!(build(&d, &Space::Model, &Options { view: far, ..Default::default() }).prims.len(), 0);
|
||||
let corner = Bounds2::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 2.0));
|
||||
let part = build(&d, &Space::Model, &Options { view: Some(corner), ..Default::default() });
|
||||
assert!(!part.prims.is_empty() && part.prims.len() * 10 < full.prims.len(), "{} of {}", part.prims.len(), full.prims.len());
|
||||
// Runs are cut in the family's rotated frame: a 2x2 view at 45° reaches 1 unit out, plus one
|
||||
// line spacing (0.125 × √2 along an axis) from rounding the line range outwards.
|
||||
let near = corner.expand(1.0 + 0.125 * 2f64.sqrt() + 1e-9);
|
||||
assert!(part.verts.iter().all(|p| near.contains(*p)));
|
||||
// Nothing is lost inside the view: the drawn length within an inner box is the same.
|
||||
let inner = Bounds2::new(Vec2::new(0.2, 0.2), Vec2::new(1.8, 1.8));
|
||||
let (a, b) = (length_inside(&full, &inner), length_inside(&part, &inner));
|
||||
assert!(a > 1.0 && (a - b).abs() < 1e-9, "{a} vs {b}");
|
||||
}
|
||||
|
||||
/// Total length of a display list's polylines inside an axis-aligned box (Liang–Barsky).
|
||||
fn length_inside(l: &DisplayList, bx: &Bounds2) -> f64 {
|
||||
let mut total = 0.0;
|
||||
for p in l.prims.iter().filter(|p| p.kind == Kind::Polyline) {
|
||||
for w in l.points(p).windows(2) {
|
||||
let (p0, d) = (w[0], w[1] - w[0]);
|
||||
let (mut t0, mut t1) = (0.0f64, 1.0f64);
|
||||
let mut ok = true;
|
||||
for (pp, q) in [(-d.x, p0.x - bx.min.x), (d.x, bx.max.x - p0.x), (-d.y, p0.y - bx.min.y), (d.y, bx.max.y - p0.y)] {
|
||||
if pp == 0.0 {
|
||||
ok &= q >= 0.0;
|
||||
} else if pp < 0.0 {
|
||||
t0 = t0.max(q / pp);
|
||||
} else {
|
||||
t1 = t1.min(q / pp);
|
||||
}
|
||||
}
|
||||
if ok && t1 > t0 {
|
||||
total += d.len() * (t1 - t0);
|
||||
}
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_hatch_seen_through_a_wide_view_is_drawn() {
|
||||
// ANSI31 (0.125 apart) over ±1e5 seen through a 3000-unit view: about 34,000 lines in the
|
||||
// family's rotated frame, past the 20,000-line guard of a whole-hatch sweep, but still drawn
|
||||
// when the sweep is limited to the view.
|
||||
let d = drawing_with(pattern_hatch("ANSI31", 1.0, (-1e5, -1e5, 1e5, 1e5)));
|
||||
let view = Some(Bounds2::new(Vec2::new(-1500.0, -1500.0), Vec2::new(1500.0, 1500.0)));
|
||||
let l = build(&d, &Space::Model, &Options { view, ..Default::default() });
|
||||
assert!(l.prims.len() > 20_000, "{}", l.prims.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hatch_in_a_squashed_block_keeps_lines_that_show() {
|
||||
// A block hatch inserted 100x wider than tall: along x its lines are spread far apart on
|
||||
// screen, so it is drawn with lines, not merged into a fill.
|
||||
let mut d = Drawing::new_imperial();
|
||||
let mut blk = cadcraft_doc::Block::new("B");
|
||||
let mut e = cadcraft_doc::Entity::new(cadcraft_doc::Handle(1), EntityKind::Hatch(pattern_hatch("ANSI31", 1.0, (0.0, 0.0, 4.0, 4.0))));
|
||||
e.common = Common::default();
|
||||
blk.entities.push(e);
|
||||
d.blocks.insert("B".into(), std::sync::Arc::new(blk));
|
||||
let ins = cadcraft_doc::Insert {
|
||||
block: "B".into(),
|
||||
insert: Vec3::new(0.0, 0.0, 0.0),
|
||||
scale: Vec3::new(100.0, 1.0, 1.0),
|
||||
rotation: 0.0,
|
||||
attribs: vec![],
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
col_spacing: 0.0,
|
||||
row_spacing: 0.0,
|
||||
};
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Insert(ins)).unwrap();
|
||||
// One pixel = 1 unit: 0.125 spacing × 100 along x is far more than a pixel.
|
||||
let l = build(&d, &Space::Model, &screen(1.0, None));
|
||||
assert!(l.prims.iter().all(|p| p.kind != Kind::Tris), "merged into a fill");
|
||||
assert!(l.prims.len() > 10, "{}", l.prims.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_scale_is_the_most_stretched_axis() {
|
||||
let m = Mat3::scale(1.0, 0.1);
|
||||
assert!((max_scale(&m) - 1.0).abs() < 1e-12);
|
||||
let r = Mat3::rotate(0.7).then(Mat3::scale(2.0, 2.0));
|
||||
assert!((max_scale(&r) - 2.0).abs() < 1e-12);
|
||||
assert!((max_scale(&Mat3::IDENTITY) - 1.0).abs() < 1e-12);
|
||||
}
|
||||
@@ -38,7 +38,10 @@ impl Xf {
|
||||
#[derive(Default)]
|
||||
pub struct CanvasState {
|
||||
pub list: Option<DisplayList>,
|
||||
key: (u64, u64, i32, bool, usize),
|
||||
key: (u64, u64, i32, i32, bool, usize),
|
||||
/// The area `list` was built for (hatch patterns exist only inside it); a view leaving it
|
||||
/// rebuilds the list.
|
||||
window: Option<Bounds2>,
|
||||
/// Bumped whenever `list` is rebuilt (identifies it for [`PrimIndex`]).
|
||||
list_gen: u64,
|
||||
prim_index: PrimIndex,
|
||||
@@ -142,32 +145,58 @@ fn color32(c: Rgb) -> Color32 {
|
||||
Color32::from_rgb(c.0, c.1, c.2)
|
||||
}
|
||||
|
||||
fn ensure_list(app: &mut CadApp, px: f64) {
|
||||
/// Rebuild the display list when needed. `px`: drawing units per pixel in the current view;
|
||||
/// `views`: every area on screen that shows the list (the current view, other model tiles) with
|
||||
/// its drawing units per pixel.
|
||||
fn ensure_list(app: &mut CadApp, px: f64, views: &[(Bounds2, f64)]) {
|
||||
let Ok(st) = app.session.state() else { return };
|
||||
// Rebuild when the drawing changes or the zoom moves by more than 2x (tessellation band).
|
||||
let band = px.max(1e-300).log2().floor() as i32;
|
||||
let key = (st.uid, st.revision, band, app.session.settings.lwdisplay, std::sync::Arc::as_ptr(&st.doc) as usize);
|
||||
if app.canvas.list.is_some() && app.canvas.key == key {
|
||||
// Rebuild when the drawing changes, the zoom moves by more than 2x (tessellation band) or a
|
||||
// view leaves the area the list was built for.
|
||||
let band = |px: f64| px.max(1e-300).log2().floor() as i32;
|
||||
// Hatch level of detail follows the finest view showing the list.
|
||||
let lod_band = views.iter().map(|v| band(v.1)).fold(band(px), i32::min);
|
||||
let key = (st.uid, st.revision, band(px), lod_band, app.session.settings.lwdisplay, std::sync::Arc::as_ptr(&st.doc) as usize);
|
||||
let visible = views.iter().fold(Bounds2::EMPTY, |acc, v| acc.union(&v.0));
|
||||
let inside = app.canvas.window.is_none_or(|w| visible.is_empty() || w.contains_box(&visible));
|
||||
if app.canvas.list.is_some() && app.canvas.key == key && inside {
|
||||
return;
|
||||
}
|
||||
let t0 = crate::now_ms();
|
||||
let opts = cadcraft_render::Options {
|
||||
tolerance: px * 0.5 * 2f64.powi(band) / px.max(1e-300) * px.min(1.0e300),
|
||||
min_dash: px * 2.0,
|
||||
tolerance: 2f64.powi(band(px)) * 0.5,
|
||||
// Two pixels at the band's finest zoom: the list serves every zoom in the band, so its
|
||||
// level of detail must hold at the finest one.
|
||||
min_dash: 2f64.powi(lod_band) * 2.0,
|
||||
text: true,
|
||||
fill: true,
|
||||
lineweights: app.session.settings.lwdisplay,
|
||||
view_height: st.paper_view().height,
|
||||
plot_style_table: None,
|
||||
view: build_window(views),
|
||||
};
|
||||
let opts = cadcraft_render::Options { tolerance: 2f64.powi(band) * 0.5, ..opts };
|
||||
let space = st.space.clone();
|
||||
app.canvas.list = Some(cadcraft_render::build(&st.doc, &space, &opts));
|
||||
app.canvas.key = key;
|
||||
app.canvas.window = opts.view;
|
||||
app.canvas.list_gen = app.canvas.list_gen.wrapping_add(1);
|
||||
app.canvas.build_ms = crate::now_ms() - t0;
|
||||
}
|
||||
|
||||
/// The area a display list is built for: each view with half its larger side of margin all
|
||||
/// round (panning by up to that much doesn't rebuild), joined. `None` (everything) when a view
|
||||
/// isn't usable.
|
||||
fn build_window(views: &[(Bounds2, f64)]) -> Option<Bounds2> {
|
||||
let mut w = Bounds2::EMPTY;
|
||||
for (v, _) in views {
|
||||
let side = v.width().max(v.height());
|
||||
if v.is_empty() || !side.is_finite() {
|
||||
return None;
|
||||
}
|
||||
w = w.union(&v.expand(side * 0.5));
|
||||
}
|
||||
(!w.is_empty()).then_some(w)
|
||||
}
|
||||
|
||||
/// Grid spacing adapted to the zoom: multiply the unit until lines are at least `min_px` apart.
|
||||
fn adaptive(unit: f64, scale: f64, min_px: f64) -> f64 {
|
||||
let mut u = unit.max(1e-12);
|
||||
@@ -330,7 +359,7 @@ fn draw_list_gpu(c: &mut CanvasState, p: &egui::Painter, xf: &Xf, bg: Rgb, lwdis
|
||||
let key = {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
(c.key, origin.x.to_bits(), origin.y.to_bits(), bg.0, bg.1, bg.2, lwdisplay).hash(&mut h);
|
||||
(c.key, c.list_gen, origin.x.to_bits(), origin.y.to_bits(), bg.0, bg.1, bg.2, lwdisplay).hash(&mut h);
|
||||
h.finish()
|
||||
};
|
||||
if c.mesh_key != Some(key) {
|
||||
@@ -852,7 +881,12 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
|
||||
// ---------- paint ----------
|
||||
painter.rect_filled(rect, 0.0, t.canvas);
|
||||
let t0 = crate::now_ms();
|
||||
ensure_list(app, 1.0 / xf_paper.scale);
|
||||
let mut views = vec![(xf_paper.world_bounds(), 1.0 / xf_paper.scale)];
|
||||
views.extend(tiles.iter().filter(|(r, tile)| !tile.active && r.width() >= 1.0 && r.height() >= 1.0).map(|(r, tile)| {
|
||||
let xf = tile_xf(*r, tile);
|
||||
(xf.world_bounds(), 1.0 / xf.scale)
|
||||
}));
|
||||
ensure_list(app, 1.0 / xf_paper.scale, &views);
|
||||
let sheet = app.canvas.list.as_ref().and_then(|l| l.sheet);
|
||||
let bg = match sheet {
|
||||
Some(sh) => {
|
||||
@@ -1056,6 +1090,11 @@ fn tile_rects(app: &CadApp, full: Rect) -> Vec<(Rect, cadcraft_engine::ModelTile
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The world ↔ screen transform of a model tile drawn in `r`.
|
||||
fn tile_xf(r: Rect, tile: &cadcraft_engine::ModelTile) -> Xf {
|
||||
Xf { rect: r, center: tile.view.center, scale: f64::from(r.height()) / tile.view.height.max(1e-12) }
|
||||
}
|
||||
|
||||
/// The tiles other than the current one (each with its own view, clipped to it), then the
|
||||
/// tile borders: thin, and heavy around the current tile.
|
||||
fn draw_tiles(app: &CadApp, ui: &egui::Ui, tiles: &[(Rect, cadcraft_engine::ModelTile)]) {
|
||||
@@ -1063,7 +1102,7 @@ fn draw_tiles(app: &CadApp, ui: &egui::Ui, tiles: &[(Rect, cadcraft_engine::Mode
|
||||
let bg = Rgb(t.canvas.r(), t.canvas.g(), t.canvas.b());
|
||||
for (r, tile) in tiles.iter().filter(|(r, tile)| !tile.active && r.width() >= 1.0 && r.height() >= 1.0) {
|
||||
let p = ui.painter_at(*r);
|
||||
let xf = Xf { rect: *r, center: tile.view.center, scale: f64::from(r.height()) / tile.view.height.max(1e-12) };
|
||||
let xf = tile_xf(*r, tile);
|
||||
p.rect_filled(*r, 0.0, t.canvas);
|
||||
draw_grid(app, &p, &xf);
|
||||
if let Some(list) = &app.canvas.list {
|
||||
|
||||
Reference in new issue
Block a user