fix(pick): catch a wide polyline on its band, not just its centreline

A wide polyline draws a solid band, but hit-testing only ever saw the
centreline 8 px out, so a donut could only be selected by clicking the
phantom circle inside the disc it paints. `self.hatches` — the map the
hatch pick channel walks — is filled from HATCH and SOLID alone, so the
band `wide_fills` produces was drawn (via `synced_hatch_models`) and never
picked.

The band rides in on `pick_tris`, the same channel a thickness wall uses:
`wide_fills` already computes it, so it costs one tessellation and inherits
the block transform. It ear clips rather than fans — an arc band is an
annular sector, concave on its inner edge — reusing `triangulate_planar`.

Wiring `pick_tris` through `Lines` alone was not enough: a plain wide
polyline leaves `to_truck` as `Contour`, and a PLINEGEN=0 one as
`SegmentedLines`. Both now carry it.

This needs the wire's cullable box to cover the band, and it did not.
`bounding_box()` boxes a polyline's stored vertices, knowing nothing of the
width painted around them: a donut's two vertices sit on one horizontal
line, so the box had zero height and rejected every click on the disc
before any triangle was tested. Every site that stamps the entity box onto
a wire now goes through `set_wire_aabb`, which widens it over `pick_tris`
(a no-op for the entities that have none, which is nearly all), and the
block cache chains `pick_tris` into its own box for the same reason.

That box was already too small for the centreline itself — an arc leaves
the vertex box — so a donut was unselectable at its top and bottom at
enough zoom. Widening fixes that too.

Verified end-to-end: tessellate a width-0.5 donut on a 0.05 centreline
radius, then `click_hit` five points 20 px clear of the centreline — all
land, and a click outside the disc still misses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-17 15:22:56 +03:00
commit 4f1e710b35
9 changed files with 111 additions and 34 deletions

View file

@ -96,15 +96,22 @@ pub struct WireModel {
/// tessellation from CAD f64 fills it so fills stay precise at UTM scale.
pub fill_tris_low: Vec<[f32; 3]>,
/// Pre-triangulated pick-only geometry: flat vertex list, 3 per triangle.
/// Hit-testing treats these as solid, the renderer never uploads them.
/// Hit-testing treats these as solid; this wire's own draw never uses them.
///
/// Non-empty only for entities extruded by a DXF thickness (code 39): the
/// swept wall between each base segment and its extruded copy. The wall is
/// drawn as its four wireframe edges (which live in [`points`]), so it has
/// no fill geometry to pick — without this the interior is a hole the
/// cursor falls through. Separate from [`fill_tris`] because that channel
/// reaches the GPU, and a wall that renders shaded would change how every
/// thickness drawing looks.
/// Carries the interior of things whose surface the cursor would otherwise
/// fall through, because [`points`] only bounds them:
///
/// - An entity extruded by a DXF thickness (code 39) — the swept wall
/// between each base segment and its extruded copy. Drawn as its four
/// wireframe edges, with nothing in between to pick.
/// - A wide polyline (codes 43 / 40 / 41) — the solid band. It *is* drawn,
/// but by the hatch pipeline off `wide_fills`, so the wire that carries
/// its centreline has no fill of its own to hit-test.
///
/// Separate from [`fill_tris`] because that channel reaches the GPU: a
/// thickness wall put there would render shaded and change how every such
/// drawing looks, and a polyline band would be drawn a second time on top
/// of the hatch that already draws it.
pub pick_tris: Vec<[f32; 3]>,
/// Low residual paired with [`pick_tris`] (double-single). Empty = all-zero.
pub pick_tris_low: Vec<[f32; 3]>,