Replace the Artboard struct with a Table<Table<Graphic>> shape (#4077)

* Replace the Artboard struct with a Table<Table<Graphic>> shape

* Remove the never-functional, seemingly unneeded migrate_type_descriptor_names due to typo

* Allow negative artboard sizes
This commit is contained in:
Keavon Chambers
2026-04-28 22:58:33 -07:00
committed by GitHub
parent 0847d7b0ab
commit ba63c26c62
27 changed files with 350 additions and 442 deletions

View File

@@ -14,7 +14,7 @@ pub use glam::{DAffine2, DVec2, IVec2, UVec2};
use graphic_types::raster_types::{CPU, Image, Raster};
use graphic_types::vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke};
use graphic_types::vector_types::vector::{self, ReferencePoint};
use graphic_types::{Artboard, Graphic, Vector};
use graphic_types::{Graphic, Vector};
use raster_nodes::curve::Curve;
use rendering::RenderMetadata;
use std::fmt::Display;
@@ -184,7 +184,7 @@ tagged_value! {
Graphic(Table<Graphic>),
#[serde(deserialize_with = "graphic_types::artboard::migrate_artboard")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "ArtboardGroup")]
Artboard(Table<Artboard>),
Artboard(Table<Table<Graphic>>),
#[serde(deserialize_with = "core_types::misc::migrate_color")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "ColorTable", alias = "OptionalColor", alias = "ColorNotInTable")]
Color(Table<Color>),

View File

@@ -21,7 +21,7 @@ use graphene_std::table::Table;
use graphene_std::transform::Footprint;
use graphene_std::uuid::NodeId;
use graphene_std::vector::Vector;
use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future};
use graphene_std::{Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future};
use node_registry_macros::{async_node, convert_node, into_node};
use std::collections::HashMap;
#[cfg(feature = "gpu")]
@@ -63,7 +63,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
// MONITOR NODES
// =============
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ()]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<Artboard>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<Table<Graphic>>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<Graphic>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<Vector>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<Raster<CPU>>]),
@@ -145,7 +145,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
// ==========
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => ()]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => bool]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Artboard>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Table<Graphic>>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Graphic>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Vector>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Raster<CPU>>]),

View File

@@ -33,7 +33,9 @@ pub use num_traits;
use std::any::TypeId;
use std::future::Future;
use std::pin::Pin;
pub use table::{ATTR_ALPHA_BLENDING, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_END, ATTR_NAME, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE};
pub use table::{
ATTR_ALPHA_BLENDING, ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_END, ATTR_LOCATION, ATTR_NAME, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE,
};
#[cfg(feature = "wasm")]
pub use tsify;
pub use types::Cow;

View File

@@ -44,6 +44,18 @@ pub const ATTR_NAME: &str = "name";
/// `json_query_all` text node alongside each extracted value.
pub const ATTR_TYPE: &str = "type";
/// Attribute key for an artboard row's `DVec2` top-left corner location in document coordinates.
pub const ATTR_LOCATION: &str = "location";
/// Attribute key for an artboard row's `DVec2` width and height.
pub const ATTR_DIMENSIONS: &str = "dimensions";
/// Attribute key for an artboard row's `Color` background fill.
pub const ATTR_BACKGROUND: &str = "background";
/// Attribute key for an artboard row's `bool` flag indicating whether content is clipped to the artboard bounds.
pub const ATTR_CLIP: &str = "clip";
// =====================
// TRAIT: AttributeValue
// =====================

View File

@@ -153,41 +153,12 @@ impl Display for ProtoNodeIdentifier {
}
}
fn migrate_type_descriptor_names<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Cow<'static, str>, D::Error> {
use serde::Deserialize;
let name = String::deserialize(deserializer)?;
let name = match name.as_str() {
"f32" => "f64".to_string(),
"grahpene_core::transform::Footprint" => "std::option::Option<std::sync::Arc<grahpene_core::context::OwnedContextImpl>>".to_string(),
"grahpene_core::graphic_element::GraphicGroup" => "grahpene_core::table::Table<grahpene_core::graphic_types::Graphic>".to_string(),
"grahpene_core::raster::image::ImageFrame<Color>"
| "grahpene_core::raster::image::ImageFrame<grahpene_core::raster::color::Color>"
| "grahpene_core::instances::Instances<grahpene_core::raster::image::ImageFrame<Color>>"
| "grahpene_core::instances::Instances<grahpene_core::raster::image::ImageFrame<grahpene_core::raster::color::Color>>"
| "grahpene_core::instances::Instances<grahpene_core::raster::image::Image<grahpene_core::raster::color::Color>>" => {
"grahpene_core::table::Table<grahpene_core::raster::image::Image<grahpene_core::raster::color::Color>>".to_string()
}
"grahpene_core::vector::vector_data::VectorData"
| "grahpene_core::instances::Instances<grahpene_core::vector::vector_data::VectorData>"
| "grahpene_core::table::Table<grahpene_core::vector::vector_data::VectorData>"
| "grahpene_core::table::Table<grahpene_core::vector::vector_data::Vector>" => "grahpene_core::table::Table<grahpene_core::vector::vector_types::Vector>".to_string(),
"grahpene_core::instances::Instances<grahpene_core::graphic_element::Artboard>" => "grahpene_core::table::Table<grahpene_core::artboard::Artboard>".to_string(),
"grahpene_core::vector::vector_data::modification::VectorModification" => "grahpene_core::vector::vector_modification::VectorModification".to_string(),
"grahpene_core::table::Table<grahpene_core::graphic_element::Graphic>" => "grahpene_core::table::Table<grahpene_core::graphic_types::Graphic>".to_string(),
_ => name,
};
Ok(Cow::Owned(name))
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Debug, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TypeDescriptor {
#[cfg_attr(feature = "serde", serde(skip))]
pub id: Option<TypeId>,
#[cfg_attr(feature = "serde", serde(deserialize_with = "migrate_type_descriptor_names"))]
pub name: Cow<'static, str>,
#[cfg_attr(feature = "serde", serde(default))]
pub alias: Option<Cow<'static, str>>,

View File

@@ -1,107 +1,45 @@
use crate::graphic::Graphic;
use core_types::blending::AlphaBlending;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::math::quad::Quad;
use core_types::render_complexity::RenderComplexity;
use core_types::table::{Table, TableRow};
use core_types::transform::Transform;
use core_types::uuid::NodeId;
use core_types::{ATTR_TRANSFORM, Color};
use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, Color};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2, IVec2};
use graphene_hash::CacheHash;
use glam::{DAffine2, IVec2};
/// Some [`ArtboardData`] with some optional clipping bounds that can be exported.
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Artboard {
pub content: Table<Graphic>,
pub label: String,
pub location: IVec2,
pub dimensions: IVec2,
pub background: Color,
pub clip: bool,
}
impl Default for Artboard {
fn default() -> Self {
Self::new(IVec2::ZERO, IVec2::new(1920, 1080))
}
}
impl Artboard {
pub fn new(location: IVec2, dimensions: IVec2) -> Self {
Self {
content: Table::new(),
label: "Artboard".to_string(),
location: location.min(location + dimensions),
dimensions: dimensions.abs(),
background: Color::WHITE,
clip: true,
}
}
}
impl BoundingBox for Artboard {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
let artboard_bounds = || (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box();
if self.clip {
return RenderBoundingBox::Rectangle(artboard_bounds());
}
let mut combined_bounds = None;
for (element, row_transform) in self.content.iter_element_values().zip(self.content.iter_attribute_values_or_default::<DAffine2>(ATTR_TRANSFORM)) {
match element.bounding_box(transform * row_transform, include_stroke) {
RenderBoundingBox::None => continue,
RenderBoundingBox::Infinite => return RenderBoundingBox::Infinite,
RenderBoundingBox::Rectangle(bounds) => match combined_bounds {
Some(existing) => combined_bounds = Some(Quad::combine_bounds(existing, bounds)),
None => combined_bounds = Some(bounds),
},
}
}
match combined_bounds {
Some(content_bounds) => RenderBoundingBox::Rectangle(Quad::combine_bounds(content_bounds, artboard_bounds())),
None => RenderBoundingBox::Rectangle(artboard_bounds()),
}
}
}
impl RenderComplexity for Artboard {
fn render_complexity(&self) -> usize {
self.content.render_complexity()
}
}
// Implementations for Artboard
impl Transform for Artboard {
fn transform(&self) -> DAffine2 {
DAffine2::from_translation(self.location.as_dvec2())
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
self.location.as_dvec2() + self.dimensions.as_dvec2() * pivot
}
}
// An artboard table is `Table<Table<Graphic>>`: each row's element is the artboard's content
// (a `Table<Graphic>`), with the artboard's metadata stored alongside on the row as attributes
// (see `ATTR_LOCATION`, `ATTR_DIMENSIONS`, `ATTR_BACKGROUND`, `ATTR_CLIP`).
//
// The artboard's user-visible name is the parent layer's display name (resolved live from the
// network interface via the row's `ATTR_EDITOR_LAYER_PATH` attribute) — not stored here, so it
// can never go stale.
//
// These metadata attributes are populated at runtime by the `Artboard` proto node from its
// inputs and therefore aren't persisted in document files; the proto node's input values are
// what get serialized.
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Artboard>, D::Error> {
pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Table<Graphic>>, D::Error> {
use serde::Deserialize;
#[derive(Clone, Default, Debug, PartialEq, DynAny)]
/// Pre-migration shape of the artboard's stored data: the struct that used to live as the element
/// of `Table<Artboard>`. Kept as a private type so we can deserialize legacy documents into the new
/// `Table<Table<Graphic>>` (element = `content`, other fields → row attributes).
#[derive(Clone, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ArtboardGroup {
pub artboards: Vec<(Artboard, Option<NodeId>)>,
pub struct LegacyArtboard {
pub content: Table<Graphic>,
pub label: String,
pub location: IVec2,
pub dimensions: IVec2,
pub background: Color,
pub clip: bool,
}
#[derive(Clone, Default, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum ArtboardFormat {
ArtboardGroup(ArtboardGroup),
OldArtboardTable(OldTable<Artboard>),
ArtboardTable(Table<Artboard>),
pub struct LegacyArtboardGroup {
pub artboards: Vec<(LegacyArtboard, Option<NodeId>)>,
}
#[derive(Clone, Debug)]
@@ -113,13 +51,30 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re
alpha_blending: Vec<AlphaBlending>,
}
// Attributes (transform, alpha_blending, editor:layer_path) are not serialized, so migration only needs
// to recover the elements. Per-item attribute values are populated at runtime by the node graph.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum ArtboardFormat {
ArtboardGroup(LegacyArtboardGroup),
OldArtboardTable(OldTable<LegacyArtboard>),
LegacyArtboardTable(Table<LegacyArtboard>),
// Note: this variant must come last so older formats above are tried first; an empty
// `Table<Table<Graphic>>` would otherwise match (since `Table<T>` has the same shell across `T`).
ArtboardTable(Table<Table<Graphic>>),
}
fn legacy_to_row(legacy: LegacyArtboard) -> TableRow<Table<Graphic>> {
// Legacy `label` field is dropped — the artboard's name now comes from its parent layer's display name.
TableRow::new_from_element(legacy.content)
.with_attribute(ATTR_LOCATION, legacy.location.as_dvec2())
.with_attribute(ATTR_DIMENSIONS, legacy.dimensions.as_dvec2())
.with_attribute(ATTR_BACKGROUND, legacy.background)
.with_attribute(ATTR_CLIP, legacy.clip)
}
Ok(match ArtboardFormat::deserialize(deserializer)? {
ArtboardFormat::ArtboardGroup(artboard_group) => artboard_group.artboards.into_iter().map(|(artboard, _)| TableRow::new_from_element(artboard)).collect(),
ArtboardFormat::OldArtboardTable(old_table) => old_table.element.into_iter().map(TableRow::new_from_element).collect(),
ArtboardFormat::ArtboardGroup(group) => group.artboards.into_iter().map(|(artboard, _)| legacy_to_row(artboard)).collect(),
ArtboardFormat::OldArtboardTable(old_table) => old_table.element.into_iter().map(legacy_to_row).collect(),
ArtboardFormat::LegacyArtboardTable(legacy_table) => legacy_table.into_iter().map(|row| legacy_to_row(row.into_element())).collect(),
ArtboardFormat::ArtboardTable(artboard_table) => artboard_table,
})
}
// Node definitions moved to graphic-nodes crate

View File

@@ -7,7 +7,6 @@ pub use raster_types;
pub use vector_types;
// Re-export commonly used types at the crate root
pub use artboard::Artboard;
pub use graphic::{Graphic, IntoGraphicTable, TryFromGraphic, Vector};
pub mod migrations {

View File

@@ -8,19 +8,19 @@ use core_types::color::{Alpha, Color};
use core_types::math::quad::Quad;
use core_types::render_complexity::RenderComplexity;
use core_types::table::{Table, TableRow};
use core_types::transform::{Footprint, Transform};
use core_types::transform::Footprint;
use core_types::uuid::{NodeId, generate_uuid};
use core_types::{ATTR_ALPHA_BLENDING, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM};
use core_types::{ATTR_ALPHA_BLENDING, ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_LOCATION, ATTR_TRANSFORM};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use graphene_hash::CacheHashWrapper;
use graphic_types::Graphic;
use graphic_types::Vector;
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster};
use graphic_types::vector_types::gradient::{GradientStops, GradientType};
use graphic_types::vector_types::subpath::Subpath;
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, Stroke, StrokeAlign};
use graphic_types::{Artboard, Graphic};
use kurbo::{Affine, Cap, Join, Shape};
use num_traits::Zero;
use std::collections::{HashMap, HashSet};
@@ -500,172 +500,172 @@ impl Render for Graphic {
}
}
impl Render for Artboard {
/// Reads the artboard metadata for the row at `index` from a `Table<Table<Graphic>>` of artboards.
fn read_artboard_attributes(table: &Table<Table<Graphic>>, index: usize) -> (DVec2, DVec2, Color, bool) {
let location: DVec2 = table.attribute_cloned_or_default(ATTR_LOCATION, index);
let dimensions: DVec2 = table.attribute_cloned_or_default(ATTR_DIMENSIONS, index);
let background: Color = table.attribute_cloned_or_default(ATTR_BACKGROUND, index);
let clip: bool = table.attribute_cloned_or_default(ATTR_CLIP, index);
(location, dimensions, background, clip)
}
impl Render for Table<Table<Graphic>> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
let x = self.location.x.min(self.location.x + self.dimensions.x);
let y = self.location.y.min(self.location.y + self.dimensions.y);
let width = self.dimensions.x.abs();
let height = self.dimensions.y.abs();
for index in 0..self.len() {
let Some(content) = self.element(index) else { continue };
let (location, dimensions, background, clip) = read_artboard_attributes(self, index);
// Rectangle for the artboard
if !render_params.hide_artboards {
// Transparency checkerboard behind the artboard background (viewport only)
let show_checkerboard = self.background.alpha() < 1. && render_params.to_canvas();
if show_checkerboard && render_params.viewport_zoom > 0. {
let checker_id = format!("checkered-artboard-{}", generate_uuid());
let cell_size = 8. / render_params.viewport_zoom;
let pattern_size = cell_size * 2.;
let x = location.x.min(location.x + dimensions.x);
let y = location.y.min(location.y + dimensions.y);
let width = dimensions.x.abs();
let height = dimensions.y.abs();
// Anchor pattern at this artboard's top-left corner (x, y), not the document origin
let _ = write!(
&mut render.svg_defs,
r##"<pattern id="{checker_id}" x="{x}" y="{y}" width="{pattern_size}" height="{pattern_size}" patternUnits="userSpaceOnUse"><rect width="{pattern_size}" height="{pattern_size}" fill="#fff" /><rect x="{cell_size}" y="0" width="{cell_size}" height="{cell_size}" fill="#ccc" /><rect x="0" y="{cell_size}" width="{cell_size}" height="{cell_size}" fill="#ccc" /></pattern>"##
);
// Rectangle for the artboard
if !render_params.hide_artboards {
// Transparency checkerboard behind the artboard background (viewport only)
let show_checkerboard = background.alpha() < 1. && render_params.to_canvas();
if show_checkerboard && render_params.viewport_zoom > 0. {
let checker_id = format!("checkered-artboard-{}", generate_uuid());
let cell_size = 8. / render_params.viewport_zoom;
let pattern_size = cell_size * 2.;
// Anchor pattern at this artboard's top-left corner (x, y), not the document origin
let _ = write!(
&mut render.svg_defs,
r##"<pattern id="{checker_id}" x="{x}" y="{y}" width="{pattern_size}" height="{pattern_size}" patternUnits="userSpaceOnUse"><rect width="{pattern_size}" height="{pattern_size}" fill="#fff" /><rect x="{cell_size}" y="0" width="{cell_size}" height="{cell_size}" fill="#ccc" /><rect x="0" y="{cell_size}" width="{cell_size}" height="{cell_size}" fill="#ccc" /></pattern>"##
);
render.leaf_tag("rect", |attributes| {
attributes.push("x", x.to_string());
attributes.push("y", y.to_string());
attributes.push("width", width.to_string());
attributes.push("height", height.to_string());
attributes.push("fill", format!("url(#{checker_id})"));
});
}
// Background
render.leaf_tag("rect", |attributes| {
attributes.push("fill", format!("#{}", background.to_rgb_hex_srgb_from_gamma()));
if background.a() < 1. {
attributes.push("fill-opacity", ((background.a() * 1000.).round() / 1000.).to_string());
}
attributes.push("x", x.to_string());
attributes.push("y", y.to_string());
attributes.push("width", width.to_string());
attributes.push("height", height.to_string());
attributes.push("fill", format!("url(#{checker_id})"));
});
}
// Background
render.leaf_tag("rect", |attributes| {
attributes.push("fill", format!("#{}", self.background.to_rgb_hex_srgb_from_gamma()));
if self.background.a() < 1. {
attributes.push("fill-opacity", ((self.background.a() * 1000.).round() / 1000.).to_string());
}
attributes.push("x", x.to_string());
attributes.push("y", y.to_string());
attributes.push("width", width.to_string());
attributes.push("height", height.to_string());
});
// Artwork
render.parent_tag(
// SVG group tag
"g",
// Group tag attributes
|attributes| {
let matrix = format_transform_matrix(DAffine2::from_translation(location));
if !matrix.is_empty() {
attributes.push(ATTR_TRANSFORM, matrix);
}
if clip {
let id = format!("artboard-{}", generate_uuid());
let selector = format!("url(#{id})");
write!(
&mut attributes.0.svg_defs,
r##"<clipPath id="{id}"><rect x="0" y="0" width="{}" height="{}" /></clipPath>"##,
dimensions.x, dimensions.y,
)
.unwrap();
attributes.push("clip-path", selector);
}
},
// Artwork content
|render| {
let mut render_params = render_params.clone();
render_params.artboard_background = Some(background);
content.render_svg(render, &render_params);
},
);
}
// Artwork
render.parent_tag(
// SVG group tag
"g",
// Group tag attributes
|attributes| {
let matrix = format_transform_matrix(self.transform());
if !matrix.is_empty() {
attributes.push(ATTR_TRANSFORM, matrix);
}
if self.clip {
let id = format!("artboard-{}", generate_uuid());
let selector = format!("url(#{id})");
write!(
&mut attributes.0.svg_defs,
r##"<clipPath id="{id}"><rect x="0" y="0" width="{}" height="{}" /></clipPath>"##,
self.dimensions.x, self.dimensions.y,
)
.unwrap();
attributes.push("clip-path", selector);
}
},
// Artwork content
|render| {
let mut render_params = render_params.clone();
render_params.artboard_background = Some(self.background);
self.content.render_svg(render, &render_params);
},
);
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
use vello::peniko;
let [a, b] = [self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()];
let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y));
for index in 0..self.len() {
let Some(content) = self.element(index) else { continue };
let (location, dimensions, background, clip) = read_artboard_attributes(self, index);
// Render background
if !render_params.hide_artboards {
let artboard_transform = kurbo::Affine::new(transform.to_cols_array());
let [a, b] = [location, location + dimensions];
let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y));
// Transparency checkerboard behind the artboard background (viewport only)
let show_checkerboard = self.background.alpha() < 1. && render_params.to_canvas();
if show_checkerboard && render_params.viewport_zoom > 0. {
// Anchor pattern at THIS artboard's top-left corner
// brush_transform is an image placement transform: it maps brush pixel coords → shape coords
// scale(1/zoom) sets each brush pixel to 1/zoom document units (constant CSS size after viewport transform)
// then_translate places the brush origin at the artboard corner
let brush_transform = kurbo::Affine::scale(1. / render_params.viewport_zoom).then_translate(kurbo::Vec2::new(rect.x0, rect.y0));
scene.fill(peniko::Fill::NonZero, artboard_transform, &checkerboard_brush(), Some(brush_transform), &rect);
// Render background
if !render_params.hide_artboards {
let artboard_transform = kurbo::Affine::new(transform.to_cols_array());
// Transparency checkerboard behind the artboard background (viewport only)
let show_checkerboard = background.alpha() < 1. && render_params.to_canvas();
if show_checkerboard && render_params.viewport_zoom > 0. {
// Anchor pattern at THIS artboard's top-left corner
// brush_transform is an image placement transform: it maps brush pixel coords → shape coords
// scale(1/zoom) sets each brush pixel to 1/zoom document units (constant CSS size after viewport transform)
// then_translate places the brush origin at the artboard corner
let brush_transform = kurbo::Affine::scale(1. / render_params.viewport_zoom).then_translate(kurbo::Vec2::new(rect.x0, rect.y0));
scene.fill(peniko::Fill::NonZero, artboard_transform, &checkerboard_brush(), Some(brush_transform), &rect);
}
let color = peniko::Color::new([background.r(), background.g(), background.b(), background.a()]);
scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., artboard_transform, &rect);
scene.fill(peniko::Fill::NonZero, artboard_transform, color, None, &rect);
scene.pop_layer();
}
let color = peniko::Color::new([self.background.r(), self.background.g(), self.background.b(), self.background.a()]);
scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., artboard_transform, &rect);
scene.fill(peniko::Fill::NonZero, artboard_transform, color, None, &rect);
scene.pop_layer();
}
if self.clip {
scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), &rect);
}
// Since the content's transform is right multiplied in when rendering the content, we just need to right multiply by the artboard offset here.
let child_transform = transform * DAffine2::from_translation(self.location.as_dvec2());
let mut render_params = render_params.clone();
render_params.artboard_background = Some(self.background);
self.content.render_to_vello(scene, child_transform, context, &render_params);
if self.clip {
scene.pop_layer();
}
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option<NodeId>) {
if let Some(element_id) = element_id {
let subpath = Subpath::new_rectangle(DVec2::ZERO, self.dimensions.as_dvec2());
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.).into()]);
metadata.upstream_footprints.insert(element_id, footprint);
metadata.local_transforms.insert(element_id, DAffine2::from_translation(self.location.as_dvec2()));
if self.clip {
metadata.clip_targets.insert(element_id);
if clip {
scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), &rect);
}
}
footprint.transform *= self.transform();
self.content.collect_metadata(metadata, footprint, None);
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, self.dimensions.as_dvec2());
click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.));
}
fn contains_artboard(&self) -> bool {
true
}
}
impl Render for Table<Artboard> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for element in self.iter_element_values() {
element.render_svg(render, render_params);
}
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
for element in self.iter_element_values() {
element.render_to_vello(scene, transform, context, render_params);
// Since the content's transform is right multiplied in when rendering the content, we just need to right multiply by the artboard offset here.
let child_transform = transform * DAffine2::from_translation(location);
let mut render_params = render_params.clone();
render_params.artboard_background = Some(background);
content.render_to_vello(scene, child_transform, context, &render_params);
if clip {
scene.pop_layer();
}
}
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option<NodeId>) {
for index in 0..self.len() {
let Some(content) = self.element(index) else { continue };
let (location, dimensions, _background, clip) = read_artboard_attributes(self, index);
let layer_path: Table<NodeId> = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index);
let layer = layer_path.iter_element_values().next_back().copied();
self.element(index).unwrap().collect_metadata(metadata, footprint, layer);
let element_id = layer_path.iter_element_values().next_back().copied();
if let Some(element_id) = element_id {
let subpath = Subpath::new_rectangle(DVec2::ZERO, dimensions);
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.).into()]);
metadata.upstream_footprints.insert(element_id, footprint);
metadata.local_transforms.insert(element_id, DAffine2::from_translation(location));
if clip {
metadata.clip_targets.insert(element_id);
}
}
let mut child_footprint = footprint;
child_footprint.transform *= DAffine2::from_translation(location);
content.collect_metadata(metadata, child_footprint, None);
}
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for element in self.iter_element_values() {
element.add_upstream_click_targets(click_targets);
for index in 0..self.len() {
let dimensions: DVec2 = self.attribute_cloned_or_default(ATTR_DIMENSIONS, index);
let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, dimensions);
click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.));
}
}

View File

@@ -3,7 +3,7 @@ use core_types::transform::Footprint;
use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use graphic_types::vector_types::GradientStops;
use graphic_types::{Artboard, Graphic, Vector};
use graphic_types::{Graphic, Vector};
use raster_types::{CPU, GPU, Raster};
const DAY: f64 = 1000. * 3600. * 24.;
@@ -78,7 +78,7 @@ async fn quantize_real_time<T>(
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> Table<Artboard>,
Context -> Table<Table<Graphic>>,
Context -> Table<GradientStops>,
Context -> Table<String>,
Context -> Table<f64>,
@@ -118,7 +118,7 @@ async fn quantize_animation_time<T>(
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> Table<Artboard>,
Context -> Table<Table<Graphic>>,
Context -> Table<GradientStops>,
Context -> Table<String>,
Context -> Table<f64>,

View File

@@ -6,7 +6,7 @@ use core_types::uuid::NodeId;
use core_types::{Color, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use graphic_types::vector_types::GradientStops;
use graphic_types::{Artboard, Graphic, Vector};
use graphic_types::{Graphic, Vector};
use raster_types::{CPU, GPU, Raster};
/// Filters out what should be unused components of the context based on the specified requirements.
@@ -35,7 +35,7 @@ async fn context_modification<T>(
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> Table<Artboard>,
Context -> Table<Table<Graphic>>,
Context -> Table<GradientStops>,
)]
value: impl Node<Context<'static>, Output = T>,

View File

@@ -1,13 +1,19 @@
use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl, table::Table, transform::TransformMut};
use glam::{DAffine2, DVec2, IVec2};
use core_types::{
ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl,
table::{Table, TableRow},
transform::TransformMut,
};
use glam::{DAffine2, DVec2};
use graphic_types::{
Artboard, Vector,
Vector,
graphic::{Graphic, IntoGraphicTable},
};
use raster_types::{CPU, GPU, Raster};
use vector_types::GradientStops;
/// Constructs a new single-item `Table<Artboard>` with the chosen properties.
/// Constructs a new single-item `Table<Table<Graphic>>` (an artboard table) where the row's element is
/// the artboard's content and the metadata (label, location, dimensions, background, clip) is stored as
/// per-row attributes.
#[node_macro::node(category(""))]
pub async fn create_artboard<T: IntoGraphicTable + 'n>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
@@ -22,42 +28,40 @@ pub async fn create_artboard<T: IntoGraphicTable + 'n>(
Context -> DAffine2,
)]
content: impl Node<Context<'static>, Output = T>,
/// Name of the artboard, shown in parts of the editor.
label: String,
/// Coordinate of the top-left corner of the artboard within the document.
location: DVec2,
/// Width and height of the artboard within the document. Only integers are valid.
/// Width and height of the artboard within the document.
dimensions: DVec2,
/// Color of the artboard background. Only positive integers are valid.
/// Color of the artboard background.
background: Table<Color>,
/// Whether to cut off the contained content that extends outside the artboard, or keep it visible.
#[default(true)]
clip: bool,
) -> Table<Artboard> {
let location = location.as_ivec2();
) -> Table<Table<Graphic>> {
let footprint = ctx.try_footprint().copied();
let mut new_ctx = OwnedContextImpl::from(ctx);
if let Some(mut footprint) = footprint {
footprint.translate(location.as_dvec2());
footprint.translate(location);
new_ctx = new_ctx.with_footprint(footprint);
}
let content = content.eval(new_ctx.into_context()).await.into_graphic_table();
let dimensions = dimensions.as_ivec2().max(IVec2::ONE);
let location = location.min(location + dimensions);
let dimensions = dimensions.abs();
// Normalize so `location` is the top-left corner and `dimensions` are positive (allowing negative input
// dimensions to represent dragging from the opposite corner). Compute the corner using the raw signed
// dimensions before clamping, otherwise negative inputs collapse to the original corner instead of inverting.
let normalized_location = location.min(location + dimensions);
let normalized_dimensions = dimensions.abs().max(DVec2::ONE);
let background = background.element(0).copied().unwrap_or(Color::WHITE);
Table::new_from_element(Artboard {
content,
label,
location,
dimensions,
background,
clip,
})
// The artboard's user-visible name is its parent layer's display name; not stored as an attribute here so
// it can't go stale. The data panel resolves it live from the row's `editor:layer_path` NodeId via the network
// interface, so renaming the layer reflects everywhere on the next refresh.
Table::new_from_row(
TableRow::new_from_element(content)
.with_attribute(ATTR_LOCATION, normalized_location)
.with_attribute(ATTR_DIMENSIONS, normalized_dimensions)
.with_attribute(ATTR_BACKGROUND, background)
.with_attribute(ATTR_CLIP, clip),
)
}

View File

@@ -4,8 +4,8 @@ use core_types::table::{Table, TableRow};
use core_types::uuid::NodeId;
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM, AnyHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use graphic_types::Vector;
use graphic_types::graphic::{Graphic, IntoGraphicTable};
use graphic_types::{Artboard, Vector};
use raster_types::{CPU, GPU, Raster};
use vector_types::{GradientStop, GradientStops, ReferencePoint};
@@ -16,7 +16,7 @@ pub fn index_elements<T: graphic_types::graphic::AtIndex + Clone + Default>(
_: impl Ctx,
/// The list of data.
#[implementations(
Table<Artboard>,
Table<Table<Graphic>>,
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
@@ -48,7 +48,7 @@ pub fn omit_element<T: graphic_types::graphic::OmitIndex + Clone + Default>(
/// The list of data.
#[implementations(
Table<String>,
Table<Artboard>,
Table<Table<Graphic>>,
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
@@ -86,7 +86,7 @@ pub fn extract_element<T: Clone + Default + Send + Sync + 'static>(
Table<Vector>,
Table<Raster<CPU>>,
Table<Graphic>,
Table<Artboard>,
Table<Table<Graphic>>,
)]
table: Table<T>,
/// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item.
@@ -225,7 +225,7 @@ async fn write_attribute<T: AnyHash + Clone + Send + Sync + core_types::CacheHas
ctx: impl ExtractAll + CloneVarArgs + Ctx,
/// The `Table` whose items will gain or have replaced the named attribute.
#[implementations(
Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>, Table<Artboard>,
Table<Table<Graphic>>, Table<Table<Graphic>>, Table<Table<Graphic>>, Table<Table<Graphic>>, Table<Table<Graphic>>, Table<Table<Graphic>>, Table<Table<Graphic>>, Table<Table<Graphic>>, Table<Table<Graphic>>, Table<Table<Graphic>>,
Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>, Table<Graphic>,
Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>, Table<Vector>,
Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>, Table<Raster<CPU>>,
@@ -262,11 +262,11 @@ async fn write_attribute<T: AnyHash + Clone + Send + Sync + core_types::CacheHas
pub async fn extend<T: 'n + Send + Clone>(
_: impl Ctx,
/// The `Table` whose items will appear at the start of the extended `Table`.
#[implementations(Table<Artboard>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)]
#[implementations(Table<Table<Graphic>>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)]
base: Table<T>,
/// The `Table` whose items will appear at the end of the extended `Table`.
#[expose]
#[implementations(Table<Artboard>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)]
#[implementations(Table<Table<Graphic>>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)]
new: Table<T>,
) -> Table<T> {
let mut base = base;
@@ -281,9 +281,9 @@ pub async fn extend<T: 'n + Send + Clone>(
#[node_macro::node(category(""))]
pub async fn legacy_layer_extend<T: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(Table<Artboard>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)] base: Table<T>,
#[implementations(Table<Table<Graphic>>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)] base: Table<T>,
#[expose]
#[implementations(Table<Artboard>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)]
#[implementations(Table<Table<Graphic>>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)]
new: Table<T>,
nested_node_path: Table<NodeId>,
) -> Table<T> {

View File

@@ -11,7 +11,7 @@ pub use graphene_application_io as application_io;
pub use graphene_core;
pub use graphene_core::debug;
pub use graphic_nodes;
pub use graphic_types::{Artboard, Graphic, Vector};
pub use graphic_types::{Graphic, Vector};
pub use math_nodes;
pub use path_bool_nodes;
pub use raster_nodes;
@@ -44,7 +44,6 @@ pub mod vector {
pub mod graphic {
pub use graphic_nodes::graphic::*;
pub use graphic_types::Artboard;
pub use graphic_types::graphic::*;
}

View File

@@ -9,7 +9,7 @@ pub use graph_craft::document::value::RenderOutputType;
use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig};
use graphic_types::raster_types::Image;
use graphic_types::raster_types::{CPU, Raster};
use graphic_types::{Artboard, Graphic, Vector};
use graphic_types::{Graphic, Vector};
use rendering::{Render, RenderOutputType as RenderOutputTypeRequest, RenderParams, RenderSvgSegmentList, SvgRender, checkerboard_brush};
use rendering::{RenderMetadata, SvgSegment};
use std::collections::HashMap;
@@ -39,7 +39,7 @@ pub struct RenderIntermediate {
async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + Sync>(
ctx: impl Ctx + ExtractVarArgs + ExtractAll + CloneVarArgs,
#[implementations(
Context -> Table<Artboard>,
Context -> Table<Table<Graphic>>,
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,

View File

@@ -5,7 +5,7 @@ use core_types::transform::Footprint;
use core_types::{Color, Ctx, num_traits};
use glam::{DAffine2, DVec2};
use graphic_types::raster_types::{CPU, GPU, Raster};
use graphic_types::{Artboard, Graphic, Vector};
use graphic_types::{Graphic, Vector};
use log::warn;
use math_parser::ast;
use math_parser::context::{EvalContext, NothingMap, ValueProvider};
@@ -753,7 +753,7 @@ async fn switch<T, C: Send + 'n + Clone>(
Context -> u64,
Context -> DVec2,
Context -> DAffine2,
Context -> Table<Artboard>,
Context -> Table<Table<Graphic>>,
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
@@ -772,7 +772,7 @@ async fn switch<T, C: Send + 'n + Clone>(
Context -> u64,
Context -> DVec2,
Context -> DAffine2,
Context -> Table<Artboard>,
Context -> Table<Table<Graphic>>,
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,