Replace Table<Table<Graphic>> with Table<Artboard> where Artboard is a type boundary newtype (#4093)

Replace Table<Table<Graphic>> with Table<Artboard> with Artboard as a type boundary newtype
This commit is contained in:
Keavon Chambers
2026-05-01 21:57:50 -07:00
committed by GitHub
parent 9943af5248
commit a0d5f418d9
16 changed files with 134 additions and 86 deletions

View File

@@ -1,25 +1,71 @@
use crate::graphic::Graphic;
use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash;
use core_types::render_complexity::RenderComplexity;
use core_types::table::{Table, TableRow};
use core_types::uuid::NodeId;
use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, Color};
use dyn_any::DynAny;
use glam::{DAffine2, IVec2};
// 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.
/// Nominal wrapper around `Table<Graphic>` representing a single artboard's content.
///
/// Per-artboard metadata (location, dimensions, background, clip) lives as row attributes on the
/// enclosing `Table<Artboard>`, not as fields here. This keeps `Artboard` a pure type-system boundary
/// that prevents arbitrary `Table<Table<...<Graphic>>>` nesting.
#[derive(Clone, Debug, Default, CacheHash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Artboard(Table<Graphic>);
impl Artboard {
pub fn new(content: Table<Graphic>) -> Self {
Self(content)
}
pub fn as_graphic_table(&self) -> &Table<Graphic> {
&self.0
}
pub fn as_graphic_table_mut(&mut self) -> &mut Table<Graphic> {
&mut self.0
}
pub fn into_graphic_table(self) -> Table<Graphic> {
self.0
}
}
impl From<Table<Graphic>> for Artboard {
fn from(content: Table<Graphic>) -> Self {
Self(content)
}
}
impl From<Artboard> for Table<Graphic> {
fn from(artboard: Artboard) -> Self {
artboard.0
}
}
impl BoundingBox for Artboard {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
self.0.bounding_box(transform, include_stroke)
}
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
self.0.thumbnail_bounding_box(transform, include_stroke)
}
}
impl RenderComplexity for Artboard {
fn render_complexity(&self) -> usize {
self.0.render_complexity()
}
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Table<Graphic>>, D::Error> {
pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Artboard>, D::Error> {
use serde::Deserialize;
/// Mirrors the removed `AlphaBlending` struct for legacy document deserialization.
@@ -33,9 +79,7 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re
pub clip: bool,
}
/// 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).
/// Legacy artboard struct shape, kept for deserializing old documents into `Table<Artboard>`.
#[derive(Clone, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LegacyArtboard {
@@ -68,14 +112,14 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re
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>>),
// NOTE: Must come last so older tagged formats above are tried first.
// Also covers the intermediate `Table<Table<Graphic>>` shape since `Artboard` deserializes transparently.
ArtboardTable(Table<Artboard>),
}
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)
fn legacy_to_row(legacy: LegacyArtboard) -> TableRow<Artboard> {
// Legacy `label` field is dropped (the artboard's name comes from its parent layer's display name)
TableRow::new_from_element(Artboard::new(legacy.content))
.with_attribute(ATTR_LOCATION, legacy.location.as_dvec2())
.with_attribute(ATTR_DIMENSIONS, legacy.dimensions.as_dvec2())
.with_attribute(ATTR_BACKGROUND, legacy.background)

View File

@@ -7,6 +7,7 @@ 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

@@ -17,13 +17,12 @@ use core_types::{
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, Vector};
use kurbo::{Affine, Cap, Join, Shape};
use num_traits::Zero;
use std::collections::{HashMap, HashSet};
@@ -514,8 +513,8 @@ impl Render for Graphic {
}
}
/// 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) {
/// Reads the artboard metadata for the row at `index` from a `Table<Artboard>`.
fn read_artboard_attributes(table: &Table<Artboard>, 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);
@@ -523,10 +522,10 @@ fn read_artboard_attributes(table: &Table<Table<Graphic>>, index: usize) -> (DVe
(location, dimensions, background, clip)
}
impl Render for Table<Table<Graphic>> {
impl Render for Table<Artboard> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for index in 0..self.len() {
let Some(content) = self.element(index) else { continue };
let Some(content) = self.element(index).map(Artboard::as_graphic_table) else { continue };
let (location, dimensions, background, clip) = read_artboard_attributes(self, index);
let x = location.x.min(location.x + dimensions.x);
@@ -584,7 +583,7 @@ impl Render for Table<Table<Graphic>> {
use vello::peniko;
for index in 0..self.len() {
let Some(content) = self.element(index) else { continue };
let Some(content) = self.element(index).map(Artboard::as_graphic_table) else { continue };
let (location, dimensions, background, clip) = read_artboard_attributes(self, index);
let [a, b] = [location, location + dimensions];
@@ -614,7 +613,7 @@ impl Render for Table<Table<Graphic>> {
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 Some(content) = self.element(index).map(Artboard::as_graphic_table) 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);