From 78ca0c29e0818472abc6133976106f8906d93f0c Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 29 Jan 2024 20:31:09 -0500 Subject: [PATCH 1/9] Initial python library --- .gitignore | 1 + Cargo.lock | 115 ++ Cargo.toml | 4 +- README.md | 4 +- avenger-python/Cargo.toml | 20 + avenger-python/avenger/__init__.py | 6 + avenger-python/avenger/altair_utils.py | 48 + avenger-python/pyproject.toml | 14 + avenger-python/src/lib.rs | 50 + avenger-vega-test-data/README.md | 12 +- avenger-vega-test-data/src/main.rs | 4 +- .../vega-scenegraphs/text/emoji.png | Bin 159016 -> 172006 bytes .../vega-scenegraphs/text/emoji.sg.json | 4 +- .../vega-specs/text/emoji.vg.json | 1 - avenger-vega/Cargo.toml | 1 + avenger-vega/README.md | 4 +- avenger-vega/src/error.rs | 18 +- avenger-vega/src/image/mod.rs | 6 +- avenger-vega/src/image/reqwest_fetcher.rs | 4 +- avenger-vega/src/marks/arc.rs | 4 +- avenger-vega/src/marks/area.rs | 4 +- avenger-vega/src/marks/group.rs | 6 +- avenger-vega/src/marks/image.rs | 4 +- avenger-vega/src/marks/line.rs | 4 +- avenger-vega/src/marks/path.rs | 6 +- avenger-vega/src/marks/rect.rs | 4 +- avenger-vega/src/marks/rule.rs | 4 +- avenger-vega/src/marks/shape.rs | 6 +- avenger-vega/src/marks/symbol.rs | 12 +- avenger-vega/src/marks/text.rs | 4 +- avenger-vega/src/marks/trail.rs | 4 +- avenger-vega/src/marks/values.rs | 14 +- avenger-vega/src/scene_graph.rs | 6 +- avenger-wgpu/Cargo.toml | 1 + avenger-wgpu/README.md | 4 +- avenger-wgpu/src/canvas.rs | 50 +- avenger-wgpu/src/error.rs | 20 +- avenger-wgpu/src/marks/image.rs | 8 +- avenger-wgpu/src/marks/path.rs | 10 +- avenger-wgpu/src/marks/symbol.rs | 4 +- avenger/Cargo.toml | 1 + avenger/README.md | 2 +- avenger/src/error.rs | 13 +- pixi.lock | 1263 ++++++++++++++++- pixi.toml | 4 + 45 files changed, 1646 insertions(+), 132 deletions(-) create mode 100644 avenger-python/Cargo.toml create mode 100644 avenger-python/avenger/__init__.py create mode 100644 avenger-python/avenger/altair_utils.py create mode 100644 avenger-python/pyproject.toml create mode 100644 avenger-python/src/lib.rs diff --git a/.gitignore b/.gitignore index dfd4373..2be0a09 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ scratch/ # pixi environments .pixi +/avenger-python/avenger/_avenger.abi3.so diff --git a/Cargo.lock b/Cargo.lock index b02e9d7..3745e4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -313,11 +313,26 @@ version = "0.1.0" dependencies = [ "image", "lyon_path", + "pyo3", "reqwest", "serde", "thiserror", ] +[[package]] +name = "avenger-python" +version = "0.0.1" +dependencies = [ + "avenger", + "avenger-vega", + "avenger-wgpu", + "image", + "pollster", + "pyo3", + "pythonize", + "serde", +] + [[package]] name = "avenger-vega" version = "0.1.0" @@ -328,6 +343,7 @@ dependencies = [ "image", "lyon_extra", "lyon_path", + "pyo3", "reqwest", "serde", "serde_json", @@ -364,6 +380,7 @@ dependencies = [ "log", "lyon", "pollster", + "pyo3", "rstest", "serde_json", "thiserror", @@ -2957,6 +2974,12 @@ dependencies = [ "serde", ] +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + [[package]] name = "inotify" version = "0.9.6" @@ -3534,6 +3557,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "metal" version = "0.27.0" @@ -4561,6 +4593,77 @@ dependencies = [ "cc", ] +[[package]] +name = "pyo3" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a89dc7a5850d0e983be1ec2a463a171d20990487c3cfcd68b5363f1ee3d6fe0" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset 0.9.0", + "parking_lot 0.12.1", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07426f0d8fe5a601f26293f300afd1a7b1ed5e78b2a705870c5f30893c5163be" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb7dec17e17766b46bca4f1a4215a85006b4c2ecde122076c562dd058da6cf1" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f738b4e40d50b5711957f142878cfa0f28e054aa0ebdfc3fd137a843f74ed3" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc910d4851847827daf9d6cdd4a823fbdaab5b8818325c5e97a86da79e8881f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "pythonize" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd1c3ef39c725d63db5f9bc455461bafd80540cb7824c61afb823501921a850" +dependencies = [ + "pyo3", + "serde", +] + [[package]] name = "qoi" version = "0.4.1" @@ -6131,6 +6234,12 @@ dependencies = [ "libc", ] +[[package]] +name = "target-lexicon" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" + [[package]] name = "tempfile" version = "3.8.1" @@ -6649,6 +6758,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + [[package]] name = "universal-hash" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 75ccf7a..c6153a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "avenger-vega", "avenger-wgpu", "avenger-vega-test-data", + "avenger-python", ] resolver = "2" @@ -16,4 +17,5 @@ lyon_path = "1.0.1" lyon_extra = "1.0.1" lyon = "1.0.1" image = { version="0.24.8", default-features = false } -lazy_static = "1.4.0" \ No newline at end of file +lazy_static = "1.4.0" +pyo3 = "0.20.2" diff --git a/README.md b/README.md index ca9844f..83a3ad1 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ a non-Vega application, please open an issue. ## Run native To launch a wgpu rendered visualization in a native window, run the following: ``` -cd sg2d-wgpu +cd examples/wgpu-winit cargo run ``` @@ -45,7 +45,7 @@ image export, and to VegaFusion to support serverside rendering of large marks. # Testing To start with, the most valuable contribution of this project is probably the testing infrastructure. By relying on vl-convert, a collection of input Vega specs are rendered to PNG and converted to scene graphs. The GPU rendered -PNG images are then compared for similarity to the baselines using structural similarity. See the `gen-test-data` +PNG images are then compared for similarity to the baselines using structural similarity. See the `avenger-vega-test-data` crate for more information. Note: These tests aren't running on GitHub Actions yet due to a `MakeWgpuAdapterError` error that diff --git a/avenger-python/Cargo.toml b/avenger-python/Cargo.toml new file mode 100644 index 0000000..30f6bb4 --- /dev/null +++ b/avenger-python/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "avenger-python" +version = "0.0.1" +edition = "2021" +license = "BSD-3-Clause" +publish = false + +[lib] +name = "avenger" +crate-type = ["cdylib"] + +[dependencies] +avenger = { path = "../avenger", features = ["pyo3"] } +avenger-vega = { path = "../avenger-vega", features = ["pyo3"] } +avenger-wgpu = { path = "../avenger-wgpu", features = ["pyo3"] } +pyo3 = { workspace = true, features = ["extension-module", "abi3-py38"] } +pythonize = "0.20.0" +serde = {workspace = true} +pollster = "0.3" +image = {workspace = true} diff --git a/avenger-python/avenger/__init__.py b/avenger-python/avenger/__init__.py new file mode 100644 index 0000000..fb26786 --- /dev/null +++ b/avenger-python/avenger/__init__.py @@ -0,0 +1,6 @@ +from ._avenger import * +from . import altair_utils + +# Re-export public members of the Rust _avenger modules +if hasattr(_avenger, "__all__"): + __all__ = _avenger.__all__ diff --git a/avenger-python/avenger/altair_utils.py b/avenger-python/avenger/altair_utils.py new file mode 100644 index 0000000..764464d --- /dev/null +++ b/avenger-python/avenger/altair_utils.py @@ -0,0 +1,48 @@ +from ._avenger import SceneGraph + + +def avenger_png_renderer(spec: dict, **kwargs) -> dict: + """ + Altair renderer plugin that uses Avenger to render charts to PNG + + This function is registered as avenger-png in the altair.vegalite.v5.renderer + entry point group. It may be enabled in Altair using: + + >>> import altair as alt + >>> alt.renderers.enable('avenger-png') + + See https://altair-viz.github.io/user_guide/custom_renderers.html + for more information + """ + import altair as alt + import vl_convert as vlc + + if alt.data_transformers.active == "vegafusion": + # When the vegafusion transformer is enabled we convert the spec to + # Vega, which will include the pre-transformed inline data + vg_spec = alt.Chart.from_dict(spec).to_dict(format="vega") + vega_sg = vlc.vega_to_scenegraph(vg_spec) + else: + vega_sg = vlc.vegalite_to_scenegraph(spec) + + sg = SceneGraph.from_vega_scenegraph(vega_sg) + return {"image/png": sg.to_png(scale=kwargs.get("scale", None))} + + +def chart_to_png(chart, scale=1) -> bytes: + """ + Convert an altair chart to a png image bytes + :param chart: Altair Chart + :param scale: Scale factor (default 1.0) + :return: png image bytes + """ + import altair as alt + import vl_convert as vlc + if alt.data_transformers.active == "vegafusion": + vg_spec = chart.to_dict(format="vega") + vega_sg = vlc.vega_to_scenegraph(vg_spec) + else: + vl_spec = chart.to_dict(format="vega-lite") + vega_sg = vlc.vegalite_to_scenegraph(vl_spec) + sg = SceneGraph.from_vega_scenegraph(vega_sg) + return sg.to_png(scale=scale) diff --git a/avenger-python/pyproject.toml b/avenger-python/pyproject.toml new file mode 100644 index 0000000..ff35447 --- /dev/null +++ b/avenger-python/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "avenger" +version = "0.0.1" + +[tool.maturin] +module-name = "avenger._avenger" + +[build-system] +requires = ["maturin>=1.1.0,<2"] +build-backend = "maturin" + +# Register Altair renderer plugin +[project.entry-points."altair.vegalite.v5.renderer"] +avenger-png = "avenger.altair_utils:avenger_png_renderer" \ No newline at end of file diff --git a/avenger-python/src/lib.rs b/avenger-python/src/lib.rs new file mode 100644 index 0000000..15c64f5 --- /dev/null +++ b/avenger-python/src/lib.rs @@ -0,0 +1,50 @@ +use avenger::scene_graph::SceneGraph as RsSceneGraph; +use avenger_vega::scene_graph::VegaSceneGraph; +use avenger_wgpu::canvas::{Canvas, CanvasDimensions, PngCanvas}; +use image::{EncodableLayout, ImageOutputFormat}; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::PyBytes; +use pythonize::depythonize; +use std::io::Cursor; + +#[pyclass] +pub struct SceneGraph { + inner: RsSceneGraph, +} + +#[pymethods] +impl SceneGraph { + #[staticmethod] + fn from_vega_scenegraph(vega_sg: &PyAny) -> PyResult { + let vega_sg: VegaSceneGraph = depythonize(vega_sg)?; + let inner = vega_sg.to_scene_graph()?; + Ok(Self { inner }) + } + + fn to_png(&mut self, py: Python, scale: Option) -> PyResult { + let mut png_canvas = pollster::block_on(PngCanvas::new(CanvasDimensions { + size: [self.inner.width, self.inner.height], + scale: scale.unwrap_or(1.0), + }))?; + + png_canvas.set_scene(&self.inner)?; + + let img = pollster::block_on(png_canvas.render())?; + let mut png_data = Vec::new(); + + img.write_to(&mut Cursor::new(&mut png_data), ImageOutputFormat::Png) + .map_err(|err| { + PyValueError::new_err(format!("Failed to convert image to PNG: {err:?}")) + })?; + + Ok(PyObject::from(PyBytes::new(py, png_data.as_bytes()))) + } +} + +#[pymodule] +fn _avenger(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add("__version__", env!("CARGO_PKG_VERSION"))?; + Ok(()) +} diff --git a/avenger-vega-test-data/README.md b/avenger-vega-test-data/README.md index b4ae657..b6fd343 100644 --- a/avenger-vega-test-data/README.md +++ b/avenger-vega-test-data/README.md @@ -1,18 +1,18 @@ ## Generate Test Data -This is a binary crate responsible for generating image baseline vega test data for the sg2d-wgpu renderer. +This is a binary crate responsible for generating image baseline vega test data for the avenger-wgpu renderer. From the project root ``` -cargo run -p sg2d-vega-test-data +cargo run -p avenger-vega-test-data ``` ## How it works -A collection of Vega specs are located under `sg2d-vega-test-data/vega-specs` inside category directories. -For example: `sg2d-vega-test-data/vega-specs/rect/stacked_bar.vg.json`. +A collection of Vega specs are located under `avenger-vega-test-data/vega-specs` inside category directories. +For example: `avenger-vega-test-data/vega-specs/rect/stacked_bar.vg.json`. The main binary entry point scans this directory and for each Vega spec it uses [`vl-convert-rs`](https://github.com/vega/vl-convert) to -output the following three files in matching category directory under `sg2d-vega-test-data/vega-scenegraphs` +output the following three files in matching category directory under `avenger-vega-test-data/vega-scenegraphs` 1. `{spec_name}.sg.json`: This is the JSON representation of the scenegraph that Vega generates for this Vega spec. This is used as the input for the wgpu renderer. 2. `{spec_name}.dims.json`: This JSON file contains the final chart width, height, and origin values. @@ -21,5 +21,5 @@ output the following three files in matching category directory under `sg2d-vega chart to SVG and then renders the SVG to PNG using [resvg](https://github.com/RazrFalcon/resvg). This PNG image serves as the baseline that wgpu rendered PNGs are compared to. -Image baselines are tested in `sg2d-wgpu/tests/test_image_baselines.rs`. Image similarity is measured +Image baselines are tested in `avenger-wgpu/tests/test_image_baselines.rs`. Image similarity is measured using [DSSIM](https://github.com/kornelski/dssim). diff --git a/avenger-vega-test-data/src/main.rs b/avenger-vega-test-data/src/main.rs index 1929130..aba7f8a 100644 --- a/avenger-vega-test-data/src/main.rs +++ b/avenger-vega-test-data/src/main.rs @@ -3,8 +3,8 @@ use std::path::Path; use std::{fs, io}; use vl_convert_rs::VlConverter; -/// Generate test data for each Vega spec located in `sg2d-vega-test-data/vega-specs` -/// For each spec, the following three files are saved to `sg2d-vega-test-data/vega-scenegraphs` +/// Generate test data for each Vega spec located in `avenger-vega-test-data/vega-specs` +/// For each spec, the following three files are saved to `avenger-vega-test-data/vega-scenegraphs` /// 1. spec_name.dims.json: This is a JSON file containing the chart's width, height, and origin /// 2. spec_name.sg.json: This is a JSON file containing the chart's scene graph /// 3. spec_name.png: This is a PNG rendering of the chart using vl-convert with resvg diff --git a/avenger-vega-test-data/vega-scenegraphs/text/emoji.png b/avenger-vega-test-data/vega-scenegraphs/text/emoji.png index 514c55a47e922cc37b7ab2eae5d5f2229ebf7fe6..c84795c673d47ad215e9faef61f6b3e5f14316a4 100644 GIT binary patch literal 172006 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

Sq`Z~Fmnfi`e(sE{OjXdC-`AAb6>h`;R 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 literal 159016 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

~n6w_0A* zYFJ)74wNw3kcVF6=4D_%jP3o9KpZpz5S%Z*&>*}ygx zUY8zu3TkSIANYDfeO8NXmHqvHtF&fXLiW_o-%b6< z_yM2qxy9@Qy=MwjETAOLc=8orO zpU3z^ryV?E(N8}IBldYSeqL_7n9Lu~-ZOc>iE<8It{Z`l1o3TQ%x;?Tnj$ztK!lfd zgAK?P5}XiaX_Y`XcRYo51RUCT@{zuQe|EmlB_|QkzO|yTlzUtb~Z2t6L-SdATOUPpY diff --git a/avenger-vega-test-data/vega-scenegraphs/text/emoji.sg.json b/avenger-vega-test-data/vega-scenegraphs/text/emoji.sg.json index bdd7c5a..b4cd02e 100644 --- a/avenger-vega-test-data/vega-scenegraphs/text/emoji.sg.json +++ b/avenger-vega-test-data/vega-scenegraphs/text/emoji.sg.json @@ -294,7 +294,7 @@ } ], "fill": "transparent", - "stroke": "transparent", + "stroke": "#ddd", "width": 550, "height": 140, "x": 0, @@ -556,7 +556,7 @@ } ], "fill": "transparent", - "stroke": "transparent", + "stroke": "#ddd", "width": 550, "height": 140, "x": 0, diff --git a/avenger-vega-test-data/vega-specs/text/emoji.vg.json b/avenger-vega-test-data/vega-specs/text/emoji.vg.json index 1fb25a0..e983355 100644 --- a/avenger-vega-test-data/vega-specs/text/emoji.vg.json +++ b/avenger-vega-test-data/vega-specs/text/emoji.vg.json @@ -2,7 +2,6 @@ "$schema": "https://vega.github.io/schema/vega/v5.json", "background": "white", "padding": 5, - "config": {"style": {"cell": {"stroke": "transparent"}}}, "data": [ { "name": "data-006229bd86d4a5200814600dc915ef80", diff --git a/avenger-vega/Cargo.toml b/avenger-vega/Cargo.toml index aa61f44..f1f14e0 100644 --- a/avenger-vega/Cargo.toml +++ b/avenger-vega/Cargo.toml @@ -18,3 +18,4 @@ lyon_extra = { workspace = true } lyon_path = { workspace = true, features = ["serialization"]} image = { workspace = true, default-features = false, features = ["png"] } reqwest = { version = "0.11.23", features = ["blocking"], optional = true } +pyo3 = { workspace = true, optional = true } \ No newline at end of file diff --git a/avenger-vega/README.md b/avenger-vega/README.md index 172e1a4..4a2e376 100644 --- a/avenger-vega/README.md +++ b/avenger-vega/README.md @@ -1,3 +1,3 @@ -## sg2d-vega +## avenger-vega -This crate contains the logic for parsing vega scenegraphs and building sg2d scenegraphs \ No newline at end of file +This crate contains the logic for parsing vega scenegraphs and building avenger scenegraphs \ No newline at end of file diff --git a/avenger-vega/src/error.rs b/avenger-vega/src/error.rs index 811e615..18785ff 100644 --- a/avenger-vega/src/error.rs +++ b/avenger-vega/src/error.rs @@ -1,9 +1,12 @@ use thiserror::Error; +#[cfg(feature = "pyo3")] +use pyo3::{exceptions::PyValueError, PyErr}; + #[derive(Error, Debug)] -pub enum VegaSceneGraphError { - #[error("SceneGraph error")] - SceneGraphError(#[from] avenger::error::SceneGraphError), +pub enum AvengerVegaError { + #[error("Avenger error")] + AvengerError(#[from] avenger::error::AvengerError), #[error("css color parse error")] InvalidColor(#[from] csscolorparser::ParseColorError), @@ -26,8 +29,15 @@ pub enum VegaSceneGraphError { ReqwestError(#[from] reqwest::Error), } -impl From for VegaSceneGraphError { +impl From for AvengerVegaError { fn from(value: lyon_extra::parser::ParseError) -> Self { Self::InvalidSvgPath(value) } } + +#[cfg(feature = "pyo3")] +impl From for PyErr { + fn from(err: AvengerVegaError) -> PyErr { + PyValueError::new_err(err.to_string()) + } +} diff --git a/avenger-vega/src/image/mod.rs b/avenger-vega/src/image/mod.rs index f5838d2..803109e 100644 --- a/avenger-vega/src/image/mod.rs +++ b/avenger-vega/src/image/mod.rs @@ -4,14 +4,14 @@ pub mod reqwest_fetcher; #[cfg(feature = "image-request")] use crate::image::reqwest_fetcher::ReqwestImageFetcher; -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use image::DynamicImage; pub trait ImageFetcher { - fn fetch_image(&self, url: &str) -> Result; + fn fetch_image(&self, url: &str) -> Result; } -pub fn make_image_fetcher() -> Result, VegaSceneGraphError> { +pub fn make_image_fetcher() -> Result, AvengerVegaError> { cfg_if::cfg_if! { if #[cfg(feature = "image-request")] { Ok(Box::new(ReqwestImageFetcher::new())) diff --git a/avenger-vega/src/image/reqwest_fetcher.rs b/avenger-vega/src/image/reqwest_fetcher.rs index 83a471c..eb166e2 100644 --- a/avenger-vega/src/image/reqwest_fetcher.rs +++ b/avenger-vega/src/image/reqwest_fetcher.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use crate::image::ImageFetcher; use image::DynamicImage; use reqwest::blocking::Client; @@ -22,7 +22,7 @@ impl ReqwestImageFetcher { } impl ImageFetcher for ReqwestImageFetcher { - fn fetch_image(&self, url: &str) -> Result { + fn fetch_image(&self, url: &str) -> Result { let img_data = self.client.get(url).send()?.bytes()?.to_vec(); let diffuse_image = image::load_from_memory(img_data.as_slice())?; Ok(diffuse_image) diff --git a/avenger-vega/src/marks/arc.rs b/avenger-vega/src/marks/arc.rs index 0651c5f..5c20d6c 100644 --- a/avenger-vega/src/marks/arc.rs +++ b/avenger-vega/src/marks/arc.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use crate::marks::mark::{VegaMarkContainer, VegaMarkItem}; use crate::marks::values::CssColorOrGradient; use avenger::marks::arc::ArcMark; @@ -29,7 +29,7 @@ pub struct VegaArcItem { impl VegaMarkItem for VegaArcItem {} impl VegaMarkContainer { - pub fn to_scene_graph(&self) -> Result { + pub fn to_scene_graph(&self) -> Result { // Init mark with scalar defaults let mut mark = ArcMark { clip: self.clip, diff --git a/avenger-vega/src/marks/area.rs b/avenger-vega/src/marks/area.rs index 53c509a..5331d11 100644 --- a/avenger-vega/src/marks/area.rs +++ b/avenger-vega/src/marks/area.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use crate::marks::mark::{VegaMarkContainer, VegaMarkItem}; use crate::marks::values::{CssColorOrGradient, StrokeDashSpec}; use avenger::marks::area::{AreaMark, AreaOrientation}; @@ -29,7 +29,7 @@ pub struct VegaAreaItem { impl VegaMarkItem for VegaAreaItem {} impl VegaMarkContainer { - pub fn to_scene_graph(&self) -> Result { + pub fn to_scene_graph(&self) -> Result { // Get shape of first item and use that for all items for now let first = self.items.first(); let stroke_cap = first.and_then(|item| item.stroke_cap).unwrap_or_default(); diff --git a/avenger-vega/src/marks/group.rs b/avenger-vega/src/marks/group.rs index 4583259..7c5d009 100644 --- a/avenger-vega/src/marks/group.rs +++ b/avenger-vega/src/marks/group.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use crate::marks::mark::{VegaMark, VegaMarkItem}; use crate::marks::values::CssColorOrGradient; use avenger::marks::group::{GroupBounds, SceneGroup}; @@ -30,7 +30,7 @@ pub struct VegaGroupItem { impl VegaMarkItem for VegaGroupItem {} impl VegaGroupItem { - pub fn to_scene_graph(&self) -> Result { + pub fn to_scene_graph(&self) -> Result { let mut marks: Vec = Vec::new(); for item in &self.items { let item_marks: Vec<_> = match item { @@ -38,7 +38,7 @@ impl VegaGroupItem { .items .iter() .map(|item| Ok(SceneMark::Group(item.to_scene_graph()?))) - .collect::, VegaSceneGraphError>>()?, + .collect::, AvengerVegaError>>()?, VegaMark::Rect(mark) => { vec![mark.to_scene_graph()?] } diff --git a/avenger-vega/src/marks/image.rs b/avenger-vega/src/marks/image.rs index 135bdc8..ad055c2 100644 --- a/avenger-vega/src/marks/image.rs +++ b/avenger-vega/src/marks/image.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use crate::marks::mark::{VegaMarkContainer, VegaMarkItem}; use crate::image::make_image_fetcher; @@ -35,7 +35,7 @@ fn default_true() -> bool { impl VegaMarkItem for VegaImageItem {} impl VegaMarkContainer { - pub fn to_scene_graph(&self) -> Result { + pub fn to_scene_graph(&self) -> Result { let name = self .name .clone() diff --git a/avenger-vega/src/marks/line.rs b/avenger-vega/src/marks/line.rs index c0994ea..802689c 100644 --- a/avenger-vega/src/marks/line.rs +++ b/avenger-vega/src/marks/line.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use crate::marks::mark::{VegaMarkContainer, VegaMarkItem}; use crate::marks::values::{CssColorOrGradient, StrokeDashSpec}; use avenger::marks::line::LineMark; @@ -26,7 +26,7 @@ pub struct VegaLineItem { impl VegaMarkItem for VegaLineItem {} impl VegaMarkContainer { - pub fn to_scene_graph(&self) -> Result { + pub fn to_scene_graph(&self) -> Result { // Get shape of first item and use that for all items for now let first = self.items.first(); let stroke_width = first.and_then(|item| item.stroke_width).unwrap_or(1.0); diff --git a/avenger-vega/src/marks/path.rs b/avenger-vega/src/marks/path.rs index 0139b71..611912c 100644 --- a/avenger-vega/src/marks/path.rs +++ b/avenger-vega/src/marks/path.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use crate::marks::mark::{VegaMarkContainer, VegaMarkItem}; use crate::marks::symbol::parse_svg_path; use crate::marks::values::CssColorOrGradient; @@ -33,7 +33,7 @@ pub struct VegaPathItem { impl VegaMarkItem for VegaPathItem {} impl VegaMarkContainer { - pub fn to_scene_graph(&self) -> Result { + pub fn to_scene_graph(&self) -> Result { // Get shape of first item and use that for all items for now let first = self.items.first(); let first_has_stroke = first.map(|item| item.stroke.is_some()).unwrap_or(false); @@ -139,7 +139,7 @@ impl VegaMarkContainer { let paths = path_str .iter() .map(|p| parse_svg_path(p)) - .collect::, VegaSceneGraphError>>()?; + .collect::, AvengerVegaError>>()?; mark.path = EncodingValue::Array { values: paths }; } diff --git a/avenger-vega/src/marks/rect.rs b/avenger-vega/src/marks/rect.rs index a8dd8e7..a129e94 100644 --- a/avenger-vega/src/marks/rect.rs +++ b/avenger-vega/src/marks/rect.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use crate::marks::mark::{VegaMarkContainer, VegaMarkItem}; use crate::marks::values::CssColorOrGradient; use avenger::marks::mark::SceneMark; @@ -28,7 +28,7 @@ pub struct VegaRectItem { impl VegaMarkItem for VegaRectItem {} impl VegaMarkContainer { - pub fn to_scene_graph(&self) -> Result { + pub fn to_scene_graph(&self) -> Result { let mut mark = RectMark { clip: self.clip, ..Default::default() diff --git a/avenger-vega/src/marks/rule.rs b/avenger-vega/src/marks/rule.rs index 057fe4a..e61bff0 100644 --- a/avenger-vega/src/marks/rule.rs +++ b/avenger-vega/src/marks/rule.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use crate::marks::mark::{VegaMarkContainer, VegaMarkItem}; use crate::marks::values::{CssColorOrGradient, StrokeDashSpec}; use avenger::marks::mark::SceneMark; @@ -25,7 +25,7 @@ pub struct VegaRuleItem { impl VegaMarkItem for VegaRuleItem {} impl VegaMarkContainer { - pub fn to_scene_graph(&self) -> Result { + pub fn to_scene_graph(&self) -> Result { // Init mark with scalar defaults let mut mark = RuleMark { clip: self.clip, diff --git a/avenger-vega/src/marks/shape.rs b/avenger-vega/src/marks/shape.rs index 985c8ab..a45e0d7 100644 --- a/avenger-vega/src/marks/shape.rs +++ b/avenger-vega/src/marks/shape.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use crate::marks::mark::{VegaMarkContainer, VegaMarkItem}; use crate::marks::symbol::parse_svg_path; use crate::marks::values::CssColorOrGradient; @@ -28,7 +28,7 @@ pub struct VegaShapeItem { impl VegaMarkItem for VegaShapeItem {} impl VegaMarkContainer { - pub fn to_scene_graph(&self) -> Result { + pub fn to_scene_graph(&self) -> Result { // Get shape of first item and use that for all items for now let first = self.items.first(); let first_has_stroke = first.map(|item| item.stroke.is_some()).unwrap_or(false); @@ -123,7 +123,7 @@ impl VegaMarkContainer { let paths = path_str .iter() .map(|p| parse_svg_path(p)) - .collect::, VegaSceneGraphError>>()?; + .collect::, AvengerVegaError>>()?; mark.path = EncodingValue::Array { values: paths }; } diff --git a/avenger-vega/src/marks/symbol.rs b/avenger-vega/src/marks/symbol.rs index a33db10..7bc628e 100644 --- a/avenger-vega/src/marks/symbol.rs +++ b/avenger-vega/src/marks/symbol.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use crate::marks::mark::{VegaMarkContainer, VegaMarkItem}; use crate::marks::values::{CssColorOrGradient, StrokeDashSpec}; use avenger::marks::group::{GroupBounds, SceneGroup}; @@ -37,7 +37,7 @@ pub struct VegaSymbolItem { impl VegaMarkItem for VegaSymbolItem {} impl VegaMarkContainer { - pub fn to_scene_graph(&self) -> Result { + pub fn to_scene_graph(&self) -> Result { // Get shape of first item and use that for all items for now let first = self.items.first(); let first_shape = first @@ -74,7 +74,7 @@ impl VegaMarkContainer { stroke_dash: item .stroke_dash .clone() - .map(|d| Ok::, VegaSceneGraphError>(d.to_array()?.to_vec())) + .map(|d| Ok::, AvengerVegaError>(d.to_array()?.to_vec())) .transpose()?, gradients, ..Default::default() @@ -204,7 +204,7 @@ impl VegaMarkContainer { mark.shapes = shape_strings .iter() .map(|s| shape_to_path(s)) - .collect::, VegaSceneGraphError>>()?; + .collect::, AvengerVegaError>>()?; } // Add gradients @@ -214,7 +214,7 @@ impl VegaMarkContainer { } } -pub fn shape_to_path(shape: &str) -> Result { +pub fn shape_to_path(shape: &str) -> Result { let tan30: f32 = (30.0 * std::f32::consts::PI / 180.0).tan(); let sqrt3: f32 = 3.0f32.sqrt(); @@ -353,7 +353,7 @@ pub fn shape_to_path(shape: &str) -> Result { }) } -pub fn parse_svg_path(path: &str) -> Result { +pub fn parse_svg_path(path: &str) -> Result { let mut source = Source::new(path.chars()); let mut parser = lyon_extra::parser::PathParser::new(); let opts = ParserOptions::DEFAULT; diff --git a/avenger-vega/src/marks/text.rs b/avenger-vega/src/marks/text.rs index 7866d69..5077180 100644 --- a/avenger-vega/src/marks/text.rs +++ b/avenger-vega/src/marks/text.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use crate::marks::mark::{VegaMarkContainer, VegaMarkItem}; use avenger::marks::mark::SceneMark; use avenger::marks::text::{ @@ -36,7 +36,7 @@ pub struct VegaTextItem { impl VegaMarkItem for VegaTextItem {} impl VegaMarkContainer { - pub fn to_scene_graph(&self) -> Result { + pub fn to_scene_graph(&self) -> Result { // Init mark with scalar defaults let mut mark = TextMark { clip: self.clip, diff --git a/avenger-vega/src/marks/trail.rs b/avenger-vega/src/marks/trail.rs index e476224..9ab67f7 100644 --- a/avenger-vega/src/marks/trail.rs +++ b/avenger-vega/src/marks/trail.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use crate::marks::mark::{VegaMarkContainer, VegaMarkItem}; use crate::marks::values::CssColorOrGradient; use avenger::marks::mark::SceneMark; @@ -21,7 +21,7 @@ pub struct VegaTrailItem { impl VegaMarkItem for VegaTrailItem {} impl VegaMarkContainer { - pub fn to_scene_graph(&self) -> Result { + pub fn to_scene_graph(&self) -> Result { // Get shape of first item and use that for all items for now let first = self.items.first(); let mut gradients = Vec::::new(); diff --git a/avenger-vega/src/marks/values.rs b/avenger-vega/src/marks/values.rs index e539adb..9d4b7b8 100644 --- a/avenger-vega/src/marks/values.rs +++ b/avenger-vega/src/marks/values.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use avenger::marks::value::{ ColorOrGradient, Gradient, GradientStop, LinearGradient, RadialGradient, }; @@ -13,7 +13,7 @@ pub enum StrokeDashSpec { } impl StrokeDashSpec { - pub fn to_array(&self) -> Result>, VegaSceneGraphError> { + pub fn to_array(&self) -> Result>, AvengerVegaError> { match self { StrokeDashSpec::Array(a) => Ok(Cow::Borrowed(a)), StrokeDashSpec::String(s) => { @@ -22,7 +22,7 @@ impl StrokeDashSpec { for s in clean_dash_str.split_whitespace() { let d = s .parse::() - .map_err(|_| VegaSceneGraphError::InvalidDashString(s.to_string()))? + .map_err(|_| AvengerVegaError::InvalidDashString(s.to_string()))? .abs(); dashes.push(d); } @@ -44,7 +44,7 @@ impl CssColorOrGradient { &self, opacity: f32, gradients: &mut Vec, - ) -> Result { + ) -> Result { match self { CssColorOrGradient::Color(c) => { let c = csscolorparser::parse(c)?; @@ -67,7 +67,7 @@ impl CssColorOrGradient { .stops .iter() .map(|s| s.to_grad_stop(opacity)) - .collect::, VegaSceneGraphError>>()?, + .collect::, AvengerVegaError>>()?, }), VegaGradientType::Radial => Gradient::RadialGradient(RadialGradient { x0: grad.x1.unwrap_or(0.5), @@ -80,7 +80,7 @@ impl CssColorOrGradient { .stops .iter() .map(|s| s.to_grad_stop(opacity)) - .collect::, VegaSceneGraphError>>()?, + .collect::, AvengerVegaError>>()?, }), }; @@ -127,7 +127,7 @@ pub struct CssGradientStop { } impl CssGradientStop { - pub fn to_grad_stop(&self, opacity: f32) -> Result { + pub fn to_grad_stop(&self, opacity: f32) -> Result { let c = csscolorparser::parse(&self.color)?; Ok(GradientStop { offset: self.offset, diff --git a/avenger-vega/src/scene_graph.rs b/avenger-vega/src/scene_graph.rs index fea6bc2..215712c 100644 --- a/avenger-vega/src/scene_graph.rs +++ b/avenger-vega/src/scene_graph.rs @@ -1,4 +1,4 @@ -use crate::error::VegaSceneGraphError; +use crate::error::AvengerVegaError; use crate::marks::group::VegaGroupItem; use crate::marks::mark::VegaMarkContainer; use avenger::scene_graph::SceneGraph; @@ -13,13 +13,13 @@ pub struct VegaSceneGraph { } impl VegaSceneGraph { - pub fn to_scene_graph(&self) -> Result { + pub fn to_scene_graph(&self) -> Result { let groups = self .scenegraph .items .iter() .map(|group| group.to_scene_graph()) - .collect::, VegaSceneGraphError>>()?; + .collect::, AvengerVegaError>>()?; Ok(SceneGraph { groups, diff --git a/avenger-wgpu/Cargo.toml b/avenger-wgpu/Cargo.toml index 74274cd..4bf1467 100644 --- a/avenger-wgpu/Cargo.toml +++ b/avenger-wgpu/Cargo.toml @@ -28,6 +28,7 @@ futures-intrusive = "^0.5" etagere = "0.2.10" colorgrad = "0.6.2" lazy_static = { workspace=true } +pyo3 = { workspace = true, optional = true } # glyphon branch that includes text rotation support: https://github.com/jonmmease/glyphon/pull/1 glyphon = { git = "https://github.com/jonmmease/glyphon.git", rev="c468f5dacd4130b27a29b098c4de3f4d5c146209", optional = true } diff --git a/avenger-wgpu/README.md b/avenger-wgpu/README.md index 93b73e8..419fab0 100644 --- a/avenger-wgpu/README.md +++ b/avenger-wgpu/README.md @@ -1,3 +1,3 @@ -## sg2d-wgpu +## avenger-wgpu -This crate supports rendering sg2d SceneGraphs using wgpu +This crate supports rendering Avenger SceneGraphs using wgpu diff --git a/avenger-wgpu/src/canvas.rs b/avenger-wgpu/src/canvas.rs index 0aba5ab..6b4193d 100644 --- a/avenger-wgpu/src/canvas.rs +++ b/avenger-wgpu/src/canvas.rs @@ -4,15 +4,14 @@ use wgpu::{ CommandEncoderDescriptor, Device, DeviceDescriptor, Extent3d, ImageCopyBuffer, ImageCopyTexture, ImageDataLayout, LoadOp, MapMode, Operations, Origin3d, PowerPreference, Queue, RenderPassColorAttachment, RenderPassDescriptor, RequestAdapterOptions, StoreOp, - Surface, SurfaceConfiguration, SurfaceError, Texture, TextureAspect, TextureDescriptor, - TextureDimension, TextureFormat, TextureFormatFeatureFlags, TextureUsages, TextureView, - TextureViewDescriptor, + Surface, SurfaceConfiguration, Texture, TextureAspect, TextureDescriptor, TextureDimension, + TextureFormat, TextureFormatFeatureFlags, TextureUsages, TextureView, TextureViewDescriptor, }; use winit::dpi::Size; use winit::event::WindowEvent; use winit::window::Window; -use crate::error::Sg2dWgpuError; +use crate::error::AvengerWgpuError; use crate::marks::arc::ArcShader; use crate::marks::basic_mark::BasicMarkRenderer; use crate::marks::image::ImageShader; @@ -81,7 +80,7 @@ pub trait Canvas { &mut self, mark: &ArcMark, group_bounds: GroupBounds, - ) -> Result<(), Sg2dWgpuError> { + ) -> Result<(), AvengerWgpuError> { self.add_mark_renderer(MarkRenderer::Instanced(InstancedMarkRenderer::new( self.device(), self.texture_format(), @@ -99,7 +98,7 @@ pub trait Canvas { &mut self, mark: &PathMark, group_bounds: GroupBounds, - ) -> Result<(), Sg2dWgpuError> { + ) -> Result<(), AvengerWgpuError> { self.add_mark_renderer(MarkRenderer::Basic(BasicMarkRenderer::new( self.device(), self.texture_format(), @@ -117,7 +116,7 @@ pub trait Canvas { &mut self, mark: &LineMark, group_bounds: GroupBounds, - ) -> Result<(), Sg2dWgpuError> { + ) -> Result<(), AvengerWgpuError> { self.add_mark_renderer(MarkRenderer::Basic(BasicMarkRenderer::new( self.device(), self.texture_format(), @@ -135,7 +134,7 @@ pub trait Canvas { &mut self, mark: &TrailMark, group_bounds: GroupBounds, - ) -> Result<(), Sg2dWgpuError> { + ) -> Result<(), AvengerWgpuError> { self.add_mark_renderer(MarkRenderer::Basic(BasicMarkRenderer::new( self.device(), self.texture_format(), @@ -153,7 +152,7 @@ pub trait Canvas { &mut self, mark: &AreaMark, group_bounds: GroupBounds, - ) -> Result<(), Sg2dWgpuError> { + ) -> Result<(), AvengerWgpuError> { self.add_mark_renderer(MarkRenderer::Basic(BasicMarkRenderer::new( self.device(), self.texture_format(), @@ -171,7 +170,7 @@ pub trait Canvas { &mut self, mark: &SymbolMark, group_bounds: GroupBounds, - ) -> Result<(), Sg2dWgpuError> { + ) -> Result<(), AvengerWgpuError> { self.add_mark_renderer(MarkRenderer::Instanced(InstancedMarkRenderer::new( self.device(), self.texture_format(), @@ -189,7 +188,7 @@ pub trait Canvas { &mut self, mark: &RectMark, group_bounds: GroupBounds, - ) -> Result<(), Sg2dWgpuError> { + ) -> Result<(), AvengerWgpuError> { self.add_mark_renderer(MarkRenderer::Instanced(InstancedMarkRenderer::new( self.device(), self.texture_format(), @@ -207,7 +206,7 @@ pub trait Canvas { &mut self, mark: &RuleMark, group_bounds: GroupBounds, - ) -> Result<(), Sg2dWgpuError> { + ) -> Result<(), AvengerWgpuError> { self.add_mark_renderer(MarkRenderer::Instanced(InstancedMarkRenderer::new( self.device(), self.texture_format(), @@ -225,7 +224,7 @@ pub trait Canvas { &mut self, mark: &TextMark, group_bounds: GroupBounds, - ) -> Result<(), Sg2dWgpuError> { + ) -> Result<(), AvengerWgpuError> { cfg_if::cfg_if! { if #[cfg(feature = "text-glyphon")] { self.add_mark_renderer(MarkRenderer::Text(TextMarkRenderer::new( @@ -239,7 +238,7 @@ pub trait Canvas { ))); Ok(()) } else { - Err(Sg2dWgpuError::TextNotEnabled("Use the text-glyphon feature flag to enable text".to_string())) + Err(AvengerWgpuError::TextNotEnabled("Use the text-glyphon feature flag to enable text".to_string())) } } } @@ -248,7 +247,7 @@ pub trait Canvas { &mut self, mark: &ImageMark, group_bounds: GroupBounds, - ) -> Result<(), Sg2dWgpuError> { + ) -> Result<(), AvengerWgpuError> { self.add_mark_renderer(MarkRenderer::Basic(BasicMarkRenderer::new( self.device(), self.texture_format(), @@ -266,7 +265,7 @@ pub trait Canvas { &mut self, group: &SceneGroup, group_bounds: GroupBounds, - ) -> Result<(), Sg2dWgpuError> { + ) -> Result<(), AvengerWgpuError> { // Maybe add rect around group boundary if let Some(rect) = group.make_rect() { self.add_rect_mark(&rect, group_bounds)?; @@ -318,7 +317,7 @@ pub trait Canvas { Ok(()) } - fn set_scene(&mut self, scene_graph: &SceneGraph) -> Result<(), Sg2dWgpuError> { + fn set_scene(&mut self, scene_graph: &SceneGraph) -> Result<(), AvengerWgpuError> { // Clear existing marks self.clear_mark_renderer(); @@ -384,7 +383,7 @@ fn make_wgpu_instance() -> wgpu::Instance { async fn make_wgpu_adapter( instance: &wgpu::Instance, compatible_surface: Option<&Surface>, -) -> Result { +) -> Result { instance .request_adapter(&RequestAdapterOptions { power_preference: PowerPreference::default(), @@ -392,10 +391,10 @@ async fn make_wgpu_adapter( force_fallback_adapter: false, }) .await - .ok_or(Sg2dWgpuError::MakeWgpuAdapterError) + .ok_or(AvengerWgpuError::MakeWgpuAdapterError) } -async fn request_wgpu_device(adapter: &Adapter) -> Result<(Device, Queue), Sg2dWgpuError> { +async fn request_wgpu_device(adapter: &Adapter) -> Result<(Device, Queue), AvengerWgpuError> { Ok(adapter .request_device( &DeviceDescriptor { @@ -466,7 +465,10 @@ pub struct WindowCanvas { } impl WindowCanvas { - pub async fn new(window: Window, dimensions: CanvasDimensions) -> Result { + pub async fn new( + window: Window, + dimensions: CanvasDimensions, + ) -> Result { window.set_inner_size(Size::Physical(dimensions.to_physical_size())); let instance = make_wgpu_instance(); let surface = unsafe { instance.create_surface(&window) }?; @@ -541,7 +543,7 @@ impl WindowCanvas { pub fn update(&mut self) {} - pub fn render(&mut self) -> Result<(), SurfaceError> { + pub fn render(&mut self) -> Result<(), AvengerWgpuError> { let output = self.surface.get_current_texture()?; let view = output .texture @@ -641,7 +643,7 @@ pub struct PngCanvas { } impl PngCanvas { - pub async fn new(dimensions: CanvasDimensions) -> Result { + pub async fn new(dimensions: CanvasDimensions) -> Result { let instance = make_wgpu_instance(); let adapter = make_wgpu_adapter(&instance, None).await?; let (device, queue) = request_wgpu_device(&adapter).await?; @@ -710,7 +712,7 @@ impl PngCanvas { }) } - pub async fn render(&mut self) -> Result { + pub async fn render(&mut self) -> Result { // Build encoder for chart background let background_command = if self.sample_count > 1 { make_background_command( diff --git a/avenger-wgpu/src/error.rs b/avenger-wgpu/src/error.rs index e7e15eb..afc3cce 100644 --- a/avenger-wgpu/src/error.rs +++ b/avenger-wgpu/src/error.rs @@ -1,10 +1,13 @@ use lyon::tessellation::TessellationError; use thiserror::Error; +#[cfg(feature = "pyo3")] +use pyo3::{exceptions::PyValueError, PyErr}; + #[derive(Error, Debug)] -pub enum Sg2dWgpuError { - #[error("SceneGraph error")] - SceneGraphError(#[from] avenger::error::SceneGraphError), +pub enum AvengerWgpuError { + #[error("Avenger error")] + AvengerError(#[from] avenger::error::AvengerError), #[error("Device request failed")] RequestDeviceError(#[from] wgpu::RequestDeviceError), @@ -12,6 +15,9 @@ pub enum Sg2dWgpuError { #[error("Failed to create surface")] CreateSurfaceError(#[from] wgpu::CreateSurfaceError), + #[error("Failed to create surface")] + SurfaceError(#[from] wgpu::SurfaceError), + #[error("WGPU adapter creation failed")] MakeWgpuAdapterError, @@ -24,3 +30,11 @@ pub enum Sg2dWgpuError { #[error("Text support is not enabled: {0}")] TextNotEnabled(String), } + +// Conversion to PyO3 error +#[cfg(feature = "pyo3")] +impl From for PyErr { + fn from(err: AvengerWgpuError) -> PyErr { + PyValueError::new_err(err.to_string()) + } +} diff --git a/avenger-wgpu/src/marks/image.rs b/avenger-wgpu/src/marks/image.rs index 11d0a00..ea7c837 100644 --- a/avenger-wgpu/src/marks/image.rs +++ b/avenger-wgpu/src/marks/image.rs @@ -1,5 +1,5 @@ use crate::canvas::CanvasDimensions; -use crate::error::Sg2dWgpuError; +use crate::error::AvengerWgpuError; use crate::marks::basic_mark::{BasicMarkBatch, BasicMarkShader}; use avenger::marks::group::GroupBounds; use avenger::marks::image::ImageMark; @@ -76,7 +76,7 @@ impl ImageShader { mark: &ImageMark, dimensions: CanvasDimensions, group_bounds: GroupBounds, - ) -> Result { + ) -> Result { let mut verts: Vec = Vec::new(); let mut indices: Vec = Vec::new(); let mut batches: Vec = Vec::new(); @@ -133,12 +133,12 @@ impl ImageShader { atlas_allocator.allocate(Size::new(img.width as i32, img.height as i32)) else { if img.width > texture_size.width || img.height > texture_size.height { - return Err(Sg2dWgpuError::ImageAllocationError(format!( + return Err(AvengerWgpuError::ImageAllocationError(format!( "Image dimensions ({}, {}) exceed the maximum size of ({}, {})", img.width, img.height, texture_size.width, texture_size.height ))); } else { - return Err(Sg2dWgpuError::ImageAllocationError( + return Err(AvengerWgpuError::ImageAllocationError( "Unknown error".to_string(), )); } diff --git a/avenger-wgpu/src/marks/path.rs b/avenger-wgpu/src/marks/path.rs index 9b6f115..d27846c 100644 --- a/avenger-wgpu/src/marks/path.rs +++ b/avenger-wgpu/src/marks/path.rs @@ -1,5 +1,5 @@ use crate::canvas::CanvasDimensions; -use crate::error::Sg2dWgpuError; +use crate::error::AvengerWgpuError; use crate::marks::basic_mark::{BasicMarkBatch, BasicMarkShader}; use crate::marks::gradient::{build_gradients_image, to_color_or_gradient_coord}; use avenger::marks::area::{AreaMark, AreaOrientation}; @@ -91,7 +91,7 @@ impl PathShader { mark: &PathMark, dimensions: CanvasDimensions, group_bounds: GroupBounds, - ) -> Result { + ) -> Result { let (gradients_image, texture_size) = build_gradients_image(&mark.gradients); let mut verts: Vec = Vec::new(); @@ -174,7 +174,7 @@ impl PathShader { mark: &AreaMark, dimensions: CanvasDimensions, group_bounds: GroupBounds, - ) -> Result { + ) -> Result { // Handle gradients: let (gradients_image, texture_size) = build_gradients_image(&mark.gradients); @@ -299,7 +299,7 @@ impl PathShader { mark: &LineMark, dimensions: CanvasDimensions, group_bounds: GroupBounds, - ) -> Result { + ) -> Result { let (gradients_image, texture_size) = build_gradients_image(&mark.gradients); let mut defined_paths: Vec = Vec::new(); @@ -443,7 +443,7 @@ impl PathShader { mark: &TrailMark, dimensions: CanvasDimensions, group_bounds: GroupBounds, - ) -> Result { + ) -> Result { let (gradients_image, texture_size) = build_gradients_image(&mark.gradients); let size_idx: AttributeIndex = 0; diff --git a/avenger-wgpu/src/marks/symbol.rs b/avenger-wgpu/src/marks/symbol.rs index 886cef6..9ef5209 100644 --- a/avenger-wgpu/src/marks/symbol.rs +++ b/avenger-wgpu/src/marks/symbol.rs @@ -1,5 +1,5 @@ use crate::canvas::CanvasDimensions; -use crate::error::Sg2dWgpuError; +use crate::error::AvengerWgpuError; use crate::marks::gradient::{build_gradients_image, to_color_or_gradient_coord}; use crate::marks::instanced_mark::{InstancedMarkBatch, InstancedMarkShader}; use avenger::marks::group::GroupBounds; @@ -144,7 +144,7 @@ impl SymbolShader { mark: &SymbolMark, dimensions: CanvasDimensions, group_bounds: GroupBounds, - ) -> Result { + ) -> Result { let shapes = &mark.shapes; let has_stroke = mark.stroke_width.is_some(); let mut verts: Vec = Vec::new(); diff --git a/avenger/Cargo.toml b/avenger/Cargo.toml index 91e84f2..7ed16f3 100644 --- a/avenger/Cargo.toml +++ b/avenger/Cargo.toml @@ -11,3 +11,4 @@ serde = { workspace = true } lyon_path = { workspace = true, features = ["serialization"] } image = { workspace = true, features = ["png"] } reqwest = { version="0.11.23" , features = ["blocking"]} +pyo3 = { workspace = true, optional = true } diff --git a/avenger/README.md b/avenger/README.md index 49a833a..75aadd1 100644 --- a/avenger/README.md +++ b/avenger/README.md @@ -1,4 +1,4 @@ -## sg2d +## avenger This crates holds a serializable scene graph representation that is independent of Vega and independent of rendering diff --git a/avenger/src/error.rs b/avenger/src/error.rs index 62b3a1e..77f929c 100644 --- a/avenger/src/error.rs +++ b/avenger/src/error.rs @@ -1,7 +1,18 @@ use thiserror::Error; +#[cfg(feature = "pyo3")] +use pyo3::{exceptions::PyValueError, PyErr}; + #[derive(Error, Debug)] -pub enum SceneGraphError { +pub enum AvengerError { #[error("Internal error: `{0}`")] InternalError(String), } + +// Conversion to PyO3 error +#[cfg(feature = "pyo3")] +impl From for PyErr { + fn from(err: AvengerError) -> PyErr { + PyValueError::new_err(err.to_string()) + } +} diff --git a/pixi.lock b/pixi.lock index dd2e020..57adbff 100644 --- a/pixi.lock +++ b/pixi.lock @@ -234,6 +234,286 @@ package: timestamp: 1704011393776 purls: - pkg:pypi/attrs +- platform: osx-arm64 + name: aws-c-auth + version: 0.7.11 + category: main + manager: conda + dependencies: + - aws-c-cal >=0.6.9,<0.6.10.0a0 + - aws-c-common >=0.9.12,<0.9.13.0a0 + - aws-c-http >=0.8.0,<0.8.1.0a0 + - aws-c-io >=0.14.0,<0.14.1.0a0 + - aws-c-sdkutils >=0.1.13,<0.1.14.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.7.11-ha4ce7b8_1.conda + hash: + md5: ed467f71fac4eca9454ca1ff99be7f84 + sha256: 4b4318d4ad5cb9d3f3e141e43528e3c7f161e8f167195ff2627e6ec778bd890f + build: ha4ce7b8_1 + arch: aarch64 + subdir: osx-arm64 + build_number: 1 + license: Apache-2.0 + license_family: Apache + size: 89277 + timestamp: 1704837158697 +- platform: osx-arm64 + name: aws-c-cal + version: 0.6.9 + category: main + manager: conda + dependencies: + - aws-c-common >=0.9.12,<0.9.13.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.6.9-h4fd42c2_3.conda + hash: + md5: c06a837ae2f0c217141c32cb408c8b92 + sha256: dde08312c4db4e2e646e37bf5e3dc96affa0dfa87a3044821f545635cad2b440 + build: h4fd42c2_3 + arch: aarch64 + subdir: osx-arm64 + build_number: 3 + license: Apache-2.0 + license_family: Apache + size: 39811 + timestamp: 1704317171219 +- platform: osx-arm64 + name: aws-c-common + version: 0.9.12 + category: main + manager: conda + dependencies: [] + url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.9.12-h93a5062_0.conda + hash: + md5: afe8c81d8e34a96a124640788296b02e + sha256: 75d963943aefae31ab1a02956a5ee41c0fa347da9550bd1ce57b5cbbea7ea7e6 + build: h93a5062_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: Apache-2.0 + license_family: Apache + size: 203635 + timestamp: 1703907168442 +- platform: osx-arm64 + name: aws-c-compression + version: 0.2.17 + category: main + manager: conda + dependencies: + - aws-c-common >=0.9.12,<0.9.13.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.2.17-h4fd42c2_8.conda + hash: + md5: c9b738b496c34db0d27b42491eb16c23 + sha256: 0500a040f6d2838af312c26fbea6ed2a9cac54d8a74347a9c1964af8f57ff033 + build: h4fd42c2_8 + arch: aarch64 + subdir: osx-arm64 + build_number: 8 + license: Apache-2.0 + license_family: Apache + size: 18250 + timestamp: 1704317081353 +- platform: osx-arm64 + name: aws-c-event-stream + version: 0.4.1 + category: main + manager: conda + dependencies: + - aws-c-common >=0.9.12,<0.9.13.0a0 + - aws-c-io >=0.14.0,<0.14.1.0a0 + - aws-checksums >=0.1.17,<0.1.18.0a0 + - libcxx >=15 + url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.4.1-he66824e_2.conda + hash: + md5: 64e84b88c3e9ff59fbd63816877a23d5 + sha256: 7ba075401a7963fd50d9f364053806c4a86e4f51cd8d2f063be875a78306e689 + build: he66824e_2 + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + license: Apache-2.0 + license_family: Apache + size: 47412 + timestamp: 1704832224840 +- platform: osx-arm64 + name: aws-c-http + version: 0.8.0 + category: main + manager: conda + dependencies: + - aws-c-cal >=0.6.9,<0.6.10.0a0 + - aws-c-common >=0.9.12,<0.9.13.0a0 + - aws-c-compression >=0.2.17,<0.2.18.0a0 + - aws-c-io >=0.14.0,<0.14.1.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.8.0-hd3d28cd_2.conda + hash: + md5: bf12b06426420df2055eaa104889bc07 + sha256: 8e80c37e5f2cc84f92634c9c60a4eaa062c2b57dcc1001c5faf711b77318abb8 + build: hd3d28cd_2 + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + license: Apache-2.0 + license_family: Apache + size: 151638 + timestamp: 1704832516933 +- platform: osx-arm64 + name: aws-c-io + version: 0.14.0 + category: main + manager: conda + dependencies: + - aws-c-cal >=0.6.9,<0.6.10.0a0 + - aws-c-common >=0.9.12,<0.9.13.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.14.0-h8daa835_1.conda + hash: + md5: c15089f0a5df47a3278232235a6c2d3a + sha256: 026567637cd89f840fdb70550aa2fe5d325ca3cf52b8d66b48e588d3f0cfc2ea + build: h8daa835_1 + arch: aarch64 + subdir: osx-arm64 + build_number: 1 + license: Apache-2.0 + license_family: Apache + size: 136440 + timestamp: 1704324776900 +- platform: osx-arm64 + name: aws-c-mqtt + version: 0.10.1 + category: main + manager: conda + dependencies: + - aws-c-common >=0.9.12,<0.9.13.0a0 + - aws-c-http >=0.8.0,<0.8.1.0a0 + - aws-c-io >=0.14.0,<0.14.1.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.10.1-h59ff425_0.conda + hash: + md5: aef14e17e37ef7ff95c1deb57cee8a23 + sha256: 2e88ba1370be78b0532870bd1a5cffbc464e31b5d64f5763d2517b5c53753af4 + build: h59ff425_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: Apache-2.0 + license_family: Apache + size: 117799 + timestamp: 1704951841008 +- platform: osx-arm64 + name: aws-c-s3 + version: 0.4.9 + category: main + manager: conda + dependencies: + - aws-c-auth >=0.7.11,<0.7.12.0a0 + - aws-c-cal >=0.6.9,<0.6.10.0a0 + - aws-c-common >=0.9.12,<0.9.13.0a0 + - aws-c-http >=0.8.0,<0.8.1.0a0 + - aws-c-io >=0.14.0,<0.14.1.0a0 + - aws-checksums >=0.1.17,<0.1.18.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.4.9-h7f99a2c_0.conda + hash: + md5: 912d57a741e590a1f568345088409393 + sha256: 9ce65a457cc2a02e12badb0110615bbb8498c6a33c8b96d98bec9f9aea520172 + build: h7f99a2c_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: Apache-2.0 + license_family: Apache + size: 90889 + timestamp: 1704951269051 +- platform: osx-arm64 + name: aws-c-sdkutils + version: 0.1.13 + category: main + manager: conda + dependencies: + - aws-c-common >=0.9.12,<0.9.13.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.1.13-h4fd42c2_1.conda + hash: + md5: d45de4f4fd881f65d794f86a4471e370 + sha256: da5567016574499b732dbf276c0840751dafe7efbd61159917ea527c079a8c01 + build: h4fd42c2_1 + arch: aarch64 + subdir: osx-arm64 + build_number: 1 + license: Apache-2.0 + license_family: Apache + size: 46876 + timestamp: 1704305885624 +- platform: osx-arm64 + name: aws-checksums + version: 0.1.17 + category: main + manager: conda + dependencies: + - aws-c-common >=0.9.12,<0.9.13.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.1.17-h4fd42c2_7.conda + hash: + md5: 22e536282755e9e87ff48c652c9eec7b + sha256: 92a00157c3e3f387d0ba171bcbb6516893ea16fc34c34f535dd74ae38fb3db8e + build: h4fd42c2_7 + arch: aarch64 + subdir: osx-arm64 + build_number: 7 + license: Apache-2.0 + license_family: Apache + size: 49004 + timestamp: 1704306282795 +- platform: osx-arm64 + name: aws-crt-cpp + version: 0.26.0 + category: main + manager: conda + dependencies: + - aws-c-auth >=0.7.11,<0.7.12.0a0 + - aws-c-cal >=0.6.9,<0.6.10.0a0 + - aws-c-common >=0.9.12,<0.9.13.0a0 + - aws-c-event-stream >=0.4.1,<0.4.2.0a0 + - aws-c-http >=0.8.0,<0.8.1.0a0 + - aws-c-io >=0.14.0,<0.14.1.0a0 + - aws-c-mqtt >=0.10.1,<0.10.2.0a0 + - aws-c-s3 >=0.4.9,<0.4.10.0a0 + - aws-c-sdkutils >=0.1.13,<0.1.14.0a0 + - libcxx >=15 + url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.26.0-hfff802b_8.conda + hash: + md5: 9f4ebd51ab78bed865f2cea217cc2800 + sha256: a88854f232025c297d3161a43795909d8a00a936cb782780fa2e3fc83ea6d489 + build: hfff802b_8 + arch: aarch64 + subdir: osx-arm64 + build_number: 8 + license: Apache-2.0 + license_family: Apache + size: 217993 + timestamp: 1704982595239 +- platform: osx-arm64 + name: aws-sdk-cpp + version: 1.11.210 + category: main + manager: conda + dependencies: + - aws-c-common >=0.9.12,<0.9.13.0a0 + - aws-c-event-stream >=0.4.1,<0.4.2.0a0 + - aws-checksums >=0.1.17,<0.1.18.0a0 + - aws-crt-cpp >=0.26.0,<0.26.1.0a0 + - libcurl >=8.5.0,<9.0a0 + - libcxx >=15 + - libzlib >=1.2.13,<1.3.0a0 + - openssl >=3.2.0,<4.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.210-he93ac2d_10.conda + hash: + md5: 7b36897c51a1a12db6b3a79a3c6e0a80 + sha256: 861ef77ea13a8ca24f115bf7aea446b38ad491977188350cb74df1423a8b7841 + build: he93ac2d_10 + arch: aarch64 + subdir: osx-arm64 + build_number: 10 + license: Apache-2.0 + license_family: Apache + size: 3278036 + timestamp: 1704954424076 - platform: osx-arm64 name: babel version: 2.14.0 @@ -347,6 +627,24 @@ package: license_family: BSD size: 122325 timestamp: 1699280294368 +- platform: osx-arm64 + name: c-ares + version: 1.26.0 + category: main + manager: conda + dependencies: [] + url: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.26.0-h93a5062_0.conda + hash: + md5: 58b9187431de0a2ffebc907f4590e2e5 + sha256: 1dfdbac985a74a905f2bcf62f13ce758a8356e50d4b28ddbc2027df8580717d8 + build: h93a5062_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: MIT + license_family: MIT + size: 145325 + timestamp: 1706300196534 - platform: osx-arm64 name: ca-certificates version: 2023.11.17 @@ -647,6 +945,63 @@ package: noarch: python size: 14395 timestamp: 1638810388635 +- platform: osx-arm64 + name: gflags + version: 2.2.2 + category: main + manager: conda + dependencies: + - libcxx >=11.0.0.rc1 + url: https://conda.anaconda.org/conda-forge/osx-arm64/gflags-2.2.2-hc88da5d_1004.tar.bz2 + hash: + md5: aab9ddfad863e9ef81229a1f8852211b + sha256: 25d4a20af2e5ace95fdec88970f6d190e77e20074d2f6d3cef766198b76a4289 + build: hc88da5d_1004 + arch: aarch64 + subdir: osx-arm64 + build_number: 1004 + license: BSD-3-Clause + license_family: BSD + size: 86690 + timestamp: 1599590990520 +- platform: osx-arm64 + name: glog + version: 0.6.0 + category: main + manager: conda + dependencies: + - gflags >=2.2.2,<2.3.0a0 + - libcxx >=12.0.1 + url: https://conda.anaconda.org/conda-forge/osx-arm64/glog-0.6.0-h6da1cb0_0.tar.bz2 + hash: + md5: 5a570729c7709399cf8511aeeda6f989 + sha256: 4d772c42477f64be708594ac45870feba3e838977871118eb25e00deb0e9a73c + build: h6da1cb0_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: BSD-3-Clause + license_family: BSD + size: 97658 + timestamp: 1649144191039 +- platform: osx-arm64 + name: icu + version: '73.2' + category: main + manager: conda + dependencies: [] + url: https://conda.anaconda.org/conda-forge/osx-arm64/icu-73.2-hc8870d7_0.conda + hash: + md5: 8521bd47c0e11c5902535bb1a17c565f + sha256: ff9cd0c6cd1349954c801fb443c94192b637e1b414514539f3c49c56a39f51b1 + build: hc8870d7_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: MIT + license_family: MIT + size: 11997841 + timestamp: 1692902104771 - platform: osx-arm64 name: idna version: '3.6' @@ -1248,6 +1603,226 @@ package: noarch: python size: 48860 timestamp: 1700310989409 +- platform: osx-arm64 + name: krb5 + version: 1.21.2 + category: main + manager: conda + dependencies: + - libcxx >=15.0.7 + - libedit >=3.1.20191231,<3.2.0a0 + - libedit >=3.1.20191231,<4.0a0 + - openssl >=3.1.2,<4.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.2-h92f50d5_0.conda + hash: + md5: 92f1cff174a538e0722bf2efb16fc0b2 + sha256: 70bdb9b4589ec7c7d440e485ae22b5a352335ffeb91a771d4c162996c3070875 + build: h92f50d5_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: MIT + license_family: MIT + size: 1195575 + timestamp: 1692098070699 +- platform: osx-arm64 + name: libabseil + version: '20230802.1' + category: main + manager: conda + dependencies: + - libcxx >=15.0.7 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20230802.1-cxx17_h13dd4ca_0.conda + hash: + md5: fb6dfadc1898666616dfda242d8aea10 + sha256: 459a58f36607246b4483d7a370c2d9a03e7f824e79da2c6e3e9d62abf80393e7 + build: cxx17_h13dd4ca_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + constrains: + - libabseil-static =20230802.1=cxx17* + - abseil-cpp =20230802.1 + license: Apache-2.0 + license_family: Apache + size: 1173407 + timestamp: 1695064439482 +- platform: osx-arm64 + name: libarrow + version: 14.0.2 + category: main + manager: conda + dependencies: + - aws-crt-cpp >=0.26.0,<0.26.1.0a0 + - aws-sdk-cpp >=1.11.210,<1.11.211.0a0 + - bzip2 >=1.0.8,<2.0a0 + - glog >=0.6.0,<0.7.0a0 + - libabseil * cxx17* + - libabseil >=20230802.1,<20230803.0a0 + - libbrotlidec >=1.1.0,<1.2.0a0 + - libbrotlienc >=1.1.0,<1.2.0a0 + - libcxx >=14 + - libgoogle-cloud >=2.12.0,<2.13.0a0 + - libre2-11 >=2023.6.2,<2024.0a0 + - libutf8proc >=2.8.0,<3.0a0 + - libzlib >=1.2.13,<1.3.0a0 + - lz4-c >=1.9.3,<1.10.0a0 + - orc >=1.9.2,<1.9.3.0a0 + - re2 + - snappy >=1.1.10,<2.0a0 + - zstd >=1.5.5,<1.6.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-14.0.2-h4ce3932_2_cpu.conda + hash: + md5: d61d4cee3c195a5f574b3ade7a85ef94 + sha256: 6c176a5ece3c72c0c1b7d7be5cc0f0a8dc637e634c936730a6d744e564fb75cb + build: h4ce3932_2_cpu + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + constrains: + - apache-arrow-proc =*=cpu + - arrow-cpp <0.0a0 + - parquet-cpp <0.0a0 + license: Apache-2.0 + license_family: APACHE + size: 14634457 + timestamp: 1704355766916 +- platform: osx-arm64 + name: libarrow-acero + version: 14.0.2 + category: main + manager: conda + dependencies: + - libarrow 14.0.2 h4ce3932_2_cpu + - libcxx >=14 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-14.0.2-h13dd4ca_2_cpu.conda + hash: + md5: b4b1760597af9889cd2f5311b0c34e7f + sha256: 93784ab7aec5fe72a96bb028868037fc95ee0f72a43c5cdcdc98b31b3f6b3ef6 + build: h13dd4ca_2_cpu + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + license: Apache-2.0 + license_family: APACHE + size: 494126 + timestamp: 1704355933680 +- platform: osx-arm64 + name: libarrow-dataset + version: 14.0.2 + category: main + manager: conda + dependencies: + - libarrow 14.0.2 h4ce3932_2_cpu + - libarrow-acero 14.0.2 h13dd4ca_2_cpu + - libcxx >=14 + - libparquet 14.0.2 hf6ce1d5_2_cpu + url: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-14.0.2-h13dd4ca_2_cpu.conda + hash: + md5: bec70ec592be6a282cb06250fa5e71a4 + sha256: 4a72d1476f49b5c234a2ef798ef33c473f8d6307aaceec65341a4f11db5ba23c + build: h13dd4ca_2_cpu + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + license: Apache-2.0 + license_family: APACHE + size: 526384 + timestamp: 1704356487597 +- platform: osx-arm64 + name: libarrow-flight + version: 14.0.2 + category: main + manager: conda + dependencies: + - libabseil * cxx17* + - libabseil >=20230802.1,<20230803.0a0 + - libarrow 14.0.2 h4ce3932_2_cpu + - libcxx >=14 + - libgrpc >=1.59.3,<1.60.0a0 + - libprotobuf >=4.24.4,<4.24.5.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-flight-14.0.2-ha94d253_2_cpu.conda + hash: + md5: 9acf1572e3db073db00e67d21cfb7477 + sha256: 6e18f49f8c6b58958882d9735529ebfec402ce3443e0934b9fd89ac1d7d22c57 + build: ha94d253_2_cpu + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + license: Apache-2.0 + license_family: APACHE + size: 331157 + timestamp: 1704356092185 +- platform: osx-arm64 + name: libarrow-flight-sql + version: 14.0.2 + category: main + manager: conda + dependencies: + - libarrow 14.0.2 h4ce3932_2_cpu + - libarrow-flight 14.0.2 ha94d253_2_cpu + - libcxx >=14 + - libprotobuf >=4.24.4,<4.24.5.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-flight-sql-14.0.2-h39a9b85_2_cpu.conda + hash: + md5: c6e0c75f69f3c31b0a4f02d415e0739d + sha256: 3621589405facf900779210f9f49556290cee0861a9797585eb6af7933017d65 + build: h39a9b85_2_cpu + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + license: Apache-2.0 + license_family: APACHE + size: 161406 + timestamp: 1704356624291 +- platform: osx-arm64 + name: libarrow-gandiva + version: 14.0.2 + category: main + manager: conda + dependencies: + - libarrow 14.0.2 h4ce3932_2_cpu + - libcxx >=14 + - libllvm15 >=15.0.7,<15.1.0a0 + - libre2-11 >=2023.6.2,<2024.0a0 + - libutf8proc >=2.8.0,<3.0a0 + - openssl >=3.2.0,<4.0a0 + - re2 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-gandiva-14.0.2-hf757142_2_cpu.conda + hash: + md5: f4285d86a247a339bf7bf43fda4ded17 + sha256: 237007a5c35c612823762327ae355f396ba34b66b40317e87de41f36a2f839bc + build: hf757142_2_cpu + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + license: Apache-2.0 + license_family: APACHE + size: 689008 + timestamp: 1704356234671 +- platform: osx-arm64 + name: libarrow-substrait + version: 14.0.2 + category: main + manager: conda + dependencies: + - libarrow 14.0.2 h4ce3932_2_cpu + - libarrow-acero 14.0.2 h13dd4ca_2_cpu + - libarrow-dataset 14.0.2 h13dd4ca_2_cpu + - libcxx >=14 + - libprotobuf >=4.24.4,<4.24.5.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-substrait-14.0.2-h7fd9903_2_cpu.conda + hash: + md5: 98b3c3e9288e0d451d9329fa784fe256 + sha256: f7712ad3916fb83171c82ba5031f4356df7cb96cff2a6fa9ef17e44fe9940270 + build: h7fd9903_2_cpu + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + license: Apache-2.0 + license_family: APACHE + size: 474175 + timestamp: 1704356753787 - platform: osx-arm64 name: libblas version: 3.9.0 @@ -1273,6 +1848,62 @@ package: license_family: BSD size: 14915 timestamp: 1705980172730 +- platform: osx-arm64 + name: libbrotlicommon + version: 1.1.0 + category: main + manager: conda + dependencies: [] + url: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.1.0-hb547adb_1.conda + hash: + md5: cd68f024df0304be41d29a9088162b02 + sha256: 556f0fddf4bd4d35febab404d98cb6862ce3b7ca843e393da0451bfc4654cf07 + build: hb547adb_1 + arch: aarch64 + subdir: osx-arm64 + build_number: 1 + license: MIT + license_family: MIT + size: 68579 + timestamp: 1695990426128 +- platform: osx-arm64 + name: libbrotlidec + version: 1.1.0 + category: main + manager: conda + dependencies: + - libbrotlicommon 1.1.0 hb547adb_1 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.1.0-hb547adb_1.conda + hash: + md5: ee1a519335cc10d0ec7e097602058c0a + sha256: c1c85937828ad3bc434ac60b7bcbde376f4d2ea4ee42d15d369bf2a591775b4a + build: hb547adb_1 + arch: aarch64 + subdir: osx-arm64 + build_number: 1 + license: MIT + license_family: MIT + size: 28928 + timestamp: 1695990463780 +- platform: osx-arm64 + name: libbrotlienc + version: 1.1.0 + category: main + manager: conda + dependencies: + - libbrotlicommon 1.1.0 hb547adb_1 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.1.0-hb547adb_1.conda + hash: + md5: d7e077f326a98b2cc60087eaff7c730b + sha256: 690dfc98e891ee1871c54166d30f6e22edfc2d7d6b29e7988dde5f1ce271c81a + build: hb547adb_1 + arch: aarch64 + subdir: osx-arm64 + build_number: 1 + license: MIT + license_family: MIT + size: 280943 + timestamp: 1695990509392 - platform: osx-arm64 name: libcblas version: 3.9.0 @@ -1296,6 +1927,49 @@ package: license_family: BSD size: 14800 timestamp: 1705980195551 +- platform: osx-arm64 + name: libcrc32c + version: 1.1.2 + category: main + manager: conda + dependencies: + - libcxx >=11.1.0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libcrc32c-1.1.2-hbdafb3b_0.tar.bz2 + hash: + md5: 32bd82a6a625ea6ce090a81c3d34edeb + sha256: 58477b67cc719060b5b069ba57161e20ba69b8695d154a719cb4b60caf577929 + build: hbdafb3b_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: BSD-3-Clause + license_family: BSD + size: 18765 + timestamp: 1633683992603 +- platform: osx-arm64 + name: libcurl + version: 8.5.0 + category: main + manager: conda + dependencies: + - krb5 >=1.21.2,<1.22.0a0 + - libnghttp2 >=1.58.0,<2.0a0 + - libssh2 >=1.11.0,<2.0a0 + - libzlib >=1.2.13,<1.3.0a0 + - openssl >=3.2.0,<4.0a0 + - zstd >=1.5.5,<1.6.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.5.0-h2d989ff_0.conda + hash: + md5: f1211ed00947a84e15a964a8f459f620 + sha256: f1c04be217aaf161ce3c99a8d618871295b5dc1eae2f7ff7b32078af50303f5b + build: h2d989ff_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: curl + license_family: MIT + size: 350298 + timestamp: 1701860532373 - platform: osx-arm64 name: libcxx version: 16.0.6 @@ -1314,6 +1988,62 @@ package: license_family: Apache size: 1160232 timestamp: 1686896993785 +- platform: osx-arm64 + name: libedit + version: 3.1.20191231 + category: main + manager: conda + dependencies: + - ncurses >=6.2,<7.0.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20191231-hc8eb9b7_2.tar.bz2 + hash: + md5: 30e4362988a2623e9eb34337b83e01f9 + sha256: 3912636197933ecfe4692634119e8644904b41a58f30cad9d1fc02f6ba4d9fca + build: hc8eb9b7_2 + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + license: BSD-2-Clause + license_family: BSD + size: 96607 + timestamp: 1597616630749 +- platform: osx-arm64 + name: libev + version: '4.33' + category: main + manager: conda + dependencies: [] + url: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + hash: + md5: 36d33e440c31857372a72137f78bacf5 + sha256: 95cecb3902fbe0399c3a7e67a5bed1db813e5ab0e22f4023a5e0f722f2cc214f + build: h93a5062_2 + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + license: BSD-2-Clause + license_family: BSD + size: 107458 + timestamp: 1702146414478 +- platform: osx-arm64 + name: libevent + version: 2.1.12 + category: main + manager: conda + dependencies: + - openssl >=3.1.1,<4.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libevent-2.1.12-h2757513_1.conda + hash: + md5: 1a109764bff3bdc7bdd84088347d71dc + sha256: 8c136d7586259bb5c0d2b913aaadc5b9737787ae4f40e3ad1beaf96c80b919b7 + build: h2757513_1 + arch: aarch64 + subdir: osx-arm64 + build_number: 1 + license: BSD-3-Clause + license_family: BSD + size: 368167 + timestamp: 1685726248899 - platform: osx-arm64 name: libexpat version: 2.5.0 @@ -1358,40 +2088,116 @@ package: category: main manager: conda dependencies: - - libgfortran5 13.2.0 hf226fd6_2 - url: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-5.0.0-13_2_0_hd922786_2.conda + - libgfortran5 13.2.0 hf226fd6_2 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-5.0.0-13_2_0_hd922786_2.conda + hash: + md5: 50c44da4cd89e99a5b18382f565585d8 + sha256: 8af9f94c34150567f2993392c7c1036c99b6844625aea0338535293e4d7b5d23 + build: 13_2_0_hd922786_2 + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 110207 + timestamp: 1705769417313 +- platform: osx-arm64 + name: libgfortran5 + version: 13.2.0 + category: main + manager: conda + dependencies: + - llvm-openmp >=8.0.0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-13.2.0-hf226fd6_2.conda + hash: + md5: 55c6859a3606c1516d89768a05ce9074 + sha256: 0b7e069f0227402deef36d04a2695411b0302ef99fe6bf8a9488e472d2e217c1 + build: hf226fd6_2 + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + constrains: + - libgfortran 5.0.0 13_2_0_*_2 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 997116 + timestamp: 1705769362034 +- platform: osx-arm64 + name: libgoogle-cloud + version: 2.12.0 + category: main + manager: conda + dependencies: + - __osx >=10.9 + - libabseil * cxx17* + - libabseil >=20230802.1,<20230803.0a0 + - libcrc32c >=1.1.2,<1.2.0a0 + - libcurl >=8.4.0,<9.0a0 + - libcxx >=16.0.6 + - libgrpc >=1.59.2,<1.60.0a0 + - libprotobuf >=4.24.4,<4.24.5.0a0 + - openssl >=3.1.4,<4.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-2.12.0-hfb399a7_4.conda + hash: + md5: d62901188ab756c841cbb9a80c6c3f3c + sha256: 22122939a462f64a82ca2f305c43e5e5cf5a55f1ae12979c2445f9dc196b7047 + build: hfb399a7_4 + arch: aarch64 + subdir: osx-arm64 + build_number: 4 + constrains: + - google-cloud-cpp 2.12.0 *_4 + license: Apache-2.0 + license_family: Apache + size: 31440327 + timestamp: 1698982048456 +- platform: osx-arm64 + name: libgrpc + version: 1.59.3 + category: main + manager: conda + dependencies: + - __osx >=10.9 + - c-ares >=1.21.0,<2.0a0 + - libabseil * cxx17* + - libabseil >=20230802.1,<20230803.0a0 + - libcxx >=16.0.6 + - libprotobuf >=4.24.4,<4.24.5.0a0 + - libre2-11 >=2023.6.2,<2024.0a0 + - libzlib >=1.2.13,<1.3.0a0 + - openssl >=3.1.4,<4.0a0 + - re2 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libgrpc-1.59.3-hbcf6334_0.conda hash: - md5: 50c44da4cd89e99a5b18382f565585d8 - sha256: 8af9f94c34150567f2993392c7c1036c99b6844625aea0338535293e4d7b5d23 - build: 13_2_0_hd922786_2 + md5: e9c7cbc84af929dd47501629a5e19713 + sha256: 54cacd1fc7503d48c135301a775568f15089b537b3c56804767c627a89a20c30 + build: hbcf6334_0 arch: aarch64 subdir: osx-arm64 - build_number: 2 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - size: 110207 - timestamp: 1705769417313 + build_number: 0 + constrains: + - grpc-cpp =1.59.3 + license: Apache-2.0 + license_family: APACHE + size: 3950361 + timestamp: 1700260902499 - platform: osx-arm64 - name: libgfortran5 - version: 13.2.0 + name: libiconv + version: '1.17' category: main manager: conda - dependencies: - - llvm-openmp >=8.0.0 - url: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-13.2.0-hf226fd6_2.conda + dependencies: [] + url: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.17-h0d3ecfb_2.conda hash: - md5: 55c6859a3606c1516d89768a05ce9074 - sha256: 0b7e069f0227402deef36d04a2695411b0302ef99fe6bf8a9488e472d2e217c1 - build: hf226fd6_2 + md5: 69bda57310071cf6d2b86caf11573d2d + sha256: bc7de5097b97bcafcf7deaaed505f7ce02f648aac8eccc0d5a47cc599a1d0304 + build: h0d3ecfb_2 arch: aarch64 subdir: osx-arm64 build_number: 2 - constrains: - - libgfortran 5.0.0 13_2_0_*_2 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - size: 997116 - timestamp: 1705769362034 + license: LGPL-2.1-only + size: 676469 + timestamp: 1702682458114 - platform: osx-arm64 name: liblapack version: 3.9.0 @@ -1415,6 +2221,52 @@ package: license_family: BSD size: 14829 timestamp: 1705980215575 +- platform: osx-arm64 + name: libllvm15 + version: 15.0.7 + category: main + manager: conda + dependencies: + - libcxx >=16 + - libxml2 >=2.12.1,<3.0.0a0 + - libzlib >=1.2.13,<1.3.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm15-15.0.7-h2621b3d_4.conda + hash: + md5: 8d7f7a7286d99a2671df2619cb3bfb2c + sha256: 63e22ccd4c1b80dfc7da169c65c62a878a46ef0e5771c3b0c091071e718ae1b1 + build: h2621b3d_4 + arch: aarch64 + subdir: osx-arm64 + build_number: 4 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + size: 22049607 + timestamp: 1701372072765 +- platform: osx-arm64 + name: libnghttp2 + version: 1.58.0 + category: main + manager: conda + dependencies: + - __osx >=10.9 + - c-ares >=1.23.0,<2.0a0 + - libcxx >=16.0.6 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libzlib >=1.2.13,<1.3.0a0 + - openssl >=3.2.0,<4.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.58.0-ha4dd798_1.conda + hash: + md5: 1813e066bfcef82de579a0be8a766df4 + sha256: fc97aaaf0c6d0f508be313d86c2705b490998d382560df24be918b8e977802cd + build: ha4dd798_1 + arch: aarch64 + subdir: osx-arm64 + build_number: 1 + license: MIT + license_family: MIT + size: 565451 + timestamp: 1702130473930 - platform: osx-arm64 name: libopenblas version: 0.3.26 @@ -1438,6 +2290,75 @@ package: license_family: BSD size: 2917606 timestamp: 1704950245195 +- platform: osx-arm64 + name: libparquet + version: 14.0.2 + category: main + manager: conda + dependencies: + - libarrow 14.0.2 h4ce3932_2_cpu + - libcxx >=14 + - libthrift >=0.19.0,<0.19.1.0a0 + - openssl >=3.2.0,<4.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libparquet-14.0.2-hf6ce1d5_2_cpu.conda + hash: + md5: 3ec6dde23ee1ecea255c788e5ce16c82 + sha256: e1deb2b68a0a24c85f75831e44a59d582168b5b9481ab7138683226e702416c2 + build: hf6ce1d5_2_cpu + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + license: Apache-2.0 + license_family: APACHE + size: 911760 + timestamp: 1704356359375 +- platform: osx-arm64 + name: libprotobuf + version: 4.24.4 + category: main + manager: conda + dependencies: + - __osx >=10.9 + - libabseil * cxx17* + - libabseil >=20230802.1,<20230803.0a0 + - libcxx >=16.0.6 + - libzlib >=1.2.13,<1.3.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-4.24.4-hc9861d8_0.conda + hash: + md5: ac5438d981e105e053b341eb30c44273 + sha256: 2e81e023f463ef239e2fb7f56a4e8eed61a1d8e9ca3f2f07bec1668cc369b2ce + build: hc9861d8_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: BSD-3-Clause + license_family: BSD + size: 2060711 + timestamp: 1696556460522 +- platform: osx-arm64 + name: libre2-11 + version: 2023.06.02 + category: main + manager: conda + dependencies: + - __osx >=10.9 + - libabseil * cxx17* + - libabseil >=20230802.1,<20230803.0a0 + - libcxx >=16.0.6 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libre2-11-2023.06.02-h1753957_0.conda + hash: + md5: 3b8652db4bf4e27fa1446526f7a78498 + sha256: 8bafee8f8ef27f4cb0afffe5404dd1abfc5fd6eac1ee9b4847a756d440bd7aa7 + build: h1753957_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + constrains: + - re2 2023.06.02.* + license: BSD-3-Clause + license_family: BSD + size: 169587 + timestamp: 1697065827986 - platform: osx-arm64 name: libsodium version: 1.0.18 @@ -1473,6 +2394,88 @@ package: license: Unlicense size: 815254 timestamp: 1700863572318 +- platform: osx-arm64 + name: libssh2 + version: 1.11.0 + category: main + manager: conda + dependencies: + - libzlib >=1.2.13,<1.3.0a0 + - openssl >=3.1.1,<4.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.0-h7a5bd25_0.conda + hash: + md5: 029f7dc931a3b626b94823bc77830b01 + sha256: bb57d0c53289721fff1eeb3103a1c6a988178e88d8a8f4345b0b91a35f0e0015 + build: h7a5bd25_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: BSD-3-Clause + license_family: BSD + size: 255610 + timestamp: 1685837894256 +- platform: osx-arm64 + name: libthrift + version: 0.19.0 + category: main + manager: conda + dependencies: + - libcxx >=15.0.7 + - libevent >=2.1.12,<2.1.13.0a0 + - libzlib >=1.2.13,<1.3.0a0 + - openssl >=3.1.3,<4.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libthrift-0.19.0-h026a170_1.conda + hash: + md5: 4b8b21eb00d9019e9fa351141da2a6ac + sha256: b2c1b30d36f0412c0c0313db76a0236d736f3a9b887b8ed16182f531e4b7cb80 + build: h026a170_1 + arch: aarch64 + subdir: osx-arm64 + build_number: 1 + license: Apache-2.0 + license_family: APACHE + size: 331154 + timestamp: 1695958512679 +- platform: osx-arm64 + name: libutf8proc + version: 2.8.0 + category: main + manager: conda + dependencies: [] + url: https://conda.anaconda.org/conda-forge/osx-arm64/libutf8proc-2.8.0-h1a8c8d9_0.tar.bz2 + hash: + md5: f8c9c41a122ab3abdf8943b13f4957ee + sha256: a3faddac08efd930fa3a1cc254b5053b4ed9428c49a888d437bf084d403c931a + build: h1a8c8d9_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: MIT + license_family: MIT + size: 103492 + timestamp: 1667316405233 +- platform: osx-arm64 + name: libxml2 + version: 2.12.4 + category: main + manager: conda + dependencies: + - icu >=73.2,<74.0a0 + - libiconv >=1.17,<2.0a0 + - libzlib >=1.2.13,<1.3.0a0 + - xz >=5.2.6,<6.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.12.4-h0d0cfa8_1.conda + hash: + md5: 2ce68362b6ba7e78a066abce22811df7 + sha256: 70863a5554cbdd573cf852571a6ef015e5376f8969068725523a01dff7ff4de3 + build: h0d0cfa8_1 + arch: aarch64 + subdir: osx-arm64 + build_number: 1 + license: MIT + license_family: MIT + size: 588541 + timestamp: 1705355310031 - platform: osx-arm64 name: libzlib version: 1.2.13 @@ -1513,6 +2516,25 @@ package: license_family: APACHE size: 274631 timestamp: 1701222947083 +- platform: osx-arm64 + name: lz4-c + version: 1.9.4 + category: main + manager: conda + dependencies: + - libcxx >=14.0.6 + url: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.9.4-hb7217d7_0.conda + hash: + md5: 45505bec548634f7d05e02fb25262cb9 + sha256: fc343b8c82efe40819b986e29ba748366514e5ab94a1e1138df195af5f45fa24 + build: hb7217d7_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: BSD-2-Clause + license_family: BSD + size: 141188 + timestamp: 1674727268278 - platform: osx-arm64 name: markupsafe version: 2.1.4 @@ -1810,6 +2832,31 @@ package: license_family: Apache size: 2856233 timestamp: 1701162541844 +- platform: osx-arm64 + name: orc + version: 1.9.2 + category: main + manager: conda + dependencies: + - __osx >=10.9 + - libcxx >=16.0.6 + - libprotobuf >=4.24.4,<4.24.5.0a0 + - libzlib >=1.2.13,<1.3.0a0 + - lz4-c >=1.9.3,<1.10.0a0 + - snappy >=1.1.10,<2.0a0 + - zstd >=1.5.5,<1.6.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/orc-1.9.2-h7c018df_0.conda + hash: + md5: 1ef4159e9686d95ce8ea9f1d4d999f29 + sha256: b1ad0f09dc69a8956079371d9853534f991f8311352e4e21503e6e5d20e4017b + build: h7c018df_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: Apache-2.0 + license_family: Apache + size: 405599 + timestamp: 1700373052638 - platform: osx-arm64 name: overrides version: 7.7.0 @@ -2079,6 +3126,33 @@ package: noarch: python size: 270398 timestamp: 1702399557137 +- platform: osx-arm64 + name: protobuf + version: 4.24.4 + category: main + manager: conda + dependencies: + - __osx >=10.9 + - libabseil * cxx17* + - libabseil >=20230802.1,<20230803.0a0 + - libcxx >=16.0.6 + - libprotobuf >=4.24.4,<4.24.5.0a0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + - setuptools + url: https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-4.24.4-py312h71bcfcd_0.conda + hash: + md5: 0fb253835031f65faae1618da4948c7f + sha256: 4af89f189fd2a537faf7d925db2e30c1582948996e0572d4f560ddc25276b484 + build: py312h71bcfcd_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: BSD-3-Clause + license_family: BSD + size: 365633 + timestamp: 1696713120397 - platform: osx-arm64 name: psutil version: 5.9.8 @@ -2145,6 +3219,39 @@ package: timestamp: 1642876055775 purls: - pkg:pypi/pure-eval +- platform: osx-arm64 + name: pyarrow + version: 14.0.2 + category: main + manager: conda + dependencies: + - libarrow 14.0.2 h4ce3932_2_cpu + - libarrow-acero 14.0.2 h13dd4ca_2_cpu + - libarrow-dataset 14.0.2 h13dd4ca_2_cpu + - libarrow-flight 14.0.2 ha94d253_2_cpu + - libarrow-flight-sql 14.0.2 h39a9b85_2_cpu + - libarrow-gandiva 14.0.2 hf757142_2_cpu + - libarrow-substrait 14.0.2 h7fd9903_2_cpu + - libcxx >=14 + - libparquet 14.0.2 hf6ce1d5_2_cpu + - numpy >=1.26.3,<2.0a0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + url: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-14.0.2-py312h14a4a34_2_cpu.conda + hash: + md5: 18b2713e68476ecd0e857f0b170fdd22 + sha256: ed26e7c459ee7199f62aa5b960d7af47a5c3257cf00932f883229424812d30dd + build: py312h14a4a34_2_cpu + arch: aarch64 + subdir: osx-arm64 + build_number: 2 + constrains: + - apache-arrow-proc =*=cpu + license: Apache-2.0 + license_family: APACHE + size: 4055875 + timestamp: 1704360388886 - platform: osx-arm64 name: pycparser version: '2.21' @@ -2467,6 +3574,25 @@ package: timestamp: 1701783687130 purls: - pkg:pypi/pyzmq +- platform: osx-arm64 + name: re2 + version: 2023.06.02 + category: main + manager: conda + dependencies: + - libre2-11 2023.06.02 h1753957_0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/re2-2023.06.02-h6135d0a_0.conda + hash: + md5: 8f23674174b155300696a2be8b5c1407 + sha256: 963847258a82d9647311c5eb8829a49ac2161df12a304d5d6e61f788f0563442 + build: h6135d0a_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: BSD-3-Clause + license_family: BSD + size: 27073 + timestamp: 1697065856329 - platform: osx-arm64 name: readline version: '8.2' @@ -2674,6 +3800,25 @@ package: timestamp: 1620240338595 purls: - pkg:pypi/six +- platform: osx-arm64 + name: snappy + version: 1.1.10 + category: main + manager: conda + dependencies: + - libcxx >=14.0.6 + url: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.1.10-h17c5cce_0.conda + hash: + md5: ac82a611d1a67a598096ebaa857198e3 + sha256: dfae03cd2339587871e53b42833657faa4c9e42e3e2c56ee9e32bc60797c7f62 + build: h17c5cce_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: BSD-3-Clause + license_family: BSD + size: 33879 + timestamp: 1678534968831 - platform: osx-arm64 name: sniffio version: 1.3.0 @@ -3065,6 +4210,57 @@ package: timestamp: 1606414171959 purls: - pkg:pypi/vega-datasets +- platform: osx-arm64 + name: vegafusion + version: 1.6.1 + category: main + manager: conda + dependencies: + - altair >=4.2.0 + - pandas + - protobuf + - psutil + - pyarrow >=6 + - python >=3.7 + - vegafusion-python-embed >=1.0 + url: https://conda.anaconda.org/conda-forge/noarch/vegafusion-1.6.1-pyhd8ed1ab_0.conda + hash: + md5: 9ee62af730f460753bf8aee3c6730d0f + sha256: f9d0ed7fb31c86461e50a015bf8c72896a05175a340c673f203337d26427e8f1 + build: pyhd8ed1ab_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: BSD-3-Clause + license_family: BSD + noarch: python + size: 49207 + timestamp: 1704850581143 + purls: + - pkg:pypi/vegafusion +- platform: osx-arm64 + name: vegafusion-python-embed + version: 1.6.1 + category: main + manager: conda + dependencies: + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + url: https://conda.anaconda.org/conda-forge/osx-arm64/vegafusion-python-embed-1.6.1-py312h5280bc4_0.conda + hash: + md5: 8fe134243a591f2ac735122f3e0ed5a3 + sha256: 946e37ed95f4399729a67fdc316719558e1c90b6c8cafdaaa67a8cc7c293758e + build: py312h5280bc4_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: BSD-3-Clause + license_family: BSD + size: 13666950 + timestamp: 1704852841946 + purls: + - pkg:pypi/vegafusion-python-embed - platform: osx-arm64 name: vl-convert-python version: 1.2.2 @@ -3274,3 +4470,22 @@ package: timestamp: 1695255262261 purls: - pkg:pypi/zipp +- platform: osx-arm64 + name: zstd + version: 1.5.5 + category: main + manager: conda + dependencies: + - libzlib >=1.2.13,<1.3.0a0 + url: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.5-h4f39d0f_0.conda + hash: + md5: 5b212cfb7f9d71d603ad891879dc7933 + sha256: 7e1fe6057628bbb56849a6741455bbb88705bae6d6646257e57904ac5ee5a481 + build: h4f39d0f_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: BSD-3-Clause + license_family: BSD + size: 400508 + timestamp: 1693151393180 diff --git a/pixi.toml b/pixi.toml index 5f205e6..7a3f489 100644 --- a/pixi.toml +++ b/pixi.toml @@ -7,6 +7,8 @@ channels = ["conda-forge"] platforms = ["osx-arm64"] [tasks] +dev-py = { cmd = ["maturin", "develop", "-m", "avenger-python/Cargo.toml", "--release"]} + [dependencies] python = "3.12.*" @@ -16,3 +18,5 @@ pip = ">=23.3.2,<23.4" vl-convert-python = ">=1.2.2,<1.3" altair = ">=5.2.0,<5.3" vega_datasets = ">=0.9.0,<0.10" +vegafusion = ">=1.6.1,<1.7" +vegafusion-python-embed = ">=1.6.1,<1.7" From 4a82926e5eec9d108b10c5286df5d49010b7c90e Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 29 Jan 2024 20:44:23 -0500 Subject: [PATCH 2/9] Fix examples --- avenger-vega/src/image/mod.rs | 2 +- examples/scatter-panning/src/util.rs | 30 ++++++++++++++++++++++------ examples/wgpu-winit/src/util.rs | 24 ++++++++++++++++------ 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/avenger-vega/src/image/mod.rs b/avenger-vega/src/image/mod.rs index 803109e..083a6e0 100644 --- a/avenger-vega/src/image/mod.rs +++ b/avenger-vega/src/image/mod.rs @@ -16,7 +16,7 @@ pub fn make_image_fetcher() -> Result, AvengerVegaError> { if #[cfg(feature = "image-request")] { Ok(Box::new(ReqwestImageFetcher::new())) } else { - Err(VegaSceneGraphError::NoImageFetcherConfigured( + Err(AvengerVegaError::NoImageFetcherConfigured( "Image fetching requeres the image-reqwest feature flag".to_string() )) } diff --git a/examples/scatter-panning/src/util.rs b/examples/scatter-panning/src/util.rs index 5afeeeb..44efcb9 100644 --- a/examples/scatter-panning/src/util.rs +++ b/examples/scatter-panning/src/util.rs @@ -6,6 +6,7 @@ use avenger::scene_graph::SceneGraph; use avenger_vega::marks::symbol::shape_to_path; use avenger_vega::scene_graph::VegaSceneGraph; use avenger_wgpu::canvas::{Canvas, CanvasDimensions, WindowCanvas}; +use avenger_wgpu::error::AvengerWgpuError; use winit::event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}; use winit::event_loop::{ControlFlow, EventLoop}; use winit::window::WindowBuilder; @@ -149,13 +150,23 @@ pub async fn run() { match canvas.render() { Ok(_) => {} // Reconfigure the surface if it's lost or outdated - Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => { - canvas.resize(canvas.get_size()) + Err(AvengerWgpuError::SurfaceError(err)) => { + match err { + wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated => { + canvas.resize(canvas.get_size()); + } + wgpu::SurfaceError::OutOfMemory => { + // The system is out of memory, we should probably quit + *control_flow = ControlFlow::Exit; + } + wgpu::SurfaceError::Timeout => { + log::warn!("Surface timeout"); + } + } + } + Err(err) => { + log::error!("{:?}", err); } - // The system is out of memory, we should probably quit - Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit, - - Err(wgpu::SurfaceError::Timeout) => log::warn!("Surface timeout"), } } Event::RedrawEventsCleared => { @@ -186,6 +197,7 @@ fn make_sg( SceneGraph { groups: vec![SceneGroup { + name: "".to_string(), bounds: GroupBounds { x: 0.0, y: 0.0, @@ -210,6 +222,12 @@ fn make_sg( gradients: vec![], shape_index: EncodingValue::Scalar { value: 0 } })], + gradients: vec![], + fill: None, + stroke: None, + stroke_width: None, + stroke_offset: None, + corner_radius: None, }], width, height, diff --git a/examples/wgpu-winit/src/util.rs b/examples/wgpu-winit/src/util.rs index 596e025..228bcea 100644 --- a/examples/wgpu-winit/src/util.rs +++ b/examples/wgpu-winit/src/util.rs @@ -1,6 +1,7 @@ use avenger::scene_graph::SceneGraph; use avenger_vega::scene_graph::VegaSceneGraph; use avenger_wgpu::canvas::{Canvas, CanvasDimensions, WindowCanvas}; +use avenger_wgpu::error::AvengerWgpuError; use winit::event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}; use winit::event_loop::{ControlFlow, EventLoop}; use winit::window::WindowBuilder; @@ -95,16 +96,27 @@ pub async fn run() { } Event::RedrawRequested(window_id) if window_id == canvas.window().id() => { canvas.update(); + match canvas.render() { Ok(_) => {} // Reconfigure the surface if it's lost or outdated - Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => { - canvas.resize(canvas.get_size()) + Err(AvengerWgpuError::SurfaceError(err)) => { + match err { + wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated => { + canvas.resize(canvas.get_size()); + } + wgpu::SurfaceError::OutOfMemory => { + // The system is out of memory, we should probably quit + *control_flow = ControlFlow::Exit; + } + wgpu::SurfaceError::Timeout => { + log::warn!("Surface timeout"); + } + } + } + Err(err) => { + log::error!("{:?}", err); } - // The system is out of memory, we should probably quit - Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit, - - Err(wgpu::SurfaceError::Timeout) => log::warn!("Surface timeout"), } } Event::RedrawEventsCleared => { From 3247f13d03991dc3f552f4dca314cc6af6d600bb Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 30 Jan 2024 07:20:15 -0500 Subject: [PATCH 3/9] Add CI builds --- .github/workflows/python.yml | 143 +++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 .github/workflows/python.yml diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..8ac4a97 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,143 @@ +name: Python + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build-python-macos: + runs-on: macos-latest + strategy: + matrix: + target: + - x86_64 + - aarch64-apple-darwin + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: 3.9 + architecture: x64 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: "build-python-macos-${{ matrix.target }}" + - name: Build wheels - x86_64 + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --sdist -m avenger-python/Cargo.toml --strip + - name: Install built wheel - x86_64 + if: matrix.target == 'x86_64' + run: | + pip install avenger --no-index --find-links dist --force-reinstall + python -c "import avenger" + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: python-wheels + path: dist + + build-python-windows: + runs-on: windows-latest + strategy: + matrix: + target: + - x64 + # - x86 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: 3.9 + architecture: ${{ matrix.target }} + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: "build-python-windows-${{ matrix.target }}" + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist -m avenger-python/Cargo.toml --strip + - name: Install built wheel + run: | + pip install avenger --no-index --find-links dist --force-reinstall + python -c "import avenger" + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: python-wheels + path: dist + + build-python-linux: + runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.9 + architecture: x64 + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + manylinux: auto + args: --release --out dist -m avenger-python/Cargo.toml --strip + - name: Install built wheel and import + if: matrix.target == 'x86_64' + run: | + pip install avenger --no-index --find-links dist --force-reinstall + python -c "import avenger" + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: python-wheels + path: dist + + build-python-linux-cross: + runs-on: ubuntu-latest + strategy: + matrix: + target: + - aarch64 + # # Add additional architectures in the future + # - armv7 + # - s390x + # - ppc64le + # - ppc64 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: 3.9 + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + manylinux: auto + args: --release --out dist -m avenger-python/Cargo.toml + - uses: uraimo/run-on-arch-action@v2.6.0 + if: matrix.target != 'ppc64' + name: Install built wheel + with: + arch: ${{ matrix.target }} + distro: ubuntu20.04 + githubToken: ${{ github.token }} + install: | + apt-get update + apt-get install -y --no-install-recommends python3 python3-pip + pip3 install -U pip + run: | + pip3 install avenger --no-index --find-links dist/ --force-reinstall + python3 -c "import avenger" + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: python-wheels + path: dist \ No newline at end of file From f1a4e96a0083f45cafd9edd7a7bc2b40e4046efe Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 30 Jan 2024 07:27:19 -0500 Subject: [PATCH 4/9] Suppress clippy warning --- avenger-python/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/avenger-python/src/lib.rs b/avenger-python/src/lib.rs index 15c64f5..0589e3b 100644 --- a/avenger-python/src/lib.rs +++ b/avenger-python/src/lib.rs @@ -22,6 +22,7 @@ impl SceneGraph { Ok(Self { inner }) } + #[allow(clippy::wrong_self_convention)] fn to_png(&mut self, py: Python, scale: Option) -> PyResult { let mut png_canvas = pollster::block_on(PngCanvas::new(CanvasDimensions { size: [self.inner.width, self.inner.height], From 97e8becc8c9198d4b63f536149af1e1d030d5526 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 30 Jan 2024 07:28:05 -0500 Subject: [PATCH 5/9] Use rustls with request for self-contained build --- avenger-vega/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/avenger-vega/Cargo.toml b/avenger-vega/Cargo.toml index f1f14e0..bcf6654 100644 --- a/avenger-vega/Cargo.toml +++ b/avenger-vega/Cargo.toml @@ -17,5 +17,5 @@ serde_json = { version = "1.0.111" } lyon_extra = { workspace = true } lyon_path = { workspace = true, features = ["serialization"]} image = { workspace = true, default-features = false, features = ["png"] } -reqwest = { version = "0.11.23", features = ["blocking"], optional = true } -pyo3 = { workspace = true, optional = true } \ No newline at end of file +reqwest = { version = "0.11.23", features = ["blocking", "rustls"], optional = true } +pyo3 = { workspace = true, optional = true } From 16f96308bb2703e6ee3422283d6e3103e45ec30c Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 30 Jan 2024 07:36:20 -0500 Subject: [PATCH 6/9] avenger crate doesn't need reqwest --- avenger/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/avenger/Cargo.toml b/avenger/Cargo.toml index 7ed16f3..33c9d66 100644 --- a/avenger/Cargo.toml +++ b/avenger/Cargo.toml @@ -10,5 +10,4 @@ thiserror = { workspace = true } serde = { workspace = true } lyon_path = { workspace = true, features = ["serialization"] } image = { workspace = true, features = ["png"] } -reqwest = { version="0.11.23" , features = ["blocking"]} pyo3 = { workspace = true, optional = true } From a3e1d67aca3cc524951f93866dd27aecca7fa0fa Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 30 Jan 2024 07:38:39 -0500 Subject: [PATCH 7/9] lockfile --- Cargo.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 3745e4f..88283eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,7 +314,6 @@ dependencies = [ "image", "lyon_path", "pyo3", - "reqwest", "serde", "thiserror", ] From e05f130b79d9c9b21f4d43fba4bd509d705240de Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 30 Jan 2024 08:04:04 -0500 Subject: [PATCH 8/9] Add bump_version.py script, set version to 0.0.1-a1 --- Cargo.lock | 10 +++--- automation/bump_version.py | 47 +++++++++++++++++++++++++++ avenger-python/Cargo.toml | 35 +++++++++++++++----- avenger-vega-test-data/Cargo.toml | 10 +++--- avenger-vega/Cargo.toml | 48 +++++++++++++++++++++------- avenger-wgpu/Cargo.toml | 53 +++++++++++++++++++++---------- avenger/Cargo.toml | 25 ++++++++++----- pixi.lock | 45 ++++++++++++++++++++++++++ pixi.toml | 4 ++- 9 files changed, 222 insertions(+), 55 deletions(-) create mode 100644 automation/bump_version.py diff --git a/Cargo.lock b/Cargo.lock index 88283eb..4b9bf3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -309,7 +309,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "avenger" -version = "0.1.0" +version = "0.0.1-a1" dependencies = [ "image", "lyon_path", @@ -320,7 +320,7 @@ dependencies = [ [[package]] name = "avenger-python" -version = "0.0.1" +version = "0.0.1-a1" dependencies = [ "avenger", "avenger-vega", @@ -334,7 +334,7 @@ dependencies = [ [[package]] name = "avenger-vega" -version = "0.1.0" +version = "0.0.1-a1" dependencies = [ "avenger", "cfg-if", @@ -351,7 +351,7 @@ dependencies = [ [[package]] name = "avenger-vega-test-data" -version = "0.1.0" +version = "0.0.1-a1" dependencies = [ "pollster", "serde_json", @@ -360,7 +360,7 @@ dependencies = [ [[package]] name = "avenger-wgpu" -version = "0.1.0" +version = "0.0.1-a1" dependencies = [ "avenger", "avenger-vega", diff --git a/automation/bump_version.py b/automation/bump_version.py new file mode 100644 index 0000000..b68bae8 --- /dev/null +++ b/automation/bump_version.py @@ -0,0 +1,47 @@ +import click +from pathlib import Path +import toml + +root = Path(__file__).parent.parent.absolute() + + +@click.command() +@click.argument('version') +def bump_version(version): + print(f"Updating version to {version}") + + # Handle Cargo.toml files + cargo_packages = [ + "avenger", + "avenger-vega", + "avenger-wgpu", + "avenger-vega-test-data", + "avenger-python", + ] + + for package in cargo_packages: + cargo_toml_path = (root / package / "Cargo.toml") + cargo_toml = toml.loads(cargo_toml_path.read_text()) + # Update this package's version + cargo_toml["package"]["version"] = version + + # Look for local workspace dependencies and update their versions + for dep_type in ["dependencies", "dev-dependencies", "build-dependencies"]: + for p, props in cargo_toml.get(dep_type, {}).items(): + if isinstance(props, dict) and props.get("path", "").startswith("../avenger"): + props["version"] = version + + # Fix quotes in target so that they don't get double escaped when written back out + new_target = {} + for target, val in cargo_toml.get("target", {}).items(): + unescaped_target = target.replace(r'\"', '"') + new_target[unescaped_target] = val + if new_target: + cargo_toml["target"] = new_target + + cargo_toml_path.write_text(toml.dumps(cargo_toml)) + print(f"Updated version in {cargo_toml_path}") + + +if __name__ == '__main__': + bump_version() diff --git a/avenger-python/Cargo.toml b/avenger-python/Cargo.toml index 30f6bb4..2a3a8f5 100644 --- a/avenger-python/Cargo.toml +++ b/avenger-python/Cargo.toml @@ -1,20 +1,39 @@ [package] name = "avenger-python" -version = "0.0.1" +version = "0.0.1-a1" edition = "2021" license = "BSD-3-Clause" publish = false [lib] name = "avenger" -crate-type = ["cdylib"] +crate-type = [ "cdylib",] [dependencies] -avenger = { path = "../avenger", features = ["pyo3"] } -avenger-vega = { path = "../avenger-vega", features = ["pyo3"] } -avenger-wgpu = { path = "../avenger-wgpu", features = ["pyo3"] } -pyo3 = { workspace = true, features = ["extension-module", "abi3-py38"] } pythonize = "0.20.0" -serde = {workspace = true} pollster = "0.3" -image = {workspace = true} + +[dependencies.avenger] +path = "../avenger" +features = [ "pyo3",] +version = "0.0.1-a1" + +[dependencies.avenger-vega] +path = "../avenger-vega" +features = [ "pyo3",] +version = "0.0.1-a1" + +[dependencies.avenger-wgpu] +path = "../avenger-wgpu" +features = [ "pyo3",] +version = "0.0.1-a1" + +[dependencies.pyo3] +workspace = true +features = [ "extension-module", "abi3-py38",] + +[dependencies.serde] +workspace = true + +[dependencies.image] +workspace = true diff --git a/avenger-vega-test-data/Cargo.toml b/avenger-vega-test-data/Cargo.toml index 1f4c453..530bacd 100644 --- a/avenger-vega-test-data/Cargo.toml +++ b/avenger-vega-test-data/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "avenger-vega-test-data" -version = "0.1.0" +version = "0.0.1-a1" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -serde_json = {workspace = true} vl-convert-rs = "1.2.3" -pollster = "0.3" \ No newline at end of file +pollster = "0.3" + +[dependencies.serde_json] +workspace = true diff --git a/avenger-vega/Cargo.toml b/avenger-vega/Cargo.toml index bcf6654..5af124c 100644 --- a/avenger-vega/Cargo.toml +++ b/avenger-vega/Cargo.toml @@ -1,21 +1,45 @@ [package] name = "avenger-vega" -version = "0.1.0" +version = "0.0.1-a1" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -image-request = ["reqwest"] +image-request = [ "reqwest",] [dependencies] -avenger = { path = "../avenger" } cfg-if = "1" -thiserror = { workspace = true } -serde = { workspace = true } csscolorparser = "0.6.2" -serde_json = { version = "1.0.111" } -lyon_extra = { workspace = true } -lyon_path = { workspace = true, features = ["serialization"]} -image = { workspace = true, default-features = false, features = ["png"] } -reqwest = { version = "0.11.23", features = ["blocking", "rustls"], optional = true } -pyo3 = { workspace = true, optional = true } + +[dependencies.avenger] +path = "../avenger" +version = "0.0.1-a1" + +[dependencies.thiserror] +workspace = true + +[dependencies.serde] +workspace = true + +[dependencies.serde_json] +version = "1.0.111" + +[dependencies.lyon_extra] +workspace = true + +[dependencies.lyon_path] +workspace = true +features = [ "serialization",] + +[dependencies.image] +workspace = true +default-features = false +features = [ "png",] + +[dependencies.reqwest] +version = "0.11.23" +features = [ "blocking", "rustls",] +optional = true + +[dependencies.pyo3] +workspace = true +optional = true diff --git a/avenger-wgpu/Cargo.toml b/avenger-wgpu/Cargo.toml index 4bf1467..98b7e84 100644 --- a/avenger-wgpu/Cargo.toml +++ b/avenger-wgpu/Cargo.toml @@ -1,42 +1,63 @@ [package] name = "avenger-wgpu" -version = "0.1.0" +version = "0.0.1-a1" edition = "2021" [lib] -crate-type = ["cdylib", "rlib"] +crate-type = [ "cdylib", "rlib",] [features] -text-glyphon = ["glyphon"] -default = ["text-glyphon"] +text-glyphon = [ "glyphon",] +default = [ "text-glyphon",] [dependencies] -avenger = { path = "../avenger" } - -thiserror = { workspace = true } cfg-if = "1" winit = "0.28" env_logger = "0.10" log = "0.4" wgpu = "0.18" pollster = "0.3" -bytemuck = { version = "1.14", features = [ "derive" ] } cgmath = "0.18.0" itertools = "0.12.0" image = "0.24.7" futures-intrusive = "^0.5" etagere = "0.2.10" colorgrad = "0.6.2" -lazy_static = { workspace=true } -pyo3 = { workspace = true, optional = true } - -# glyphon branch that includes text rotation support: https://github.com/jonmmease/glyphon/pull/1 -glyphon = { git = "https://github.com/jonmmease/glyphon.git", rev="c468f5dacd4130b27a29b098c4de3f4d5c146209", optional = true } -lyon = { workspace = true } [dev-dependencies] -avenger-vega = { path = "../avenger-vega", features = ["image-request"] } -serde_json = { version = "1.0.111" } dssim = "3.2.4" rstest = "0.18.2" +[dependencies.avenger] +path = "../avenger" +version = "0.0.1-a1" + +[dependencies.thiserror] +workspace = true + +[dependencies.bytemuck] +version = "1.14" +features = [ "derive",] + +[dependencies.lazy_static] +workspace = true + +[dependencies.pyo3] +workspace = true +optional = true + +[dependencies.glyphon] +git = "https://github.com/jonmmease/glyphon.git" +rev = "c468f5dacd4130b27a29b098c4de3f4d5c146209" +optional = true + +[dependencies.lyon] +workspace = true + +[dev-dependencies.avenger-vega] +path = "../avenger-vega" +features = [ "image-request",] +version = "0.0.1-a1" + +[dev-dependencies.serde_json] +version = "1.0.111" diff --git a/avenger/Cargo.toml b/avenger/Cargo.toml index 33c9d66..5d48ed0 100644 --- a/avenger/Cargo.toml +++ b/avenger/Cargo.toml @@ -1,13 +1,22 @@ [package] name = "avenger" -version = "0.1.0" +version = "0.0.1-a1" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies.thiserror] +workspace = true -[dependencies] -thiserror = { workspace = true } -serde = { workspace = true } -lyon_path = { workspace = true, features = ["serialization"] } -image = { workspace = true, features = ["png"] } -pyo3 = { workspace = true, optional = true } +[dependencies.serde] +workspace = true + +[dependencies.lyon_path] +workspace = true +features = [ "serialization",] + +[dependencies.image] +workspace = true +features = [ "png",] + +[dependencies.pyo3] +workspace = true +optional = true diff --git a/pixi.lock b/pixi.lock index 57adbff..6dd1c59 100644 --- a/pixi.lock +++ b/pixi.lock @@ -768,6 +768,29 @@ package: timestamp: 1698833765762 purls: - pkg:pypi/charset-normalizer +- platform: osx-arm64 + name: click + version: 8.1.7 + category: main + manager: conda + dependencies: + - __unix + - python >=3.8 + url: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda + hash: + md5: f3ad426304898027fc619827ff428eca + sha256: f0016cbab6ac4138a429e28dbcb904a90305b34b3fe41a9b89d697c90401caec + build: unix_pyh707e725_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: BSD-3-Clause + license_family: BSD + noarch: python + size: 84437 + timestamp: 1692311973840 + purls: + - pkg:pypi/click - platform: osx-arm64 name: comm version: 0.2.1 @@ -3955,6 +3978,28 @@ package: license_family: BSD size: 3145523 timestamp: 1699202432999 +- platform: osx-arm64 + name: toml + version: 0.10.2 + category: main + manager: conda + dependencies: + - python >=2.7 + url: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2 + hash: + md5: f832c45a477c78bebd107098db465095 + sha256: f0f3d697349d6580e4c2f35ba9ce05c65dc34f9f049e85e45da03800b46139c1 + build: pyhd8ed1ab_0 + arch: aarch64 + subdir: osx-arm64 + build_number: 0 + license: MIT + license_family: MIT + noarch: python + size: 18433 + timestamp: 1604308660817 + purls: + - pkg:pypi/toml - platform: osx-arm64 name: tomli version: 2.0.1 diff --git a/pixi.toml b/pixi.toml index 7a3f489..644dc9e 100644 --- a/pixi.toml +++ b/pixi.toml @@ -8,7 +8,7 @@ platforms = ["osx-arm64"] [tasks] dev-py = { cmd = ["maturin", "develop", "-m", "avenger-python/Cargo.toml", "--release"]} - +bump-version = { cmd = ["python", "automation/bump_version.py"] } [dependencies] python = "3.12.*" @@ -20,3 +20,5 @@ altair = ">=5.2.0,<5.3" vega_datasets = ">=0.9.0,<0.10" vegafusion = ">=1.6.1,<1.7" vegafusion-python-embed = ">=1.6.1,<1.7" +click = ">=8.1.7,<8.2" +toml = ">=0.10.2,<0.11" From cc27b6059f2e5f41a9876e954fe7d901b1b48516 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 30 Jan 2024 08:07:20 -0500 Subject: [PATCH 9/9] Add publish-rs pixi task --- pixi.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pixi.toml b/pixi.toml index 644dc9e..15dc63e 100644 --- a/pixi.toml +++ b/pixi.toml @@ -10,6 +10,13 @@ platforms = ["osx-arm64"] dev-py = { cmd = ["maturin", "develop", "-m", "avenger-python/Cargo.toml", "--release"]} bump-version = { cmd = ["python", "automation/bump_version.py"] } +[tasks.publish-rs] +cmd = """ +cargo publish -p avenger && +cargo publish -p avenger-vega && +cargo publish -p avenger-wgpu +""" + [dependencies] python = "3.12.*" jupyterlab = ">=4.0.11,<4.1"