@@ -3,10 +3,17 @@ use crate::map::{
33 geometry_collection:: { DEFAULT_STYLE , Geometry , Metadata } ,
44} ;
55use egui:: {
6- Color32 , Painter , Shape , Stroke ,
6+ Color32 , ColorImage , Painter , Pos2 , Rect , Shape , Stroke ,
77 epaint:: { CircleShape , PathShape , PathStroke } ,
88} ;
99use std:: collections:: HashMap ;
10+ use tiny_skia:: {
11+ FillRule , LineCap , LineJoin , Paint , PathBuilder , Pixmap , Stroke as SkiaStroke ,
12+ Transform as SkiaTransform ,
13+ } ;
14+
15+ /// Bold highlight stroke width, in screen pixels.
16+ const HIGHLIGHT_STROKE_WIDTH : f32 = 6.0 ;
1017
1118/// Manages geometry highlighting with unique IDs
1219pub struct GeometryHighlighter {
@@ -118,18 +125,15 @@ pub fn draw_highlighted_geometry(
118125 Geometry :: LineString ( coords, metadata) => {
119126 draw_highlighted_linestring ( coords, metadata, painter, transform) ;
120127 }
121- Geometry :: Polygon ( coords, metadata) => {
122- draw_highlighted_polygon ( coords, metadata, painter, transform) ;
128+ Geometry :: Polygon ( _, _) | Geometry :: Heatmap ( _, _) => {
129+ // Polygons render via tiny-skia (`rasterize_highlighted_polygons`);
130+ // heatmaps are not individually highlightable.
123131 }
124132 Geometry :: GeometryCollection ( geometries, _) => {
125- // Draw all geometries in the collection as highlighted
126133 for nested_geometry in geometries {
127134 draw_highlighted_geometry ( nested_geometry, painter, transform, false ) ;
128135 }
129136 }
130- Geometry :: Heatmap ( _, _) => {
131- // Heatmaps are not individually highlightable.
132- }
133137 }
134138}
135139
@@ -175,25 +179,132 @@ fn draw_highlighted_linestring(
175179 painter. add ( shape) ;
176180}
177181
178- /// Draw a highlighted polygon
179- fn draw_highlighted_polygon (
180- coords : & [ PixelCoordinate ] ,
181- metadata : & Metadata ,
182- painter : & Painter ,
182+ /// Rasterize all polygons inside `geometry` (recursively) into a pixmap
183+ /// clipped to `viewport`, drawing each polygon's fill and bold solid stroke
184+ /// via tiny-skia. egui can't draw these correctly: its fan-triangulator
185+ /// produces fold-overs on concave/bridged polygons, and its `PathStroke` has
186+ /// no `LineJoin` control so sharp angles produce miter spikes. tiny-skia
187+ /// uses winding-rule fill and `LineJoin::Round`, so neither happens.
188+ pub fn rasterize_highlighted_polygons (
189+ geometry : & Geometry < PixelCoordinate > ,
183190 transform : & Transform ,
184- ) {
185- let style = metadata. style . as_ref ( ) . unwrap_or ( & DEFAULT_STYLE ) ;
186- let base_color = style. color ( ) ;
191+ viewport : Rect ,
192+ ) -> Option < ( ColorImage , Rect ) > {
193+ let mut bbox: Option < Rect > = None ;
194+ collect_polygon_bbox ( geometry, transform, & mut bbox) ;
195+ // Pad for the stroke half-width plus a feathering margin.
196+ let pad = HIGHLIGHT_STROKE_WIDTH * 0.5 + 1.0 ;
197+ let bbox = bbox?. expand ( pad) ;
198+ let clipped = bbox. intersect ( viewport) ;
199+ if clipped. width ( ) <= 0.0 || clipped. height ( ) <= 0.0 {
200+ return None ;
201+ }
202+ #[ allow( clippy:: cast_possible_truncation, clippy:: cast_sign_loss) ]
203+ let w = clipped. width ( ) . ceil ( ) as u32 ;
204+ #[ allow( clippy:: cast_possible_truncation, clippy:: cast_sign_loss) ]
205+ let h = clipped. height ( ) . ceil ( ) as u32 ;
206+ if w == 0 || h == 0 {
207+ return None ;
208+ }
209+ let mut pixmap = Pixmap :: new ( w, h) ?;
210+ fill_polygons_into_pixmap ( geometry, transform, & mut pixmap, clipped. min ) ;
187211
188- // Draw highlighted polygon with solid colors - fill matches stroke color
189- let highlight_stroke = base_color; // Solid stroke in original color
190- let highlight_fill = base_color; // Fill always matches stroke color
212+ // tiny-skia stores premultiplied RGBA; un-premultiply for ColorImage.
213+ let mut straight = Vec :: with_capacity ( pixmap. data ( ) . len ( ) ) ;
214+ for p in pixmap. data ( ) . chunks_exact ( 4 ) {
215+ let a = p[ 3 ] ;
216+ if a == 0 {
217+ straight. extend_from_slice ( & [ 0 , 0 , 0 , 0 ] ) ;
218+ } else {
219+ let inv = 255.0_f32 / f32:: from ( a) ;
220+ #[ allow( clippy:: cast_possible_truncation, clippy:: cast_sign_loss) ]
221+ {
222+ straight. push ( ( f32:: from ( p[ 0 ] ) * inv) . min ( 255.0 ) as u8 ) ;
223+ straight. push ( ( f32:: from ( p[ 1 ] ) * inv) . min ( 255.0 ) as u8 ) ;
224+ straight. push ( ( f32:: from ( p[ 2 ] ) * inv) . min ( 255.0 ) as u8 ) ;
225+ }
226+ straight. push ( a) ;
227+ }
228+ }
229+ Some ( (
230+ ColorImage :: from_rgba_unmultiplied ( [ w as usize , h as usize ] , & straight) ,
231+ clipped,
232+ ) )
233+ }
191234
192- let shape = Shape :: Path ( PathShape {
193- points : coords. iter ( ) . map ( |c| transform. apply ( * c) . into ( ) ) . collect ( ) ,
194- closed : true ,
195- fill : highlight_fill,
196- stroke : PathStroke :: new ( 6.0 , highlight_stroke) ,
197- } ) ;
198- painter. add ( shape) ;
235+ fn collect_polygon_bbox (
236+ geometry : & Geometry < PixelCoordinate > ,
237+ transform : & Transform ,
238+ acc : & mut Option < Rect > ,
239+ ) {
240+ match geometry {
241+ Geometry :: Polygon ( coords, _) => {
242+ for c in coords {
243+ let p: Pos2 = transform. apply ( * c) . into ( ) ;
244+ let r = Rect :: from_min_max ( p, p) ;
245+ * acc = Some ( acc. map_or ( r, |a| a. union ( r) ) ) ;
246+ }
247+ }
248+ Geometry :: GeometryCollection ( geoms, _) => {
249+ for g in geoms {
250+ collect_polygon_bbox ( g, transform, acc) ;
251+ }
252+ }
253+ _ => { }
254+ }
255+ }
256+
257+ fn fill_polygons_into_pixmap (
258+ geometry : & Geometry < PixelCoordinate > ,
259+ transform : & Transform ,
260+ pixmap : & mut Pixmap ,
261+ origin : Pos2 ,
262+ ) {
263+ match geometry {
264+ Geometry :: Polygon ( coords, metadata) => {
265+ let style = metadata. style . as_ref ( ) . unwrap_or ( & DEFAULT_STYLE ) ;
266+ let color = style. color ( ) ;
267+ let mut iter = coords. iter ( ) ;
268+ let Some ( first) = iter. next ( ) else { return } ;
269+ let mut pb = PathBuilder :: new ( ) ;
270+ let p: Pos2 = transform. apply ( * first) . into ( ) ;
271+ pb. move_to ( p. x - origin. x , p. y - origin. y ) ;
272+ for c in iter {
273+ let p: Pos2 = transform. apply ( * c) . into ( ) ;
274+ pb. line_to ( p. x - origin. x , p. y - origin. y ) ;
275+ }
276+ pb. close ( ) ;
277+ let Some ( path) = pb. finish ( ) else { return } ;
278+ let mut paint = Paint :: default ( ) ;
279+ paint. set_color ( tiny_skia:: Color :: from_rgba8 (
280+ color. r ( ) ,
281+ color. g ( ) ,
282+ color. b ( ) ,
283+ color. a ( ) ,
284+ ) ) ;
285+ paint. anti_alias = true ;
286+ pixmap. fill_path (
287+ & path,
288+ & paint,
289+ FillRule :: Winding ,
290+ SkiaTransform :: identity ( ) ,
291+ None ,
292+ ) ;
293+ // Bold solid stroke with round joins to suppress miter spikes on
294+ // bridged-multipolygon path reversals.
295+ let stroke = SkiaStroke {
296+ width : HIGHLIGHT_STROKE_WIDTH ,
297+ line_cap : LineCap :: Round ,
298+ line_join : LineJoin :: Round ,
299+ ..SkiaStroke :: default ( )
300+ } ;
301+ pixmap. stroke_path ( & path, & paint, & stroke, SkiaTransform :: identity ( ) , None ) ;
302+ }
303+ Geometry :: GeometryCollection ( geoms, _) => {
304+ for g in geoms {
305+ fill_polygons_into_pixmap ( g, transform, pixmap, origin) ;
306+ }
307+ }
308+ _ => { }
309+ }
199310}
0 commit comments