fix(wipeout): use real clip vertices, correct image-pixel→WCS mapping

acadrust's DWG reader read the wipeout/raster-image clip boundary
points only to advance the bit cursor and then discarded them, so every
wipeout fell back to `Wipeout::new()`'s default placeholder rectangle
(`-0.5..0.5`). For files that store the real polygon mask in clip
vertices (anteen.dwg's `DIMBLOCK` style: u_vector ≈ 643k, size = (1, 1),
clip polygon ~0.0016×0.0033 in pixel space), the wipeout rendered at the
full image bbox — hundreds of thousands of units wide — instead of the
intended ~1000×2000 mask.

Pinned to the new `fix/wipeout-clip-vertices` acadrust branch (carries
the upstream parser fix). On the H7CAD side, the polygon mapping now
follows the DXF convention:

  x_off = (clip.x + size.x/2) × u_vec
  y_off = (size.y/2 − clip.y) × v_vec      (image-Y points down)

…in `wipeout_boundary_2d` (fill), `Wipeout::to_truck` (border line),
and `Wipeout::grips` (vertex handles) so all three stay in sync. The
fill path also explicitly closes the polygon loop — without it the GPU
`in_polygon` ray-cast skipped the v(n-1)→v(0) edge and the solid mask
bled far past the boundary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan 2026-05-22 14:14:22 +03:00
commit 8e6491d4a8
4 changed files with 49 additions and 25 deletions

2
Cargo.lock generated
View file

@ -42,7 +42,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]]
name = "acadrust"
version = "0.3.4"
source = "git+https://github.com/HakanSeven12/acadrust?branch=fix%2Fhatch-boundary-handle-cap#62716f51e05aba084533f65e0eb3cfd3c7cc1dfa"
source = "git+https://github.com/HakanSeven12/acadrust?branch=fix%2Fwipeout-clip-vertices#15e434f733a4c970d5d680de89acc2cb381accbe"
dependencies = [
"ahash",
"anyhow",

View file

@ -24,7 +24,8 @@ ureq = { version = "2", default-features = false, features = ["tls"] }
windows-sys = { version = "0.59", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
[patch.crates-io]
# TEMP: tracks https://github.com/hakanaktt/acadrust/pull/27 (hatch
# boundary-handle cap). When that PR lands on hakanaktt/main, revert to
# `git = "https://github.com/hakanaktt/acadrust", branch = "main"`.
acadrust = { git = "https://github.com/HakanSeven12/acadrust", branch = "fix/hatch-boundary-handle-cap" }
# TEMP: tracks fix/wipeout-clip-vertices on HakanSeven12/acadrust, which
# stacks the wipeout/raster-image clip-vertex fix on top of the earlier
# hatch boundary-handle cap. Revert to `branch = "main"` once both
# upstream PRs land.
acadrust = { git = "https://github.com/HakanSeven12/acadrust", branch = "fix/wipeout-clip-vertices" }

View file

@ -277,8 +277,12 @@ impl TruckConvertible for Wipeout {
self.clip_type,
acadrust::entities::WipeoutClipType::Polygonal
) {
// Convert pixel-space boundary vertices to world space:
// world = insertion_point + u_vector * v.x * size.x + v_vector * v.y * size.y
// Clip vertices are stored in image-pixel space, centred on the
// image (range ±size/2). The image's bottom-left corner sits at
// `insertion_point`, the image-Y axis points DOWN (per DXF
// "v_vector points down the image"), so map:
// x_off = (clip.x + size.x/2) × u_vector
// y_off = (size.y/2 clip.y) × v_vector ← y flipped
let ox = self.insertion_point.x;
let oy = self.insertion_point.y;
let oz = self.insertion_point.z;
@ -286,12 +290,11 @@ impl TruckConvertible for Wipeout {
.clip_boundary_vertices
.iter()
.map(|v| {
let wx = self.u_vector.x * v.x * self.size.x
+ self.v_vector.x * v.y * self.size.y;
let wy = self.u_vector.y * v.x * self.size.x
+ self.v_vector.y * v.y * self.size.y;
let wz = self.u_vector.z * v.x * self.size.x
+ self.v_vector.z * v.y * self.size.y;
let cx = v.x + self.size.x * 0.5;
let cy = self.size.y * 0.5 - v.y;
let wx = self.u_vector.x * cx + self.v_vector.x * cy;
let wy = self.u_vector.y * cx + self.v_vector.y * cy;
let wz = self.u_vector.z * cx + self.v_vector.z * cy;
[ox + wx, oy + wy, oz + wz]
})
.collect();
@ -329,16 +332,17 @@ impl Grippable for Wipeout {
let ox = self.insertion_point.x as f32;
let oy = self.insertion_point.y as f32;
let oz = self.insertion_point.z as f32;
// Same image-pixel-space → WCS mapping as `to_truck` so grips
// sit exactly on the rendered polygon vertices.
self.clip_boundary_vertices
.iter()
.enumerate()
.map(|(i, v)| {
let wx = (self.u_vector.x * v.x * self.size.x
+ self.v_vector.x * v.y * self.size.y) as f32;
let wy = (self.u_vector.y * v.x * self.size.x
+ self.v_vector.y * v.y * self.size.y) as f32;
let wz = (self.u_vector.z * v.x * self.size.x
+ self.v_vector.z * v.y * self.size.y) as f32;
let cx = v.x + self.size.x * 0.5;
let cy = self.size.y * 0.5 - v.y;
let wx = (self.u_vector.x * cx + self.v_vector.x * cy) as f32;
let wy = (self.u_vector.y * cx + self.v_vector.y * cy) as f32;
let wz = (self.u_vector.z * cx + self.v_vector.z * cy) as f32;
if i == 0 {
square_grip(i, Vec3::new(ox + wx, oy + wy, oz + wz))
} else {

View file

@ -2744,16 +2744,35 @@ impl Scene {
if is_polygon {
let ox = (wo.insertion_point.x - wox) as f32;
let oy = (wo.insertion_point.y - woy) as f32;
wo.clip_boundary_vertices
// DXF clip vertices live in image-pixel space, centred on the
// image (range size/2 … +size/2). Image-bottom-left → insertion,
// image-y-axis points DOWN (per the DXF "v_vector points down the
// image" convention), so map:
// x_off = (clip.x + size.x/2) × u_vec
// y_off = (size.y/2 clip.y) × v_vec ← y flipped
let cx_of = |v: &acadrust::types::Vector2| v.x + wo.size.x * 0.5;
let cy_of = |v: &acadrust::types::Vector2| wo.size.y * 0.5 - v.y;
let mut poly: Vec<[f32; 2]> = wo
.clip_boundary_vertices
.iter()
.map(|v| {
let wx =
(wo.u_vector.x * v.x * wo.size.x + wo.v_vector.x * v.y * wo.size.y) as f32;
let wy =
(wo.u_vector.y * v.x * wo.size.x + wo.v_vector.y * v.y * wo.size.y) as f32;
let cx = cx_of(v);
let cy = cy_of(v);
let wx = (wo.u_vector.x * cx + wo.v_vector.x * cy) as f32;
let wy = (wo.u_vector.y * cx + wo.v_vector.y * cy) as f32;
[ox + wx, oy + wy]
})
.collect()
.collect();
// Close the loop: the GPU `in_polygon` ray-cast walks
// sequential pairs and doesn't wrap, so without an explicit
// closing vertex the last edge (vN-1 → v0) is never tested and
// the fill bleeds far past the boundary.
if let Some(&first) = poly.first() {
if poly.last() != Some(&first) {
poly.push(first);
}
}
poly
} else {
// Rectangular boundary from 4 corners.
let ox = (wo.insertion_point.x - wox) as f32;