2026-03-22 14:56:27 +03:00
|
|
|
|
// HatchModel — CPU-side hatch fill data; rendered entirely on the GPU.
|
2026-05-06 22:42:22 +03:00
|
|
|
|
use std::sync::Arc;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
//
|
2026-05-10 20:29:39 +03:00
|
|
|
|
// The boundary is a closed polygon in world XY coordinates.
|
2026-03-22 14:56:27 +03:00
|
|
|
|
// The GPU fragment shader performs point-in-polygon and hatch-line tests so
|
|
|
|
|
|
// no line geometry is ever tessellated on the CPU.
|
|
|
|
|
|
|
2026-05-10 20:29:39 +03:00
|
|
|
|
pub const MAX_HATCH_BOUNDARY_VERTS: usize = 1024;
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
/// One line family from a PAT-format hatch pattern.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Format mirrors the standard PAT line format:
|
|
|
|
|
|
/// `angle_deg, x0, y0, dx, dy [, dash1, dash2, ...]`
|
|
|
|
|
|
///
|
|
|
|
|
|
/// The perpendicular spacing between adjacent parallel lines is:
|
|
|
|
|
|
/// `| -dx * sin(angle) + dy * cos(angle) |`
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
|
pub struct PatFamily {
|
|
|
|
|
|
/// Line direction in degrees.
|
|
|
|
|
|
pub angle_deg: f32,
|
|
|
|
|
|
/// Origin of the first line in this family.
|
|
|
|
|
|
pub x0: f32,
|
|
|
|
|
|
pub y0: f32,
|
|
|
|
|
|
/// Step vector to the next parallel line.
|
|
|
|
|
|
pub dx: f32,
|
|
|
|
|
|
pub dy: f32,
|
|
|
|
|
|
/// Dash/gap sequence: positive = dash length, negative = gap length.
|
|
|
|
|
|
/// Empty = solid (no dash pattern).
|
|
|
|
|
|
pub dashes: Vec<f32>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Hatch fill pattern.
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
|
pub enum HatchPattern {
|
|
|
|
|
|
/// Opaque solid fill.
|
|
|
|
|
|
Solid,
|
|
|
|
|
|
/// One or more line families (PAT format).
|
|
|
|
|
|
Pattern(Vec<PatFamily>),
|
|
|
|
|
|
/// Linear gradient from `color` to `color2` along `angle_deg`.
|
|
|
|
|
|
Gradient { angle_deg: f32, color2: [f32; 4] },
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// A hatched region defined by a closed polygon boundary.
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
|
pub struct HatchModel {
|
2026-05-16 08:41:23 +03:00
|
|
|
|
/// World XY anchor (in the same offset-relative coordinate space as
|
|
|
|
|
|
/// the rest of the scene — `world_offset` already subtracted, but
|
|
|
|
|
|
/// kept at f64 precision). Boundary vertices are stored as f32
|
|
|
|
|
|
/// offsets from this anchor so that:
|
|
|
|
|
|
/// 1) hit-test / paper_canvas can still read small-magnitude f32
|
|
|
|
|
|
/// coords without precision loss from the f64 → f32 cast that
|
|
|
|
|
|
/// would otherwise happen at large drawing extents (UTM, etc.).
|
|
|
|
|
|
/// 2) the GPU pipeline can pre-shift the quad in hatch-local
|
|
|
|
|
|
/// space (so the fragment shader's `xz` varying stays small)
|
|
|
|
|
|
/// and add `world_origin` back inside the view_proj multiply.
|
|
|
|
|
|
/// Reconstruct WCS-relative coords as `(world_origin.x + v.x as f64,
|
|
|
|
|
|
/// world_origin.y + v.y as f64)`.
|
|
|
|
|
|
pub world_origin: [f64; 2],
|
|
|
|
|
|
/// World-XY coordinates of the boundary polygon vertices, stored as
|
|
|
|
|
|
/// f32 offsets from `world_origin`. NaN-NaN sentinels separate
|
|
|
|
|
|
/// disconnected paths and must be preserved un-shifted by consumers.
|
2026-05-06 22:42:22 +03:00
|
|
|
|
pub boundary: Arc<Vec<[f32; 2]>>,
|
2026-07-11 21:55:10 +03:00
|
|
|
|
/// Exact absolute-WCS boundary in f64, set only by the draw commands so a
|
|
|
|
|
|
/// typed boundary vertex is persisted without the f32 quantization the
|
|
|
|
|
|
/// render-side `boundary` would incur (issue #311). `None` for hatches
|
|
|
|
|
|
/// rebuilt from a DXF entity — `add_hatch` then reconstructs the persisted
|
|
|
|
|
|
/// vertices from `boundary` + `world_origin` instead.
|
|
|
|
|
|
pub boundary_wcs: Option<Arc<Vec<[f64; 2]>>>,
|
2026-03-22 14:56:27 +03:00
|
|
|
|
/// Fill pattern.
|
|
|
|
|
|
pub pattern: HatchPattern,
|
|
|
|
|
|
/// Catalog name for this pattern (e.g. "ANSI31", "SOLID", "LINEAR").
|
|
|
|
|
|
/// Stored so `add_hatch()` can write the correct name to the DXF entity.
|
|
|
|
|
|
pub name: String,
|
|
|
|
|
|
/// RGBA color in [0,1].
|
|
|
|
|
|
pub color: [f32; 4],
|
|
|
|
|
|
/// Pattern rotation offset in radians (from DXF `pattern_angle`).
|
|
|
|
|
|
/// Applied on top of each family's base angle at render time.
|
|
|
|
|
|
pub angle_offset: f32,
|
|
|
|
|
|
/// Pattern scale multiplier (from DXF `pattern_scale`).
|
|
|
|
|
|
pub scale: f32,
|
2026-05-15 23:13:16 +03:00
|
|
|
|
/// Optional world-space XY rect [x0, y0, x1, y1] for paper-space
|
|
|
|
|
|
/// viewport clipping. When `Some`, the pipeline translates it into
|
|
|
|
|
|
/// a per-frame pixel scissor and clips this hatch's draw call to it,
|
|
|
|
|
|
/// preventing viewport-projected content from bleeding past the
|
|
|
|
|
|
/// viewport frame. Mirrors `WireModel.vp_scissor`.
|
|
|
|
|
|
pub vp_scissor: Option<[f32; 4]>,
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
|
/// Normalized draw-order depth in (0,1); higher draws on top. Fed to the
|
|
|
|
|
|
/// hatch pipeline as a small clip-z bias so this fill orders correctly
|
|
|
|
|
|
/// against other entity types. 0.0 for transient/preview hatches.
|
|
|
|
|
|
pub draw_depth: f32,
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
|
|
|
|
|
|
impl HatchModel {
|
|
|
|
|
|
/// CPU-side rasteriser for `HatchPattern::Pattern` — produces the line
|
|
|
|
|
|
/// segments inside the boundary so non-GPU consumers (PDF export,
|
|
|
|
|
|
/// `paper_canvas`, print preview) can draw the actual pattern instead
|
|
|
|
|
|
/// of just the outline.
|
|
|
|
|
|
///
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
/// Coordinate frame: each emitted segment is absolute WCS in f64, i.e.
|
|
|
|
|
|
/// `world_origin + boundary[i]` resolved. Solid / gradient hatches return an
|
|
|
|
|
|
/// empty vec — callers fall back to their solid-fill path.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// The whole rasterisation is f64 because the pattern is anchored at world
|
|
|
|
|
|
/// (0, 0): a hatch at UTM sits ~5e5 away from that anchor, so the line index
|
|
|
|
|
|
/// `k` runs to ~1e7 and `origin + k · step` in f32 quantises the family lines
|
|
|
|
|
|
/// into visible garbage, as does the `edge − line` cancellation below.
|
|
|
|
|
|
pub fn pattern_segments(&self) -> Vec<[[f64; 2]; 2]> {
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
let HatchPattern::Pattern(families) = &self.pattern else {
|
|
|
|
|
|
return Vec::new();
|
|
|
|
|
|
};
|
|
|
|
|
|
if self.boundary.is_empty() || families.is_empty() {
|
|
|
|
|
|
return Vec::new();
|
|
|
|
|
|
}
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let ox = self.world_origin[0];
|
|
|
|
|
|
let oy = self.world_origin[1];
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
|
|
|
|
|
|
// ── Build edge list from boundary, splitting on NaN sentinels.
|
|
|
|
|
|
// Each sub-path is closed (last → first edge) so even-odd
|
|
|
|
|
|
// inside-tests work for islands / holes.
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let mut edges: Vec<([f64; 2], [f64; 2])> = Vec::new();
|
|
|
|
|
|
let mut sub_start: Option<[f64; 2]> = None;
|
|
|
|
|
|
let mut prev: Option<[f64; 2]> = None;
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
for &[bx, by] in self.boundary.iter() {
|
|
|
|
|
|
if bx.is_nan() || by.is_nan() {
|
|
|
|
|
|
if let (Some(s), Some(p)) = (sub_start, prev) {
|
|
|
|
|
|
if (s[0] - p[0]).abs() > 1e-6 || (s[1] - p[1]).abs() > 1e-6 {
|
|
|
|
|
|
edges.push((p, s));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
sub_start = None;
|
|
|
|
|
|
prev = None;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let pt = [bx as f64 + ox, by as f64 + oy];
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
match (sub_start, prev) {
|
|
|
|
|
|
(None, _) => {
|
|
|
|
|
|
sub_start = Some(pt);
|
|
|
|
|
|
prev = Some(pt);
|
|
|
|
|
|
}
|
|
|
|
|
|
(Some(_), Some(p)) => {
|
|
|
|
|
|
edges.push((p, pt));
|
|
|
|
|
|
prev = Some(pt);
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if let (Some(s), Some(p)) = (sub_start, prev) {
|
|
|
|
|
|
if (s[0] - p[0]).abs() > 1e-6 || (s[1] - p[1]).abs() > 1e-6 {
|
|
|
|
|
|
edges.push((p, s));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if edges.is_empty() {
|
|
|
|
|
|
return Vec::new();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── AABB of the boundary in world coords.
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let mut min_x = f64::INFINITY;
|
|
|
|
|
|
let mut max_x = f64::NEG_INFINITY;
|
|
|
|
|
|
let mut min_y = f64::INFINITY;
|
|
|
|
|
|
let mut max_y = f64::NEG_INFINITY;
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
for &(a, b) in &edges {
|
|
|
|
|
|
for [x, y] in [a, b] {
|
|
|
|
|
|
min_x = min_x.min(x);
|
|
|
|
|
|
max_x = max_x.max(x);
|
|
|
|
|
|
min_y = min_y.min(y);
|
|
|
|
|
|
max_y = max_y.max(y);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let scale = self.scale.max(1e-6) as f64;
|
|
|
|
|
|
let angle_offset = self.angle_offset as f64;
|
|
|
|
|
|
let mut segments: Vec<[[f64; 2]; 2]> = Vec::new();
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
|
|
|
|
|
|
// Hard cap to keep pathological patterns / huge boundaries bounded.
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
// `k` is i64: at UTM with a fine spacing the index legitimately reaches
|
|
|
|
|
|
// ~1e7 and an out-of-range `as i32` saturates silently, collapsing the
|
|
|
|
|
|
// range to a bogus run of lines from i32::MIN.
|
|
|
|
|
|
const MAX_LINES_PER_FAMILY: i64 = 4096;
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
const MAX_SEGMENTS_TOTAL: usize = 200_000;
|
|
|
|
|
|
|
|
|
|
|
|
let cos_off = angle_offset.cos();
|
|
|
|
|
|
let sin_off = angle_offset.sin();
|
|
|
|
|
|
for family in families {
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let angle = (family.angle_deg as f64).to_radians() + angle_offset;
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
let cos_a = angle.cos();
|
|
|
|
|
|
let sin_a = angle.sin();
|
|
|
|
|
|
// PAT local frame: dx = along-line phase, dy = perpendicular
|
|
|
|
|
|
// spacing. Lines step in world by k · (dx, dy)_local rotated
|
|
|
|
|
|
// into the family's frame.
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let (fdx, fdy) = (family.dx as f64, family.dy as f64);
|
|
|
|
|
|
let step_x = (fdx * cos_a - fdy * sin_a) * scale;
|
|
|
|
|
|
let step_y = (fdx * sin_a + fdy * cos_a) * scale;
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
let perp_x = -sin_a;
|
|
|
|
|
|
let perp_y = cos_a;
|
|
|
|
|
|
let step_perp = step_x * perp_x + step_y * perp_y;
|
|
|
|
|
|
if step_perp.abs() < 1e-6 {
|
|
|
|
|
|
continue; // degenerate spacing
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// k range: project AABB corners onto perp direction relative
|
|
|
|
|
|
// to the family's origin and divide by signed perp step. The
|
|
|
|
|
|
// pattern origin is rotated by `angle_offset` and scaled —
|
|
|
|
|
|
// same convention as the GPU shader, so PAT patterns whose
|
|
|
|
|
|
// `x0/y0` are non-zero (e.g. brick offsets) line up with the
|
|
|
|
|
|
// on-screen render.
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let (fx0, fy0) = (family.x0 as f64, family.y0 as f64);
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
let origin = [
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
(fx0 * cos_off - fy0 * sin_off) * scale,
|
|
|
|
|
|
(fx0 * sin_off + fy0 * cos_off) * scale,
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
];
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let mut p_min = f64::INFINITY;
|
|
|
|
|
|
let mut p_max = f64::NEG_INFINITY;
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
for &[cx, cy] in &[
|
|
|
|
|
|
[min_x, min_y],
|
|
|
|
|
|
[max_x, min_y],
|
|
|
|
|
|
[min_x, max_y],
|
|
|
|
|
|
[max_x, max_y],
|
|
|
|
|
|
] {
|
|
|
|
|
|
let p = (cx - origin[0]) * perp_x + (cy - origin[1]) * perp_y;
|
|
|
|
|
|
p_min = p_min.min(p);
|
|
|
|
|
|
p_max = p_max.max(p);
|
|
|
|
|
|
}
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let mut k_lo = (p_min / step_perp).floor() as i64 - 1;
|
|
|
|
|
|
let mut k_hi = (p_max / step_perp).ceil() as i64 + 1;
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
if k_lo > k_hi {
|
|
|
|
|
|
std::mem::swap(&mut k_lo, &mut k_hi);
|
|
|
|
|
|
}
|
fix(plot): correct hatch rendering in PDF / plot export
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
2026-07-06 12:24:10 +02:00
|
|
|
|
// Cap the line COUNT (span), not the absolute index. A hatch far
|
|
|
|
|
|
// from the pattern origin (0,0) — e.g. a fine-spaced fill at large
|
|
|
|
|
|
// drawing coordinates — has large-magnitude k at both ends but a
|
|
|
|
|
|
// small span. Clamping the absolute index to ±MAX_LINES_PER_FAMILY
|
|
|
|
|
|
// would invert the range (k_lo > k_hi) and emit nothing, silently
|
|
|
|
|
|
// dropping the whole fill.
|
|
|
|
|
|
if k_hi.saturating_sub(k_lo) > MAX_LINES_PER_FAMILY {
|
|
|
|
|
|
k_hi = k_lo.saturating_add(MAX_LINES_PER_FAMILY);
|
|
|
|
|
|
}
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let period: f64 = family.dashes.iter().map(|d| d.abs() as f64).sum::<f64>() * scale;
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
let has_dashes = !family.dashes.is_empty() && period > 1e-6;
|
|
|
|
|
|
|
|
|
|
|
|
for k in k_lo..=k_hi {
|
|
|
|
|
|
if segments.len() >= MAX_SEGMENTS_TOTAL {
|
|
|
|
|
|
return segments;
|
|
|
|
|
|
}
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let kf = k as f64;
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
let lx = origin[0] + kf * step_x;
|
|
|
|
|
|
let ly = origin[1] + kf * step_y;
|
|
|
|
|
|
|
|
|
|
|
|
// Intersect line P(t) = L + t·(cos_a, sin_a) against each
|
|
|
|
|
|
// boundary edge; collect t-values where the edge actually
|
|
|
|
|
|
// crosses (s ∈ [0,1]).
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let mut ts: Vec<f64> = Vec::with_capacity(8);
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
for &(a, b) in &edges {
|
|
|
|
|
|
let ex = b[0] - a[0];
|
|
|
|
|
|
let ey = b[1] - a[1];
|
|
|
|
|
|
let det = ex * sin_a - ey * cos_a; // = sin_a·ex − cos_a·ey
|
|
|
|
|
|
if det.abs() < 1e-9 {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
let rx = a[0] - lx;
|
|
|
|
|
|
let ry = a[1] - ly;
|
|
|
|
|
|
let t = (ex * ry - ey * rx) / det;
|
|
|
|
|
|
let s = (cos_a * ry - sin_a * rx) / det;
|
|
|
|
|
|
if s >= 0.0 && s <= 1.0 {
|
|
|
|
|
|
ts.push(t);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if ts.len() < 2 {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
ts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
|
|
|
|
|
// De-duplicate near-coincident hits (line clipping a vertex).
|
|
|
|
|
|
ts.dedup_by(|a, b| (*a - *b).abs() < 1e-5);
|
|
|
|
|
|
if ts.len() < 2 {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Even-odd: emit segments between consecutive pairs.
|
|
|
|
|
|
for pair in ts.chunks_exact(2) {
|
|
|
|
|
|
let t0 = pair[0];
|
|
|
|
|
|
let t1 = pair[1];
|
|
|
|
|
|
if t1 - t0 < 1e-6 {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if !has_dashes {
|
|
|
|
|
|
let p0 = [lx + t0 * cos_a, ly + t0 * sin_a];
|
|
|
|
|
|
let p1 = [lx + t1 * cos_a, ly + t1 * sin_a];
|
|
|
|
|
|
segments.push([p0, p1]);
|
|
|
|
|
|
} else {
|
2026-06-09 23:15:45 +03:00
|
|
|
|
// Walk the dash sequence along this clipped span with
|
|
|
|
|
|
// absolute phase (so the pattern aligns across spans,
|
|
|
|
|
|
// matching the GPU shader). Positive entries are
|
|
|
|
|
|
// dashes, negative are gaps, and a zero-length entry is
|
|
|
|
|
|
// a dot — rendered as a short mark so dot patterns
|
|
|
|
|
|
// (e.g. DOTS) are visible instead of drawing nothing.
|
|
|
|
|
|
let n = family.dashes.len();
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let dot_len: f64 = (period * 0.06).max(1e-3);
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
let phase = t0.rem_euclid(period);
|
2026-06-09 23:15:45 +03:00
|
|
|
|
// Start at the period boundary at or before t0; the
|
|
|
|
|
|
// span clip below drops anything before t0.
|
|
|
|
|
|
let mut seg_t = t0 - phase;
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
let mut idx = 0usize;
|
2026-06-09 23:15:45 +03:00
|
|
|
|
let max_iters = (((t1 - t0) / period).ceil() as usize + 2) * n + 8;
|
|
|
|
|
|
let mut iters = 0usize;
|
|
|
|
|
|
while seg_t < t1 && iters < max_iters {
|
fix(export): keep plot coordinates in f64 until they cancel
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:25:28 +03:00
|
|
|
|
let d = family.dashes[idx] as f64;
|
2026-06-09 23:15:45 +03:00
|
|
|
|
let dl = d.abs() * scale;
|
|
|
|
|
|
if d > 0.0 {
|
|
|
|
|
|
let a = seg_t.max(t0);
|
|
|
|
|
|
let b = (seg_t + dl).min(t1);
|
|
|
|
|
|
if b > a {
|
|
|
|
|
|
segments.push([
|
|
|
|
|
|
[lx + a * cos_a, ly + a * sin_a],
|
|
|
|
|
|
[lx + b * cos_a, ly + b * sin_a],
|
|
|
|
|
|
]);
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
}
|
2026-06-09 23:15:45 +03:00
|
|
|
|
} else if d == 0.0 && seg_t >= t0 - 1e-6 && seg_t <= t1 + 1e-6 {
|
|
|
|
|
|
// Short centered mark for the dot.
|
|
|
|
|
|
let a = (seg_t - dot_len * 0.5).max(t0);
|
|
|
|
|
|
let b = (seg_t + dot_len * 0.5).min(t1);
|
|
|
|
|
|
if b > a {
|
|
|
|
|
|
segments.push([
|
|
|
|
|
|
[lx + a * cos_a, ly + a * sin_a],
|
|
|
|
|
|
[lx + b * cos_a, ly + b * sin_a],
|
|
|
|
|
|
]);
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-09 23:15:45 +03:00
|
|
|
|
seg_t += dl;
|
|
|
|
|
|
idx = (idx + 1) % n;
|
|
|
|
|
|
iters += 1;
|
feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:52:20 +03:00
|
|
|
|
if segments.len() >= MAX_SEGMENTS_TOTAL {
|
|
|
|
|
|
return segments;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
segments
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|