POINT objects were always a one-pixel dot. They now draw the figure PDMODE picks (nothing, plus, cross, tick, with an optional circle and/or square) at the size PDSIZE gives: drawing units when positive, a percentage of the view height when negative, 5 % when 0. The canvas passes its view height; exports and plots use the plotted window or the extents. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
575e690cc5
commit
4c056c54bd
|
|
@ -130,7 +130,7 @@ pub fn pdf(d: &Drawing, space: &Space, o: &PdfOptions) -> Result<Vec<u8>> {
|
|||
Space::Paper(_) => page.lineweights,
|
||||
Space::Model => true,
|
||||
});
|
||||
let ropts = cadcraft_render::Options { tolerance: 0.001, min_dash: 0.0, text: true, fill: true, lineweights };
|
||||
let ropts = cadcraft_render::Options { tolerance: 0.001, min_dash: 0.0, text: true, fill: true, lineweights, view_height: 0.0 };
|
||||
let k = unit_mm * PT_PER_MM;
|
||||
let window = o.window.filter(|w| matches!(space, Space::Model) && !w.is_empty());
|
||||
let fit = window.is_some() || o.fit.unwrap_or(matches!(space, Space::Model));
|
||||
|
|
@ -138,7 +138,9 @@ pub fn pdf(d: &Drawing, space: &Space, o: &PdfOptions) -> Result<Vec<u8>> {
|
|||
let est = plot_scale(&window.unwrap_or_else(|| d.extents(space)), &sheet, fit, o.scale);
|
||||
let tol = 0.05 / unit_mm / est.max(1e-300);
|
||||
let tolerance = if tol.is_finite() && tol > 0.0 { tol } else { ropts.tolerance };
|
||||
let list = cadcraft_render::build_plot(d, space, &cadcraft_render::Options { tolerance, ..ropts });
|
||||
// Relative point sizes follow the plotted window (else the extents).
|
||||
let view_height = window.map(|w| w.height()).unwrap_or(0.0);
|
||||
let list = cadcraft_render::build_plot(d, space, &cadcraft_render::Options { tolerance, view_height, ..ropts });
|
||||
let b = window.unwrap_or(list.bounds);
|
||||
let s = plot_scale(&b, &sheet, fit, o.scale);
|
||||
let map = if fit || matches!(space, Space::Model) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ mod fill;
|
|||
mod hatch;
|
||||
mod linetype;
|
||||
pub mod paper;
|
||||
mod point;
|
||||
pub mod raster;
|
||||
pub mod units;
|
||||
|
||||
|
|
@ -90,11 +91,14 @@ pub struct Options {
|
|||
/// 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 }
|
||||
Options { tolerance: 0.001, min_dash: 0.0, text: true, fill: true, lineweights: false, view_height: 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,6 +234,7 @@ fn top_ctx<'a>(d: &'a Drawing, xf: Mat3, top: Handle, frozen: &'a [String], vp_c
|
|||
}
|
||||
|
||||
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);
|
||||
|
|
@ -251,6 +256,18 @@ fn build_space(d: &Drawing, space: &Space, opts: &Options, plotting: bool) -> Di
|
|||
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;
|
||||
|
||||
|
|
@ -353,6 +370,15 @@ fn push_raw(dst: &mut DisplayList, p: &DPrim, kind: Kind, pts: &[Vec2]) {
|
|||
|
||||
/// 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);
|
||||
|
|
@ -547,7 +573,18 @@ fn entity(b: &mut Builder, ctx: &Ctx, e: &Entity) {
|
|||
mtext(b, ctx, t, rgb, lw);
|
||||
}
|
||||
}
|
||||
EntityKind::Point(p) => b.point(ctx, rgb, p.p.xy()),
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
//! POINT display figures: point style (`PDMODE`) and size (`PDSIZE`).
|
||||
|
||||
use cadcraft_geom::{Circle, Vec2};
|
||||
|
||||
/// The figure a POINT draws.
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct Figure {
|
||||
/// A one-pixel dot at the point.
|
||||
pub dot: bool,
|
||||
/// Line work (polylines).
|
||||
pub lines: Vec<Vec<Vec2>>,
|
||||
}
|
||||
|
||||
/// Figure size in drawing units: a positive `PDSIZE` is absolute, a negative one a percentage
|
||||
/// of `view_height`, and 0 is 5 % of `view_height`.
|
||||
pub fn size(pdsize: f64, view_height: f64) -> f64 {
|
||||
if pdsize.is_finite() && pdsize > 0.0 {
|
||||
return pdsize;
|
||||
}
|
||||
let percent = if pdsize.is_finite() && pdsize < 0.0 { -pdsize } else { 5.0 };
|
||||
view_height * percent / 100.0
|
||||
}
|
||||
|
||||
/// Whether `PDMODE` `mode` draws a figure with a size (anything but a dot or nothing).
|
||||
pub fn sized(mode: i64) -> bool {
|
||||
matches!(mode & 31, 2..=4) || mode & 96 != 0
|
||||
}
|
||||
|
||||
/// The figure for `PDMODE` `mode`, `size` across, at `at`, turned by `angle` (radians). The low
|
||||
/// bits pick dot (0), nothing (1), plus (2), cross (3) or a tick upwards (4); +32 adds a
|
||||
/// circle and +64 a square. Without a usable size every visible figure falls back to a dot.
|
||||
pub fn figure(mode: i64, size: f64, at: Vec2, angle: f64, tol: f64) -> Figure {
|
||||
let base = mode & 31;
|
||||
let mut f = Figure::default();
|
||||
let r = size / 2.0;
|
||||
if !(r.is_finite() && r > 0.0) {
|
||||
f.dot = base != 1;
|
||||
return f;
|
||||
}
|
||||
let angle = if angle.is_finite() { angle } else { 0.0 };
|
||||
let p = |x: f64, y: f64| at + Vec2::new(x, y).rotate(angle);
|
||||
match base {
|
||||
1 => {}
|
||||
2 => f.lines.extend([vec![p(-r, 0.0), p(r, 0.0)], vec![p(0.0, -r), p(0.0, r)]]),
|
||||
3 => f.lines.extend([vec![p(-r, -r), p(r, r)], vec![p(-r, r), p(r, -r)]]),
|
||||
4 => f.lines.push(vec![at, p(0.0, r)]),
|
||||
_ => f.dot = true,
|
||||
}
|
||||
if mode & 32 != 0 {
|
||||
let mut pts = Vec::new();
|
||||
Circle { center: at, radius: r }.tessellate(tol.max(r * 1e-4), &mut pts);
|
||||
f.lines.push(pts);
|
||||
}
|
||||
if mode & 64 != 0 {
|
||||
f.lines.push(vec![p(-r, -r), p(r, -r), p(r, r), p(-r, r), p(-r, -r)]);
|
||||
}
|
||||
f
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
//! POINT display: point style (PDMODE) and size (PDSIZE) (issue #261).
|
||||
|
||||
use cadcraft_doc::{Common, Drawing, EntityKind, Line, Point, Space};
|
||||
use cadcraft_geom::{Bounds2, Vec3};
|
||||
use cadcraft_render::{DisplayList, Kind, Options, build};
|
||||
|
||||
/// A point at (5,5) and a vertical line 100 units long (the extents are 100 high).
|
||||
fn drawing(pdmode: i64, pdsize: f64) -> Drawing {
|
||||
let mut d = Drawing::new_imperial();
|
||||
d.header.set_i64("PDMODE", pdmode);
|
||||
d.header.set_f64("PDSIZE", pdsize);
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Point(Point { p: Vec3::new(5.0, 5.0, 0.0), angle: 0.0 })).unwrap();
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: Vec3::new(0.0, 0.0, 0.0), b: Vec3::new(0.0, 100.0, 0.0) })).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// Dots and the bounds of the point's line work (the line along x = 0 is left out).
|
||||
fn point_parts(l: &DisplayList) -> (usize, usize, Bounds2) {
|
||||
let dots = l.prims.iter().filter(|p| p.kind == Kind::Point).count();
|
||||
let lines: Vec<_> = l.prims.iter().filter(|p| p.kind == Kind::Polyline && l.points(p).iter().any(|q| q.x > 1.0)).collect();
|
||||
let b = Bounds2::from_points(lines.iter().flat_map(|p| l.points(p).iter().copied()));
|
||||
(dots, lines.len(), b)
|
||||
}
|
||||
|
||||
fn close(b: &Bounds2, x0: f64, y0: f64, x1: f64, y1: f64) -> bool {
|
||||
[b.min.x - x0, b.min.y - y0, b.max.x - x1, b.max.y - y1].iter().all(|v| v.abs() < 1e-3)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_in_circle_with_absolute_size() {
|
||||
let l = build(&drawing(35, 1.0), &Space::Model, &Options::default());
|
||||
let (dots, lines, b) = point_parts(&l);
|
||||
assert_eq!((dots, lines), (0, 3), "an X (two lines) and a circle, no dot");
|
||||
assert!(close(&b, 4.5, 4.5, 5.5, 5.5), "{b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dot_and_nothing() {
|
||||
let (dots, lines, _) = point_parts(&build(&drawing(0, 1.0), &Space::Model, &Options::default()));
|
||||
assert_eq!((dots, lines), (1, 0));
|
||||
let (dots, lines, _) = point_parts(&build(&drawing(1, 1.0), &Space::Model, &Options::default()));
|
||||
assert_eq!((dots, lines), (0, 0));
|
||||
// 96 = square and circle around a dot.
|
||||
let (dots, lines, b) = point_parts(&build(&drawing(96, 2.0), &Space::Model, &Options::default()));
|
||||
assert_eq!((dots, lines), (1, 2));
|
||||
assert!(close(&b, 4.0, 4.0, 6.0, 6.0), "{b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_sizes_follow_the_view_height() {
|
||||
// PDSIZE -10: 10 % of a 50-unit view = 5 across.
|
||||
let opts = Options { view_height: 50.0, ..Options::default() };
|
||||
let (_, lines, b) = point_parts(&build(&drawing(2, -10.0), &Space::Model, &opts));
|
||||
assert_eq!(lines, 2);
|
||||
assert!(close(&b, 2.5, 2.5, 7.5, 7.5), "{b:?}");
|
||||
// PDSIZE 0 without a view height: 5 % of the 100-unit extents = 5 across.
|
||||
let (_, _, b) = point_parts(&build(&drawing(3, 0.0), &Space::Model, &Options::default()));
|
||||
assert!(close(&b, 2.5, 2.5, 7.5, 7.5), "{b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_point_settings_do_not_panic() {
|
||||
for mode in [i64::MIN, -1, 5, 31, 127, i64::MAX] {
|
||||
for size in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -1e300, 1e300, 0.0, -5.0] {
|
||||
let l = build(&drawing(mode, size), &Space::Model, &Options::default());
|
||||
assert!(l.verts.iter().all(|v| v.is_finite()), "PDMODE {mode} PDSIZE {size}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -175,6 +175,7 @@ fn ensure_list(app: &mut CadApp, px: f64) {
|
|||
text: true,
|
||||
fill: true,
|
||||
lineweights: app.session.settings.lwdisplay,
|
||||
view_height: st.paper_view().height,
|
||||
};
|
||||
let opts = cadcraft_render::Options { tolerance: 2f64.powi(band) * 0.5, ..opts };
|
||||
let space = st.space.clone();
|
||||
|
|
|
|||
Loading…
Reference in New Issue