fix(precision): keep typed hatch boundaries exact in f64 (#311)

The HATCH/GRADIENT/BOUNDARY commands built the persisted hatch through
the render-side boundary (f32 offsets from world_origin), and with a
zero world_origin that meant absolute f32 - so a typed boundary vertex
of 2000.8 was stored as 2000.8020, and at UTM scale the fill landed far
from its boundary.

HatchModel.boundary stays f32 (the GPU render contract). Added
HatchModel.boundary_wcs: Option<Arc<Vec<[f64;2]>>>, an exact absolute-WCS
boundary set only by the draw commands. Scene::add_hatch persists from
boundary_wcs when present (exact f64 -> DxfHatch), else falls back to the
f32 + world_origin reconstruction used by DXF-rebuilt hatches. manual_pts
is now Vec<DVec3>; make_hatch takes the f64 boundary, stores the exact
copy and derives the RTE render rep. The committed render model is
rebuilt from the DXF entity, so nothing renders the command model's f32
boundary. This also fixes command hatches mis-placing at large
coordinates (world_origin was [0,0]).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-11 21:55:10 +03:00
commit 1f89b78a7e
6 changed files with 106 additions and 43 deletions

View file

@ -580,6 +580,7 @@ impl Scene {
for boundary in fills {
models.push(HatchModel {
boundary: Arc::new(boundary),
boundary_wcs: None,
pattern: model::hatch_model::HatchPattern::Solid,
name: "SOLID".into(),
color,
@ -854,6 +855,7 @@ impl Scene {
};
models.push(HatchModel {
boundary: Arc::new(ring),
boundary_wcs: None,
pattern: model::hatch_model::HatchPattern::Solid,
name: "SOLID".into(),
color,
@ -909,6 +911,7 @@ impl Scene {
}
models.push(HatchModel {
boundary: Arc::new(boundary),
boundary_wcs: None,
pattern: model::hatch_model::HatchPattern::Solid,
name: "WIPEOUT_FILL".into(),
color: fill_color,
@ -1430,6 +1433,7 @@ impl Scene {
Some(HatchModel {
boundary: std::sync::Arc::new(boundary_f32),
boundary_wcs: None,
pattern,
name,
color,
@ -1624,6 +1628,7 @@ impl Scene {
];
HatchModel {
boundary: std::sync::Arc::new(boundary),
boundary_wcs: None,
pattern: model::hatch_model::HatchPattern::Solid,
name: "SOLID".into(),
color,
@ -1641,18 +1646,26 @@ impl Scene {
model.pattern,
crate::scene::model::hatch_model::HatchPattern::Solid
);
// The boundary points arrive in local render space (world_offset
// already subtracted). The stored DXF entity must hold WCS, so add the
// offset back — otherwise the boundary wire, re-projected through the
// normal entity path, lands `world_offset` away from the fill.
let wx = model.world_origin[0];
let wy = model.world_origin[1];
let verts: Vec<Vector2> = model
.boundary
.iter()
.filter(|v| v[0].is_finite() && v[1].is_finite())
.map(|&[x, y]| Vector2::new(x as f64 + wx, y as f64 + wy))
.collect();
// Prefer the command-supplied exact f64 boundary so a typed vertex is
// persisted without f32 quantization (issue #311). Falling back to the
// render-side `boundary`, the points arrive in local render space
// (world_offset already subtracted), so add `world_origin` back —
// otherwise the boundary wire lands `world_offset` away from the fill.
let verts: Vec<Vector2> = if let Some(wcs) = &model.boundary_wcs {
wcs.iter()
.filter(|v| v[0].is_finite() && v[1].is_finite())
.map(|&[x, y]| Vector2::new(x, y))
.collect()
} else {
let wx = model.world_origin[0];
let wy = model.world_origin[1];
model
.boundary
.iter()
.filter(|v| v[0].is_finite() && v[1].is_finite())
.map(|&[x, y]| Vector2::new(x as f64 + wx, y as f64 + wy))
.collect()
};
let edge = PolylineEdge::new(verts, true);
let mut path = BoundaryPath::external();
path.add_edge(BoundaryEdge::Polyline(edge));