Points draw with the point style (PDMODE/PDSIZE) (fixes #261) (#265)

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:
Valentin Klein 2026-10-10 21:33:43 +02:00 committed by GitHub
parent 575e690cc5
commit 4c056c54bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 171 additions and 4 deletions

View File

@ -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) {

View File

@ -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 {

View File

@ -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
}

View File

@ -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}");
}
}
}

View File

@ -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();