From 075815e714149941a96acc9e7a80cddd765bbcbf Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 20 Apr 2024 14:38:49 -0400 Subject: [PATCH 01/22] Support rule mark in vega renderer, compare with vega svg renderer --- avenger-vega-renderer/js/index.js | 88 +++++++++++++++----- avenger-vega-renderer/src/builder.rs | 52 +++++++++--- avenger-vega-renderer/test/test_baselines.py | 34 +++++--- 3 files changed, 130 insertions(+), 44 deletions(-) diff --git a/avenger-vega-renderer/js/index.js b/avenger-vega-renderer/js/index.js index fdec0b6..b0511dc 100644 --- a/avenger-vega-renderer/js/index.js +++ b/avenger-vega-renderer/js/index.js @@ -301,25 +301,42 @@ function importSymbol(vegaSymbolMark, force_clip) { return symbolMark; } -function importRule(vegaRuleMark) { - const len = vegaRuleMark.items.length; - const ruleMark = new RuleMark(len, vegaRuleMark.clip, vegaRuleMark.name); +function importRule(vegaRuleMark, forceClip) { + const items = vegaRuleMark.items; + const len = items.length; + + const ruleMark = new RuleMark( + len, vegaRuleMark.clip || forceClip, vegaRuleMark.name, vegaRuleMark.zindex + ); + if (len === 0) { + return ruleMark; + } + + const firstItem = items[0]; const x0 = new Float32Array(len).fill(0); const y0 = new Float32Array(len).fill(0); const x1 = new Float32Array(len).fill(0); const y1 = new Float32Array(len).fill(0); - const width = new Float32Array(len).fill(1); - let anyWidth = false; + const strokeWidth = new Float32Array(len); + let anyStrokeWidth = false; - const opacity = new Float32Array(len).fill(1); - let anyOpacity = false; + const strokeOpacity = new Float32Array(len).fill(1); - const stroke = new Array(len).fill(""); + const stroke = new Array(len); let anyStroke = false; + let strokeIsGradient = firstItem.stroke != null && typeof firstItem.stroke === "object"; + + const strokeCap = new Array(len); + let anyStrokeCap = false; + + const strokeDash = new Array(len); + let anyStrokeDash = false; + + const zindex = new Float32Array(len).fill(0); + let anyZindex = false; - const items = vegaRuleMark.items; items.forEach((item, i) => { if (item.x != null) { x0[i] = item.x; @@ -337,29 +354,58 @@ function importRule(vegaRuleMark) { } else { y1[i] = y0[i]; } - if (item.width != null) { - width[i] = item.width; - anyWidth ||= true; - } - if (item.opacity != null) { - opacity[i] = item.opacity; - anyOpacity ||= true; + if (item.strokeWidth != null) { + strokeWidth[i] = item.strokeWidth; + anyStrokeWidth ||= true; } + strokeOpacity[i] = (item.strokeOpacity ?? 1) * (item.opacity ?? 1); + if (item.stroke != null) { stroke[i] = item.stroke; anyStroke ||= true; } + + if (item.strokeCap != null) { + strokeCap[i] = item.strokeCap; + anyStrokeCap ||= true; + } + + if (item.strokeDash != null) { + strokeDash[i] = item.strokeDash; + anyStrokeDash ||= true; + } + + if (item.zindex != null) { + zindex[i] = item.zindex; + anyZindex ||= true; + } }) ruleMark.set_xy(x0, y0, x1, y1); - if (anyWidth) { - ruleMark.set_stroke_width(width); + if (anyStrokeWidth) { + ruleMark.set_stroke_width(strokeWidth); } - if (anyStroke || anyOpacity) { - const encoded = encodeStringArray(stroke); - ruleMark.set_stroke(encoded.values, encoded.indices, opacity); + if (anyStroke) { + if (strokeIsGradient) { + ruleMark.set_stroke_gradient(stroke, strokeOpacity); + } else { + const encoded = encodeStringArray(stroke); + ruleMark.set_stroke(encoded.values, encoded.indices, strokeOpacity); + } + } + + if (anyStrokeCap) { + ruleMark.set_stroke_cap(strokeCap); + } + + if (anyStrokeDash) { + ruleMark.set_stroke_dash(strokeDash); + } + + if (anyZindex) { + ruleMark.set_zindex(zindex); } return ruleMark; diff --git a/avenger-vega-renderer/src/builder.rs b/avenger-vega-renderer/src/builder.rs index 806dfa4..a28b8d9 100644 --- a/avenger-vega-renderer/src/builder.rs +++ b/avenger-vega-renderer/src/builder.rs @@ -3,13 +3,13 @@ use avenger::marks::group::{Clip, SceneGroup as RsSceneGroup}; use avenger::marks::mark::SceneMark; use avenger::marks::symbol::SymbolShape; use avenger::marks::text::{FontStyleSpec, FontWeightSpec, TextAlignSpec, TextBaselineSpec}; -use avenger::marks::value::{ColorOrGradient, EncodingValue, Gradient}; +use avenger::marks::value::{ColorOrGradient, EncodingValue, Gradient, StrokeCap}; use avenger::marks::{ rule::RuleMark as RsRuleMark, symbol::SymbolMark as RsSymbolMark, text::TextMark as RsTextMark, }; use avenger::scene_graph::SceneGraph as RsSceneGraph; use avenger_vega::error::AvengerVegaError; -use avenger_vega::marks::values::CssColorOrGradient; +use avenger_vega::marks::values::{CssColorOrGradient, StrokeDashSpec}; use gloo_utils::format::JsValueSerdeExt; use wasm_bindgen::prelude::*; @@ -34,9 +34,7 @@ impl SymbolMark { } pub fn set_zindex(&mut self, zindex: Vec) { - let mut indices: Vec = (0..self.inner.len as usize).collect(); - indices.sort_by_key(|i| zindex[*i]); - self.inner.indices = Some(indices); + self.inner.indices = Some(zindex_to_indices(zindex)); } pub fn set_xy(&mut self, x: Vec, y: Vec) { @@ -123,22 +121,20 @@ pub struct RuleMark { #[wasm_bindgen] impl RuleMark { #[wasm_bindgen(constructor)] - pub fn new(len: u32, clip: bool, name: Option) -> Self { + pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { Self { inner: RsRuleMark { len, clip, name: name.unwrap_or_default(), - stroke: EncodingValue::Scalar { - value: ColorOrGradient::Color([0.0, 0.0, 0.0, 1.0]), - }, + zindex, ..Default::default() }, } } - pub fn set_zindex(&mut self, zindex: Option) { - self.inner.zindex = zindex; + pub fn set_zindex(&mut self, zindex: Vec) { + self.inner.indices = Some(zindex_to_indices(zindex)); } pub fn set_xy(&mut self, x0: Vec, y0: Vec, x1: Vec, y1: Vec) { @@ -163,6 +159,34 @@ impl RuleMark { }; Ok(()) } + + pub fn set_stroke_gradient( + &mut self, + values: JsValue, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.stroke = EncodingValue::Array { + values: decode_gradients(values, opacity, &mut self.inner.gradients)?, + }; + Ok(()) + } + + pub fn set_stroke_cap(&mut self, values: JsValue) -> Result<(), JsError> { + let values: Vec = values.into_serde()?; + self.inner.stroke_cap = EncodingValue::Array { values }; + Ok(()) + } + + pub fn set_stroke_dash(&mut self, values: JsValue) -> Result<(), JsError> { + let values: Vec = values.into_serde()?; + let values = values.into_iter().map( + |s| Ok(s.to_array()?.to_vec()) + ).collect::, AvengerVegaError>>() + .map_err(|_| JsError::new("Failed to parse dash spec"))?; + self.inner.stroke_dash = Some(EncodingValue::Array { values }); + Ok(()) + } + } #[wasm_bindgen] @@ -447,3 +471,9 @@ impl SceneGraph { self.inner.origin } } + +pub fn zindex_to_indices(zindex: Vec) -> Vec { + let mut indices: Vec = (0..zindex.len()).collect(); + indices.sort_by_key(|i| zindex[*i]); + indices +} \ No newline at end of file diff --git a/avenger-vega-renderer/test/test_baselines.py b/avenger-vega-renderer/test/test_baselines.py index 2cabb47..ecf1d1f 100644 --- a/avenger-vega-renderer/test/test_baselines.py +++ b/avenger-vega-renderer/test/test_baselines.py @@ -60,7 +60,7 @@ def failures_path(): ("symbol", "binned_scatter_triangle-left", 0.0001), ("symbol", "binned_scatter_triangle-right", 0.0001), ("symbol", "binned_scatter_triangle", 0.0001), - ("symbol", "binned_scatter_wedge", 0.0001), + ("symbol", "binned_scatter_wedge", 0.0003), ("symbol", "binned_scatter_arrow", 0.0001), ("symbol", "binned_scatter_cross", 0.0001), ("symbol", "binned_scatter_circle", 0.0001), @@ -80,10 +80,16 @@ def failures_path(): ("symbol", "zindex_circles", 0.0001), ("symbol", "mixed_symbols", 0.0001), + # lyon seems to omit closing square cap, need to investigate + ("rule", "wide_transparent_caps", 0.003), + ("rule", "dashed_rules", 0.0004), + ("rule", "wide_rule_axes", 0.0001), + # The canvas renderer messes up these gradients, avenger renders them correctly - ("gradients", "symbol_cross_gradient", 0.03), - ("gradients", "symbol_circles_gradient_stroke", 0.03), + ("gradients", "symbol_cross_gradient", 0.0001), + ("gradients", "symbol_circles_gradient_stroke", 0.0001), ("gradients", "symbol_radial_gradient", 0.0002), + ("gradients", "rules_with_gradients", 0.003), # Lyon square caps issue ], ) def test_image_baselines( @@ -106,7 +112,7 @@ def test_image_baselines( if comparison_res.score > tolerance: outdir = failures_path / category / spec_name outdir.mkdir(parents=True, exist_ok=True) - comparison_res.canvas_img.save(outdir / "canvas.png") + comparison_res.svg_img.save(outdir / "svg.png") comparison_res.avenger_img.save(outdir / "avenger.png") comparison_res.diff_img.save(outdir / "diff.png") with open(outdir / f"metrics.json", "wt") as f: @@ -124,7 +130,7 @@ def test_image_baselines( @dataclass class ComparisonResult: - canvas_img: Image + svg_img: Image avenger_img: Image diff_img: Image mismatch: int @@ -138,12 +144,12 @@ def compare(page: Page, spec: dict) -> ComparisonResult: if avenger_errs: pytest.fail('\n'.join(avenger_errs)) - canvas_img = spec_to_image(page, spec, "canvas") - diff_img = Image.new("RGBA", canvas_img.size) - mismatch = pixelmatch(canvas_img, avenger_img, diff_img, threshold=0.2) - score = mismatch / (canvas_img.width * canvas_img.height) + svg_img = spec_to_image(page, spec, "svg") + diff_img = Image.new("RGBA", svg_img.size) + mismatch = pixelmatch(svg_img, avenger_img, diff_img, threshold=0.2) + score = mismatch / (svg_img.width * svg_img.height) return ComparisonResult( - canvas_img=canvas_img, + svg_img=svg_img, avenger_img=avenger_img, diff_img=diff_img, mismatch=mismatch, @@ -152,14 +158,18 @@ def compare(page: Page, spec: dict) -> ComparisonResult: def spec_to_image( - page: Page, spec: dict, renderer: Literal["canvas", "avenger"] + page: Page, spec: dict, renderer: Literal["canvas", "avenger", "svg"] ) -> Image: embed_opts = {"actions": False, "renderer": renderer} script = ( f"vegaEmbed('#plot-container', {json.dumps(spec)}, {json.dumps(embed_opts)});" ) page.evaluate_handle(script) - img = Image.open(io.BytesIO(page.locator("canvas").first.screenshot())) + if renderer == "svg": + locator = page.locator("svg") + else: + locator = page.locator("canvas") + img = Image.open(io.BytesIO(locator.first.screenshot())) # Check that the image is not entirely white (which happens on rendering errors sometimes) pixels = img.load() From c356fbf36094a691879f7dc015df8ba2335d459b Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 20 Apr 2024 16:34:58 -0400 Subject: [PATCH 02/22] Text fixes and add text tests --- avenger-vega-renderer/js/index.js | 23 +++++++++----------- avenger-vega-renderer/src/builder.rs | 11 +++++----- avenger-vega-renderer/src/text.rs | 16 ++++++++------ avenger-vega-renderer/test/test_baselines.py | 7 ++++++ 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/avenger-vega-renderer/js/index.js b/avenger-vega-renderer/js/index.js index b0511dc..2fe0395 100644 --- a/avenger-vega-renderer/js/index.js +++ b/avenger-vega-renderer/js/index.js @@ -435,9 +435,9 @@ function importText(vegaTextMark) { const font = new Array(len); let anyFont = false; - const color = new Array(len).fill(""); - const opacity = new Float32Array(len).fill(1.0); - let anyColorOrOpacity = false; + const fill = new Array(len); + const fillOpacity = new Float32Array(len).fill(1); + let anyFill = false; const baseline = new Array(len); let anyBaseline = false; @@ -479,14 +479,11 @@ function importText(vegaTextMark) { anyLimit ||= true; } - if (item.color != null) { - color[i] = item.color; - anyColorOrOpacity ||= true; - } - if (item.opacity != null) { - opacity[i] = item.opacity; - anyColorOrOpacity ||= true; + if (item.fill != null) { + fill[i] = item.fill; + anyFill ||= true; } + fillOpacity[i] = (item.fillOpacity ?? 1) * (item.opacity ?? 1); if (item.font != null) { font[i] = item.font; @@ -525,9 +522,9 @@ function importText(vegaTextMark) { } // String columns to pass as encoded unique values + indices - if (anyColorOrOpacity) { - const encoded = encodeStringArray(color); - textMark.set_color(encoded.values, encoded.indices, opacity); + if (anyFill) { + const encoded = encodeStringArray(fill); + textMark.set_color(encoded.values, encoded.indices, fillOpacity); } if (anyFont) { const encoded = encodeStringArray(font); diff --git a/avenger-vega-renderer/src/builder.rs b/avenger-vega-renderer/src/builder.rs index a28b8d9..b8656aa 100644 --- a/avenger-vega-renderer/src/builder.rs +++ b/avenger-vega-renderer/src/builder.rs @@ -197,19 +197,20 @@ pub struct TextMark { #[wasm_bindgen] impl TextMark { #[wasm_bindgen(constructor)] - pub fn new(len: u32, clip: bool, name: Option) -> Self { + pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { Self { inner: RsTextMark { len, clip, name: name.unwrap_or_default(), + zindex, ..Default::default() }, } } - pub fn set_zindex(&mut self, zindex: Option) { - self.inner.zindex = zindex; + pub fn set_zindex(&mut self, zindex: Vec) { + self.inner.indices = Some(zindex_to_indices(zindex)); } pub fn set_xy(&mut self, x: Vec, y: Vec) { @@ -309,7 +310,7 @@ impl TextMark { ) -> Result<(), JsError> { // Parse unique colors let color_values: Vec = color_values.into_serde()?; - let unique_strokes = color_values + let unique_colors = color_values .iter() .map(|c| { let Ok(c) = csscolorparser::parse(c) else { @@ -324,7 +325,7 @@ impl TextMark { .iter() .zip(opacity) .map(|(ind, opacity)| { - let [r, g, b, a] = unique_strokes[*ind as usize]; + let [r, g, b, a] = unique_colors[*ind]; [r as f32, g as f32, b as f32, a as f32 * opacity] }) .collect::>(); diff --git a/avenger-vega-renderer/src/text.rs b/avenger-vega-renderer/src/text.rs index 6071665..c698621 100644 --- a/avenger-vega-renderer/src/text.rs +++ b/avenger-vega-renderer/src/text.rs @@ -60,13 +60,14 @@ impl TextRasterizer for HtmlCanvasTextRasterizer { text_context.set_font(&font_str); let color = config.color; - let color_str = JsValue::from_str(&format!( + let color_str = format!( "rgba({}, {}, {}, {})", (color[0] * 255.0) as u8, (color[1] * 255.0) as u8, (color[2] * 255.0) as u8, color[3], - )); + ); + let color_js_str = JsValue::from_str(&color_str); // Initialize string container that will hold the full string up through each cluster let mut str_so_far = String::new(); @@ -84,7 +85,7 @@ impl TextRasterizer for HtmlCanvasTextRasterizer { } // Build cache key concatenating font and cluster - let cache_key = calculate_cache_key(&font_str, &cluster); + let cache_key = calculate_cache_key(&font_str, &cluster, &color_str); // Compute metrics of cumulative string up to this cluster let cumulative_metrics = text_context.measure_text(&str_so_far)?; @@ -133,7 +134,7 @@ impl TextRasterizer for HtmlCanvasTextRasterizer { let glyph_context = glyph_context.dyn_into::()?; glyph_context.set_font(&font_str); - glyph_context.set_fill_style(&color_str); + glyph_context.set_fill_style(&color_js_str); // // Debugging, add bbox outline // glyph_context.set_stroke_style(&"red".into()); @@ -179,8 +180,8 @@ impl TextRasterizer for HtmlCanvasTextRasterizer { let buffer_width = full_metrics.actual_bounding_box_left() + full_metrics.actual_bounding_box_right(); let buffer_height = - full_metrics.actual_bounding_box_ascent() + full_metrics.actual_bounding_box_descent(); - let buffer_line_y = full_metrics.actual_bounding_box_ascent(); + full_metrics.font_bounding_box_ascent() + full_metrics.font_bounding_box_descent(); + let buffer_line_y = full_metrics.font_bounding_box_ascent(); Ok(TextRasterizationBuffer { glyphs, @@ -191,9 +192,10 @@ impl TextRasterizer for HtmlCanvasTextRasterizer { } } -fn calculate_cache_key(font_str: &str, cluster: &str) -> u64 { +fn calculate_cache_key(font_str: &str, cluster: &str, color_str: &str) -> u64 { let mut s = DefaultHasher::new(); font_str.hash(&mut s); cluster.hash(&mut s); + color_str.hash(&mut s); s.finish() } diff --git a/avenger-vega-renderer/test/test_baselines.py b/avenger-vega-renderer/test/test_baselines.py index ecf1d1f..9ecc2d2 100644 --- a/avenger-vega-renderer/test/test_baselines.py +++ b/avenger-vega-renderer/test/test_baselines.py @@ -85,6 +85,13 @@ def failures_path(): ("rule", "dashed_rules", 0.0004), ("rule", "wide_rule_axes", 0.0001), + ("text", "text_alignment", 0.016), + ("text", "text_rotation", 0.016), + ("text", "letter_scatter", 0.027), + # ("text", "lasagna_plot", 0.02), + # ("text", "arc_radial", 0.0001), + # ("text", "emoji", 0.0001), + # The canvas renderer messes up these gradients, avenger renders them correctly ("gradients", "symbol_cross_gradient", 0.0001), ("gradients", "symbol_circles_gradient_stroke", 0.0001), From e05d5b1d4e00ced34bc0e600fbb503f21f1dc615 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 20 Apr 2024 17:31:01 -0400 Subject: [PATCH 03/22] Split marks into separate files --- avenger-vega-renderer/src/builder.rs | 480 ---------------------- avenger-vega-renderer/src/lib.rs | 5 +- avenger-vega-renderer/src/marks/group.rs | 67 +++ avenger-vega-renderer/src/marks/mod.rs | 5 + avenger-vega-renderer/src/marks/rule.rs | 90 ++++ avenger-vega-renderer/src/marks/symbol.rs | 113 +++++ avenger-vega-renderer/src/marks/text.rs | 160 ++++++++ avenger-vega-renderer/src/marks/util.rs | 54 +++ avenger-vega-renderer/src/scene.rs | 59 +++ 9 files changed, 551 insertions(+), 482 deletions(-) delete mode 100644 avenger-vega-renderer/src/builder.rs create mode 100644 avenger-vega-renderer/src/marks/group.rs create mode 100644 avenger-vega-renderer/src/marks/mod.rs create mode 100644 avenger-vega-renderer/src/marks/rule.rs create mode 100644 avenger-vega-renderer/src/marks/symbol.rs create mode 100644 avenger-vega-renderer/src/marks/text.rs create mode 100644 avenger-vega-renderer/src/marks/util.rs create mode 100644 avenger-vega-renderer/src/scene.rs diff --git a/avenger-vega-renderer/src/builder.rs b/avenger-vega-renderer/src/builder.rs deleted file mode 100644 index b8656aa..0000000 --- a/avenger-vega-renderer/src/builder.rs +++ /dev/null @@ -1,480 +0,0 @@ -use avenger::error::AvengerError; -use avenger::marks::group::{Clip, SceneGroup as RsSceneGroup}; -use avenger::marks::mark::SceneMark; -use avenger::marks::symbol::SymbolShape; -use avenger::marks::text::{FontStyleSpec, FontWeightSpec, TextAlignSpec, TextBaselineSpec}; -use avenger::marks::value::{ColorOrGradient, EncodingValue, Gradient, StrokeCap}; -use avenger::marks::{ - rule::RuleMark as RsRuleMark, symbol::SymbolMark as RsSymbolMark, text::TextMark as RsTextMark, -}; -use avenger::scene_graph::SceneGraph as RsSceneGraph; -use avenger_vega::error::AvengerVegaError; -use avenger_vega::marks::values::{CssColorOrGradient, StrokeDashSpec}; -use gloo_utils::format::JsValueSerdeExt; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -pub struct SymbolMark { - inner: RsSymbolMark, -} - -#[wasm_bindgen] -impl SymbolMark { - #[wasm_bindgen(constructor)] - pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { - Self { - inner: RsSymbolMark { - len, - clip, - zindex, - name: name.unwrap_or_default(), - ..Default::default() - }, - } - } - - pub fn set_zindex(&mut self, zindex: Vec) { - self.inner.indices = Some(zindex_to_indices(zindex)); - } - - pub fn set_xy(&mut self, x: Vec, y: Vec) { - self.inner.x = EncodingValue::Array { values: x }; - self.inner.y = EncodingValue::Array { values: y }; - } - - pub fn set_size(&mut self, size: Vec) { - self.inner.size = EncodingValue::Array { values: size }; - } - - pub fn set_angle(&mut self, angle: Vec) { - self.inner.angle = EncodingValue::Array { values: angle }; - } - - pub fn set_stroke_width(&mut self, width: Option) { - self.inner.stroke_width = width; - } - - pub fn set_stroke( - &mut self, - color_values: JsValue, - indices: Vec, - opacity: Vec, - ) -> Result<(), JsError> { - self.inner.stroke = EncodingValue::Array { - values: decode_colors(color_values, indices, opacity)?, - }; - Ok(()) - } - - pub fn set_stroke_gradient( - &mut self, - values: JsValue, - opacity: Vec, - ) -> Result<(), JsError> { - self.inner.stroke = EncodingValue::Array { - values: decode_gradients(values, opacity, &mut self.inner.gradients)?, - }; - Ok(()) - } - - pub fn set_fill( - &mut self, - color_values: JsValue, - indices: Vec, - opacity: Vec, - ) -> Result<(), JsError> { - self.inner.fill = EncodingValue::Array { - values: decode_colors(color_values, indices, opacity)?, - }; - Ok(()) - } - - pub fn set_fill_gradient(&mut self, values: JsValue, opacity: Vec) -> Result<(), JsError> { - self.inner.fill = EncodingValue::Array { - values: decode_gradients(values, opacity, &mut self.inner.gradients)?, - }; - Ok(()) - } - - pub fn set_shape(&mut self, shape_values: JsValue, indices: Vec) -> Result<(), JsError> { - let shapes: Vec = shape_values.into_serde()?; - let shapes = shapes - .iter() - .map(|s| SymbolShape::from_vega_str(s)) - .collect::, AvengerError>>() - .map_err(|_| JsError::new("Failed to parse shapes"))?; - - self.inner.shapes = shapes; - self.inner.shape_index = EncodingValue::Array { values: indices }; - Ok(()) - } - - // TODO - // pub indices: Option>, -} - -#[wasm_bindgen] -pub struct RuleMark { - inner: RsRuleMark, -} - -#[wasm_bindgen] -impl RuleMark { - #[wasm_bindgen(constructor)] - pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { - Self { - inner: RsRuleMark { - len, - clip, - name: name.unwrap_or_default(), - zindex, - ..Default::default() - }, - } - } - - pub fn set_zindex(&mut self, zindex: Vec) { - self.inner.indices = Some(zindex_to_indices(zindex)); - } - - pub fn set_xy(&mut self, x0: Vec, y0: Vec, x1: Vec, y1: Vec) { - self.inner.x0 = EncodingValue::Array { values: x0 }; - self.inner.y0 = EncodingValue::Array { values: y0 }; - self.inner.x1 = EncodingValue::Array { values: x1 }; - self.inner.y1 = EncodingValue::Array { values: y1 }; - } - - pub fn set_stroke_width(&mut self, width: Vec) { - self.inner.stroke_width = EncodingValue::Array { values: width } - } - - pub fn set_stroke( - &mut self, - color_values: JsValue, - indices: Vec, - opacity: Vec, - ) -> Result<(), JsError> { - self.inner.stroke = EncodingValue::Array { - values: decode_colors(color_values, indices, opacity)?, - }; - Ok(()) - } - - pub fn set_stroke_gradient( - &mut self, - values: JsValue, - opacity: Vec, - ) -> Result<(), JsError> { - self.inner.stroke = EncodingValue::Array { - values: decode_gradients(values, opacity, &mut self.inner.gradients)?, - }; - Ok(()) - } - - pub fn set_stroke_cap(&mut self, values: JsValue) -> Result<(), JsError> { - let values: Vec = values.into_serde()?; - self.inner.stroke_cap = EncodingValue::Array { values }; - Ok(()) - } - - pub fn set_stroke_dash(&mut self, values: JsValue) -> Result<(), JsError> { - let values: Vec = values.into_serde()?; - let values = values.into_iter().map( - |s| Ok(s.to_array()?.to_vec()) - ).collect::, AvengerVegaError>>() - .map_err(|_| JsError::new("Failed to parse dash spec"))?; - self.inner.stroke_dash = Some(EncodingValue::Array { values }); - Ok(()) - } - -} - -#[wasm_bindgen] -pub struct TextMark { - inner: RsTextMark, -} - -#[wasm_bindgen] -impl TextMark { - #[wasm_bindgen(constructor)] - pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { - Self { - inner: RsTextMark { - len, - clip, - name: name.unwrap_or_default(), - zindex, - ..Default::default() - }, - } - } - - pub fn set_zindex(&mut self, zindex: Vec) { - self.inner.indices = Some(zindex_to_indices(zindex)); - } - - pub fn set_xy(&mut self, x: Vec, y: Vec) { - self.inner.x = EncodingValue::Array { values: x }; - self.inner.y = EncodingValue::Array { values: y }; - } - - pub fn set_angle(&mut self, angle: Vec) { - self.inner.angle = EncodingValue::Array { values: angle }; - } - - pub fn set_font_size(&mut self, font_size: Vec) { - self.inner.font_size = EncodingValue::Array { values: font_size }; - } - - pub fn set_font_limit(&mut self, limit: Vec) { - self.inner.limit = EncodingValue::Array { values: limit }; - } - - pub fn set_indices(&mut self, indices: Vec) { - self.inner.indices = Some(indices); - } - - pub fn set_text(&mut self, text: JsValue) -> Result<(), JsError> { - let text: Vec = text.into_serde()?; - self.inner.text = EncodingValue::Array { values: text }; - Ok(()) - } - - pub fn set_font(&mut self, font_values: JsValue, indices: Vec) -> Result<(), JsError> { - let font_values: Vec = font_values.into_serde()?; - let values = indices - .iter() - .map(|ind| font_values[*ind].clone()) - .collect::>(); - self.inner.font = EncodingValue::Array { values }; - Ok(()) - } - - pub fn set_align(&mut self, align_values: JsValue, indices: Vec) -> Result<(), JsError> { - let align_values: Vec = align_values.into_serde()?; - let values = indices - .iter() - .map(|ind| align_values[*ind].clone()) - .collect::>(); - self.inner.align = EncodingValue::Array { values }; - Ok(()) - } - - pub fn set_baseline( - &mut self, - baseline_values: JsValue, - indices: Vec, - ) -> Result<(), JsError> { - let baseline_values: Vec = baseline_values.into_serde()?; - let values = indices - .iter() - .map(|ind| baseline_values[*ind].clone()) - .collect::>(); - self.inner.baseline = EncodingValue::Array { values }; - Ok(()) - } - - pub fn set_font_weight( - &mut self, - weight_values: JsValue, - indices: Vec, - ) -> Result<(), JsError> { - let weight_values: Vec = weight_values.into_serde()?; - let values = indices - .iter() - .map(|ind| weight_values[*ind].clone()) - .collect::>(); - self.inner.font_weight = EncodingValue::Array { values }; - Ok(()) - } - - pub fn set_font_style( - &mut self, - style_values: JsValue, - indices: Vec, - ) -> Result<(), JsError> { - let style_values: Vec = style_values.into_serde()?; - let values = indices - .iter() - .map(|ind| style_values[*ind].clone()) - .collect::>(); - self.inner.font_style = EncodingValue::Array { values }; - Ok(()) - } - - pub fn set_color( - &mut self, - color_values: JsValue, - indices: Vec, - opacity: Vec, - ) -> Result<(), JsError> { - // Parse unique colors - let color_values: Vec = color_values.into_serde()?; - let unique_colors = color_values - .iter() - .map(|c| { - let Ok(c) = csscolorparser::parse(c) else { - return [0.0, 0.0, 0.0, 1.0]; - }; - [c.r as f32, c.g as f32, c.b as f32, c.a as f32] - }) - .collect::>(); - - // Combine with opacity to build - let colors = indices - .iter() - .zip(opacity) - .map(|(ind, opacity)| { - let [r, g, b, a] = unique_colors[*ind]; - [r as f32, g as f32, b as f32, a as f32 * opacity] - }) - .collect::>(); - - self.inner.color = EncodingValue::Array { values: colors }; - Ok(()) - } -} - -fn decode_gradients( - values: JsValue, - opacity: Vec, - gradients: &mut Vec, -) -> Result, JsError> { - let values: Vec = values.into_serde()?; - values - .iter() - .zip(opacity) - .map(|(grad, opacity)| grad.to_color_or_grad(opacity, gradients)) - .collect::, AvengerVegaError>>() - .map_err(|_| JsError::new("Failed to parse gradients")) -} - -fn decode_colors( - color_values: JsValue, - indices: Vec, - opacity: Vec, -) -> Result, JsError> { - // Parse unique colors - let color_values: Vec = color_values.into_serde()?; - let unique_strokes = color_values - .iter() - .map(|c| { - let Ok(c) = csscolorparser::parse(c) else { - return [0.0, 0.0, 0.0, 1.0]; - }; - [c.r as f32, c.g as f32, c.b as f32, c.a as f32] - }) - .collect::>(); - - // Combine with opacity to build - let colors = indices - .iter() - .zip(opacity) - .map(|(ind, opacity)| { - let [r, g, b, a] = unique_strokes[*ind as usize]; - ColorOrGradient::Color([r as f32, g as f32, b as f32, a as f32 * opacity]) - }) - .collect::>(); - Ok(colors) -} - -#[wasm_bindgen] -pub struct GroupMark { - inner: RsSceneGroup, -} - -#[wasm_bindgen] -impl GroupMark { - #[wasm_bindgen(constructor)] - pub fn new( - origin_x: f32, - origin_y: f32, - name: Option, - width: Option, - height: Option, - ) -> Self { - let clip = if let (Some(width), Some(height)) = (width, height) { - Clip::Rect { - x: 0.0, - y: 0.0, - width: width.clone(), - height: height.clone(), - } - } else { - Clip::None - }; - - Self { - inner: RsSceneGroup { - origin: [origin_x, origin_y], - name: name.unwrap_or_default(), - clip, - ..Default::default() - }, - } - } - - pub fn add_symbol_mark(&mut self, mark: SymbolMark) { - self.inner.marks.push(SceneMark::Symbol(mark.inner)); - } - - pub fn add_rule_mark(&mut self, mark: RuleMark) { - self.inner.marks.push(SceneMark::Rule(mark.inner)); - } - - pub fn add_text_mark(&mut self, mark: TextMark) { - self.inner.marks.push(SceneMark::Text(Box::new(mark.inner))); - } - - pub fn add_group_mark(&mut self, mark: GroupMark) { - self.inner.marks.push(SceneMark::Group(mark.inner)); - } -} - -#[wasm_bindgen] -pub struct SceneGraph { - inner: RsSceneGraph, -} - -#[wasm_bindgen] -impl SceneGraph { - #[wasm_bindgen(constructor)] - pub fn new(width: f32, height: f32, origin_x: f32, origin_y: f32) -> Self { - Self { - inner: RsSceneGraph { - width, - height, - origin: [origin_x, origin_y], - groups: Vec::new(), - }, - } - } - - pub fn add_group(&mut self, group: GroupMark) { - self.inner.groups.push(group.inner) - } -} - -impl SceneGraph { - pub fn build(self) -> RsSceneGraph { - self.inner - } - - pub fn width(&self) -> f32 { - self.inner.width - } - - pub fn height(&self) -> f32 { - self.inner.height - } - - pub fn origin(&self) -> [f32; 2] { - self.inner.origin - } -} - -pub fn zindex_to_indices(zindex: Vec) -> Vec { - let mut indices: Vec = (0..zindex.len()).collect(); - indices.sort_by_key(|i| zindex[*i]); - indices -} \ No newline at end of file diff --git a/avenger-vega-renderer/src/lib.rs b/avenger-vega-renderer/src/lib.rs index 8b7acda..9fe2a25 100644 --- a/avenger-vega-renderer/src/lib.rs +++ b/avenger-vega-renderer/src/lib.rs @@ -1,7 +1,8 @@ -mod builder; +mod marks; +mod scene; mod text; -use crate::builder::SceneGraph; +use crate::scene::SceneGraph; use crate::text::HtmlCanvasTextRasterizer; use avenger_wgpu::canvas::{Canvas, CanvasConfig, CanvasDimensions, PngCanvas}; diff --git a/avenger-vega-renderer/src/marks/group.rs b/avenger-vega-renderer/src/marks/group.rs new file mode 100644 index 0000000..4fad0f7 --- /dev/null +++ b/avenger-vega-renderer/src/marks/group.rs @@ -0,0 +1,67 @@ +use crate::marks::rule::RuleMark; +use crate::marks::symbol::SymbolMark; +use crate::marks::text::TextMark; +use avenger::marks::group::{Clip, SceneGroup as RsSceneGroup}; +use avenger::marks::mark::SceneMark; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen] +pub struct GroupMark { + inner: RsSceneGroup, +} + +impl GroupMark { + pub fn build(self) -> RsSceneGroup { + self.inner + } +} + +#[wasm_bindgen] +impl GroupMark { + #[wasm_bindgen(constructor)] + pub fn new( + origin_x: f32, + origin_y: f32, + name: Option, + width: Option, + height: Option, + ) -> Self { + let clip = if let (Some(width), Some(height)) = (width, height) { + Clip::Rect { + x: 0.0, + y: 0.0, + width: width.clone(), + height: height.clone(), + } + } else { + Clip::None + }; + + Self { + inner: RsSceneGroup { + origin: [origin_x, origin_y], + name: name.unwrap_or_default(), + clip, + ..Default::default() + }, + } + } + + pub fn add_symbol_mark(&mut self, mark: SymbolMark) { + self.inner.marks.push(SceneMark::Symbol(mark.build())); + } + + pub fn add_rule_mark(&mut self, mark: RuleMark) { + self.inner.marks.push(SceneMark::Rule(mark.build())); + } + + pub fn add_text_mark(&mut self, mark: TextMark) { + self.inner + .marks + .push(SceneMark::Text(Box::new(mark.build()))); + } + + pub fn add_group_mark(&mut self, mark: GroupMark) { + self.inner.marks.push(SceneMark::Group(mark.inner)); + } +} diff --git a/avenger-vega-renderer/src/marks/mod.rs b/avenger-vega-renderer/src/marks/mod.rs new file mode 100644 index 0000000..483e5dd --- /dev/null +++ b/avenger-vega-renderer/src/marks/mod.rs @@ -0,0 +1,5 @@ +pub mod group; +pub mod rule; +pub mod symbol; +pub mod text; +pub mod util; diff --git a/avenger-vega-renderer/src/marks/rule.rs b/avenger-vega-renderer/src/marks/rule.rs new file mode 100644 index 0000000..c9ce499 --- /dev/null +++ b/avenger-vega-renderer/src/marks/rule.rs @@ -0,0 +1,90 @@ +use crate::marks::util::{decode_colors, decode_gradients, zindex_to_indices}; +use avenger::marks::rule::RuleMark as RsRuleMark; +use avenger::marks::value::{EncodingValue, StrokeCap}; +use avenger_vega::error::AvengerVegaError; +use avenger_vega::marks::values::StrokeDashSpec; +use gloo_utils::format::JsValueSerdeExt; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; + +#[wasm_bindgen] +pub struct RuleMark { + inner: RsRuleMark, +} + +impl RuleMark { + pub fn build(self) -> RsRuleMark { + self.inner + } +} + +#[wasm_bindgen] +impl RuleMark { + #[wasm_bindgen(constructor)] + pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { + Self { + inner: RsRuleMark { + len, + clip, + name: name.unwrap_or_default(), + zindex, + ..Default::default() + }, + } + } + + pub fn set_zindex(&mut self, zindex: Vec) { + self.inner.indices = Some(zindex_to_indices(zindex)); + } + + pub fn set_xy(&mut self, x0: Vec, y0: Vec, x1: Vec, y1: Vec) { + self.inner.x0 = EncodingValue::Array { values: x0 }; + self.inner.y0 = EncodingValue::Array { values: y0 }; + self.inner.x1 = EncodingValue::Array { values: x1 }; + self.inner.y1 = EncodingValue::Array { values: y1 }; + } + + pub fn set_stroke_width(&mut self, width: Vec) { + self.inner.stroke_width = EncodingValue::Array { values: width } + } + + pub fn set_stroke( + &mut self, + color_values: JsValue, + indices: Vec, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.stroke = EncodingValue::Array { + values: decode_colors(color_values, indices, opacity)?, + }; + Ok(()) + } + + pub fn set_stroke_gradient( + &mut self, + values: JsValue, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.stroke = EncodingValue::Array { + values: decode_gradients(values, opacity, &mut self.inner.gradients)?, + }; + Ok(()) + } + + pub fn set_stroke_cap(&mut self, values: JsValue) -> Result<(), JsError> { + let values: Vec = values.into_serde()?; + self.inner.stroke_cap = EncodingValue::Array { values }; + Ok(()) + } + + pub fn set_stroke_dash(&mut self, values: JsValue) -> Result<(), JsError> { + let values: Vec = values.into_serde()?; + let values = values + .into_iter() + .map(|s| Ok(s.to_array()?.to_vec())) + .collect::, AvengerVegaError>>() + .map_err(|_| JsError::new("Failed to parse dash spec"))?; + self.inner.stroke_dash = Some(EncodingValue::Array { values }); + Ok(()) + } +} diff --git a/avenger-vega-renderer/src/marks/symbol.rs b/avenger-vega-renderer/src/marks/symbol.rs new file mode 100644 index 0000000..06f5fbc --- /dev/null +++ b/avenger-vega-renderer/src/marks/symbol.rs @@ -0,0 +1,113 @@ +use crate::marks::util::{decode_colors, decode_gradients, zindex_to_indices}; +use avenger::error::AvengerError; +use avenger::marks::symbol::{SymbolMark as RsSymbolMark, SymbolShape}; +use avenger::marks::value::EncodingValue; +use gloo_utils::format::JsValueSerdeExt; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; + +#[wasm_bindgen] +pub struct SymbolMark { + inner: RsSymbolMark, +} + +impl SymbolMark { + pub fn build(self) -> RsSymbolMark { + self.inner + } +} + +#[wasm_bindgen] +impl SymbolMark { + #[wasm_bindgen(constructor)] + pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { + Self { + inner: RsSymbolMark { + len, + clip, + zindex, + name: name.unwrap_or_default(), + ..Default::default() + }, + } + } + + pub fn set_zindex(&mut self, zindex: Vec) { + self.inner.indices = Some(zindex_to_indices(zindex)); + } + + pub fn set_xy(&mut self, x: Vec, y: Vec) { + self.inner.x = EncodingValue::Array { values: x }; + self.inner.y = EncodingValue::Array { values: y }; + } + + pub fn set_size(&mut self, size: Vec) { + self.inner.size = EncodingValue::Array { values: size }; + } + + pub fn set_angle(&mut self, angle: Vec) { + self.inner.angle = EncodingValue::Array { values: angle }; + } + + pub fn set_stroke_width(&mut self, width: Option) { + self.inner.stroke_width = width; + } + + pub fn set_stroke( + &mut self, + color_values: JsValue, + indices: Vec, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.stroke = EncodingValue::Array { + values: decode_colors(color_values, indices, opacity)?, + }; + Ok(()) + } + + pub fn set_stroke_gradient( + &mut self, + values: JsValue, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.stroke = EncodingValue::Array { + values: decode_gradients(values, opacity, &mut self.inner.gradients)?, + }; + Ok(()) + } + + pub fn set_fill( + &mut self, + color_values: JsValue, + indices: Vec, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.fill = EncodingValue::Array { + values: decode_colors(color_values, indices, opacity)?, + }; + Ok(()) + } + + pub fn set_fill_gradient(&mut self, values: JsValue, opacity: Vec) -> Result<(), JsError> { + self.inner.fill = EncodingValue::Array { + values: decode_gradients(values, opacity, &mut self.inner.gradients)?, + }; + Ok(()) + } + + pub fn set_shape(&mut self, shape_values: JsValue, indices: Vec) -> Result<(), JsError> { + let shapes: Vec = shape_values.into_serde()?; + let shapes = shapes + .iter() + .map(|s| SymbolShape::from_vega_str(s)) + .collect::, AvengerError>>() + .map_err(|_| JsError::new("Failed to parse shapes"))?; + + self.inner.shapes = shapes; + self.inner.shape_index = EncodingValue::Array { values: indices }; + Ok(()) + } + + // TODO + // pub indices: Option>, +} diff --git a/avenger-vega-renderer/src/marks/text.rs b/avenger-vega-renderer/src/marks/text.rs new file mode 100644 index 0000000..4a42186 --- /dev/null +++ b/avenger-vega-renderer/src/marks/text.rs @@ -0,0 +1,160 @@ +use crate::marks::util::zindex_to_indices; +use avenger::marks::text::{ + FontStyleSpec, FontWeightSpec, TextAlignSpec, TextBaselineSpec, TextMark as RsTextMark, +}; +use avenger::marks::value::EncodingValue; +use gloo_utils::format::JsValueSerdeExt; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; + +#[wasm_bindgen] +pub struct TextMark { + inner: RsTextMark, +} + +impl TextMark { + pub fn build(self) -> RsTextMark { + self.inner + } +} + +#[wasm_bindgen] +impl TextMark { + #[wasm_bindgen(constructor)] + pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { + Self { + inner: RsTextMark { + len, + clip, + name: name.unwrap_or_default(), + zindex, + ..Default::default() + }, + } + } + + pub fn set_zindex(&mut self, zindex: Vec) { + self.inner.indices = Some(zindex_to_indices(zindex)); + } + + pub fn set_xy(&mut self, x: Vec, y: Vec) { + self.inner.x = EncodingValue::Array { values: x }; + self.inner.y = EncodingValue::Array { values: y }; + } + + pub fn set_angle(&mut self, angle: Vec) { + self.inner.angle = EncodingValue::Array { values: angle }; + } + + pub fn set_font_size(&mut self, font_size: Vec) { + self.inner.font_size = EncodingValue::Array { values: font_size }; + } + + pub fn set_font_limit(&mut self, limit: Vec) { + self.inner.limit = EncodingValue::Array { values: limit }; + } + + pub fn set_indices(&mut self, indices: Vec) { + self.inner.indices = Some(indices); + } + + pub fn set_text(&mut self, text: JsValue) -> Result<(), JsError> { + let text: Vec = text.into_serde()?; + self.inner.text = EncodingValue::Array { values: text }; + Ok(()) + } + + pub fn set_font(&mut self, font_values: JsValue, indices: Vec) -> Result<(), JsError> { + let font_values: Vec = font_values.into_serde()?; + let values = indices + .iter() + .map(|ind| font_values[*ind].clone()) + .collect::>(); + self.inner.font = EncodingValue::Array { values }; + Ok(()) + } + + pub fn set_align(&mut self, align_values: JsValue, indices: Vec) -> Result<(), JsError> { + let align_values: Vec = align_values.into_serde()?; + let values = indices + .iter() + .map(|ind| align_values[*ind].clone()) + .collect::>(); + self.inner.align = EncodingValue::Array { values }; + Ok(()) + } + + pub fn set_baseline( + &mut self, + baseline_values: JsValue, + indices: Vec, + ) -> Result<(), JsError> { + let baseline_values: Vec = baseline_values.into_serde()?; + let values = indices + .iter() + .map(|ind| baseline_values[*ind].clone()) + .collect::>(); + self.inner.baseline = EncodingValue::Array { values }; + Ok(()) + } + + pub fn set_font_weight( + &mut self, + weight_values: JsValue, + indices: Vec, + ) -> Result<(), JsError> { + let weight_values: Vec = weight_values.into_serde()?; + let values = indices + .iter() + .map(|ind| weight_values[*ind].clone()) + .collect::>(); + self.inner.font_weight = EncodingValue::Array { values }; + Ok(()) + } + + pub fn set_font_style( + &mut self, + style_values: JsValue, + indices: Vec, + ) -> Result<(), JsError> { + let style_values: Vec = style_values.into_serde()?; + let values = indices + .iter() + .map(|ind| style_values[*ind].clone()) + .collect::>(); + self.inner.font_style = EncodingValue::Array { values }; + Ok(()) + } + + pub fn set_color( + &mut self, + color_values: JsValue, + indices: Vec, + opacity: Vec, + ) -> Result<(), JsError> { + // Parse unique colors + let color_values: Vec = color_values.into_serde()?; + let unique_colors = color_values + .iter() + .map(|c| { + let Ok(c) = csscolorparser::parse(c) else { + return [0.0, 0.0, 0.0, 1.0]; + }; + [c.r as f32, c.g as f32, c.b as f32, c.a as f32] + }) + .collect::>(); + + // Combine with opacity to build + let colors = indices + .iter() + .zip(opacity) + .map(|(ind, opacity)| { + let [r, g, b, a] = unique_colors[*ind]; + [r as f32, g as f32, b as f32, a as f32 * opacity] + }) + .collect::>(); + + self.inner.color = EncodingValue::Array { values: colors }; + Ok(()) + } +} diff --git a/avenger-vega-renderer/src/marks/util.rs b/avenger-vega-renderer/src/marks/util.rs new file mode 100644 index 0000000..7876dcf --- /dev/null +++ b/avenger-vega-renderer/src/marks/util.rs @@ -0,0 +1,54 @@ +use avenger::marks::value::{ColorOrGradient, Gradient}; +use avenger_vega::error::AvengerVegaError; +use avenger_vega::marks::values::CssColorOrGradient; +use gloo_utils::format::JsValueSerdeExt; +use wasm_bindgen::{JsError, JsValue}; + +pub fn decode_gradients( + values: JsValue, + opacity: Vec, + gradients: &mut Vec, +) -> Result, JsError> { + let values: Vec = values.into_serde()?; + values + .iter() + .zip(opacity) + .map(|(grad, opacity)| grad.to_color_or_grad(opacity, gradients)) + .collect::, AvengerVegaError>>() + .map_err(|_| JsError::new("Failed to parse gradients")) +} + +pub fn decode_colors( + color_values: JsValue, + indices: Vec, + opacity: Vec, +) -> Result, JsError> { + // Parse unique colors + let color_values: Vec = color_values.into_serde()?; + let unique_strokes = color_values + .iter() + .map(|c| { + let Ok(c) = csscolorparser::parse(c) else { + return [0.0, 0.0, 0.0, 1.0]; + }; + [c.r as f32, c.g as f32, c.b as f32, c.a as f32] + }) + .collect::>(); + + // Combine with opacity to build + let colors = indices + .iter() + .zip(opacity) + .map(|(ind, opacity)| { + let [r, g, b, a] = unique_strokes[*ind as usize]; + ColorOrGradient::Color([r as f32, g as f32, b as f32, a as f32 * opacity]) + }) + .collect::>(); + Ok(colors) +} + +pub fn zindex_to_indices(zindex: Vec) -> Vec { + let mut indices: Vec = (0..zindex.len()).collect(); + indices.sort_by_key(|i| zindex[*i]); + indices +} diff --git a/avenger-vega-renderer/src/scene.rs b/avenger-vega-renderer/src/scene.rs new file mode 100644 index 0000000..39e8d15 --- /dev/null +++ b/avenger-vega-renderer/src/scene.rs @@ -0,0 +1,59 @@ +// use avenger::error::AvengerError; +// +// use avenger::marks::mark::SceneMark; +// use avenger::marks::symbol::SymbolShape; +// use avenger::marks::text::{FontStyleSpec, FontWeightSpec, TextAlignSpec, TextBaselineSpec}; +// use avenger::marks::value::{ColorOrGradient, EncodingValue, Gradient, StrokeCap}; +// use avenger::marks::{ +// rule::RuleMark as RsRuleMark, symbol::SymbolMark as RsSymbolMark, text::TextMark as RsTextMark, +// }; + +// use avenger_vega::error::AvengerVegaError; +// use avenger_vega::marks::values::{CssColorOrGradient, StrokeDashSpec}; +// use gloo_utils::format::JsValueSerdeExt; + +use crate::marks::group::GroupMark; +use avenger::scene_graph::SceneGraph as RsSceneGraph; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct SceneGraph { + inner: RsSceneGraph, +} + +#[wasm_bindgen] +impl SceneGraph { + #[wasm_bindgen(constructor)] + pub fn new(width: f32, height: f32, origin_x: f32, origin_y: f32) -> Self { + Self { + inner: RsSceneGraph { + width, + height, + origin: [origin_x, origin_y], + groups: Vec::new(), + }, + } + } + + pub fn add_group(&mut self, group: GroupMark) { + self.inner.groups.push(group.build()) + } +} + +impl SceneGraph { + pub fn build(self) -> RsSceneGraph { + self.inner + } + + pub fn width(&self) -> f32 { + self.inner.width + } + + pub fn height(&self) -> f32 { + self.inner.height + } + + pub fn origin(&self) -> [f32; 2] { + self.inner.origin + } +} From 3982e06593806df97e6ee6d5a2bd5a9cf98fbf06 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 20 Apr 2024 18:05:19 -0400 Subject: [PATCH 04/22] Split JS marks into separate files --- avenger-vega-renderer/js/index.js | 440 +---------------------- avenger-vega-renderer/js/marks/group.js | 33 ++ avenger-vega-renderer/js/marks/rule.js | 112 ++++++ avenger-vega-renderer/js/marks/symbol.js | 138 +++++++ avenger-vega-renderer/js/marks/text.js | 137 +++++++ avenger-vega-renderer/js/marks/util.js | 27 ++ 6 files changed, 449 insertions(+), 438 deletions(-) create mode 100644 avenger-vega-renderer/js/marks/group.js create mode 100644 avenger-vega-renderer/js/marks/rule.js create mode 100644 avenger-vega-renderer/js/marks/symbol.js create mode 100644 avenger-vega-renderer/js/marks/text.js create mode 100644 avenger-vega-renderer/js/marks/util.js diff --git a/avenger-vega-renderer/js/index.js b/avenger-vega-renderer/js/index.js index 2fe0395..777fa4c 100644 --- a/avenger-vega-renderer/js/index.js +++ b/avenger-vega-renderer/js/index.js @@ -1,6 +1,7 @@ -import { AvengerCanvas, SceneGraph, GroupMark, SymbolMark, RuleMark, TextMark, scene_graph_to_png } from "../pkg/avenger_wasm.js"; +import { AvengerCanvas, SceneGraph, scene_graph_to_png } from "../pkg/avenger_wasm.js"; import { Renderer, CanvasHandler, domClear, domChild } from 'vega-scenegraph'; import { inherits } from 'vega-util'; +import { importGroup } from "./marks/group.js" function devicePixelRatio() { @@ -136,443 +137,6 @@ function importScenegraph(vegaSceneGroups, width, height, origin) { return sceneGraph; } -function importGroup(vegaGroup) { - const groupMark = new GroupMark( - vegaGroup.x, vegaGroup.y, vegaGroup.name, vegaGroup.width, vegaGroup.height - ); - - for (const vegaMark of vegaGroup.items) { - switch (vegaMark.marktype) { - case "symbol": - groupMark.add_symbol_mark(importSymbol(vegaMark)); - break; - case "rule": - groupMark.add_rule_mark(importRule(vegaMark)); - break; - case "text": - groupMark.add_text_mark(importText(vegaMark)); - break; - case "group": - for (const groupItem of vegaMark.items) { - groupMark.add_group_mark(importGroup(groupItem)); - } - break; - default: - console.log("Unsupported mark type: " + vegaMark.marktype) - } - } - - return groupMark; -} - -function importSymbol(vegaSymbolMark, force_clip) { - const items = vegaSymbolMark.items; - const len = items.length; - - const symbolMark = new SymbolMark( - len, vegaSymbolMark.clip || force_clip, vegaSymbolMark.name, vegaSymbolMark.zindex - ); - - // Handle empty mark - if (len === 0) { - return symbolMark; - } - - const firstItem = items[0]; - const firstShape = firstItem.shape ?? "circle"; - - if (firstShape === "stroke") { - // TODO: Handle line legends - return symbolMark - } - - // Only include stroke_width if there is a stroke color - const firstHasStroke = firstItem.stroke != null; - let strokeWidth; - if (firstHasStroke) { - strokeWidth = firstItem.strokeWidth ?? 1; - } - symbolMark.set_stroke_width(strokeWidth); - - // Semi-required values get initialized - const x = new Float32Array(len).fill(0); - const y = new Float32Array(len).fill(0); - - const fill = new Array(len); - let anyFill = false; - let fillIsGradient = firstItem.fill != null && typeof firstItem.fill === "object"; - - const size = new Float32Array(len).fill(20); - let anySize = false; - - const stroke = new Array(len); - let anyStroke = false; - let strokeIsGradient = firstItem.stroke != null && typeof firstItem.stroke === "object"; - - const angle = new Float32Array(len).fill(0); - let anyAngle = false; - - const zindex = new Float32Array(len).fill(0); - let anyZindex = false; - - const fillOpacity = new Float32Array(len).fill(1); - const strokeOpacity = new Float32Array(len).fill(1); - - const shapes = new Array(len); - let anyShape = false; - - items.forEach((item, i) => { - x[i] = item.x ?? 0; - y[i] = item.y ?? 0; - - const baseOpacity = item.opacity ?? 1; - fillOpacity[i] = (item.fillOpacity ?? 1) * baseOpacity; - strokeOpacity[i] = (item.strokeOpacity ?? 1) * baseOpacity; - - if (item.fill != null) { - fill[i] = item.fill; - anyFill ||= true; - } - - if (item.size != null) { - size[i] = item.size; - anySize = true; - } - - if (item.stroke != null) { - stroke[i] = item.stroke; - anyStroke ||= true; - } - - if (item.angle != null) { - angle[i] = item.angle; - anyAngle ||= true; - } - - if (item.zindex != null) { - zindex[i] = item.zindex; - anyZindex ||= true; - } - - if (item.shape != null) { - shapes[i] = item.shape; - anyShape ||= true; - } - }) - - symbolMark.set_xy(x, y); - - if (anyFill) { - if (fillIsGradient) { - symbolMark.set_fill_gradient(fill, fillOpacity); - } else { - const encoded = encodeStringArray(fill); - symbolMark.set_fill(encoded.values, encoded.indices, fillOpacity); - } - } - - if (anySize) { - symbolMark.set_size(size); - } - - if (anyStroke) { - if (strokeIsGradient) { - symbolMark.set_stroke_gradient(stroke, strokeOpacity); - } else { - const encoded = encodeStringArray(stroke); - symbolMark.set_stroke(encoded.values, encoded.indices, strokeOpacity); - } - } - - if (anyAngle) { - symbolMark.set_angle(angle); - } - - if (anyZindex) { - symbolMark.set_zindex(zindex); - } - - if (anyShape) { - const encoded = encodeStringArray(shapes); - console.log() - symbolMark.set_shape(encoded.values, encoded.indices); - } - - return symbolMark; -} - -function importRule(vegaRuleMark, forceClip) { - const items = vegaRuleMark.items; - const len = items.length; - - const ruleMark = new RuleMark( - len, vegaRuleMark.clip || forceClip, vegaRuleMark.name, vegaRuleMark.zindex - ); - if (len === 0) { - return ruleMark; - } - - const firstItem = items[0]; - - const x0 = new Float32Array(len).fill(0); - const y0 = new Float32Array(len).fill(0); - const x1 = new Float32Array(len).fill(0); - const y1 = new Float32Array(len).fill(0); - - const strokeWidth = new Float32Array(len); - let anyStrokeWidth = false; - - const strokeOpacity = new Float32Array(len).fill(1); - - const stroke = new Array(len); - let anyStroke = false; - let strokeIsGradient = firstItem.stroke != null && typeof firstItem.stroke === "object"; - - const strokeCap = new Array(len); - let anyStrokeCap = false; - - const strokeDash = new Array(len); - let anyStrokeDash = false; - - const zindex = new Float32Array(len).fill(0); - let anyZindex = false; - - items.forEach((item, i) => { - if (item.x != null) { - x0[i] = item.x; - } - if (item.y != null) { - y0[i] = item.y; - } - if (item.x2 != null) { - x1[i] = item.x2; - } else { - x1[i] = x0[i]; - } - if (item.y2 != null) { - y1[i] = item.y2; - } else { - y1[i] = y0[i]; - } - if (item.strokeWidth != null) { - strokeWidth[i] = item.strokeWidth; - anyStrokeWidth ||= true; - } - strokeOpacity[i] = (item.strokeOpacity ?? 1) * (item.opacity ?? 1); - - if (item.stroke != null) { - stroke[i] = item.stroke; - anyStroke ||= true; - } - - if (item.strokeCap != null) { - strokeCap[i] = item.strokeCap; - anyStrokeCap ||= true; - } - - if (item.strokeDash != null) { - strokeDash[i] = item.strokeDash; - anyStrokeDash ||= true; - } - - if (item.zindex != null) { - zindex[i] = item.zindex; - anyZindex ||= true; - } - }) - - ruleMark.set_xy(x0, y0, x1, y1); - - if (anyStrokeWidth) { - ruleMark.set_stroke_width(strokeWidth); - } - - if (anyStroke) { - if (strokeIsGradient) { - ruleMark.set_stroke_gradient(stroke, strokeOpacity); - } else { - const encoded = encodeStringArray(stroke); - ruleMark.set_stroke(encoded.values, encoded.indices, strokeOpacity); - } - } - - if (anyStrokeCap) { - ruleMark.set_stroke_cap(strokeCap); - } - - if (anyStrokeDash) { - ruleMark.set_stroke_dash(strokeDash); - } - - if (anyZindex) { - ruleMark.set_zindex(zindex); - } - - return ruleMark; -} - -function importText(vegaTextMark) { - const len = vegaTextMark.items.length; - const textMark = new TextMark(len, vegaTextMark.clip, vegaTextMark.name); - - // semi-required columns where we will pass the full array no matter what - const x = new Float32Array(len).fill(0); - const y = new Float32Array(len).fill(0); - const text = new Array(len).fill(""); - - // Optional properties where we will only pass the full array if any value is specified - const fontSize = new Float32Array(len); - let anyFontSize = false; - - const angle = new Float32Array(len); - let anyAngle = false; - - const limit = new Float32Array(len); - let anyLimit = false; - - // String properties that typically have lots of duplicates, so - // unique values and indices. - const font = new Array(len); - let anyFont = false; - - const fill = new Array(len); - const fillOpacity = new Float32Array(len).fill(1); - let anyFill = false; - - const baseline = new Array(len); - let anyBaseline = false; - - const align = new Array(len); - let anyAlign = false; - - const fontWeight = new Array(len); - let anyFontWeight = false; - - const items = vegaTextMark.items; - items.forEach((item, i) => { - // Semi-required properties have been initialized - if (item.x != null) { - x[i] = item.x; - } - - if (item.x != null) { - y[i] = item.y; - } - - if (item.text != null) { - text[i] = item.text; - } - - // Optional properties have not been initialized, we need to track if any are specified - if (item.fontSize != null) { - fontSize[i] = item.fontSize; - anyFontSize ||= true; - } - - if (item.angle != null) { - angle[i] = item.angle; - anyAngle ||= true; - } - - if (item.limit != null) { - limit[i] = item.limit; - anyLimit ||= true; - } - - if (item.fill != null) { - fill[i] = item.fill; - anyFill ||= true; - } - fillOpacity[i] = (item.fillOpacity ?? 1) * (item.opacity ?? 1); - - if (item.font != null) { - font[i] = item.font; - anyFont ||= true; - } - - if (item.baseline != null) { - baseline[i] = item.baseline; - anyBaseline ||= true; - } - - if (item.align != null) { - align[i] = item.align; - anyAlign ||= true; - } - - if (item.fontWeight != null) { - fontWeight[i] = item.fontWeight; - anyFontWeight ||= true; - } - }) - - // Set semi-required properties as full arrays - textMark.set_xy(x, y); - textMark.set_text(text); - - // Set optional properties if any were defined - if (anyFontSize) { - textMark.set_font_size(fontSize) - } - if (anyAngle) { - textMark.set_angle(angle); - } - if (anyLimit) { - textMark.set_font_limit(limit); - } - - // String columns to pass as encoded unique values + indices - if (anyFill) { - const encoded = encodeStringArray(fill); - textMark.set_color(encoded.values, encoded.indices, fillOpacity); - } - if (anyFont) { - const encoded = encodeStringArray(font); - textMark.set_font(encoded.values, encoded.indices); - } - if (anyBaseline) { - const encoded = encodeStringArray(baseline); - textMark.set_baseline(encoded.values, encoded.indices); - } - if (anyAlign) { - const encoded = encodeStringArray(align); - textMark.set_align(encoded.values, encoded.indices); - } - if (anyFontWeight) { - const encoded = encodeStringArray(fontWeight); - textMark.set_font_weight(encoded.values, encoded.indices); - } - - return textMark; -} - -function encodeStringArray(originalArray) { - const uniqueStringsMap = new Map(); - let index = 0; - - // Populate the map with unique strings and their indices - for (const str of originalArray) { - if (!uniqueStringsMap.has(str)) { - uniqueStringsMap.set(str, index++); - } - } - - // Generate the array of unique strings. - // Note, Maps preserve the insertion order of their elements - const uniqueStringsArray = Array.from(uniqueStringsMap.keys()); - - // Build index array - let indices = new Uint32Array(originalArray.length); - originalArray.forEach((str, i) => { - indices[i] = uniqueStringsMap.get(str); - }); - - return { - values: uniqueStringsArray, - indices, - }; -} - export function registerVegaRenderer(renderModule) { // Call with renderModule function from 'vega-scenegraph' renderModule('avenger', { diff --git a/avenger-vega-renderer/js/marks/group.js b/avenger-vega-renderer/js/marks/group.js new file mode 100644 index 0000000..f710fc2 --- /dev/null +++ b/avenger-vega-renderer/js/marks/group.js @@ -0,0 +1,33 @@ +import {GroupMark} from "../../pkg/avenger_wasm.js"; +import { importSymbol } from "./symbol.js" +import { importRule } from "./rule.js"; +import {importText} from "./text.js"; + +export function importGroup(vegaGroup) { + const groupMark = new GroupMark( + vegaGroup.x, vegaGroup.y, vegaGroup.name, vegaGroup.width, vegaGroup.height + ); + + for (const vegaMark of vegaGroup.items) { + switch (vegaMark.marktype) { + case "symbol": + groupMark.add_symbol_mark(importSymbol(vegaMark)); + break; + case "rule": + groupMark.add_rule_mark(importRule(vegaMark)); + break; + case "text": + groupMark.add_text_mark(importText(vegaMark)); + break; + case "group": + for (const groupItem of vegaMark.items) { + groupMark.add_group_mark(importGroup(groupItem)); + } + break; + default: + console.log("Unsupported mark type: " + vegaMark.marktype) + } + } + + return groupMark; +} diff --git a/avenger-vega-renderer/js/marks/rule.js b/avenger-vega-renderer/js/marks/rule.js new file mode 100644 index 0000000..f48a5c4 --- /dev/null +++ b/avenger-vega-renderer/js/marks/rule.js @@ -0,0 +1,112 @@ +import {RuleMark} from "../../pkg/avenger_wasm.js"; +import {encodeStringArray} from "./util.js"; + +export function importRule(vegaRuleMark, forceClip) { + const items = vegaRuleMark.items; + const len = items.length; + + const ruleMark = new RuleMark( + len, vegaRuleMark.clip || forceClip, vegaRuleMark.name, vegaRuleMark.zindex + ); + if (len === 0) { + return ruleMark; + } + + const firstItem = items[0]; + + const x0 = new Float32Array(len).fill(0); + const y0 = new Float32Array(len).fill(0); + const x1 = new Float32Array(len).fill(0); + const y1 = new Float32Array(len).fill(0); + + const strokeWidth = new Float32Array(len); + let anyStrokeWidth = false; + + const strokeOpacity = new Float32Array(len).fill(1); + + const stroke = new Array(len); + let anyStroke = false; + let strokeIsGradient = firstItem.stroke != null && typeof firstItem.stroke === "object"; + + const strokeCap = new Array(len); + let anyStrokeCap = false; + + const strokeDash = new Array(len); + let anyStrokeDash = false; + + const zindex = new Float32Array(len).fill(0); + let anyZindex = false; + + items.forEach((item, i) => { + if (item.x != null) { + x0[i] = item.x; + } + if (item.y != null) { + y0[i] = item.y; + } + if (item.x2 != null) { + x1[i] = item.x2; + } else { + x1[i] = x0[i]; + } + if (item.y2 != null) { + y1[i] = item.y2; + } else { + y1[i] = y0[i]; + } + if (item.strokeWidth != null) { + strokeWidth[i] = item.strokeWidth; + anyStrokeWidth ||= true; + } + strokeOpacity[i] = (item.strokeOpacity ?? 1) * (item.opacity ?? 1); + + if (item.stroke != null) { + stroke[i] = item.stroke; + anyStroke ||= true; + } + + if (item.strokeCap != null) { + strokeCap[i] = item.strokeCap; + anyStrokeCap ||= true; + } + + if (item.strokeDash != null) { + strokeDash[i] = item.strokeDash; + anyStrokeDash ||= true; + } + + if (item.zindex != null) { + zindex[i] = item.zindex; + anyZindex ||= true; + } + }) + + ruleMark.set_xy(x0, y0, x1, y1); + + if (anyStrokeWidth) { + ruleMark.set_stroke_width(strokeWidth); + } + + if (anyStroke) { + if (strokeIsGradient) { + ruleMark.set_stroke_gradient(stroke, strokeOpacity); + } else { + const encoded = encodeStringArray(stroke); + ruleMark.set_stroke(encoded.values, encoded.indices, strokeOpacity); + } + } + + if (anyStrokeCap) { + ruleMark.set_stroke_cap(strokeCap); + } + + if (anyStrokeDash) { + ruleMark.set_stroke_dash(strokeDash); + } + + if (anyZindex) { + ruleMark.set_zindex(zindex); + } + + return ruleMark; +} diff --git a/avenger-vega-renderer/js/marks/symbol.js b/avenger-vega-renderer/js/marks/symbol.js new file mode 100644 index 0000000..f3d127a --- /dev/null +++ b/avenger-vega-renderer/js/marks/symbol.js @@ -0,0 +1,138 @@ +import {SymbolMark} from "../../pkg/avenger_wasm.js"; +import {encodeStringArray} from "./util.js"; + +export function importSymbol(vegaSymbolMark, force_clip) { + const items = vegaSymbolMark.items; + const len = items.length; + + const symbolMark = new SymbolMark( + len, vegaSymbolMark.clip || force_clip, vegaSymbolMark.name, vegaSymbolMark.zindex + ); + + // Handle empty mark + if (len === 0) { + return symbolMark; + } + + const firstItem = items[0]; + const firstShape = firstItem.shape ?? "circle"; + + if (firstShape === "stroke") { + // TODO: Handle line legends + return symbolMark + } + + // Only include stroke_width if there is a stroke color + const firstHasStroke = firstItem.stroke != null; + let strokeWidth; + if (firstHasStroke) { + strokeWidth = firstItem.strokeWidth ?? 1; + } + symbolMark.set_stroke_width(strokeWidth); + + // Semi-required values get initialized + const x = new Float32Array(len).fill(0); + const y = new Float32Array(len).fill(0); + + const fill = new Array(len); + let anyFill = false; + let fillIsGradient = firstItem.fill != null && typeof firstItem.fill === "object"; + + const size = new Float32Array(len).fill(20); + let anySize = false; + + const stroke = new Array(len); + let anyStroke = false; + let strokeIsGradient = firstItem.stroke != null && typeof firstItem.stroke === "object"; + + const angle = new Float32Array(len).fill(0); + let anyAngle = false; + + const zindex = new Float32Array(len).fill(0); + let anyZindex = false; + + const fillOpacity = new Float32Array(len).fill(1); + const strokeOpacity = new Float32Array(len).fill(1); + + const shapes = new Array(len); + let anyShape = false; + + items.forEach((item, i) => { + x[i] = item.x ?? 0; + y[i] = item.y ?? 0; + + const baseOpacity = item.opacity ?? 1; + fillOpacity[i] = (item.fillOpacity ?? 1) * baseOpacity; + strokeOpacity[i] = (item.strokeOpacity ?? 1) * baseOpacity; + + if (item.fill != null) { + fill[i] = item.fill; + anyFill ||= true; + } + + if (item.size != null) { + size[i] = item.size; + anySize = true; + } + + if (item.stroke != null) { + stroke[i] = item.stroke; + anyStroke ||= true; + } + + if (item.angle != null) { + angle[i] = item.angle; + anyAngle ||= true; + } + + if (item.zindex != null) { + zindex[i] = item.zindex; + anyZindex ||= true; + } + + if (item.shape != null) { + shapes[i] = item.shape; + anyShape ||= true; + } + }) + + symbolMark.set_xy(x, y); + + if (anyFill) { + if (fillIsGradient) { + symbolMark.set_fill_gradient(fill, fillOpacity); + } else { + const encoded = encodeStringArray(fill); + symbolMark.set_fill(encoded.values, encoded.indices, fillOpacity); + } + } + + if (anySize) { + symbolMark.set_size(size); + } + + if (anyStroke) { + if (strokeIsGradient) { + symbolMark.set_stroke_gradient(stroke, strokeOpacity); + } else { + const encoded = encodeStringArray(stroke); + symbolMark.set_stroke(encoded.values, encoded.indices, strokeOpacity); + } + } + + if (anyAngle) { + symbolMark.set_angle(angle); + } + + if (anyZindex) { + symbolMark.set_zindex(zindex); + } + + if (anyShape) { + const encoded = encodeStringArray(shapes); + console.log() + symbolMark.set_shape(encoded.values, encoded.indices); + } + + return symbolMark; +} diff --git a/avenger-vega-renderer/js/marks/text.js b/avenger-vega-renderer/js/marks/text.js new file mode 100644 index 0000000..81fe414 --- /dev/null +++ b/avenger-vega-renderer/js/marks/text.js @@ -0,0 +1,137 @@ +import {TextMark} from "../../pkg/avenger_wasm.js"; +import {encodeStringArray} from "./util.js"; + +export function importText(vegaTextMark) { + const len = vegaTextMark.items.length; + const textMark = new TextMark(len, vegaTextMark.clip, vegaTextMark.name); + + // semi-required columns where we will pass the full array no matter what + const x = new Float32Array(len).fill(0); + const y = new Float32Array(len).fill(0); + const text = new Array(len).fill(""); + + // Optional properties where we will only pass the full array if any value is specified + const fontSize = new Float32Array(len); + let anyFontSize = false; + + const angle = new Float32Array(len); + let anyAngle = false; + + const limit = new Float32Array(len); + let anyLimit = false; + + // String properties that typically have lots of duplicates, so + // unique values and indices. + const font = new Array(len); + let anyFont = false; + + const fill = new Array(len); + const fillOpacity = new Float32Array(len).fill(1); + let anyFill = false; + + const baseline = new Array(len); + let anyBaseline = false; + + const align = new Array(len); + let anyAlign = false; + + const fontWeight = new Array(len); + let anyFontWeight = false; + + const items = vegaTextMark.items; + items.forEach((item, i) => { + // Semi-required properties have been initialized + if (item.x != null) { + x[i] = item.x; + } + + if (item.x != null) { + y[i] = item.y; + } + + if (item.text != null) { + text[i] = item.text; + } + + // Optional properties have not been initialized, we need to track if any are specified + if (item.fontSize != null) { + fontSize[i] = item.fontSize; + anyFontSize ||= true; + } + + if (item.angle != null) { + angle[i] = item.angle; + anyAngle ||= true; + } + + if (item.limit != null) { + limit[i] = item.limit; + anyLimit ||= true; + } + + if (item.fill != null) { + fill[i] = item.fill; + anyFill ||= true; + } + fillOpacity[i] = (item.fillOpacity ?? 1) * (item.opacity ?? 1); + + if (item.font != null) { + font[i] = item.font; + anyFont ||= true; + } + + if (item.baseline != null) { + baseline[i] = item.baseline; + anyBaseline ||= true; + } + + if (item.align != null) { + align[i] = item.align; + anyAlign ||= true; + } + + if (item.fontWeight != null) { + fontWeight[i] = item.fontWeight; + anyFontWeight ||= true; + } + }) + + // Set semi-required properties as full arrays + textMark.set_xy(x, y); + textMark.set_text(text); + + // Set optional properties if any were defined + if (anyFontSize) { + textMark.set_font_size(fontSize) + } + if (anyAngle) { + textMark.set_angle(angle); + } + if (anyLimit) { + textMark.set_font_limit(limit); + } + + // String columns to pass as encoded unique values + indices + if (anyFill) { + const encoded = encodeStringArray(fill); + textMark.set_color(encoded.values, encoded.indices, fillOpacity); + } + if (anyFont) { + const encoded = encodeStringArray(font); + textMark.set_font(encoded.values, encoded.indices); + } + if (anyBaseline) { + const encoded = encodeStringArray(baseline); + textMark.set_baseline(encoded.values, encoded.indices); + } + if (anyAlign) { + const encoded = encodeStringArray(align); + textMark.set_align(encoded.values, encoded.indices); + } + if (anyFontWeight) { + const encoded = encodeStringArray(fontWeight); + textMark.set_font_weight(encoded.values, encoded.indices); + } + + return textMark; +} diff --git a/avenger-vega-renderer/js/marks/util.js b/avenger-vega-renderer/js/marks/util.js new file mode 100644 index 0000000..81be589 --- /dev/null +++ b/avenger-vega-renderer/js/marks/util.js @@ -0,0 +1,27 @@ + +export function encodeStringArray(originalArray) { + const uniqueStringsMap = new Map(); + let index = 0; + + // Populate the map with unique strings and their indices + for (const str of originalArray) { + if (!uniqueStringsMap.has(str)) { + uniqueStringsMap.set(str, index++); + } + } + + // Generate the array of unique strings. + // Note, Maps preserve the insertion order of their elements + const uniqueStringsArray = Array.from(uniqueStringsMap.keys()); + + // Build index array + let indices = new Uint32Array(originalArray.length); + originalArray.forEach((str, i) => { + indices[i] = uniqueStringsMap.get(str); + }); + + return { + values: uniqueStringsArray, + indices, + }; +} From 4491dc40cd7ef384236f9bfe048f5d9c26a0ed26 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 20 Apr 2024 19:39:48 -0400 Subject: [PATCH 05/22] Initial TypeScript checking with JSDoc comments --- avenger-vega-renderer/js/index.js | 13 +++-- avenger-vega-renderer/js/marks/group.js | 49 ++++++++++++++--- avenger-vega-renderer/js/marks/rule.js | 40 ++++++++++++-- avenger-vega-renderer/js/marks/symbol.js | 45 +++++++++++++--- avenger-vega-renderer/js/marks/text.js | 66 +++++++++++++++++++---- avenger-vega-renderer/js/marks/util.js | 22 +++++--- avenger-vega-renderer/package-lock.json | 16 ++++++ avenger-vega-renderer/package.json | 5 +- avenger-vega-renderer/src/marks/rule.rs | 19 +++++++ avenger-vega-renderer/src/marks/symbol.rs | 30 +++++++++-- avenger-vega-renderer/src/marks/text.rs | 35 ++++++++++++ avenger-vega-renderer/tsconfig.json | 15 ++++++ 12 files changed, 312 insertions(+), 43 deletions(-) create mode 100644 avenger-vega-renderer/tsconfig.json diff --git a/avenger-vega-renderer/js/index.js b/avenger-vega-renderer/js/index.js index 777fa4c..eaefc32 100644 --- a/avenger-vega-renderer/js/index.js +++ b/avenger-vega-renderer/js/index.js @@ -129,10 +129,17 @@ inherits(AvengerHandler, CanvasHandler, { } }); -function importScenegraph(vegaSceneGroups, width, height, origin) { +/** + * @param {GroupMarkSpec} groupMark + * @param {number} width + * @param {number} height + * @param {[number, number]} origin + * @returns {SceneGraph} + */ +function importScenegraph(groupMark, width, height, origin) { const sceneGraph = new SceneGraph(width, height, origin[0], origin[1]); - for (const vegaGroup of vegaSceneGroups.items) { - sceneGraph.add_group(importGroup(vegaGroup)); + for (const vegaGroup of groupMark.items) { + sceneGraph.add_group(importGroup(vegaGroup, groupMark.name)); } return sceneGraph; } diff --git a/avenger-vega-renderer/js/marks/group.js b/avenger-vega-renderer/js/marks/group.js index f710fc2..7ca81e4 100644 --- a/avenger-vega-renderer/js/marks/group.js +++ b/avenger-vega-renderer/js/marks/group.js @@ -3,29 +3,62 @@ import { importSymbol } from "./symbol.js" import { importRule } from "./rule.js"; import {importText} from "./text.js"; -export function importGroup(vegaGroup) { +/** + * @typedef {import('./symbol.js').SymbolMarkSpec} SymbolMarkSpec + * @typedef {import('./text.js').TextMarkSpec} TextMarkSpec + * @typedef {import('./rule.js').RuleMarkSpec} RuleMarkSpec + * + * + * @typedef {Object} GroupItemSpec + * @property {"group"} marktype + * @property {boolean} clip + * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec)[]} items + * @property {string} fill + * @property {string} stroke + * @property {number} x + * @property {number} y + * @property {number} width + * @property {number} height + */ + +/** + * @typedef {Object} GroupMarkSpec + * @property {"group"} marktype + * @property {boolean} clip + * @property {boolean} interactive + * @property {GroupItemSpec[]} items + * @property {string} name + * @property {string} role + * @property {number} zindex + */ + +/** + * @param {GroupItemSpec} vegaGroup + * @param {string} name + * @returns {GroupMark} + */ +export function importGroup(vegaGroup, name) { const groupMark = new GroupMark( - vegaGroup.x, vegaGroup.y, vegaGroup.name, vegaGroup.width, vegaGroup.height + vegaGroup.x, vegaGroup.y, name, vegaGroup.width, vegaGroup.height ); + const forceClip = false; for (const vegaMark of vegaGroup.items) { switch (vegaMark.marktype) { case "symbol": - groupMark.add_symbol_mark(importSymbol(vegaMark)); + groupMark.add_symbol_mark(importSymbol(vegaMark, forceClip)); break; case "rule": - groupMark.add_rule_mark(importRule(vegaMark)); + groupMark.add_rule_mark(importRule(vegaMark, forceClip)); break; case "text": - groupMark.add_text_mark(importText(vegaMark)); + groupMark.add_text_mark(importText(vegaMark, forceClip)); break; case "group": for (const groupItem of vegaMark.items) { - groupMark.add_group_mark(importGroup(groupItem)); + groupMark.add_group_mark(importGroup(groupItem, vegaMark.name)); } break; - default: - console.log("Unsupported mark type: " + vegaMark.marktype) } } diff --git a/avenger-vega-renderer/js/marks/rule.js b/avenger-vega-renderer/js/marks/rule.js index f48a5c4..ff17bd9 100644 --- a/avenger-vega-renderer/js/marks/rule.js +++ b/avenger-vega-renderer/js/marks/rule.js @@ -1,6 +1,38 @@ import {RuleMark} from "../../pkg/avenger_wasm.js"; -import {encodeStringArray} from "./util.js"; - +import {encodeSimpleArray} from "./util.js"; + + +/** + * Represents the style and configuration of a graphic element. + * @typedef {Object} RuleItem + * @property {number} strokeWidth + * @property {string} stroke + * @property {string|number[]} strokeDash + * @property {number} x + * @property {number} x2 + * @property {number} y + * @property {number} y2 + * @property {number} opacity + * @property {number} strokeOpacity + * @property {string} strokeCap + * @property {number} zindex + */ + +/** + * Represents a graphical object configuration. + * @typedef {Object} RuleMarkSpec + * @property {"rule"} marktype + * @property {boolean} clip + * @property {RuleItem[]} items + * @property {string} name + * @property {number} zindex + */ + +/** + * @param {RuleMarkSpec} vegaRuleMark + * @param {boolean} forceClip + * @returns {RuleMark} + */ export function importRule(vegaRuleMark, forceClip) { const items = vegaRuleMark.items; const len = items.length; @@ -34,7 +66,7 @@ export function importRule(vegaRuleMark, forceClip) { const strokeDash = new Array(len); let anyStrokeDash = false; - const zindex = new Float32Array(len).fill(0); + const zindex = new Int32Array(len).fill(0); let anyZindex = false; items.forEach((item, i) => { @@ -91,7 +123,7 @@ export function importRule(vegaRuleMark, forceClip) { if (strokeIsGradient) { ruleMark.set_stroke_gradient(stroke, strokeOpacity); } else { - const encoded = encodeStringArray(stroke); + const encoded = encodeSimpleArray(stroke); ruleMark.set_stroke(encoded.values, encoded.indices, strokeOpacity); } } diff --git a/avenger-vega-renderer/js/marks/symbol.js b/avenger-vega-renderer/js/marks/symbol.js index f3d127a..0f1749d 100644 --- a/avenger-vega-renderer/js/marks/symbol.js +++ b/avenger-vega-renderer/js/marks/symbol.js @@ -1,6 +1,39 @@ import {SymbolMark} from "../../pkg/avenger_wasm.js"; -import {encodeStringArray} from "./util.js"; - +import {encodeSimpleArray} from "./util.js"; + + +/** + * @typedef {Object} SymbolItem + * @property {number} strokeWidth + * @property {string|object} fill + * @property {string|object} stroke + * @property {number} x + * @property {number} y + * @property {number} size + * @property {number} opacity + * @property {number} strokeOpacity + * @property {number} fillOpacity + * @property {string} shape + * @property {number} angle + * @property {number} zindex + */ + +/** + * @typedef {Object} SymbolMarkSpec + * @property {"symbol"} marktype + * @property {boolean} clip + * @property {boolean} interactive + * @property {SymbolItem[]} items + * @property {string} name + * @property {string} role + * @property {number} zindex + */ + +/** + * @param {SymbolMarkSpec} vegaSymbolMark + * @param {boolean} force_clip + * @returns {SymbolMark} + */ export function importSymbol(vegaSymbolMark, force_clip) { const items = vegaSymbolMark.items; const len = items.length; @@ -48,7 +81,7 @@ export function importSymbol(vegaSymbolMark, force_clip) { const angle = new Float32Array(len).fill(0); let anyAngle = false; - const zindex = new Float32Array(len).fill(0); + const zindex = new Int32Array(len).fill(0); let anyZindex = false; const fillOpacity = new Float32Array(len).fill(1); @@ -102,7 +135,7 @@ export function importSymbol(vegaSymbolMark, force_clip) { if (fillIsGradient) { symbolMark.set_fill_gradient(fill, fillOpacity); } else { - const encoded = encodeStringArray(fill); + const encoded = encodeSimpleArray(fill); symbolMark.set_fill(encoded.values, encoded.indices, fillOpacity); } } @@ -115,7 +148,7 @@ export function importSymbol(vegaSymbolMark, force_clip) { if (strokeIsGradient) { symbolMark.set_stroke_gradient(stroke, strokeOpacity); } else { - const encoded = encodeStringArray(stroke); + const encoded = encodeSimpleArray(stroke); symbolMark.set_stroke(encoded.values, encoded.indices, strokeOpacity); } } @@ -129,7 +162,7 @@ export function importSymbol(vegaSymbolMark, force_clip) { } if (anyShape) { - const encoded = encodeStringArray(shapes); + const encoded = encodeSimpleArray(shapes); console.log() symbolMark.set_shape(encoded.values, encoded.indices); } diff --git a/avenger-vega-renderer/js/marks/text.js b/avenger-vega-renderer/js/marks/text.js index 81fe414..a487546 100644 --- a/avenger-vega-renderer/js/marks/text.js +++ b/avenger-vega-renderer/js/marks/text.js @@ -1,9 +1,45 @@ import {TextMark} from "../../pkg/avenger_wasm.js"; -import {encodeStringArray} from "./util.js"; - -export function importText(vegaTextMark) { +import {encodeSimpleArray} from "./util.js"; + +/** + * @typedef {Object} TextItem + * @property {string} text + * @property {string} font + * @property {number} fontSize + * @property {string} fill + * @property {number} x + * @property {number} y + * @property {number} angle + * @property {number} limit + * @property {number} opacity + * @property {number} fillOpacity + * @property {"alphabetic"|"top"|"middle"|"bottom"|"line-top"|"line-bottom"} baseline + * @property {"left"|"center"|"right"} align + * @property {number|"normal"|"bold"} fontWeight + * @property {number} zindex + */ + +/** + * @typedef {Object} TextMarkSpec + * @property {"text"} marktype + * @property {boolean} clip + * @property {boolean} interactive + * @property {TextItem[]} items + * @property {string} name + * @property {string} role + * @property {number} zindex + */ + +/** + * @param {TextMarkSpec} vegaTextMark + * @param {boolean} force_clip + * @returns {TextMark} + */ +export function importText(vegaTextMark, force_clip) { const len = vegaTextMark.items.length; - const textMark = new TextMark(len, vegaTextMark.clip, vegaTextMark.name); + const textMark = new TextMark( + len, vegaTextMark.clip || force_clip, vegaTextMark.name, vegaTextMark.zindex + ); // semi-required columns where we will pass the full array no matter what const x = new Float32Array(len).fill(0); @@ -38,6 +74,9 @@ export function importText(vegaTextMark) { const fontWeight = new Array(len); let anyFontWeight = false; + const zindex = new Int32Array(len).fill(0); + let anyZindex = false; + const items = vegaTextMark.items; items.forEach((item, i) => { // Semi-required properties have been initialized @@ -94,6 +133,11 @@ export function importText(vegaTextMark) { fontWeight[i] = item.fontWeight; anyFontWeight ||= true; } + + if (item.zindex != null) { + zindex[i] = item.zindex; + anyZindex ||= true; + } }) // Set semi-required properties as full arrays @@ -113,25 +157,27 @@ export function importText(vegaTextMark) { // String columns to pass as encoded unique values + indices if (anyFill) { - const encoded = encodeStringArray(fill); + const encoded = encodeSimpleArray(fill); textMark.set_color(encoded.values, encoded.indices, fillOpacity); } if (anyFont) { - const encoded = encodeStringArray(font); + const encoded = encodeSimpleArray(font); textMark.set_font(encoded.values, encoded.indices); } if (anyBaseline) { - const encoded = encodeStringArray(baseline); + const encoded = encodeSimpleArray(baseline); textMark.set_baseline(encoded.values, encoded.indices); } if (anyAlign) { - const encoded = encodeStringArray(align); + const encoded = encodeSimpleArray(align); textMark.set_align(encoded.values, encoded.indices); } if (anyFontWeight) { - const encoded = encodeStringArray(fontWeight); + const encoded = encodeSimpleArray(fontWeight); textMark.set_font_weight(encoded.values, encoded.indices); } - + if (anyZindex) { + textMark.set_zindex(zindex); + } return textMark; } diff --git a/avenger-vega-renderer/js/marks/util.js b/avenger-vega-renderer/js/marks/util.js index 81be589..46b03ad 100644 --- a/avenger-vega-renderer/js/marks/util.js +++ b/avenger-vega-renderer/js/marks/util.js @@ -1,27 +1,33 @@ - -export function encodeStringArray(originalArray) { - const uniqueStringsMap = new Map(); +/** + * Encode an array of strings as an array of unique strings and a Uint32Array + * of indices into the array of unique strings. + * + * @param {(string|number)[]} originalArray + * @returns {{indices: Uint32Array, values: string[]}} + */ +export function encodeSimpleArray(originalArray) { + const uniqueValuesMap = new Map(); let index = 0; // Populate the map with unique strings and their indices for (const str of originalArray) { - if (!uniqueStringsMap.has(str)) { - uniqueStringsMap.set(str, index++); + if (!uniqueValuesMap.has(str)) { + uniqueValuesMap.set(str, index++); } } // Generate the array of unique strings. // Note, Maps preserve the insertion order of their elements - const uniqueStringsArray = Array.from(uniqueStringsMap.keys()); + const uniqueValuesArray = Array.from(uniqueValuesMap.keys()); // Build index array let indices = new Uint32Array(originalArray.length); originalArray.forEach((str, i) => { - indices[i] = uniqueStringsMap.get(str); + indices[i] = uniqueValuesMap.get(str); }); return { - values: uniqueStringsArray, + values: uniqueValuesArray, indices, }; } diff --git a/avenger-vega-renderer/package-lock.json b/avenger-vega-renderer/package-lock.json index aa379c1..10d944c 100644 --- a/avenger-vega-renderer/package-lock.json +++ b/avenger-vega-renderer/package-lock.json @@ -8,6 +8,9 @@ "name": "avenger-vega-renderer", "version": "0.1.0", "license": "ISC", + "devDependencies": { + "typescript": "^5" + }, "peerDependencies": { "vega-scenegraph": "^4.11.2", "vega-util": "^1.17.2" @@ -234,6 +237,19 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "peer": true }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/vega-canvas": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/vega-canvas/-/vega-canvas-1.2.7.tgz", diff --git a/avenger-vega-renderer/package.json b/avenger-vega-renderer/package.json index acc161f..910117b 100644 --- a/avenger-vega-renderer/package.json +++ b/avenger-vega-renderer/package.json @@ -12,12 +12,15 @@ "build": "rm -rf pkg && wasm-pack build && rm -rf dist/ && mkdir -p dist/ && cp -r js/ dist/js && cp -r pkg dist/ && cp package.json dist/ && rm dist/pkg/package.json dist/pkg/.gitignore", "build-deno": "rm -rf pkg_deno && wasm-pack build --target=deno --features=deno --out-dir=pkg_deno && rm -rf dist_deno/ && mkdir -p dist_deno/ && cp -r js/ dist_deno/js && cp -r pkg_deno dist_deno/pkg && cp package.json dist_deno/ && rm dist_deno/pkg/.gitignore", "pack": "npm run build && cd dist && npm pack", - "test": "echo \"Error: no test specified\" && exit 1" + "type-check": "tsc" }, "peerDependencies": { "vega-scenegraph": "^4.11.2", "vega-util": "^1.17.2" }, + "devDependencies": { + "typescript": "^5" + }, "keywords": [], "author": "", "license": "ISC" diff --git a/avenger-vega-renderer/src/marks/rule.rs b/avenger-vega-renderer/src/marks/rule.rs index c9ce499..ed31eaa 100644 --- a/avenger-vega-renderer/src/marks/rule.rs +++ b/avenger-vega-renderer/src/marks/rule.rs @@ -48,6 +48,12 @@ impl RuleMark { self.inner.stroke_width = EncodingValue::Array { values: width } } + /// Set stroke color. + /// + /// @param {string[]} color_values + /// @param {Uint32Array} indices + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] pub fn set_stroke( &mut self, color_values: JsValue, @@ -60,6 +66,11 @@ impl RuleMark { Ok(()) } + /// Set stroke gradient + /// + /// @param {(string|object)[]} values + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] pub fn set_stroke_gradient( &mut self, values: JsValue, @@ -71,12 +82,20 @@ impl RuleMark { Ok(()) } + /// Set stroke cap + /// + /// @param {("butt"|"round"|"square")[]} values + #[wasm_bindgen(skip_jsdoc)] pub fn set_stroke_cap(&mut self, values: JsValue) -> Result<(), JsError> { let values: Vec = values.into_serde()?; self.inner.stroke_cap = EncodingValue::Array { values }; Ok(()) } + /// Set stroke dash + /// + /// @param {string|number[]} values + #[wasm_bindgen(skip_jsdoc)] pub fn set_stroke_dash(&mut self, values: JsValue) -> Result<(), JsError> { let values: Vec = values.into_serde()?; let values = values diff --git a/avenger-vega-renderer/src/marks/symbol.rs b/avenger-vega-renderer/src/marks/symbol.rs index 06f5fbc..1c4b99e 100644 --- a/avenger-vega-renderer/src/marks/symbol.rs +++ b/avenger-vega-renderer/src/marks/symbol.rs @@ -53,6 +53,12 @@ impl SymbolMark { self.inner.stroke_width = width; } + /// Set stroke color + /// + /// @param {string[]} color_values + /// @param {Uint32Array} indices + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] pub fn set_stroke( &mut self, color_values: JsValue, @@ -65,6 +71,11 @@ impl SymbolMark { Ok(()) } + /// Set stroke gradient + /// + /// @param {(string|object)[]} values + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] pub fn set_stroke_gradient( &mut self, values: JsValue, @@ -76,6 +87,12 @@ impl SymbolMark { Ok(()) } + /// Set fill color + /// + /// @param {string[]} color_values + /// @param {Uint32Array} indices + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] pub fn set_fill( &mut self, color_values: JsValue, @@ -88,6 +105,11 @@ impl SymbolMark { Ok(()) } + /// Set fill gradient + /// + /// @param {(string|object)[]} values + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] pub fn set_fill_gradient(&mut self, values: JsValue, opacity: Vec) -> Result<(), JsError> { self.inner.fill = EncodingValue::Array { values: decode_gradients(values, opacity, &mut self.inner.gradients)?, @@ -95,6 +117,11 @@ impl SymbolMark { Ok(()) } + /// Set symbol shape + /// + /// @param {string[]} shape_values + /// @param {Uint32Array} indices + #[wasm_bindgen(skip_jsdoc)] pub fn set_shape(&mut self, shape_values: JsValue, indices: Vec) -> Result<(), JsError> { let shapes: Vec = shape_values.into_serde()?; let shapes = shapes @@ -107,7 +134,4 @@ impl SymbolMark { self.inner.shape_index = EncodingValue::Array { values: indices }; Ok(()) } - - // TODO - // pub indices: Option>, } diff --git a/avenger-vega-renderer/src/marks/text.rs b/avenger-vega-renderer/src/marks/text.rs index 4a42186..50d5c0d 100644 --- a/avenger-vega-renderer/src/marks/text.rs +++ b/avenger-vega-renderer/src/marks/text.rs @@ -58,12 +58,21 @@ impl TextMark { self.inner.indices = Some(indices); } + /// Set text + /// + /// @param {string[]} text + #[wasm_bindgen(skip_jsdoc)] pub fn set_text(&mut self, text: JsValue) -> Result<(), JsError> { let text: Vec = text.into_serde()?; self.inner.text = EncodingValue::Array { values: text }; Ok(()) } + /// Set font + /// + /// @param {string[]} font_values + /// @param {Uint32Array} indices + #[wasm_bindgen(skip_jsdoc)] pub fn set_font(&mut self, font_values: JsValue, indices: Vec) -> Result<(), JsError> { let font_values: Vec = font_values.into_serde()?; let values = indices @@ -74,6 +83,11 @@ impl TextMark { Ok(()) } + /// Set alignment + /// + /// @param {("left"|"center"|"right")[]} align_values + /// @param {Uint32Array} indices + #[wasm_bindgen(skip_jsdoc)] pub fn set_align(&mut self, align_values: JsValue, indices: Vec) -> Result<(), JsError> { let align_values: Vec = align_values.into_serde()?; let values = indices @@ -84,6 +98,11 @@ impl TextMark { Ok(()) } + /// Set alignment + /// + /// @param {("alphabetic"|"top"|"middle"|"bottom"|"line-top"|"line-bottom")[]} baseline_values + /// @param {Uint32Array} indices + #[wasm_bindgen(skip_jsdoc)] pub fn set_baseline( &mut self, baseline_values: JsValue, @@ -98,6 +117,11 @@ impl TextMark { Ok(()) } + /// Set font weight + /// + /// @param {(number|"normal"|"bold")[]} weight_values + /// @param {Uint32Array} indices + #[wasm_bindgen(skip_jsdoc)] pub fn set_font_weight( &mut self, weight_values: JsValue, @@ -112,6 +136,11 @@ impl TextMark { Ok(()) } + /// Set font style + /// + /// @param {("normal"|"italic")[]} style_values + /// @param {Uint32Array} indices + #[wasm_bindgen(skip_jsdoc)] pub fn set_font_style( &mut self, style_values: JsValue, @@ -126,6 +155,12 @@ impl TextMark { Ok(()) } + /// Set text color + /// + /// @param {string[]} color_values + /// @param {Uint32Array} indices + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] pub fn set_color( &mut self, color_values: JsValue, diff --git a/avenger-vega-renderer/tsconfig.json b/avenger-vega-renderer/tsconfig.json new file mode 100644 index 0000000..6a28874 --- /dev/null +++ b/avenger-vega-renderer/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "baseUrl": "./", + "target": "ES2015", + "paths": { + "*": ["js/marks/*"] + } + }, + "include": [ + "js/marks/**/*" + ] +} \ No newline at end of file From cc76cabd10cacf4bbe77989e5e577582daff2f3d Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 20 Apr 2024 20:03:35 -0400 Subject: [PATCH 06/22] Move importScenegraph into marks so it's typechecked --- avenger-vega-renderer/js/index.js | 20 ++------------------ avenger-vega-renderer/js/marks/scenegraph.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 18 deletions(-) create mode 100644 avenger-vega-renderer/js/marks/scenegraph.js diff --git a/avenger-vega-renderer/js/index.js b/avenger-vega-renderer/js/index.js index eaefc32..e77f9fe 100644 --- a/avenger-vega-renderer/js/index.js +++ b/avenger-vega-renderer/js/index.js @@ -1,8 +1,7 @@ -import { AvengerCanvas, SceneGraph, scene_graph_to_png } from "../pkg/avenger_wasm.js"; +import { AvengerCanvas, scene_graph_to_png } from "../pkg/avenger_wasm.js"; import { Renderer, CanvasHandler, domClear, domChild } from 'vega-scenegraph'; import { inherits } from 'vega-util'; -import { importGroup } from "./marks/group.js" - +import { importScenegraph } from "./marks/scenegraph.js" function devicePixelRatio() { return typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1; @@ -129,21 +128,6 @@ inherits(AvengerHandler, CanvasHandler, { } }); -/** - * @param {GroupMarkSpec} groupMark - * @param {number} width - * @param {number} height - * @param {[number, number]} origin - * @returns {SceneGraph} - */ -function importScenegraph(groupMark, width, height, origin) { - const sceneGraph = new SceneGraph(width, height, origin[0], origin[1]); - for (const vegaGroup of groupMark.items) { - sceneGraph.add_group(importGroup(vegaGroup, groupMark.name)); - } - return sceneGraph; -} - export function registerVegaRenderer(renderModule) { // Call with renderModule function from 'vega-scenegraph' renderModule('avenger', { diff --git a/avenger-vega-renderer/js/marks/scenegraph.js b/avenger-vega-renderer/js/marks/scenegraph.js new file mode 100644 index 0000000..63c1cc0 --- /dev/null +++ b/avenger-vega-renderer/js/marks/scenegraph.js @@ -0,0 +1,17 @@ +import { SceneGraph } from "../../pkg/avenger_wasm.js"; +import { importGroup } from "./group.js"; + +/** + * @param {import("group").GroupMarkSpec} groupMark + * @param {number} width + * @param {number} height + * @param {[number, number]} origin + * @returns {SceneGraph} + */ +export function importScenegraph(groupMark, width, height, origin) { + const sceneGraph = new SceneGraph(width, height, origin[0], origin[1]); + for (const vegaGroup of groupMark.items) { + sceneGraph.add_group(importGroup(vegaGroup, groupMark.name)); + } + return sceneGraph; +} \ No newline at end of file From ffc742d27157c40a6dafd48acbf6f675cf018332 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 22 Apr 2024 09:29:22 -0400 Subject: [PATCH 07/22] Add rect and group mark support --- Cargo.lock | 1 + avenger-vega-renderer/Cargo.toml | 1 + avenger-vega-renderer/js/marks/group.js | 48 +++++- avenger-vega-renderer/js/marks/rect.js | 155 ++++++++++++++++++ avenger-vega-renderer/js/marks/rule.js | 9 +- avenger-vega-renderer/js/marks/symbol.js | 14 +- avenger-vega-renderer/js/marks/text.js | 2 +- avenger-vega-renderer/src/lib.rs | 2 +- avenger-vega-renderer/src/marks/group.rs | 115 +++++++++++++ avenger-vega-renderer/src/marks/mod.rs | 1 + avenger-vega-renderer/src/marks/rect.rs | 120 ++++++++++++++ avenger-vega-renderer/src/marks/text.rs | 2 +- avenger-vega-renderer/src/marks/util.rs | 30 +++- avenger-vega-renderer/test/test_baselines.py | 17 +- .../test/test_server/package-lock.json | 3 + 15 files changed, 496 insertions(+), 24 deletions(-) create mode 100644 avenger-vega-renderer/js/marks/rect.js create mode 100644 avenger-vega-renderer/src/marks/rect.rs diff --git a/Cargo.lock b/Cargo.lock index d3e6f8c..c4aca0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -398,6 +398,7 @@ dependencies = [ "image", "js-sys", "lazy_static", + "lyon_path", "serde_json", "unicode-segmentation", "wasm-bindgen", diff --git a/avenger-vega-renderer/Cargo.toml b/avenger-vega-renderer/Cargo.toml index fd1e931..cba4a57 100644 --- a/avenger-vega-renderer/Cargo.toml +++ b/avenger-vega-renderer/Cargo.toml @@ -18,6 +18,7 @@ wgpu = { version = "0.19.3", default-features = false, features = ["wgsl", "webg lazy_static = "1.4.0" serde_json = "1.0.114" csscolorparser = "0.6.2" +lyon_path = "*" wasm-bindgen = { version = "=0.2.92" } wasm-bindgen-futures = "0.4.30" gloo-utils = { version = "0.2.0", features = ["serde"] } diff --git a/avenger-vega-renderer/js/marks/group.js b/avenger-vega-renderer/js/marks/group.js index 7ca81e4..25db6e1 100644 --- a/avenger-vega-renderer/js/marks/group.js +++ b/avenger-vega-renderer/js/marks/group.js @@ -2,23 +2,34 @@ import {GroupMark} from "../../pkg/avenger_wasm.js"; import { importSymbol } from "./symbol.js" import { importRule } from "./rule.js"; import {importText} from "./text.js"; +import {importRect} from "./rect.js"; /** * @typedef {import('./symbol.js').SymbolMarkSpec} SymbolMarkSpec * @typedef {import('./text.js').TextMarkSpec} TextMarkSpec * @typedef {import('./rule.js').RuleMarkSpec} RuleMarkSpec + * @typedef {import('./rect.js').RectMarkSpec} RectMarkSpec * * * @typedef {Object} GroupItemSpec * @property {"group"} marktype - * @property {boolean} clip - * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec)[]} items - * @property {string} fill - * @property {string} stroke + * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec)[]} items * @property {number} x * @property {number} y * @property {number} width * @property {number} height + * @property {boolean} clip + * @property {string|object} fill + * @property {string|object} stroke + * @property {number} strokeWidth + * @property {number} opacity + * @property {number} fillOpacity + * @property {number} strokeOpacity + * @property {number} cornerRadius + * @property {number} cornerRadiusTopLeft + * @property {number} cornerRadiusTopRight + * @property {number} cornerRadiusBottomLeft + * @property {number} cornerRadiusBottomRight */ /** @@ -51,6 +62,9 @@ export function importGroup(vegaGroup, name) { case "rule": groupMark.add_rule_mark(importRule(vegaMark, forceClip)); break; + case "rect": + groupMark.add_rect_mark(importRect(vegaMark, forceClip)); + break; case "text": groupMark.add_text_mark(importText(vegaMark, forceClip)); break; @@ -62,5 +76,31 @@ export function importGroup(vegaGroup, name) { } } + // Set styling + const fillOpacity = (vegaGroup.opacity ?? 1) * (vegaGroup.fillOpacity ?? 1); + if (typeof vegaGroup.fill === "string") { + groupMark.set_fill(vegaGroup.fill, fillOpacity); + } else if (vegaGroup.fill != null) { + groupMark.set_fill_gradient(vegaGroup.fill, fillOpacity); + } + + const strokeOpacity = (vegaGroup.opacity ?? 1) * (vegaGroup.strokeOpacity ?? 1); + if (typeof vegaGroup.stroke === "string") { + groupMark.set_stroke(vegaGroup.stroke, strokeOpacity); + } else if (vegaGroup.stroke != null) { + groupMark.set_stroke_gradient(vegaGroup.stroke, strokeOpacity); + } + + // set clip + groupMark.set_clip( + vegaGroup.width, + vegaGroup.height, + vegaGroup.cornerRadius, + vegaGroup.cornerRadiusTopLeft, + vegaGroup.cornerRadiusTopRight, + vegaGroup.cornerRadiusBottomLeft, + vegaGroup.cornerRadiusBottomRight, + ) + return groupMark; } diff --git a/avenger-vega-renderer/js/marks/rect.js b/avenger-vega-renderer/js/marks/rect.js new file mode 100644 index 0000000..2dc242b --- /dev/null +++ b/avenger-vega-renderer/js/marks/rect.js @@ -0,0 +1,155 @@ +import {RectMark} from "../../pkg/avenger_wasm.js"; +import {encodeSimpleArray} from "./util.js"; + + +/** + * Represents the style and configuration of a graphic element. + * @typedef {Object} RectItem + * @property {number} strokeWidth + * @property {string|object} stroke + * @property {string|number[]} strokeDash + * @property {string|object} fill + * @property {number} x + * @property {number} y + * @property {number} width + * @property {number} height + * @property {number} cornerRadius + * @property {number} opacity + * @property {number} fillOpacity + * @property {number} strokeOpacity + * @property {string} strokeCap + * @property {number} zindex + */ + +/** + * Represents a graphical object configuration. + * @typedef {Object} RectMarkSpec + * @property {"rect"} marktype + * @property {boolean} clip + * @property {RectItem[]} items + * @property {string} name + * @property {number} zindex + */ + +/** + * @param {RectMarkSpec} vegaRectMark + * @param {boolean} forceClip + * @returns {RectMark} + */ +export function importRect(vegaRectMark, forceClip) { + const items = vegaRectMark.items; + const len = items.length; + + const rectMark = new RectMark( + len, vegaRectMark.clip || forceClip, vegaRectMark.name, vegaRectMark.zindex + ); + if (len === 0) { + return rectMark; + } + + const firstItem = items[0]; + + const x = new Float32Array(len).fill(0); + const y = new Float32Array(len).fill(0); + const width = new Float32Array(len).fill(0); + const height = new Float32Array(len).fill(0); + + const fill = new Array(len).fill(""); + let anyFill = false; + let anyFillIsGradient = false; + + const stroke = new Array(len).fill(""); + let anyStroke = false; + let anyStrokeIsGradient = false; + + const strokeWidth = new Float32Array(len); + let anyStrokeWidth = false; + + const strokeOpacity = new Float32Array(len).fill(1); + const fillOpacity = new Float32Array(len).fill(1); + + const cornerRadius = new Float32Array(len); + let anyCornerRadius = false; + + const zindex = new Int32Array(len).fill(0); + let anyZindex = false; + + items.forEach((item, i) => { + if (item.x != null) { + x[i] = item.x; + } + if (item.y != null) { + y[i] = item.y; + } + if (item.width != null) { + width[i] = item.width; + } + if (item.height != null) { + height[i] = item.height; + } + if (item.fill != null) { + fill[i] = item.fill; + anyFill ||= true; + anyFillIsGradient ||= typeof item.fill === "object"; + } + fillOpacity[i] = (item.fillOpacity ?? 1) * (item.opacity ?? 1); + + if (item.stroke != null) { + stroke[i] = item.stroke; + anyStroke ||= true; + anyStrokeIsGradient ||= typeof item.stroke === "object"; + } + if (item.strokeWidth != null) { + strokeWidth[i] = item.strokeWidth; + anyStrokeWidth ||= true; + } + strokeOpacity[i] = (item.strokeOpacity ?? 1) * (item.opacity ?? 1); + + if (item.cornerRadius != null) { + cornerRadius[i] = item.cornerRadius; + anyCornerRadius ||= true; + } + + if (item.zindex != null) { + zindex[i] = item.zindex; + anyZindex ||= true; + } + }) + + rectMark.set_xy(x, y); + rectMark.set_width(width); + rectMark.set_height(height); + + if (anyFill) { + if (anyFillIsGradient) { + rectMark.set_fill_gradient(fill, fillOpacity); + } else { + const encoded = encodeSimpleArray(fill); + rectMark.set_fill(encoded.values, encoded.indices, strokeOpacity); + } + } + + if (anyStroke) { + if (anyStrokeIsGradient) { + rectMark.set_stroke_gradient(stroke, strokeOpacity); + } else { + const encoded = encodeSimpleArray(stroke); + rectMark.set_stroke(encoded.values, encoded.indices, strokeOpacity); + } + } + + if (anyStrokeWidth) { + rectMark.set_stroke_width(strokeWidth); + } + + + if (anyCornerRadius) { + rectMark.set_corner_radius(cornerRadius); + } + + if (anyZindex) { + rectMark.set_zindex(zindex); + } + + return rectMark; +} diff --git a/avenger-vega-renderer/js/marks/rule.js b/avenger-vega-renderer/js/marks/rule.js index ff17bd9..4fbb386 100644 --- a/avenger-vega-renderer/js/marks/rule.js +++ b/avenger-vega-renderer/js/marks/rule.js @@ -44,8 +44,6 @@ export function importRule(vegaRuleMark, forceClip) { return ruleMark; } - const firstItem = items[0]; - const x0 = new Float32Array(len).fill(0); const y0 = new Float32Array(len).fill(0); const x1 = new Float32Array(len).fill(0); @@ -56,9 +54,9 @@ export function importRule(vegaRuleMark, forceClip) { const strokeOpacity = new Float32Array(len).fill(1); - const stroke = new Array(len); + const stroke = new Array(len).fill(""); let anyStroke = false; - let strokeIsGradient = firstItem.stroke != null && typeof firstItem.stroke === "object"; + let anyStrokeIsGradient = false; const strokeCap = new Array(len); let anyStrokeCap = false; @@ -95,6 +93,7 @@ export function importRule(vegaRuleMark, forceClip) { if (item.stroke != null) { stroke[i] = item.stroke; anyStroke ||= true; + anyStrokeIsGradient ||= typeof item.stroke === "object"; } if (item.strokeCap != null) { @@ -120,7 +119,7 @@ export function importRule(vegaRuleMark, forceClip) { } if (anyStroke) { - if (strokeIsGradient) { + if (anyStrokeIsGradient) { ruleMark.set_stroke_gradient(stroke, strokeOpacity); } else { const encoded = encodeSimpleArray(stroke); diff --git a/avenger-vega-renderer/js/marks/symbol.js b/avenger-vega-renderer/js/marks/symbol.js index 0f1749d..7f62ba2 100644 --- a/avenger-vega-renderer/js/marks/symbol.js +++ b/avenger-vega-renderer/js/marks/symbol.js @@ -67,16 +67,16 @@ export function importSymbol(vegaSymbolMark, force_clip) { const x = new Float32Array(len).fill(0); const y = new Float32Array(len).fill(0); - const fill = new Array(len); + const fill = new Array(len).fill("");; let anyFill = false; - let fillIsGradient = firstItem.fill != null && typeof firstItem.fill === "object"; + let anyFillIsGradient = false; const size = new Float32Array(len).fill(20); let anySize = false; - const stroke = new Array(len); + const stroke = new Array(len).fill("");; let anyStroke = false; - let strokeIsGradient = firstItem.stroke != null && typeof firstItem.stroke === "object"; + let anyStrokeIsGradient = false; const angle = new Float32Array(len).fill(0); let anyAngle = false; @@ -101,6 +101,7 @@ export function importSymbol(vegaSymbolMark, force_clip) { if (item.fill != null) { fill[i] = item.fill; anyFill ||= true; + anyFillIsGradient ||= typeof item.fill === "object"; } if (item.size != null) { @@ -111,6 +112,7 @@ export function importSymbol(vegaSymbolMark, force_clip) { if (item.stroke != null) { stroke[i] = item.stroke; anyStroke ||= true; + anyStrokeIsGradient ||= typeof item.stroke === "object"; } if (item.angle != null) { @@ -132,7 +134,7 @@ export function importSymbol(vegaSymbolMark, force_clip) { symbolMark.set_xy(x, y); if (anyFill) { - if (fillIsGradient) { + if (anyFillIsGradient) { symbolMark.set_fill_gradient(fill, fillOpacity); } else { const encoded = encodeSimpleArray(fill); @@ -145,7 +147,7 @@ export function importSymbol(vegaSymbolMark, force_clip) { } if (anyStroke) { - if (strokeIsGradient) { + if (anyStrokeIsGradient) { symbolMark.set_stroke_gradient(stroke, strokeOpacity); } else { const encoded = encodeSimpleArray(stroke); diff --git a/avenger-vega-renderer/js/marks/text.js b/avenger-vega-renderer/js/marks/text.js index a487546..b37c70e 100644 --- a/avenger-vega-renderer/js/marks/text.js +++ b/avenger-vega-renderer/js/marks/text.js @@ -61,7 +61,7 @@ export function importText(vegaTextMark, force_clip) { const font = new Array(len); let anyFont = false; - const fill = new Array(len); + const fill = new Array(len).fill("");; const fillOpacity = new Float32Array(len).fill(1); let anyFill = false; diff --git a/avenger-vega-renderer/src/lib.rs b/avenger-vega-renderer/src/lib.rs index 9fe2a25..d651994 100644 --- a/avenger-vega-renderer/src/lib.rs +++ b/avenger-vega-renderer/src/lib.rs @@ -67,7 +67,7 @@ impl AvengerCanvas { pub fn set_scene(&mut self, scene_graph: SceneGraph) -> Result<(), JsError> { let window = web_sys::window().expect("should have a window in this context"); - let performance = window + let _performance = window .performance() .expect("performance should be available"); diff --git a/avenger-vega-renderer/src/marks/group.rs b/avenger-vega-renderer/src/marks/group.rs index 4fad0f7..9df23a4 100644 --- a/avenger-vega-renderer/src/marks/group.rs +++ b/avenger-vega-renderer/src/marks/group.rs @@ -1,9 +1,16 @@ +use lyon_path::builder::BorderRadii; +use lyon_path::geom::Box2D; +use lyon_path::geom::euclid::Point2D; +use lyon_path::Winding; +use wasm_bindgen::{JsError, JsValue}; use crate::marks::rule::RuleMark; use crate::marks::symbol::SymbolMark; use crate::marks::text::TextMark; use avenger::marks::group::{Clip, SceneGroup as RsSceneGroup}; use avenger::marks::mark::SceneMark; use wasm_bindgen::prelude::wasm_bindgen; +use crate::marks::rect::RectMark; +use crate::marks::util::{decode_color, decode_gradient}; #[wasm_bindgen] pub struct GroupMark { @@ -47,10 +54,118 @@ impl GroupMark { } } + pub fn set_clip( + &mut self, + width: Option, + height: Option, + corner_radius: Option, + corner_radius_top_left: Option, + corner_radius_top_right: Option, + corner_radius_bottom_left: Option, + corner_radius_bottom_right: Option, + ) { + let clip = if let (Some(width), Some(height)) = (width, height) { + let corner_radius = corner_radius.unwrap_or(0.0); + let corner_radius_top_left = + corner_radius_top_left.unwrap_or(corner_radius); + let corner_radius_top_right = + corner_radius_top_right.unwrap_or(corner_radius); + let corner_radius_bottom_left = corner_radius_bottom_left + .unwrap_or(corner_radius); + let corner_radius_bottom_right = corner_radius_bottom_right + .unwrap_or(corner_radius); + + if corner_radius_top_left > 0.0 + || corner_radius_top_right > 0.0 + || corner_radius_bottom_left > 0.0 + || corner_radius_bottom_right > 0.0 + { + // Rounded rectange path + let mut builder = lyon_path::Path::builder(); + builder.add_rounded_rectangle( + &Box2D::new(Point2D::new(0.0, 0.0), Point2D::new(width, height)), + &BorderRadii { + top_left: corner_radius_top_left, + top_right: corner_radius_top_right, + bottom_left: corner_radius_bottom_left, + bottom_right: corner_radius_bottom_right, + }, + Winding::Positive, + ); + Clip::Path(builder.build()) + } else { + // Rect + Clip::Rect { + x: 0.0, // x and y are zero to align with origin + y: 0.0, + width, + height, + } + } + } else { + Clip::None + }; + self.inner.clip = clip; + } + + /// Set fill color + /// + /// @param {string} color_value + /// @param {number} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_fill( + &mut self, + color_value: &str, + opacity: f32, + ) -> Result<(), JsError> { + self.inner.fill = Some(decode_color(color_value, opacity)?); + Ok(()) + } + + /// Set fill gradient + /// + /// @param {(string|object)} value + /// @param {number} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_fill_gradient(&mut self, value: JsValue, opacity: f32) -> Result<(), JsError> { + let grad = decode_gradient(value, opacity, &mut self.inner.gradients)?; + self.inner.fill = Some(grad); + Ok(()) + } + + /// Set stroke color + /// + /// @param {string} color_value + /// @param {number} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_stroke( + &mut self, + color_value: &str, + opacity: f32, + ) -> Result<(), JsError> { + self.inner.stroke = Some(decode_color(color_value, opacity)?); + Ok(()) + } + + /// Set stroke gradient + /// + /// @param {(string|object)} value + /// @param {number} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_stroke_gradient(&mut self, value: JsValue, opacity: f32) -> Result<(), JsError> { + let grad = decode_gradient(value, opacity, &mut self.inner.gradients)?; + self.inner.stroke = Some(grad); + Ok(()) + } + pub fn add_symbol_mark(&mut self, mark: SymbolMark) { self.inner.marks.push(SceneMark::Symbol(mark.build())); } + pub fn add_rect_mark(&mut self, mark: RectMark) { + self.inner.marks.push(SceneMark::Rect(mark.build())); + } + pub fn add_rule_mark(&mut self, mark: RuleMark) { self.inner.marks.push(SceneMark::Rule(mark.build())); } diff --git a/avenger-vega-renderer/src/marks/mod.rs b/avenger-vega-renderer/src/marks/mod.rs index 483e5dd..d7bf61f 100644 --- a/avenger-vega-renderer/src/marks/mod.rs +++ b/avenger-vega-renderer/src/marks/mod.rs @@ -3,3 +3,4 @@ pub mod rule; pub mod symbol; pub mod text; pub mod util; +pub mod rect; diff --git a/avenger-vega-renderer/src/marks/rect.rs b/avenger-vega-renderer/src/marks/rect.rs new file mode 100644 index 0000000..0520234 --- /dev/null +++ b/avenger-vega-renderer/src/marks/rect.rs @@ -0,0 +1,120 @@ +use crate::marks::util::{decode_colors, decode_gradients, zindex_to_indices}; +use avenger::marks::rect::RectMark as RsRectMark; +use avenger::marks::value::EncodingValue; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; + +#[wasm_bindgen] +pub struct RectMark { + inner: RsRectMark, +} + +impl RectMark { + pub fn build(self) -> RsRectMark { + self.inner + } +} + +#[wasm_bindgen] +impl RectMark { + #[wasm_bindgen(constructor)] + pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { + Self { + inner: RsRectMark { + len, + clip, + name: name.unwrap_or_default(), + zindex, + ..Default::default() + }, + } + } + pub fn set_zindex(&mut self, zindex: Vec) { + self.inner.indices = Some(zindex_to_indices(zindex)); + } + + pub fn set_xy(&mut self, x: Vec, y: Vec) { + self.inner.x = EncodingValue::Array { values: x }; + self.inner.y = EncodingValue::Array { values: y }; + } + + pub fn set_width(&mut self, width: Vec) { + self.inner.width = EncodingValue::Array { values: width }; + } + + pub fn set_height(&mut self, height: Vec) { + self.inner.height = EncodingValue::Array { values: height }; + } + + pub fn set_corner_radius(&mut self, corner_radius: Vec) { + self.inner.corner_radius = EncodingValue::Array { values: corner_radius }; + } + + pub fn set_stroke_width(&mut self, width: Vec) { + self.inner.stroke_width = EncodingValue::Array { values: width } + } + + /// Set stroke color. + /// + /// @param {string[]} color_values + /// @param {Uint32Array} indices + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_stroke( + &mut self, + color_values: JsValue, + indices: Vec, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.stroke = EncodingValue::Array { + values: decode_colors(color_values, indices, opacity)?, + }; + Ok(()) + } + + /// Set stroke gradient + /// + /// @param {(string|object)[]} values + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_stroke_gradient( + &mut self, + values: JsValue, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.stroke = EncodingValue::Array { + values: decode_gradients(values, opacity, &mut self.inner.gradients)?, + }; + Ok(()) + } + + /// Set fill color + /// + /// @param {string[]} color_values + /// @param {Uint32Array} indices + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_fill( + &mut self, + color_values: JsValue, + indices: Vec, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.fill = EncodingValue::Array { + values: decode_colors(color_values, indices, opacity)?, + }; + Ok(()) + } + + /// Set fill gradient + /// + /// @param {(string|object)[]} values + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_fill_gradient(&mut self, values: JsValue, opacity: Vec) -> Result<(), JsError> { + self.inner.fill = EncodingValue::Array { + values: decode_gradients(values, opacity, &mut self.inner.gradients)?, + }; + Ok(()) + } +} diff --git a/avenger-vega-renderer/src/marks/text.rs b/avenger-vega-renderer/src/marks/text.rs index 50d5c0d..df19801 100644 --- a/avenger-vega-renderer/src/marks/text.rs +++ b/avenger-vega-renderer/src/marks/text.rs @@ -173,7 +173,7 @@ impl TextMark { .iter() .map(|c| { let Ok(c) = csscolorparser::parse(c) else { - return [0.0, 0.0, 0.0, 1.0]; + return [0.0, 0.0, 0.0, 0.0]; }; [c.r as f32, c.g as f32, c.b as f32, c.a as f32] }) diff --git a/avenger-vega-renderer/src/marks/util.rs b/avenger-vega-renderer/src/marks/util.rs index 7876dcf..040aeef 100644 --- a/avenger-vega-renderer/src/marks/util.rs +++ b/avenger-vega-renderer/src/marks/util.rs @@ -18,6 +18,16 @@ pub fn decode_gradients( .map_err(|_| JsError::new("Failed to parse gradients")) } +pub fn decode_gradient( + value: JsValue, + opacity: f32, + gradients: &mut Vec, +) -> Result { + let grad: CssColorOrGradient = value.into_serde()?; + grad.to_color_or_grad(opacity, gradients) + .map_err(|_| JsError::new("Failed to parse gradient")) +} + pub fn decode_colors( color_values: JsValue, indices: Vec, @@ -25,11 +35,11 @@ pub fn decode_colors( ) -> Result, JsError> { // Parse unique colors let color_values: Vec = color_values.into_serde()?; - let unique_strokes = color_values + let unique_colors = color_values .iter() .map(|c| { let Ok(c) = csscolorparser::parse(c) else { - return [0.0, 0.0, 0.0, 1.0]; + return [0.0, 0.0, 0.0, 0.0]; }; [c.r as f32, c.g as f32, c.b as f32, c.a as f32] }) @@ -40,13 +50,27 @@ pub fn decode_colors( .iter() .zip(opacity) .map(|(ind, opacity)| { - let [r, g, b, a] = unique_strokes[*ind as usize]; + let [r, g, b, a] = unique_colors[*ind as usize]; ColorOrGradient::Color([r as f32, g as f32, b as f32, a as f32 * opacity]) }) .collect::>(); Ok(colors) } +pub fn decode_color( + color_value: &str, + opacity: f32, +) -> Result { + Ok(match csscolorparser::parse(color_value) { + Ok(c) => { + ColorOrGradient::Color([c.r as f32, c.g as f32, c.b as f32, c.a as f32 * opacity]) + } + Err(_) => { + ColorOrGradient::Color([0.0, 0.0, 0.0, 0.0]) + } + }) +} + pub fn zindex_to_indices(zindex: Vec) -> Vec { let mut indices: Vec = (0..zindex.len()).collect(); indices.sort_by_key(|i| zindex[*i]); diff --git a/avenger-vega-renderer/test/test_baselines.py b/avenger-vega-renderer/test/test_baselines.py index 9ecc2d2..f4e8bb1 100644 --- a/avenger-vega-renderer/test/test_baselines.py +++ b/avenger-vega-renderer/test/test_baselines.py @@ -53,6 +53,13 @@ def failures_path(): @pytest.mark.parametrize( "category,spec_name,tolerance", [ + ("rect", "stacked_bar", 0.0001), + ("rect", "stacked_bar_stroke", 0.0001), + ("rect", "stacked_bar_rounded", 0.0001), + ("rect", "stacked_bar_rounded_stroke", 0.0001), + ("rect", "stacked_bar_rounded_stroke_opacity", 0.0001), + ("rect", "heatmap", 0.0001), + ("symbol", "binned_scatter_diamonds", 0.0001), ("symbol", "binned_scatter_square", 0.0001), ("symbol", "binned_scatter_triangle-down", 0.0001), @@ -88,15 +95,19 @@ def failures_path(): ("text", "text_alignment", 0.016), ("text", "text_rotation", 0.016), ("text", "letter_scatter", 0.027), - # ("text", "lasagna_plot", 0.02), + ("text", "lasagna_plot", 0.04), # ("text", "arc_radial", 0.0001), - # ("text", "emoji", 0.0001), + ("text", "emoji", 0.05), - # The canvas renderer messes up these gradients, avenger renders them correctly ("gradients", "symbol_cross_gradient", 0.0001), ("gradients", "symbol_circles_gradient_stroke", 0.0001), ("gradients", "symbol_radial_gradient", 0.0002), ("gradients", "rules_with_gradients", 0.003), # Lyon square caps issue + ("gradients", "heatmap_with_colorbar", 0.0008), + ("gradients", "diagonal_gradient_bars_rounded", 0.0001), + ("gradients", "default_gradient_bars_rounded_stroke", 0.0001), + ("gradients", "residuals_colorscale", 0.001), + ("gradients", "stroke_rect_gradient", 0.0001), ], ) def test_image_baselines( diff --git a/avenger-vega-renderer/test/test_server/package-lock.json b/avenger-vega-renderer/test/test_server/package-lock.json index 3090367..c9c6f2d 100644 --- a/avenger-vega-renderer/test/test_server/package-lock.json +++ b/avenger-vega-renderer/test/test_server/package-lock.json @@ -48,6 +48,9 @@ "name": "avenger-vega-renderer", "version": "0.1.0", "license": "ISC", + "devDependencies": { + "typescript": "^5" + }, "peerDependencies": { "vega-scenegraph": "^4.11.2", "vega-util": "^1.17.2" From c19b95b52164423aac86ce600d7f3eb8d38a8e70 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 22 Apr 2024 17:11:04 -0400 Subject: [PATCH 08/22] Set stroke width --- avenger-vega-renderer/js/marks/group.js | 2 ++ avenger-vega-renderer/src/marks/group.rs | 5 +++++ avenger-vega-renderer/test/test_baselines.py | 8 ++++++++ 3 files changed, 15 insertions(+) diff --git a/avenger-vega-renderer/js/marks/group.js b/avenger-vega-renderer/js/marks/group.js index 25db6e1..249180f 100644 --- a/avenger-vega-renderer/js/marks/group.js +++ b/avenger-vega-renderer/js/marks/group.js @@ -91,6 +91,8 @@ export function importGroup(vegaGroup, name) { groupMark.set_stroke_gradient(vegaGroup.stroke, strokeOpacity); } + groupMark.set_stroke_width(vegaGroup.strokeWidth); + // set clip groupMark.set_clip( vegaGroup.width, diff --git a/avenger-vega-renderer/src/marks/group.rs b/avenger-vega-renderer/src/marks/group.rs index 9df23a4..663f50f 100644 --- a/avenger-vega-renderer/src/marks/group.rs +++ b/avenger-vega-renderer/src/marks/group.rs @@ -9,6 +9,7 @@ use crate::marks::text::TextMark; use avenger::marks::group::{Clip, SceneGroup as RsSceneGroup}; use avenger::marks::mark::SceneMark; use wasm_bindgen::prelude::wasm_bindgen; +use avenger::marks::value::EncodingValue; use crate::marks::rect::RectMark; use crate::marks::util::{decode_color, decode_gradient}; @@ -158,6 +159,10 @@ impl GroupMark { Ok(()) } + pub fn set_stroke_width(&mut self, width: Option) { + self.inner.stroke_width = width; + } + pub fn add_symbol_mark(&mut self, mark: SymbolMark) { self.inner.marks.push(SceneMark::Symbol(mark.build())); } diff --git a/avenger-vega-renderer/test/test_baselines.py b/avenger-vega-renderer/test/test_baselines.py index f4e8bb1..971e02a 100644 --- a/avenger-vega-renderer/test/test_baselines.py +++ b/avenger-vega-renderer/test/test_baselines.py @@ -108,6 +108,14 @@ def failures_path(): ("gradients", "default_gradient_bars_rounded_stroke", 0.0001), ("gradients", "residuals_colorscale", 0.001), ("gradients", "stroke_rect_gradient", 0.0001), + + ("clip", "text_clip", 0.006), + ("clip", "text_clip_rounded", 0.006), + + # # TODO: + # ("clip", "clip_mixed_marks", 0.0), + # ("clip", "clip_rounded", 0.0), + # ("clip", "bar_rounded", 0.0), ], ) def test_image_baselines( From fa76bbe43d18f54b29a3cc2f77bf67d7a353791f Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 22 Apr 2024 20:01:05 -0400 Subject: [PATCH 09/22] Fix rounded bars example --- avenger-vega-renderer/js/index.js | 1 - avenger-vega-renderer/js/marks/group.js | 28 ++-- avenger-vega-renderer/js/marks/rect.js | 8 +- avenger-vega-renderer/js/marks/scenegraph.js | 4 +- avenger-vega-renderer/js/marks/symbol.js | 1 - avenger-vega-renderer/src/marks/group.rs | 42 ++---- avenger-vega-renderer/src/marks/rect.rs | 4 +- avenger-vega-renderer/src/marks/util.rs | 13 +- avenger-vega-renderer/test/test_baselines.py | 3 +- .../vega-specs/clip/bar_rounded2.vg.json | 138 ++++++++++++++++++ 10 files changed, 186 insertions(+), 56 deletions(-) create mode 100644 avenger-vega-test-data/vega-specs/clip/bar_rounded2.vg.json diff --git a/avenger-vega-renderer/js/index.js b/avenger-vega-renderer/js/index.js index e77f9fe..d70a74e 100644 --- a/avenger-vega-renderer/js/index.js +++ b/avenger-vega-renderer/js/index.js @@ -75,7 +75,6 @@ inherits(AvengerRenderer, Renderer, { this._handlerCanvas.setAttribute('class', 'marks'); // Create Avenger canvas - console.log("create: ", width, height, origin); this._avengerCanvasPromise = new AvengerCanvas(this._avengerHtmlCanvas, width, height, origin[0], origin[1]); this._lastRenderFinishTime = performance.now(); diff --git a/avenger-vega-renderer/js/marks/group.js b/avenger-vega-renderer/js/marks/group.js index 249180f..a2324b4 100644 --- a/avenger-vega-renderer/js/marks/group.js +++ b/avenger-vega-renderer/js/marks/group.js @@ -18,6 +18,8 @@ import {importRect} from "./rect.js"; * @property {number} y * @property {number} width * @property {number} height + * @property {number} x2 + * @property {number} y2 * @property {boolean} clip * @property {string|object} fill * @property {string|object} stroke @@ -35,7 +37,6 @@ import {importRect} from "./rect.js"; /** * @typedef {Object} GroupMarkSpec * @property {"group"} marktype - * @property {boolean} clip * @property {boolean} interactive * @property {GroupItemSpec[]} items * @property {string} name @@ -46,31 +47,36 @@ import {importRect} from "./rect.js"; /** * @param {GroupItemSpec} vegaGroup * @param {string} name + * @param {boolean} forceClip * @returns {GroupMark} */ -export function importGroup(vegaGroup, name) { +export function importGroup(vegaGroup, name, forceClip) { + + const width = vegaGroup.width ?? (vegaGroup.x2 != null? vegaGroup.x2 - vegaGroup.x: null); + const height = vegaGroup.height ?? (vegaGroup.y2 != null? vegaGroup.y2 - vegaGroup.y: null); + const groupMark = new GroupMark( - vegaGroup.x, vegaGroup.y, name, vegaGroup.width, vegaGroup.height + vegaGroup.x ?? 0, vegaGroup.y ?? 0, name, width, height ); - const forceClip = false; for (const vegaMark of vegaGroup.items) { + const clip = vegaGroup.clip || forceClip; switch (vegaMark.marktype) { case "symbol": - groupMark.add_symbol_mark(importSymbol(vegaMark, forceClip)); + groupMark.add_symbol_mark(importSymbol(vegaMark, clip)); break; case "rule": - groupMark.add_rule_mark(importRule(vegaMark, forceClip)); + groupMark.add_rule_mark(importRule(vegaMark, clip)); break; case "rect": - groupMark.add_rect_mark(importRect(vegaMark, forceClip)); + groupMark.add_rect_mark(importRect(vegaMark, clip)); break; case "text": - groupMark.add_text_mark(importText(vegaMark, forceClip)); + groupMark.add_text_mark(importText(vegaMark, clip)); break; case "group": for (const groupItem of vegaMark.items) { - groupMark.add_group_mark(importGroup(groupItem, vegaMark.name)); + groupMark.add_group_mark(importGroup(groupItem, vegaMark.name, clip)); } break; } @@ -95,8 +101,8 @@ export function importGroup(vegaGroup, name) { // set clip groupMark.set_clip( - vegaGroup.width, - vegaGroup.height, + width, + height, vegaGroup.cornerRadius, vegaGroup.cornerRadiusTopLeft, vegaGroup.cornerRadiusTopRight, diff --git a/avenger-vega-renderer/js/marks/rect.js b/avenger-vega-renderer/js/marks/rect.js index 2dc242b..7f86e5f 100644 --- a/avenger-vega-renderer/js/marks/rect.js +++ b/avenger-vega-renderer/js/marks/rect.js @@ -13,6 +13,8 @@ import {encodeSimpleArray} from "./util.js"; * @property {number} y * @property {number} width * @property {number} height + * @property {number} x2 + * @property {number} y2 * @property {number} cornerRadius * @property {number} opacity * @property {number} fillOpacity @@ -47,8 +49,6 @@ export function importRect(vegaRectMark, forceClip) { return rectMark; } - const firstItem = items[0]; - const x = new Float32Array(len).fill(0); const y = new Float32Array(len).fill(0); const width = new Float32Array(len).fill(0); @@ -83,9 +83,13 @@ export function importRect(vegaRectMark, forceClip) { } if (item.width != null) { width[i] = item.width; + } else if (item.x2 != null) { + width[i] = item.x2 - x[i]; } if (item.height != null) { height[i] = item.height; + } else if (item.y2 != null) { + height[i] = item.y2 - y[i]; } if (item.fill != null) { fill[i] = item.fill; diff --git a/avenger-vega-renderer/js/marks/scenegraph.js b/avenger-vega-renderer/js/marks/scenegraph.js index 63c1cc0..c28b2b2 100644 --- a/avenger-vega-renderer/js/marks/scenegraph.js +++ b/avenger-vega-renderer/js/marks/scenegraph.js @@ -11,7 +11,7 @@ import { importGroup } from "./group.js"; export function importScenegraph(groupMark, width, height, origin) { const sceneGraph = new SceneGraph(width, height, origin[0], origin[1]); for (const vegaGroup of groupMark.items) { - sceneGraph.add_group(importGroup(vegaGroup, groupMark.name)); + sceneGraph.add_group(importGroup(vegaGroup, groupMark.name, false)); } return sceneGraph; -} \ No newline at end of file +} diff --git a/avenger-vega-renderer/js/marks/symbol.js b/avenger-vega-renderer/js/marks/symbol.js index 7f62ba2..1c6e739 100644 --- a/avenger-vega-renderer/js/marks/symbol.js +++ b/avenger-vega-renderer/js/marks/symbol.js @@ -165,7 +165,6 @@ export function importSymbol(vegaSymbolMark, force_clip) { if (anyShape) { const encoded = encodeSimpleArray(shapes); - console.log() symbolMark.set_shape(encoded.values, encoded.indices); } diff --git a/avenger-vega-renderer/src/marks/group.rs b/avenger-vega-renderer/src/marks/group.rs index 663f50f..3794810 100644 --- a/avenger-vega-renderer/src/marks/group.rs +++ b/avenger-vega-renderer/src/marks/group.rs @@ -1,17 +1,17 @@ -use lyon_path::builder::BorderRadii; -use lyon_path::geom::Box2D; -use lyon_path::geom::euclid::Point2D; -use lyon_path::Winding; -use wasm_bindgen::{JsError, JsValue}; +use crate::marks::rect::RectMark; use crate::marks::rule::RuleMark; use crate::marks::symbol::SymbolMark; use crate::marks::text::TextMark; +use crate::marks::util::{decode_color, decode_gradient}; use avenger::marks::group::{Clip, SceneGroup as RsSceneGroup}; use avenger::marks::mark::SceneMark; -use wasm_bindgen::prelude::wasm_bindgen; use avenger::marks::value::EncodingValue; -use crate::marks::rect::RectMark; -use crate::marks::util::{decode_color, decode_gradient}; +use lyon_path::builder::BorderRadii; +use lyon_path::geom::euclid::Point2D; +use lyon_path::geom::Box2D; +use lyon_path::Winding; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; #[wasm_bindgen] pub struct GroupMark { @@ -67,21 +67,17 @@ impl GroupMark { ) { let clip = if let (Some(width), Some(height)) = (width, height) { let corner_radius = corner_radius.unwrap_or(0.0); - let corner_radius_top_left = - corner_radius_top_left.unwrap_or(corner_radius); - let corner_radius_top_right = - corner_radius_top_right.unwrap_or(corner_radius); - let corner_radius_bottom_left = corner_radius_bottom_left - .unwrap_or(corner_radius); - let corner_radius_bottom_right = corner_radius_bottom_right - .unwrap_or(corner_radius); + let corner_radius_top_left = corner_radius_top_left.unwrap_or(corner_radius); + let corner_radius_top_right = corner_radius_top_right.unwrap_or(corner_radius); + let corner_radius_bottom_left = corner_radius_bottom_left.unwrap_or(corner_radius); + let corner_radius_bottom_right = corner_radius_bottom_right.unwrap_or(corner_radius); if corner_radius_top_left > 0.0 || corner_radius_top_right > 0.0 || corner_radius_bottom_left > 0.0 || corner_radius_bottom_right > 0.0 { - // Rounded rectange path + // Rounded rectangle path let mut builder = lyon_path::Path::builder(); builder.add_rounded_rectangle( &Box2D::new(Point2D::new(0.0, 0.0), Point2D::new(width, height)), @@ -114,11 +110,7 @@ impl GroupMark { /// @param {string} color_value /// @param {number} opacity #[wasm_bindgen(skip_jsdoc)] - pub fn set_fill( - &mut self, - color_value: &str, - opacity: f32, - ) -> Result<(), JsError> { + pub fn set_fill(&mut self, color_value: &str, opacity: f32) -> Result<(), JsError> { self.inner.fill = Some(decode_color(color_value, opacity)?); Ok(()) } @@ -139,11 +131,7 @@ impl GroupMark { /// @param {string} color_value /// @param {number} opacity #[wasm_bindgen(skip_jsdoc)] - pub fn set_stroke( - &mut self, - color_value: &str, - opacity: f32, - ) -> Result<(), JsError> { + pub fn set_stroke(&mut self, color_value: &str, opacity: f32) -> Result<(), JsError> { self.inner.stroke = Some(decode_color(color_value, opacity)?); Ok(()) } diff --git a/avenger-vega-renderer/src/marks/rect.rs b/avenger-vega-renderer/src/marks/rect.rs index 0520234..f118813 100644 --- a/avenger-vega-renderer/src/marks/rect.rs +++ b/avenger-vega-renderer/src/marks/rect.rs @@ -47,7 +47,9 @@ impl RectMark { } pub fn set_corner_radius(&mut self, corner_radius: Vec) { - self.inner.corner_radius = EncodingValue::Array { values: corner_radius }; + self.inner.corner_radius = EncodingValue::Array { + values: corner_radius, + }; } pub fn set_stroke_width(&mut self, width: Vec) { diff --git a/avenger-vega-renderer/src/marks/util.rs b/avenger-vega-renderer/src/marks/util.rs index 040aeef..2fdcc03 100644 --- a/avenger-vega-renderer/src/marks/util.rs +++ b/avenger-vega-renderer/src/marks/util.rs @@ -57,17 +57,10 @@ pub fn decode_colors( Ok(colors) } -pub fn decode_color( - color_value: &str, - opacity: f32, -) -> Result { +pub fn decode_color(color_value: &str, opacity: f32) -> Result { Ok(match csscolorparser::parse(color_value) { - Ok(c) => { - ColorOrGradient::Color([c.r as f32, c.g as f32, c.b as f32, c.a as f32 * opacity]) - } - Err(_) => { - ColorOrGradient::Color([0.0, 0.0, 0.0, 0.0]) - } + Ok(c) => ColorOrGradient::Color([c.r as f32, c.g as f32, c.b as f32, c.a as f32 * opacity]), + Err(_) => ColorOrGradient::Color([0.0, 0.0, 0.0, 0.0]), }) } diff --git a/avenger-vega-renderer/test/test_baselines.py b/avenger-vega-renderer/test/test_baselines.py index 971e02a..e3f38cb 100644 --- a/avenger-vega-renderer/test/test_baselines.py +++ b/avenger-vega-renderer/test/test_baselines.py @@ -111,11 +111,11 @@ def failures_path(): ("clip", "text_clip", 0.006), ("clip", "text_clip_rounded", 0.006), + ("clip", "bar_rounded2", 0.0), # # TODO: # ("clip", "clip_mixed_marks", 0.0), # ("clip", "clip_rounded", 0.0), - # ("clip", "bar_rounded", 0.0), ], ) def test_image_baselines( @@ -134,6 +134,7 @@ def test_image_baselines( spec = json.load(f) comparison_res = compare(page, spec) + page.close() print(f"score: {comparison_res.score}") if comparison_res.score > tolerance: outdir = failures_path / category / spec_name diff --git a/avenger-vega-test-data/vega-specs/clip/bar_rounded2.vg.json b/avenger-vega-test-data/vega-specs/clip/bar_rounded2.vg.json new file mode 100644 index 0000000..9b31a2b --- /dev/null +++ b/avenger-vega-test-data/vega-specs/clip/bar_rounded2.vg.json @@ -0,0 +1,138 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "background": "white", + "padding": 5, + "height": 200, + "style": "cell", + "data": [ + { + "name": "source_0", + "url": "https://raw.githubusercontent.com/vega/vega-datasets/main/data/seattle-weather.csv", + "format": {"type": "csv", "parse": {"date": "date"}}, + "transform": [ + { + "field": "date", + "type": "timeunit", + "units": ["month"], + "as": ["month_date", "month_date_end"] + }, + { + "type": "aggregate", + "groupby": ["month_date", "weather"], + "ops": ["count"], + "fields": [null], + "as": ["__count"] + }, + { + "type": "stack", + "groupby": ["month_date"], + "field": "__count", + "sort": {"field": ["weather"], "order": ["descending"]}, + "as": ["__count_start", "__count_end"], + "offset": "zero" + } + ] + } + ], + "signals": [ + {"name": "x_step", "value": 20}, + { + "name": "width", + "update": "bandspace(domain('x').length, 0.1, 0.05) * x_step" + } + ], + "marks": [ + { + "type": "group", + "from": { + "facet": { + "data": "source_0", + "name": "stack_group_main", + "groupby": ["month_date", "month_date_end"], + "aggregate": { + "fields": [ + "__count_start", + "__count_start", + "__count_end", + "__count_end" + ], + "ops": ["min", "max", "min", "max"] + } + } + }, + "encode": { + "update": { + "x": {"scale": "x", "field": "month_date"}, + "width": {"signal": "max(0.25, bandwidth('x'))"}, + "y": { + "signal": "min(scale('y',datum[\"min___count_start\"]),scale('y',datum[\"max___count_start\"]),scale('y',datum[\"min___count_end\"]),scale('y',datum[\"max___count_end\"]))" + }, + "y2": { + "signal": "max(scale('y',datum[\"min___count_start\"]),scale('y',datum[\"max___count_start\"]),scale('y',datum[\"min___count_end\"]),scale('y',datum[\"max___count_end\"]))" + }, + "clip": {"value": true}, + "cornerRadiusTopLeft": {"value": 10}, + "cornerRadiusTopRight": {"value": 5} + } + }, + "marks": [ + { + "type": "group", + "encode": { + "update": { + "y": {"field": {"group": "y"}, "mult": -1}, + "width": {"field": {"group": "width"}} + } + }, + "marks": [ + { + "name": "marks", + "type": "rect", + "style": ["bar"], + "from": {"data": "stack_group_main"}, + "encode": { + "update": { + "fill": {"scale": "color", "field": "weather"}, + "ariaRoleDescription": {"value": "bar"}, + "description": { + "signal": "\"date (month): \" + (timeFormat(datum[\"month_date\"], timeUnitSpecifier([\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"}))) + \"; Count of Records: \" + (format(datum[\"__count\"], \"\")) + \"; weather: \" + (isValid(datum[\"weather\"]) ? datum[\"weather\"] : \"\"+datum[\"weather\"])" + }, + "width": {"field": {"group": "width"}}, + "y": {"scale": "y", "field": "__count_end"}, + "y2": {"scale": "y", "field": "__count_start"} + } + } + } + ] + } + ] + } + ], + "scales": [ + { + "name": "x", + "type": "band", + "domain": {"data": "source_0", "field": "month_date", "sort": true}, + "range": {"step": {"signal": "x_step"}}, + "paddingInner": 0.1, + "paddingOuter": 0.05 + }, + { + "name": "y", + "type": "linear", + "domain": { + "data": "source_0", + "fields": ["__count_start", "__count_end"] + }, + "range": [{"signal": "height"}, 0], + "nice": true, + "zero": true + }, + { + "name": "color", + "type": "ordinal", + "domain": {"data": "source_0", "field": "weather", "sort": true}, + "range": "category" + } + ] +} \ No newline at end of file From 08f3f1aa388e0d516eb6af04cf34bd471cba5c28 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 23 Apr 2024 07:07:48 -0400 Subject: [PATCH 10/22] support radial text --- avenger-vega-renderer/js/marks/text.js | 17 +++++++++++++++++ avenger-vega-renderer/src/marks/group.rs | 1 - 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/avenger-vega-renderer/js/marks/text.js b/avenger-vega-renderer/js/marks/text.js index b37c70e..0567c37 100644 --- a/avenger-vega-renderer/js/marks/text.js +++ b/avenger-vega-renderer/js/marks/text.js @@ -10,6 +10,10 @@ import {encodeSimpleArray} from "./util.js"; * @property {number} x * @property {number} y * @property {number} angle + * @property {number} radius + * @property {number} theta + * @property {number} dx + * @property {number} dy * @property {number} limit * @property {number} opacity * @property {number} fillOpacity @@ -88,6 +92,19 @@ export function importText(vegaTextMark, force_clip) { y[i] = item.y; } + if (item.radius != null && item.theta != null) { + x[i] += item.radius * Math.cos(item.theta - Math.PI / 2.0); + y[i] += item.radius * Math.sin(item.theta - Math.PI / 2.0); + } + + if (item.dx != null) { + x[i] += item.dx; + } + + if (item.dy != null) { + y[i] += item.dy; + } + if (item.text != null) { text[i] = item.text; } diff --git a/avenger-vega-renderer/src/marks/group.rs b/avenger-vega-renderer/src/marks/group.rs index 3794810..ebe06c8 100644 --- a/avenger-vega-renderer/src/marks/group.rs +++ b/avenger-vega-renderer/src/marks/group.rs @@ -5,7 +5,6 @@ use crate::marks::text::TextMark; use crate::marks::util::{decode_color, decode_gradient}; use avenger::marks::group::{Clip, SceneGroup as RsSceneGroup}; use avenger::marks::mark::SceneMark; -use avenger::marks::value::EncodingValue; use lyon_path::builder::BorderRadii; use lyon_path::geom::euclid::Point2D; use lyon_path::geom::Box2D; From c0deadca82c06b3a2d74a0d442a7c7f42a211149 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 23 Apr 2024 08:12:04 -0400 Subject: [PATCH 11/22] Fixes, add arc tests --- avenger-vega-renderer/js/marks/arc.js | 169 +++++++++++++++++++ avenger-vega-renderer/js/marks/group.js | 7 +- avenger-vega-renderer/js/marks/rect.js | 2 +- avenger-vega-renderer/src/marks/arc.rs | 132 +++++++++++++++ avenger-vega-renderer/src/marks/group.rs | 5 + avenger-vega-renderer/src/marks/mod.rs | 1 + avenger-vega-renderer/test/test_baselines.py | 12 +- 7 files changed, 325 insertions(+), 3 deletions(-) create mode 100644 avenger-vega-renderer/js/marks/arc.js create mode 100644 avenger-vega-renderer/src/marks/arc.rs diff --git a/avenger-vega-renderer/js/marks/arc.js b/avenger-vega-renderer/js/marks/arc.js new file mode 100644 index 0000000..9064278 --- /dev/null +++ b/avenger-vega-renderer/js/marks/arc.js @@ -0,0 +1,169 @@ +import {ArcMark} from "../../pkg/avenger_wasm.js"; +import {encodeSimpleArray} from "./util.js"; + + +/** + * Represents the style and configuration of a graphic element. + * @typedef {Object} ArcItem + * @property {number} x + * @property {number} y + * @property {number} startAngle + * @property {number} endAngle + * @property {number} outerRadius + * @property {number} innerRadius + * @property {string|object} fill + * @property {string|object} stroke + * @property {number} strokeWidth + * @property {number} opacity + * @property {number} fillOpacity + * @property {number} strokeOpacity + * @property {number} zindex + */ + +/** + * Represents a graphical object configuration. + * @typedef {Object} ArcMarkSpec + * @property {"arc"} marktype + * @property {boolean} clip + * @property {ArcItem[]} items + * @property {string} name + * @property {number} zindex + */ + +/** + * @param {ArcMarkSpec} vegaArcMark + * @param {boolean} forceClip + * @returns {ArcMark} + */ +export function importArc(vegaArcMark, forceClip) { + const items = vegaArcMark.items; + const len = items.length; + + const arcMark = new ArcMark( + len, vegaArcMark.clip || forceClip, vegaArcMark.name, vegaArcMark.zindex + ); + if (len === 0) { + return arcMark; + } + + const x = new Float32Array(len).fill(0); + const y = new Float32Array(len).fill(0); + + const startAngle = new Float32Array(len).fill(0); + let anyStartAngle = false; + + const endAngle = new Float32Array(len).fill(0); + let anyEndAngle = false; + + const outerRadius = new Float32Array(len).fill(0); + let anyOuterRadius = false; + + const innerRadius = new Float32Array(len).fill(0); + let anyInnerRadius = false; + + const fill = new Array(len).fill(""); + let anyFill = false; + let anyFillIsGradient = false; + + const stroke = new Array(len).fill(""); + let anyStroke = false; + let anyStrokeIsGradient = false; + + const strokeWidth = new Float32Array(len); + let anyStrokeWidth = false; + + const strokeOpacity = new Float32Array(len).fill(1); + const fillOpacity = new Float32Array(len).fill(1); + + const zindex = new Int32Array(len).fill(0); + let anyZindex = false; + + items.forEach((item, i) => { + if (item.x != null) { + x[i] = item.x; + } + if (item.y != null) { + y[i] = item.y; + } + if (item.startAngle != null) { + startAngle[i] = item.startAngle; + anyStartAngle ||= true; + } + if (item.endAngle != null) { + endAngle[i] = item.endAngle; + anyEndAngle ||= true; + } + if (item.outerRadius != null) { + outerRadius[i] = item.outerRadius; + anyOuterRadius ||= true; + } + if (item.innerRadius != null) { + innerRadius[i] = item.innerRadius; + anyInnerRadius ||= true; + } + if (item.fill != null) { + fill[i] = item.fill; + anyFill ||= true; + anyFillIsGradient ||= typeof item.fill === "object"; + } + fillOpacity[i] = (item.fillOpacity ?? 1) * (item.opacity ?? 1); + + if (item.stroke != null) { + stroke[i] = item.stroke; + anyStroke ||= true; + anyStrokeIsGradient ||= typeof item.stroke === "object"; + } + if (item.strokeWidth != null) { + strokeWidth[i] = item.strokeWidth; + anyStrokeWidth ||= true; + } + strokeOpacity[i] = (item.strokeOpacity ?? 1) * (item.opacity ?? 1); + + if (item.zindex != null) { + zindex[i] = item.zindex; + anyZindex ||= true; + } + }) + + arcMark.set_xy(x, y); + if (anyStartAngle) { + arcMark.set_start_angle(startAngle); + } + if (anyEndAngle) { + arcMark.set_end_angle(endAngle); + } + if (anyOuterRadius) { + arcMark.set_outer_radius(outerRadius); + } + if (anyInnerRadius) { + arcMark.set_inner_radius(innerRadius) + } + + if (anyFill) { + if (anyFillIsGradient) { + arcMark.set_fill_gradient(fill, fillOpacity); + } else { + const encoded = encodeSimpleArray(fill); + arcMark.set_fill(encoded.values, encoded.indices, fillOpacity); + } + } + + if (anyStroke) { + if (anyStrokeIsGradient) { + arcMark.set_stroke_gradient(stroke, strokeOpacity); + } else { + const encoded = encodeSimpleArray(stroke); + arcMark.set_stroke(encoded.values, encoded.indices, strokeOpacity); + } + } + + if (anyStrokeWidth) { + arcMark.set_stroke_width(strokeWidth); + } + + if (anyZindex) { + arcMark.set_zindex(zindex); + } + + return arcMark; +} diff --git a/avenger-vega-renderer/js/marks/group.js b/avenger-vega-renderer/js/marks/group.js index a2324b4..3b70bbf 100644 --- a/avenger-vega-renderer/js/marks/group.js +++ b/avenger-vega-renderer/js/marks/group.js @@ -3,17 +3,19 @@ import { importSymbol } from "./symbol.js" import { importRule } from "./rule.js"; import {importText} from "./text.js"; import {importRect} from "./rect.js"; +import {importArc} from "./arc.js";; /** * @typedef {import('./symbol.js').SymbolMarkSpec} SymbolMarkSpec * @typedef {import('./text.js').TextMarkSpec} TextMarkSpec * @typedef {import('./rule.js').RuleMarkSpec} RuleMarkSpec * @typedef {import('./rect.js').RectMarkSpec} RectMarkSpec + * @typedef {import('./arc.js').ArcMarkSpec} ArcMarkSpec * * * @typedef {Object} GroupItemSpec * @property {"group"} marktype - * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec)[]} items + * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec|ArcMarkSpec)[]} items * @property {number} x * @property {number} y * @property {number} width @@ -71,6 +73,9 @@ export function importGroup(vegaGroup, name, forceClip) { case "rect": groupMark.add_rect_mark(importRect(vegaMark, clip)); break; + case "arc": + groupMark.add_arc_mark(importArc(vegaMark, clip)); + break; case "text": groupMark.add_text_mark(importText(vegaMark, clip)); break; diff --git a/avenger-vega-renderer/js/marks/rect.js b/avenger-vega-renderer/js/marks/rect.js index 7f86e5f..4187afb 100644 --- a/avenger-vega-renderer/js/marks/rect.js +++ b/avenger-vega-renderer/js/marks/rect.js @@ -129,7 +129,7 @@ export function importRect(vegaRectMark, forceClip) { rectMark.set_fill_gradient(fill, fillOpacity); } else { const encoded = encodeSimpleArray(fill); - rectMark.set_fill(encoded.values, encoded.indices, strokeOpacity); + rectMark.set_fill(encoded.values, encoded.indices, fillOpacity); } } diff --git a/avenger-vega-renderer/src/marks/arc.rs b/avenger-vega-renderer/src/marks/arc.rs new file mode 100644 index 0000000..d68736a --- /dev/null +++ b/avenger-vega-renderer/src/marks/arc.rs @@ -0,0 +1,132 @@ +use crate::marks::util::{decode_colors, decode_gradients, zindex_to_indices}; +use avenger::marks::arc::ArcMark as RsArcMark; +use avenger::marks::value::EncodingValue; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; +use crate::log; + +#[wasm_bindgen] +pub struct ArcMark { + inner: RsArcMark, +} + +impl ArcMark { + pub fn build(self) -> RsArcMark { + self.inner + } +} + +#[wasm_bindgen] +impl ArcMark { + #[wasm_bindgen(constructor)] + pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { + Self { + inner: RsArcMark { + len, + clip, + name: name.unwrap_or_default(), + zindex, + ..Default::default() + }, + } + } + pub fn set_zindex(&mut self, zindex: Vec) { + self.inner.indices = Some(zindex_to_indices(zindex)); + } + + pub fn set_xy(&mut self, x: Vec, y: Vec) { + self.inner.x = EncodingValue::Array { values: x }; + self.inner.y = EncodingValue::Array { values: y }; + } + + pub fn set_start_angle(&mut self, start_angle: Vec) { + self.inner.start_angle = EncodingValue::Array { values: start_angle }; + } + + pub fn set_end_angle(&mut self, end_angle: Vec) { + self.inner.end_angle = EncodingValue::Array { values: end_angle }; + } + + pub fn set_outer_radius(&mut self, outer_radius: Vec) { + self.inner.outer_radius = EncodingValue::Array { values: outer_radius }; + } + + pub fn set_inner_radius(&mut self, inner_radius: Vec) { + self.inner.inner_radius = EncodingValue::Array { values: inner_radius }; + } + + pub fn set_corner_radius(&mut self, corner_radius: Vec) { + self.inner.corner_radius = EncodingValue::Array { + values: corner_radius, + }; + } + + + /// Set stroke color. + /// + /// @param {string[]} color_values + /// @param {Uint32Array} indices + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_stroke( + &mut self, + color_values: JsValue, + indices: Vec, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.stroke = EncodingValue::Array { + values: decode_colors(color_values, indices, opacity)?, + }; + Ok(()) + } + + /// Set stroke gradient + /// + /// @param {(string|object)[]} values + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_stroke_gradient( + &mut self, + values: JsValue, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.stroke = EncodingValue::Array { + values: decode_gradients(values, opacity, &mut self.inner.gradients)?, + }; + Ok(()) + } + + /// Set fill color + /// + /// @param {string[]} color_values + /// @param {Uint32Array} indices + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_fill( + &mut self, + color_values: JsValue, + indices: Vec, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.fill = EncodingValue::Array { + values: decode_colors(color_values, indices, opacity)?, + }; + Ok(()) + } + + /// Set fill gradient + /// + /// @param {(string|object)[]} values + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_fill_gradient(&mut self, values: JsValue, opacity: Vec) -> Result<(), JsError> { + self.inner.fill = EncodingValue::Array { + values: decode_gradients(values, opacity, &mut self.inner.gradients)?, + }; + Ok(()) + } + + pub fn set_stroke_width(&mut self, width: Vec) { + self.inner.stroke_width = EncodingValue::Array { values: width } + } +} diff --git a/avenger-vega-renderer/src/marks/group.rs b/avenger-vega-renderer/src/marks/group.rs index ebe06c8..a7d26eb 100644 --- a/avenger-vega-renderer/src/marks/group.rs +++ b/avenger-vega-renderer/src/marks/group.rs @@ -2,6 +2,7 @@ use crate::marks::rect::RectMark; use crate::marks::rule::RuleMark; use crate::marks::symbol::SymbolMark; use crate::marks::text::TextMark; +use crate::marks::arc::ArcMark; use crate::marks::util::{decode_color, decode_gradient}; use avenger::marks::group::{Clip, SceneGroup as RsSceneGroup}; use avenger::marks::mark::SceneMark; @@ -168,6 +169,10 @@ impl GroupMark { .push(SceneMark::Text(Box::new(mark.build()))); } + pub fn add_arc_mark(&mut self, mark: ArcMark) { + self.inner.marks.push(SceneMark::Arc(mark.build())); + } + pub fn add_group_mark(&mut self, mark: GroupMark) { self.inner.marks.push(SceneMark::Group(mark.inner)); } diff --git a/avenger-vega-renderer/src/marks/mod.rs b/avenger-vega-renderer/src/marks/mod.rs index d7bf61f..36a233c 100644 --- a/avenger-vega-renderer/src/marks/mod.rs +++ b/avenger-vega-renderer/src/marks/mod.rs @@ -4,3 +4,4 @@ pub mod symbol; pub mod text; pub mod util; pub mod rect; +pub mod arc; diff --git a/avenger-vega-renderer/test/test_baselines.py b/avenger-vega-renderer/test/test_baselines.py index e3f38cb..159d5bb 100644 --- a/avenger-vega-renderer/test/test_baselines.py +++ b/avenger-vega-renderer/test/test_baselines.py @@ -96,9 +96,17 @@ def failures_path(): ("text", "text_rotation", 0.016), ("text", "letter_scatter", 0.027), ("text", "lasagna_plot", 0.04), - # ("text", "arc_radial", 0.0001), + ("text", "arc_radial", 0.005), ("text", "emoji", 0.05), + ("arc", "single_arc_no_inner", 0.0001), + ("arc", "single_arc_with_inner_radius", 0.0001), + ("arc", "single_arc_with_inner_radius_wrap", 0.0001), + ("arc", "single_arc_with_inner_radius_wrap_stroke", 0.0001), + ("arc", "arcs_with_variable_outer_radius", 0.0001), + ("arc", "arcs_with_variable_outer_radius_stroke", 0.0001), + ("arc", "arc_with_stroke", 0.0001), + ("gradients", "symbol_cross_gradient", 0.0001), ("gradients", "symbol_circles_gradient_stroke", 0.0001), ("gradients", "symbol_radial_gradient", 0.0002), @@ -108,6 +116,7 @@ def failures_path(): ("gradients", "default_gradient_bars_rounded_stroke", 0.0001), ("gradients", "residuals_colorscale", 0.001), ("gradients", "stroke_rect_gradient", 0.0001), + ("gradients", "arc_gradient", 0.0001), ("clip", "text_clip", 0.006), ("clip", "text_clip_rounded", 0.006), @@ -192,6 +201,7 @@ def spec_to_image( f"vegaEmbed('#plot-container', {json.dumps(spec)}, {json.dumps(embed_opts)});" ) page.evaluate_handle(script) + page.wait_for_timeout(100) if renderer == "svg": locator = page.locator("svg") else: From c6b68cb70efce272bf7c5086aaa5fa092dfde846 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 23 Apr 2024 17:14:37 -0400 Subject: [PATCH 12/22] Add path mark --- Cargo.lock | 1 + avenger-vega-renderer/Cargo.toml | 1 + avenger-vega-renderer/js/marks/group.js | 9 +- avenger-vega-renderer/js/marks/path.js | 146 +++++++++++++++++++ avenger-vega-renderer/src/marks/group.rs | 5 + avenger-vega-renderer/src/marks/mod.rs | 1 + avenger-vega-renderer/src/marks/path.rs | 145 ++++++++++++++++++ avenger-vega-renderer/test/test_baselines.py | 8 + 8 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 avenger-vega-renderer/js/marks/path.js create mode 100644 avenger-vega-renderer/src/marks/path.rs diff --git a/Cargo.lock b/Cargo.lock index c4aca0a..10878a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -396,6 +396,7 @@ dependencies = [ "csscolorparser", "gloo-utils", "image", + "itertools 0.12.0", "js-sys", "lazy_static", "lyon_path", diff --git a/avenger-vega-renderer/Cargo.toml b/avenger-vega-renderer/Cargo.toml index cba4a57..79efb8b 100644 --- a/avenger-vega-renderer/Cargo.toml +++ b/avenger-vega-renderer/Cargo.toml @@ -19,6 +19,7 @@ lazy_static = "1.4.0" serde_json = "1.0.114" csscolorparser = "0.6.2" lyon_path = "*" +itertools = "0.12.0" wasm-bindgen = { version = "=0.2.92" } wasm-bindgen-futures = "0.4.30" gloo-utils = { version = "0.2.0", features = ["serde"] } diff --git a/avenger-vega-renderer/js/marks/group.js b/avenger-vega-renderer/js/marks/group.js index 3b70bbf..2da5ea9 100644 --- a/avenger-vega-renderer/js/marks/group.js +++ b/avenger-vega-renderer/js/marks/group.js @@ -3,7 +3,8 @@ import { importSymbol } from "./symbol.js" import { importRule } from "./rule.js"; import {importText} from "./text.js"; import {importRect} from "./rect.js"; -import {importArc} from "./arc.js";; +import {importArc} from "./arc.js"; +import {importPath} from "./path.js"; /** * @typedef {import('./symbol.js').SymbolMarkSpec} SymbolMarkSpec @@ -11,11 +12,12 @@ import {importArc} from "./arc.js";; * @typedef {import('./rule.js').RuleMarkSpec} RuleMarkSpec * @typedef {import('./rect.js').RectMarkSpec} RectMarkSpec * @typedef {import('./arc.js').ArcMarkSpec} ArcMarkSpec + * @typedef {import('./path.js').PathMarkSpec} PathMarkSpec * * * @typedef {Object} GroupItemSpec * @property {"group"} marktype - * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec|ArcMarkSpec)[]} items + * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec|ArcMarkSpec|PathMarkSpec)[]} items * @property {number} x * @property {number} y * @property {number} width @@ -76,6 +78,9 @@ export function importGroup(vegaGroup, name, forceClip) { case "arc": groupMark.add_arc_mark(importArc(vegaMark, clip)); break; + case "path": + groupMark.add_path_mark(importPath(vegaMark, clip)); + break; case "text": groupMark.add_text_mark(importText(vegaMark, clip)); break; diff --git a/avenger-vega-renderer/js/marks/path.js b/avenger-vega-renderer/js/marks/path.js new file mode 100644 index 0000000..16ca439 --- /dev/null +++ b/avenger-vega-renderer/js/marks/path.js @@ -0,0 +1,146 @@ +import {PathMark} from "../../pkg/avenger_wasm.js"; +import {encodeSimpleArray} from "./util.js"; + + +/** + * @typedef {Object} PathItem + * @property {number} strokeWidth + * @property {string|object} fill + * @property {string|object} stroke + * @property {number} x + * @property {number} y + * @property {number} scaleX + * @property {number} scaleY + * @property {number} angle + + * @property {number} opacity + * @property {number} strokeOpacity + * @property {number} fillOpacity + * @property {string} path + * @property {number} zindex + */ + +/** + * @typedef {Object} PathMarkSpec + * @property {"path"} marktype + * @property {boolean} clip + * @property {boolean} interactive + * @property {PathItem[]} items + * @property {string} name + * @property {string} role + * @property {number} zindex + */ + +/** + * @param {PathMarkSpec} vegaPathMark + * @param {boolean} force_clip + * @returns {PathMark} + */ +export function importPath(vegaPathMark, force_clip) { + console.log(vegaPathMark); + const items = vegaPathMark.items; + const len = items.length; + + const pathMark = new PathMark( + len, vegaPathMark.clip || force_clip, vegaPathMark.name, vegaPathMark.zindex + ); + + // Handle empty mark + if (len === 0) { + return pathMark; + } + + // Only include stroke_width if there is a stroke color + const firstItem = items[0]; + const firstHasStroke = firstItem.stroke != null; + let strokeWidth; + if (firstHasStroke) { + strokeWidth = firstItem.strokeWidth ?? 1; + } + pathMark.set_stroke_width(strokeWidth); + + // Semi-required values get initialized + const x = new Float32Array(len).fill(0); + const y = new Float32Array(len).fill(0); + const scale_x = new Float32Array(len).fill(1); + const scale_y = new Float32Array(len).fill(1); + const angle = new Float32Array(len).fill(0); + + const fill = new Array(len).fill("");; + let anyFill = false; + let anyFillIsGradient = false; + + const stroke = new Array(len).fill("");; + let anyStroke = false; + let anyStrokeIsGradient = false; + + const zindex = new Int32Array(len).fill(0); + let anyZindex = false; + + const fillOpacity = new Float32Array(len).fill(1); + const strokeOpacity = new Float32Array(len).fill(1); + + const path = new Array(len).fill(""); + + items.forEach((item, i) => { + x[i] = item.x ?? 0; + y[i] = item.y ?? 0; + scale_x[i] = item.scaleX ?? 1; + scale_y[i] = item.scaleY ?? 1; + angle[i] = item.angle ?? 0; + + const baseOpacity = item.opacity ?? 1; + fillOpacity[i] = (item.fillOpacity ?? 1) * baseOpacity; + strokeOpacity[i] = (item.strokeOpacity ?? 1) * baseOpacity; + + if (item.fill != null) { + fill[i] = item.fill; + anyFill ||= true; + anyFillIsGradient ||= typeof item.fill === "object"; + } + + if (item.stroke != null) { + stroke[i] = item.stroke; + anyStroke ||= true; + anyStrokeIsGradient ||= typeof item.stroke === "object"; + } + + if (item.zindex != null) { + zindex[i] = item.zindex; + anyZindex ||= true; + } + + if (item.path != null) { + path[i] = item.path; + } + }) + + pathMark.set_transform(x, y, scale_x, scale_y, angle); + + const encodedPaths = encodeSimpleArray(path); + pathMark.set_path(encodedPaths.values, encodedPaths.indices); + + if (anyFill) { + if (anyFillIsGradient) { + pathMark.set_fill_gradient(fill, fillOpacity); + } else { + const encoded = encodeSimpleArray(fill); + pathMark.set_fill(encoded.values, encoded.indices, fillOpacity); + } + } + + if (anyStroke) { + if (anyStrokeIsGradient) { + pathMark.set_stroke_gradient(stroke, strokeOpacity); + } else { + const encoded = encodeSimpleArray(stroke); + pathMark.set_stroke(encoded.values, encoded.indices, strokeOpacity); + } + } + + if (anyZindex) { + pathMark.set_zindex(zindex); + } + + return pathMark; +} diff --git a/avenger-vega-renderer/src/marks/group.rs b/avenger-vega-renderer/src/marks/group.rs index a7d26eb..86f02b3 100644 --- a/avenger-vega-renderer/src/marks/group.rs +++ b/avenger-vega-renderer/src/marks/group.rs @@ -12,6 +12,7 @@ use lyon_path::geom::Box2D; use lyon_path::Winding; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsError, JsValue}; +use crate::marks::path::PathMark; #[wasm_bindgen] pub struct GroupMark { @@ -173,6 +174,10 @@ impl GroupMark { self.inner.marks.push(SceneMark::Arc(mark.build())); } + pub fn add_path_mark(&mut self, mark: PathMark) { + self.inner.marks.push(SceneMark::Path(mark.build())); + } + pub fn add_group_mark(&mut self, mark: GroupMark) { self.inner.marks.push(SceneMark::Group(mark.inner)); } diff --git a/avenger-vega-renderer/src/marks/mod.rs b/avenger-vega-renderer/src/marks/mod.rs index 36a233c..56f7056 100644 --- a/avenger-vega-renderer/src/marks/mod.rs +++ b/avenger-vega-renderer/src/marks/mod.rs @@ -5,3 +5,4 @@ pub mod text; pub mod util; pub mod rect; pub mod arc; +pub mod path; diff --git a/avenger-vega-renderer/src/marks/path.rs b/avenger-vega-renderer/src/marks/path.rs new file mode 100644 index 0000000..377fc49 --- /dev/null +++ b/avenger-vega-renderer/src/marks/path.rs @@ -0,0 +1,145 @@ +use gloo_utils::format::JsValueSerdeExt; +use itertools::izip; +use lyon_path::geom::Angle; +use lyon_path::geom::euclid::Vector2D; +use crate::marks::util::{decode_colors, decode_gradients, zindex_to_indices}; +use avenger::marks::path::{PathMark as RsPathMark, PathTransform}; +use avenger::marks::value::EncodingValue; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; +use avenger::error::AvengerError; +use avenger::marks::symbol::{parse_svg_path, SymbolShape}; +use crate::log; + +#[wasm_bindgen] +pub struct PathMark { + inner: RsPathMark, +} + +impl PathMark { + pub fn build(self) -> RsPathMark { + log(&format!("{:#?}", self.inner)); + self.inner + } +} + +#[wasm_bindgen] +impl PathMark { + #[wasm_bindgen(constructor)] + pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { + Self { + inner: RsPathMark { + len, + clip, + name: name.unwrap_or_default(), + zindex, + ..Default::default() + }, + } + } + pub fn set_zindex(&mut self, zindex: Vec) { + self.inner.indices = Some(zindex_to_indices(zindex)); + } + + pub fn set_transform( + &mut self, + xs: Vec, + ys: Vec, + scale_xs: Vec, + scale_ys: Vec, + angles: Vec, + ) { + let transforms = izip!(xs, ys, scale_xs, scale_ys, angles).map(|(x, y, scale_x, scale_y, angle)| { + PathTransform::scale(scale_x, scale_y) + .then_rotate(Angle::degrees(angle)) + .then_translate(Vector2D::new(x, y)) + }).collect::>(); + self.inner.transform = EncodingValue::Array { values: transforms }; + } + + /// Set path as SVG string + /// + /// @param {string[]} path_values + /// @param {Uint32Array} indices + #[wasm_bindgen(skip_jsdoc)] + pub fn set_path(&mut self, path_values: JsValue, indices: Vec) -> Result<(), JsError> { + let svg_paths: Vec = path_values.into_serde()?; + let unique_paths = svg_paths + .iter() + .map(|s| parse_svg_path(s)) + .collect::, AvengerError>>() + .map_err(|_| JsError::new("Failed to parse shapes"))?; + + let paths = indices.into_iter().map(|i| unique_paths[i].clone()).collect::>(); + self.inner.path = EncodingValue::Array { values: paths }; + Ok(()) + } + + /// Set stroke color. + /// + /// @param {string[]} color_values + /// @param {Uint32Array} indices + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_stroke( + &mut self, + color_values: JsValue, + indices: Vec, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.stroke = EncodingValue::Array { + values: decode_colors(color_values, indices, opacity)?, + }; + Ok(()) + } + + /// Set stroke gradient + /// + /// @param {(string|object)[]} values + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_stroke_gradient( + &mut self, + values: JsValue, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.stroke = EncodingValue::Array { + values: decode_gradients(values, opacity, &mut self.inner.gradients)?, + }; + Ok(()) + } + + /// Set fill color + /// + /// @param {string[]} color_values + /// @param {Uint32Array} indices + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_fill( + &mut self, + color_values: JsValue, + indices: Vec, + opacity: Vec, + ) -> Result<(), JsError> { + self.inner.fill = EncodingValue::Array { + values: decode_colors(color_values, indices, opacity)?, + }; + Ok(()) + } + + /// Set fill gradient + /// + /// @param {(string|object)[]} values + /// @param {Float32Array} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_fill_gradient(&mut self, values: JsValue, opacity: Vec) -> Result<(), JsError> { + self.inner.fill = EncodingValue::Array { + values: decode_gradients(values, opacity, &mut self.inner.gradients)?, + }; + Ok(()) + } + + pub fn set_stroke_width(&mut self, width: Option) { + self.inner.stroke_width = width + } +} diff --git a/avenger-vega-renderer/test/test_baselines.py b/avenger-vega-renderer/test/test_baselines.py index 159d5bb..8838a6d 100644 --- a/avenger-vega-renderer/test/test_baselines.py +++ b/avenger-vega-renderer/test/test_baselines.py @@ -107,6 +107,13 @@ def failures_path(): ("arc", "arcs_with_variable_outer_radius_stroke", 0.0001), ("arc", "arc_with_stroke", 0.0001), + ("path", "single_path_no_stroke", 0.0), + ("path", "multi_path_no_stroke", 0.0), + ("path", "single_path_with_stroke", 0.0), + ("path", "single_path_with_stroke_no_fill", 0.0), + ("path", "multi_path_with_stroke", 0.0), + ("path", "multi_path_with_stroke_no_fill", 0.0), + ("gradients", "symbol_cross_gradient", 0.0001), ("gradients", "symbol_circles_gradient_stroke", 0.0001), ("gradients", "symbol_radial_gradient", 0.0002), @@ -117,6 +124,7 @@ def failures_path(): ("gradients", "residuals_colorscale", 0.001), ("gradients", "stroke_rect_gradient", 0.0001), ("gradients", "arc_gradient", 0.0001), + ("gradients", "path_with_stroke_gradients", 0.0), ("clip", "text_clip", 0.006), ("clip", "text_clip_rounded", 0.006), From 45ca51082eee3b34fa583ef9e5c2d2c043968d67 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 23 Apr 2024 19:01:33 -0400 Subject: [PATCH 13/22] Add shape mark --- avenger-vega-renderer/js/marks/group.js | 16 +- avenger-vega-renderer/js/marks/shape.js | 155 +++++++++++++++++++ avenger-vega-renderer/test/test_baselines.py | 4 + 3 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 avenger-vega-renderer/js/marks/shape.js diff --git a/avenger-vega-renderer/js/marks/group.js b/avenger-vega-renderer/js/marks/group.js index 2da5ea9..dac1dfb 100644 --- a/avenger-vega-renderer/js/marks/group.js +++ b/avenger-vega-renderer/js/marks/group.js @@ -1,10 +1,11 @@ import {GroupMark} from "../../pkg/avenger_wasm.js"; import { importSymbol } from "./symbol.js" import { importRule } from "./rule.js"; -import {importText} from "./text.js"; -import {importRect} from "./rect.js"; -import {importArc} from "./arc.js"; -import {importPath} from "./path.js"; +import { importText } from "./text.js"; +import { importRect } from "./rect.js"; +import { importArc } from "./arc.js"; +import { importPath } from "./path.js"; +import { importShape } from "./shape.js"; /** * @typedef {import('./symbol.js').SymbolMarkSpec} SymbolMarkSpec @@ -13,11 +14,11 @@ import {importPath} from "./path.js"; * @typedef {import('./rect.js').RectMarkSpec} RectMarkSpec * @typedef {import('./arc.js').ArcMarkSpec} ArcMarkSpec * @typedef {import('./path.js').PathMarkSpec} PathMarkSpec - * + * @typedef {import('./shape.js').ShapeMarkSpec} ShapeMarkSpec * * @typedef {Object} GroupItemSpec * @property {"group"} marktype - * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec|ArcMarkSpec|PathMarkSpec)[]} items + * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec|ArcMarkSpec|PathMarkSpec|ShapeMarkSpec)[]} items * @property {number} x * @property {number} y * @property {number} width @@ -81,6 +82,9 @@ export function importGroup(vegaGroup, name, forceClip) { case "path": groupMark.add_path_mark(importPath(vegaMark, clip)); break; + case "shape": + groupMark.add_path_mark(importShape(vegaMark, clip)); + break; case "text": groupMark.add_text_mark(importText(vegaMark, clip)); break; diff --git a/avenger-vega-renderer/js/marks/shape.js b/avenger-vega-renderer/js/marks/shape.js new file mode 100644 index 0000000..4c2939c --- /dev/null +++ b/avenger-vega-renderer/js/marks/shape.js @@ -0,0 +1,155 @@ +import {PathMark} from "../../pkg/avenger_wasm.js"; +import {encodeSimpleArray} from "./util.js"; + + +/** + @typedef {function(ShapeItem): string} ShapeFunction + */ + +/** + * @typedef {Object} ShapeItem + * @property {string|object} fill + * @property {string|object} stroke + * @property {number} x + * @property {number} y + * @property {number} scaleX + * @property {number} scaleY + * @property {number} angle + * + * @property {number} strokeWidth + * @property {string} strokeCap + * @property {string} strokeJoin + * + * @property {number} opacity + * @property {number} strokeOpacity + * @property {number} fillOpacity + * @property {ShapeFunction} shape + * @property {number} zindex + */ + +/** + * @typedef {Object} ShapeMarkSpec + * @property {"shape"} marktype + * @property {boolean} clip + * @property {boolean} interactive + * @property {ShapeItem[]} items + * @property {string} name + * @property {string} role + * @property {number} zindex + */ + +/** + * @param {ShapeMarkSpec} vegaShapeMark + * @param {boolean} force_clip + * @returns {PathMark} + */ +export function importShape(vegaShapeMark, force_clip) { + console.log(vegaShapeMark); + const items = vegaShapeMark.items; + const len = items.length; + + const pathMark = new PathMark( + len, vegaShapeMark.clip || force_clip, vegaShapeMark.name, vegaShapeMark.zindex + ); + + // Handle empty mark + if (len === 0) { + return pathMark; + } + + // Only include stroke_width if there is a stroke color + const firstItem = items[0]; + const firstHasStroke = firstItem.stroke != null; + let strokeWidth; + if (firstHasStroke) { + strokeWidth = firstItem.strokeWidth ?? 1; + } + pathMark.set_stroke_width(strokeWidth); + + // Semi-required values get initialized + const x = new Float32Array(len).fill(0); + const y = new Float32Array(len).fill(0); + const scale_x = new Float32Array(len).fill(1); + const scale_y = new Float32Array(len).fill(1); + const angle = new Float32Array(len).fill(0); + + const fill = new Array(len).fill("");; + let anyFill = false; + let anyFillIsGradient = false; + + const stroke = new Array(len).fill("");; + let anyStroke = false; + let anyStrokeIsGradient = false; + + const zindex = new Int32Array(len).fill(0); + let anyZindex = false; + + const fillOpacity = new Float32Array(len).fill(1); + const strokeOpacity = new Float32Array(len).fill(1); + + const path = new Array(len).fill(""); + + items.forEach((item, i) => { + x[i] = item.x ?? 0; + y[i] = item.y ?? 0; + scale_x[i] = item.scaleX ?? 1; + scale_y[i] = item.scaleY ?? 1; + angle[i] = item.angle ?? 0; + + const baseOpacity = item.opacity ?? 1; + fillOpacity[i] = (item.fillOpacity ?? 1) * baseOpacity; + strokeOpacity[i] = (item.strokeOpacity ?? 1) * baseOpacity; + + if (item.fill != null) { + fill[i] = item.fill; + anyFill ||= true; + anyFillIsGradient ||= typeof item.fill === "object"; + } + + if (item.stroke != null) { + stroke[i] = item.stroke; + anyStroke ||= true; + anyStrokeIsGradient ||= typeof item.stroke === "object"; + } + + if (item.zindex != null) { + zindex[i] = item.zindex; + anyZindex ||= true; + } + + if (item.shape != null) { + // @ts-ignore + item.shape.context(); + path[i] = item.shape(item) ?? ""; + } + }) + + pathMark.set_transform(x, y, scale_x, scale_y, angle); + + const encodedPaths = encodeSimpleArray(path); + pathMark.set_path(encodedPaths.values, encodedPaths.indices); + + if (anyFill) { + if (anyFillIsGradient) { + pathMark.set_fill_gradient(fill, fillOpacity); + } else { + const encoded = encodeSimpleArray(fill); + pathMark.set_fill(encoded.values, encoded.indices, fillOpacity); + } + } + + if (anyStroke) { + if (anyStrokeIsGradient) { + pathMark.set_stroke_gradient(stroke, strokeOpacity); + } else { + const encoded = encodeSimpleArray(stroke); + pathMark.set_stroke(encoded.values, encoded.indices, strokeOpacity); + } + } + + if (anyZindex) { + pathMark.set_zindex(zindex); + } + + return pathMark; +} diff --git a/avenger-vega-renderer/test/test_baselines.py b/avenger-vega-renderer/test/test_baselines.py index 8838a6d..2e0af4a 100644 --- a/avenger-vega-renderer/test/test_baselines.py +++ b/avenger-vega-renderer/test/test_baselines.py @@ -113,6 +113,10 @@ def failures_path(): ("path", "single_path_with_stroke_no_fill", 0.0), ("path", "multi_path_with_stroke", 0.0), ("path", "multi_path_with_stroke_no_fill", 0.0), + ("shape", "us-counties", 0.0001), + ("shape", "us-map", 0.0001), + ("shape", "world-natural-earth-projection", 0.0001), + ("shape", "london_tubes", 0.0001), ("gradients", "symbol_cross_gradient", 0.0001), ("gradients", "symbol_circles_gradient_stroke", 0.0001), From b57766a28394f9fd51c0963dfccf5b02f6412880 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 24 Apr 2024 08:34:16 -0400 Subject: [PATCH 14/22] Add line mark --- avenger-vega-renderer/js/marks/group.js | 7 +- avenger-vega-renderer/js/marks/line.js | 79 ++++++++++++++++ avenger-vega-renderer/src/marks/arc.rs | 14 +-- avenger-vega-renderer/src/marks/group.rs | 9 +- avenger-vega-renderer/src/marks/line.rs | 94 ++++++++++++++++++++ avenger-vega-renderer/src/marks/mod.rs | 7 +- avenger-vega-renderer/src/marks/path.rs | 31 ++++--- avenger-vega-renderer/src/marks/rule.rs | 2 +- avenger-vega-renderer/test/test_baselines.py | 17 +++- 9 files changed, 234 insertions(+), 26 deletions(-) create mode 100644 avenger-vega-renderer/js/marks/line.js create mode 100644 avenger-vega-renderer/src/marks/line.rs diff --git a/avenger-vega-renderer/js/marks/group.js b/avenger-vega-renderer/js/marks/group.js index dac1dfb..3287fb1 100644 --- a/avenger-vega-renderer/js/marks/group.js +++ b/avenger-vega-renderer/js/marks/group.js @@ -6,6 +6,7 @@ import { importRect } from "./rect.js"; import { importArc } from "./arc.js"; import { importPath } from "./path.js"; import { importShape } from "./shape.js"; +import {importLine} from "./line.js"; /** * @typedef {import('./symbol.js').SymbolMarkSpec} SymbolMarkSpec @@ -15,10 +16,11 @@ import { importShape } from "./shape.js"; * @typedef {import('./arc.js').ArcMarkSpec} ArcMarkSpec * @typedef {import('./path.js').PathMarkSpec} PathMarkSpec * @typedef {import('./shape.js').ShapeMarkSpec} ShapeMarkSpec + * @typedef {import('./line.js').LineMarkSpec} LineMarkSpec * * @typedef {Object} GroupItemSpec * @property {"group"} marktype - * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec|ArcMarkSpec|PathMarkSpec|ShapeMarkSpec)[]} items + * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec|ArcMarkSpec|PathMarkSpec|ShapeMarkSpec|LineMarkSpec)[]} items * @property {number} x * @property {number} y * @property {number} width @@ -85,6 +87,9 @@ export function importGroup(vegaGroup, name, forceClip) { case "shape": groupMark.add_path_mark(importShape(vegaMark, clip)); break; + case "line": + groupMark.add_line_mark(importLine(vegaMark, clip)); + break; case "text": groupMark.add_text_mark(importText(vegaMark, clip)); break; diff --git a/avenger-vega-renderer/js/marks/line.js b/avenger-vega-renderer/js/marks/line.js new file mode 100644 index 0000000..2a55e8b --- /dev/null +++ b/avenger-vega-renderer/js/marks/line.js @@ -0,0 +1,79 @@ +import {LineMark} from "../../pkg/avenger_wasm.js"; + + +/** + * @typedef {Object} LineItem + * @property {number} strokeWidth + * @property {string|object} stroke + * @property {"butt"|"round"|"square"} strokeCap + * @property {"bevel"|"miter"|"round"} strokeJoin + * @property {string|number[]} strokeDash + * @property {number} x + * @property {number} y + * @property {number} defined + * @property {number} opacity + * @property {number} strokeOpacity + */ + +/** + * @typedef {Object} LineMarkSpec + * @property {"line"} marktype + * @property {boolean} clip + * @property {boolean} interactive + * @property {LineItem[]} items + * @property {string} name + * @property {string} role + * @property {number} zindex + */ + +/** + * @param {LineMarkSpec} vegaLineMark + * @param {boolean} force_clip + * @returns {LineMark} + */ +export function importLine(vegaLineMark, force_clip) { + const items = vegaLineMark.items; + const len = items.length; + + const lineMark = new LineMark( + len, vegaLineMark.clip || force_clip, vegaLineMark.name, vegaLineMark.zindex + ); + + // Handle empty mark + if (len === 0) { + return lineMark; + } + + // Set scalar values based on first element + const firstItem = items[0]; + lineMark.set_stroke_width(firstItem.strokeWidth ?? 1); + lineMark.set_stroke_join(firstItem.strokeJoin ?? "miter"); + lineMark.set_stroke_cap(firstItem.strokeCap ?? "butt"); + if (firstItem.strokeDash != null) { + lineMark.set_stroke_dash(firstItem.strokeDash); + } + const strokeOpacity = (firstItem.strokeOpacity ?? 1) * (firstItem.opacity ?? 1); + lineMark.set_stroke(firstItem.stroke, strokeOpacity); + + // Semi-required values get initialized + const x = new Float32Array(len).fill(0); + const y = new Float32Array(len).fill(0); + const defined = new Uint8Array(len).fill(1); + let anyDefined = false; + + items.forEach((item, i) => { + x[i] = item.x ?? 0; + y[i] = item.y ?? 0; + + if (item.defined != null) { + defined[i] = item.defined; + anyDefined ||= true; + } + }) + + lineMark.set_xy(x, y); + if (anyDefined) { + lineMark.set_defined(defined); + } + return lineMark; +} diff --git a/avenger-vega-renderer/src/marks/arc.rs b/avenger-vega-renderer/src/marks/arc.rs index d68736a..819409a 100644 --- a/avenger-vega-renderer/src/marks/arc.rs +++ b/avenger-vega-renderer/src/marks/arc.rs @@ -3,7 +3,6 @@ use avenger::marks::arc::ArcMark as RsArcMark; use avenger::marks::value::EncodingValue; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsError, JsValue}; -use crate::log; #[wasm_bindgen] pub struct ArcMark { @@ -40,7 +39,9 @@ impl ArcMark { } pub fn set_start_angle(&mut self, start_angle: Vec) { - self.inner.start_angle = EncodingValue::Array { values: start_angle }; + self.inner.start_angle = EncodingValue::Array { + values: start_angle, + }; } pub fn set_end_angle(&mut self, end_angle: Vec) { @@ -48,11 +49,15 @@ impl ArcMark { } pub fn set_outer_radius(&mut self, outer_radius: Vec) { - self.inner.outer_radius = EncodingValue::Array { values: outer_radius }; + self.inner.outer_radius = EncodingValue::Array { + values: outer_radius, + }; } pub fn set_inner_radius(&mut self, inner_radius: Vec) { - self.inner.inner_radius = EncodingValue::Array { values: inner_radius }; + self.inner.inner_radius = EncodingValue::Array { + values: inner_radius, + }; } pub fn set_corner_radius(&mut self, corner_radius: Vec) { @@ -61,7 +66,6 @@ impl ArcMark { }; } - /// Set stroke color. /// /// @param {string[]} color_values diff --git a/avenger-vega-renderer/src/marks/group.rs b/avenger-vega-renderer/src/marks/group.rs index 86f02b3..8fd38f8 100644 --- a/avenger-vega-renderer/src/marks/group.rs +++ b/avenger-vega-renderer/src/marks/group.rs @@ -1,8 +1,10 @@ +use crate::marks::arc::ArcMark; +use crate::marks::line::LineMark; +use crate::marks::path::PathMark; use crate::marks::rect::RectMark; use crate::marks::rule::RuleMark; use crate::marks::symbol::SymbolMark; use crate::marks::text::TextMark; -use crate::marks::arc::ArcMark; use crate::marks::util::{decode_color, decode_gradient}; use avenger::marks::group::{Clip, SceneGroup as RsSceneGroup}; use avenger::marks::mark::SceneMark; @@ -12,7 +14,6 @@ use lyon_path::geom::Box2D; use lyon_path::Winding; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsError, JsValue}; -use crate::marks::path::PathMark; #[wasm_bindgen] pub struct GroupMark { @@ -178,6 +179,10 @@ impl GroupMark { self.inner.marks.push(SceneMark::Path(mark.build())); } + pub fn add_line_mark(&mut self, mark: LineMark) { + self.inner.marks.push(SceneMark::Line(mark.build())); + } + pub fn add_group_mark(&mut self, mark: GroupMark) { self.inner.marks.push(SceneMark::Group(mark.inner)); } diff --git a/avenger-vega-renderer/src/marks/line.rs b/avenger-vega-renderer/src/marks/line.rs new file mode 100644 index 0000000..e32f936 --- /dev/null +++ b/avenger-vega-renderer/src/marks/line.rs @@ -0,0 +1,94 @@ +use avenger::marks::line::LineMark as RsLineMark; +use avenger::marks::value::{EncodingValue, StrokeCap, StrokeJoin}; +use avenger_vega::marks::values::{CssColorOrGradient, StrokeDashSpec}; +use gloo_utils::format::JsValueSerdeExt; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; + +#[wasm_bindgen] +pub struct LineMark { + inner: RsLineMark, +} + +impl LineMark { + pub fn build(self) -> RsLineMark { + self.inner + } +} + +#[wasm_bindgen] +impl LineMark { + #[wasm_bindgen(constructor)] + pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { + Self { + inner: RsLineMark { + len, + clip, + zindex, + name: name.unwrap_or_default(), + ..Default::default() + }, + } + } + + pub fn set_stroke_width(&mut self, width: f32) { + self.inner.stroke_width = width; + } + + /// Set stroke cap + /// + /// @param {"butt"|"round"|"square"} cap + pub fn set_stroke_cap(&mut self, cap: JsValue) -> Result<(), JsError> { + let cap: StrokeCap = cap.into_serde()?; + self.inner.stroke_cap = cap; + Ok(()) + } + + /// Set stroke cap + /// + /// @param {"bevel"|"miter"|"round"} join + pub fn set_stroke_join(&mut self, join: JsValue) -> Result<(), JsError> { + let join: StrokeJoin = join.into_serde()?; + self.inner.stroke_join = join; + Ok(()) + } + + /// Set line color + /// + /// @param {string|object} color + /// @param {number} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_stroke(&mut self, color: JsValue, opacity: f32) -> Result<(), JsError> { + let fill: CssColorOrGradient = color.into_serde()?; + let fill = fill + .to_color_or_grad(opacity, &mut self.inner.gradients) + .map_err(|_| JsError::new("Failed to parse stroke color"))?; + self.inner.stroke = fill; + Ok(()) + } + + /// Set stroke dash + /// + /// @param {string|number[]} values + pub fn set_stroke_dash(&mut self, dash: JsValue) -> Result<(), JsError> { + let dash: StrokeDashSpec = dash.into_serde()?; + let dash_array = dash + .to_array() + .map(|a| a.to_vec()) + .map_err(|_| JsError::new("Failed to parse dash spec"))?; + self.inner.stroke_dash = Some(dash_array); + Ok(()) + } + + pub fn set_xy(&mut self, x: Vec, y: Vec) { + self.inner.x = EncodingValue::Array { values: x }; + self.inner.y = EncodingValue::Array { values: y }; + } + + pub fn set_defined(&mut self, defined: Vec) -> Result<(), JsError> { + self.inner.defined = EncodingValue::Array { + values: defined.into_iter().map(|d| d != 0).collect(), + }; + Ok(()) + } +} diff --git a/avenger-vega-renderer/src/marks/mod.rs b/avenger-vega-renderer/src/marks/mod.rs index 56f7056..ba7bed2 100644 --- a/avenger-vega-renderer/src/marks/mod.rs +++ b/avenger-vega-renderer/src/marks/mod.rs @@ -1,8 +1,9 @@ +pub mod arc; pub mod group; +pub mod line; +pub mod path; +pub mod rect; pub mod rule; pub mod symbol; pub mod text; pub mod util; -pub mod rect; -pub mod arc; -pub mod path; diff --git a/avenger-vega-renderer/src/marks/path.rs b/avenger-vega-renderer/src/marks/path.rs index 377fc49..7b1baa9 100644 --- a/avenger-vega-renderer/src/marks/path.rs +++ b/avenger-vega-renderer/src/marks/path.rs @@ -1,15 +1,15 @@ -use gloo_utils::format::JsValueSerdeExt; -use itertools::izip; -use lyon_path::geom::Angle; -use lyon_path::geom::euclid::Vector2D; +use crate::log; use crate::marks::util::{decode_colors, decode_gradients, zindex_to_indices}; +use avenger::error::AvengerError; use avenger::marks::path::{PathMark as RsPathMark, PathTransform}; +use avenger::marks::symbol::parse_svg_path; use avenger::marks::value::EncodingValue; +use gloo_utils::format::JsValueSerdeExt; +use itertools::izip; +use lyon_path::geom::euclid::Vector2D; +use lyon_path::geom::Angle; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsError, JsValue}; -use avenger::error::AvengerError; -use avenger::marks::symbol::{parse_svg_path, SymbolShape}; -use crate::log; #[wasm_bindgen] pub struct PathMark { @@ -49,11 +49,13 @@ impl PathMark { scale_ys: Vec, angles: Vec, ) { - let transforms = izip!(xs, ys, scale_xs, scale_ys, angles).map(|(x, y, scale_x, scale_y, angle)| { - PathTransform::scale(scale_x, scale_y) - .then_rotate(Angle::degrees(angle)) - .then_translate(Vector2D::new(x, y)) - }).collect::>(); + let transforms = izip!(xs, ys, scale_xs, scale_ys, angles) + .map(|(x, y, scale_x, scale_y, angle)| { + PathTransform::scale(scale_x, scale_y) + .then_rotate(Angle::degrees(angle)) + .then_translate(Vector2D::new(x, y)) + }) + .collect::>(); self.inner.transform = EncodingValue::Array { values: transforms }; } @@ -70,7 +72,10 @@ impl PathMark { .collect::, AvengerError>>() .map_err(|_| JsError::new("Failed to parse shapes"))?; - let paths = indices.into_iter().map(|i| unique_paths[i].clone()).collect::>(); + let paths = indices + .into_iter() + .map(|i| unique_paths[i].clone()) + .collect::>(); self.inner.path = EncodingValue::Array { values: paths }; Ok(()) } diff --git a/avenger-vega-renderer/src/marks/rule.rs b/avenger-vega-renderer/src/marks/rule.rs index ed31eaa..5254291 100644 --- a/avenger-vega-renderer/src/marks/rule.rs +++ b/avenger-vega-renderer/src/marks/rule.rs @@ -94,7 +94,7 @@ impl RuleMark { /// Set stroke dash /// - /// @param {string|number[]} values + /// @param {(string|number[])[]} values #[wasm_bindgen(skip_jsdoc)] pub fn set_stroke_dash(&mut self, values: JsValue) -> Result<(), JsError> { let values: Vec = values.into_serde()?; diff --git a/avenger-vega-renderer/test/test_baselines.py b/avenger-vega-renderer/test/test_baselines.py index 2e0af4a..3032e0c 100644 --- a/avenger-vega-renderer/test/test_baselines.py +++ b/avenger-vega-renderer/test/test_baselines.py @@ -118,6 +118,17 @@ def failures_path(): ("shape", "world-natural-earth-projection", 0.0001), ("shape", "london_tubes", 0.0001), + ("line", "simple_line_round_cap", 0.0), + ("line", "simple_line_butt_cap_miter_join", 0.0), + ("line", "simple_line_square_cap_bevel_join", 0.0002), # square-cap + ("line", "connected_scatter", 0.0001), + ("line", "lines_with_open_symbols", 0.0), + ("line", "stocks", 0.0001), + ("line", "simple_dashed", 0.0), + ("line", "line_dashed_round_undefined", 0.0), + ("line", "line_dashed_square_undefined", 0.02), # square-cap + ("line", "line_dashed_butt_undefined", 0.0), + ("gradients", "symbol_cross_gradient", 0.0001), ("gradients", "symbol_circles_gradient_stroke", 0.0001), ("gradients", "symbol_radial_gradient", 0.0002), @@ -134,9 +145,13 @@ def failures_path(): ("clip", "text_clip_rounded", 0.006), ("clip", "bar_rounded2", 0.0), - # # TODO: + # # TODO: Need more marks # ("clip", "clip_mixed_marks", 0.0), # ("clip", "clip_rounded", 0.0), + + # # TODO: line legends + # ("line", "stocks-legend", 0.0), + # ("line", "stocks_dashed", 0.0), ], ) def test_image_baselines( From 4457e1ddd47e35c16ffc22819c15020239760bf0 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 24 Apr 2024 18:41:57 -0400 Subject: [PATCH 15/22] Add area mark --- avenger-vega-renderer/js/marks/area.js | 118 ++++++++++++++++ avenger-vega-renderer/js/marks/group.js | 7 +- avenger-vega-renderer/src/marks/area.rs | 137 +++++++++++++++++++ avenger-vega-renderer/src/marks/group.rs | 5 + avenger-vega-renderer/src/marks/line.rs | 38 +++-- avenger-vega-renderer/src/marks/mod.rs | 1 + avenger-vega-renderer/test/test_baselines.py | 8 ++ 7 files changed, 298 insertions(+), 16 deletions(-) create mode 100644 avenger-vega-renderer/js/marks/area.js create mode 100644 avenger-vega-renderer/src/marks/area.rs diff --git a/avenger-vega-renderer/js/marks/area.js b/avenger-vega-renderer/js/marks/area.js new file mode 100644 index 0000000..74285f4 --- /dev/null +++ b/avenger-vega-renderer/js/marks/area.js @@ -0,0 +1,118 @@ +import { AreaMark } from "../../pkg/avenger_wasm.js"; + +/** + * @typedef {Object} AreaItem + * @property {number} strokeWidth + * @property {"vertical"|"horizontal"} orient + * @property {string|object} stroke + * @property {string|object} fill + * @property {"butt"|"round"|"square"} strokeCap + * @property {"bevel"|"miter"|"round"} strokeJoin + * @property {string|number[]} strokeDash + * @property {number} x + * @property {number} y + * @property {number} x2 + * @property {number} y2 + * @property {number} defined + * @property {number} opacity + * @property {number} strokeOpacity + * @property {number} fillOpacity + */ + +/** + * @typedef {Object} AreaMarkSpec + * @property {"area"} marktype + * @property {boolean} clip + * @property {boolean} interactive + * @property {AreaItem[]} items + * @property {string} name + * @property {string} role + * @property {number} zindex + */ + +/** + * @param {AreaMarkSpec} vegaLineMark + * @param {boolean} force_clip + * @returns {AreaMark} + */ +export function importArea(vegaLineMark, force_clip) { + const items = vegaLineMark.items; + const len = items.length; + + const areaMark = new AreaMark( + len, vegaLineMark.clip || force_clip, vegaLineMark.name, vegaLineMark.zindex + ); + + // Handle empty mark + if (len === 0) { + return areaMark; + } + + // Set scalar values based on first element + const firstItem = items[0]; + areaMark.set_stroke_width(firstItem.strokeWidth ?? 1); + areaMark.set_stroke_join(firstItem.strokeJoin ?? "miter"); + areaMark.set_stroke_cap(firstItem.strokeCap ?? "butt"); + if (firstItem.strokeDash != null) { + areaMark.set_stroke_dash(firstItem.strokeDash); + } + const strokeOpacity = (firstItem.strokeOpacity ?? 1) * (firstItem.opacity ?? 1); + areaMark.set_stroke(firstItem.stroke, strokeOpacity); + + if (firstItem.fill != null) { + const fillOpacity = (firstItem.fillOpacity ?? 1) * (firstItem.opacity ?? 1); + areaMark.set_fill(firstItem.fill, fillOpacity); + } + + if (firstItem.orient != null) { + areaMark.set_orient(firstItem.orient) + } + + // Semi-required values get initialized + const x = new Float32Array(len).fill(0); + const y = new Float32Array(len).fill(0); + + const x2 = new Float32Array(len).fill(0); + let anyX2 = false; + + const y2 = new Float32Array(len).fill(0); + let anyY2 = false; + + const defined = new Uint8Array(len).fill(1); + let anyDefined = false; + + items.forEach((item, i) => { + x[i] = item.x ?? 0; + y[i] = item.y ?? 0; + + if (item.x2 != null) { + x2[i] = item.x2; + anyX2 ||= true; + } + + if (item.y2 != null) { + y2[i] = item.y2; + anyY2 ||= true; + } + + if (item.defined != null) { + defined[i] = item.defined; + anyDefined ||= true; + } + }) + + areaMark.set_xy(x, y); + + if (anyX2) { + areaMark.set_x2(x2); + } + + if (anyY2) { + areaMark.set_y2(y2); + } + + if (anyDefined) { + areaMark.set_defined(defined); + } + return areaMark; +} diff --git a/avenger-vega-renderer/js/marks/group.js b/avenger-vega-renderer/js/marks/group.js index 3287fb1..95ea968 100644 --- a/avenger-vega-renderer/js/marks/group.js +++ b/avenger-vega-renderer/js/marks/group.js @@ -7,6 +7,7 @@ import { importArc } from "./arc.js"; import { importPath } from "./path.js"; import { importShape } from "./shape.js"; import {importLine} from "./line.js"; +import {importArea} from "./area.js"; /** * @typedef {import('./symbol.js').SymbolMarkSpec} SymbolMarkSpec @@ -17,10 +18,11 @@ import {importLine} from "./line.js"; * @typedef {import('./path.js').PathMarkSpec} PathMarkSpec * @typedef {import('./shape.js').ShapeMarkSpec} ShapeMarkSpec * @typedef {import('./line.js').LineMarkSpec} LineMarkSpec + * @typedef {import('./area.js').AreaMarkSpec} AreaMarkSpec * * @typedef {Object} GroupItemSpec * @property {"group"} marktype - * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec|ArcMarkSpec|PathMarkSpec|ShapeMarkSpec|LineMarkSpec)[]} items + * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec|ArcMarkSpec|PathMarkSpec|ShapeMarkSpec|LineMarkSpec|AreaMarkSpec)[]} items * @property {number} x * @property {number} y * @property {number} width @@ -90,6 +92,9 @@ export function importGroup(vegaGroup, name, forceClip) { case "line": groupMark.add_line_mark(importLine(vegaMark, clip)); break; + case "area": + groupMark.add_area_mark(importArea(vegaMark, clip)); + break; case "text": groupMark.add_text_mark(importText(vegaMark, clip)); break; diff --git a/avenger-vega-renderer/src/marks/area.rs b/avenger-vega-renderer/src/marks/area.rs new file mode 100644 index 0000000..7a33145 --- /dev/null +++ b/avenger-vega-renderer/src/marks/area.rs @@ -0,0 +1,137 @@ +use avenger::marks::area::{AreaMark as RsAreaMark, AreaOrientation}; +use avenger::marks::value::{EncodingValue, StrokeCap, StrokeJoin}; +use avenger_vega::marks::values::{CssColorOrGradient, StrokeDashSpec}; +use gloo_utils::format::JsValueSerdeExt; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; + +#[wasm_bindgen] +pub struct AreaMark { + inner: RsAreaMark, +} + +impl AreaMark { + pub fn build(self) -> RsAreaMark { + self.inner + } +} + +#[wasm_bindgen] +impl AreaMark { + #[wasm_bindgen(constructor)] + pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { + Self { + inner: RsAreaMark { + len, + clip, + zindex, + name: name.unwrap_or_default(), + ..Default::default() + }, + } + } + + pub fn set_stroke_width(&mut self, width: f32) { + self.inner.stroke_width = width; + } + + /// Set stroke cap + /// + /// @param {"butt"|"round"|"square"} cap + pub fn set_stroke_cap(&mut self, cap: JsValue) -> Result<(), JsError> { + let cap: Option = cap.into_serde()?; + if let Some(cap) = cap { + self.inner.stroke_cap = cap; + } + Ok(()) + } + + /// Set stroke cap + /// + /// @param {"bevel"|"miter"|"round"} join + pub fn set_stroke_join(&mut self, join: JsValue) -> Result<(), JsError> { + let join: Option = join.into_serde()?; + if let Some(join) = join { + self.inner.stroke_join = join; + } + Ok(()) + } + + /// Set stroke cap + /// + /// @param {"vertical"|"horizontal"} orient + pub fn set_orient(&mut self, orient: JsValue) -> Result<(), JsError> { + let orient: Option = orient.into_serde()?; + if let Some(orient) = orient { + self.inner.orientation = orient; + } + Ok(()) + } + + /// Set stroke color + /// + /// @param {string|object} color + /// @param {number} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_stroke(&mut self, color: JsValue, opacity: f32) -> Result<(), JsError> { + let stroke: Option = color.into_serde()?; + if let Some(stroke) = stroke { + let stroke = stroke + .to_color_or_grad(opacity, &mut self.inner.gradients) + .map_err(|_| JsError::new("Failed to parse stroke color"))?; + self.inner.stroke = stroke; + } + Ok(()) + } + + /// Set fill color + /// + /// @param {string|object} color + /// @param {number} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_fill(&mut self, color: JsValue, opacity: f32) -> Result<(), JsError> { + let fill: Option = color.into_serde()?; + if let Some(fill) = fill { + let fill = fill + .to_color_or_grad(opacity, &mut self.inner.gradients) + .map_err(|_| JsError::new("Failed to parse stroke color"))?; + self.inner.fill = fill; + } + Ok(()) + } + + /// Set stroke dash + /// + /// @param {string|number[]} values + pub fn set_stroke_dash(&mut self, dash: JsValue) -> Result<(), JsError> { + let dash: Option = dash.into_serde()?; + if let Some(dash) = dash { + let dash_array = dash + .to_array() + .map(|a| a.to_vec()) + .map_err(|_| JsError::new("Failed to parse dash spec"))?; + self.inner.stroke_dash = Some(dash_array); + } + Ok(()) + } + + pub fn set_xy(&mut self, x: Vec, y: Vec) { + self.inner.x = EncodingValue::Array { values: x }; + self.inner.y = EncodingValue::Array { values: y }; + } + + pub fn set_x2(&mut self, x2: Vec) { + self.inner.x2 = EncodingValue::Array { values: x2 }; + } + + pub fn set_y2(&mut self, y2: Vec) { + self.inner.y2 = EncodingValue::Array { values: y2 }; + } + + pub fn set_defined(&mut self, defined: Vec) -> Result<(), JsError> { + self.inner.defined = EncodingValue::Array { + values: defined.into_iter().map(|d| d != 0).collect(), + }; + Ok(()) + } +} diff --git a/avenger-vega-renderer/src/marks/group.rs b/avenger-vega-renderer/src/marks/group.rs index 8fd38f8..3eafc3c 100644 --- a/avenger-vega-renderer/src/marks/group.rs +++ b/avenger-vega-renderer/src/marks/group.rs @@ -1,4 +1,5 @@ use crate::marks::arc::ArcMark; +use crate::marks::area::AreaMark; use crate::marks::line::LineMark; use crate::marks::path::PathMark; use crate::marks::rect::RectMark; @@ -183,6 +184,10 @@ impl GroupMark { self.inner.marks.push(SceneMark::Line(mark.build())); } + pub fn add_area_mark(&mut self, mark: AreaMark) { + self.inner.marks.push(SceneMark::Area(mark.build())); + } + pub fn add_group_mark(&mut self, mark: GroupMark) { self.inner.marks.push(SceneMark::Group(mark.inner)); } diff --git a/avenger-vega-renderer/src/marks/line.rs b/avenger-vega-renderer/src/marks/line.rs index e32f936..d971227 100644 --- a/avenger-vega-renderer/src/marks/line.rs +++ b/avenger-vega-renderer/src/marks/line.rs @@ -39,8 +39,10 @@ impl LineMark { /// /// @param {"butt"|"round"|"square"} cap pub fn set_stroke_cap(&mut self, cap: JsValue) -> Result<(), JsError> { - let cap: StrokeCap = cap.into_serde()?; - self.inner.stroke_cap = cap; + let cap: Option = cap.into_serde()?; + if let Some(cap) = cap { + self.inner.stroke_cap = cap; + } Ok(()) } @@ -48,8 +50,10 @@ impl LineMark { /// /// @param {"bevel"|"miter"|"round"} join pub fn set_stroke_join(&mut self, join: JsValue) -> Result<(), JsError> { - let join: StrokeJoin = join.into_serde()?; - self.inner.stroke_join = join; + let join: Option = join.into_serde()?; + if let Some(join) = join { + self.inner.stroke_join = join; + } Ok(()) } @@ -59,11 +63,13 @@ impl LineMark { /// @param {number} opacity #[wasm_bindgen(skip_jsdoc)] pub fn set_stroke(&mut self, color: JsValue, opacity: f32) -> Result<(), JsError> { - let fill: CssColorOrGradient = color.into_serde()?; - let fill = fill - .to_color_or_grad(opacity, &mut self.inner.gradients) - .map_err(|_| JsError::new("Failed to parse stroke color"))?; - self.inner.stroke = fill; + let fill: Option = color.into_serde()?; + if let Some(fill) = fill { + let fill = fill + .to_color_or_grad(opacity, &mut self.inner.gradients) + .map_err(|_| JsError::new("Failed to parse stroke color"))?; + self.inner.stroke = fill; + } Ok(()) } @@ -71,12 +77,14 @@ impl LineMark { /// /// @param {string|number[]} values pub fn set_stroke_dash(&mut self, dash: JsValue) -> Result<(), JsError> { - let dash: StrokeDashSpec = dash.into_serde()?; - let dash_array = dash - .to_array() - .map(|a| a.to_vec()) - .map_err(|_| JsError::new("Failed to parse dash spec"))?; - self.inner.stroke_dash = Some(dash_array); + let dash: Option = dash.into_serde()?; + if let Some(dash) = dash { + let dash_array = dash + .to_array() + .map(|a| a.to_vec()) + .map_err(|_| JsError::new("Failed to parse dash spec"))?; + self.inner.stroke_dash = Some(dash_array); + } Ok(()) } diff --git a/avenger-vega-renderer/src/marks/mod.rs b/avenger-vega-renderer/src/marks/mod.rs index ba7bed2..1f543f2 100644 --- a/avenger-vega-renderer/src/marks/mod.rs +++ b/avenger-vega-renderer/src/marks/mod.rs @@ -1,4 +1,5 @@ pub mod arc; +pub mod area; pub mod group; pub mod line; pub mod path; diff --git a/avenger-vega-renderer/test/test_baselines.py b/avenger-vega-renderer/test/test_baselines.py index 3032e0c..9ad16e7 100644 --- a/avenger-vega-renderer/test/test_baselines.py +++ b/avenger-vega-renderer/test/test_baselines.py @@ -129,6 +129,14 @@ def failures_path(): ("line", "line_dashed_square_undefined", 0.02), # square-cap ("line", "line_dashed_butt_undefined", 0.0), + ("area", "100_percent_stacked_area", 0.0), + ("area", "simple_unemployment", 0.0), + ("area", "simple_unemployment_stroke", 0.0), + ("area", "stacked_area", 0.0001), + ("area", "streamgraph_area", 0.0002), + ("area", "with_undefined", 0.0), + ("area", "with_undefined_horizontal", 0.0), + ("gradients", "symbol_cross_gradient", 0.0001), ("gradients", "symbol_circles_gradient_stroke", 0.0001), ("gradients", "symbol_radial_gradient", 0.0002), From b3fd55fca763b24c2e9b3a6dfd9e1e6b2d92819f Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 24 Apr 2024 19:14:39 -0400 Subject: [PATCH 16/22] Add trail mark --- avenger-vega-renderer/js/marks/group.js | 7 +- avenger-vega-renderer/js/marks/trail.js | 83 ++++++++++++++++++++ avenger-vega-renderer/src/marks/group.rs | 5 ++ avenger-vega-renderer/src/marks/line.rs | 6 +- avenger-vega-renderer/src/marks/mod.rs | 1 + avenger-vega-renderer/src/marks/trail.rs | 65 +++++++++++++++ avenger-vega-renderer/test/test_baselines.py | 3 + 7 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 avenger-vega-renderer/js/marks/trail.js create mode 100644 avenger-vega-renderer/src/marks/trail.rs diff --git a/avenger-vega-renderer/js/marks/group.js b/avenger-vega-renderer/js/marks/group.js index 95ea968..076f180 100644 --- a/avenger-vega-renderer/js/marks/group.js +++ b/avenger-vega-renderer/js/marks/group.js @@ -8,6 +8,7 @@ import { importPath } from "./path.js"; import { importShape } from "./shape.js"; import {importLine} from "./line.js"; import {importArea} from "./area.js"; +import {importTrail} from "./trail.js"; /** * @typedef {import('./symbol.js').SymbolMarkSpec} SymbolMarkSpec @@ -18,11 +19,12 @@ import {importArea} from "./area.js"; * @typedef {import('./path.js').PathMarkSpec} PathMarkSpec * @typedef {import('./shape.js').ShapeMarkSpec} ShapeMarkSpec * @typedef {import('./line.js').LineMarkSpec} LineMarkSpec + * @typedef {import('./trail.js').TrailMarkSpec} TrailMarkSpec * @typedef {import('./area.js').AreaMarkSpec} AreaMarkSpec * * @typedef {Object} GroupItemSpec * @property {"group"} marktype - * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec|ArcMarkSpec|PathMarkSpec|ShapeMarkSpec|LineMarkSpec|AreaMarkSpec)[]} items + * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec|ArcMarkSpec|PathMarkSpec|ShapeMarkSpec|LineMarkSpec|AreaMarkSpec|TrailMarkSpec)[]} items * @property {number} x * @property {number} y * @property {number} width @@ -95,6 +97,9 @@ export function importGroup(vegaGroup, name, forceClip) { case "area": groupMark.add_area_mark(importArea(vegaMark, clip)); break; + case "trail": + groupMark.add_trail_mark(importTrail(vegaMark, clip)); + break; case "text": groupMark.add_text_mark(importText(vegaMark, clip)); break; diff --git a/avenger-vega-renderer/js/marks/trail.js b/avenger-vega-renderer/js/marks/trail.js new file mode 100644 index 0000000..f37a5d3 --- /dev/null +++ b/avenger-vega-renderer/js/marks/trail.js @@ -0,0 +1,83 @@ +import {TrailMark} from "../../pkg/avenger_wasm.js"; + + +/** + * @typedef {Object} TrailItem + * @property {number} x + * @property {number} y + * @property {number} size + * @property {number} defined + * @property {string|object} fill + * @property {number} opacity + * @property {number} fillOpacity + */ + +/** + * @typedef {Object} TrailMarkSpec + * @property {"trail"} marktype + * @property {boolean} clip + * @property {boolean} interactive + * @property {TrailItem[]} items + * @property {string} name + * @property {string} role + * @property {number} zindex + */ + +/** + * @param {TrailMarkSpec} vegaLineMark + * @param {boolean} force_clip + * @returns {TrailMark} + */ +export function importTrail(vegaLineMark, force_clip) { + const items = vegaLineMark.items; + const len = items.length; + + const trailMark = new TrailMark( + len, vegaLineMark.clip || force_clip, vegaLineMark.name, vegaLineMark.zindex + ); + + // Handle empty mark + if (len === 0) { + return trailMark; + } + + // Set scalar values based on first element + const firstItem = items[0]; + const fillOpacity = (firstItem.fillOpacity ?? 1) * (firstItem.opacity ?? 1); + + // Note: Vega calls the color fill, avenger calls it stroke + trailMark.set_stroke(firstItem.fill, fillOpacity); + + // Semi-required values get initialized + const x = new Float32Array(len).fill(0); + const y = new Float32Array(len).fill(0); + const size = new Float32Array(len).fill(1); + let anySize = false; + + const defined = new Uint8Array(len).fill(1); + let anyDefined = false; + + items.forEach((item, i) => { + x[i] = item.x ?? 0; + y[i] = item.y ?? 0; + + if (item.size != null) { + size[i] = item.size; + anySize ||= true; + } + + if (item.defined != null) { + defined[i] = item.defined; + anyDefined ||= true; + } + }) + + trailMark.set_xy(x, y); + if (anySize) { + trailMark.set_size(size); + } + if (anyDefined) { + trailMark.set_defined(defined); + } + return trailMark; +} diff --git a/avenger-vega-renderer/src/marks/group.rs b/avenger-vega-renderer/src/marks/group.rs index 3eafc3c..acba2ba 100644 --- a/avenger-vega-renderer/src/marks/group.rs +++ b/avenger-vega-renderer/src/marks/group.rs @@ -6,6 +6,7 @@ use crate::marks::rect::RectMark; use crate::marks::rule::RuleMark; use crate::marks::symbol::SymbolMark; use crate::marks::text::TextMark; +use crate::marks::trail::TrailMark; use crate::marks::util::{decode_color, decode_gradient}; use avenger::marks::group::{Clip, SceneGroup as RsSceneGroup}; use avenger::marks::mark::SceneMark; @@ -188,6 +189,10 @@ impl GroupMark { self.inner.marks.push(SceneMark::Area(mark.build())); } + pub fn add_trail_mark(&mut self, mark: TrailMark) { + self.inner.marks.push(SceneMark::Trail(mark.build())); + } + pub fn add_group_mark(&mut self, mark: GroupMark) { self.inner.marks.push(SceneMark::Group(mark.inner)); } diff --git a/avenger-vega-renderer/src/marks/line.rs b/avenger-vega-renderer/src/marks/line.rs index d971227..592fe5b 100644 --- a/avenger-vega-renderer/src/marks/line.rs +++ b/avenger-vega-renderer/src/marks/line.rs @@ -63,9 +63,9 @@ impl LineMark { /// @param {number} opacity #[wasm_bindgen(skip_jsdoc)] pub fn set_stroke(&mut self, color: JsValue, opacity: f32) -> Result<(), JsError> { - let fill: Option = color.into_serde()?; - if let Some(fill) = fill { - let fill = fill + let stroke: Option = color.into_serde()?; + if let Some(stroke) = stroke { + let fill = stroke .to_color_or_grad(opacity, &mut self.inner.gradients) .map_err(|_| JsError::new("Failed to parse stroke color"))?; self.inner.stroke = fill; diff --git a/avenger-vega-renderer/src/marks/mod.rs b/avenger-vega-renderer/src/marks/mod.rs index 1f543f2..1905568 100644 --- a/avenger-vega-renderer/src/marks/mod.rs +++ b/avenger-vega-renderer/src/marks/mod.rs @@ -7,4 +7,5 @@ pub mod rect; pub mod rule; pub mod symbol; pub mod text; +pub mod trail; pub mod util; diff --git a/avenger-vega-renderer/src/marks/trail.rs b/avenger-vega-renderer/src/marks/trail.rs new file mode 100644 index 0000000..a631b2a --- /dev/null +++ b/avenger-vega-renderer/src/marks/trail.rs @@ -0,0 +1,65 @@ +use avenger::marks::trail::TrailMark as RsTrailMark; +use avenger::marks::value::EncodingValue; +use avenger_vega::marks::values::CssColorOrGradient; +use gloo_utils::format::JsValueSerdeExt; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; + +#[wasm_bindgen] +pub struct TrailMark { + inner: RsTrailMark, +} + +impl TrailMark { + pub fn build(self) -> RsTrailMark { + self.inner + } +} + +#[wasm_bindgen] +impl TrailMark { + #[wasm_bindgen(constructor)] + pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { + Self { + inner: RsTrailMark { + len, + clip, + zindex, + name: name.unwrap_or_default(), + ..Default::default() + }, + } + } + + /// Set fill color + /// + /// @param {string|object} color + /// @param {number} opacity + #[wasm_bindgen(skip_jsdoc)] + pub fn set_stroke(&mut self, color: JsValue, opacity: f32) -> Result<(), JsError> { + let stroke: Option = color.into_serde()?; + if let Some(stroke) = stroke { + let fill = stroke + .to_color_or_grad(opacity, &mut self.inner.gradients) + .map_err(|_| JsError::new("Failed to parse stroke color"))?; + self.inner.stroke = fill; + } + Ok(()) + } + + pub fn set_xy(&mut self, x: Vec, y: Vec) { + self.inner.x = EncodingValue::Array { values: x }; + self.inner.y = EncodingValue::Array { values: y }; + } + + pub fn set_defined(&mut self, defined: Vec) -> Result<(), JsError> { + self.inner.defined = EncodingValue::Array { + values: defined.into_iter().map(|d| d != 0).collect(), + }; + Ok(()) + } + + pub fn set_size(&mut self, size: Vec) { + self.inner.size = EncodingValue::Array { values: size }; + } +} diff --git a/avenger-vega-renderer/test/test_baselines.py b/avenger-vega-renderer/test/test_baselines.py index 9ad16e7..0240855 100644 --- a/avenger-vega-renderer/test/test_baselines.py +++ b/avenger-vega-renderer/test/test_baselines.py @@ -137,6 +137,9 @@ def failures_path(): ("area", "with_undefined", 0.0), ("area", "with_undefined_horizontal", 0.0), + ("trail", "trail_stocks", 0.0), + ("trail", "trail_stocks_opacity", 0.0), + ("gradients", "symbol_cross_gradient", 0.0001), ("gradients", "symbol_circles_gradient_stroke", 0.0001), ("gradients", "symbol_radial_gradient", 0.0002), From 03e10a0d30a75184afdd4e7b61028020cf059aea Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 27 Apr 2024 21:09:34 -0400 Subject: [PATCH 17/22] Support image mark --- Cargo.lock | 12 ++ avenger-vega-renderer/Cargo.toml | 1 + avenger-vega-renderer/js/index.js | 9 +- avenger-vega-renderer/js/marks/group.js | 13 +- avenger-vega-renderer/js/marks/image.js | 210 +++++++++++++++++++ avenger-vega-renderer/js/marks/scenegraph.js | 6 +- avenger-vega-renderer/src/marks/group.rs | 5 + avenger-vega-renderer/src/marks/image.rs | 112 ++++++++++ avenger-vega-renderer/src/marks/mod.rs | 1 + avenger-vega-renderer/test/test_baselines.py | 17 +- avenger/src/marks/image.rs | 23 +- 11 files changed, 392 insertions(+), 17 deletions(-) create mode 100644 avenger-vega-renderer/js/marks/image.js create mode 100644 avenger-vega-renderer/src/marks/image.rs diff --git a/Cargo.lock b/Cargo.lock index 10878a5..0374acd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -400,6 +400,7 @@ dependencies = [ "js-sys", "lazy_static", "lyon_path", + "serde-wasm-bindgen", "serde_json", "unicode-segmentation", "wasm-bindgen", @@ -5573,6 +5574,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_bytes" version = "0.11.12" diff --git a/avenger-vega-renderer/Cargo.toml b/avenger-vega-renderer/Cargo.toml index 79efb8b..3876c50 100644 --- a/avenger-vega-renderer/Cargo.toml +++ b/avenger-vega-renderer/Cargo.toml @@ -23,6 +23,7 @@ itertools = "0.12.0" wasm-bindgen = { version = "=0.2.92" } wasm-bindgen-futures = "0.4.30" gloo-utils = { version = "0.2.0", features = ["serde"] } +serde-wasm-bindgen = "0.6.5" js-sys = "0.3.69" web-sys = { version = "0.3.69", features = [ "Document", "Window", "Element", "Performance", "OffscreenCanvas", "OffscreenCanvasRenderingContext2d", "TextMetrics", "ImageData" diff --git a/avenger-vega-renderer/js/index.js b/avenger-vega-renderer/js/index.js index d70a74e..2fd2b63 100644 --- a/avenger-vega-renderer/js/index.js +++ b/avenger-vega-renderer/js/index.js @@ -102,14 +102,15 @@ inherits(AvengerRenderer, Renderer, { console.log("scene graph construction time: " + (performance.now() - this._lastRenderFinishTime)); this._avengerCanvasPromise.then((avengerCanvas) => { var start = performance.now(); - const sceneGraph = importScenegraph( + importScenegraph( scene, avengerCanvas.width(), avengerCanvas.height(), [avengerCanvas.origin_x(), avengerCanvas.origin_y()] - ); - avengerCanvas.set_scene(sceneGraph); - console.log("_render time: " + (performance.now() - start)); + ).then((sceneGraph) => { + avengerCanvas.set_scene(sceneGraph); + console.log("_render time: " + (performance.now() - start)); + }); }); this._lastRenderFinishTime = performance.now(); return this; diff --git a/avenger-vega-renderer/js/marks/group.js b/avenger-vega-renderer/js/marks/group.js index 076f180..1e851e3 100644 --- a/avenger-vega-renderer/js/marks/group.js +++ b/avenger-vega-renderer/js/marks/group.js @@ -9,6 +9,7 @@ import { importShape } from "./shape.js"; import {importLine} from "./line.js"; import {importArea} from "./area.js"; import {importTrail} from "./trail.js"; +import {importImage} from "./image.js"; /** * @typedef {import('./symbol.js').SymbolMarkSpec} SymbolMarkSpec @@ -21,10 +22,11 @@ import {importTrail} from "./trail.js"; * @typedef {import('./line.js').LineMarkSpec} LineMarkSpec * @typedef {import('./trail.js').TrailMarkSpec} TrailMarkSpec * @typedef {import('./area.js').AreaMarkSpec} AreaMarkSpec + * @typedef {import('./image.js').ImageMarkSpec} ImageMarkSpec * * @typedef {Object} GroupItemSpec * @property {"group"} marktype - * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec|ArcMarkSpec|PathMarkSpec|ShapeMarkSpec|LineMarkSpec|AreaMarkSpec|TrailMarkSpec)[]} items + * @property {(GroupMarkSpec|SymbolMarkSpec|TextMarkSpec|RuleMarkSpec|RectMarkSpec|ArcMarkSpec|PathMarkSpec|ShapeMarkSpec|LineMarkSpec|AreaMarkSpec|TrailMarkSpec|ImageMarkSpec)[]} items * @property {number} x * @property {number} y * @property {number} width @@ -59,9 +61,9 @@ import {importTrail} from "./trail.js"; * @param {GroupItemSpec} vegaGroup * @param {string} name * @param {boolean} forceClip - * @returns {GroupMark} + * @returns {Promise} */ -export function importGroup(vegaGroup, name, forceClip) { +export async function importGroup(vegaGroup, name, forceClip) { const width = vegaGroup.width ?? (vegaGroup.x2 != null? vegaGroup.x2 - vegaGroup.x: null); const height = vegaGroup.height ?? (vegaGroup.y2 != null? vegaGroup.y2 - vegaGroup.y: null); @@ -100,12 +102,15 @@ export function importGroup(vegaGroup, name, forceClip) { case "trail": groupMark.add_trail_mark(importTrail(vegaMark, clip)); break; + case "image": + groupMark.add_image_mark(await importImage(vegaMark, clip)); + break; case "text": groupMark.add_text_mark(importText(vegaMark, clip)); break; case "group": for (const groupItem of vegaMark.items) { - groupMark.add_group_mark(importGroup(groupItem, vegaMark.name, clip)); + groupMark.add_group_mark(await importGroup(groupItem, vegaMark.name, clip)); } break; } diff --git a/avenger-vega-renderer/js/marks/image.js b/avenger-vega-renderer/js/marks/image.js new file mode 100644 index 0000000..080b2ea --- /dev/null +++ b/avenger-vega-renderer/js/marks/image.js @@ -0,0 +1,210 @@ +import {ImageMark} from "../../pkg/avenger_wasm.js"; +import {encodeSimpleArray} from "./util.js"; + + +/** + * Represents the style and configuration of a graphic element. + * @typedef {Object} ImageItem + * @property {string} url + * @property {number} x + * @property {number} y + * @property {number} width + * @property {number} height + * @property {number} x2 + * @property {number} y2 + * @property {"left"|"center"|"right"} align + * @property {"top"|"middle"|"bottom"} baseline + * @property {boolean} smooth + * @property {boolean} aspect + * @property {number} zindex + */ + +/** + * Represents a graphical object configuration. + * @typedef {Object} ImageMarkSpec + * @property {"image"} marktype + * @property {boolean} clip + * @property {ImageItem[]} items + * @property {string} name + * @property {number} zindex + */ + +/** + * @param {ImageMarkSpec} vegaImageMark + * @param {boolean} forceClip + * @returns {Promise} + */ +export async function importImage(vegaImageMark, forceClip) { + const items = vegaImageMark.items; + const len = items.length; + + const imageMark = new ImageMark( + len, vegaImageMark.clip || forceClip, vegaImageMark.name, vegaImageMark.zindex + ); + if (len === 0) { + return imageMark; + } + + // Set scalar properties based on first item + const firstItem = items[0]; + if (firstItem.aspect != null) { + imageMark.set_aspect(firstItem.aspect); + } + if (firstItem.smooth != null) { + imageMark.set_smooth(firstItem.smooth); + } + + const image = new Array(len); + const x = new Float32Array(len).fill(0); + const y = new Float32Array(len).fill(0); + const width = new Float32Array(len).fill(0); + const height = new Float32Array(len).fill(0); + + const align = new Array(len); + let anyAlign = false; + + const baseline = new Array(len); + let anyBaseline = false; + + const zindex = new Int32Array(len).fill(0); + let anyZindex = false; + + for (let i = 0; i < items.length; i++) { + let item = items[i]; + + if (item.url != null) { + let url; + if (item.url.startsWith("data/")) { + url = "https://vega.github.io/vega-datasets/" + item.url; + } else { + url = item.url; + } + image[i] = await fetchImage(url); + } + + if (item.x != null) { + x[i] = item.x; + } + if (item.y != null) { + y[i] = item.y; + } + + if (item.width != null) { + width[i] = item.width; + } else if (item.x2 != null) { + width[i] = item.x2 - x[i]; + } + + if (item.height != null) { + height[i] = item.height; + } else if (item.y2 != null) { + height[i] = item.y2 - y[i]; + } + + if (item.align != null) { + align[i] = item.align; + anyAlign ||= true; + } + + if (item.baseline != null) { + baseline[i] = item.baseline; + anyBaseline ||= true; + } + + if (item.zindex != null) { + zindex[i] = item.zindex; + anyZindex ||= true; + } + } + + imageMark.set_xy(x, y); + imageMark.set_width(width); + imageMark.set_height(height); + imageMark.set_image(image); + + if (anyAlign) { + const encoded = encodeSimpleArray(align); + imageMark.set_align(encoded.values, encoded.indices); + } + + if (anyBaseline) { + const encoded = encodeSimpleArray(baseline); + imageMark.set_baseline(encoded.values, encoded.indices); + } + + if (anyZindex) { + imageMark.set_zindex(zindex); + } + return imageMark; +} + +/** + * @typedef {Object} RgbaImage + * @property {number} width - The width of the image in pixels. + * @property {number} height - The height of the image in pixels. + * @property {Uint8Array} data - The RGBA data of the image. + */ + +/** + * Cache for storing image data promises. The keys are image URLs (strings), + * and the values are promises that resolve to RgbaImage objects. + * @type {Map>} + */ +const imageCache = new Map(); + +/** + * Fetches an image from the specified URL and returns its RGBA data. + * If the image has been fetched before, the cached result will be used. + * @param {string} url - The URL of the image to fetch. + * @returns {Promise} A promise that resolves with the RGBA data of the image. + */ +async function fetchImage(url) { + // Check if the image data is already cached in the Map + if (imageCache.has(url)) { + return imageCache.get(url); + } + + // Fetch and process the image, then cache the promise in the Map + const imagePromise = performFetchImage(url); + imageCache.set(url, imagePromise); + return imagePromise; +} + +/** + * Fetches and processes the image to extract RGBA data. + * @param {string} url - The URL of the image to fetch. + * @returns {Promise} A promise that resolves with the RGBA data of the image. + */ +async function performFetchImage(url) { + try { + const response = await fetch(url); + const blob = await response.blob(); + + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const data = new Uint8Array(imageData.data.buffer); + + resolve({ + width: img.width, + height: img.height, + data: data + }); + }; + + img.onerror = () => { + reject(new Error("Failed to load image.")); + }; + + img.src = URL.createObjectURL(blob); + }); + } catch (error) { + console.error("Error fetching image:", error); + } +} \ No newline at end of file diff --git a/avenger-vega-renderer/js/marks/scenegraph.js b/avenger-vega-renderer/js/marks/scenegraph.js index c28b2b2..6035bb4 100644 --- a/avenger-vega-renderer/js/marks/scenegraph.js +++ b/avenger-vega-renderer/js/marks/scenegraph.js @@ -6,12 +6,12 @@ import { importGroup } from "./group.js"; * @param {number} width * @param {number} height * @param {[number, number]} origin - * @returns {SceneGraph} + * @returns {Promise} */ -export function importScenegraph(groupMark, width, height, origin) { +export async function importScenegraph(groupMark, width, height, origin) { const sceneGraph = new SceneGraph(width, height, origin[0], origin[1]); for (const vegaGroup of groupMark.items) { - sceneGraph.add_group(importGroup(vegaGroup, groupMark.name, false)); + sceneGraph.add_group(await importGroup(vegaGroup, groupMark.name, false)); } return sceneGraph; } diff --git a/avenger-vega-renderer/src/marks/group.rs b/avenger-vega-renderer/src/marks/group.rs index acba2ba..2ec9a20 100644 --- a/avenger-vega-renderer/src/marks/group.rs +++ b/avenger-vega-renderer/src/marks/group.rs @@ -7,6 +7,7 @@ use crate::marks::rule::RuleMark; use crate::marks::symbol::SymbolMark; use crate::marks::text::TextMark; use crate::marks::trail::TrailMark; +use crate::marks::image::ImageMark; use crate::marks::util::{decode_color, decode_gradient}; use avenger::marks::group::{Clip, SceneGroup as RsSceneGroup}; use avenger::marks::mark::SceneMark; @@ -193,6 +194,10 @@ impl GroupMark { self.inner.marks.push(SceneMark::Trail(mark.build())); } + pub fn add_image_mark(&mut self, mark: ImageMark) { + self.inner.marks.push(SceneMark::Image(Box::new(mark.build()))); + } + pub fn add_group_mark(&mut self, mark: GroupMark) { self.inner.marks.push(SceneMark::Group(mark.inner)); } diff --git a/avenger-vega-renderer/src/marks/image.rs b/avenger-vega-renderer/src/marks/image.rs new file mode 100644 index 0000000..94d8a94 --- /dev/null +++ b/avenger-vega-renderer/src/marks/image.rs @@ -0,0 +1,112 @@ +use gloo_utils::format::JsValueSerdeExt; +use wasm_bindgen::{JsError, JsValue}; +use wasm_bindgen::prelude::wasm_bindgen; +use avenger::marks::value::{EncodingValue, ImageAlign, ImageBaseline}; +use avenger::marks::image::{ImageMark as RsImageMark, RgbaImage}; +use crate::marks::util::zindex_to_indices; + +#[wasm_bindgen] +pub struct ImageMark { + inner: RsImageMark, +} + +impl ImageMark { + pub fn build(self) -> RsImageMark { + self.inner + } +} + +#[wasm_bindgen] +impl ImageMark { + #[wasm_bindgen(constructor)] + pub fn new(len: u32, clip: bool, name: Option, zindex: Option) -> Self { + Self { + inner: RsImageMark { + len, + clip, + name: name.unwrap_or_default(), + zindex, + ..Default::default() + }, + } + } + + pub fn set_smooth(&mut self, smooth: bool) { + self.inner.smooth = smooth; + } + + pub fn set_aspect(&mut self, aspect: bool) { + self.inner.aspect = aspect; + } + + pub fn set_zindex(&mut self, zindex: Vec) { + self.inner.indices = Some(zindex_to_indices(zindex)); + } + + pub fn set_xy(&mut self, x: Vec, y: Vec) { + self.inner.x = EncodingValue::Array { values: x }; + self.inner.y = EncodingValue::Array { values: y }; + } + + pub fn set_width(&mut self, width: Vec) { + self.inner.width = EncodingValue::Array { values: width }; + } + + pub fn set_height(&mut self, height: Vec) { + self.inner.height = EncodingValue::Array { values: height }; + } + + /// Set alignment + /// + /// @param {("left"|"center"|"right")[]} align_values + /// @param {Uint32Array} indices + #[wasm_bindgen(skip_jsdoc)] + pub fn set_align(&mut self, align_values: JsValue, indices: Vec) -> Result<(), JsError> { + let align_values: Vec = align_values.into_serde()?; + let values = indices + .iter() + .map(|ind| align_values[*ind].clone()) + .collect::>(); + self.inner.align = EncodingValue::Array { values }; + Ok(()) + } + + /// Set alignment + /// + /// @param {("alphabetic"|"top"|"middle"|"bottom"|"line-top"|"line-bottom")[]} baseline_values + /// @param {Uint32Array} indices + #[wasm_bindgen(skip_jsdoc)] + pub fn set_baseline( + &mut self, + baseline_values: JsValue, + indices: Vec, + ) -> Result<(), JsError> { + let baseline_values: Vec = baseline_values.into_serde()?; + let values = indices + .iter() + .map(|ind| baseline_values[*ind].clone()) + .collect::>(); + self.inner.baseline = EncodingValue::Array { values }; + Ok(()) + } + + /// Set image + /// + /// @typedef {Object} RgbaImage + /// @property {number} width - The width of the image in pixels. + /// @property {number} height - The height of the image in pixels. + /// @property {Uint8Array} data - The raw byte data of the image. + /// + /// @param {RgbaImage[]} images + #[wasm_bindgen(skip_jsdoc)] + pub fn set_image( + &mut self, + images: JsValue, + ) -> Result<(), JsError> { + // Use serde_wasm_bindgen instead of gloo_utils to supported + // nested struct + let images: Vec = serde_wasm_bindgen::from_value(images)?; + self.inner.image = EncodingValue::Array { values: images }; + Ok(()) + } +} diff --git a/avenger-vega-renderer/src/marks/mod.rs b/avenger-vega-renderer/src/marks/mod.rs index 1905568..1ffc2d0 100644 --- a/avenger-vega-renderer/src/marks/mod.rs +++ b/avenger-vega-renderer/src/marks/mod.rs @@ -9,3 +9,4 @@ pub mod symbol; pub mod text; pub mod trail; pub mod util; +pub mod image; diff --git a/avenger-vega-renderer/test/test_baselines.py b/avenger-vega-renderer/test/test_baselines.py index 0240855..53c81e2 100644 --- a/avenger-vega-renderer/test/test_baselines.py +++ b/avenger-vega-renderer/test/test_baselines.py @@ -140,6 +140,15 @@ def failures_path(): ("trail", "trail_stocks", 0.0), ("trail", "trail_stocks_opacity", 0.0), + ("image", "logos", 0.0002), + ("image", "logos_sized_aspect_false", 0.0002), + ("image", "logos_sized_aspect_false_align_baseline", 0.0002), + ("image", "logos_sized_aspect_true_align_baseline", 0.0002), + # ("image", "smooth_false", 0.0), # Smooth false not supported yet + ("image", "smooth_true", 0.0002), + ("image", "many_images", 0.04), # svg renderer shows missing images for some + # ("image", "large_images", 0.0), # CORS issue loading from cdn + ("gradients", "symbol_cross_gradient", 0.0001), ("gradients", "symbol_circles_gradient_stroke", 0.0001), ("gradients", "symbol_radial_gradient", 0.0002), @@ -155,10 +164,8 @@ def failures_path(): ("clip", "text_clip", 0.006), ("clip", "text_clip_rounded", 0.006), ("clip", "bar_rounded2", 0.0), - - # # TODO: Need more marks - # ("clip", "clip_mixed_marks", 0.0), - # ("clip", "clip_rounded", 0.0), + ("clip", "clip_mixed_marks", 0.0), + ("clip", "clip_rounded", 0.0), # # TODO: line legends # ("line", "stocks-legend", 0.0), @@ -239,7 +246,7 @@ def spec_to_image( f"vegaEmbed('#plot-container', {json.dumps(spec)}, {json.dumps(embed_opts)});" ) page.evaluate_handle(script) - page.wait_for_timeout(100) + page.wait_for_timeout(1000) if renderer == "svg": locator = page.locator("svg") else: diff --git a/avenger/src/marks/image.rs b/avenger/src/marks/image.rs index 7c3b342..074221e 100644 --- a/avenger/src/marks/image.rs +++ b/avenger/src/marks/image.rs @@ -46,7 +46,28 @@ impl ImageMark { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +impl Default for ImageMark { + fn default() -> Self { + Self { + name: "image_mark".to_string(), + clip: true, + len: 1, + aspect: true, + indices: None, + smooth: true, + x: EncodingValue::Scalar { value: 0.0 }, + y: EncodingValue::Scalar { value: 0.0 }, + width: EncodingValue::Scalar { value: 0.0 }, + height: EncodingValue::Scalar { value: 0.0 }, + align: EncodingValue::Scalar { value: Default::default() }, + baseline: EncodingValue::Scalar { value: Default::default() }, + image: EncodingValue::Scalar { value: Default::default() }, + zindex: None, + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct RgbaImage { pub width: u32, pub height: u32, From 470a599aa8bb1def127bdcd6c778bddc57009a3c Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 28 Apr 2024 17:04:20 -0400 Subject: [PATCH 18/22] Move images to avenger repo --- .../vega-specs/image/large_images.vg.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/avenger-vega-test-data/vega-specs/image/large_images.vg.json b/avenger-vega-test-data/vega-specs/image/large_images.vg.json index ab2c3c1..336a54d 100644 --- a/avenger-vega-test-data/vega-specs/image/large_images.vg.json +++ b/avenger-vega-test-data/vega-specs/image/large_images.vg.json @@ -13,52 +13,52 @@ { "x": 0.5, "y": 0.5, - "img": "https://filedn.com/lo5VE4SmtWKXIvNsinHVy7F/datasets/logos/js_logo.png" + "img": "https://raw.githubusercontent.com/jonmmease/avenger/main/avenger-vega-test-data/images/js_logo.png" }, { "x": 1.5, "y": 1.5, - "img": "https://filedn.com/lo5VE4SmtWKXIvNsinHVy7F/datasets/logos/matplotlib.png" + "img": "https://raw.githubusercontent.com/jonmmease/avenger/main/avenger-vega-test-data/images/matplotlib.png" }, { "x": 2.5, "y": 2.5, - "img": "https://filedn.com/lo5VE4SmtWKXIvNsinHVy7F/datasets/logos/python_logo.png" + "img": "https://raw.githubusercontent.com/jonmmease/avenger/main/avenger-vega-test-data/images/python_logo.png" }, { "x": 3.5, "y": 3.5, - "img": "https://filedn.com/lo5VE4SmtWKXIvNsinHVy7F/datasets/logos/rust_logo.png" + "img": "https://raw.githubusercontent.com/jonmmease/avenger/main/avenger-vega-test-data/images/rust_logo.png" }, { "x": 4.5, "y": 4.5, - "img": "https://filedn.com/lo5VE4SmtWKXIvNsinHVy7F/datasets/logos/scipy_logo.png" + "img": "https://raw.githubusercontent.com/jonmmease/avenger/main/avenger-vega-test-data/images/scipy_logo.png" }, { "x": 5.5, "y": 5.5, - "img": "https://filedn.com/lo5VE4SmtWKXIvNsinHVy7F/datasets/logos/VegaFusion-512x512.png" + "img": "https://raw.githubusercontent.com/jonmmease/avenger/main/avenger-vega-test-data/images/VegaFusion-512x512.png" }, { "x": 6.5, "y": 6.5, - "img": "https://filedn.com/lo5VE4SmtWKXIvNsinHVy7F/datasets/logos/VG_Black%40512.png" + "img": "https://raw.githubusercontent.com/jonmmease/avenger/main/avenger-vega-test-data/images/VG_Black@512.png" }, { "x": 7.5, "y": 7.5, - "img": "https://filedn.com/lo5VE4SmtWKXIvNsinHVy7F/datasets/logos/VG_Color%40512.png" + "img": "https://raw.githubusercontent.com/jonmmease/avenger/main/avenger-vega-test-data/images/VG_Color%40512.png" }, { "x": 8.5, "y": 8.5, - "img": "https://filedn.com/lo5VE4SmtWKXIvNsinHVy7F/datasets/logos/VL_Black%40512.png" + "img": "https://raw.githubusercontent.com/jonmmease/avenger/main/avenger-vega-test-data/images/VL_Black%40512.png" }, { "x": 9.5, "y": 9.5, - "img": "https://filedn.com/lo5VE4SmtWKXIvNsinHVy7F/datasets/logos/VL_Color%40512.png" + "img": "https://raw.githubusercontent.com/jonmmease/avenger/main/avenger-vega-test-data/images/VL_Color%40512.png" } ] }, From d3dd42a9689c8f4fe626ddcb2ca76ccc40106ce6 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 28 Apr 2024 17:22:33 -0400 Subject: [PATCH 19/22] Use vega's resource loader for image loading --- avenger-vega-renderer/js/index.js | 3 +- avenger-vega-renderer/js/marks/group.js | 16 +++-- avenger-vega-renderer/js/marks/image.js | 59 ++++++++++--------- avenger-vega-renderer/js/marks/scenegraph.js | 13 +++- avenger-vega-renderer/test/test_baselines.py | 2 +- .../test/test_server/webpack.config.js | 6 ++ 6 files changed, 61 insertions(+), 38 deletions(-) diff --git a/avenger-vega-renderer/js/index.js b/avenger-vega-renderer/js/index.js index 2fd2b63..6a93881 100644 --- a/avenger-vega-renderer/js/index.js +++ b/avenger-vega-renderer/js/index.js @@ -106,7 +106,8 @@ inherits(AvengerRenderer, Renderer, { scene, avengerCanvas.width(), avengerCanvas.height(), - [avengerCanvas.origin_x(), avengerCanvas.origin_y()] + [avengerCanvas.origin_x(), avengerCanvas.origin_y()], + this._loader, ).then((sceneGraph) => { avengerCanvas.set_scene(sceneGraph); console.log("_render time: " + (performance.now() - start)); diff --git a/avenger-vega-renderer/js/marks/group.js b/avenger-vega-renderer/js/marks/group.js index 1e851e3..27e8e87 100644 --- a/avenger-vega-renderer/js/marks/group.js +++ b/avenger-vega-renderer/js/marks/group.js @@ -1,5 +1,5 @@ import {GroupMark} from "../../pkg/avenger_wasm.js"; -import { importSymbol } from "./symbol.js" +import { importSymbol, importStrokeLegend } from "./symbol.js" import { importRule } from "./rule.js"; import { importText } from "./text.js"; import { importRect } from "./rect.js"; @@ -12,6 +12,7 @@ import {importTrail} from "./trail.js"; import {importImage} from "./image.js"; /** + * @typedef {import('./scenegraph.js').IResourceLoader} IResourceLoader * @typedef {import('./symbol.js').SymbolMarkSpec} SymbolMarkSpec * @typedef {import('./text.js').TextMarkSpec} TextMarkSpec * @typedef {import('./rule.js').RuleMarkSpec} RuleMarkSpec @@ -61,9 +62,10 @@ import {importImage} from "./image.js"; * @param {GroupItemSpec} vegaGroup * @param {string} name * @param {boolean} forceClip + * @param {IResourceLoader} loader * @returns {Promise} */ -export async function importGroup(vegaGroup, name, forceClip) { +export async function importGroup(vegaGroup, name, forceClip, loader) { const width = vegaGroup.width ?? (vegaGroup.x2 != null? vegaGroup.x2 - vegaGroup.x: null); const height = vegaGroup.height ?? (vegaGroup.y2 != null? vegaGroup.y2 - vegaGroup.y: null); @@ -76,7 +78,11 @@ export async function importGroup(vegaGroup, name, forceClip) { const clip = vegaGroup.clip || forceClip; switch (vegaMark.marktype) { case "symbol": - groupMark.add_symbol_mark(importSymbol(vegaMark, clip)); + if (vegaMark.items.length && vegaMark.items[0].shape === "stroke") { + groupMark.add_group_mark(importStrokeLegend(vegaMark, clip)); + } else { + groupMark.add_symbol_mark(importSymbol(vegaMark, clip)); + } break; case "rule": groupMark.add_rule_mark(importRule(vegaMark, clip)); @@ -103,14 +109,14 @@ export async function importGroup(vegaGroup, name, forceClip) { groupMark.add_trail_mark(importTrail(vegaMark, clip)); break; case "image": - groupMark.add_image_mark(await importImage(vegaMark, clip)); + groupMark.add_image_mark(await importImage(vegaMark, clip, loader)); break; case "text": groupMark.add_text_mark(importText(vegaMark, clip)); break; case "group": for (const groupItem of vegaMark.items) { - groupMark.add_group_mark(await importGroup(groupItem, vegaMark.name, clip)); + groupMark.add_group_mark(await importGroup(groupItem, vegaMark.name, clip, loader)); } break; } diff --git a/avenger-vega-renderer/js/marks/image.js b/avenger-vega-renderer/js/marks/image.js index 080b2ea..f747fc6 100644 --- a/avenger-vega-renderer/js/marks/image.js +++ b/avenger-vega-renderer/js/marks/image.js @@ -30,11 +30,14 @@ import {encodeSimpleArray} from "./util.js"; */ /** + * @typedef {import('./scenegraph.js').IResourceLoader} IResourceLoader + * * @param {ImageMarkSpec} vegaImageMark * @param {boolean} forceClip + * @param {IResourceLoader} loader * @returns {Promise} */ -export async function importImage(vegaImageMark, forceClip) { +export async function importImage(vegaImageMark, forceClip, loader) { const items = vegaImageMark.items; const len = items.length; @@ -79,7 +82,7 @@ export async function importImage(vegaImageMark, forceClip) { } else { url = item.url; } - image[i] = await fetchImage(url); + image[i] = await fetchImage(url, loader); } if (item.x != null) { @@ -156,55 +159,53 @@ const imageCache = new Map(); * Fetches an image from the specified URL and returns its RGBA data. * If the image has been fetched before, the cached result will be used. * @param {string} url - The URL of the image to fetch. + * @param {IResourceLoader} loader * @returns {Promise} A promise that resolves with the RGBA data of the image. */ -async function fetchImage(url) { +async function fetchImage(url, loader) { // Check if the image data is already cached in the Map if (imageCache.has(url)) { return imageCache.get(url); } // Fetch and process the image, then cache the promise in the Map - const imagePromise = performFetchImage(url); + const imagePromise = performFetchImage(url, loader); imageCache.set(url, imagePromise); return imagePromise; } /** - * Fetches and processes the image to extract RGBA data. + * Fetches and processes the image to extract RGBA data using a given resource loader. * @param {string} url - The URL of the image to fetch. + * @param {IResourceLoader} resourceLoader - The resource loader instance to use for loading the image. * @returns {Promise} A promise that resolves with the RGBA data of the image. */ -async function performFetchImage(url) { +async function performFetchImage(url, resourceLoader) { try { - const response = await fetch(url); - const blob = await response.blob(); + const img = await resourceLoader.loadImage(url); + await resourceLoader.ready(); return new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - canvas.width = img.width; - canvas.height = img.height; - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const data = new Uint8Array(imageData.data.buffer); - - resolve({ - width: img.width, - height: img.height, - data: data - }); - }; - - img.onerror = () => { + if (!img.complete || img.naturalWidth === 0) { reject(new Error("Failed to load image.")); - }; + } - img.src = URL.createObjectURL(blob); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const data = new Uint8Array(imageData.data.buffer); + + resolve({ + width: img.width, + height: img.height, + data: data + }); }); } catch (error) { - console.error("Error fetching image:", error); + console.error("Error fetching image using resource loader:", error); + throw new Error("Error in resource loader image fetch"); } } \ No newline at end of file diff --git a/avenger-vega-renderer/js/marks/scenegraph.js b/avenger-vega-renderer/js/marks/scenegraph.js index 6035bb4..3d921cf 100644 --- a/avenger-vega-renderer/js/marks/scenegraph.js +++ b/avenger-vega-renderer/js/marks/scenegraph.js @@ -1,17 +1,26 @@ import { SceneGraph } from "../../pkg/avenger_wasm.js"; import { importGroup } from "./group.js"; +/** + * @typedef {Object} IResourceLoader + * @property {function(): number} pending - Returns the number of pending load operations. + * @property {function(string): Promise} sanitizeURL - Sanitizes a given URI and returns a promise that resolves to sanitized URI options. + * @property {function(string): Promise} loadImage - Attempts to load an image from a given URI, handling load counters, and returns a promise. + * @property {function(): Promise} ready - Returns a promise that resolves when all pending operations have completed. + */ + /** * @param {import("group").GroupMarkSpec} groupMark * @param {number} width * @param {number} height * @param {[number, number]} origin + * @param {IResourceLoader} loader * @returns {Promise} */ -export async function importScenegraph(groupMark, width, height, origin) { +export async function importScenegraph(groupMark, width, height, origin, loader) { const sceneGraph = new SceneGraph(width, height, origin[0], origin[1]); for (const vegaGroup of groupMark.items) { - sceneGraph.add_group(await importGroup(vegaGroup, groupMark.name, false)); + sceneGraph.add_group(await importGroup(vegaGroup, groupMark.name, false, loader)); } return sceneGraph; } diff --git a/avenger-vega-renderer/test/test_baselines.py b/avenger-vega-renderer/test/test_baselines.py index 53c81e2..0ed7d89 100644 --- a/avenger-vega-renderer/test/test_baselines.py +++ b/avenger-vega-renderer/test/test_baselines.py @@ -147,7 +147,7 @@ def failures_path(): # ("image", "smooth_false", 0.0), # Smooth false not supported yet ("image", "smooth_true", 0.0002), ("image", "many_images", 0.04), # svg renderer shows missing images for some - # ("image", "large_images", 0.0), # CORS issue loading from cdn + ("image", "large_images", 0.0002), # CORS issue loading from cdn ("gradients", "symbol_cross_gradient", 0.0001), ("gradients", "symbol_circles_gradient_stroke", 0.0001), diff --git a/avenger-vega-renderer/test/test_server/webpack.config.js b/avenger-vega-renderer/test/test_server/webpack.config.js index a5de76f..76e3c77 100644 --- a/avenger-vega-renderer/test/test_server/webpack.config.js +++ b/avenger-vega-renderer/test/test_server/webpack.config.js @@ -8,6 +8,7 @@ module.exports = { filename: "bootstrap.js", }, mode: "development", + devtool: 'source-map', // Enables source maps experiments: { asyncWebAssembly: true, // enabling async WebAssembly }, @@ -30,5 +31,10 @@ module.exports = { client: { overlay: false, // Disabling the error overlay }, + headers: { + 'Access-Control-Allow-Origin': '*', // Allows access from any origin + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', // Specify allowed methods + 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization' // Specify allowed headers + }, }, }; From d49702cd5581d67d00bb82833d80a30bc8fdddc0 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 28 Apr 2024 17:26:12 -0400 Subject: [PATCH 20/22] Support line legends --- avenger-vega-renderer/js/marks/symbol.js | 61 +++++++++++++++++--- avenger-vega-renderer/test/test_baselines.py | 8 +-- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/avenger-vega-renderer/js/marks/symbol.js b/avenger-vega-renderer/js/marks/symbol.js index 1c6e739..378d420 100644 --- a/avenger-vega-renderer/js/marks/symbol.js +++ b/avenger-vega-renderer/js/marks/symbol.js @@ -1,10 +1,13 @@ -import {SymbolMark} from "../../pkg/avenger_wasm.js"; +import {SymbolMark, GroupMark, LineMark} from "../../pkg/avenger_wasm.js"; import {encodeSimpleArray} from "./util.js"; /** * @typedef {Object} SymbolItem * @property {number} strokeWidth + * @property {"butt"|"round"|"square"} strokeCap + * @property {"bevel"|"miter"|"round"} strokeJoin + * @property {string|number[]} strokeDash * @property {string|object} fill * @property {string|object} stroke * @property {number} x @@ -48,13 +51,6 @@ export function importSymbol(vegaSymbolMark, force_clip) { } const firstItem = items[0]; - const firstShape = firstItem.shape ?? "circle"; - - if (firstShape === "stroke") { - // TODO: Handle line legends - return symbolMark - } - // Only include stroke_width if there is a stroke color const firstHasStroke = firstItem.stroke != null; let strokeWidth; @@ -170,3 +166,52 @@ export function importSymbol(vegaSymbolMark, force_clip) { return symbolMark; } + + +/** + * Handle special case of symbols with shape == "stroke". This happens when lines are + * sed in legends. We convert these to a group of regular line marks + * @param {SymbolMarkSpec} vegaSymbolMark + * @param {boolean} force_clip + * @returns {GroupMark} + */ +export function importStrokeLegend(vegaSymbolMark, force_clip) { + const groupMark = new GroupMark( + 0, 0, "symbol_line_legend", undefined, undefined + ); + + for (let item of vegaSymbolMark.items) { + let width = Math.sqrt(item.size ?? 100.0); + let x = item.x ?? 0; + let y = item.y ?? 0; + const lineMark = new LineMark( + 2, vegaSymbolMark.clip || force_clip, undefined, vegaSymbolMark.zindex + ); + + lineMark.set_xy( + new Float32Array([x - width / 2.0, x + width / 2.0]), + new Float32Array([y, y]) + ) + + lineMark.set_stroke(item.stroke ?? "", (item.opacity ?? 1) * (item.strokeOpacity ?? 1)); + if (item.strokeWidth != null) { + lineMark.set_stroke_width(item.strokeWidth); + } + + if (item.strokeCap != null) { + lineMark.set_stroke_cap(item.strokeCap); + } + + if (item.strokeJoin != null) { + lineMark.set_stroke_join(item.strokeJoin); + } + + if (item.strokeDash != null) { + lineMark.set_stroke_dash(item.strokeDash); + } + + groupMark.add_line_mark(lineMark); + } + + return groupMark +} diff --git a/avenger-vega-renderer/test/test_baselines.py b/avenger-vega-renderer/test/test_baselines.py index 0ed7d89..894cd7e 100644 --- a/avenger-vega-renderer/test/test_baselines.py +++ b/avenger-vega-renderer/test/test_baselines.py @@ -128,6 +128,8 @@ def failures_path(): ("line", "line_dashed_round_undefined", 0.0), ("line", "line_dashed_square_undefined", 0.02), # square-cap ("line", "line_dashed_butt_undefined", 0.0), + ("line", "stocks-legend", 0.006), + ("line", "stocks_dashed", 0.006), ("area", "100_percent_stacked_area", 0.0), ("area", "simple_unemployment", 0.0), @@ -166,10 +168,6 @@ def failures_path(): ("clip", "bar_rounded2", 0.0), ("clip", "clip_mixed_marks", 0.0), ("clip", "clip_rounded", 0.0), - - # # TODO: line legends - # ("line", "stocks-legend", 0.0), - # ("line", "stocks_dashed", 0.0), ], ) def test_image_baselines( @@ -223,7 +221,7 @@ def compare(page: Page, spec: dict) -> ComparisonResult: page.on("pageerror", lambda e: avenger_errs.append(e)) avenger_img = spec_to_image(page, spec, "avenger") if avenger_errs: - pytest.fail('\n'.join(avenger_errs)) + pytest.fail(avenger_errs) svg_img = spec_to_image(page, spec, "svg") diff_img = Image.new("RGBA", svg_img.size) From d4a37fe3adeed1154a15c4edb47246d45947f356 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 28 Apr 2024 18:30:05 -0400 Subject: [PATCH 21/22] fmt --- avenger-vega-renderer/src/marks/group.rs | 6 ++++-- avenger-vega-renderer/src/marks/image.rs | 13 +++++-------- avenger-vega-renderer/src/marks/mod.rs | 2 +- avenger/src/marks/image.rs | 12 +++++++++--- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/avenger-vega-renderer/src/marks/group.rs b/avenger-vega-renderer/src/marks/group.rs index 2ec9a20..1341b08 100644 --- a/avenger-vega-renderer/src/marks/group.rs +++ b/avenger-vega-renderer/src/marks/group.rs @@ -1,5 +1,6 @@ use crate::marks::arc::ArcMark; use crate::marks::area::AreaMark; +use crate::marks::image::ImageMark; use crate::marks::line::LineMark; use crate::marks::path::PathMark; use crate::marks::rect::RectMark; @@ -7,7 +8,6 @@ use crate::marks::rule::RuleMark; use crate::marks::symbol::SymbolMark; use crate::marks::text::TextMark; use crate::marks::trail::TrailMark; -use crate::marks::image::ImageMark; use crate::marks::util::{decode_color, decode_gradient}; use avenger::marks::group::{Clip, SceneGroup as RsSceneGroup}; use avenger::marks::mark::SceneMark; @@ -195,7 +195,9 @@ impl GroupMark { } pub fn add_image_mark(&mut self, mark: ImageMark) { - self.inner.marks.push(SceneMark::Image(Box::new(mark.build()))); + self.inner + .marks + .push(SceneMark::Image(Box::new(mark.build()))); } pub fn add_group_mark(&mut self, mark: GroupMark) { diff --git a/avenger-vega-renderer/src/marks/image.rs b/avenger-vega-renderer/src/marks/image.rs index 94d8a94..478d234 100644 --- a/avenger-vega-renderer/src/marks/image.rs +++ b/avenger-vega-renderer/src/marks/image.rs @@ -1,9 +1,9 @@ +use crate::marks::util::zindex_to_indices; +use avenger::marks::image::{ImageMark as RsImageMark, RgbaImage}; +use avenger::marks::value::{EncodingValue, ImageAlign, ImageBaseline}; use gloo_utils::format::JsValueSerdeExt; -use wasm_bindgen::{JsError, JsValue}; use wasm_bindgen::prelude::wasm_bindgen; -use avenger::marks::value::{EncodingValue, ImageAlign, ImageBaseline}; -use avenger::marks::image::{ImageMark as RsImageMark, RgbaImage}; -use crate::marks::util::zindex_to_indices; +use wasm_bindgen::{JsError, JsValue}; #[wasm_bindgen] pub struct ImageMark { @@ -99,10 +99,7 @@ impl ImageMark { /// /// @param {RgbaImage[]} images #[wasm_bindgen(skip_jsdoc)] - pub fn set_image( - &mut self, - images: JsValue, - ) -> Result<(), JsError> { + pub fn set_image(&mut self, images: JsValue) -> Result<(), JsError> { // Use serde_wasm_bindgen instead of gloo_utils to supported // nested struct let images: Vec = serde_wasm_bindgen::from_value(images)?; diff --git a/avenger-vega-renderer/src/marks/mod.rs b/avenger-vega-renderer/src/marks/mod.rs index 1ffc2d0..b09443a 100644 --- a/avenger-vega-renderer/src/marks/mod.rs +++ b/avenger-vega-renderer/src/marks/mod.rs @@ -1,6 +1,7 @@ pub mod arc; pub mod area; pub mod group; +pub mod image; pub mod line; pub mod path; pub mod rect; @@ -9,4 +10,3 @@ pub mod symbol; pub mod text; pub mod trail; pub mod util; -pub mod image; diff --git a/avenger/src/marks/image.rs b/avenger/src/marks/image.rs index 074221e..439d516 100644 --- a/avenger/src/marks/image.rs +++ b/avenger/src/marks/image.rs @@ -59,9 +59,15 @@ impl Default for ImageMark { y: EncodingValue::Scalar { value: 0.0 }, width: EncodingValue::Scalar { value: 0.0 }, height: EncodingValue::Scalar { value: 0.0 }, - align: EncodingValue::Scalar { value: Default::default() }, - baseline: EncodingValue::Scalar { value: Default::default() }, - image: EncodingValue::Scalar { value: Default::default() }, + align: EncodingValue::Scalar { + value: Default::default(), + }, + baseline: EncodingValue::Scalar { + value: Default::default(), + }, + image: EncodingValue::Scalar { + value: Default::default(), + }, zindex: None, } } From 77dadd482f1fc0bbe08356da428df86bc69dfbb8 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 29 Apr 2024 08:40:01 -0400 Subject: [PATCH 22/22] Fix import paths --- avenger-vega-renderer/js/index.js | 2 +- avenger-vega-renderer/js/marks/arc.js | 2 +- avenger-vega-renderer/js/marks/area.js | 2 +- avenger-vega-renderer/js/marks/group.js | 2 +- avenger-vega-renderer/js/marks/image.js | 2 +- avenger-vega-renderer/js/marks/line.js | 2 +- avenger-vega-renderer/js/marks/path.js | 2 +- avenger-vega-renderer/js/marks/rect.js | 2 +- avenger-vega-renderer/js/marks/rule.js | 2 +- avenger-vega-renderer/js/marks/scenegraph.js | 2 +- avenger-vega-renderer/js/marks/shape.js | 2 +- avenger-vega-renderer/js/marks/symbol.js | 2 +- avenger-vega-renderer/js/marks/text.js | 2 +- avenger-vega-renderer/js/marks/trail.js | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/avenger-vega-renderer/js/index.js b/avenger-vega-renderer/js/index.js index 0be4dee..fbae4d5 100644 --- a/avenger-vega-renderer/js/index.js +++ b/avenger-vega-renderer/js/index.js @@ -1,4 +1,4 @@ -import { instantiate, AvengerCanvas, scene_graph_to_png } from "../pkg/avenger_wasm.js"; +import { instantiate, AvengerCanvas, scene_graph_to_png } from "../lib/avenger_vega_renderer.generated.js"; import { Renderer, CanvasHandler, domClear, domChild } from 'vega-scenegraph'; import { inherits } from 'vega-util'; import { importScenegraph } from "./marks/scenegraph.js" diff --git a/avenger-vega-renderer/js/marks/arc.js b/avenger-vega-renderer/js/marks/arc.js index 9064278..d891a90 100644 --- a/avenger-vega-renderer/js/marks/arc.js +++ b/avenger-vega-renderer/js/marks/arc.js @@ -1,4 +1,4 @@ -import {ArcMark} from "../../pkg/avenger_wasm.js"; +import {ArcMark} from "../../lib/avenger_vega_renderer.generated.js"; import {encodeSimpleArray} from "./util.js"; diff --git a/avenger-vega-renderer/js/marks/area.js b/avenger-vega-renderer/js/marks/area.js index 74285f4..88704ec 100644 --- a/avenger-vega-renderer/js/marks/area.js +++ b/avenger-vega-renderer/js/marks/area.js @@ -1,4 +1,4 @@ -import { AreaMark } from "../../pkg/avenger_wasm.js"; +import { AreaMark } from "../../lib/avenger_vega_renderer.generated.js"; /** * @typedef {Object} AreaItem diff --git a/avenger-vega-renderer/js/marks/group.js b/avenger-vega-renderer/js/marks/group.js index 27e8e87..a1d0974 100644 --- a/avenger-vega-renderer/js/marks/group.js +++ b/avenger-vega-renderer/js/marks/group.js @@ -1,4 +1,4 @@ -import {GroupMark} from "../../pkg/avenger_wasm.js"; +import {GroupMark} from "../../lib/avenger_vega_renderer.generated.js"; import { importSymbol, importStrokeLegend } from "./symbol.js" import { importRule } from "./rule.js"; import { importText } from "./text.js"; diff --git a/avenger-vega-renderer/js/marks/image.js b/avenger-vega-renderer/js/marks/image.js index f747fc6..273af3c 100644 --- a/avenger-vega-renderer/js/marks/image.js +++ b/avenger-vega-renderer/js/marks/image.js @@ -1,4 +1,4 @@ -import {ImageMark} from "../../pkg/avenger_wasm.js"; +import {ImageMark} from "../../lib/avenger_vega_renderer.generated.js"; import {encodeSimpleArray} from "./util.js"; diff --git a/avenger-vega-renderer/js/marks/line.js b/avenger-vega-renderer/js/marks/line.js index 2a55e8b..f6922ab 100644 --- a/avenger-vega-renderer/js/marks/line.js +++ b/avenger-vega-renderer/js/marks/line.js @@ -1,4 +1,4 @@ -import {LineMark} from "../../pkg/avenger_wasm.js"; +import {LineMark} from "../../lib/avenger_vega_renderer.generated.js"; /** diff --git a/avenger-vega-renderer/js/marks/path.js b/avenger-vega-renderer/js/marks/path.js index 16ca439..70ef971 100644 --- a/avenger-vega-renderer/js/marks/path.js +++ b/avenger-vega-renderer/js/marks/path.js @@ -1,4 +1,4 @@ -import {PathMark} from "../../pkg/avenger_wasm.js"; +import {PathMark} from "../../lib/avenger_vega_renderer.generated.js"; import {encodeSimpleArray} from "./util.js"; diff --git a/avenger-vega-renderer/js/marks/rect.js b/avenger-vega-renderer/js/marks/rect.js index 4187afb..c1b8986 100644 --- a/avenger-vega-renderer/js/marks/rect.js +++ b/avenger-vega-renderer/js/marks/rect.js @@ -1,4 +1,4 @@ -import {RectMark} from "../../pkg/avenger_wasm.js"; +import {RectMark} from "../../lib/avenger_vega_renderer.generated.js"; import {encodeSimpleArray} from "./util.js"; diff --git a/avenger-vega-renderer/js/marks/rule.js b/avenger-vega-renderer/js/marks/rule.js index 4fbb386..f76ae66 100644 --- a/avenger-vega-renderer/js/marks/rule.js +++ b/avenger-vega-renderer/js/marks/rule.js @@ -1,4 +1,4 @@ -import {RuleMark} from "../../pkg/avenger_wasm.js"; +import {RuleMark} from "../../lib/avenger_vega_renderer.generated.js"; import {encodeSimpleArray} from "./util.js"; diff --git a/avenger-vega-renderer/js/marks/scenegraph.js b/avenger-vega-renderer/js/marks/scenegraph.js index 3d921cf..9640079 100644 --- a/avenger-vega-renderer/js/marks/scenegraph.js +++ b/avenger-vega-renderer/js/marks/scenegraph.js @@ -1,4 +1,4 @@ -import { SceneGraph } from "../../pkg/avenger_wasm.js"; +import { SceneGraph } from "../../lib/avenger_vega_renderer.generated.js"; import { importGroup } from "./group.js"; /** diff --git a/avenger-vega-renderer/js/marks/shape.js b/avenger-vega-renderer/js/marks/shape.js index 4c2939c..183112d 100644 --- a/avenger-vega-renderer/js/marks/shape.js +++ b/avenger-vega-renderer/js/marks/shape.js @@ -1,4 +1,4 @@ -import {PathMark} from "../../pkg/avenger_wasm.js"; +import {PathMark} from "../../lib/avenger_vega_renderer.generated.js"; import {encodeSimpleArray} from "./util.js"; diff --git a/avenger-vega-renderer/js/marks/symbol.js b/avenger-vega-renderer/js/marks/symbol.js index 378d420..334966d 100644 --- a/avenger-vega-renderer/js/marks/symbol.js +++ b/avenger-vega-renderer/js/marks/symbol.js @@ -1,4 +1,4 @@ -import {SymbolMark, GroupMark, LineMark} from "../../pkg/avenger_wasm.js"; +import {SymbolMark, GroupMark, LineMark} from "../../lib/avenger_vega_renderer.generated.js"; import {encodeSimpleArray} from "./util.js"; diff --git a/avenger-vega-renderer/js/marks/text.js b/avenger-vega-renderer/js/marks/text.js index 0567c37..0f9e5c8 100644 --- a/avenger-vega-renderer/js/marks/text.js +++ b/avenger-vega-renderer/js/marks/text.js @@ -1,4 +1,4 @@ -import {TextMark} from "../../pkg/avenger_wasm.js"; +import {TextMark} from "../../lib/avenger_vega_renderer.generated.js"; import {encodeSimpleArray} from "./util.js"; /** diff --git a/avenger-vega-renderer/js/marks/trail.js b/avenger-vega-renderer/js/marks/trail.js index f37a5d3..118663a 100644 --- a/avenger-vega-renderer/js/marks/trail.js +++ b/avenger-vega-renderer/js/marks/trail.js @@ -1,4 +1,4 @@ -import {TrailMark} from "../../pkg/avenger_wasm.js"; +import {TrailMark} from "../../lib/avenger_vega_renderer.generated.js"; /**