From 3c58044ca9cacbebb96157af7b5385f6fde37494 Mon Sep 17 00:00:00 2001 From: Maxim Date: Thu, 2 Apr 2026 00:45:42 +0900 Subject: [PATCH] feat: Support rendering lines with dasharrays Dasharray property is added ot the VT sytle and maplibre layers using it are correctly converted. --- galileo-maplibre/src/layer/vector_tile.rs | 10 +- galileo-maplibre/src/style/layer/line.rs | 2 +- .../src/layer/feature_layer/symbol/contour.rs | 1 + .../src/layer/feature_layer/symbol/polygon.rs | 1 + .../src/layer/vector_tile_layer/builder.rs | 1 + galileo/src/layer/vector_tile_layer/style.rs | 8 +- .../tile_provider/vt_processor.rs | 6 +- galileo/src/render/mod.rs | 25 ++- galileo/src/render/point_paint.rs | 34 +-- galileo/src/render/render_bundle/mod.rs | 4 +- galileo/src/render/render_bundle/world_set.rs | 195 +++++++++++++++--- 11 files changed, 229 insertions(+), 58 deletions(-) diff --git a/galileo-maplibre/src/layer/vector_tile.rs b/galileo-maplibre/src/layer/vector_tile.rs index e580aa4c..60171927 100644 --- a/galileo-maplibre/src/layer/vector_tile.rs +++ b/galileo-maplibre/src/layer/vector_tile.rs @@ -263,14 +263,6 @@ fn fill_rule(fill: &FillLayer, tile_schema: &TileSchema) -> Option { /// Converts a [`LineLayer`] to a [`StyleRule`], or logs and returns `None` if unsupported. fn line_rule(line: &LineLayer, tile_schema: &TileSchema) -> Option { - if line.paint.line_dasharray.is_some() { - log::debug!( - "{UNSUPPORTED} Line dasharray is not supported yet; skipping layer {}", - line.id - ); - return None; - } - log_unsupported_field!(line.paint.line_blur); log_unsupported_field!(line.paint.line_gap_width); log_unsupported_field!(line.paint.line_gradient); @@ -306,12 +298,14 @@ fn line_rule(line: &LineLayer, tile_schema: &TileSchema) -> Option { .minzoom .and_then(|lod| tile_schema.lod_resolution(lod.round() as u32)); let filter = line.filter.as_ref().and_then(|v| v.to_galileo_expr()); + let dasharray = line.paint.line_dasharray.clone(); Some(StyleRule { layer_name: Some(source_layer), symbol: VectorTileSymbol::Line(VectorTileLineSymbol { width, stroke_color: color, + dasharray, }), min_resolution, max_resolution, diff --git a/galileo-maplibre/src/style/layer/line.rs b/galileo-maplibre/src/style/layer/line.rs index 43829f74..aa4fdc6d 100644 --- a/galileo-maplibre/src/style/layer/line.rs +++ b/galileo-maplibre/src/style/layer/line.rs @@ -48,7 +48,7 @@ pub struct LinePaint { /// Dash pattern for the line. Supports expressions. #[serde(rename = "line-dasharray", skip_serializing_if = "Option::is_none")] - pub line_dasharray: Option, + pub line_dasharray: Option>, /// Gap width for a casing effect. Supports expressions. #[serde(rename = "line-gap-width", skip_serializing_if = "Option::is_none")] diff --git a/galileo/src/layer/feature_layer/symbol/contour.rs b/galileo/src/layer/feature_layer/symbol/contour.rs index 8da7ecb6..c7ca02a5 100644 --- a/galileo/src/layer/feature_layer/symbol/contour.rs +++ b/galileo/src/layer/feature_layer/symbol/contour.rs @@ -36,6 +36,7 @@ impl Symbol for SimpleContourSymbol { width: self.width, offset: 0.0, line_cap: LineCap::Butt, + dasharray: None, }; match geometry { diff --git a/galileo/src/layer/feature_layer/symbol/polygon.rs b/galileo/src/layer/feature_layer/symbol/polygon.rs index 22c6ab50..027823c4 100644 --- a/galileo/src/layer/feature_layer/symbol/polygon.rs +++ b/galileo/src/layer/feature_layer/symbol/polygon.rs @@ -78,6 +78,7 @@ impl SimplePolygonSymbol { width: self.stroke_width, offset: self.stroke_offset, line_cap: LineCap::Butt, + dasharray: None, }; for contour in polygon.iter_contours() { diff --git a/galileo/src/layer/vector_tile_layer/builder.rs b/galileo/src/layer/vector_tile_layer/builder.rs index 4593e3d6..594aae25 100644 --- a/galileo/src/layer/vector_tile_layer/builder.rs +++ b/galileo/src/layer/vector_tile_layer/builder.rs @@ -498,6 +498,7 @@ impl VectorTileLayerBuilder { symbol: VectorTileSymbol::Line(VectorTileLineSymbol { width: 1.0.into(), stroke_color: Color::BLACK.into(), + dasharray: None, }), }, StyleRule { diff --git a/galileo/src/layer/vector_tile_layer/style.rs b/galileo/src/layer/vector_tile_layer/style.rs index 76f6caad..b36e478f 100644 --- a/galileo/src/layer/vector_tile_layer/style.rs +++ b/galileo/src/layer/vector_tile_layer/style.rs @@ -172,15 +172,21 @@ pub struct VectorTileLineSymbol { pub width: NumExpr, /// Color of the line in pixels. pub stroke_color: ColorExpr, + /// Parameters of dash array for the line. + /// + /// Sets length of "dash - gap - dash - ..." of widths of the line. If the specification contains not even number of + /// values, the whole pattern is repeated twice when applied. + pub dasharray: Option>, } impl VectorTileLineSymbol { - pub(crate) fn to_paint(&self, feature: &MvtFeature, view: ExprView) -> Option { + pub(crate) fn to_paint(&self, feature: &MvtFeature, view: ExprView) -> Option> { Some(LinePaint { color: self.stroke_color.eval(feature, view)?, width: self.width.eval(feature, view)?, offset: 0.0, line_cap: LineCap::Butt, + dasharray: self.dasharray.as_deref(), }) } } diff --git a/galileo/src/layer/vector_tile_layer/tile_provider/vt_processor.rs b/galileo/src/layer/vector_tile_layer/tile_provider/vt_processor.rs index ecff7cf5..6b3500a9 100644 --- a/galileo/src/layer/vector_tile_layer/tile_provider/vt_processor.rs +++ b/galileo/src/layer/vector_tile_layer/tile_provider/vt_processor.rs @@ -196,11 +196,11 @@ impl VtProcessor { )) } - fn get_line_symbol( - rule: &StyleRule, + fn get_line_symbol<'a>( + rule: &'a StyleRule, feature: &MvtFeature, view: ExprView, - ) -> Option { + ) -> Option> { rule.symbol.line().and_then(|s| s.to_paint(feature, view)) } diff --git a/galileo/src/render/mod.rs b/galileo/src/render/mod.rs index eaa90eb5..047c8cc4 100644 --- a/galileo/src/render/mod.rs +++ b/galileo/src/render/mod.rs @@ -12,6 +12,7 @@ use render_bundle::RenderBundle; use serde::{Deserialize, Serialize}; use crate::Color; +use crate::render::render_bundle::world_set::{DashArray, LineParameters}; #[cfg(feature = "wgpu")] mod wgpu; @@ -101,8 +102,8 @@ pub struct PolygonPaint { } /// Parameter to draw a line primitive with. -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct LinePaint { +#[derive(Debug, Clone)] +pub struct LinePaint<'a> { /// Color of the line. pub color: Color, /// Width of the line in pixels. @@ -112,6 +113,26 @@ pub struct LinePaint { pub offset: f64, /// Type of the cap of the line. pub line_cap: LineCap, + /// Parameters of dash array for the line. + /// + /// Sets length of "dash - gap - dash - ..." of widths of the line. If the specification contains not even number of + /// values, the whole pattern is repeated twice when applied. + pub dasharray: Option<&'a [f32]>, +} + +impl LinePaint<'_> { + pub(crate) fn line_parameters(&self) -> LineParameters { + LineParameters { + color: self.color, + width: self.width as f32, + offset: self.offset as f32, + cap: self.line_cap, + } + } + + pub(crate) fn dasharray(&self) -> Option> { + self.dasharray.as_ref().map(|v| DashArray(v)) + } } /// Cap (end point) style of the line. diff --git a/galileo/src/render/point_paint.rs b/galileo/src/render/point_paint.rs index 52666d2b..90ce527b 100644 --- a/galileo/src/render/point_paint.rs +++ b/galileo/src/render/point_paint.rs @@ -13,7 +13,7 @@ use crate::render::text::TextStyle; use crate::render::{LineCap, LinePaint}; /// Specifies the way a point should be drawn to the map. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct PointPaint<'a> { pub(crate) shape: PointShape<'a>, pub(crate) offset: Vector2, @@ -36,13 +36,15 @@ impl<'a> PointPaint<'a> { pub fn sector(color: Color, diameter: f32, start_angle: f32, end_angle: f32) -> Self { Self { offset: Vector2::default(), - shape: PointShape::Sector(SectorParameters { - fill: color.into(), - radius: diameter / 2.0, - start_angle, - end_angle, + shape: PointShape::Sector { + parameters: SectorParameters { + fill: color.into(), + radius: diameter / 2.0, + start_angle, + end_angle, + }, outline: None, - }), + }, } } @@ -112,6 +114,7 @@ impl<'a> PointPaint<'a> { width: width as f64, offset: 0.0, line_cap: LineCap::Round, + dasharray: None, }) } _ => {} @@ -133,8 +136,7 @@ impl<'a> PointPaint<'a> { } } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type")] +#[derive(Debug, Clone)] pub(crate) enum PointShape<'a> { Dot { color: Color, @@ -142,18 +144,21 @@ pub(crate) enum PointShape<'a> { Circle { fill: CircleFill, radius: f32, - outline: Option, + outline: Option>, + }, + Sector { + parameters: SectorParameters, + outline: Option>, }, - Sector(SectorParameters), Square { fill: Color, size: f32, - outline: Option, + outline: Option>, }, FreeShape { fill: Color, scale: f32, - outline: Option, + outline: Option>, shape: Cow<'a, ClosedContour>>, }, Label { @@ -176,13 +181,12 @@ pub enum MarkerStyle { }, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub(crate) struct SectorParameters { pub fill: CircleFill, pub radius: f32, pub start_angle: f32, pub end_angle: f32, - pub outline: Option, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] diff --git a/galileo/src/render/render_bundle/mod.rs b/galileo/src/render/render_bundle/mod.rs index ead295df..07665969 100644 --- a/galileo/src/render/render_bundle/mod.rs +++ b/galileo/src/render/render_bundle/mod.rs @@ -68,7 +68,7 @@ impl RenderBundle { pub fn add_line(&mut self, line: &C, paint: &LinePaint, min_resolution: f64) where N: AsPrimitive, - P: CartesianPoint3d, + P: CartesianPoint3d + Copy, C: Contour, { self.world_set.add_line(line, paint, min_resolution); @@ -82,7 +82,7 @@ impl RenderBundle { min_resolution: f64, ) where N: AsPrimitive, - P: CartesianPoint3d, + P: CartesianPoint3d + Copy, Poly: Polygon, Poly::Contour: Contour, { diff --git a/galileo/src/render/render_bundle/world_set.rs b/galileo/src/render/render_bundle/world_set.rs index 42838b12..cf4aac25 100644 --- a/galileo/src/render/render_bundle/world_set.rs +++ b/galileo/src/render/render_bundle/world_set.rs @@ -2,9 +2,10 @@ use std::mem::size_of; use std::sync::Arc; use galileo_types::Polygon; -use galileo_types::cartesian::{CartesianPoint2d, CartesianPoint3d, Point2, Vector2}; +use galileo_types::cartesian::{CartesianPoint2d, CartesianPoint3d, Point2, Point3, Vector2}; use galileo_types::contour::Contour; use galileo_types::impls::ClosedContour; +use itertools::Itertools; use lyon::lyon_tessellation::{ BuffersBuilder, FillOptions, FillTessellator, FillVertex, FillVertexConstructor, LineJoin, Side, StrokeOptions, StrokeTessellator, StrokeVertex, StrokeVertexConstructor, VertexBuffers, @@ -21,7 +22,7 @@ use crate::Color; use crate::decoded_image::DecodedImage; use crate::render::point_paint::{CircleFill, PointPaint, PointShape, SectorParameters}; use crate::render::text::{TextService, TextShaping, TextStyle}; -use crate::render::{ImagePaint, LinePaint, PolygonPaint}; +use crate::render::{ImagePaint, LineCap, LinePaint, PolygonPaint}; #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct WorldRenderSet { @@ -55,6 +56,16 @@ impl Default for WorldRenderSet { } } +#[derive(Debug, Copy, Clone)] +pub(crate) struct LineParameters { + pub(crate) color: Color, + pub(crate) width: f32, + pub(crate) offset: f32, + pub(crate) cap: LineCap, +} + +pub(crate) struct DashArray<'a>(pub(crate) &'a [f32]); + impl WorldRenderSet { pub fn new(dpi_scale_factor: f32) -> Self { Self { @@ -179,17 +190,32 @@ impl WorldRenderSet { radius, outline, } => { - self.add_circle(point, *fill, *radius, *outline, paint.offset); + self.add_circle( + point, + *fill, + *radius, + outline.as_ref().map(LinePaint::line_parameters), + paint.offset, + ); } - PointShape::Sector(parameters) => { - self.add_circle_sector(point, *parameters, paint.offset); + PointShape::Sector { + parameters, + outline, + } => { + self.add_circle_sector( + point, + *parameters, + outline.as_ref().map(LinePaint::line_parameters), + paint.offset, + ); } PointShape::Square { fill, size, outline, } => { - self.add_shape(point, *fill, *size, *outline, &square_shape(), paint.offset); + let outline = outline.as_ref().map(LinePaint::line_parameters); + self.add_shape(point, *fill, *size, outline, &square_shape(), paint.offset); } PointShape::FreeShape { fill, @@ -197,7 +223,8 @@ impl WorldRenderSet { outline, shape, } => { - self.add_shape(point, *fill, *scale, *outline, shape, paint.offset); + let outline = outline.as_ref().map(LinePaint::line_parameters); + self.add_shape(point, *fill, *scale, outline, shape, paint.offset); } PointShape::Label { text, style } => self.add_label(point, text, style, paint.offset), }; @@ -206,21 +233,136 @@ impl WorldRenderSet { pub fn add_line(&mut self, line: &C, paint: &LinePaint, min_resolution: f64) where N: AsPrimitive, - P: CartesianPoint3d, + P: CartesianPoint3d + Copy, C: Contour, { - self.add_line_lod(line, *paint, min_resolution); + match paint.dasharray() { + Some(dasharray) => { + self.add_dashed_line(line, dasharray, paint.line_parameters(), min_resolution) + } + None => self.tessellate_line( + line.iter_points(), + line.is_closed(), + paint.line_parameters(), + min_resolution, + ), + } } - fn add_line_lod(&mut self, line: &C, paint: LinePaint, min_resolution: f64) - where + fn add_dashed_line( + &mut self, + line: &C, + dasharray: DashArray, + paint: LineParameters, + min_resolution: f64, + ) where N: AsPrimitive, - P: CartesianPoint3d, + P: CartesianPoint3d + Copy, C: Contour, + { + const MIN_PATTERN_LENGTH: f32 = 0.1; + + let scale = 1.0 / min_resolution as f32; + let pattern = dasharray.0; + if pattern.is_empty() { + return; + } + + let total_pattern_len = pattern.iter().sum::() * paint.width; + if total_pattern_len <= MIN_PATTERN_LENGTH { + // If pattern is too short, the result looks exactly the same as a continuous line, but + // computations required are much higher. So we just render a line without dashes. + return self.tessellate_line( + line.iter_points(), + line.is_closed(), + paint, + min_resolution, + ); + } + + let mut pattern_index = 0usize; + let mut is_dash = true; + let mut remaining_in_segment = pattern[0] * paint.width; + let mut current_dash: Vec> = vec![]; + + for (from, to) in line.iter_points_closing().tuple_windows() { + let fx = from.x().as_(); + let fy = from.y().as_(); + let fz = from.z().as_(); + let tx = to.x().as_(); + let ty = to.y().as_(); + let tz = to.z().as_(); + + let dx = (tx - fx) * scale; + let dy = (ty - fy) * scale; + let segment_len = (dx * dx + dy * dy).sqrt(); + + if segment_len <= 0.0 { + continue; + } + + let mut traveled = 0.0f32; + + while traveled < segment_len { + let t_start = traveled / segment_len; + let step = remaining_in_segment.min(segment_len - traveled); + traveled += step; + let t_end = traveled / segment_len; + + if is_dash { + if current_dash.is_empty() { + current_dash.push(Point3::new( + fx + (tx - fx) * t_start, + fy + (ty - fy) * t_start, + fz + (tz - fz) * t_start, + )); + } + current_dash.push(Point3::new( + fx + (tx - fx) * t_end, + fy + (ty - fy) * t_end, + fz + (tz - fz) * t_end, + )); + } + + remaining_in_segment -= step; + + if remaining_in_segment <= 0.0 { + if is_dash && current_dash.len() >= 2 { + self.tessellate_line( + std::mem::take(&mut current_dash), + false, + paint, + min_resolution, + ); + } else { + current_dash.clear(); + } + + pattern_index = (pattern_index + 1) % pattern.len(); + is_dash = !is_dash; + remaining_in_segment = pattern[pattern_index] * paint.width; + } + } + } + + if is_dash && current_dash.len() >= 2 { + self.tessellate_line(current_dash, false, paint, min_resolution); + } + } + + fn tessellate_line( + &mut self, + line: impl IntoIterator, + is_closed: bool, + paint: LineParameters, + min_resolution: f64, + ) where + N: AsPrimitive, + P: CartesianPoint3d, { let tessellation = &mut self.poly_tessellation; let mut path_builder = BuilderWithAttributes::new(1); - let mut iterator = line.iter_points(); + let mut iterator = line.into_iter(); let Some(first_point) = iterator.next() else { return; @@ -244,12 +386,12 @@ impl WorldRenderSet { ); } - path_builder.end(line.is_closed()); + path_builder.end(is_closed); let path = path_builder.build(); let vertex_constructor = LineVertexConstructor { - width: paint.width as f32 * self.dpi_scale_factor, - offset: paint.offset as f32, + width: paint.width * self.dpi_scale_factor, + offset: paint.offset, color: paint.color.to_f32_array(), resolution: min_resolution as f32, path: &path, @@ -262,8 +404,8 @@ impl WorldRenderSet { if let Err(err) = tesselator.tessellate_path( &path, &StrokeOptions::DEFAULT - .with_line_cap(paint.line_cap.into()) - .with_line_width(paint.width as f32) + .with_line_cap(paint.cap.into()) + .with_line_width(paint.width) .with_miter_limit(1.0) .with_tolerance(0.1) .with_line_join(LineJoin::MiterClip), @@ -363,12 +505,12 @@ impl WorldRenderSet { } } - pub fn add_shape( + fn add_shape( &mut self, position: &P, fill: Color, scale: f32, - outline: Option, + outline: Option, shape: &ClosedContour>, offset: Vector2, ) where @@ -393,7 +535,8 @@ impl WorldRenderSet { if let Err(err) = StrokeTessellator::new().tessellate( &path, - &StrokeOptions::DEFAULT.with_line_width(outline.width as f32 * 2.0), + &StrokeOptions::DEFAULT + .with_line_width(outline.width * 2.0 * self.dpi_scale_factor), &mut BuffersBuilder::new(tessellation, vertex_constructor), ) { log::warn!("Shape tessellation failed: {err:?}"); @@ -428,7 +571,7 @@ impl WorldRenderSet { position: &P, fill: CircleFill, radius: f32, - outline: Option, + outline: Option, offset: Vector2, ) where N: AsPrimitive, @@ -441,8 +584,8 @@ impl WorldRenderSet { radius, start_angle: 0.0, end_angle: std::f32::consts::PI * 2.0, - outline, }, + outline, offset, ) } @@ -451,6 +594,7 @@ impl WorldRenderSet { &mut self, position: &P, parameters: SectorParameters, + outline: Option, offset: Vector2, ) where N: AsPrimitive, @@ -461,7 +605,6 @@ impl WorldRenderSet { radius, start_angle, end_angle, - outline, } = parameters; const TOLERANCE: f32 = 0.1; let dr = (end_angle - start_angle) @@ -509,11 +652,11 @@ impl WorldRenderSet { self.poly_tessellation.vertices.append(&mut vertices); self.poly_tessellation.indices.append(&mut indices); - if let Some(mut outline) = outline { + if let Some(outline) = outline { if !is_full_circle { contour.push(Point2::new(0.0, 0.0)); } - outline.width *= self.dpi_scale_factor as f64; + self.add_shape( position, Color::TRANSPARENT,