Make the data model use Item and List types universally, with nodes authored as rank-polymorphic kernels (#4335)

* Add rank polymorphism node audit classifying all 271 nodes

* Implement StaticType for Item<T>

* Generate Item and mapped List wire variants for nodes declaring an Item<T> primary input

* Migrate nine nodes to Item element-wise kernels, dissolving the blending trait boilerplate

* Document the Item kernel implementation and staging plan

* Route Item<Vector> through TaggedValue::TypeDefault

* Add executor integration tests covering the Item and List wire variants

* Collapse element-wise Item/List wire pairs to the List form for conversion insertion

* Migrate sixteen vector modifier nodes to Item element-wise kernels

* Migrate Sample Image, Extend Image to Bounds, and Dehaze to Item element-wise kernels

* Fix bevel_with_transform test to actually exercise the transform attribute

* Implement From<T> for Item<T>

* Register PromoteNode rank adapters wrapping bare values into Item wires

* Insert PromoteNode adapters for Item/List wire pair fields in the preprocessor

* Define a real promote node backing the PromoteNode registry identifiers

* Zip ranked Item connectors by frame slot in the mapped element-wise variant

* Register ItemToListNode singleton raise adapters

* Resolve Item wires against List connectors by inserting promotion adapters at construction

* Rank the Offset Points distance connector and prove mixed-rank resolution end-to-end

* Implement Clampable for Item and List wires with per-variant clamp bounds

* Rank the Round Corners radius connector, exercising hard bounds on a ranked wire

* Implement ApplyTransform for Item

* Add Item wire implementations to the Transform node, keeping rank-0 chains rank 0

* Detect element-wise nodes by lazy primary connectors declaring Output = Item

* Convert Transform to an Item kernel with ranked parameters, delivering the broadcast milestone

* Rename Apply Transform to Bake Transform, baking item transforms on Vector, DAffine2, and DVec2

* Promote bare wires onto Item connectors at resolution via WrapItemNode adapters

* Rank the numeric, vector, and boolean parameters across the migrated element-wise nodes

* Rank the enum, integer, and seed parameters, registering their rank adapters via a consolidated macro

* Amend the audit with the DashPattern value type resolution

* Migrate the string family to Item element-wise kernels

* Unwrap Item wires into bare legacy connectors at resolution via UnwrapItemNode adapters

* Shadow owned node parameters in bodies instead of mut in signatures

* Migrate the math family and string measure nodes to Item element-wise kernels

* Convert the comparison and clamp nodes to Item kernels, dropping unreachable &str rows

* Flat-map expander kernels returning List under the mapped variant's frame

* Migrate the expander nodes to Item kernels flat-mapping under the frame

* Remove the unused peel_list helper

* Rank the raster adjustment and blending kernels, recontextualizing shader nodes onto an Item stand-in

Migrate the 16 adjustment nodes, Mix, Color Overlay, and Gradient Map from whole-List kernels to rank-0 Item kernels, letting the macro derive the List-mapped (zip) variants. Move the Adjust and Blend per-element seams off List onto the element types (add the Raster<CPU> impls, drop the now-dead List impls).

Shader nodes keep their bodies verbatim: PerPixelAdjust re-emits the identical kernel against a transparent no_std Item stand-in, so every Item<T> connector and .element() call resolves to a zero-cost identity on the GPU while the uniform buffer stays bare repr(C). The macro peels Item off ranked uniform params, wraps the fetched texel and uniforms at the entry point, and unwraps the result. This drops the shader_node/Item incompatibility guard. Register rank adapters for the adjustment enums.

* Update the rank polymorphism roadmap for the landed shader-node and adjustments chunk

* Rename the GPU Item stand-in to ShaderItem, aliased as Item at its shader-node import sites

* Flip the vector shape generators to emit rank-0 Item<Vector>

The shape generators (Rectangle, Circle, Ellipse, Arc, Spiral, Polygon, Star, Arrow, Line, Grid, QR Code) each produced exactly one shape wrapped in a singleton List<Vector>. Emit Item<Vector> directly so they connect to the rank-0 content connector of the migrated Transform node. Downstream List consumers receive the value through the existing Item to List promotion.

Relax the element-wise validation so a `()` (generator) primary may return Item<T> without being element-wise. Adapt the Repeat on Points test, which still takes a List content connector, by raising the generator's Item output through a singleton wrapper node.

* Parse ranked Item<T> parameter defaults against the bare element type

A ranked `Item<T>` parameter's default value is a bare, unranked `T` (promoted to the wire at resolution), but the preprocessor was handed the wrapped `Item<T>` type and could not parse the literal, flooding the console with warnings and dropping the defaults. Key the field's default_type metadata off the peeled element type for concrete ranked parameters, leaving generic `Item<T>` primaries and skip_impl nodes untouched.

* Parse an element-wise primary's scalar default against the bare element type

An element-wise node's primary reports its default_type as the List wire form so an unconnected primary defaults to an empty list. But when the primary carries a scalar `#[default]` (such as Root's radicand), that literal must parse as a bare element, not a List. Key the primary's default_type off the bare element type when it has a Default value source, keeping the List form otherwise.

* Add the DashPattern value type for stroke dash sequences

Introduce a rank-0 DashPattern value type (a Vec<f64> of alternating dash and gap lengths) so a stroke's dash pattern is a single frameable value rather than a rank-1 List<f64>. Register it as an auto-generated TaggedValue variant, parse its default from a comma or space separated string, and register its rank adapters. Not yet wired into the Stroke node.

* Rank the Fill and Stroke nodes element-wise and give Stroke a DashPattern connector

Migrate Fill and Stroke to element-wise Item<V> primaries (over Vector and Graphic element types) via a new element-level VectorItemMut trait, so styling one shape yields one shape and rank is preserved instead of promoting the input to a singleton List and emitting a List. The macro derives the List-mapped variant for genuine collections.

Wire the Stroke dash sequence to the new rank-0 DashPattern value type, collapsing the old content x paint x dash cartesian and dropping the IntoF64Vec trait. Update the stroke properties dash widget, the drawing tool, and graph-operation plumbing to read and write DashPattern, and migrate legacy F64Array, F64, and String dash inputs on document open.

Assign Colors stays a whole-collection node: each element's gradient position depends on its index among all siblings, which the element frame does not expose, so it keeps its List primary and the VectorListIterMut trait.

* Register rank adapters for the ranked Stroke enum parameters

The element-wise Stroke node ranks its align, cap, and paint order parameters as Item<StrokeAlign>, Item<StrokeCap>, and Item<PaintOrder>, but those enums lacked promotion adapters, so a bare default enum value could not be promoted to its Item wire and no Stroke variant resolved ("No construct found for node"). Register their rank adapters alongside StrokeJoin.

* Display Item wires in the Data panel without a List's ID column

Add a TableItemLayout impl for Item<T> and recognize Item wire types when introspecting graph data. An Item holds a single element, so it renders as a one-row table of the element plus its attributes with no leading index column, and it labels as its element type T rather than a List's T[]. Add ItemAttributeValues::get_any for the attribute widget dispatch.

* Register MonitorNode for Item wire types so the Data panel introspects them directly

Graph introspection wraps the inspected output in a generic MonitorNode typed to the wire. Without Item<T> monitor registrations, an Item<Vector> output could only be monitored after an Item to List promotion, so the Data panel captured and displayed a List<Vector> despite the connector being Item<Vector>. Register monitors for the Item types the element-wise nodes emit, and add the matching Data panel downcast entries.

* Color and double Item/List wires and cleave layer-stack connectors in the node graph

* Route wire color and rank through hidden nodes and refresh them on type changes

* Rework the DashPattern connector conversions with element-wise promotion and an explicit reducer node

* Rank the remaining value, context, aggregation, and transform nodes onto Item<T> wires

* Back DashPattern with a List<f64> so the Data panel can introspect its lengths

* Carry a single Item<T> through varargs so the Read context nodes emit Item<T> not List<T>

* Relax rank validation for aggregation shapes, add element adapters, and match variants by fewest promotions

* Rank the remaining bare and unnecessarily-List connectors across the node catalog

* Add Graphic::None and the FillChoice paint value, making colors and gradients plain values

* Rename GradientStops to Gradient and the legacy Gradient/Fill structs to LegacyGradient/LegacyFill

* Restore generator frame-from-params ranking to the roadmap as a planned stage

* Rename the ranked-field adapter identifier from PromoteNode to FieldAdapterNode to reflect its full contract

* Unload only the wires whose displayed style changed when types update

* Peel wire rank in the editor's semantic type checks so rank-0 layers are recognized

* Restore the whole-List Transform variant so rank-1 content wires resolve again

* Register the Item wire forms for the Memoize and Context Modification infrastructure nodes

* Give every ranked connector a field adapter and add numeric cast variants for legacy wires

* Key a ranked param's type default off its Item wire form when no literal default exists

* Inherit the layer's content value when splicing a node into an empty chain

* Migrate stale List-form TypeDefault inputs to the definition's current default

* Generate the mapped wire variant only when the element-wise node has a frame source

* Let a bare wire feed a List connector via a wrap-raise adapter, costed as two rank steps

* Add a zip companion to the whole-List Transform so ranked List parameters pair per slot

* Add the Sum, Average, Minimum, Maximum, Any, and All list reducers

* Convert the measure family to element-wise Item kernels per the audit classification

* Prefer the bare element value over the Item type default so ranked params keep their widgets

* Rename GradientStopsUI to GradientUI

* Split Fill's optional transform into a _has_transform bool and a ranked _transform matrix

* Rename the migration-only OptionalDAffine2 TaggedValue to LegacyOptionalDAffine2

* Flow byte buffers as Item<Resource> instead of List<u8> across the byte nodes

* Macro-generate the list-content wire variant, retiring the hand-written Transform-zip, Area, and Centroid companions

* Let ()-primary generators take ranked params and frame over them via the mapped variant, ranking Circle's radius

* Rank the vector shape generators' params to Item, adding a rank-aware input grab to the introspection harness

* Rank the value, color, and text generator params to Item

* Rank the raster, web-request, and context-reader generator params to Item

* Fix the repeat and brush test wirings left behind by the param-ranking sweeps

* Delete the vestigial Some, Unwrap Option, and Size Of debug nodes

* Delete the Attach Attribute node, folding its role into Write Attribute

* Add the Filter and Sort list companion nodes

* Guard the removed-definition migration swap target with a test

* Add the Box Corners value type in place of the rectangle corner radius list

* Split Text to Vector's per-glyph mode into a Text to Vector Glyphs node

* Rank the Combine Channels node's channel connectors to Item

* Make Map Points an element-wise node

* Delete the deprecated Upload Texture node

* Update the implementation roadmap to reflect the landed stages

* Let monitor introspection read rank-0 wires, locking in the layer coercion promotion path

* Prefer the rank-0 default when disconnecting a rank-capable input

* Make Path Modify an element-wise node

* Wrap node paths in a NodeIdPath newtype so they flow as a single Item

* Give Item<Raster<CPU>> a default so an unconnected Brush background resolves

* Stop the Brush node from setting layer attributes its paint operation doesn't produce

* Present-gate Flatten Path's adopted layer path like its fill and stroke

* Gate carried layer attributes on static column presence, not runtime values

* Give the remaining graphic Item<T> types a default so unconnected primaries resolve

* Dispatch a ranked param's Properties widget from its rank-0 element type

* Make Extract Transform an element-wise node, restoring the Origins to Polyline body

* Rename Flatten Path to Combine Paths

* Stamp Legacy Layer Extend's adopted layer path as a readable NodeIdPath

* Drop the dead List<u8> and List<NodeId> wire rows

* Rank Flatten Graphic's Fully Flatten toggle to Item

* Update the implementation roadmap with the endgame scope

* Make Combine Paths a reducer that collapses the whole frame into one path

* Stop type-converter nodes from carrying the source's unrelated attributes

* Format the Origins to Polyline regression test

* Wrap the Brush node's trace in a BrushTrace newtype so it flows as one value

* Make Switch a framed element-wise select, bundling whole collections

* Widen and align element-type coverage across the list and graphic nodes

* Register the compiler's cache chain pair for every ranked enum and newtype wire

* Fix wire colors for Passthrough outputs, bundled lists, and bools, and widen list wires

* Represent List wire types structurally with Type::List, replacing name-parsed rank promotion

* Treat scope and data fields as environment, rank scope wires as Item, and feed the render boundary through a context vararg

* Delete the vestigial Clone debug node

* Reinstate Upload Texture as an element-wise node and fix the GPU variants' scope executor and rank adapters

* Rename Combine Paths back to Flatten Path, deferring that rename to its own PR

* Deduplicate the promotion adapter registrations into the field adapter macro

* Rank Write Attribute's value connector to Item<AttributeValueDyn>, retiring the UnwrapItem bridge

* Vertical wire styling

* Store the editor layer path attribute as a bare NodeIdPath, not an Item<NodeIdPath>

* Rank Context Modification's features connector to Item<ContextFeatures>, dropping the dead memoize row

* Rank Path Modify's modification parameter to Item<Box<VectorModification>>

* Rename the field adapter node family to input adapter

* Drop the dead bare scalar rows from Context Modification's implementations list

* Move the dynamic executor's test module into its own file

* Drop the registry's unreachable bare rows for Memoize, the cache chain, and ConvertNode

* Materialize stored TaggedValues as ranked Item wires at the source

* Remove the bare-wire promotion and adapter machinery made dead by ranked value materialization

* Plant the input adapter for List-only inputs, composing position conversion from standard rows

* Consolidate Into/Convert conversions into the input adapter umbrella and rename the rank adapter identifiers

* Fix grouped layers gaining a phantom None stack element from the FillChoice default hijacking every List<Graphic> disconnect

* Enforce ranked node inputs in the macro, rejecting bare wire declarations

* Remove the unit Context => () machinery rows, leaving () purely as the no-primary sentinel

* Add a --signatures rank-audit mode to node-docs for the ranked-wire migration

* Remove the node-docs --signatures rank-audit mode now that ranked wires are enforced

* Migrate legacy no-color values on the Black & White, Color Overlay, and Empty Image color inputs

* Rewrite the element-wise accessor wire type at the primary input, not raw index 0

* Register the cache chain for Resource wires, replacing the lone hand-written Monitor row

* Gate the remaining Raster<GPU> registry rows behind the gpu feature

* Let List<DVec2> wires erase to ListDyn for the attribute reader and element counter

* Rename Extract Element to Item at Index, Count Elements to List Length, and Omit Element to Remove at Index

* Store paint picks as plain color/gradient values, removing the FillChoice value type

* Code review restructuring

* Sort by the consumed sort_key attribute or natural element order, adding the Sort Key node

* Remove the new list-combinator and reducer nodes to defer them to a follow-up PR

* Parse Fill and Stroke color defaults through the paint wire's Graphic element

* Emit ranked implementation-row default types structurally so their element TypeIds survive to default-literal parsing

* Exempt the deliberate no-paint choice from the stale List-form TypeDefault migration

* Migrate the legacy 4-input Fill directly to the split has-transform shape

* Upgrade the demo artwork

* Fix the valid AI review findings: Item eq/hash contract, table-era no-paint migration, quantize List rows, and other smaller issues

* Remove the rank polymorphism working documents

* Hash Item attribute values directly instead of debug-formatting them, speeding up cached evaluation

* Replace the data panel's dead bare-wire downcast arms with full coverage of the ranked monitor row types

* Derive PartialEq for Item now that attributes participate in equality

* Extend the data panel's attribute dispatchers with the newly supported scalar and choice enum types

* Add List monitor rows for the framed numeric conversion outputs so inspecting them resolves, with matching data panel arms
This commit is contained in:
Keavon Chambers
2026-09-08 16:03:01 +00:00
committed by Timon
parent 296185b7fc
commit a708a54492
3257 changed files with 766342 additions and 1829 deletions
@@ -0,0 +1,34 @@
[package]
name = "graphene-application-io"
version = "0.1.0"
edition = "2024"
description = "graphene application io interface"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde", "core-types/serde", "vector-types/serde", "text-nodes/serde", "graphene-resource/serde"]
wasm = ["dep:web-sys"]
wgpu = ["dep:raster-types", "raster-types/wgpu"]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
core-types = { workspace = true }
graphene-hash = { workspace = true, features = ["derive"] }
vector-types = { workspace = true }
text-nodes = { workspace = true }
graphene-resource = { workspace = true }
# Optional local dependencies
raster-types = { workspace = true, optional = true }
# Workspace dependencies
blake3 = { workspace = true }
glam = { workspace = true }
log = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true }
web-sys = { workspace = true, optional = true }
@@ -0,0 +1,170 @@
use core_types::transform::Footprint;
use core_types::{Context, OwnedContextImpl};
use dyn_any::{DynAny, StaticType, StaticTypeSized};
use glam::DVec2;
use graphene_hash::CacheHash;
use std::fmt::Debug;
use std::hash::{Hash, Hasher};
use std::ptr::addr_of;
use std::sync::Arc;
use std::time::Duration;
use vector_types::vector::style::RenderMode;
pub use graphene_resource as resource;
#[cfg(feature = "wgpu")]
pub use raster_types::Texture;
#[cfg(not(feature = "wgpu"))]
#[derive(Debug, Clone, Hash, PartialEq, Eq, DynAny)]
pub struct Texture; // TODO: Consider removing this
pub trait ApplicationIo {
type Executor;
fn gpu_executor(&self) -> Option<&Self::Executor> {
None
}
fn gpu_executor_arc(&self) -> Option<Arc<Self::Executor>> {
None
}
fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture<'_>;
}
impl<T: ApplicationIo> ApplicationIo for &T {
type Executor = T::Executor;
fn gpu_executor(&self) -> Option<&T::Executor> {
(**self).gpu_executor()
}
fn gpu_executor_arc(&self) -> Option<Arc<T::Executor>> {
(**self).gpu_executor_arc()
}
fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture<'_> {
(**self).load_resource(hash)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum NodeGraphUpdateMessage {}
pub trait NodeGraphUpdateSender {
fn send(&self, message: NodeGraphUpdateMessage);
}
impl<T: NodeGraphUpdateSender> NodeGraphUpdateSender for std::sync::Mutex<T> {
fn send(&self, message: NodeGraphUpdateMessage) {
self.lock().as_mut().unwrap().send(message)
}
}
pub trait GetEditorPreferences {
fn max_render_region_area(&self) -> u32;
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ExportFormat {
#[default]
Svg,
Raster,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TimingInformation {
pub time: f64,
pub animation_time: Duration,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RenderConfig {
pub viewport: Footprint,
pub scale: f64,
pub time: TimingInformation,
pub pointer: DVec2,
#[cfg_attr(feature = "serde", serde(alias = "view_mode"))]
pub render_mode: RenderMode,
pub export_format: ExportFormat,
pub for_export: bool,
pub for_eyedropper: bool,
}
impl RenderConfig {
/// Wraps this render configuration as the sole vararg of a fresh context, the call argument of a compiled network's boundary node.
pub fn into_context(self) -> Context<'static> {
OwnedContextImpl::default().with_vararg(Box::new(self)).into_context()
}
}
struct Logger;
impl NodeGraphUpdateSender for Logger {
fn send(&self, message: NodeGraphUpdateMessage) {
log::warn!("dispatching message with fallback node graph update sender {message:?}");
}
}
struct DummyPreferences;
impl GetEditorPreferences for DummyPreferences {
fn max_render_region_area(&self) -> u32 {
1024 * 1024
}
}
pub struct EditorApi<Io> {
/// Gives access to APIs like resources.
pub application_io: Option<Arc<Io>>,
pub node_graph_message_sender: Box<dyn NodeGraphUpdateSender + Send + Sync>,
/// Editor preferences made available to the graph through the `PlatformEditorApi`.
pub editor_preferences: Box<dyn GetEditorPreferences + Send + Sync>,
pub runtime: core_types::runtime::RuntimeHandle,
}
impl<Io> Eq for EditorApi<Io> {}
impl<Io: Default> Default for EditorApi<Io> {
fn default() -> Self {
Self {
application_io: None,
node_graph_message_sender: Box::new(Logger),
editor_preferences: Box::new(DummyPreferences),
runtime: Default::default(),
}
}
}
impl<Io> Hash for EditorApi<Io> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.application_io.as_ref().map_or(0, |io| io as *const _ as usize).hash(state);
(self.node_graph_message_sender.as_ref() as *const dyn NodeGraphUpdateSender).hash(state);
(self.editor_preferences.as_ref() as *const dyn GetEditorPreferences).hash(state);
}
}
impl<Io> CacheHash for EditorApi<Io> {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(self, state);
}
}
impl<Io> PartialEq for EditorApi<Io> {
fn eq(&self, other: &Self) -> bool {
self.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize) == other.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize)
&& std::ptr::eq(self.node_graph_message_sender.as_ref() as *const _, other.node_graph_message_sender.as_ref() as *const _)
&& std::ptr::eq(self.editor_preferences.as_ref() as *const _, other.editor_preferences.as_ref() as *const _)
}
}
impl<T> Debug for EditorApi<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EditorApi").finish()
}
}
unsafe impl<T: StaticTypeSized> StaticType for EditorApi<T> {
type Static = EditorApi<T::Static>;
}
@@ -0,0 +1,28 @@
[package]
name = "graphene-canvas-utils"
version = "0.1.0"
edition = "2024"
description = "graphene canvas utilities"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "MIT OR Apache-2.0"
[features]
wgpu = ["dep:wgpu", "dep:wgpu-executor"]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
core-types = { workspace = true }
vector-types = { workspace = true }
text-nodes = { workspace = true }
graphene-application-io = { workspace = true }
# Workspace dependencies
web-sys = { workspace = true }
glam = { workspace = true }
serde = { workspace = true }
log = { workspace = true }
# Optional workspace dependencies
wgpu = { workspace = true, optional = true }
wgpu-executor = { workspace = true, optional = true }
@@ -0,0 +1,8 @@
//! Utilities for working with HTML canvases.
//! This library is designed to be used in a WebAssembly context.
//! It doesn't expose any functionality when compiled for non-WebAssembly targets
#[cfg(target_family = "wasm")]
mod wasm;
#[cfg(target_family = "wasm")]
pub use wasm::*;
@@ -0,0 +1,213 @@
use dyn_any::DynAny;
#[cfg(feature = "wgpu")]
use graphene_application_io::Texture;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use web_sys::js_sys::{Object, Reflect};
use web_sys::wasm_bindgen::{JsCast, JsValue};
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, window};
#[cfg(feature = "wgpu")]
use wgpu_executor::{WgpuCurrentSurfaceTexture, WgpuExecutor, WgpuSurface};
const CANVASES_OBJECT_KEY: &str = "imageCanvases";
pub type CanvasId = u64;
static CANVAS_IDS: AtomicU64 = AtomicU64::new(0);
pub trait Canvas {
fn id(&mut self) -> CanvasId;
fn context(&mut self) -> CanvasRenderingContext2d;
fn set_resolution(&mut self, resolution: glam::UVec2);
}
#[cfg(feature = "wgpu")]
pub trait CanvasSurface: Canvas {
fn present(&mut self, texture: &Texture, executor: &WgpuExecutor);
}
#[derive(Clone, DynAny)]
pub struct CanvasHandle(Option<Arc<CanvasImpl>>);
impl CanvasHandle {
pub fn new() -> Self {
Self(None)
}
fn get(&mut self) -> &CanvasImpl {
if self.0.is_none() {
self.0 = Some(Arc::new(CanvasImpl::new()));
}
self.0.as_ref().unwrap()
}
}
impl Canvas for CanvasHandle {
fn id(&mut self) -> CanvasId {
self.get().canvas_id
}
fn context(&mut self) -> CanvasRenderingContext2d {
self.get().context()
}
fn set_resolution(&mut self, resolution: glam::UVec2) {
self.get().set_resolution(resolution);
}
}
#[cfg(feature = "wgpu")]
pub struct CanvasSurfaceHandle(CanvasHandle, Option<Arc<WgpuSurface>>);
#[cfg(feature = "wgpu")]
impl CanvasSurfaceHandle {
pub fn new() -> Self {
Self(CanvasHandle::new(), None)
}
fn surface(&mut self, executor: &WgpuExecutor) -> &WgpuSurface {
if self.1.is_none() {
let canvas = self.0.get().canvas.clone();
let surface = executor
.context()
.instance
.create_surface(wgpu::SurfaceTarget::Canvas(canvas))
.expect("Failed to create surface from canvas");
self.1 = Some(Arc::new(surface));
}
self.1.as_ref().unwrap()
}
}
#[cfg(feature = "wgpu")]
impl Canvas for CanvasSurfaceHandle {
fn id(&mut self) -> CanvasId {
self.0.id()
}
fn context(&mut self) -> CanvasRenderingContext2d {
self.0.context()
}
fn set_resolution(&mut self, resolution: glam::UVec2) {
self.0.set_resolution(resolution);
}
}
#[cfg(feature = "wgpu")]
impl CanvasSurface for CanvasSurfaceHandle {
fn present(&mut self, texture: &Texture, executor: &WgpuExecutor) {
let context = executor.context();
let source_texture: &wgpu::Texture = texture.as_ref();
let surface = self.surface(executor);
// Blit the texture to the surface
let mut encoder = context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Texture to Surface Blit"),
});
let size = source_texture.size();
// Configure the surface at physical resolution (for HiDPI displays)
let surface_caps = surface.get_capabilities(&context.adapter);
surface.configure(
&context.device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
format: wgpu::TextureFormat::Rgba8Unorm,
width: size.width,
height: size.height,
present_mode: surface_caps.present_modes[0],
alpha_mode: wgpu::CompositeAlphaMode::PreMultiplied,
view_formats: vec![],
desired_maximum_frame_latency: 2,
},
);
let surface_texture = match surface.get_current_texture(&context.queue) {
WgpuCurrentSurfaceTexture::Success(t) | WgpuCurrentSurfaceTexture::Suboptimal(t) => t,
_ => panic!("Failed to get surface texture"),
};
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfoBase {
texture: source_texture,
mip_level: 0,
origin: Default::default(),
aspect: Default::default(),
},
wgpu::TexelCopyTextureInfoBase {
texture: &surface_texture.texture,
mip_level: 0,
origin: Default::default(),
aspect: Default::default(),
},
source_texture.size(),
);
surface_texture.queue.submit([encoder.finish()]);
surface_texture.present();
}
}
/// A wgpu surface backed by an HTML canvas element.
/// Holds a reference to the canvas to prevent garbage collection.
pub struct CanvasImpl {
canvas_id: u64,
canvas: HtmlCanvasElement,
}
impl CanvasImpl {
fn new() -> Self {
let document = window().expect("should have a window in this context").document().expect("window should have a document");
let canvas: HtmlCanvasElement = document.create_element("canvas").unwrap().dyn_into::<HtmlCanvasElement>().unwrap();
let canvas_id = CANVAS_IDS.fetch_add(1, Ordering::SeqCst);
// Store the canvas in the global scope so it doesn't get garbage collected
let window = window().expect("should have a window in this context");
let window_obj = Object::from(window);
let image_canvases_key = JsValue::from_str(CANVASES_OBJECT_KEY);
let mut canvases = Reflect::get(&window_obj, &image_canvases_key);
if canvases.is_err() || canvases.as_ref().map_or(false, |v| v.is_undefined() || v.is_null()) {
Reflect::set(&window_obj.clone(), &image_canvases_key, &Object::new()).unwrap();
canvases = Reflect::get(&window_obj, &image_canvases_key);
}
// Convert key and value to JsValue
let js_key = JsValue::from_str(canvas_id.to_string().as_str());
let js_value = JsValue::from(canvas.clone());
let canvases = Object::from(canvases.unwrap());
// Use Reflect API to set property
Reflect::set(&canvases, &js_key, &js_value).unwrap();
Self { canvas_id, canvas }
}
fn context(&self) -> CanvasRenderingContext2d {
self.canvas
.get_context("2d")
.expect("Failed to get 2D context from canvas")
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()
.expect("Failed to cast context to CanvasRenderingContext2d")
}
fn set_resolution(&self, resolution: glam::UVec2) {
self.canvas.set_width(resolution.x);
self.canvas.set_height(resolution.y);
}
}
impl Drop for CanvasImpl {
fn drop(&mut self) {
let canvas_id = self.canvas_id;
let window = window().expect("should have a window in this context");
let window_obj = Object::from(window);
let image_canvases_key = JsValue::from_str(CANVASES_OBJECT_KEY);
if let Ok(canvases) = Reflect::get(&window_obj, &image_canvases_key) {
let canvases = Object::from(canvases);
let js_key = JsValue::from_str(canvas_id.to_string().as_str());
Reflect::delete_property(&canvases, &js_key).unwrap();
}
}
}
// SAFETY: WASM is single-threaded, so Send/Sync are safe
unsafe impl Send for CanvasImpl {}
unsafe impl Sync for CanvasImpl {}
@@ -0,0 +1,54 @@
[package]
name = "core-types"
version = "0.1.0"
edition = "2024"
description = "Core types and traits for Graphene node system"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde"]
nightly = []
type_id_logging = []
dealloc_nodes = []
wasm = ["tsify", "wasm-bindgen", "no-std-types/wasm"]
[dependencies]
# Local dependencies
no-std-types = { workspace = true, features = ["std"] }
graphene-hash = { workspace = true, features = ["derive"] }
# Workspace dependencies
color = { workspace = true }
bitflags = { workspace = true }
bytemuck = { workspace = true }
node-macro = { workspace = true }
num-traits = { workspace = true }
rand = { workspace = true }
glam = { workspace = true }
serde_json = { workspace = true }
petgraph = { workspace = true }
rustc-hash = { workspace = true }
dyn-any = { workspace = true }
ctor = { workspace = true }
rand_chacha = { workspace = true }
image = { workspace = true }
tinyvec = { workspace = true }
parley = { workspace = true }
skrifa = { workspace = true }
kurbo = { workspace = true }
lyon_geom = { workspace = true }
log = { workspace = true }
base64 = { workspace = true }
polycool = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true }
tsify = { workspace = true, optional = true }
wasm-bindgen = { workspace = true, optional = true }
[dev-dependencies]
# Workspace dependencies
tokio = { workspace = true }
serde_json = { workspace = true }
@@ -0,0 +1,862 @@
use std::any::TypeId;
use std::cell::UnsafeCell;
use std::collections::HashMap;
use std::marker::PhantomData;
use std::mem::MaybeUninit;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
/// Handle word layout: 24 generation bits above 40 offset bits, so a 1 TiB arena is
/// addressable and generations run out after ~3 days of 60fps resets.
const OFFSET_BITS: u32 = 40;
const OFFSET_MASK: u64 = (1 << OFFSET_BITS) - 1;
const GENERATION_MASK: u64 = (1 << (64 - OFFSET_BITS)) - 1;
/// Out of the encodable range, so no handle, including `NULL`, matches it.
const PARKED_GENERATION: u64 = GENERATION_MASK + 1;
pub struct Arena {
generation: AtomicU64,
offset: AtomicUsize,
buf: Box<[UnsafeCell<MaybeUninit<u8>>]>,
drops: Mutex<Vec<DropEntry>>,
/// Set by a refused reservation and cleared by [`Arena::reset`], so a region
/// no evaluation resets can be seen to need one.
exhausted: AtomicBool,
/// Heap the parked payloads keep alive, which occupancy does not measure:
/// a park costs one pointer in the arena and owns its content outside it.
retained_heap: AtomicUsize,
/// Where [`Arena::move_park`] sent each moved park, as its offset here to
/// the header's address in the receiving arena, the moved type's key and
/// that arena's generation, so a payload two records share is moved once
/// while a mistyped sharer, a sharer naming another destination, and a
/// sharer whose destination has since flushed are all refused. Cleared by
/// [`Arena::reset`], so a forwarding holds for one generation.
forwarded: Mutex<HashMap<usize, (usize, TypeId, u64)>>,
}
impl std::fmt::Debug for Arena {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Arena").field("generation", &self.generation).field("size", &self.buf.len()).finish()
}
}
struct DropEntry {
offset: usize,
/// The parked payload's static-type key, `None` where the park was
/// allocated without one and so never moves.
type_of: Option<TypeId>,
drop_fn: unsafe fn(*mut u8),
/// The park glue's estimate of the heap this payload owns, 0 where the
/// glue cannot measure it, so the counter is a lower bound.
retained: usize,
}
/// The glue a tombstoned entry carries: its payload was moved to another arena,
/// which now owns the obligation.
///
/// # Safety
/// None: the payload is neither read nor dropped, so any pointer is accepted.
unsafe fn inert(_: *mut u8) {}
/// The entry parking `offset`, `None` where the arena holds no obligation for
/// it. Entries are pushed in reserve order, which is offset order, so the
/// search is exact; the scan covers a push order that concurrency interleaved.
fn entry_at(entries: &[DropEntry], offset: usize) -> Option<usize> {
let probe = entries.partition_point(|entry| entry.offset < offset);
match entries.get(probe).is_some_and(|entry| entry.offset == offset) {
true => Some(probe),
false => entries.iter().rposition(|entry| entry.offset == offset),
}
}
// SAFETY: disjoint regions are handed out by an atomic bump; a region is written
// only by its allocating caller before publication, and cross-thread hand-off is
// ordered by the Release/Acquire pair on the published handle word. Payloads are
// `Send + Sync` by the bound on every allocating method.
unsafe impl Sync for Arena {}
unsafe impl Send for Arena {}
impl std::panic::UnwindSafe for Arena {}
impl std::panic::RefUnwindSafe for Arena {}
/// Shared by all arenas, so a foreign handle misses like a stale one.
static NEXT_GENERATION: AtomicU64 = AtomicU64::new(1);
static LIVE_ARENAS: AtomicUsize = AtomicUsize::new(0);
/// `None` past [`GENERATION_MASK`], where a reissued generation would let an ancient
/// handle upgrade against a current arena. Recovered by `reset_generation_counter`.
fn next_generation() -> Option<u64> {
let generation = NEXT_GENERATION.fetch_add(1, Ordering::Relaxed);
(generation <= GENERATION_MASK).then_some(generation)
}
/// Rewinds the shared generation counter so previously issued values are reused.
/// Returns `false` without rewinding when an [`Arena`] is live at the time of the
/// check, which is a debugging aid rather than a guarantee.
///
/// # Safety
///
/// No [`ArenaWeak`] minted before this call may be upgraded afterwards. Dropping
/// every [`Arena`] is not sufficient, since nodes also hold handles in
/// [`ArenaCell`]s; those nodes must be dropped too. No arena may be constructed
/// concurrently either, since the live check and the rewind are separate steps.
pub unsafe fn reset_generation_counter() -> bool {
if LIVE_ARENAS.load(Ordering::Acquire) != 0 {
return false;
}
NEXT_GENERATION.store(1, Ordering::Release);
true
}
impl Arena {
pub fn new(capacity: usize) -> Option<Self> {
let generation = next_generation()?;
let buf = (0..capacity).map(|_| UnsafeCell::new(MaybeUninit::uninit())).collect();
LIVE_ARENAS.fetch_add(1, Ordering::Release);
Some(Self {
generation: AtomicU64::new(generation),
offset: AtomicUsize::new(0),
buf,
drops: Mutex::new(Vec::new()),
exhausted: AtomicBool::new(false),
retained_heap: AtomicUsize::new(0),
forwarded: Mutex::new(HashMap::new()),
})
}
/// An arena that refuses every allocation and resolves no handle, so a caller that
/// cannot fail can degrade instead of propagating exhaustion.
pub fn parked() -> Self {
LIVE_ARENAS.fetch_add(1, Ordering::Release);
Self {
generation: AtomicU64::new(PARKED_GENERATION),
offset: AtomicUsize::new(0),
buf: Box::new([]),
drops: Mutex::new(Vec::new()),
exhausted: AtomicBool::new(false),
retained_heap: AtomicUsize::new(0),
forwarded: Mutex::new(HashMap::new()),
}
}
/// Whether a reservation has been refused since the last [`Arena::reset`].
pub fn exhausted(&self) -> bool {
self.exhausted.load(Ordering::Relaxed)
}
pub fn generation(&self) -> u64 {
self.generation.load(Ordering::Acquire)
}
/// Bytes handed out since the last [`Arena::reset`], including alignment
/// padding, so a caller can flush at a boundary before a refusal.
pub fn occupancy(&self) -> usize {
self.offset.load(Ordering::Relaxed)
}
pub fn capacity(&self) -> usize {
self.buf.len()
}
/// The heap the parked payloads own, summed from the park glue's hints.
/// A lower bound: glue that cannot measure its payload contributes 0.
pub fn retained_heap(&self) -> usize {
self.retained_heap.load(Ordering::Relaxed)
}
/// Whether `ptr` addresses this arena's backbone, which is the provenance
/// question a promote asks of every parked reference.
pub fn contains(&self, ptr: *const u8) -> bool {
let base = self.base() as usize;
(ptr as usize).wrapping_sub(base) < self.buf.len()
}
fn base(&self) -> *mut u8 {
self.buf.as_ptr() as *mut u8
}
fn reserve(&self, size: usize, align: usize) -> Option<usize> {
debug_assert!(align.is_power_of_two());
let base = self.base() as usize;
let mut start = 0;
let reserved = self
.offset
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
// Alignment is computed on the absolute address; the backbone
// allocation itself has no alignment guarantee.
let addr = (base.checked_add(current)?.checked_add(align - 1)?) & !(align - 1);
start = addr - base;
let end = start.checked_add(size)?;
(end <= self.buf.len()).then_some(end)
})
.ok();
#[cfg(debug_assertions)]
{
static ARENA_TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
if *ARENA_TRACE.get_or_init(|| std::env::var_os("GRAPHENE_ARENA_DEBUG").is_some()) {
static COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let count = COUNT.fetch_add(1, Ordering::Relaxed);
if reserved.is_none() {
eprintln!("arena> EXHAUSTED after {count} allocations, wanted {size} bytes\n{}", std::backtrace::Backtrace::force_capture());
} else if count.is_multiple_of(20000) {
eprintln!("arena> {count} allocations, offset {}, this {size} bytes", self.offset.load(Ordering::Relaxed));
}
}
}
if reserved.is_none() {
self.exhausted.store(true, Ordering::Relaxed);
return None;
}
Some(start)
}
pub fn alloc<T: Send + Sync>(&self, value: T) -> Option<(&T, ArenaWeak<T>)> {
self.alloc_sized(value, 0)
}
/// [`Arena::alloc`] with the park glue's estimate of the heap `value` owns,
/// which the region's own occupancy cannot see.
pub fn alloc_sized<T: Send + Sync>(&self, value: T, retained: usize) -> Option<(&T, ArenaWeak<T>)> {
self.alloc_stamped(value, retained, None)
}
/// [`Arena::alloc_sized`] stamping the park's static-type key, which is what
/// [`Arena::move_park`] matches on, so only a park allocated here can move.
pub fn alloc_sized_keyed<T: Send + Sync + dyn_any::StaticTypeSized>(&self, value: T, retained: usize) -> Option<(&T, ArenaWeak<T>)> {
self.alloc_stamped(value, retained, Some(TypeId::of::<T::Static>()))
}
/// [`Arena::alloc_sized_keyed`] for park glue already holding the element's
/// static form, which projects the key from the element type instead of
/// from the value's own. Crate-private: a wrong key mistypes a move.
pub(crate) fn alloc_sized_as<T: Send + Sync>(&self, value: T, retained: usize, type_of: TypeId) -> Option<(&T, ArenaWeak<T>)> {
self.alloc_stamped(value, retained, Some(type_of))
}
fn alloc_stamped<T: Send + Sync>(&self, value: T, retained: usize, type_of: Option<TypeId>) -> Option<(&T, ArenaWeak<T>)> {
let offset = self.reserve(size_of::<T>(), align_of::<T>())?;
// Built before the write so an unencodable offset drops `value` here
// rather than stranding it in the arena without drop glue.
let weak = ArenaWeak::new(self.generation(), offset)?;
// SAFETY: `reserve` returned `offset`, so it is within the backbone.
let ptr = unsafe { self.base().add(offset) }.cast::<T>();
// SAFETY: freshly reserved, aligned, in-bounds, unaliased.
unsafe { ptr.write(value) };
if std::mem::needs_drop::<T>() {
/// # Safety
/// `p` must address the live `T` this entry was registered for.
unsafe fn glue<T>(p: *mut u8) {
// SAFETY: the caller's contract.
unsafe { p.cast::<T>().drop_in_place() }
}
self.drops.lock().unwrap().push(DropEntry { offset, type_of, drop_fn: glue::<T>, retained });
self.retained_heap.fetch_add(retained, Ordering::Relaxed);
}
// SAFETY: initialized above; insert-only, so no `&mut` to it can exist.
Some((unsafe { &*ptr }, weak))
}
/// Moves the payload at `src` out of this arena and into `dst`: the header's
/// own bytes are copied, its drop obligation is pushed onto `dst` with the
/// `retained` hint, and the entry here is tombstoned in place, its glue
/// swapped for [`inert`] so the reset costs nothing per entry to honour it.
/// The heap the payload owns is neither copied nor freed; ownership travels
/// with the obligation and is discharged by `dst`'s flush.
///
/// THE MOVE'S OWNERSHIP CONTRACT: the source header keeps its bytes and
/// stays readable to this generation's remaining sharers, but it owns
/// nothing, so no read of it may outlive `dst`'s flush. A payload moved
/// once is not moved again: its recorded destination is returned, so a
/// fan-out sharer's second move reaches the one header and the obligation
/// is never duplicated.
///
/// A forwarding is to one destination generation, since the returned
/// pointer is only the caller's to publish where it lands in the `dst` it
/// named: a second move to another arena, or to one that has flushed since,
/// is refused rather than answered with a foreign or freed header. The
/// refusal reads as a decline, so the caller takes its clone path.
///
/// `dst` is credited `retained`, which is what a clone of the payload would
/// have credited it, and this arena is debited what its own park recorded,
/// so neither counter reads worse than it did before the move.
///
/// The move republishes the payload at `T::Static`, which is the key both
/// [`Arena::alloc_sized_keyed`] and this stamp, so a park and its move
/// project the type exactly once each and agree by construction.
///
/// `None` where `src` is not this arena's park keyed to `T::Static`, where
/// the type is zero-sized (whose parks share an offset and so cannot be
/// told apart), or where `dst` refused the header.
///
/// # Safety
/// `src` must address a live `T`, and `T` must own all of its content: the
/// moved header may reference no storage of this arena, which is also what
/// lets the payload be republished at `T::Static`. The key settles the
/// type, so the caller owes no size or identity argument beyond those two.
pub unsafe fn move_park<T: dyn_any::StaticTypeSized>(&self, src: *const u8, dst: &Arena, retained: usize) -> Option<*const T::Static>
where
T::Static: Send + Sync,
{
(size_of::<T::Static>() != 0).then_some(())?;
let offset = (src as usize).checked_sub(self.base() as usize)?;
(offset < self.buf.len()).then_some(())?;
let type_of = TypeId::of::<T::Static>();
let mut forwarded = self.forwarded.lock().unwrap();
if let Some(&(moved, moved_type, moved_generation)) = forwarded.get(&offset) {
(moved_type == type_of).then_some(())?;
// Generations are globally unique, so the match is `dst` itself and
// its current epoch; anything else declines.
(moved_generation == dst.generation()).then_some(())?;
return Some(moved as *const T::Static);
}
let mut entries = self.drops.lock().unwrap();
let entry = entry_at(&entries, offset)?;
(entries[entry].type_of == Some(type_of)).then_some(())?;
let slot = dst.alloc_scratch::<T::Static>(1)?;
let target = slot.as_mut_ptr().cast::<T::Static>();
// SAFETY: the caller's contract on `src`, into a freshly reserved,
// aligned, unaliased slot of the same type.
unsafe { std::ptr::copy_nonoverlapping(src.cast::<T::Static>(), target, 1) };
let parked = std::mem::replace(&mut entries[entry].retained, 0);
entries[entry].drop_fn = inert;
drop(entries);
/// # Safety
/// `p` must address the live `T` this entry was registered for.
unsafe fn glue<T>(p: *mut u8) {
// SAFETY: the caller's contract.
unsafe { p.cast::<T>().drop_in_place() }
}
dst.drops.lock().unwrap().push(DropEntry {
offset: target as usize - dst.base() as usize,
type_of: Some(type_of),
drop_fn: glue::<T::Static>,
retained,
});
dst.retained_heap.fetch_add(retained, Ordering::Relaxed);
let _ = self.retained_heap.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| Some(current.saturating_sub(parked)));
forwarded.insert(offset, (target as usize, type_of, dst.generation()));
Some(target.cast_const())
}
pub fn alloc_slice_copy<T: Copy + Send + Sync>(&self, src: &[T]) -> Option<&[T]> {
let buf = self.alloc_scratch::<T>(src.len())?;
for (slot, &value) in buf.iter_mut().zip(src) {
slot.write(value);
}
// SAFETY: every lane written above from `src`.
Some(unsafe { std::slice::from_raw_parts(buf.as_ptr().cast::<T>(), src.len()) })
}
// Not drop-tracked: callers must either consume every written lane or
// restrict themselves to `Copy` payloads (leak, not UB, otherwise).
#[allow(clippy::mut_from_ref)]
pub fn alloc_scratch<T: Send + Sync>(&self, len: usize) -> Option<&mut [MaybeUninit<T>]> {
let size = size_of::<T>().checked_mul(len)?;
let offset = self.reserve(size, align_of::<T>())?;
// SAFETY: `reserve` returned `offset`, so it is within the backbone.
let ptr = unsafe { self.base().add(offset) }.cast::<MaybeUninit<T>>();
// SAFETY: exclusive region; lifetime tied to `&self`, and `reset` takes
// `&mut self`, so the slice cannot outlive the generation.
Some(unsafe { std::slice::from_raw_parts_mut(ptr, len) })
}
/// The generation-checked handle for a region this arena holds, `None` for
/// a pointer from anywhere else. The handle keeps the region's provenance,
/// so a cache stores one where it would otherwise launder an address.
///
/// The offset must sit below the bump watermark, not merely inside the
/// backbone: [`ArenaWeak::upgrade`] hands out a `&T` at it, so only bytes a
/// reservation already handed out may be minted into a handle.
pub fn handle_at(&self, ptr: *const u8) -> Option<ArenaWeak<u8>> {
let offset = (ptr as usize).checked_sub(self.base() as usize)?;
(offset < self.offset.load(Ordering::Acquire)).then_some(())?;
ArenaWeak::new(self.generation(), offset)
}
/// `false` once generations are exhausted, parking the arena on [`PARKED_GENERATION`]
/// where every handle misses and further allocation is refused.
///
/// A park [`Arena::move_park`] handed to another arena carries [`inert`]
/// glue and a zeroed hint, so it costs the loop nothing beyond the call it
/// would have made anyway.
pub fn reset(&mut self) -> bool {
let base = self.base();
let entries = std::mem::take(self.drops.get_mut().unwrap());
self.forwarded.get_mut().unwrap().clear();
self.generation.store(PARKED_GENERATION, Ordering::Release);
for entry in entries.into_iter().rev() {
// Debited before the glue runs: a panicking destructor drops the
// remaining entries unglued, so a hint subtracted afterwards would
// strand on the counter for the arena's whole life.
let retained = self.retained_heap.get_mut();
*retained = retained.saturating_sub(entry.retained);
// SAFETY: registered at alloc time; insert-only means the region was
// never overwritten within this generation.
unsafe { (entry.drop_fn)(base.add(entry.offset)) }
}
*self.offset.get_mut() = 0;
*self.exhausted.get_mut() = false;
let Some(generation) = next_generation() else { return false };
self.generation.store(generation, Ordering::Release);
true
}
}
impl Drop for Arena {
fn drop(&mut self) {
self.reset();
LIVE_ARENAS.fetch_sub(1, Ordering::Release);
}
}
pub struct ArenaWeak<T> {
word: u64,
_marker: PhantomData<fn() -> T>,
}
impl<T> Clone for ArenaWeak<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for ArenaWeak<T> {}
impl<T> ArenaWeak<T> {
pub const NULL: Self = ArenaWeak { word: 0, _marker: PhantomData };
/// `None` once either field leaves its encodable range, so an oversized or parked
/// arena refuses to hand out a handle rather than truncating it to a live address.
fn new(generation: u64, offset: usize) -> Option<Self> {
let offset = u64::try_from(offset).ok().filter(|offset| *offset <= OFFSET_MASK)?;
(generation <= GENERATION_MASK).then_some(Self {
word: (generation << OFFSET_BITS) | offset,
_marker: PhantomData,
})
}
pub fn upgrade(self, arena: &Arena) -> Option<&T> {
let generation = self.word >> OFFSET_BITS;
if generation != arena.generation() {
return None;
}
let offset = (self.word & OFFSET_MASK) as usize;
// SAFETY: same generation means the entry was fully written before its
// word was published (Release) and cannot move or be overwritten within
// a generation (insert-only); the Acquire load that produced this word
// ordered the payload writes.
Some(unsafe { &*arena.base().add(offset).cast::<T>() })
}
}
pub struct ArenaCell<T> {
word: AtomicU64,
_marker: PhantomData<fn() -> T>,
}
impl<T> Clone for ArenaCell<T> {
fn clone(&self) -> Self {
Self {
word: AtomicU64::new(self.word.load(Ordering::Acquire)),
_marker: PhantomData,
}
}
}
impl<T> std::fmt::Debug for ArenaCell<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ArenaCell").field("word", &self.word.load(Ordering::Relaxed)).finish()
}
}
impl<T> Default for ArenaCell<T> {
fn default() -> Self {
Self {
word: AtomicU64::new(0),
_marker: PhantomData,
}
}
}
impl<T> ArenaCell<T> {
pub fn new() -> Self {
Self::default()
}
pub fn load<'e>(&self, arena: &'e Arena) -> Option<&'e T> {
let weak = ArenaWeak::<T> {
word: self.word.load(Ordering::Acquire),
_marker: PhantomData,
};
weak.upgrade(arena)
}
pub fn store(&self, weak: ArenaWeak<T>) {
self.word.store(weak.word, Ordering::Release);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::PoisonError;
use std::sync::atomic::AtomicU32;
#[test]
fn alloc_upgrade_reset_miss() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
let mut arena = Arena::new(1024).unwrap();
let cell = ArenaCell::new();
let (value, weak) = arena.alloc(41u32).unwrap();
assert_eq!(*value, 41);
cell.store(weak);
assert_eq!(cell.load(&arena), Some(&41));
arena.reset();
assert_eq!(cell.load(&arena), None, "stale handle must miss");
}
#[test]
fn capacity_survives_reset() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
let mut arena = Arena::new(64 + align_of::<u32>() - 1).unwrap();
for _ in 0..10 {
for _ in 0..16 {
assert!(arena.alloc(0u32).is_some());
}
assert!(arena.alloc(0u32).is_none(), "exhausted within generation");
arena.reset();
}
}
#[test]
fn panics_leave_the_arena_coherent() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
fn assert_ref_unwind_safe<T: std::panic::RefUnwindSafe>() {}
assert_ref_unwind_safe::<Arena>();
static DROPS: AtomicU32 = AtomicU32::new(0);
struct Probe(#[allow(dead_code)] String);
impl Drop for Probe {
fn drop(&mut self) {
DROPS.fetch_add(1, Ordering::Relaxed);
}
}
let mut arena = Arena::new(1024).unwrap();
let cell = ArenaCell::new();
let result = std::panic::catch_unwind(|| {
let (_, weak) = arena.alloc(Probe("pre-panic".into())).unwrap();
cell.store(weak);
panic!("mid-eval");
});
assert!(result.is_err());
assert!(cell.load(&arena).is_some(), "the generation is still live after the caught panic");
arena.reset();
assert_eq!(DROPS.load(Ordering::Relaxed), 1, "reset reclaims pre-panic allocations");
assert!(cell.load(&arena).is_none(), "the bump kills stale handles");
assert!(arena.alloc(0u32).is_some(), "the arena stays usable");
}
#[test]
fn a_panicking_destructor_leaves_no_resolvable_handle() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
struct Bomb;
impl Drop for Bomb {
fn drop(&mut self) {
panic!("payload destructor");
}
}
let mut arena = Arena::new(1024).unwrap();
let cell = ArenaCell::new();
let (_, weak) = arena.alloc(Bomb).unwrap();
cell.store(weak);
let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| arena.reset()));
assert!(unwound.is_err(), "the panic must propagate out of reset");
assert!(cell.load(&arena).is_none(), "a half-dropped generation must resolve no handle");
}
#[test]
fn handles_mint_only_over_reserved_bytes() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
let arena = Arena::new(1024).unwrap();
let (value, _) = arena.alloc(41u32).unwrap();
let ptr = std::ptr::from_ref(value).cast::<u8>();
assert!(arena.handle_at(ptr).is_some(), "a byte the bump handed out mints a handle");
let unreserved = ptr.wrapping_add(64);
assert!(arena.contains(unreserved), "the fixture stays inside the backbone");
assert!(arena.handle_at(unreserved).is_none(), "a byte past the watermark mints nothing");
}
#[test]
fn a_forwarding_answers_only_the_destination_it_recorded() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
struct Probe(#[allow(dead_code)] String);
unsafe impl dyn_any::StaticType for Probe {
type Static = Probe;
}
let transient = Arena::new(1024).unwrap();
let mut first = Arena::new(1024).unwrap();
let second = Arena::new(1024).unwrap();
let (parked, _) = transient.alloc_sized_keyed(Probe(String::from("shared by two records")), 21).unwrap();
let src = std::ptr::from_ref(parked).cast::<u8>();
unsafe { transient.move_park::<Probe>(src, &first, 21) }.unwrap();
assert!(unsafe { transient.move_park::<Probe>(src, &second, 21) }.is_none(), "a sharer naming another destination is refused");
first.reset();
assert!(unsafe { transient.move_park::<Probe>(src, &first, 21) }.is_none(), "a flushed destination is refused too");
}
#[test]
fn a_panicking_destructor_does_not_strand_the_retained_hint() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
struct Bomb;
impl Drop for Bomb {
fn drop(&mut self) {
panic!("payload destructor");
}
}
let mut arena = Arena::new(1024).unwrap();
arena.alloc_sized(Bomb, 64).unwrap();
assert_eq!(arena.retained_heap(), 64);
let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| arena.reset()));
assert!(unwound.is_err(), "the panic must propagate out of reset");
assert_eq!(arena.retained_heap(), 0, "the hint is debited before the glue that panics");
}
/// Held by every test that perturbs [`NEXT_GENERATION`], so a swapped-out counter
/// is never observed by a concurrently constructing test.
static COUNTER_GUARD: Mutex<()> = Mutex::new(());
#[test]
fn an_exhausted_reset_parks_the_arena_and_refuses_handles() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
let mut arena = Arena::new(1024).unwrap();
let (_, weak) = arena.alloc(41u32).unwrap();
let restore = NEXT_GENERATION.swap(GENERATION_MASK + 1, Ordering::Relaxed);
assert!(!arena.reset(), "an exhausted counter must report failure");
NEXT_GENERATION.store(restore, Ordering::Relaxed);
assert_eq!(weak.upgrade(&arena), None, "a parked arena resolves no handle");
assert_eq!(ArenaWeak::<u32>::NULL.upgrade(&arena), None, "not even the null handle");
assert!(arena.alloc(0u32).is_none(), "a parked arena refuses allocation");
}
#[test]
fn the_generation_counter_rewinds_only_without_live_arenas() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
let arena = Arena::new(64).unwrap();
assert!(!unsafe { reset_generation_counter() }, "a live arena must block the rewind");
drop(arena);
}
#[test]
fn handles_do_not_upgrade_against_a_foreign_arena() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
let first = Arena::new(1024).unwrap();
let second = Arena::new(1024).unwrap();
let (_, weak) = first.alloc(41u32).unwrap();
assert_eq!(weak.upgrade(&first), Some(&41));
assert_eq!(weak.upgrade(&second), None, "a handle must not resolve against another arena");
assert_eq!(ArenaWeak::<u32>::NULL.upgrade(&second), None, "the null handle never upgrades");
}
#[test]
fn reset_drops_dependents_before_their_dependencies() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
static ORDER: Mutex<Vec<u32>> = Mutex::new(Vec::new());
struct Probe(u32);
impl Drop for Probe {
fn drop(&mut self) {
ORDER.lock().unwrap().push(self.0);
}
}
let mut arena = Arena::new(1024).unwrap();
for id in 0..3 {
arena.alloc(Probe(id)).unwrap();
}
arena.reset();
assert_eq!(*ORDER.lock().unwrap(), vec![2, 1, 0], "later allocations may borrow earlier ones, so they drop first");
}
#[test]
fn sized_parks_account_for_their_retained_heap() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
let mut arena = Arena::new(1024).unwrap();
assert_eq!(arena.retained_heap(), 0);
let owned = String::from("retained by the park");
let length = owned.len();
arena.alloc_sized(owned, length).unwrap();
assert_eq!(arena.retained_heap(), length, "the park's hint reaches the counter");
arena.alloc(String::from("unmeasured")).unwrap();
assert_eq!(arena.retained_heap(), length, "an unmeasured park contributes nothing");
arena.reset();
assert_eq!(arena.retained_heap(), 0, "the flush frees every parked payload");
}
#[test]
fn a_moved_park_carries_its_heap_and_its_obligation() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
let mut transient = Arena::new(1024).unwrap();
let mut persistent = Arena::new(1024).unwrap();
let owned = String::from("moved, never cloned");
let length = owned.len();
let heap = owned.as_ptr();
let (parked, _) = transient.alloc_sized_keyed(owned, length).unwrap();
let src = std::ptr::from_ref(parked).cast::<u8>();
let moved = unsafe { transient.move_park::<String>(src, &persistent, length) }.unwrap();
assert_eq!(unsafe { &*moved }.as_ptr(), heap, "the move copies the header and leaves the heap where it was");
assert!(persistent.contains(moved.cast()), "the header lands in the receiving arena");
assert_eq!(transient.retained_heap(), 0, "the parking arena gives the hint up");
assert_eq!(persistent.retained_heap(), length, "and the receiving arena takes it");
transient.reset();
assert_eq!(unsafe { &*moved }, "moved, never cloned", "the parking arena's reset leaves a moved payload alone");
persistent.reset();
assert_eq!(persistent.retained_heap(), 0, "the receiving flush frees it");
}
#[test]
fn a_park_two_records_share_moves_once_and_frees_once() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
static DROPS: AtomicU32 = AtomicU32::new(0);
struct Probe(#[allow(dead_code)] String);
unsafe impl dyn_any::StaticType for Probe {
type Static = Probe;
}
impl Drop for Probe {
fn drop(&mut self) {
DROPS.fetch_add(1, Ordering::Relaxed);
}
}
DROPS.store(0, Ordering::Relaxed);
let mut transient = Arena::new(1024).unwrap();
let mut persistent = Arena::new(1024).unwrap();
let (parked, _) = transient.alloc_sized_keyed(Probe(String::from("shared by two records")), 21).unwrap();
let src = std::ptr::from_ref(parked).cast::<u8>();
let first = unsafe { transient.move_park::<Probe>(src, &persistent, 21) }.unwrap();
let second = unsafe { transient.move_park::<Probe>(src, &persistent, 21) }.unwrap();
assert_eq!(first, second, "the second move forwards to the header the first wrote");
assert_eq!(persistent.retained_heap(), 21, "the hint transfers once, not once per sharer");
transient.reset();
assert_eq!(DROPS.load(Ordering::Relaxed), 0, "a tombstoned entry is skipped by its arena's reset");
persistent.reset();
assert_eq!(DROPS.load(Ordering::Relaxed), 1, "the flush that owns the obligation frees it exactly once");
}
#[test]
fn a_move_refuses_a_mistyped_park_and_its_forwarding() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
struct Owner(#[allow(dead_code)] String);
unsafe impl dyn_any::StaticType for Owner {
type Static = Owner;
}
/// Owner's layout exactly, so only the type key tells the two apart.
struct Twin(#[allow(dead_code)] String);
unsafe impl dyn_any::StaticType for Twin {
type Static = Twin;
}
let mut transient = Arena::new(1024).unwrap();
let mut persistent = Arena::new(1024).unwrap();
let (parked, _) = transient.alloc_sized_keyed(Owner(String::from("a keyed park")), 0).unwrap();
let src = std::ptr::from_ref(parked).cast::<u8>();
assert!(unsafe { transient.move_park::<Twin>(src, &persistent, 0) }.is_none(), "a park of another type of the same size is refused");
unsafe { transient.move_park::<Owner>(src, &persistent, 0) }.unwrap();
assert!(unsafe { transient.move_park::<Twin>(src, &persistent, 0) }.is_none(), "the forwarding refuses the same mistype");
let (unkeyed, _) = transient.alloc(Owner(String::from("an unkeyed park"))).unwrap();
let src = std::ptr::from_ref(unkeyed).cast::<u8>();
assert!(unsafe { transient.move_park::<Owner>(src, &persistent, 0) }.is_none(), "a park allocated without a key never moves");
transient.reset();
persistent.reset();
}
#[test]
fn the_forwarding_map_lives_one_generation() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
static DROPS: AtomicU32 = AtomicU32::new(0);
struct Probe(#[allow(dead_code)] String);
unsafe impl dyn_any::StaticType for Probe {
type Static = Probe;
}
impl Drop for Probe {
fn drop(&mut self) {
DROPS.fetch_add(1, Ordering::Relaxed);
}
}
DROPS.store(0, Ordering::Relaxed);
let mut transient = Arena::new(1024).unwrap();
let mut persistent = Arena::new(1024).unwrap();
let (parked, _) = transient.alloc_sized_keyed(Probe(String::from("first generation")), 16).unwrap();
let src = std::ptr::from_ref(parked).cast::<u8>();
unsafe { transient.move_park::<Probe>(src, &persistent, 16) }.unwrap();
assert_eq!(transient.forwarded.lock().unwrap().len(), 1, "the move tombstoned the entry it left");
transient.reset();
assert!(transient.forwarded.lock().unwrap().is_empty(), "the tombstone set is empty after the reset");
transient.alloc_sized(Probe(String::from("second generation")), 16).unwrap();
transient.reset();
assert_eq!(DROPS.load(Ordering::Relaxed), 1, "a fresh park at a tombstoned offset drops normally");
persistent.reset();
assert_eq!(DROPS.load(Ordering::Relaxed), 2);
}
#[test]
fn occupancy_tracks_the_bump_and_clears_on_reset() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
let mut arena = Arena::new(1024).unwrap();
assert_eq!(arena.occupancy(), 0);
assert_eq!(arena.capacity(), 1024);
arena.alloc(0u64).unwrap();
assert_eq!(arena.occupancy(), 8);
arena.reset();
assert_eq!(arena.occupancy(), 0);
}
#[test]
fn containment_answers_only_for_this_arena() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
let first = Arena::new(1024).unwrap();
let second = Arena::new(1024).unwrap();
let (value, _) = first.alloc(41u32).unwrap();
let ptr = std::ptr::from_ref(value).cast::<u8>();
assert!(first.contains(ptr));
assert!(!second.contains(ptr));
assert!(!first.contains(std::ptr::null()));
let heap = Box::new(7u32);
assert!(!first.contains(std::ptr::from_ref(&*heap).cast::<u8>()), "heap the arena does not back is outside it");
}
#[test]
fn drop_glue_runs_on_reset() {
let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
static DROPS: AtomicU32 = AtomicU32::new(0);
struct Probe(#[allow(dead_code)] String);
impl Drop for Probe {
fn drop(&mut self) {
DROPS.fetch_add(1, Ordering::Relaxed);
}
}
let mut arena = Arena::new(1024).unwrap();
arena.alloc(Probe("owns heap".into())).unwrap();
arena.alloc(Probe("me too".into())).unwrap();
assert_eq!(DROPS.load(Ordering::Relaxed), 0);
arena.reset();
assert_eq!(DROPS.load(Ordering::Relaxed), 2);
}
}
@@ -0,0 +1,543 @@
//! Attribute markers and their census. A marker declares an attribute name
//! once, fixing its value type and its name-specific default; the census
//! collects every declaration so name resolution, defaults, and diagnostics
//! run at graph compile time. One name belongs to one marker, so a name can
//! never mean two different types.
//!
//! Values are `Copy` and pack directly into record fields. Data with drop
//! glue rides the arena instead: the marker declares a reference value
//! (`&str`), the writing kernel parks the payload in the arena, and the
//! record field carries the eval-lifetime reference.
use crate::list::AnyAttributeValue;
use glam::{DAffine2, DVec2};
use std::any::TypeId;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::marker::PhantomData;
use std::ops::Deref;
use std::sync::{LazyLock, Mutex};
/// Declares an attribute name: one marker per name, fixing the value type and
/// the name-specific default. Declare markers through the [`attribute!`]
/// macro, which also registers them into the [`ATTRIBUTE_REGISTRY`] and emits
/// an impl meeting the obligations below.
///
/// # Safety
///
/// [`REPARK`](Self::REPARK) must be `Some` for every marker whose
/// [`Value<'e>`](Self::Value) can carry a borrow shorter than `'static`. The
/// plain-value arm of the census writer retypes the value [`from_stored`] hands
/// it from the stored form's lifetime to the field's, which is only a relabel
/// where the value borrows nothing; a re-parking marker instead writes a
/// reference into the arena it is given.
///
/// [`from_stored`](Self::from_stored) must accept exactly the erased form
/// [`read_erased`](Self::read_erased) produces, and [`read_erased`] must read
/// its `ptr` as this marker's own `Value`. The two are each other's inverse
/// across every persistence seam, so a marker that reads one type and stores
/// another writes a value of the wrong type into the field.
pub unsafe trait Attribute: 'static {
/// The name as it appears in documents and diagnostics.
const NAME: &'static str;
/// The value type every read and write of this name shares. The lifetime
/// is the evaluation the value flows in; non-reference values ignore it.
/// The value outlives that evaluation, so its `'static` instantiation is
/// the one the census registers and layouts stamp their type id from.
///
/// `Send + Sync` because a record's bytes are these values: `RecordValue`
/// is `Send + Sync` over whatever the field writes put there, and the write
/// path never consults the census, so the bound has to sit here.
type Value<'e>: Copy + Default + std::fmt::Debug + Send + Sync + 'e;
/// The name-specific default, filled where an item lacks the attribute.
/// Producing a value for any `'e` from no inputs, reference defaults can
/// only point at `'static` data, which is what lets the census fill them
/// as plain bytes.
fn default<'e>() -> Self::Value<'e> {
Default::default()
}
/// Borrows the value out of legacy list storage, whose stored form is the
/// owned clone [`Self::read_erased`] produces. `None` where the column is
/// absent or holds another type.
fn from_stored<'a>(stored: &'a dyn std::any::Any) -> Option<Self::Value<'a>>;
/// # Safety
/// `ptr` must point at a live field of this marker's value type.
unsafe fn read_erased(ptr: *const u8) -> Box<dyn AnyAttributeValue>;
/// Re-parks the owned clone [`Self::read_erased`] produced into fresh
/// field storage; `None` for plain values, which ride the byte copy.
const REPARK: Option<crate::list::ReparkFn> = None;
}
/// A kernel-facing attribute value. A parameter `Attr<A>` is a read of `A`
/// (yielding the declared default where the attribute is absent upstream), an
/// `Attr<A>` in the return tuple is a write, and the same marker on both
/// sides is a modify.
pub struct Attr<'e, A: Attribute>(pub A::Value<'e>);
impl<'e, A: Attribute> Deref for Attr<'e, A> {
type Target = A::Value<'e>;
fn deref(&self) -> &A::Value<'e> {
&self.0
}
}
impl<'e, A: Attribute> Clone for Attr<'e, A> {
fn clone(&self) -> Self {
*self
}
}
impl<'e, A: Attribute> Copy for Attr<'e, A> {}
impl<'e, A: Attribute> std::fmt::Debug for Attr<'e, A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple(A::NAME).field(&self.0).finish()
}
}
/// An attribute write that outlives the evaluation producing it: an async
/// source's slot persists across generations, so a reference value cannot
/// cross as itself. The value crosses deep-copied through the field glue and
/// parks into the serving arena at every lift, which is why the copy is paid
/// once per invocation rather than once per evaluation.
pub struct OwnedAttr<A: Attribute>(Box<dyn AnyAttributeValue>, PhantomData<fn() -> A>);
impl<A: Attribute> OwnedAttr<A> {
/// Deep-copies `value` out of the evaluation that produced it.
pub fn new(value: A::Value<'_>) -> Self {
// SAFETY: the read addresses a live local of the marker's value type.
let erased = unsafe { A::read_erased((&raw const value).cast()) };
OwnedAttr(crate::record::deepen_field_value(erased), PhantomData)
}
/// Parks the copy into `arena` for one evaluation; `None` reports arena
/// exhaustion.
pub fn park<'e>(&self, arena: &'e crate::arena::Arena) -> Option<A::Value<'e>> {
let resident = crate::record::replay_field_value(&*self.0, arena)?;
let mut value = A::default();
// SAFETY: the slot is a live field of the marker's value type, and the
// stored value is the copy `new` took at that same type.
unsafe { write_stored::<A>(resident.as_deref().unwrap_or(&*self.0), (&raw mut value).cast(), arena) }?;
Some(value)
}
}
impl<A: Attribute> Clone for OwnedAttr<A> {
fn clone(&self) -> Self {
OwnedAttr(self.0.clone(), PhantomData)
}
}
impl<A: Attribute> std::fmt::Debug for OwnedAttr<A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple(A::NAME).field(&self.0.display_string()).finish()
}
}
/// A deletion of `A` in a node's return tuple: the name leaves the output
/// layout, so downstream reads yield the declared default again. Functionally
/// a write of the default; the value carries nothing.
pub struct RemoveAttr<A: Attribute>(PhantomData<A>);
impl<A: Attribute> RemoveAttr<A> {
pub const fn new() -> Self {
RemoveAttr(PhantomData)
}
}
impl<A: Attribute> Default for RemoveAttr<A> {
fn default() -> Self {
Self::new()
}
}
/// A census row: what is known about one declared attribute name.
#[derive(Clone, Copy, Debug)]
pub struct AttributeInfo {
pub name: &'static str,
pub value_type: TypeId,
pub value_type_name: &'static str,
pub default: fn() -> Box<dyn AnyAttributeValue>,
pub size: usize,
pub align: usize,
/// Writes the declared default's bytes into a `size`-long slice.
pub write_default_bytes: fn(&mut [u8]),
/// The marker's field form at the given level, for layouts built at runtime.
pub field_write_at: fn(u8) -> crate::record::FieldWrite,
/// Writes a legacy stored value into a field of this marker, parking
/// droppable payloads. A wrong-typed stored value leaves the field
/// untouched; `None` reports arena exhaustion.
///
/// `dst` must address a live field of *this row's* value type: the glue
/// writes its own `Value`'s worth of bytes there, so a caller resolving the
/// field by [`name`](Self::name) checks [`size`](Self::size) and
/// [`value_type`](Self::value_type) against the field first. The fields
/// here are public and the struct is `Copy`, so a row is a claim about a
/// marker rather than a proof about a field.
pub write_stored: unsafe fn(&dyn AnyAttributeValue, *mut u8, &crate::arena::Arena) -> Option<()>,
}
/// The default's object representation, staged through zeroed storage so a
/// value with padding or an unused payload (`Option<f64>`'s `None`) hands the
/// caller deterministic bytes rather than whatever the stack held.
fn write_default_bytes<A: Attribute>(out: &mut [u8]) {
assert_eq!(out.len(), size_of::<A::Value<'static>>());
let mut staged = std::mem::MaybeUninit::<A::Value<'static>>::zeroed();
staged.write(A::default());
// SAFETY: the staging holds a live value of the type `out` is sized for.
unsafe { std::ptr::copy_nonoverlapping(staged.as_ptr().cast::<u8>(), out.as_mut_ptr(), size_of::<A::Value<'static>>()) };
}
fn field_write_at<A: Attribute>(level: u8) -> crate::record::FieldWrite
where
A::Value<'static>: graphene_hash::CacheHash + PartialEq,
{
crate::record::FieldWrite::of::<A>(level)
}
/// # Safety
/// `dst` must address a live field of `A`'s value type.
unsafe fn write_stored<A: Attribute>(stored: &dyn AnyAttributeValue, dst: *mut u8, arena: &crate::arena::Arena) -> Option<()> {
if A::from_stored(stored.as_any()).is_none() {
// A wrong-typed stored value reads as absent, so the field keeps its default.
return Some(());
}
match A::REPARK {
// SAFETY: the caller's contract; the glue is this marker's own.
Some(repark) => unsafe { repark(stored, dst, arena) },
None => {
let value = A::from_stored(stored.as_any()).expect("checked above");
// SAFETY: a marker without re-park glue stores a plain value, so the bytes carry no borrowed data.
unsafe { dst.cast::<A::Value<'_>>().write(value) };
Some(())
}
}
}
/// All declared attribute names, keyed by name.
pub static ATTRIBUTE_REGISTRY: LazyLock<Mutex<HashMap<&'static str, AttributeInfo>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
/// Registers `A` into the census. Called by the [`attribute!`] expansion at
/// startup (ctor natively, a `__node_registry_attribute_*` export on wasm).
/// Re-registration at the same value type is idempotent; a second marker
/// claiming the name at a different value type panics.
pub fn register<A: Attribute>()
where
A::Value<'static>: AnyAttributeValue + graphene_hash::CacheHash + PartialEq,
{
let info = AttributeInfo {
name: A::NAME,
value_type: TypeId::of::<A::Value<'static>>(),
value_type_name: std::any::type_name::<A::Value<'static>>(),
default: || Box::new(A::default()),
size: size_of::<A::Value<'static>>(),
align: align_of::<A::Value<'static>>(),
write_default_bytes: write_default_bytes::<A>,
field_write_at: field_write_at::<A>,
write_stored: write_stored::<A>,
};
let conflict = match ATTRIBUTE_REGISTRY.lock().unwrap().entry(A::NAME) {
Entry::Vacant(vacant) => {
vacant.insert(info);
None
}
Entry::Occupied(occupied) => (occupied.get().value_type != info.value_type).then(|| occupied.get().value_type_name),
};
if let Some(existing) = conflict {
panic!("attribute `{}` is declared at two value types: {existing} and {}", A::NAME, info.value_type_name);
}
}
/// Looks up a declared name.
pub fn info(name: &str) -> Option<AttributeInfo> {
ATTRIBUTE_REGISTRY.lock().unwrap().get(name).copied()
}
/// The name-specific default for `name`, if the name is declared.
pub fn default_value(name: &str) -> Option<Box<dyn AnyAttributeValue>> {
info(name).map(|info| (info.default)())
}
/// Declares attribute markers: for each entry, the marker struct, its
/// [`Attribute`] impl, and the census registration.
///
/// ```
/// core_types::attribute! {
/// /// How visible the content is.
/// pub Opacity("opacity"): f64 = 1.;
/// /// The item's label, parked in the arena by the writer.
/// pub Label("label"): &str;
/// }
/// ```
///
/// The trailing `= expr` is the name-specific default; without it the value
/// type's `Default` applies. A `&T` value carries the eval lifetime, so its
/// default must be `'static` data. An `Option<&T>` value is an optional
/// parked reference whose default is `None`, for attributes whose absence
/// means something a present value cannot.
#[macro_export]
macro_rules! attribute {
() => {};
($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): Option<&$value:ty>; $($rest:tt)*) => {
$(#[$meta])*
$vis struct $marker;
// SAFETY: the reference-valued arms emit `REPARK`, the plain arm's value
// type cannot name `'e`, and `read_erased` and `from_stored` are emitted
// as each other's inverse.
unsafe impl $crate::attribute::Attribute for $marker {
const NAME: &'static str = $name;
type Value<'e> = ::core::option::Option<&'e $value>;
fn from_stored<'a>(stored: &'a dyn ::std::any::Any) -> ::core::option::Option<Self::Value<'a>> {
stored
.downcast_ref::<::core::option::Option<<$value as ::std::borrow::ToOwned>::Owned>>()
.map(|owned| owned.as_ref().map(::std::borrow::Borrow::borrow))
}
unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box<dyn $crate::list::AnyAttributeValue> {
::std::boxed::Box::new(unsafe { ptr.cast::<::core::option::Option<&$value>>().read() }.map(|value| <$value as ::std::borrow::ToOwned>::to_owned(value)))
}
const REPARK: ::core::option::Option<unsafe fn(&dyn $crate::list::AnyAttributeValue, *mut u8, &$crate::arena::Arena) -> ::core::option::Option<()>> = {
unsafe fn repark(value: &dyn $crate::list::AnyAttributeValue, dst: *mut u8, arena: &$crate::arena::Arena) -> ::core::option::Option<()> {
let owned: &::core::option::Option<<$value as ::std::borrow::ToOwned>::Owned> =
value.as_any().downcast_ref().expect("an optional reference attribute replays its owned clone");
let parked = match owned {
::core::option::Option::Some(owned) => {
let (parked, _) = arena.alloc(<$value as ::std::borrow::ToOwned>::to_owned(::std::borrow::Borrow::borrow(owned)))?;
::core::option::Option::Some(::std::borrow::Borrow::borrow(parked))
}
::core::option::Option::None => ::core::option::Option::None,
};
unsafe { dst.cast::<::core::option::Option<&$value>>().write(parked) };
::core::option::Option::Some(())
}
::core::option::Option::Some(repark)
};
}
$crate::attribute!(@register $marker);
$crate::attribute!($($rest)*);
};
($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): &$value:ty $(= $default:expr)?; $($rest:tt)*) => {
$(#[$meta])*
$vis struct $marker;
// SAFETY: the reference-valued arms emit `REPARK`, the plain arm's value
// type cannot name `'e`, and `read_erased` and `from_stored` are emitted
// as each other's inverse.
unsafe impl $crate::attribute::Attribute for $marker {
const NAME: &'static str = $name;
type Value<'e> = &'e $value;
$(
fn default<'e>() -> Self::Value<'e> {
$default
}
)?
fn from_stored<'a>(stored: &'a dyn ::std::any::Any) -> ::core::option::Option<Self::Value<'a>> {
stored.downcast_ref::<<$value as ::std::borrow::ToOwned>::Owned>().map(::std::borrow::Borrow::borrow)
}
unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box<dyn $crate::list::AnyAttributeValue> {
::std::boxed::Box::new(unsafe { ptr.cast::<&$value>().read() }.to_owned())
}
const REPARK: ::core::option::Option<unsafe fn(&dyn $crate::list::AnyAttributeValue, *mut u8, &$crate::arena::Arena) -> ::core::option::Option<()>> = {
unsafe fn repark(value: &dyn $crate::list::AnyAttributeValue, dst: *mut u8, arena: &$crate::arena::Arena) -> ::core::option::Option<()> {
let owned: &<$value as ::std::borrow::ToOwned>::Owned = value.as_any().downcast_ref().expect("a reference attribute replays its owned clone");
let (parked, _) = arena.alloc(<$value as ::std::borrow::ToOwned>::to_owned(::std::borrow::Borrow::borrow(owned)))?;
unsafe { dst.cast::<&$value>().write(::std::borrow::Borrow::borrow(parked)) };
::core::option::Option::Some(())
}
::core::option::Option::Some(repark)
};
}
$crate::attribute!(@register $marker);
$crate::attribute!($($rest)*);
};
($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): $value:ty $(= $default:expr)?; $($rest:tt)*) => {
$(#[$meta])*
$vis struct $marker;
// SAFETY: the reference-valued arms emit `REPARK`, the plain arm's value
// type cannot name `'e`, and `read_erased` and `from_stored` are emitted
// as each other's inverse.
unsafe impl $crate::attribute::Attribute for $marker {
const NAME: &'static str = $name;
type Value<'e> = $value;
$(
fn default<'e>() -> Self::Value<'e> {
$default
}
)?
fn from_stored<'a>(stored: &'a dyn ::std::any::Any) -> ::core::option::Option<Self::Value<'a>> {
stored.downcast_ref::<$value>().copied()
}
unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box<dyn $crate::list::AnyAttributeValue> {
::std::boxed::Box::new(unsafe { ptr.cast::<$value>().read() })
}
}
$crate::attribute!(@register $marker);
$crate::attribute!($($rest)*);
};
(@register $marker:ident) => {
const _: () = {
#[cfg(not(target_family = "wasm"))]
#[$crate::ctor::ctor]
fn register() {
$crate::attribute::register::<$marker>();
}
#[cfg(target_family = "wasm")]
#[unsafe(export_name = concat!("__node_registry_attribute_", stringify!($marker)))]
extern "C" fn register() {
$crate::attribute::register::<$marker>();
}
};
};
}
attribute! {
/// Item's `DAffine2` transformation, composed multiplicatively through nested groups.
pub Transform("transform"): DAffine2;
/// Item's `BlendMode`, controlling how it composites with content beneath it.
pub BlendMode("blend_mode"): crate::blending::BlendMode;
/// Item's opacity multiplier, composed multiplicatively through nested groups.
/// Affects content clipped to the item.
pub Opacity("opacity"): f64 = 1.;
/// Item's fill opacity multiplier. Like opacity but does not affect content clipped to the item.
pub OpacityFill("opacity_fill"): f64 = 1.;
/// Whether an item inherits the alpha of the content beneath it (clipping mask).
pub ClippingMask("clipping_mask"): bool;
/// The document node path of the editor layer owning the item.
/// Editor tools read it to route clicks and selection back to the originating layer.
pub EditorLayerPath("editor:layer_path"): &[crate::uuid::NodeId];
/// Maps the unit square `[(0, 0), (1, 1)]` (top-left convention) onto the 'Text' node's
/// text frame in this item's local space. Each item carries the frame relative to its own
/// glyph origin so it survives 'Index Elements' filtering. The Text tool reads this to
/// position its drag cage.
pub EditorTextFrame("editor:text_frame"): DAffine2;
/// Byte offset where a regex match begins ('Regex Find All' and 'Regex Capture' text nodes).
pub Start("start"): u64;
/// Byte offset where a regex match ends ('Regex Find All' and 'Regex Capture' text nodes).
pub End("end"): u64;
/// A regex named-capture-group's name, or empty for unnamed groups.
pub Name("name"): &str;
/// A JSON value's type (`"string"`, `"number"`, `"object"`, etc.) from 'JSON Query All'.
pub Type("type"): &str;
/// Artboard's top-left corner in document coordinates.
pub Location("location"): DVec2;
/// Artboard's width and height.
pub Dimensions("dimensions"): DVec2;
/// Artboard's background fill.
pub Background("background"): crate::Color;
/// Whether an artboard clips content to its bounds.
pub Clip("clip"): bool;
/// Text item's font size in document-space units.
pub FontSize("font_size"): f64 = 24.;
/// Text item's line height as a ratio of the font size.
pub LineHeight("line_height"): f64 = 1.2;
/// Text item's extra spacing between letters in document-space units.
pub LetterSpacing("letter_spacing"): f64;
/// Text item's maximum line-wrap width in document-space units.
pub MaxWidth("max_width"): Option<f64>;
/// Text item's maximum block height in document-space units, past which lines are not drawn.
pub MaxHeight("max_height"): Option<f64>;
/// Text item's faux-italic letter tilt angle in degrees.
pub LetterTilt("letter_tilt"): f64;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn census_carries_declared_names() {
let row = info("opacity").unwrap();
assert_eq!(row.value_type, TypeId::of::<f64>());
assert_eq!(info("transform").unwrap().value_type, TypeId::of::<DAffine2>());
assert_eq!(info("max_width").unwrap().value_type, TypeId::of::<Option<f64>>());
assert_eq!(info("background").unwrap().value_type, TypeId::of::<crate::Color>());
assert!(info("never_declared").is_none());
}
#[test]
fn name_specific_default_overrides_the_type_default() {
assert_eq!(<Opacity as Attribute>::default(), 1.);
assert_eq!(<Name as Attribute>::default(), "");
assert_eq!(<FontSize as Attribute>::default(), 24.);
assert_eq!(<LineHeight as Attribute>::default(), 1.2);
assert_eq!(<MaxWidth as Attribute>::default(), None);
}
#[test]
fn erased_default_downcasts_to_the_declared_type() {
let value = default_value("opacity_fill").unwrap();
assert_eq!(*value.as_any().downcast_ref::<f64>().unwrap(), 1.);
}
#[test]
fn reference_values_register_at_the_static_instantiation() {
let row = info("name").unwrap();
assert_eq!(row.value_type, TypeId::of::<&'static str>());
assert_eq!(row.size, size_of::<&str>());
}
#[test]
fn an_owned_reference_crossing_parks_into_the_serving_arena() {
let owned = OwnedAttr::<Name>::new("crossing");
let arena = crate::arena::Arena::new(1024).unwrap();
assert_eq!(owned.park(&arena).unwrap(), "crossing");
assert_eq!(owned.clone().park(&arena).unwrap(), "crossing", "the crossing parks again on every evaluation");
}
#[test]
fn an_owned_plain_crossing_rides_its_bytes() {
let arena = crate::arena::Arena::new(64).unwrap();
assert_eq!(OwnedAttr::<Opacity>::new(0.25).park(&arena).unwrap(), 0.25);
}
#[test]
fn an_exhausted_arena_refuses_an_owned_reference_crossing() {
let owned = OwnedAttr::<Name>::new("too long for this arena");
let arena = crate::arena::Arena::new(8).unwrap();
assert!(owned.park(&arena).is_none());
}
#[test]
fn reregistration_at_the_same_type_is_idempotent() {
register::<Opacity>();
register::<Opacity>();
assert_eq!(info("opacity").unwrap().value_type, TypeId::of::<f64>());
}
#[test]
#[should_panic(expected = "two value types")]
fn a_second_marker_at_a_different_type_panics() {
struct Conflict;
// SAFETY: `bool` borrows nothing, so the plain-value arm is the right one.
unsafe impl Attribute for Conflict {
const NAME: &'static str = "opacity";
type Value<'e> = bool;
fn from_stored<'a>(stored: &'a dyn std::any::Any) -> Option<Self::Value<'a>> {
stored.downcast_ref::<bool>().copied()
}
unsafe fn read_erased(ptr: *const u8) -> Box<dyn AnyAttributeValue> {
Box::new(unsafe { ptr.cast::<bool>().read() })
}
}
register::<Conflict>();
}
}
@@ -0,0 +1,113 @@
use crate::Color;
use crate::lane::{LaneColumn, LaneSource};
use glam::{DAffine2, DVec2};
#[derive(Clone, Copy, Default, Debug, PartialEq)]
pub enum RenderBoundingBox {
#[default]
None,
Infinite,
Rectangle([DVec2; 2]),
}
pub trait BoundingBox {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox;
/// Returns the bounding box to use when sizing this value's thumbnail in the Layers panel.
///
/// Diverges from `bounding_box` for types where the rendering bounds wouldn't make a useful thumbnail frame.
/// For instance, `Gradient` is `Infinite` for rendering but returns the line's AABB here, so a `List<Graphic>`
/// group of a gradient and a vector frames around the vector's geometry rather than infinity.
/// Types with no meaningful contribution (e.g., `Color`) return `Infinite` from both; the runtime substitutes a
/// small fallback rectangle at the end if no finite bounds remain after combining.
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox;
}
macro_rules! none_impl {
($t:path) => {
impl BoundingBox for $t {
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
RenderBoundingBox::None
}
fn thumbnail_bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
RenderBoundingBox::None
}
}
};
}
none_impl!(bool);
none_impl!(f32);
none_impl!(f64);
none_impl!(DVec2);
none_impl!(String);
impl BoundingBox for Color {
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
RenderBoundingBox::Infinite
}
fn thumbnail_bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
// A solid color has no intrinsic extent, so its container's other content frames the thumbnail
RenderBoundingBox::Infinite
}
}
/// Combined bounding box of a lane source's elements, composing each lane's
/// transform attribute with the given transform.
pub fn lane_bounding_box<S: LaneSource>(source: &S, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox
where
S::Element: BoundingBox,
{
let mut combined_bounds = None;
let transforms = source.column::<crate::attribute::Transform>();
for lane in 0..source.lane_count() {
let Some(element) = source.element(lane) else { continue };
let lane_transform: DAffine2 = transforms.get(lane);
match element.bounding_box(transform * lane_transform, include_stroke) {
RenderBoundingBox::None => continue,
RenderBoundingBox::Infinite => return RenderBoundingBox::Infinite,
RenderBoundingBox::Rectangle(bounds) => match combined_bounds {
Some(existing) => combined_bounds = Some(crate::math::quad::Quad::combine_bounds(existing, bounds)),
None => combined_bounds = Some(bounds),
},
}
}
match combined_bounds {
Some(bounds) => RenderBoundingBox::Rectangle(bounds),
None => RenderBoundingBox::None,
}
}
/// As [`lane_bounding_box`], but `Infinite` lanes are skipped (rather than
/// propagating outward) so a finite sibling in a mixed group dictates the
/// framing.
pub fn lane_thumbnail_bounding_box<S: LaneSource>(source: &S, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox
where
S::Element: BoundingBox,
{
let mut combined_bounds = None;
let mut any_infinite = false;
let transforms = source.column::<crate::attribute::Transform>();
for lane in 0..source.lane_count() {
let Some(element) = source.element(lane) else { continue };
let lane_transform: DAffine2 = transforms.get(lane);
match element.thumbnail_bounding_box(transform * lane_transform, include_stroke) {
RenderBoundingBox::None => continue,
RenderBoundingBox::Infinite => any_infinite = true,
RenderBoundingBox::Rectangle(bounds) => match combined_bounds {
Some(existing) => combined_bounds = Some(crate::math::quad::Quad::combine_bounds(existing, bounds)),
None => combined_bounds = Some(bounds),
},
}
}
match (combined_bounds, any_infinite) {
(Some(bounds), _) => RenderBoundingBox::Rectangle(bounds),
(None, true) => RenderBoundingBox::Infinite,
(None, false) => RenderBoundingBox::None,
}
}
@@ -0,0 +1,11 @@
use crate::Color;
// RENDERING
pub const LAYER_OUTLINE_STROKE_COLOR: Color = Color::BLACK;
pub const LAYER_OUTLINE_STROKE_WEIGHT: f64 = 0.5;
// Fonts
pub const DEFAULT_FONT_FAMILY: &str = "Lato";
pub const DEFAULT_FONT_STYLE: &str = "Regular (400)";
pub const DEFAULT_FONT_SIZE: f64 = 24.;
pub const DEFAULT_LINE_HEIGHT: f64 = 1.2;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,93 @@
//! The typed surface handed to an `extent(fn)` helper: the node's inputs in
//! declaration order, then the queried level. Values read without unsafe or
//! internal fields, upstream extents query per level, and the one blessed
//! context modification is per-copy derived promotion. Anything beyond this
//! vocabulary uses `extent_raw(fn)`, which keeps the full node/ctx/level form.
use crate::gpoll::{Extent, GPoll};
/// A wired value input; `get` evaluates the input and yields the typed element.
pub struct ValueIn<'a, T> {
read: &'a dyn Fn() -> GPoll<T>,
}
impl<'a, T> ValueIn<'a, T> {
pub fn new(read: &'a dyn Fn() -> GPoll<T>) -> Self {
Self { read }
}
pub fn get(&self) -> GPoll<T> {
(self.read)()
}
}
/// An upstream input's extents. For derived (per-copy) content the query runs
/// at the given copy's promoted context; `at` queries copy 0, the uniform
/// default. For ordinary inputs the copy is ignored.
pub struct ExtentIn<'a> {
query: &'a dyn Fn(u64, u8) -> GPoll<Extent>,
}
impl<'a> ExtentIn<'a> {
pub fn new(query: &'a dyn Fn(u64, u8) -> GPoll<Extent>) -> Self {
Self { query }
}
pub fn at(&self, level: LevelIn) -> GPoll<Extent> {
(self.query)(0, level.level)
}
pub fn at_copy(&self, copy: u64, level: LevelIn) -> GPoll<Extent> {
(self.query)(copy, level.level)
}
}
/// A ranked (`IList`) input materialized whole: `get` drives the batch and
/// yields the level as a [`List`](crate::node::List), for extents that depend
/// on the input's data rather than its counts alone. `total` answers the
/// subject's flat count without materializing, so count-shaped extents stay
/// cheap: a materializing extent inside another's subject multiplies, and
/// nested emitters turn that into a blowup.
pub struct ListIn<'a, T> {
get: &'a dyn Fn() -> GPoll<crate::node::List<'a, T>>,
total: &'a dyn Fn() -> GPoll<crate::gpoll::Extent>,
}
impl<'a, T> ListIn<'a, T> {
pub fn new(get: &'a dyn Fn() -> GPoll<crate::node::List<'a, T>>, total: &'a dyn Fn() -> GPoll<crate::gpoll::Extent>) -> Self {
Self { get, total }
}
pub fn get(&self) -> GPoll<crate::node::List<'a, T>> {
(self.get)()
}
/// The subject input's total flat extent as a plain query.
pub fn total(&self) -> GPoll<crate::gpoll::Extent> {
(self.total)()
}
}
/// The queried absolute level (innermost `0`), paired with the node's depth.
#[derive(Clone, Copy, Debug)]
pub struct LevelIn {
pub level: u8,
pub depth: u8,
}
impl LevelIn {
pub fn new(level: u8, depth: u8) -> Self {
Self { level, depth }
}
/// Whether the query targets the node's own pushed (outermost) level.
pub fn pushed(&self) -> bool {
self.level + 1 == self.depth
}
/// Whether the query targets the topmost level; `pushed` under the name a
/// non-creator (concat, remap) reads naturally.
pub fn top(&self) -> bool {
self.pushed()
}
}
@@ -0,0 +1,290 @@
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ErrorKind {
Node(&'static str),
ArenaExhausted,
Panic,
/// A lane past the end of a lower-bound (`Extent::AtLeast`) level: the
/// end-of-data signal for draining consumers, an error for everyone else.
PastEnd,
}
impl PartialEq<&str> for ErrorKind {
fn eq(&self, other: &&str) -> bool {
matches!(self, ErrorKind::Node(kind) if kind == other)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GraphError {
pub kind: ErrorKind,
pub trace: Vec<usize>,
}
impl GraphError {
pub fn new(kind: &'static str) -> Self {
Self {
kind: ErrorKind::Node(kind),
trace: Vec::new(),
}
}
pub fn traced(mut self, input_index: usize) -> Self {
self.trace.push(input_index);
self
}
pub fn past_end() -> Self {
Self {
kind: ErrorKind::PastEnd,
trace: Vec::new(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum GPoll<T> {
Pending,
Final(T),
Partial(T),
Fallback(Box<(T, GraphError)>),
Error(Box<GraphError>),
}
impl<T> GPoll<T> {
#[inline(always)]
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> GPoll<U> {
match self {
GPoll::Pending => GPoll::Pending,
GPoll::Final(value) => GPoll::Final(f(value)),
GPoll::Partial(value) => GPoll::Partial(f(value)),
GPoll::Fallback(boxed) => {
let (value, e) = *boxed;
GPoll::Fallback(Box::new((f(value), e)))
}
GPoll::Error(e) => GPoll::Error(e),
}
}
#[inline(always)]
pub fn and_then<U>(self, f: impl FnOnce(T) -> GPoll<U>) -> GPoll<U> {
match self {
GPoll::Pending => GPoll::Pending,
GPoll::Final(value) => f(value),
GPoll::Partial(value) => match f(value) {
GPoll::Final(result) => GPoll::Partial(result),
other => other,
},
GPoll::Fallback(boxed) => {
let (value, e) = *boxed;
match f(value) {
GPoll::Pending => GPoll::Pending,
GPoll::Final(result) | GPoll::Partial(result) => GPoll::Fallback(Box::new((result, e))),
GPoll::Fallback(inner) => {
let (result, _) = *inner;
GPoll::Fallback(Box::new((result, e)))
}
GPoll::Error(inner) => GPoll::Error(inner),
}
}
GPoll::Error(e) => GPoll::Error(e),
}
}
#[inline(always)]
pub fn zip<U>(self, other: GPoll<U>) -> GPoll<(T, U)> {
match (self, other) {
(GPoll::Error(e), _) | (_, GPoll::Error(e)) => GPoll::Error(e),
(GPoll::Pending, _) | (_, GPoll::Pending) => GPoll::Pending,
(GPoll::Final(a), GPoll::Final(b)) => GPoll::Final((a, b)),
(GPoll::Fallback(boxed), GPoll::Final(b) | GPoll::Partial(b)) => {
let (a, e) = *boxed;
GPoll::Fallback(Box::new(((a, b), e)))
}
(GPoll::Final(a) | GPoll::Partial(a), GPoll::Fallback(boxed)) => {
let (b, e) = *boxed;
GPoll::Fallback(Box::new(((a, b), e)))
}
(GPoll::Fallback(first), GPoll::Fallback(second)) => {
let (a, e) = *first;
let (b, _) = *second;
GPoll::Fallback(Box::new(((a, b), e)))
}
(GPoll::Partial(a), GPoll::Final(b) | GPoll::Partial(b)) | (GPoll::Final(a), GPoll::Partial(b)) => GPoll::Partial((a, b)),
}
}
#[inline(always)]
pub fn trace(self, input: usize) -> Self {
match self {
GPoll::Fallback(mut boxed) => {
boxed.1.trace.push(input);
GPoll::Fallback(boxed)
}
GPoll::Error(mut e) => {
e.trace.push(input);
GPoll::Error(e)
}
other => other,
}
}
pub fn fallback(value: T, kind: &'static str) -> Self {
GPoll::Fallback(Box::new((value, GraphError::new(kind))))
}
pub fn error(kind: &'static str) -> Self {
GPoll::Error(Box::new(GraphError::new(kind)))
}
pub fn arena_exhausted() -> Self {
GPoll::Error(Box::new(GraphError {
kind: ErrorKind::ArenaExhausted,
trace: Vec::new(),
}))
}
pub fn panicked() -> Self {
GPoll::Error(Box::new(GraphError {
kind: ErrorKind::Panic,
trace: Vec::new(),
}))
}
pub fn past_end() -> Self {
GPoll::Error(Box::new(GraphError::past_end()))
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Interrupt {
Pending,
Error(Box<GraphError>),
}
impl From<GraphError> for Interrupt {
fn from(error: GraphError) -> Self {
Interrupt::Error(Box::new(error))
}
}
impl<T> From<Interrupt> for GPoll<T> {
fn from(interrupt: Interrupt) -> Self {
match interrupt {
Interrupt::Pending => GPoll::Pending,
Interrupt::Error(e) => GPoll::Error(e),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Extent {
Free,
Exactly(usize),
/// A sound lower bound; the true count is discoverable only by draining.
AtLeast(usize),
}
impl Extent {
pub fn meet(a: GPoll<Extent>, b: GPoll<Extent>) -> GPoll<Extent> {
a.zip(b).and_then(|(a, b)| match (a, b) {
(Extent::Free, other) | (other, Extent::Free) => GPoll::Final(other),
(Extent::Exactly(n), Extent::Exactly(m)) if n == m => GPoll::Final(Extent::Exactly(n)),
(Extent::AtLeast(a), Extent::AtLeast(b)) => GPoll::Final(Extent::AtLeast(a.max(b))),
(Extent::AtLeast(bound), Extent::Exactly(n)) | (Extent::Exactly(n), Extent::AtLeast(bound)) if n >= bound => GPoll::Final(Extent::Exactly(n)),
(Extent::AtLeast(_), Extent::Exactly(n)) | (Extent::Exactly(n), Extent::AtLeast(_)) => GPoll::fallback(Extent::Exactly(n), "extent mismatch"),
(Extent::Exactly(n), Extent::Exactly(m)) => GPoll::fallback(Extent::Exactly(n.min(m)), "extent mismatch"),
})
}
/// The product of two extents, used to compose nested-level counts; an
/// unbounded operand leaves the product unbounded, a lower-bound operand
/// keeps the product a lower bound.
pub fn mul(a: GPoll<Extent>, b: GPoll<Extent>) -> GPoll<Extent> {
a.zip(b).map(|(a, b)| match (a, b) {
(Extent::Free, _) | (_, Extent::Free) => Extent::Free,
(Extent::Exactly(n), Extent::Exactly(m)) => Extent::Exactly(n * m),
(Extent::AtLeast(n) | Extent::Exactly(n), Extent::AtLeast(m) | Extent::Exactly(m)) => Extent::AtLeast(n * m),
})
}
/// The sum of two extents, used to concatenate a level; a free operand
/// counts as one lane, so a scalar input joins a concat as a single item,
/// and a lower-bound operand keeps the sum a lower bound.
pub fn sum(a: GPoll<Extent>, b: GPoll<Extent>) -> GPoll<Extent> {
let lanes = |extent| match extent {
Extent::Exactly(count) | Extent::AtLeast(count) => count,
Extent::Free => 1,
};
a.zip(b).map(|(a, b)| match (a, b) {
(Extent::AtLeast(_), _) | (_, Extent::AtLeast(_)) => Extent::AtLeast(lanes(a) + lanes(b)),
_ => Extent::Exactly(lanes(a) + lanes(b)),
})
}
}
/// A query over a node's nesting levels: one level, the product below or above
/// it, or the whole domain. The composite [`Node::extent`](crate::node::Node::extent)
/// derives these from the per-level [`extent_at`](crate::node::Node::extent_at).
#[derive(Clone, Copy, Debug)]
pub enum Level {
At(u8),
Below(u8),
Above(u8),
Total,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Finality {
AllFinal,
Partial,
}
impl Finality {
pub fn meet(self, other: Finality) -> Finality {
match (self, other) {
(Finality::AllFinal, Finality::AllFinal) => Finality::AllFinal,
_ => Finality::Partial,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn free_is_the_meet_identity() {
let meet = Extent::meet(GPoll::Final(Extent::Free), GPoll::Final(Extent::Exactly(4)));
assert_eq!(meet, GPoll::Final(Extent::Exactly(4)));
}
#[test]
fn extent_mismatch_truncates_and_reports() {
let meet = Extent::meet(GPoll::Final(Extent::Exactly(3)), GPoll::Final(Extent::Exactly(5)));
let GPoll::Fallback(boxed) = meet else {
panic!("expected fallback, got {meet:?}");
};
assert_eq!(boxed.0, Extent::Exactly(3));
assert!(boxed.1.kind == "extent mismatch");
}
#[test]
fn error_dominates_pending_in_zip() {
let zipped = GPoll::<u32>::error("boom").zip(GPoll::<u32>::Pending);
assert!(matches!(zipped, GPoll::Error(_)));
}
#[test]
fn trace_builds_root_to_source_path() {
let poll = GPoll::<u32>::error("boom").trace(2).trace(0);
let GPoll::Error(e) = poll else { unreachable!() };
assert_eq!(e.trace, vec![2, 0]);
}
#[test]
fn interrupt_round_trips_to_gpoll() {
assert_eq!(GPoll::<u32>::from(Interrupt::Pending), GPoll::Pending);
let interrupt = Interrupt::from(GraphError::new("boom"));
assert!(matches!(GPoll::<u32>::from(interrupt), GPoll::Error(e) if e.kind == "boom"));
}
}
@@ -0,0 +1,177 @@
//! The read surface over a source of lanes, so readers name census markers
//! instead of a storage shape.
use crate::attribute::Attribute;
/// One marker's column on a source, resolved once so lane reads skip the key
/// lookup.
pub trait LaneColumn<'a, A: Attribute> {
/// The lane's value, `None` where the source carries no such column.
fn try_get(&self, lane: usize) -> Option<A::Value<'a>>;
/// The lane's value, falling back to the marker's census default.
fn get(&self, lane: usize) -> A::Value<'a> {
self.try_get(lane).unwrap_or_else(A::default)
}
}
/// A source of lanes carrying an element and census attributes.
pub trait LaneSource {
type Element;
type Column<'a, A: Attribute>: LaneColumn<'a, A>
where
Self: 'a;
fn lane_count(&self) -> usize;
fn element(&self, lane: usize) -> Option<&Self::Element>;
fn column<A: Attribute>(&self) -> Self::Column<'_, A>;
fn attr<A: Attribute>(&self, lane: usize) -> A::Value<'_> {
self.column::<A>().get(lane)
}
/// Distinguishes an absent column from one holding the census default.
fn try_attr<A: Attribute>(&self, lane: usize) -> Option<A::Value<'_>> {
self.column::<A>().try_get(lane)
}
}
/// A bare element as a one-lane source: no columns, so every marker reads its
/// census default. The read surface for a de-tabled leaf, whose attributes
/// ride the containing lane.
pub struct Single<'a, T>(pub &'a T);
/// The column of a [`Single`]: always absent, so reads fall to the census
/// default.
pub struct NoColumn;
impl<'a, A: Attribute> LaneColumn<'a, A> for NoColumn {
fn try_get(&self, _lane: usize) -> Option<A::Value<'a>> {
None
}
}
impl<T> LaneSource for Single<'_, T> {
type Element = T;
type Column<'a, A: Attribute>
= NoColumn
where
Self: 'a;
fn lane_count(&self) -> usize {
1
}
fn element(&self, lane: usize) -> Option<&T> {
(lane == 0).then_some(self.0)
}
fn column<A: Attribute>(&self) -> NoColumn {
NoColumn
}
}
impl<T: crate::bounds::BoundingBox> crate::bounds::BoundingBox for Single<'_, T> {
fn bounding_box(&self, transform: glam::DAffine2, include_stroke: bool) -> crate::bounds::RenderBoundingBox {
self.0.bounding_box(transform, include_stroke)
}
fn thumbnail_bounding_box(&self, transform: glam::DAffine2, include_stroke: bool) -> crate::bounds::RenderBoundingBox {
self.0.thumbnail_bounding_box(transform, include_stroke)
}
}
/// One lane of a source re-based as a one-lane source of a leaf element: the
/// de-tabled leaf read with its containing lane's attributes.
pub struct LeafLane<'a, S, T> {
source: &'a S,
index: usize,
element: &'a T,
}
impl<'a, S, T> LeafLane<'a, S, T> {
pub fn new(source: &'a S, index: usize, element: &'a T) -> Self {
Self { source, index, element }
}
}
pub struct LaneColumnAt<'a, S: LaneSource + 'a, A: Attribute> {
inner: S::Column<'a, A>,
index: usize,
}
impl<'a, S: LaneSource, A: Attribute> LaneColumn<'a, A> for LaneColumnAt<'a, S, A> {
fn try_get(&self, lane: usize) -> Option<A::Value<'a>> {
(lane == 0).then(|| self.inner.try_get(self.index)).flatten()
}
}
impl<S: LaneSource, T> LaneSource for LeafLane<'_, S, T> {
type Element = T;
type Column<'a, A: Attribute>
= LaneColumnAt<'a, S, A>
where
Self: 'a;
fn lane_count(&self) -> usize {
1
}
fn element(&self, lane: usize) -> Option<&T> {
(lane == 0).then_some(self.element)
}
fn column<A: Attribute>(&self) -> LaneColumnAt<'_, S, A> {
LaneColumnAt {
inner: self.source.column::<A>(),
index: self.index,
}
}
}
#[cfg(test)]
mod tests {
use super::LaneSource;
use crate::attribute::{EditorLayerPath, Opacity, Transform};
use crate::list::List;
use crate::uuid::NodeId;
use glam::DAffine2;
#[test]
fn a_plain_marker_reads_what_the_legacy_column_stores() {
let mut list = List::new_from_element(1u32);
let transform = DAffine2::from_translation((3., 4.).into());
list.set_attribute(crate::ATTR_TRANSFORM, 0, transform);
assert_eq!(list.attr::<Transform>(0), transform);
}
#[test]
fn an_absent_marker_reads_its_census_default_not_the_value_default() {
let list = List::new_from_element(1u32);
// `Opacity` declares `= 1.`, so the census default must win over `f64::default()`.
assert_eq!(list.attr::<Opacity>(0), 1.);
assert_eq!(list.attr::<Transform>(0), DAffine2::IDENTITY);
}
#[test]
fn a_reference_marker_borrows_the_stored_owned_form() {
let mut list = List::new_from_element(1u32);
let path = vec![NodeId(7), NodeId(9)];
list.set_attribute(crate::ATTR_EDITOR_LAYER_PATH, 0, path.clone());
assert_eq!(list.attr::<EditorLayerPath>(0), path.as_slice());
assert!(List::new_from_element(1u32).attr::<EditorLayerPath>(0).is_empty());
}
#[test]
fn a_column_of_the_wrong_stored_type_reads_as_absent() {
let mut list = List::new_from_element(1u32);
list.set_attribute(crate::ATTR_OPACITY, 0, "not an f64".to_string());
assert_eq!(list.attr::<Opacity>(0), 1.);
}
}
@@ -0,0 +1,72 @@
extern crate log;
pub mod arena;
pub mod attribute;
pub mod bounds;
pub mod consts;
pub mod context;
pub mod extent;
pub mod gpoll;
pub mod lane;
pub mod list;
pub mod math;
pub mod memo;
pub mod misc;
pub mod node;
pub mod ops;
pub mod record;
pub mod registry;
pub mod render_complexity;
pub mod runtime;
pub mod transform;
pub mod uuid;
pub mod value;
pub use crate as core_types;
pub use blending::*;
pub use color::Color;
pub use context::*;
pub use ctor;
pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync};
pub use graphene_hash;
pub use graphene_hash::CacheHash;
pub use list::{
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_TEXT_FRAME, ATTR_END, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT,
ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE,
};
pub use memo::MemoHash;
pub use no_std_types::AsU32;
pub use no_std_types::blending;
pub use no_std_types::choice_type;
pub use no_std_types::color;
pub use no_std_types::shaders;
pub use node::Node;
pub use num_traits;
#[cfg(feature = "wasm")]
pub use tsify;
pub use types::Cow;
mod types;
pub use types::*;
pub trait InputAccessorSource<'a, T>: InputAccessorSourceIdentifier + std::fmt::Debug {
fn get_input(&'a self, index: usize) -> Option<&'a T>;
fn set_input(&'a mut self, index: usize, value: T);
}
pub trait InputAccessorSourceIdentifier {
fn has_identifier(&self, identifier: &str) -> bool;
}
pub trait InputAccessor<'n, Source: 'n>
where
Self: Sized,
{
fn new_with_source(source: &'n Source) -> Option<Self>;
}
pub trait NodeInputDecleration {
const INDEX: usize;
fn identifier() -> ProtoNodeIdentifier;
type Result;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,109 @@
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
#[derive(Clone, Debug, DynAny)]
pub struct AxisAlignedBbox {
pub start: DVec2,
pub end: DVec2,
}
impl AxisAlignedBbox {
pub const ZERO: Self = Self { start: DVec2::ZERO, end: DVec2::ZERO };
pub const ONE: Self = Self { start: DVec2::ZERO, end: DVec2::ONE };
pub fn size(&self) -> DVec2 {
self.end - self.start
}
pub fn to_transform(&self) -> DAffine2 {
DAffine2::from_translation(self.start) * DAffine2::from_scale(self.size())
}
pub fn contains(&self, point: DVec2) -> bool {
point.x >= self.start.x && point.x <= self.end.x && point.y >= self.start.y && point.y <= self.end.y
}
pub fn intersects(&self, other: &AxisAlignedBbox) -> bool {
other.start.x <= self.end.x && other.end.x >= self.start.x && other.start.y <= self.end.y && other.end.y >= self.start.y
}
pub fn union(&self, other: &AxisAlignedBbox) -> AxisAlignedBbox {
AxisAlignedBbox {
start: DVec2::new(self.start.x.min(other.start.x), self.start.y.min(other.start.y)),
end: DVec2::new(self.end.x.max(other.end.x), self.end.y.max(other.end.y)),
}
}
pub fn union_non_empty(&self, other: &AxisAlignedBbox) -> Option<AxisAlignedBbox> {
match (self.size() == DVec2::ZERO, other.size() == DVec2::ZERO) {
(true, true) => None,
(true, _) => Some(other.clone()),
(_, true) => Some(self.clone()),
_ => Some(AxisAlignedBbox {
start: DVec2::new(self.start.x.min(other.start.x), self.start.y.min(other.start.y)),
end: DVec2::new(self.end.x.max(other.end.x), self.end.y.max(other.end.y)),
}),
}
}
pub fn intersect(&self, other: &AxisAlignedBbox) -> AxisAlignedBbox {
AxisAlignedBbox {
start: DVec2::new(self.start.x.max(other.start.x), self.start.y.max(other.start.y)),
end: DVec2::new(self.end.x.min(other.end.x), self.end.y.min(other.end.y)),
}
}
}
impl From<(DVec2, DVec2)> for AxisAlignedBbox {
fn from((start, end): (DVec2, DVec2)) -> Self {
Self { start, end }
}
}
#[derive(Clone, Debug)]
pub struct Bbox {
pub top_left: DVec2,
pub top_right: DVec2,
pub bottom_left: DVec2,
pub bottom_right: DVec2,
}
impl Bbox {
pub fn unit() -> Self {
Self {
top_left: DVec2::new(0., 1.),
top_right: DVec2::new(1., 1.),
bottom_left: DVec2::new(0., 0.),
bottom_right: DVec2::new(1., 0.),
}
}
pub fn from_transform(transform: DAffine2) -> Self {
Self {
top_left: transform.transform_point2(DVec2::new(0., 1.)),
top_right: transform.transform_point2(DVec2::new(1., 1.)),
bottom_left: transform.transform_point2(DVec2::new(0., 0.)),
bottom_right: transform.transform_point2(DVec2::new(1., 0.)),
}
}
pub fn affine_transform(self, transform: DAffine2) -> Self {
Self {
top_left: transform.transform_point2(self.top_left),
top_right: transform.transform_point2(self.top_right),
bottom_left: transform.transform_point2(self.bottom_left),
bottom_right: transform.transform_point2(self.bottom_right),
}
}
pub fn to_axis_aligned_bbox(&self) -> AxisAlignedBbox {
let start_x = self.top_left.x.min(self.top_right.x).min(self.bottom_left.x).min(self.bottom_right.x);
let start_y = self.top_left.y.min(self.top_right.y).min(self.bottom_left.y).min(self.bottom_right.y);
let end_x = self.top_left.x.max(self.top_right.x).max(self.bottom_left.x).max(self.bottom_right.x);
let end_y = self.top_left.y.max(self.top_right.y).max(self.bottom_left.y).max(self.bottom_right.y);
AxisAlignedBbox {
start: DVec2::new(start_x, start_y),
end: DVec2::new(end_x, end_y),
}
}
}
@@ -0,0 +1,4 @@
pub mod bbox;
pub mod polynomial;
pub mod quad;
pub mod rect;
@@ -0,0 +1,292 @@
use kurbo::PathSeg;
use std::fmt::{self, Display, Formatter};
use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign};
/// A struct that represents a polynomial with a maximum degree of `N-1`.
///
/// It provides basic mathematical operations for polynomials like addition, multiplication, differentiation, integration, etc.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Polynomial<const N: usize> {
coefficients: [f64; N],
}
impl<const N: usize> Polynomial<N> {
/// Create a new polynomial from the coefficients given in the array.
///
/// The coefficient for nth degree is at the nth index in array. Therefore the order of coefficients are reversed than the usual order for writing polynomials mathematically.
pub fn new(coefficients: [f64; N]) -> Polynomial<N> {
Polynomial { coefficients }
}
/// Create a polynomial where all its coefficients are zero.
pub fn zero() -> Polynomial<N> {
Polynomial { coefficients: [0.; N] }
}
/// Return an immutable reference to the coefficients.
///
/// The coefficient for nth degree is at the nth index in array. Therefore the order of coefficients are reversed than the usual order for writing polynomials mathematically.
pub fn coefficients(&self) -> &[f64; N] {
&self.coefficients
}
/// Return a mutable reference to the coefficients.
///
/// The coefficient for nth degree is at the nth index in array. Therefore the order of coefficients are reversed than the usual order for writing polynomials mathematically.
pub fn coefficients_mut(&mut self) -> &mut [f64; N] {
&mut self.coefficients
}
/// Evaluate the polynomial at `value`.
pub fn eval(&self, value: f64) -> f64 {
self.coefficients.iter().rev().copied().reduce(|acc, x| acc * value + x).unwrap()
}
/// Return the same polynomial but with a different maximum degree of `M-1`.\
///
/// Returns `None` if the polynomial cannot fit in the specified size.
pub fn as_size<const M: usize>(&self) -> Option<Polynomial<M>> {
let mut coefficients = [0.; M];
if M >= N {
coefficients[..N].copy_from_slice(&self.coefficients);
} else if self.coefficients.iter().rev().take(N - M).all(|&x| x == 0.) {
coefficients.copy_from_slice(&self.coefficients[..M])
} else {
return None;
}
Some(Polynomial { coefficients })
}
/// Computes the derivative in place.
pub fn derivative_mut(&mut self) {
self.coefficients.iter_mut().enumerate().for_each(|(index, x)| *x *= index as f64);
self.coefficients.rotate_left(1);
}
/// Computes the antiderivative at `C = 0` in place.
///
/// Returns `None` if the polynomial is not big enough to accommodate the extra degree.
pub fn antiderivative_mut(&mut self) -> Option<()> {
if self.coefficients[N - 1] != 0. {
return None;
}
self.coefficients.rotate_right(1);
self.coefficients.iter_mut().enumerate().skip(1).for_each(|(index, x)| *x /= index as f64);
Some(())
}
/// Computes the polynomial's derivative.
pub fn derivative(&self) -> Polynomial<N> {
let mut ans = *self;
ans.derivative_mut();
ans
}
/// Computes the antiderivative at `C = 0`.
///
/// Returns `None` if the polynomial is not big enough to accommodate the extra degree.
pub fn antiderivative(&self) -> Option<Polynomial<N>> {
let mut ans = *self;
ans.antiderivative_mut()?;
Some(ans)
}
}
impl<const N: usize> Default for Polynomial<N> {
fn default() -> Self {
Self::zero()
}
}
impl<const N: usize> Display for Polynomial<N> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut first = true;
for (index, coefficient) in self.coefficients.iter().enumerate().rev().filter(|&(_, &coefficient)| coefficient != 0.) {
if first {
first = false;
} else {
f.write_str(" + ")?
}
coefficient.fmt(f)?;
if index == 0 {
continue;
}
f.write_str("x")?;
if index == 1 {
continue;
}
f.write_str("^")?;
index.fmt(f)?;
}
Ok(())
}
}
impl<const N: usize> AddAssign<&Polynomial<N>> for Polynomial<N> {
fn add_assign(&mut self, rhs: &Polynomial<N>) {
self.coefficients.iter_mut().zip(rhs.coefficients.iter()).for_each(|(a, b)| *a += b);
}
}
impl<const N: usize> Add for &Polynomial<N> {
type Output = Polynomial<N>;
fn add(self, other: &Polynomial<N>) -> Polynomial<N> {
let mut output = *self;
output += other;
output
}
}
impl<const N: usize> Neg for &Polynomial<N> {
type Output = Polynomial<N>;
fn neg(self) -> Polynomial<N> {
let mut output = *self;
output.coefficients.iter_mut().for_each(|x| *x = -*x);
output
}
}
impl<const N: usize> Neg for Polynomial<N> {
type Output = Polynomial<N>;
fn neg(mut self) -> Polynomial<N> {
self.coefficients.iter_mut().for_each(|x| *x = -*x);
self
}
}
impl<const N: usize> SubAssign<&Polynomial<N>> for Polynomial<N> {
fn sub_assign(&mut self, rhs: &Polynomial<N>) {
self.coefficients.iter_mut().zip(rhs.coefficients.iter()).for_each(|(a, b)| *a -= b);
}
}
impl<const N: usize> Sub for &Polynomial<N> {
type Output = Polynomial<N>;
fn sub(self, other: &Polynomial<N>) -> Polynomial<N> {
let mut output = *self;
output -= other;
output
}
}
impl<const N: usize> MulAssign<&Polynomial<N>> for Polynomial<N> {
fn mul_assign(&mut self, rhs: &Polynomial<N>) {
for i in (0..N).rev() {
self.coefficients[i] = self.coefficients[i] * rhs.coefficients[0];
for j in 0..i {
self.coefficients[i] += self.coefficients[j] * rhs.coefficients[i - j];
}
}
}
}
impl<const N: usize> Mul for &Polynomial<N> {
type Output = Polynomial<N>;
fn mul(self, other: &Polynomial<N>) -> Polynomial<N> {
let mut output = *self;
output *= other;
output
}
}
/// Returns two [`Polynomial`]s representing the parametric equations for x and y coordinates of the bezier curve respectively.
/// The domain of both the equations are from t=0.0 representing the start and t=1.0 representing the end of the bezier curve.
pub fn pathseg_to_parametric_polynomial(segment: PathSeg) -> (Polynomial<4>, Polynomial<4>) {
match segment {
PathSeg::Line(line) => {
let term1 = line.p1 - line.p0;
(Polynomial::new([line.p0.x, term1.x, 0., 0.]), Polynomial::new([line.p0.y, term1.y, 0., 0.]))
}
PathSeg::Quad(quad_bez) => {
let term1 = 2. * (quad_bez.p1 - quad_bez.p0);
let term2 = quad_bez.p0 - 2. * quad_bez.p1.to_vec2() + quad_bez.p2.to_vec2();
(Polynomial::new([quad_bez.p0.x, term1.x, term2.x, 0.]), Polynomial::new([quad_bez.p0.y, term1.y, term2.y, 0.]))
}
PathSeg::Cubic(cubic_bez) => {
let term1 = 3. * (cubic_bez.p1 - cubic_bez.p0);
let term2 = 3. * (cubic_bez.p2 - cubic_bez.p1) - term1;
let term3 = cubic_bez.p3 - cubic_bez.p0 - term2 - term1;
(
Polynomial::new([cubic_bez.p0.x, term1.x, term2.x, term3.x]),
Polynomial::new([cubic_bez.p0.y, term1.y, term2.y, term3.y]),
)
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn evaluation() {
let p = Polynomial::new([1., 2., 3.]);
assert_eq!(p.eval(1.), 6.);
assert_eq!(p.eval(2.), 17.);
}
#[test]
fn size_change() {
let p1 = Polynomial::new([1., 2., 3.]);
let p2 = Polynomial::new([1., 2., 3., 0.]);
assert_eq!(p1.as_size(), Some(p2));
assert_eq!(p2.as_size(), Some(p1));
assert_eq!(p2.as_size::<2>(), None);
}
#[test]
fn addition_and_subtaction() {
let p1 = Polynomial::new([1., 2., 3.]);
let p2 = Polynomial::new([4., 5., 6.]);
let addition = Polynomial::new([5., 7., 9.]);
let subtraction = Polynomial::new([-3., -3., -3.]);
assert_eq!(&p1 + &p2, addition);
assert_eq!(&p1 - &p2, subtraction);
}
#[test]
fn multiplication() {
let p1 = Polynomial::new([1., 2., 3.]).as_size().unwrap();
let p2 = Polynomial::new([4., 5., 6.]).as_size().unwrap();
let multiplication = Polynomial::new([4., 13., 28., 27., 18.]);
assert_eq!(&p1 * &p2, multiplication);
}
#[test]
fn derivative_and_antiderivative() {
let mut p = Polynomial::new([1., 2., 3.]);
let p_deriv = Polynomial::new([2., 6., 0.]);
assert_eq!(p.derivative(), p_deriv);
p.coefficients_mut()[0] = 0.;
assert_eq!(p_deriv.antiderivative().unwrap(), p);
assert_eq!(p.antiderivative(), None);
}
#[test]
fn display() {
let p = Polynomial::new([1., 2., 0., 3.]);
assert_eq!(format!("{p:.2}"), "3.00x^3 + 2.00x + 1.00");
}
}
@@ -0,0 +1,192 @@
use glam::{DAffine2, DVec2};
#[derive(Debug, Clone, Default, Copy)]
/// A quad defined by four vertices. Clockwise from the top left:
///
/// `top_left`, `top_right`, `bottom_right`, `bottom_left`.
pub struct Quad(pub [DVec2; 4]);
impl Quad {
/// Get the top left corner of the quad.
pub fn top_left(&self) -> DVec2 {
self.0[0]
}
/// Get the top right corner of the quad.
pub fn top_right(&self) -> DVec2 {
self.0[1]
}
/// Get the bottom right corner of the quad.
pub fn bottom_right(&self) -> DVec2 {
self.0[2]
}
/// Get the bottom left corner of the quad.
pub fn bottom_left(&self) -> DVec2 {
self.0[3]
}
/// Create a zero-sized quad at the point.
pub fn from_point(point: DVec2) -> Self {
Self([point; 4])
}
/// Convert a box defined by two corner points to a quad. The points must be given as `minimum (top left)` then `maximum (bottom right)`.
pub fn from_box(bbox: [DVec2; 2]) -> Self {
let size = bbox[1] - bbox[0];
Self([bbox[0], bbox[0] + size * DVec2::X, bbox[1], bbox[0] + size * DVec2::Y])
}
/// Create a quad from the center and offset (distance from center to middle of an edge)
pub fn from_square(center: DVec2, offset: f64) -> Self {
Self::from_box([center - offset, center + offset])
}
/// Get all the edges in the quad.
pub fn all_edges(&self) -> [[DVec2; 2]; 4] {
[[self.0[0], self.0[1]], [self.0[1], self.0[2]], [self.0[2], self.0[3]], [self.0[3], self.0[0]]]
}
/// Get two edges as bases.
pub fn edges(&self) -> [[DVec2; 2]; 2] {
[[self.0[0], self.0[1]], [self.0[1], self.0[2]]]
}
/// Returns true only if the width and height are both greater than or equal to the given width.
pub fn all_sides_at_least_width(&self, width: f64) -> bool {
self.edges().into_iter().all(|[a, b]| (a - b).length_squared() >= width.powi(2))
}
/// Generates the axis aligned bounding box of the quad
pub fn bounding_box(&self) -> [DVec2; 2] {
[
self.0.into_iter().reduce(|a, b| a.min(b)).unwrap_or_default(),
self.0.into_iter().reduce(|a, b| a.max(b)).unwrap_or_default(),
]
}
/// Gets the center of a quad
pub fn center(&self) -> DVec2 {
self.0.iter().sum::<DVec2>() / 4.
}
/// Take the outside bounds of two axis aligned rectangles, which are defined by two corner points.
pub fn combine_bounds(a: [DVec2; 2], b: [DVec2; 2]) -> [DVec2; 2] {
[a[0].min(b[0]), a[1].max(b[1])]
}
/// "Clip" bounds of `a` to the limits of `b`.
pub fn clip(a: [DVec2; 2], b: [DVec2; 2]) -> [DVec2; 2] {
[
a[0].max(b[0]), // Constrain min corner
a[1].min(b[1]), // Constrain max corner
]
}
/// Expand a quad by a certain amount on all sides.
///
/// Not currently very optimized
pub fn inflate(&self, offset: f64) -> Quad {
let offset = |index_before, index, index_after| {
let [point_before, point, point_after]: [DVec2; 3] = [self.0[index_before], self.0[index], self.0[index_after]];
let [line_in, line_out] = [point - point_before, point_after - point];
let angle = line_in.angle_to(-line_out);
let offset_length = offset / (std::f64::consts::FRAC_PI_2 - angle / 2.).cos();
point + (line_in.perp().normalize_or_zero() + line_out.perp().normalize_or_zero()).normalize_or_zero() * offset_length
};
Self([offset(3, 0, 1), offset(0, 1, 2), offset(1, 2, 3), offset(2, 3, 0)])
}
/// Does this quad contain a point
///
/// Code from https://wrfranklin.org/Research/Short_Notes/pnpoly.html
pub fn contains(&self, p: DVec2) -> bool {
let mut inside = false;
for (i, j) in (0..4).zip([3, 0, 1, 2]) {
if (self.0[i].y > p.y) != (self.0[j].y > p.y) && p.x < ((self.0[j].x - self.0[i].x) * (p.y - self.0[i].y) / (self.0[j].y - self.0[i].y) + self.0[i].x) {
inside = !inside;
}
}
inside
}
/// https://www.cs.rpi.edu/~cutler/classes/computationalgeometry/F23/lectures/02_line_segment_intersections.pdf
fn line_intersection_t(a: DVec2, b: DVec2, c: DVec2, d: DVec2) -> (f64, f64) {
let t = ((a.x - c.x) * (c.y - d.y) - (a.y - c.y) * (c.x - d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x));
let u = ((a.x - c.x) * (a.y - b.y) - (a.y - c.y) * (a.x - b.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x));
(t, u)
}
fn intersect_lines(a: DVec2, b: DVec2, c: DVec2, d: DVec2) -> Option<DVec2> {
let (t, u) = Self::line_intersection_t(a, b, c, d);
((0. ..=1.).contains(&t) && (0. ..=1.).contains(&u)).then(|| a + t * (b - a))
}
pub fn intersect_rays(a: DVec2, a_direction: DVec2, b: DVec2, b_direction: DVec2) -> Option<DVec2> {
let (t, u) = Self::line_intersection_t(a, a + a_direction, b, b + b_direction);
(t.is_finite() && u.is_finite()).then(|| a + t * a_direction)
}
pub fn intersects(&self, other: Quad) -> bool {
let intersects = self
.all_edges()
.into_iter()
.any(|[a, b]| other.all_edges().into_iter().any(|[c, d]| Self::intersect_lines(a, b, c, d).is_some()));
self.contains(other.center()) || other.contains(self.center()) || intersects
}
}
impl std::ops::Mul<Quad> for DAffine2 {
type Output = Quad;
fn mul(self, rhs: Quad) -> Self::Output {
Quad(rhs.0.map(|point| self.transform_point2(point)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn offset_quad() {
fn eq(a: Quad, b: Quad) -> bool {
a.0.iter().zip(b.0).all(|(a, b)| a.abs_diff_eq(b, 0.0001))
}
assert!(eq(Quad::from_box([DVec2::ZERO, DVec2::ONE]).inflate(0.5), Quad::from_box([DVec2::splat(-0.5), DVec2::splat(1.5)])));
assert!(eq(Quad::from_box([DVec2::ONE, DVec2::ZERO]).inflate(0.5), Quad::from_box([DVec2::splat(1.5), DVec2::splat(-0.5)])));
assert!(eq(
(DAffine2::from_scale(DVec2::new(-1., 1.)) * Quad::from_box([DVec2::ZERO, DVec2::ONE])).inflate(0.5),
DAffine2::from_scale(DVec2::new(-1., 1.)) * Quad::from_box([DVec2::splat(-0.5), DVec2::splat(1.5)])
));
}
#[test]
fn quad_contains() {
assert!(Quad::from_box([DVec2::ZERO, DVec2::ONE]).contains(DVec2::splat(0.5)));
assert!(Quad::from_box([DVec2::ONE, DVec2::ZERO]).contains(DVec2::splat(0.5)));
assert!(Quad::from_box([DVec2::splat(300.), DVec2::splat(500.)]).contains(DVec2::splat(350.)));
assert!((DAffine2::from_scale(DVec2::new(-1., 1.)) * Quad::from_box([DVec2::ZERO, DVec2::ONE])).contains(DVec2::new(-0.5, 0.5)));
assert!(!Quad::from_box([DVec2::ZERO, DVec2::ONE]).contains(DVec2::new(1., 1.1)));
assert!(!Quad::from_box([DVec2::ONE, DVec2::ZERO]).contains(DVec2::new(0.5, -0.01)));
assert!(!(DAffine2::from_scale(DVec2::new(-1., 1.)) * Quad::from_box([DVec2::ZERO, DVec2::ONE])).contains(DVec2::splat(0.5)));
}
#[test]
fn intersect_lines() {
assert_eq!(
Quad::intersect_lines(DVec2::new(-5., 5.), DVec2::new(5., 5.), DVec2::new(2., 7.), DVec2::new(2., 3.)),
Some(DVec2::new(2., 5.))
);
assert_eq!(Quad::intersect_lines(DVec2::new(4., 6.), DVec2::new(4., 5.), DVec2::new(2., 7.), DVec2::new(2., 3.)), None);
assert_eq!(Quad::intersect_lines(DVec2::new(-5., 5.), DVec2::new(5., 5.), DVec2::new(2., 7.), DVec2::new(2., 9.)), None);
}
#[test]
fn intersect_quad() {
assert!(Quad::from_box([DVec2::ZERO, DVec2::splat(5.)]).intersects(Quad::from_box([DVec2::splat(4.), DVec2::splat(7.)])));
assert!(Quad::from_box([DVec2::ZERO, DVec2::splat(5.)]).intersects(Quad::from_box([DVec2::splat(4.), DVec2::splat(4.2)])));
assert!(!Quad::from_box([DVec2::ZERO, DVec2::splat(3.)]).intersects(Quad::from_box([DVec2::splat(4.), DVec2::splat(4.2)])));
}
}
@@ -0,0 +1,119 @@
use crate::math::quad::Quad;
use glam::{DAffine2, DVec2};
#[derive(Debug, Clone, Default, Copy, PartialEq)]
/// An axis aligned rect defined by two vertices.
pub struct Rect(pub [DVec2; 2]);
impl Rect {
/// Create a zero sized quad at the point
#[must_use]
pub fn from_point(point: DVec2) -> Self {
Self([point; 2])
}
/// Convert a box defined by two corner points to a quad.
#[must_use]
pub fn from_box(bbox: [DVec2; 2]) -> Self {
Self([bbox[0].min(bbox[1]), bbox[0].max(bbox[1])])
}
/// Create a quad from the center and offset (distance from center to middle of an edge)
#[must_use]
pub fn from_square(center: DVec2, offset: f64) -> Self {
Self::from_box([center - offset, center + offset])
}
/// Create an AABB from an iter of points, returning None if empty.
#[must_use]
pub fn point_iter(points: impl Iterator<Item = DVec2>) -> Option<Self> {
let mut bounds = None;
for point in points {
let bounds = bounds.get_or_insert(Self::from_point(point));
bounds[0] = bounds[0].min(point);
bounds[1] = bounds[1].max(point);
}
bounds
}
/// Get all the edges in the rect.
#[must_use]
pub fn edges(&self) -> [[DVec2; 2]; 4] {
let corners = [self[0], DVec2::new(self[0].x, self[1].y), self[1], DVec2::new(self[1].y, self[0].x)];
[[corners[0], corners[1]], [corners[1], corners[2]], [corners[2], corners[3]], [corners[3], corners[0]]]
}
/// Gets the center of a rect
#[must_use]
pub fn center(&self) -> DVec2 {
self.0.iter().sum::<DVec2>() / 2.
}
/// Take the outside bounds of two axis aligned rectangles, which are defined by two corner points.
#[must_use]
pub fn combine_bounds(a: Self, b: Self) -> Self {
Self::from_box([a[0].min(b[0]), a[1].max(b[1])])
}
/// Expand a rect by a certain amount on top/bottom and on left/right
#[must_use]
pub fn expand_by(&self, x: f64, y: f64) -> Self {
let delta = DVec2::new(x, y);
Self::from_box([self[0] - delta, self[1] + delta])
}
/// Checks if two rects intersect
#[must_use]
pub fn intersects(&self, other: Self) -> bool {
let [mina, maxa] = [self[0].min(self[1]), self[0].max(self[1])];
let [minb, maxb] = [other[0].min(other[1]), other[0].max(other[1])];
mina.x <= maxb.x && minb.x <= maxa.x && mina.y <= maxb.y && minb.y <= maxa.y
}
/// Does this rect contain a point
#[must_use]
pub fn contains(&self, p: DVec2) -> bool {
(self[0].x < p.x && p.x < self[1].x) && (self[0].y < p.y && p.y < self[1].y)
}
#[must_use]
pub fn min(&self) -> DVec2 {
self.0[0].min(self.0[1])
}
#[must_use]
pub fn max(&self) -> DVec2 {
self.0[0].max(self.0[1])
}
#[must_use]
pub fn translate(&self, offset: DVec2) -> Self {
Self([self.0[0] + offset, self.0[1] + offset])
}
}
impl std::ops::Mul<Rect> for DAffine2 {
type Output = Quad;
fn mul(self, rhs: Rect) -> Self::Output {
self * Quad::from_box(rhs.0)
}
}
impl std::ops::Index<usize> for Rect {
type Output = DVec2;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl std::ops::IndexMut<usize> for Rect {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.0[index]
}
}
impl From<Rect> for Quad {
fn from(val: Rect) -> Self {
Quad::from_box(val.0)
}
}
@@ -0,0 +1,129 @@
use graphene_hash::CacheHash;
use std::hash::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::sync::Arc;
/// Stores both what a node was called with and what it returned.
#[derive(Clone, Debug)]
pub struct IORecord<I, O> {
pub input: I,
pub output: O,
}
#[derive(Clone, Debug)]
pub struct MemoHash<T: CacheHash> {
hash: u64,
value: Arc<T>,
}
// Compare the value, not the cache `hash`: `CacheHash` is not guaranteed to be an equivalence relation,
// so it can't back `eq`/`cmp`. Cache-identity hashing goes through `CacheHash`.
impl<T: CacheHash + PartialEq> PartialEq for MemoHash<T> {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
impl<T: CacheHash + Eq> Eq for MemoHash<T> {}
impl<T: CacheHash + PartialOrd> PartialOrd for MemoHash<T> {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
self.value.partial_cmp(&other.value)
}
}
impl<T: CacheHash + Ord> Ord for MemoHash<T> {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.value.cmp(&other.value)
}
}
impl<'de, T: serde::Deserialize<'de> + CacheHash> serde::Deserialize<'de> for MemoHash<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
T::deserialize(deserializer).map(|value| Self::new(value))
}
}
impl<T: CacheHash + serde::Serialize> serde::Serialize for MemoHash<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.value.serialize(serializer)
}
}
impl<T: CacheHash> MemoHash<T> {
pub fn new(value: T) -> Self {
let hash = Self::calc_hash(&value);
Self { hash, value: value.into() }
}
pub fn new_with_hash(value: T, hash: u64) -> Self {
Self { hash, value: value.into() }
}
fn calc_hash(data: &T) -> u64 {
let mut hasher = DefaultHasher::new();
data.cache_hash(&mut hasher);
hasher.finish()
}
pub fn inner_mut(&mut self) -> MemoHashGuard<'_, T> {
MemoHashGuard { inner: self }
}
pub fn into_inner(self) -> Arc<T> {
self.value
}
pub fn hash_code(&self) -> u64 {
self.hash
}
}
impl<T: CacheHash> From<T> for MemoHash<T> {
fn from(value: T) -> Self {
Self::new(value)
}
}
impl<T: CacheHash> CacheHash for MemoHash<T> {
fn cache_hash<H: Hasher>(&self, state: &mut H) {
self.hash.hash(state);
}
}
impl<T: CacheHash> Deref for MemoHash<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
pub struct MemoHashGuard<'a, T: CacheHash> {
inner: &'a mut MemoHash<T>,
}
impl<T: CacheHash> Drop for MemoHashGuard<'_, T> {
fn drop(&mut self) {
let hash = MemoHash::<T>::calc_hash(&self.inner.value);
self.inner.hash = hash;
}
}
impl<T: CacheHash> Deref for MemoHashGuard<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner.value
}
}
impl<T: CacheHash + Clone> std::ops::DerefMut for MemoHashGuard<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
Arc::make_mut(&mut self.inner.value)
}
}
@@ -0,0 +1,159 @@
// TODO(TrueDoctor): Replace this with the more idiomatic approach instead of using `trait Clampable`.
/// A trait for types that can be clamped within a min/max range defined by f64.
pub trait Clampable: Sized {
/// Clamps the value to be no less than `min`.
fn clamp_hard_min(self, min: f64) -> Self;
/// Clamps the value to be no more than `max`.
fn clamp_hard_max(self, max: f64) -> Self;
}
// Implement for common numeric types
macro_rules! impl_clampable_float {
($($ty:ty),*) => {
$(
impl Clampable for $ty {
#[inline(always)]
fn clamp_hard_min(self, min: f64) -> Self {
self.max(min as $ty)
}
#[inline(always)]
fn clamp_hard_max(self, max: f64) -> Self {
self.min(max as $ty)
}
}
)*
};
}
impl_clampable_float!(f32, f64);
macro_rules! impl_clampable_int {
($($ty:ty),*) => {
$(
impl Clampable for $ty {
#[inline(always)]
fn clamp_hard_min(self, min: f64) -> Self {
// Using try_from to handle potential range issues safely, though min should ideally be valid.
// Consider using a different approach if f64 precision vs integer range is a concern.
<$ty>::try_from(min.ceil() as i64).ok().map_or(self, |min_val| self.max(min_val))
}
#[inline(always)]
fn clamp_hard_max(self, max: f64) -> Self {
<$ty>::try_from(max.floor() as i64).ok().map_or(self, |max_val| self.min(max_val))
}
}
)*
};
}
// Add relevant integer types (adjust as needed)
impl_clampable_int!(u32, u64, i32, i64);
// Implement for DVec2 (component-wise clamping)
use glam::DVec2;
impl Clampable for DVec2 {
#[inline(always)]
fn clamp_hard_min(self, min: f64) -> Self {
self.max(DVec2::splat(min))
}
#[inline(always)]
fn clamp_hard_max(self, max: f64) -> Self {
self.min(DVec2::splat(max))
}
}
// Implement for ranked wires (element-wise clamping across the frame)
use crate::list::{Item, List};
impl<T: Clampable> Clampable for Item<T> {
fn clamp_hard_min(self, min: f64) -> Self {
let (element, attributes) = self.into_parts();
Item::from_parts(element.clamp_hard_min(min), attributes)
}
fn clamp_hard_max(self, max: f64) -> Self {
let (element, attributes) = self.into_parts();
Item::from_parts(element.clamp_hard_max(max), attributes)
}
}
impl<T: Clampable> Clampable for List<T> {
fn clamp_hard_min(self, min: f64) -> Self {
self.into_iter().map(|item| item.clamp_hard_min(min)).collect()
}
fn clamp_hard_max(self, max: f64) -> Self {
self.into_iter().map(|item| item.clamp_hard_max(max)).collect()
}
}
#[cfg(feature = "serde")]
#[derive(serde::Deserialize)]
struct LegacyTable<T> {
#[serde(alias = "instances", alias = "instance")]
element: Vec<T>,
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_to_color<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<no_std_types::color::Color, D::Error> {
use no_std_types::color::Color;
use serde::Deserialize;
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum ColorFormat {
OptionalColor(Option<Color>),
List(LegacyTable<Color>),
}
Ok(match ColorFormat::deserialize(deserializer)? {
ColorFormat::OptionalColor(color) => color.unwrap_or(Color::TRANSPARENT),
ColorFormat::List(list) => list.element.into_iter().next().unwrap_or(Color::TRANSPARENT),
})
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_to_f64_array<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Vec<f64>, D::Error> {
use serde::Deserialize;
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum F64ArrayFormat {
Array(Vec<f64>),
List(LegacyTable<f64>),
}
Ok(match F64ArrayFormat::deserialize(deserializer)? {
F64ArrayFormat::Array(values) => values,
F64ArrayFormat::List(list) => list.element,
})
}
/// Parse a CSS color string (named color, hex, `rgb(...)`, `hsl(...)`, etc.) into a linear-light [`Color`] using the `color` crate's CSS Color 4 parser.
/// Tries the input as-is first (catches CSS named colors like `red`, `rgb(...)`, and well-formed hex like `#abcdef`), then falls back to treating the input as bare hex with length-based expansion to a CSS-parseable form:
/// - 1 char `f` → `#fff` (CSS 3-char shorthand)
/// - 2 char `ab` → `#ababab` (repeated to 6 chars)
/// - 4 char `abcd` → `#00abcd` (left-padded with `00`)
/// - 5 char `abcde` → `#0abcde` (left-padded with `0`)
/// - 3, 6, 8 char inputs are passed through with a `#` prefix.
pub fn parse_css_color(input: &str) -> Option<crate::Color> {
let trimmed = input.trim();
let parsed = color::parse_color(trimmed).ok().or_else(|| {
let bare = trimmed.strip_prefix('#').unwrap_or(trimmed);
if bare.is_empty() || !bare.chars().all(|c| c.is_ascii_hexdigit()) {
return None;
}
let expanded = match bare.len() {
1 => bare.repeat(3),
2 => bare.repeat(3),
4 => format!("00{bare}"),
5 => format!("0{bare}"),
_ => bare.to_string(),
};
let candidate = format!("#{expanded}");
// Avoid retrying the exact same string we just failed to parse.
(candidate != trimmed).then(|| color::parse_color(&candidate).ok()).flatten()
})?;
let srgb: color::AlphaColor<color::Srgb> = parsed.to_alpha_color();
let [red, green, blue, alpha] = srgb.components;
// Reject out-of-gamut values that `color::parse_color` accepts for newer CSS syntax (e.g., `rgb(300 -50 200)`).
let in_gamut = alpha <= 1. && ![red, green, blue, alpha].iter().any(|c| c.is_sign_negative() || !c.is_finite());
in_gamut.then(|| crate::Color::from_gamma_srgb_channels(red, green, blue, alpha))
}
@@ -0,0 +1,686 @@
use crate::context::InjectIndex;
use crate::gpoll::{Extent, Finality, GPoll, GraphError, Interrupt, Level};
use std::cell::Cell;
use std::marker::PhantomData;
use std::mem::MaybeUninit;
use std::ops::{Deref, Range};
#[derive(Debug)]
pub enum BatchStatus<'a> {
/// Producer-resident lanes, shared: read-only for the caller. The extent
/// is the producer's knowledge of the level's total after serving the
/// range: a sound lower bound, or exact; a batch shorter than the
/// requested range carries `Exactly` and marks the end of the data.
Lent(RecordBatch<'a>, Finality, Extent),
/// The caller's scratch, filled: the caller is the exclusive owner and may
/// mutate the lanes or reclaim the buffer for in-place reuse. The extent
/// hint is as for `Lent`.
Filled(RecordBatchMut<'a>, Finality, Extent),
/// No batch implementation behind this input; a driver answers with the
/// per-lane serve and copy-out loop ([`crate::record::fill_frames`]).
Unbatched,
Pending,
Error(GraphError),
NeedBuffer,
InvalidRange,
}
impl From<Interrupt> for BatchStatus<'_> {
fn from(interrupt: Interrupt) -> Self {
match interrupt {
Interrupt::Pending => BatchStatus::Pending,
Interrupt::Error(error) => BatchStatus::Error(*error),
}
}
}
/// A shared view over a batch of records in one flat frame buffer: lane `i`
/// starts at `frames + i * stride` with `stride = layout.lane_stride()`.
/// Frame bytes carry no drop glue (droppable elements ride parked,
/// arena-owned), so the view has no drop obligation; `'a` covers the frames
/// and the layout.
#[derive(Clone, Copy, Debug)]
pub struct RecordBatch<'a> {
frames: *const u8,
stride: usize,
len: usize,
layout: &'a crate::record::Layout,
_lifetime: PhantomData<&'a [u8]>,
}
impl<'a> RecordBatch<'a> {
/// # Safety
/// `frames` must hold `len` initialized records of `layout`, packed at
/// `layout.lane_stride()` stride and valid for `'a`.
pub unsafe fn new(frames: *const u8, len: usize, layout: &'a crate::record::Layout) -> Self {
Self {
frames,
stride: layout.lane_stride(),
len,
layout,
_lifetime: PhantomData,
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn layout(&self) -> &'a crate::record::Layout {
self.layout
}
pub(crate) fn frames_ptr(&self) -> *const u8 {
self.frames
}
pub fn get(&self, lane: usize) -> RecordLane<'a> {
assert!(lane < self.len, "lane {lane} out of bounds for a batch of {}", self.len);
RecordLane {
// SAFETY: in-bounds by the assert against the constructor's contract.
rec: unsafe { crate::record::Rec::new(self.frames.add(lane * self.stride)) },
layout: self.layout,
}
}
pub fn for_each(&self, mut f: impl FnMut(usize, RecordLane<'a>)) {
for lane in 0..self.len {
f(lane, self.get(lane));
}
}
}
/// The exclusive view over caller-owned frames (the `Filled` status): while it
/// lives, the borrow of the caller's scratch guarantees nobody else can read
/// the lanes, so mutating them or reclaiming the buffer is sound.
#[derive(Debug)]
pub struct RecordBatchMut<'a> {
scratch: &'a mut [MaybeUninit<u64>],
len: usize,
layout: &'a crate::record::Layout,
}
impl<'a> RecordBatchMut<'a> {
/// Minted only by a [`crate::record::SlotRun`] finishing its served lanes,
/// which serve in ascending order with no gaps, so the initialized prefix is
/// a fact rather than a contract.
pub(crate) fn new(scratch: &'a mut [MaybeUninit<u64>], len: usize, layout: &'a crate::record::Layout) -> Self {
debug_assert!(len.checked_mul(layout.lane_stride()).is_some_and(|need| need <= scratch.len() * 8));
Self { scratch, len, layout }
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn layout(&self) -> &'a crate::record::Layout {
self.layout
}
/// Reads the lanes without giving up exclusivity.
pub fn share(&self) -> RecordBatch<'_> {
// SAFETY: the constructor's contract, narrowed to the reborrow's scope.
unsafe { RecordBatch::new(self.scratch.as_ptr().cast(), self.len, self.layout) }
}
/// Gives up exclusivity for the batch's whole lifetime.
pub fn into_shared(self) -> RecordBatch<'a> {
// SAFETY: the constructor's contract; the exclusive borrow is consumed.
unsafe { RecordBatch::new(self.scratch.as_ptr().cast(), self.len, self.layout) }
}
}
/// One lane's record: its pointer paired with the batch's layout.
#[derive(Clone, Copy, Debug)]
pub struct RecordLane<'a> {
rec: crate::record::Rec<'a>,
layout: &'a crate::record::Layout,
}
impl<'a> RecordLane<'a> {
pub fn layout(&self) -> &'a crate::record::Layout {
self.layout
}
pub fn rec(&self) -> crate::record::Rec<'a> {
self.rec
}
/// The element at offset 0.
///
/// # Safety
/// `U` must be the record's element type, proven at the consumer's wiring.
pub unsafe fn element<U: Copy>(&self) -> U {
unsafe { self.rec.element::<U>() }
}
/// Attribute `A` through a token, `None` where the token was minted against
/// another layout than this lane's.
pub fn try_attr_at<A: crate::attribute::Attribute>(&self, field: crate::record::FieldOffset<A>) -> Option<A::Value<'a>> {
let offset = field.resolve(self.layout)?;
// SAFETY: resolving against this lane's own layout pins the offset and
// the field's value type, and the batch's contract makes the lane a live
// record of that layout.
Some(unsafe { self.rec.read::<A::Value<'a>>(offset) })
}
/// Attribute `A` through a token, or its census default where the token is
/// absent or names another layout.
pub fn attr_at<A: crate::attribute::Attribute>(&self, field: Option<crate::record::FieldOffset<A>>) -> A::Value<'a> {
field.and_then(|field| self.try_attr_at(field)).unwrap_or_else(A::default)
}
/// Attribute `A` at the record's top level, or its census default when the
/// layout does not carry it. Mints a token per call; lane loops hoist the
/// mint instead.
pub fn attr<A: crate::attribute::Attribute>(&self) -> A::Value<'a> {
self.attr_at(crate::record::FieldOffset::<A>::of(self.layout, 0))
}
}
/// One lane of a materialized level, element-typed. In a kernel's element
/// position the output frame is copied from this lane.
#[derive(Debug)]
pub struct Lane<'a, T> {
lane: RecordLane<'a>,
_element: PhantomData<T>,
}
// A view regardless of `T`: copying a lane copies no record.
impl<T> Clone for Lane<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Lane<'_, T> {}
impl<'a, T> Deref for Lane<'a, T> {
type Target = RecordLane<'a>;
fn deref(&self) -> &RecordLane<'a> {
&self.lane
}
}
/// A materialized nesting level handed to a folding kernel: a thin element-typed
/// view over the [`RecordBatch`] the level was collected into.
#[derive(Debug)]
pub struct List<'a, T> {
batch: RecordBatch<'a>,
_element: PhantomData<T>,
}
// A shared view regardless of `T`: copying the list copies no elements.
impl<T> Clone for List<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for List<'_, T> {}
impl<'a, T> List<'a, T> {
/// # Safety
/// `T` must be the batch's record element type, proven at the consumer's
/// wiring, and the batch's frames must stay valid for the evaluation:
/// arena-resident batches qualify, caller stack scratch does not.
pub unsafe fn new(batch: RecordBatch<'a>) -> Self {
Self { batch, _element: PhantomData }
}
/// The level as a group item over the same frames, without copying.
/// Panics where a parked element or field lacks content glue.
pub fn as_group_item(&self) -> crate::record::GroupItem<'a> {
// SAFETY: `List::new` established the frames stay valid for the
// evaluation.
unsafe { crate::record::GroupItem::from_resident(self.batch) }
}
pub fn len(&self) -> usize {
self.batch.len()
}
pub fn is_empty(&self) -> bool {
self.batch.is_empty()
}
pub fn batch(&self) -> RecordBatch<'a> {
self.batch
}
pub fn get(&self, index: usize) -> T
where
T: Copy,
{
// SAFETY: `List::new` established that `T` is the batch's element type.
unsafe { self.batch.get(index).element::<T>() }
}
/// Borrows lane `index`'s element, through the park for droppable types.
pub fn element_ref(&self, index: usize) -> &T {
// SAFETY: `List::new` established that `T` is the batch's element type,
// and the borrow lives within the batch's own lifetime.
unsafe { crate::record::borrow_element::<T>(self.batch.get(index).rec()) }
}
/// Lane `index`'s record, for attribute reads beside the element.
pub fn lane(&self, index: usize) -> Lane<'a, T> {
Lane {
lane: self.batch.get(index),
_element: PhantomData,
}
}
pub fn iter(&self) -> impl Iterator<Item = T> + '_
where
T: Copy,
{
(0..self.len()).map(move |index| self.get(index))
}
}
impl<'a, T: Copy> IntoIterator for List<'a, T> {
type Item = T;
type IntoIter = ListIter<'a, T>;
fn into_iter(self) -> ListIter<'a, T> {
ListIter { list: self, position: 0 }
}
}
pub struct ListIter<'a, T> {
list: List<'a, T>,
position: usize,
}
impl<T: Copy> Iterator for ListIter<'_, T> {
type Item = T;
fn next(&mut self) -> Option<T> {
(self.position < self.list.len()).then(|| {
let value = self.list.get(self.position);
self.position += 1;
value
})
}
}
pub trait Node<Input> {
/// Serves the node's record through the caller's claim: the writes land
/// in the claim and the returned proof is mintable only by its closing
/// methods, so the served record is of the claimed layout by
/// construction. The caller claims the frame at [`Node::layout`] out of
/// its own frame space, and the claim carries what is left, so the node
/// takes exactly its own frame out of the caller's free space.
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'e, 'l>) -> GPoll<crate::record::Served<'e>>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>;
/// The count of items at one absolute nesting level (innermost `0`). The
/// leveled primitive a structure node overrides to report a pushed level's
/// size; the scalar base is one item at every level. Uncertainty rides the
/// `GPoll` status axis.
fn extent_at<'e>(&self, _input: &Input, _level: u8, _frames: &crate::record::Frames<'e>) -> GPoll<Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
GPoll::Final(Extent::Exactly(1))
}
/// The composite domain query derived from [`extent_at`](Node::extent_at):
/// one level, the product of the levels below or above it, or the whole
/// domain's flat count. Consumers query this; nodes only write `extent_at`.
fn extent<'e>(&self, input: &Input, at: Level, frames: &crate::record::Frames<'e>) -> GPoll<Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
let product =
|range: core::ops::Range<u8>, frames: &crate::record::Frames<'e>| range.fold(GPoll::Final(Extent::Exactly(1)), |acc, level| Extent::mul(acc, self.extent_at(input, level, frames)));
match at {
Level::At(level) => self.extent_at(input, level, frames),
Level::Below(level) => product(0..level, frames),
Level::Above(level) => product((level + 1)..self.depth(), frames),
Level::Total => product(0..self.depth(), frames),
}
}
/// The node's domain depth (number of nesting levels; `0` = scalar), baked
/// into the record layout at wiring.
fn depth(&self) -> u8 {
self.layout().depth
}
/// Introspection access to node-resident records; `None` for ordinary nodes.
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
None
}
/// The record layout of this node's output; the shared empty layout for
/// element-only producers. Consumers read their carrier's layout through
/// this at wiring, and the wiring layer derives the root buffer's sizing from
/// the same layouts, in the dynamic executor and exported source alike.
fn layout(&self) -> &crate::record::Layout {
crate::record::empty_layout()
}
/// Installs this node's resolved record layout; a no-op unless it produces records.
fn set_layout(&mut self, _layout: crate::record::RecordLayout) {}
/// Batched evaluation of `range` into caller-provided frame storage of
/// `range.len() * layout.lane_stride()` bytes; see [`BatchStatus`]. The
/// default advertises no support and drivers fall back to per-lane serves
/// with copy-out ([`crate::record::fill_frames`]); overrides exist to beat
/// that loop (resident lanes, direct fills, fewer erased calls), never for
/// correctness.
fn eval_batch<'a, 'e>(&'a self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<u64>]>, frames: &crate::record::Frames<'e>) -> BatchStatus<'a>
where
Input: InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
let _ = (input, range, scratch, frames);
BatchStatus::Unbatched
}
}
impl<Input, N> Node<Input> for &N
where
N: Node<Input> + ?Sized,
{
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'e, 'l>) -> GPoll<crate::record::Served<'e>>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).serve(input, slot)
}
fn extent_at<'e>(&self, input: &Input, level: u8, frames: &crate::record::Frames<'e>) -> GPoll<Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).extent_at(input, level, frames)
}
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
(**self).serialize()
}
fn layout(&self) -> &crate::record::Layout {
(**self).layout()
}
fn eval_batch<'a, 'e>(&'a self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<u64>]>, frames: &crate::record::Frames<'e>) -> BatchStatus<'a>
where
Input: InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).eval_batch(input, range, scratch, frames)
}
}
impl<Input, N> Node<Input> for Box<N>
where
N: Node<Input> + ?Sized,
{
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'e, 'l>) -> GPoll<crate::record::Served<'e>>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).serve(input, slot)
}
fn extent_at<'e>(&self, input: &Input, level: u8, frames: &crate::record::Frames<'e>) -> GPoll<Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).extent_at(input, level, frames)
}
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
(**self).serialize()
}
fn layout(&self) -> &crate::record::Layout {
(**self).layout()
}
fn eval_batch<'a, 'e>(&'a self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<u64>]>, frames: &crate::record::Frames<'e>) -> BatchStatus<'a>
where
Input: InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).eval_batch(input, range, scratch, frames)
}
}
impl<Input, N> Node<Input> for std::sync::Arc<N>
where
N: Node<Input> + ?Sized,
{
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'e, 'l>) -> GPoll<crate::record::Served<'e>>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).serve(input, slot)
}
fn extent_at<'e>(&self, input: &Input, level: u8, frames: &crate::record::Frames<'e>) -> GPoll<Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).extent_at(input, level, frames)
}
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
(**self).serialize()
}
fn layout(&self) -> &crate::record::Layout {
(**self).layout()
}
fn eval_batch<'a, 'e>(&'a self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<u64>]>, frames: &crate::record::Frames<'e>) -> BatchStatus<'a>
where
Input: InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
(**self).eval_batch(input, range, scratch, frames)
}
}
pub struct StatusCell {
finality: Cell<Finality>,
error: Cell<Option<GraphError>>,
no_partial: bool,
}
impl Default for StatusCell {
fn default() -> Self {
Self::new()
}
}
impl StatusCell {
pub fn new() -> Self {
Self {
finality: Cell::new(Finality::AllFinal),
error: Cell::new(None),
no_partial: false,
}
}
#[inline(always)]
pub fn no_partial() -> Self {
Self { no_partial: true, ..Self::new() }
}
/// Claims the input's own frame out of `frames`, serves through it, and
/// folds the poll's status into the cell. The claim is the caller's, so
/// the input's frame is claimed exactly once per evaluation.
#[inline(always)]
pub fn eval_input<'e, Input, N: Node<Input> + ?Sized>(&self, input_index: usize, node: &N, input: &Input, frames: &crate::record::Frames<'e>) -> Result<crate::record::RecordValue<'e>, Interrupt>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
let slot = frames.claim(node.layout());
match node.serve(input, slot) {
GPoll::Final(served) => Ok(served.value()),
GPoll::Partial(_) if self.no_partial => Err(Interrupt::Pending),
GPoll::Partial(served) => {
self.finality.set(Finality::Partial);
Ok(served.value())
}
GPoll::Fallback(boxed) => {
let (served, error) = *boxed;
let first = self.error.take();
self.error.set(first.or(Some(error.traced(input_index))));
Ok(served.value())
}
GPoll::Pending => Err(Interrupt::Pending),
GPoll::Error(mut error) => {
error.trace.push(input_index);
Err(Interrupt::Error(error))
}
}
}
/// A new cell holding a copy of the accumulated status; the error stays in
/// place, cloned rather than taken.
#[inline(always)]
pub fn snapshot(&self) -> StatusCell {
let error = self.error.take();
self.error.set(error.clone());
StatusCell {
finality: Cell::new(self.finality.get()),
error: Cell::new(error),
no_partial: self.no_partial,
}
}
#[inline(always)]
pub fn finish<T>(self, value: T) -> GPoll<T> {
match (self.error.take(), self.finality.get()) {
(Some(error), _) => GPoll::Fallback(Box::new((value, error))),
(None, Finality::AllFinal) => GPoll::Final(value),
(None, Finality::Partial) => GPoll::Partial(value),
}
}
#[inline(always)]
pub fn merge<T>(self, poll: GPoll<T>) -> GPoll<T> {
match poll {
GPoll::Final(value) => self.finish(value),
GPoll::Partial(_) if self.no_partial => GPoll::Pending,
GPoll::Partial(value) => match self.finish(value) {
GPoll::Final(value) => GPoll::Partial(value),
other => other,
},
GPoll::Fallback(boxed) => {
let (value, error) = *boxed;
let first = self.error.take().unwrap_or(error);
GPoll::Fallback(Box::new((value, first)))
}
interrupted => interrupted,
}
}
}
#[derive(Clone, Copy)]
pub struct LazyInput<'a, 'f, N> {
node: &'a N,
cell: &'a StatusCell,
input_index: usize,
frames: &'a crate::record::Frames<'f>,
}
impl<'a, 'f, N> LazyInput<'a, 'f, N> {
pub fn new(node: &'a N, cell: &'a StatusCell, input_index: usize, frames: &'a crate::record::Frames<'f>) -> Self {
Self { node, cell, input_index, frames }
}
#[inline(always)]
pub fn eval<'e, Input>(&self, ctx: &Input) -> Result<crate::record::RecordValue<'e>, Interrupt>
where
N: crate::record::DerivedRecordInput<'e, Input>,
'f: 'e,
{
self.node.eval_derived(self.cell, self.input_index, ctx, self.frames)
}
/// The input's composite extent, for kernels that split or shift indices
/// over their sources.
#[inline(always)]
pub fn extent<'e, Input>(&self, ctx: &Input, at: Level) -> GPoll<Extent>
where
N: Node<Input>,
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
'f: 'e,
{
self.node.extent(ctx, at, self.frames)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::arena::Arena;
use crate::context::ExtractArena;
use crate::record::{LiftedSource, serve_input};
#[derive(Clone, Copy)]
struct TestInput<'a> {
index: u64,
arena: &'a Arena,
}
impl InjectIndex for TestInput<'_> {
fn set_index(&mut self, index: u64) {
self.index = index;
}
}
impl<'a> ExtractArena for TestInput<'a> {
type ArenaRef = &'a Arena;
fn arena(&self) -> &'a Arena {
self.arena
}
}
fn double<'a>() -> LiftedSource<u64, impl Fn(&TestInput<'a>) -> GPoll<u64>> {
LiftedSource::new(|input: &TestInput<'a>| GPoll::Final(input.index * 2))
}
#[test]
fn the_default_advertises_no_batch_support() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(1024).unwrap();
let input = TestInput { index: 0, arena: &arena };
let mut scratch = [const { MaybeUninit::uninit() }; 4];
let node = double();
assert!(matches!(node.eval_batch(&input, 2..6, Some(&mut scratch), &frames), BatchStatus::Unbatched));
assert!(matches!(node.eval_batch(&input, 2..6, None, &frames), BatchStatus::Unbatched));
}
#[test]
fn trait_is_object_safe_across_erased_edges() {
let arena = Arena::new(1024).unwrap();
let input = TestInput { index: 21, arena: &arena };
let node = double();
let layout = Node::<TestInput>::layout(&node).clone();
let erased: Box<dyn Node<TestInput>> = Box::new(node);
let frames = crate::record::test_frames(1 << 12);
let GPoll::Final(value) = serve_input(&*erased, &input, &frames) else {
panic!("the erased edge must serve a final record");
};
// SAFETY: the record was served at `layout`, whose element is the output.
assert_eq!(unsafe { crate::record::read_element::<u64>(layout.rec(&value)) }, 42);
assert!(matches!(erased.eval_batch(&input, 0..2, None, &frames), BatchStatus::Unbatched));
}
}
@@ -0,0 +1,99 @@
use crate::Node;
use crate::transform::Footprint;
use glam::DVec2;
use std::future::Future;
use std::marker::PhantomData;
// Type
// TODO: Document this
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct TypeNode<N: for<'a> Node<'a, I>, I, O>(pub N, pub PhantomData<(I, O)>);
impl<'i, N, I: 'i, O: 'i> Node<'i, I> for TypeNode<N, I, O>
where
N: for<'n> Node<'n, I, Output = O>,
{
type Output = O;
fn eval(&'i self, input: I) -> Self::Output {
self.0.eval(input)
}
fn reset(&self) {
self.0.reset();
}
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
self.0.serialize()
}
}
impl<'i, N: for<'a> Node<'a, I>, I: 'i> TypeNode<N, I, <N as Node<'i, I>>::Output> {
pub fn new(node: N) -> Self {
Self(node, PhantomData)
}
}
impl<'i, N: for<'a> Node<'a, I> + Clone, I: 'i> Clone for TypeNode<N, I, <N as Node<'i, I>>::Output> {
fn clone(&self) -> Self {
Self(self.0.clone(), self.1)
}
}
impl<'i, N: for<'a> Node<'a, I> + Copy, I: 'i> Copy for TypeNode<N, I, <N as Node<'i, I>>::Output> {}
/// The [`Convert`] trait allows for conversion between Rust primitive numeric types.
/// Because number casting is lossy, we cannot use the normal [`Into`] trait like we do for other types.
pub trait Convert<T, C>: Sized {
/// Converts this type into the (usually inferred) output type.
#[must_use]
fn convert(self, footprint: Footprint, converter: C) -> impl Future<Output = T> + Send;
}
/// Constructs `Self` from a single anchor point at the given position. Implemented by the vector crate's
/// path type so a position wire can convert to a single-point path without core-types depending on that crate.
pub trait FromAnchorPosition {
fn from_anchor_position(position: DVec2) -> Self;
}
/// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types.
macro_rules! impl_convert {
($from:ty, $to:ty) => {
impl Convert<$to, ()> for $from {
async fn convert(self, _: Footprint, _: ()) -> $to {
self as $to
}
}
};
($to:ty) => {
impl_convert!(f32, $to);
impl_convert!(f64, $to);
impl_convert!(i8, $to);
impl_convert!(u8, $to);
impl_convert!(u16, $to);
impl_convert!(i16, $to);
impl_convert!(i32, $to);
impl_convert!(u32, $to);
impl_convert!(i64, $to);
impl_convert!(u64, $to);
impl_convert!(i128, $to);
impl_convert!(u128, $to);
impl_convert!(isize, $to);
impl_convert!(usize, $to);
impl Convert<DVec2, ()> for $to {
async fn convert(self, _: Footprint, _: ()) -> DVec2 {
DVec2::splat(self as f64)
}
}
};
}
impl_convert!(f32);
impl_convert!(f64);
impl_convert!(i8);
impl_convert!(u8);
impl_convert!(u16);
impl_convert!(i16);
impl_convert!(i32);
impl_convert!(u32);
impl_convert!(i64);
impl_convert!(u64);
impl_convert!(i128);
impl_convert!(u128);
impl_convert!(isize);
impl_convert!(usize);
@@ -0,0 +1,275 @@
//! Raw typed access to a record at a wiring-proven layout.
use super::layout::element_parked;
use crate::attribute;
use crate::gpoll::GPoll;
/// A view of one record: a pointer whose layout is proven at wiring, borrowing
/// the storage it points into for `'r`.
#[derive(Clone, Copy, Debug)]
pub struct Rec<'r>(pub(in crate::record) *const u8, pub(in crate::record) std::marker::PhantomData<&'r u8>);
impl<'r> Rec<'r> {
/// # Safety
/// `ptr` must point to a live record of the layout the consumer resolved
/// at wiring, valid for `'r` and until the owning slot is next written.
pub unsafe fn new(ptr: *const u8) -> Self {
Rec(ptr, std::marker::PhantomData)
}
/// # Safety
/// `offset` must be a field offset of the record's layout and `T` the
/// field's type; both are proven at wiring. The record's base is aligned
/// to its layout, so field reads are aligned.
pub unsafe fn read<T: Copy>(self, offset: usize) -> T {
unsafe { self.0.add(offset).cast::<T>().read() }
}
/// # Safety
/// `T` must be the record's element type; the element sits at offset 0.
pub unsafe fn element<T: Copy>(self) -> T {
unsafe { self.read(0) }
}
pub fn ptr(self) -> *const u8 {
self.0
}
}
/// An opaque record value: every non-empty record spills to a claimed frame
/// and the value carries its pointer, while an empty record carries nothing.
#[derive(Clone, Copy)]
pub struct RecordValue<'e> {
pub(in crate::record) ptr: *const u8,
_lifetime: std::marker::PhantomData<&'e ()>,
}
impl std::fmt::Debug for RecordValue<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("RecordValue(..)")
}
}
// SAFETY: `element_write` requires the element `Send + Sync` and every field holds an
// `Attribute::Value`, which the trait bounds `Send + Sync`, so the record bytes behind
// the pointer are thread-safe; `'e` ties the pointer's validity to the shared arena and
// record-stack discipline.
unsafe impl Send for RecordValue<'_> {}
// SAFETY: as `Send`.
unsafe impl Sync for RecordValue<'_> {}
impl<'e> RecordValue<'e> {
#[doc(hidden)]
pub fn zeroed() -> Self {
RecordValue {
ptr: std::ptr::null(),
_lifetime: std::marker::PhantomData,
}
}
/// The inline storage under construction; writes land in the value itself.
#[doc(hidden)]
pub fn as_mut_ptr(&mut self) -> *mut u8 {
(&raw mut *self).cast()
}
#[doc(hidden)]
pub fn spilled(rec: Rec<'_>) -> Self {
RecordValue {
ptr: rec.ptr(),
_lifetime: std::marker::PhantomData,
}
}
/// Rebinds the eval lifetime, lengthening a derived scope's record back
/// onto the outer evaluation's. Sound only because the record's bytes live
/// in the outer caller's slot region: a derived context shortens the
/// caller's frame space rather than owning any, so the frame the record
/// sits in outlives the derivation, exactly as it outlives the arena
/// borrow the derived context carries.
pub(in crate::record) fn rebind<'a>(self) -> RecordValue<'a> {
RecordValue {
ptr: self.ptr,
_lifetime: std::marker::PhantomData,
}
}
}
/// Reads a declared attribute out of a record at a wiring-resolved offset,
/// falling back to the marker's census default where the layout does not carry
/// the name.
///
/// # Safety
/// `rec` must be a live record of the layout `offset` was resolved against,
/// and `offset`, where present, must be that layout's offset for `A` at the
/// read's level. The census admits one value type per attribute name and
/// panics on a conflicting declaration, so that field's bytes are a value of
/// `A::Value`, differing from the read type only in `'e`, which must not
/// outlive the evaluation a parked payload is arena-resident for.
pub unsafe fn read_at<'e, A: attribute::Attribute>(rec: Rec<'_>, offset: Option<usize>) -> attribute::Attr<'e, A> {
attribute::Attr(match offset {
// SAFETY: the caller's contract.
Some(offset) => unsafe { rec.read::<A::Value<'e>>(offset) },
None => A::default(),
})
}
/// The read-less [`DerivedLazyInput`] glue: the token alone.
///
/// # Safety
/// `rec` must be a spilled record's frame.
pub unsafe fn token_only<'e>(rec: Rec<'_>, _reads: &[Option<usize>]) -> RecordValue<'e> {
RecordValue::spilled(rec)
}
/// # Safety
/// `offset` must be a field offset of the layout of the record under
/// construction at `dst` and `T` the field's type; both are proven at wiring.
pub unsafe fn write_field<T>(dst: *mut u8, offset: usize, value: T) {
unsafe { dst.add(offset).cast::<T>().write(value) }
}
/// # Safety
/// The record's element must be a `T` in the form [`element_parked`] picks,
/// and the borrow is only valid while the record is.
pub unsafe fn borrow_element<'e, T>(rec: Rec<'_>) -> &'e T {
match element_parked::<T>() {
true => unsafe { rec.element::<&T>() },
false => unsafe { &*rec.ptr().cast::<T>() },
}
}
/// # Safety
/// The record's element must be a `T` in the form [`element_parked`] picks.
pub unsafe fn read_element<T: Clone>(rec: Rec<'_>) -> T {
unsafe { borrow_element::<T>(rec) }.clone()
}
/// # Safety
/// `dst` must be fresh element storage of a record whose element is `T`.
/// `None` reports arena exhaustion for a parked element.
pub unsafe fn write_element<T: Send + Sync + dyn_any::StaticTypeSized>(dst: *mut u8, value: T, arena: &crate::arena::Arena) -> Option<()> {
unsafe { write_element_sized(dst, value, arena, 0) }
}
/// [`write_element`] with the park glue's estimate of the heap `value` owns.
///
/// # Safety
/// As [`write_element`].
pub unsafe fn write_element_sized<T: Send + Sync + dyn_any::StaticTypeSized>(dst: *mut u8, value: T, arena: &crate::arena::Arena, retained: usize) -> Option<()> {
// SAFETY: the caller's contract; `T::Static` is the element type's own key.
unsafe { write_element_keyed(dst, value, arena, retained, std::any::TypeId::of::<T::Static>()) }
}
/// [`write_element_sized`] for replay glue already holding the element's
/// static form, whose key the element type it replays into supplies.
///
/// # Safety
/// As [`write_element`], and `type_of` must be that element type's key, since
/// a park carrying it is what [`Promotion::move_park`] moves at.
pub(in crate::record) unsafe fn write_element_keyed<T: Send + Sync>(dst: *mut u8, value: T, arena: &crate::arena::Arena, retained: usize, type_of: std::any::TypeId) -> Option<()> {
match element_parked::<T>() {
true => {
let (parked, _) = arena.alloc_sized_as(value, retained, type_of)?;
unsafe { dst.cast::<&T>().write(parked) };
Some(())
}
false => {
unsafe { dst.cast::<T>().write(value) };
Some(())
}
}
}
/// Finishes a carried record frame: the element lands beside the fields
/// already carried into `dst`, and inline frames copy out of the scratch
/// bytes. Arena exhaustion of a parked element reports as an error poll.
///
/// # Safety
/// `dst` must be the claimed frame (or inline scratch when `frame_bytes` is
/// 0) of a record whose element is `T` and whose frame size is `frame_bytes`,
/// with every carried field already written.
pub(in crate::record) unsafe fn lift_poll_into<'e, T: Send + Sync + dyn_any::StaticTypeSized>(
poll: GPoll<T>,
dst: *mut u8,
frame_bytes: usize,
arena: &'e crate::arena::Arena,
) -> GPoll<RecordValue<'e>> {
let build = |element: T| {
let written = unsafe { write_element(dst, element, arena) };
written.map(|()| match frame_bytes {
0 => unsafe { dst.cast::<RecordValue>().read() },
_ => RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) }),
})
};
let exhausted = || {
GPoll::Error(Box::new(crate::gpoll::GraphError {
kind: crate::gpoll::ErrorKind::ArenaExhausted,
trace: Vec::new(),
}))
};
match poll {
GPoll::Final(element) => build(element).map_or_else(exhausted, GPoll::Final),
GPoll::Partial(element) => build(element).map_or_else(exhausted, GPoll::Partial),
GPoll::Fallback(boxed) => {
let (element, error) = *boxed;
build(element).map_or_else(exhausted, |value| GPoll::Fallback(Box::new((value, error))))
}
GPoll::Pending => GPoll::Pending,
GPoll::Error(error) => GPoll::Error(error),
}
}
/// # Safety
/// `src` must be a record of the plan's source layout and `dst` a buffer of
/// the plan's target layout; both are proven at wiring. The two records must
/// not overlap: a plan routinely carries identity moves, so each entry copies
/// non-overlapping and an aliasing pair is undefined on the first copy.
pub unsafe fn apply_plan(src: Rec<'_>, dst: *mut u8, plan: &[(usize, usize, usize)]) {
for &(from, to, size) in plan {
unsafe { std::ptr::copy_nonoverlapping(src.ptr().add(from), dst.add(to), size) };
}
}
/// The value with its lifetimes substituted by `'static`, for erased storage
/// whose reads re-bind a live lifetime.
///
/// # Safety
/// The erased value's borrows must not be used past their real lifetimes: the
/// stored form may only be read through a surface that re-binds a lifetime no
/// longer than the borrows' own, or after deep glue replaced every borrow with
/// owned content.
pub unsafe fn erase_static<T: dyn_any::StaticTypeSized>(value: T) -> T::Static {
let value = std::mem::ManuallyDrop::new(value);
// SAFETY: `Static` is `Self` with lifetimes substituted, layout-identical
// by `StaticTypeSized`'s contract.
unsafe { std::ptr::read((&raw const value).cast::<T::Static>()) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn elements_write_and_read_in_their_picked_form() {
let arena = crate::arena::Arena::new(256).unwrap();
let mut inline = [0u64; 2];
unsafe { write_element(inline.as_mut_ptr().cast(), 4.5f64, &arena) }.unwrap();
let rec = unsafe { Rec::new(inline.as_ptr().cast()) };
assert_eq!(unsafe { *borrow_element::<f64>(rec) }, 4.5);
assert_eq!(unsafe { read_element::<f64>(rec) }, 4.5);
let mut parked = [0u64; 2];
unsafe { write_element(parked.as_mut_ptr().cast(), String::from("moved once"), &arena) }.unwrap();
let rec = unsafe { Rec::new(parked.as_ptr().cast()) };
assert_eq!(unsafe { borrow_element::<String>(rec) }.as_str(), "moved once");
assert_eq!(unsafe { read_element::<String>(rec) }, "moved once");
}
#[test]
fn record_values_are_one_word() {
assert_eq!(size_of::<RecordValue>(), 8);
assert_eq!(align_of::<RecordValue>(), 8);
}
}
@@ -0,0 +1,185 @@
//! The evaluation's record frame storage.
use super::access::RecordValue;
use super::layout::Layout;
use super::serve::{FrameClaim, SlotRun};
/// The evaluation's record frame space: a grow-only buffer the executor owns
/// and lends by `&mut`, sized by the wiring-derived frame need, so exhaustion
/// is an accounting failure the debug assertion catches rather than a hot-path
/// branch. Frame bytes carry no drop glue, so growth and reuse are plain
/// buffer operations.
#[derive(Debug, Default)]
pub struct FrameArena {
buf: Vec<u64>,
}
impl FrameArena {
pub fn new() -> Self {
Self { buf: Vec::new() }
}
/// Grows the buffer to hold `bytes`, the root's wiring-derived frame need.
/// Grow-only, so repeated evaluations reuse one allocation.
pub fn reserve(&mut self, bytes: usize) {
let words = bytes.div_ceil(8).max(1);
if self.buf.len() < words {
self.buf.resize(words, 0);
}
}
/// The whole buffer as free space, for one evaluation.
pub fn frames(&mut self) -> Frames<'_> {
let base = self.buf.as_mut_ptr().cast::<u8>();
Frames {
base: std::cell::Cell::new(base),
words: std::cell::Cell::new(self.buf.len()),
bounds: (base as usize, self.buf.len() * 8),
_lifetime: std::marker::PhantomData,
}
}
}
/// The free frame space at one point of an evaluation. [`Self::claim`] splits
/// a node's own frame off the front and the claim carries the remainder, so a
/// node's inputs claim beyond its frame, one after another, and the space they
/// used is free again once the claim dies: the release is the claim's
/// lifetime, not a rewind contract. The cursor is shared through `&self` so
/// the lazy inputs a kernel holds claim beyond each other rather than over each
/// other. Covariant in `'e`, so a claim minted at the evaluation shortens onto
/// a derived context's arena lifetime.
pub struct Frames<'e> {
base: std::cell::Cell<*mut u8>,
words: std::cell::Cell<usize>,
/// The whole buffer as (address, bytes), which stays fixed while claims
/// advance the cursor, so a promote can range-check a reference against it.
bounds: (usize, usize),
_lifetime: std::marker::PhantomData<&'e ()>,
}
impl std::fmt::Debug for Frames<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Frames").field("free_words", &self.words.get()).finish()
}
}
impl<'e> Frames<'e> {
/// An independent cursor over the same free space: claims made through it
/// are free again when it dies, so a repeated evaluation reuses one region.
/// Two live cursors hand out the same region, so only one may be claimed
/// from at a time.
pub fn reborrow(&self) -> Frames<'e> {
Frames {
base: std::cell::Cell::new(self.base.get()),
words: std::cell::Cell::new(self.words.get()),
bounds: self.bounds,
_lifetime: std::marker::PhantomData,
}
}
/// The whole frame buffer as (address, bytes), the range a promote treats
/// as evaluation-lived.
pub fn bounds(&self) -> (usize, usize) {
self.bounds
}
/// Runs claims against this space and gives it back on the guard's drop, so
/// a loop that evaluates a subtree per iteration reuses the same region.
pub fn scope(&self) -> FrameScope<'_, 'e> {
FrameScope {
frames: self,
base: self.base.get(),
words: self.words.get(),
}
}
/// The words still free, the observable the frame accounting asserts on.
pub fn free_words(&self) -> usize {
self.words.get()
}
/// Claims `layout`'s frame at the front of the free space; the claim
/// carries the remainder, and an inline layout's record builds in the
/// claim itself.
pub fn claim<'l>(&self, layout: &'l Layout) -> FrameClaim<'e, 'l> {
let frame = match layout.frame_bytes() {
0 => None,
bytes => Some(self.split(bytes)),
};
FrameClaim {
layout,
inline: RecordValue::zeroed(),
frame,
free: self.reborrow(),
filled_fields: false,
}
}
/// Splits `bytes` (rounded to word alignment) off the front.
fn split(&self, bytes: usize) -> *mut u8 {
let words = bytes.div_ceil(8);
debug_assert!(words <= self.words.get(), "record frame space exhausted: the root buffer must cover the graph's frame need");
let frame = self.base.get();
// SAFETY: the buffer covers the wiring-derived need, so the advanced
// cursor stays within it.
self.base.set(unsafe { frame.add(words * 8) });
self.words.set(self.words.get() - words);
frame
}
/// A run of same-layout slots over caller scratch: lanes serve in place,
/// each backed by its own region of the slab, and the collected proofs
/// certify the filled prefix. `None` where the scratch cannot hold `len`
/// lanes.
pub fn run<'a>(&self, scratch: &'a mut [std::mem::MaybeUninit<u64>], len: usize, layout: &'a Layout) -> Option<SlotRun<'a>> {
SlotRun::new(scratch, len, layout)
}
}
/// See [`Frames::scope`]. A forgotten guard leaks its region until the frame
/// space itself dies, rather than releasing it.
pub struct FrameScope<'s, 'e> {
frames: &'s Frames<'e>,
base: *mut u8,
words: usize,
}
impl<'e> std::ops::Deref for FrameScope<'_, 'e> {
type Target = Frames<'e>;
fn deref(&self) -> &Frames<'e> {
self.frames
}
}
impl Drop for FrameScope<'_, '_> {
fn drop(&mut self) {
self.frames.base.set(self.base);
self.frames.words.set(self.words);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::layout::element_write;
use crate::record::test_support::f64_field;
#[test]
fn a_scope_releases_and_reuses_its_frames() {
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")]);
let mut frame_arena = FrameArena::new();
frame_arena.reserve(1 << 10);
let frames = frame_arena.frames();
let free = frames.free_words();
let mut addresses = Vec::new();
for _ in 0..3 {
let scope = frames.scope();
let mut claim = scope.claim(&layout);
addresses.push(claim.dst() as usize);
drop(scope);
assert_eq!(frames.free_words(), free, "the scope returns its claims");
}
assert!(addresses.windows(2).all(|pair| pair[0] == pair[1]), "each claim reuses the same region");
}
}
@@ -0,0 +1,524 @@
//! Consumer-side bindings onto a record input, and the drivers they run.
use super::access::{Rec, RecordValue, read_element};
use super::frames::Frames;
use super::layout::Layout;
use super::serve::{FrameClaim, Served, serve_input};
use crate::gpoll::GPoll;
use crate::node::Node;
/// A record input evaluable at a derived context, yielding the record at that
/// context's lifetime. The lifetime is a trait parameter because a bound like
/// `for<'d> Node<Derived<'d, C>>` cannot also say the derived context's arena
/// is at `'d`: the equality binding `ExtractArena<ArenaRef = &'d Arena>` is an
/// unconstrained position under a higher rank.
pub trait DerivedRecordInput<'derived, C> {
fn eval_derived(&self, cell: &crate::node::StatusCell, input_index: usize, ctx: &C, frames: &Frames<'derived>) -> Result<RecordValue<'derived>, crate::gpoll::Interrupt>;
fn extent_at_derived(&self, ctx: &C, level: u8, frames: &Frames<'derived>) -> GPoll<crate::gpoll::Extent>;
}
impl<'derived, C, N> DerivedRecordInput<'derived, C> for N
where
N: Node<C>,
C: crate::context::ExtractArena<ArenaRef = &'derived crate::arena::Arena>,
{
fn eval_derived(&self, cell: &crate::node::StatusCell, input_index: usize, ctx: &C, frames: &Frames<'derived>) -> Result<RecordValue<'derived>, crate::gpoll::Interrupt> {
cell.eval_input(input_index, self, ctx, frames)
}
fn extent_at_derived(&self, ctx: &C, level: u8, frames: &Frames<'derived>) -> GPoll<crate::gpoll::Extent> {
self.extent_at(ctx, level, frames)
}
}
/// Fills caller scratch with one frame per lane of `range`: the input serves
/// into the lane's own region of the slab, and the lane's own frame space is
/// free again at the next lane, so the frame peak stays at one lane's need and
/// every lane's bytes are distinct.
pub fn fill_frames<'a, 'e, C, N>(node: &'a N, input: &C, range: std::ops::Range<u64>, scratch: Option<&'a mut [std::mem::MaybeUninit<u64>]>, frames: &Frames<'e>) -> crate::node::BatchStatus<'a>
where
C: crate::context::InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
N: Node<C>,
{
use crate::node::BatchStatus;
let Some(scratch) = scratch else {
return BatchStatus::NeedBuffer;
};
let Some(len) = range.end.checked_sub(range.start).and_then(|len| usize::try_from(len).ok()) else {
return BatchStatus::InvalidRange;
};
let Some(mut run) = frames.run(scratch, len, node.layout()) else {
return BatchStatus::InvalidRange;
};
let mut local = *input;
let mut finality = crate::gpoll::Finality::AllFinal;
let mut hint = crate::gpoll::Extent::AtLeast(range.end as usize);
for lane in 0..len {
local.set_index(range.start + lane as u64);
let lane_frames = frames.scope();
let slot = run.slot(lane, &lane_frames);
let served = match node.serve(&local, slot) {
GPoll::Final(served) => served,
GPoll::Partial(served) => {
finality = crate::gpoll::Finality::Partial;
served
}
GPoll::Pending => return BatchStatus::Pending,
GPoll::Fallback(boxed) => return BatchStatus::Error(boxed.1),
// A lane past a lower-bound level ends the data: the fill comes
// back short and the hint turns exact.
GPoll::Error(error) if error.kind == crate::gpoll::ErrorKind::PastEnd => {
hint = crate::gpoll::Extent::Exactly(range.start as usize + lane);
break;
}
GPoll::Error(error) => return BatchStatus::Error(*error),
};
run.served(lane, &served);
}
BatchStatus::Filled(run.finish(), finality, hint)
}
/// The driver a consumer runs on a record input: a resident batch returns with
/// no allocation, a node's own batch impl gets `n * frame_bytes` of arena
/// scratch, and an unbatched input falls back to the [`fill_frames`] loop.
pub fn materialize_batch<'a, 'e, C, N>(node: &'a N, input: &'a C, range: std::ops::Range<u64>, arena: &'a crate::arena::Arena, frames: &Frames<'e>) -> crate::node::BatchStatus<'a>
where
C: crate::context::InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
N: Node<C>,
{
use crate::node::BatchStatus;
let Some(len) = range.end.checked_sub(range.start).and_then(|len| usize::try_from(len).ok()) else {
return BatchStatus::InvalidRange;
};
// Checked: a wrapped product would size the scratch below the run.
let Some(words) = len.checked_mul(node.layout().lane_stride()).map(|bytes| bytes / 8) else {
return BatchStatus::InvalidRange;
};
let exhausted = || {
BatchStatus::Error(crate::gpoll::GraphError {
kind: crate::gpoll::ErrorKind::ArenaExhausted,
trace: Vec::new(),
})
};
match node.eval_batch(input, range.clone(), None, frames) {
BatchStatus::Unbatched => match arena.alloc_scratch::<u64>(words) {
Some(scratch) => fill_frames(node, input, range, Some(scratch), frames),
None => exhausted(),
},
BatchStatus::NeedBuffer => match arena.alloc_scratch::<u64>(words) {
Some(scratch) => node.eval_batch(input, range, Some(scratch), frames),
None => exhausted(),
},
status => status,
}
}
/// The outcome of materializing a leveled input's whole flat span.
pub enum LevelStatus<'a> {
Batch(crate::node::RecordBatch<'a>, crate::gpoll::Finality),
Pending,
Error(crate::gpoll::GraphError),
}
/// Evaluates a leveled input's whole flat span into one batch: an exact total
/// fills once, a lower bound drains by guess-and-double until a short fill,
/// each reply's hint seeding the next guess. The boundary consumers' driver;
/// reducers inline the same protocol with their span offsets.
pub fn materialize_level<'a, 'e, C, N>(node: &'a N, input: &'a C, arena: &'a crate::arena::Arena, frames: &Frames<'e>) -> LevelStatus<'a>
where
C: crate::context::InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
N: Node<C>,
{
use crate::gpoll::{Extent, GraphError, Level};
use crate::node::BatchStatus;
let sized = match node.extent(input, Level::Total, frames) {
GPoll::Final(Extent::Exactly(count)) => Ok(count),
GPoll::Final(Extent::AtLeast(bound)) => Err(bound),
GPoll::Pending => return LevelStatus::Pending,
_ => return LevelStatus::Error(GraphError::new("materialize over a non-exact extent")),
};
match sized {
Ok(count) => match materialize_batch(node, input, 0..count as u64, arena, frames) {
BatchStatus::Lent(batch, finality, _) => LevelStatus::Batch(batch, finality),
BatchStatus::Filled(batch, finality, _) => LevelStatus::Batch(batch.into_shared(), finality),
BatchStatus::Pending => LevelStatus::Pending,
BatchStatus::Error(error) => LevelStatus::Error(error),
_ => LevelStatus::Error(GraphError::new("materialize batch failed")),
},
Err(bound) => {
let mut guess = bound.max(16);
loop {
let (batch, finality, hint) = match materialize_batch(node, input, 0..guess as u64, arena, frames) {
BatchStatus::Lent(batch, finality, hint) => (batch, finality, hint),
BatchStatus::Filled(batch, finality, hint) => (batch.into_shared(), finality, hint),
BatchStatus::Pending => return LevelStatus::Pending,
BatchStatus::Error(error) => return LevelStatus::Error(error),
_ => return LevelStatus::Error(GraphError::new("materialize batch failed")),
};
let filled = batch.len();
if filled < guess {
break LevelStatus::Batch(batch, finality);
}
match hint {
Extent::Exactly(total) if total <= filled => break LevelStatus::Batch(batch, finality),
Extent::Exactly(total) => guess = total,
Extent::AtLeast(more) => guess = (guess * 2).max(more),
Extent::Free => guess *= 2,
}
}
}
}
}
/// The raw lazy record input handed to a record-opaque kernel: the input plus
/// its wiring-proven layout, the pairing the kernel's unsafe record
/// operations rely on. The kernel must only pair the layout with values this
/// input produced.
pub struct RecordInput<'a, 'e, N> {
node: &'a N,
layout: &'a Layout,
frames: &'a Frames<'e>,
}
impl<'a, 'e, N> RecordInput<'a, 'e, N> {
pub fn new(node: &'a N, layout: &'a Layout, frames: &'a Frames<'e>) -> Self {
Self { node, layout, frames }
}
pub fn layout(&self) -> &Layout {
self.layout
}
/// Serves the input through the kernel's own claim: the kernel's output
/// layout is the input's, so the claim it was handed is the input's frame.
pub fn serve<'l, C>(&self, ctx: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
N: Node<C>,
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
self.node.serve(ctx, slot)
}
/// [`materialize_level`] over the input: the input's whole flat span as one
/// batch.
pub fn materialize_level<'b, C>(&'b self, ctx: &'b C, arena: &'b crate::arena::Arena) -> LevelStatus<'b>
where
N: Node<C>,
C: crate::context::InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
materialize_level(self.node, ctx, arena, self.frames)
}
}
/// # Safety
/// `rec` must be a record of the layout the offsets were resolved against
/// and `El` its element type; both are proven at wiring.
unsafe fn element_only<El: Clone>(rec: Rec<'_>, _reads: &[Option<usize>]) -> El {
// SAFETY: the caller's contract.
unsafe { read_element::<El>(rec) }
}
/// The raw lazy input handed to a poll kernel whose input rides records while
/// the kernel consumes the plain element.
pub struct ElementInput<'a, 'e, Out, N> {
node: &'a N,
layout: &'a Layout,
reads: &'a [Option<usize>],
read: unsafe fn(Rec<'_>, &[Option<usize>]) -> Out,
frames: &'a Frames<'e>,
}
impl<'a, 'e, El: Clone, N> ElementInput<'a, 'e, El, N> {
pub fn new(node: &'a N, layout: &'a Layout, frames: &'a Frames<'e>) -> Self {
Self {
node,
layout,
reads: &[],
read: element_only::<El>,
frames,
}
}
}
impl<'a, 'e, Out, N> ElementInput<'a, 'e, Out, N> {
/// `read` must be sound against the layout the offsets in `reads` were
/// resolved from; the macro proves both at wiring.
pub fn with_reads(node: &'a N, layout: &'a Layout, reads: &'a [Option<usize>], read: unsafe fn(Rec<'_>, &[Option<usize>]) -> Out, frames: &'a Frames<'e>) -> Self {
Self { node, layout, reads, read, frames }
}
/// The input's element at `ctx`, read out of a record claimed beyond the
/// kernel's own frame; the claim dies with the call, so the record is free
/// again at the next one.
pub fn eval<'d, C>(&self, ctx: &C) -> GPoll<Out>
where
N: DerivedRecordInput<'d, C>,
'e: 'd,
{
let cell = crate::node::StatusCell::new();
let scope = self.frames.scope();
match self.node.eval_derived(&cell, 0, ctx, &scope) {
// SAFETY: the read copies out by value against the input's own layout.
Ok(value) => cell.finish(unsafe { (self.read)(self.layout.rec(&value), self.reads) }),
Err(interrupt) => interrupt.into(),
}
}
}
/// The lazy input handed to a kernel whose input rides a record while
/// the kernel consumes the plain element, or the element beside its declared
/// attribute reads.
#[derive(Clone, Copy)]
pub struct ElementLazyInput<'a, 'e, Out, N> {
node: &'a N,
cell: &'a crate::node::StatusCell,
input_index: usize,
layout: &'a Layout,
reads: &'a [Option<usize>],
read: unsafe fn(Rec<'_>, &[Option<usize>]) -> Out,
frames: &'a Frames<'e>,
}
impl<'a, 'e, El: Clone, N> ElementLazyInput<'a, 'e, El, N> {
pub fn new(node: &'a N, cell: &'a crate::node::StatusCell, input_index: usize, layout: &'a Layout, frames: &'a Frames<'e>) -> Self {
Self {
node,
cell,
input_index,
layout,
reads: &[],
read: element_only::<El>,
frames,
}
}
}
impl<'a, 'e, Out, N> ElementLazyInput<'a, 'e, Out, N> {
/// `read` must be sound against the layout the offsets in `reads` were
/// resolved from; the macro proves both at wiring.
pub fn with_reads(
node: &'a N,
cell: &'a crate::node::StatusCell,
input_index: usize,
layout: &'a Layout,
reads: &'a [Option<usize>],
read: unsafe fn(Rec<'_>, &[Option<usize>]) -> Out,
frames: &'a Frames<'e>,
) -> Self {
Self {
node,
cell,
input_index,
layout,
reads,
read,
frames,
}
}
/// The read copies the element and declared attributes out by value, so
/// the record's claim dies with the call.
pub fn eval<'d, C>(&self, ctx: &C) -> Result<Out, crate::gpoll::Interrupt>
where
N: DerivedRecordInput<'d, C>,
'e: 'd,
{
let scope = self.frames.scope();
let value = self.node.eval_derived(self.cell, self.input_index, ctx, &scope)?;
// SAFETY: the reads are the input's own layout's, resolved at wiring.
Ok(unsafe { (self.read)(self.layout.rec(&value), self.reads) })
}
}
/// The lazy record input handed to a kernel that evaluates its inputs under
/// derived contexts: evaluating rebinds the record to the kernel's routing
/// lifetime, so the value escapes the derivation scope.
#[derive(Clone, Copy)]
pub struct RecordLazyInput<'a, 'e, N> {
node: &'a N,
cell: &'a crate::node::StatusCell,
input_index: usize,
inner_levels: u8,
frames: &'a Frames<'e>,
}
impl<'a, 'e, N> RecordLazyInput<'a, 'e, N> {
pub fn new(node: &'a N, cell: &'a crate::node::StatusCell, input_index: usize, inner_levels: u8, frames: &'a Frames<'e>) -> Self {
Self {
node,
cell,
input_index,
inner_levels,
frames,
}
}
pub fn eval<'d, C>(&self, ctx: &C) -> Result<RecordValue<'e>, crate::gpoll::Interrupt>
where
N: DerivedRecordInput<'d, C>,
'e: 'd,
{
Ok(self.node.eval_derived(self.cell, self.input_index, ctx, self.frames)?.rebind())
}
/// The flat lane count of one copy: the product of the input's inner-level
/// extents, queried uniform across copies (at copy 0). The dividend of a
/// structure node's decompose-and-promote.
pub fn inner_extent<B>(&self, ctx: &B) -> Result<u64, crate::gpoll::Interrupt>
where
B: crate::context::DeriveCtx,
N: for<'d> DerivedRecordInput<'d, crate::context::Derived<'d, B>>,
{
inner_extent_of(self.node, ctx, 0, self.inner_levels, self.input_index, self.frames)
}
/// The flat lane count of the copy at `copy`, for inputs whose inner
/// extents vary per copy.
pub fn inner_extent_at<B>(&self, ctx: &B, copy: u64) -> Result<u64, crate::gpoll::Interrupt>
where
B: crate::context::DeriveCtx,
N: for<'d> DerivedRecordInput<'d, crate::context::Derived<'d, B>>,
{
inner_extent_of(self.node, ctx, copy, self.inner_levels, self.input_index, self.frames)
}
}
/// See [`RecordLazyInput::inner_extent`].
fn inner_extent_of<B, N>(node: &N, ctx: &B, copy: u64, levels: u8, input_index: usize, frames: &Frames<'_>) -> Result<u64, crate::gpoll::Interrupt>
where
B: crate::context::DeriveCtx,
N: for<'d> DerivedRecordInput<'d, crate::context::Derived<'d, B>>,
{
let mut frame = crate::context::IndexLink { index: 0, outer: None };
let derived = ctx.push_level(&mut frame, copy, 0);
let mut inner: u64 = 1;
for level in 0..levels {
match node.extent_at_derived(&derived, level, frames) {
GPoll::Final(crate::gpoll::Extent::Exactly(count)) => inner *= count as u64,
GPoll::Final(crate::gpoll::Extent::AtLeast(_)) => return probed_inner(node, ctx, copy, input_index, frames),
GPoll::Pending => return Err(crate::gpoll::Interrupt::Pending),
_ => return Err(crate::gpoll::GraphError::new("structure decomposition over a non-exact extent").into()),
}
}
Ok(inner)
}
/// The flat lane count of one copy of a lower-bound input, probed by
/// evaluating lanes to the past-end signal. The probed records are
/// discarded, and their statuses land in a scratch cell.
fn probed_inner<B, N>(node: &N, ctx: &B, copy: u64, input_index: usize, frames: &Frames<'_>) -> Result<u64, crate::gpoll::Interrupt>
where
B: crate::context::DeriveCtx,
N: for<'d> DerivedRecordInput<'d, crate::context::Derived<'d, B>>,
{
let cell = crate::node::StatusCell::new();
let mut count: u64 = 0;
loop {
// The probed record is discarded, so the probe's claim is free again
// at the next iteration.
let probe_frames = frames.scope();
let mut frame = crate::context::IndexLink { index: 0, outer: None };
let probe = ctx.push_level(&mut frame, copy, count);
let result = node.eval_derived(&cell, input_index, &probe, &probe_frames);
match result {
Ok(_) => count += 1,
Err(crate::gpoll::Interrupt::Error(error)) if error.kind == crate::gpoll::ErrorKind::PastEnd => return Ok(count),
Err(interrupt) => return Err(interrupt),
}
}
}
/// The derive-routing carrier beside its declared attribute reads: evaluating
/// at a derived context yields the opaque row token and the read values in one
/// step, so the kernel drives the per-copy eval while reads stay resolved
/// against the source's wired layout.
#[derive(Clone, Copy)]
pub struct DerivedLazyInput<'a, 'e, Out, N> {
node: &'a N,
cell: &'a crate::node::StatusCell,
input_index: usize,
inner_levels: u8,
reads: &'a [Option<usize>],
read: unsafe fn(Rec<'_>, &[Option<usize>]) -> Out,
frames: &'a Frames<'e>,
}
impl<'a, 'e, Out, N> DerivedLazyInput<'a, 'e, Out, N> {
/// `read` must be sound against the layout the offsets in `reads` were
/// resolved from; the macro proves both at wiring.
pub fn new(
node: &'a N,
cell: &'a crate::node::StatusCell,
input_index: usize,
inner_levels: u8,
reads: &'a [Option<usize>],
read: unsafe fn(Rec<'_>, &[Option<usize>]) -> Out,
frames: &'a Frames<'e>,
) -> Self {
Self {
node,
cell,
input_index,
inner_levels,
reads,
read,
frames,
}
}
/// The flat lane count of one copy; see [`RecordLazyInput::inner_extent`].
pub fn inner_extent<B>(&self, ctx: &B) -> Result<u64, crate::gpoll::Interrupt>
where
B: crate::context::DeriveCtx,
N: for<'d> DerivedRecordInput<'d, crate::context::Derived<'d, B>>,
{
inner_extent_of(self.node, ctx, 0, self.inner_levels, self.input_index, self.frames)
}
pub fn eval<'d, C>(&self, ctx: &C) -> Result<Out, crate::gpoll::Interrupt>
where
N: DerivedRecordInput<'d, C>,
'e: 'd,
{
let value: RecordValue<'e> = self.node.eval_derived(self.cell, self.input_index, ctx, self.frames)?.rebind();
// SAFETY: declared reads imply a non-empty layout, so the record is
// spilled and its pointer is the frame the offsets index into.
Ok(unsafe { (self.read)(Rec::new(value.ptr), self.reads) })
}
}
/// A plain probe over a record input, cloning the element out of the parked
/// reference when it carries drop glue. Registry constructors wrap a record
/// input in one to feed a node's plain value input, keeping the input kind
/// uniform.
pub struct RecordExtract<El, N> {
edge: N,
layout: Layout,
_marker: std::marker::PhantomData<fn() -> El>,
}
impl<El, N> RecordExtract<El, N> {
pub fn new(edge: N, layout: &Layout) -> Self {
Self {
edge,
layout: layout.clone(),
_marker: std::marker::PhantomData,
}
}
}
impl<El: Clone + 'static, N> RecordExtract<El, N> {
/// The input's element, copied out of its record.
pub fn eval<'e, C>(&self, input: &C, frames: &Frames<'e>) -> GPoll<El>
where
N: Node<C>,
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
// The element copies out by value, so the input's claim dies with
// the scope.
let scope = frames.scope();
// SAFETY: the served value is a record of `self.layout`, whose element is
// `El` by the wiring that built this extract.
serve_input(&self.edge, input, &scope).map(|value| unsafe { read_element::<El>(self.layout.rec(&value)) })
}
}
@@ -0,0 +1,742 @@
//! Wiring-time shape facts: the layout a record takes and the writes it folds from.
use super::access::{Rec, RecordValue, borrow_element, erase_static, read_element, write_element_keyed};
use super::owned::deep_element_glue;
use super::promote::{Promotion, retained_measure};
use crate::attribute;
/// A field write declared at wiring, carrying the marker's erased-read glue
/// so introspection and persistence never consult the census at runtime.
#[derive(Clone, Copy, Debug)]
pub struct FieldWrite {
pub name: &'static str,
pub level: u8,
pub size: usize,
pub align: usize,
pub type_id: std::any::TypeId,
pub read_erased: unsafe fn(*const u8) -> Box<dyn crate::list::AnyAttributeValue>,
pub repark: Option<crate::list::ReparkFn>,
/// Hashes the field's content. `None` means the stored bytes are the
/// content, which holds for every unparked value.
pub content_hash: Option<unsafe fn(*const u8, &mut dyn core::hash::Hasher)>,
/// Compares two fields' content. `None` as for `content_hash`.
pub content_eq: Option<unsafe fn(*const u8, *const u8) -> bool>,
}
impl FieldWrite {
pub fn of<A: crate::attribute::Attribute>(level: u8) -> Self
where
A::Value<'static>: graphene_hash::CacheHash + PartialEq + 'static,
{
/// # Safety
/// `ptr` must address a live `V`.
unsafe fn content_hash<V: graphene_hash::CacheHash>(ptr: *const u8, state: &mut dyn core::hash::Hasher) {
let mut state = state;
// SAFETY: the caller's contract.
unsafe { &*ptr.cast::<V>() }.cache_hash(&mut state);
}
/// # Safety
/// `a` and `b` must both address a live `V`.
unsafe fn content_eq<V: PartialEq>(a: *const u8, b: *const u8) -> bool {
// SAFETY: the caller's contract.
unsafe { *a.cast::<V>() == *b.cast::<V>() }
}
Self {
name: A::NAME,
level,
size: size_of::<A::Value<'static>>(),
align: align_of::<A::Value<'static>>(),
type_id: std::any::TypeId::of::<A::Value<'static>>(),
read_erased: A::read_erased,
repark: A::REPARK,
content_hash: Some(content_hash::<A::Value<'static>>),
content_eq: Some(content_eq::<A::Value<'static>>),
}
}
}
/// One field of a [`Layout`]: a (name, level) key resolved to an offset.
/// Levels are numbered innermost-out; only level 0 exists at rank 0.
/// Equality is structural over the field's identity, its value type
/// included; only the glue pointers are excluded, since fn-pointer identity
/// is not guaranteed across codegen units and layout equality drives
/// identity forwarding.
#[derive(Clone, Debug)]
pub struct FieldDesc {
pub name: &'static str,
pub level: u8,
pub offset: usize,
pub size: usize,
pub align: usize,
pub type_id: std::any::TypeId,
pub read_erased: unsafe fn(*const u8) -> Box<dyn crate::list::AnyAttributeValue>,
pub repark: Option<crate::list::ReparkFn>,
/// Hashes the field's content. `None` means the stored bytes are the
/// content, which holds for every unparked value.
pub content_hash: Option<unsafe fn(*const u8, &mut dyn core::hash::Hasher)>,
/// Compares two fields' content. `None` as for `content_hash`.
pub content_eq: Option<unsafe fn(*const u8, *const u8) -> bool>,
}
impl PartialEq for FieldDesc {
fn eq(&self, other: &Self) -> bool {
(self.name, self.level, self.offset, self.size, self.align, self.type_id) == (other.name, other.level, other.offset, other.size, other.align, other.type_id)
}
}
impl FieldDesc {
/// The write this field re-declares when a layout's fields fold into
/// another layout.
pub fn as_write(&self) -> FieldWrite {
FieldWrite {
name: self.name,
level: self.level,
size: self.size,
align: self.align,
type_id: self.type_id,
read_erased: self.read_erased,
repark: self.repark,
content_hash: self.content_hash,
content_eq: self.content_eq,
}
}
}
impl Eq for FieldDesc {}
/// The element slot of a layout: its dimensions plus erased glue bound where
/// the element type is statically known, so generic consumers read or
/// deep-copy the element without it. Equality is structural over the
/// element's identity, its type included; glue pointers are excluded for the
/// same reason as [`FieldDesc`]'s.
#[derive(Clone, Copy, Debug)]
pub struct ElementWrite {
pub size: usize,
pub align: usize,
pub parked: bool,
pub type_id: std::any::TypeId,
pub clone_out: unsafe fn(*const u8) -> Box<dyn std::any::Any + Send + Sync>,
pub repark: unsafe fn(&(dyn std::any::Any + Send + Sync), *mut u8, &crate::arena::Arena) -> Option<()>,
/// Moves a parked payload's header into the persistent region rather than
/// cloning the heap it owns, returning the new header. `None` declines,
/// which leaves the caller its clone path.
pub park_move: unsafe fn(*const u8, &Promotion<'_>) -> Option<*const u8>,
/// Hashes the element's content. `None` means the stored bytes are the
/// content, which holds for every unparked element.
pub content_hash: Option<unsafe fn(*const u8, &mut dyn core::hash::Hasher)>,
/// Compares two elements' content. `None` as for `content_hash`.
pub content_eq: Option<unsafe fn(*const u8, *const u8) -> bool>,
}
impl PartialEq for ElementWrite {
fn eq(&self, other: &Self) -> bool {
(self.size, self.align, self.parked, self.type_id) == (other.size, other.align, other.parked, other.type_id)
}
}
impl Eq for ElementWrite {}
impl Default for ElementWrite {
fn default() -> Self {
/// # Safety
/// None: the empty element has no bytes, so nothing is read.
unsafe fn clone_out(_ptr: *const u8) -> Box<dyn std::any::Any + Send + Sync> {
Box::new(())
}
/// # Safety
/// None: nothing is written, the empty element having no slot.
unsafe fn repark(_value: &(dyn std::any::Any + Send + Sync), _dst: *mut u8, _arena: &crate::arena::Arena) -> Option<()> {
Some(())
}
/// # Safety
/// None: the move always declines, so `parked` is never read.
unsafe fn park_move(_parked: *const u8, _promotion: &Promotion<'_>) -> Option<*const u8> {
None
}
Self {
size: 0,
align: 0,
parked: false,
type_id: std::any::TypeId::of::<()>(),
clone_out,
repark,
park_move,
content_hash: None,
content_eq: None,
}
}
}
/// The widest alignment a record may need. Frames are a `Vec<u64>`, lanes
/// stride at a multiple of 8, and run slabs come from `alloc_scratch::<u64>`,
/// so nothing below a record can promise more.
pub const MAX_ALIGN: usize = 8;
/// A record layout: the element at offset 0, then the written attributes in
/// canonical order (descending alignment, then size, then name, then level).
/// Layouts are derived data, a pure function of the upstream write set.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Layout {
pub depth: u8,
pub element: ElementWrite,
pub fields: Vec<FieldDesc>,
pub size: usize,
pub align: usize,
}
impl Layout {
pub fn offset_of(&self, name: &str, level: u8) -> Option<usize> {
self.fields.iter().find(|field| field.name == name && field.level == level).map(|field| field.offset)
}
pub fn frame_bytes(&self) -> usize {
self.size.next_multiple_of(8)
}
/// One batch lane's stride: a spilled record's frame, or the value itself
/// for records this layout keeps inline (`size == 0`), whose payload rides
/// in the `RecordValue`'s own storage exactly as [`Layout::rec`] resolves.
pub fn lane_stride(&self) -> usize {
match self.size == 0 {
true => size_of::<RecordValue<'static>>(),
false => self.frame_bytes(),
}
}
/// Resolves a value of this layout, which must be its wiring-proven one,
/// to its record bytes. An empty record carries nothing and resolves to the
/// value's own storage; every other record spills and rides the pointer.
pub fn rec<'v>(&self, value: &'v RecordValue<'_>) -> Rec<'v> {
match self.size == 0 {
true => Rec((&raw const *value).cast(), std::marker::PhantomData),
false => Rec(value.ptr, std::marker::PhantomData),
}
}
/// The union of this layout's fields and `writes` over `element` at
/// `depth`, in canonical order. A (name, level) written at a different
/// size is a type conflict and panics, as does an element or attribute
/// wider than [`MAX_ALIGN`]; the census keeps declared names to one type,
/// so these only fire on wiring bugs.
pub fn with_writes(&self, depth: u8, element: ElementWrite, writes: &[FieldWrite]) -> Layout {
let mut merged: Vec<FieldWrite> = self.fields.iter().map(FieldDesc::as_write).collect();
for &write in writes {
match merged.iter().find(|field| field.name == write.name && field.level == write.level) {
Some(existing) => assert_eq!(existing.type_id, write.type_id, "attribute `{}` written at two different types", write.name),
None => merged.push(write),
}
}
assert!(element.align <= MAX_ALIGN, "a record element aligns to at most {MAX_ALIGN} bytes, but this one needs {}", element.align);
for write in &merged {
assert!(write.align <= MAX_ALIGN, "attribute `{}` aligns to at most {MAX_ALIGN} bytes, but needs {}", write.name, write.align);
}
merged.sort_by(|a, b| b.align.cmp(&a.align).then(b.size.cmp(&a.size)).then(a.name.cmp(b.name)).then(a.level.cmp(&b.level)));
let mut offset = element.size;
let mut align = element.align.max(1);
let fields = merged
.into_iter()
.map(|write| {
offset = offset.next_multiple_of(write.align.max(1));
align = align.max(write.align);
let desc = FieldDesc {
name: write.name,
level: write.level,
offset,
size: write.size,
align: write.align,
type_id: write.type_id,
read_erased: write.read_erased,
repark: write.repark,
content_hash: write.content_hash,
content_eq: write.content_eq,
};
offset += write.size;
desc
})
.collect();
Layout {
depth,
element,
fields,
size: offset,
align,
}
}
/// This layout minus the named fields, offsets recomputed. Removing an
/// absent name is a no-op: downstream reads yield the default either way.
pub fn without(&self, removes: &[(&str, u8)]) -> Layout {
let retained: Vec<FieldWrite> = self.fields.iter().filter(|field| !removes.contains(&(field.name, field.level))).map(FieldDesc::as_write).collect();
Layout::default().with_writes(self.depth, self.element, &retained)
}
/// The union of several layouts over the same element and depth.
pub fn union(layouts: &[&Layout]) -> Layout {
let first = layouts.first().expect("a union needs at least one layout");
// A shallower source joins the union lifted: a scalar concatenates as
// one lane, its fields sitting at the innermost level like any lane's.
let depth = layouts.iter().map(|layout| layout.depth).max().unwrap_or(first.depth);
let mut union = Layout::default().with_writes(depth, first.element, &[]);
for layout in layouts {
assert_eq!(union.element, layout.element, "union layouts must share the element");
let writes: Vec<FieldWrite> = layout.fields.iter().map(FieldDesc::as_write).collect();
union = union.with_writes(union.depth, union.element, &writes);
}
union
}
}
/// A field handle minted once against a layout: the marker's (name, level)
/// resolved to a field index and offset, with the marker-to-type proof taken
/// there. Resolving it against a layout re-checks that one index instead of
/// scanning names, so a token paired with any other layout resolves to
/// nothing rather than to the wrong bytes.
pub struct FieldOffset<A: attribute::Attribute> {
index: usize,
offset: usize,
level: u8,
marker: std::marker::PhantomData<fn() -> A>,
}
impl<A: attribute::Attribute> Clone for FieldOffset<A> {
fn clone(&self) -> Self {
*self
}
}
impl<A: attribute::Attribute> Copy for FieldOffset<A> {}
impl<A: attribute::Attribute> std::fmt::Debug for FieldOffset<A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FieldOffset").field("name", &A::NAME).field("offset", &self.offset).field("level", &self.level).finish()
}
}
impl<A: attribute::Attribute> FieldOffset<A> {
/// The marker's field at `level`, `None` where the layout does not carry
/// it. Panics where the layout declares the name at another value type,
/// which the census forbids.
pub fn of(layout: &Layout, level: u8) -> Option<Self> {
let (index, field) = layout.fields.iter().enumerate().find(|(_, field)| field.name == A::NAME && field.level == level)?;
assert_eq!(field.type_id, std::any::TypeId::of::<A::Value<'static>>(), "attribute `{}` is declared at another value type", A::NAME);
Some(Self {
index,
offset: field.offset,
level,
marker: std::marker::PhantomData,
})
}
pub fn offset(self) -> usize {
self.offset
}
/// The offset this token names in `layout`, `None` unless `layout` is the
/// one it was minted against. The field's value type is re-checked, so a
/// resolved offset carries the marker-to-type proof into `layout`.
pub fn resolve(self, layout: &Layout) -> Option<usize> {
let field = layout.fields.get(self.index)?;
let same = field.offset == self.offset && field.level == self.level && field.name == A::NAME && field.type_id == std::any::TypeId::of::<A::Value<'static>>();
same.then_some(self.offset)
}
}
/// The stand-in fed to a kernel's unbounded `element: T` parameter. The type
/// system forces the kernel to route it to the element position of its return
/// tuple, so the passthrough is explicit in the signature while the lowering
/// carries the element bytes untyped through the copy plan.
#[derive(Clone, Copy, Debug, Default)]
pub struct ElToken;
/// The shared empty layout: `depth` 0, no element, no fields, so `frame_bytes`
/// is 0. The `Node::layout` default returns it for element-only and test nodes,
/// which carry no record.
pub fn empty_layout() -> &'static Layout {
static EMPTY: std::sync::OnceLock<Layout> = std::sync::OnceLock::new();
EMPTY.get_or_init(Layout::default)
}
/// A record node's output layout with the frame size and carrier copy plan derived from it.
#[derive(Clone, Debug, Default)]
pub struct RecordLayout {
pub layout: Layout,
pub frame_bytes: usize,
pub plan: Vec<(usize, usize, usize)>,
/// Inputs whose value cannot change with the innermost index, as a bitmask
/// over input positions. Empty is the safe default: an uninstalled layout
/// rebinds every input per lane.
pub lane_invariant: u32,
}
/// Declarative record-io metadata for a node type, emitted by the macro into
/// its registry entry so the compiler can fold each input's layout without
/// running the node's constructor. [`fold`](LayoutMeta::fold) reproduces the
/// layout the constructor derives at wiring today; the compiler layout pass
/// calls it over the proto graph instead.
#[derive(Clone, Debug)]
pub struct LayoutMeta {
/// Input indices whose layouts union to form the base: `[]` writes a fresh
/// record, `[i]` derives from a single carrier, `[i, j, ..]` unions routing
/// sources.
pub sources: Vec<u8>,
/// Attributes read from each input. Unused by [`fold`](LayoutMeta::fold);
/// recorded for later compiler analysis (read-offset resolution, per-name
/// cache dependencies, residency).
pub reads: Vec<InputReads>,
/// The output element: a concrete write, or carried through from the base.
pub element: ElementSpec,
/// The attributes the node writes at its acting level.
pub writes: Vec<FieldWrite>,
/// The attributes removed from the base layout, as `(name, level)`.
pub removes: Vec<(&'static str, u8)>,
/// The depth change the node applies: `0` for elementwise and flip nodes,
/// `+1` for a creator, `-1` for a reducer.
pub level_delta: i8,
/// The materialized subject a reducer folds, as `(input, levels)`. The fold
/// consumes the whole subject input, so only the node's own levels remain.
pub folded: Option<(u8, u8)>,
}
/// The attributes a node reads from one input, recorded on [`LayoutMeta`] for
/// later compiler analysis. A read and a write of an attribute carry the same
/// [`FieldWrite`] descriptor; the direction is the position on the node.
#[derive(Clone, Debug)]
pub struct InputReads {
pub input: u8,
pub reads: Vec<FieldWrite>,
}
/// Where a node's output element comes from, for [`LayoutMeta`].
#[derive(Clone, Debug)]
pub enum ElementSpec {
/// The node writes this concrete element.
Concrete(ElementWrite),
/// The node carries the carrier's element through unchanged.
Carried,
}
impl LayoutMeta {
/// Keeps input 0's layout but replaces its element.
pub fn retype(element: ElementWrite) -> Self {
Self {
sources: vec![0],
reads: Vec::new(),
element: ElementSpec::Concrete(element),
writes: Vec::new(),
removes: Vec::new(),
level_delta: 0,
folded: None,
}
}
/// Folds the node's output layout from its inputs', reproducing what the
/// node's constructor derives at wiring. `inputs` is indexed by proto-input
/// position; [`sources`](LayoutMeta::sources) selects the base layouts, which
/// union (empty writes a fresh record).
pub fn fold(&self, inputs: &[Option<&Layout>]) -> Layout {
let sources: Vec<&Layout> = self.sources.iter().map(|&i| inputs[i as usize].expect("layout fold source input has no layout")).collect();
let base = match sources.as_slice() {
[] => Layout::default(),
sources => Layout::union(sources),
}
.without(&self.removes);
let depth = match self.folded {
// A fold consumes the whole subject input (a deeper input folds its
// total flat span), so only the node's own levels remain.
Some(_) => self.level_delta.max(0) as u8,
None => (base.depth as i8 + self.level_delta).max(0) as u8,
};
let element = match &self.element {
ElementSpec::Concrete(element) => *element,
ElementSpec::Carried => base.element,
};
base.with_writes(depth, element, &self.writes)
}
/// [`fold`](LayoutMeta::fold) with the frame size and carrier copy plan derived from it.
pub fn resolve(&self, inputs: &[Option<&Layout>]) -> RecordLayout {
let layout = self.fold(inputs);
let frame_bytes = layout.frame_bytes();
let plan = match self.sources.first() {
Some(&source) if self.level_delta >= 0 => {
let from = inputs[source as usize].expect("layout resolve source input has no layout");
let carry_element = matches!(self.element, ElementSpec::Carried);
let removes: Vec<(&str, u8)> = self.removes.clone();
copy_plan(from, &layout, carry_element, &removes)
}
// A reducer collapses its carrier's levels, so it writes a fresh record rather than copying fields down.
_ => Vec::new(),
};
RecordLayout {
layout,
frame_bytes,
plan,
lane_invariant: 0,
}
}
}
/// Field-by-field carry from `from`'s layout into `to`'s, computed at
/// wiring. The element copy is included when `carry_element` holds, which is
/// exactly when the node does not write a concrete element itself. `removes`
/// names the fields the node deletes, which are exactly the ones allowed to
/// be absent from `to`.
pub fn copy_plan(from: &Layout, to: &Layout, carry_element: bool, removes: &[(&str, u8)]) -> Vec<(usize, usize, usize)> {
let mut plan = Vec::new();
if carry_element {
assert_eq!(from.element.size, to.element.size, "a carried element must keep its size");
if from.element.size > 0 {
plan.push((0, 0, from.element.size));
}
}
for field in &from.fields {
if removes.contains(&(field.name, field.level)) {
continue;
}
let target = to.offset_of(field.name, field.level).expect("carried field missing from the output layout");
plan.push((field.offset, target, field.size));
}
plan
}
/// Whether elements of `T` move once into the arena and ride as references:
/// records byte-copy their contents and never run drop glue, so a type is
/// byte-carried exactly when it has none.
pub const fn element_parked<T>() -> bool {
std::mem::needs_drop::<T>()
}
/// The element (size, align) a record input of `T` carries.
pub fn element_dims<T>() -> (usize, usize) {
match element_parked::<T>() {
true => (size_of::<*const u8>(), align_of::<*const u8>()),
false => (size_of::<T>(), align_of::<T>()),
}
}
/// The element slot a record input of `T` carries, its erased glue bound at
/// the statically-known type.
pub fn element_write<T: Clone + Send + Sync + dyn_any::StaticTypeSized>() -> ElementWrite
where
T::Static: Clone + Send + Sync,
{
/// # Safety
/// `ptr` must address a live element of `T` in the form [`element_parked`] picks.
unsafe fn clone_out<T: Clone + Send + Sync + dyn_any::StaticTypeSized>(ptr: *const u8) -> Box<dyn std::any::Any + Send + Sync>
where
T::Static: Clone + Send + Sync,
{
if let Some(deep) = deep_element_glue(std::any::TypeId::of::<T::Static>()) {
// SAFETY: the caller's contract; the glue is registered for this element type.
return unsafe { (deep.clone_out)(ptr) };
}
// SAFETY: a lifetime-carrying element type registers deep glue, so this
// shallow path only erases borrow-free values. The registration is a
// whole-program convention rather than something this call can check:
// the executor asserts the in-tree types registered before it evaluates,
// which is where a missed wasm registration export is caught.
Box::new(unsafe { erase_static(read_element::<T>(Rec::new(ptr))) })
}
/// # Safety
/// `dst` must be fresh element storage of a record whose element is `T`, and
/// `value` must be the clone-out this glue produced for that element type.
unsafe fn repark<T: Clone + Send + Sync + dyn_any::StaticTypeSized>(value: &(dyn std::any::Any + Send + Sync), dst: *mut u8, arena: &crate::arena::Arena) -> Option<()>
where
T::Static: Clone + Send + Sync,
{
if let Some(deep) = deep_element_glue(std::any::TypeId::of::<T::Static>()) {
// SAFETY: the caller's contract; the glue is registered for this element type.
return unsafe { (deep.repark)(value, dst, arena) };
}
let retained = retained_measure(std::any::TypeId::of::<T::Static>()).map_or(0, |measure| measure(value));
let value = value.downcast_ref::<T::Static>().expect("an element replays at its own type");
// SAFETY: the caller's contract, keyed as this element's own parks are.
unsafe { write_element_keyed(dst, value.clone(), arena, retained, std::any::TypeId::of::<T::Static>()) }
}
/// Declines for a type carrying deep glue, whose value may hold interiors
/// the evaluation's arena owns and which a moved header may not reference.
///
/// # Safety
/// `parked` must address a live payload of `T`.
unsafe fn park_move<T: Clone + Send + Sync + dyn_any::StaticTypeSized>(parked: *const u8, promotion: &Promotion<'_>) -> Option<*const u8>
where
T::Static: Clone + Send + Sync,
{
deep_element_glue(std::any::TypeId::of::<T::Static>()).is_none().then_some(())?;
// SAFETY: the caller's contract, at the type the park was written with.
let value: &(dyn std::any::Any + Send + Sync) = unsafe { &*parked.cast::<T::Static>() };
let retained = retained_measure(std::any::TypeId::of::<T::Static>()).map_or(0, |measure| measure(value));
// SAFETY: as above, and the decline above establishes that the payload
// owns all of its content.
unsafe { promotion.move_park::<T>(parked, retained) }.map(<*const T::Static>::cast)
}
let (size, align) = element_dims::<T>();
ElementWrite {
size,
align,
parked: element_parked::<T>(),
type_id: std::any::TypeId::of::<T::Static>(),
clone_out: clone_out::<T>,
repark: repark::<T>,
park_move: park_move::<T>,
content_hash: None,
content_eq: None,
}
}
/// [`element_write`] plus the content hashing and equality glue, for element
/// types that support them.
pub fn element_write_hashed<T: Clone + Send + Sync + graphene_hash::CacheHash + PartialEq + dyn_any::StaticTypeSized>() -> ElementWrite
where
T::Static: Clone + Send + Sync,
{
/// # Safety
/// `ptr` must address a live element of `T` in the form [`element_parked`] picks.
unsafe fn content_hash<T: graphene_hash::CacheHash>(ptr: *const u8, state: &mut dyn core::hash::Hasher) {
let mut state = state;
// SAFETY: the caller's contract.
unsafe { borrow_element::<T>(Rec::new(ptr)) }.cache_hash(&mut state);
}
/// # Safety
/// `a` and `b` must each address a live element of `T` in that same form.
unsafe fn content_eq<T: PartialEq>(a: *const u8, b: *const u8) -> bool {
// SAFETY: the caller's contract.
unsafe { borrow_element::<T>(Rec::new(a)) == borrow_element::<T>(Rec::new(b)) }
}
ElementWrite {
content_hash: Some(content_hash::<T>),
content_eq: Some(content_eq::<T>),
..element_write::<T>()
}
}
/// Selects [`element_write_hashed`] when the element type supports the
/// content glue and [`element_write`] otherwise, by autoref method
/// resolution: call `(&ElementWritePick::<T>(..)).element_write()` with both
/// traits in scope.
pub struct ElementWritePick<T>(pub std::marker::PhantomData<T>);
pub trait ElementWritePickHashed {
fn element_write(&self) -> ElementWrite;
}
impl<T: Clone + Send + Sync + graphene_hash::CacheHash + PartialEq + dyn_any::StaticTypeSized> ElementWritePickHashed for ElementWritePick<T>
where
T::Static: Clone + Send + Sync,
{
fn element_write(&self) -> ElementWrite {
element_write_hashed::<T>()
}
}
pub trait ElementWritePickPlain {
fn element_write(&self) -> ElementWrite;
}
impl<T: Clone + Send + Sync + dyn_any::StaticTypeSized> ElementWritePickPlain for &ElementWritePick<T>
where
T::Static: Clone + Send + Sync,
{
fn element_write(&self) -> ElementWrite {
element_write::<T>()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::test_support::{f64_field, sized_field};
#[test]
fn canonical_order_and_offsets() {
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[sized_field("tint", 4, 4), f64_field("opacity"), sized_field("flag", 1, 1)]);
assert_eq!(layout.offset_of("opacity", 0), Some(8));
assert_eq!(layout.offset_of("tint", 0), Some(16));
assert_eq!(layout.offset_of("flag", 0), Some(20));
assert_eq!(layout.size, 21);
assert_eq!(layout.align, 8);
}
#[test]
#[should_panic(expected = "a record element aligns to at most 8 bytes")]
fn an_over_aligned_element_is_refused_at_wiring() {
Layout::default().with_writes(0, element_write::<u128>(), &[]);
}
#[test]
#[should_panic(expected = "attribute `wide` aligns to at most 8 bytes")]
fn an_over_aligned_attribute_is_refused_at_wiring() {
Layout::default().with_writes(0, element_write::<f64>(), &[sized_field("wide", 16, 16)]);
}
#[test]
#[should_panic(expected = "two different types")]
fn type_conflicts_panic() {
let mut conflicting = sized_field("opacity", 8, 8);
conflicting.type_id = std::any::TypeId::of::<u64>();
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")]);
layout.with_writes(0, element_write::<f64>(), &[conflicting]);
}
#[test]
fn a_token_resolves_only_in_the_layout_it_was_minted_against() {
use crate::attribute::{Attribute, Opacity, Transform};
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[FieldWrite::of::<Opacity>(0)]);
let token = FieldOffset::<Opacity>::of(&layout, 0).expect("the layout carries the marker");
assert_eq!(token.resolve(&layout), layout.offset_of(Opacity::NAME, 0));
let shifted = Layout::default().with_writes(0, element_write::<f64>(), &[FieldWrite::of::<Transform>(0), FieldWrite::of::<Opacity>(0)]);
assert_eq!(FieldOffset::<Opacity>::of(&shifted, 0).and_then(|token| token.resolve(&layout)), None);
assert!(FieldOffset::<Opacity>::of(&Layout::default(), 0).is_none());
}
#[test]
fn union_is_order_independent() {
let a = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")]);
let b = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("length")]);
assert_eq!(Layout::union(&[&a, &b]), Layout::union(&[&b, &a]));
assert!(Layout::union(&[&a, &b]).offset_of("length", 0).is_some());
}
#[test]
fn the_element_write_pick_selects_the_content_glue_by_type() {
use super::{ElementWritePickHashed as _, ElementWritePickPlain as _};
#[derive(Clone, dyn_any::DynAny)]
struct Opaque;
let hashed = ElementWritePick::<String>(std::marker::PhantomData).element_write();
assert!(hashed.content_hash.is_some() && hashed.content_eq.is_some());
let plain = (&ElementWritePick::<Opaque>(std::marker::PhantomData)).element_write();
assert!(plain.content_hash.is_none() && plain.content_eq.is_none());
}
#[test]
fn elements_ride_as_bytes_exactly_without_drop_glue() {
assert!(!element_parked::<f64>());
assert!(!element_parked::<[f64; 4]>());
assert!(element_parked::<String>());
assert!(element_parked::<std::sync::Arc<str>>());
assert_eq!(element_dims::<[f64; 4]>(), (32, 8));
assert_eq!(element_dims::<String>(), (8, 8));
}
#[test]
fn layouts_resolve_spilled_values() {
let small = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")]);
assert_eq!(small.frame_bytes(), 16);
let backing = [4f64, 0.5];
let value = RecordValue::spilled(unsafe { Rec::new(backing.as_ptr().cast()) });
assert_eq!(unsafe { small.rec(&value).element::<f64>() }, 4.);
assert_eq!(unsafe { small.rec(&value).read::<f64>(small.offset_of("opacity", 0).unwrap()) }, 0.5);
let spilled = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity"), f64_field("length")]);
assert_eq!(spilled.frame_bytes(), 24);
let record = [1f64, 2., 3.];
let value = RecordValue::spilled(unsafe { Rec::new(record.as_ptr().cast()) });
assert_eq!(unsafe { spilled.rec(&value).element::<f64>() }, 1.);
assert_eq!(unsafe { spilled.rec(&value).read::<f64>(spilled.offset_of("length", 0).unwrap()) }, 2.);
assert_eq!(unsafe { spilled.rec(&value).read::<f64>(spilled.offset_of("opacity", 0).unwrap()) }, 3.);
}
}
@@ -0,0 +1,36 @@
//! The packed-record tier at rank 0. A record is the element at offset 0
//! plus one field per written attribute; its [`Layout`] is computed at
//! wiring from the upstream write set and never serialized. Records of
//! inline layouts live in the [`RecordValue`] itself; larger ones live as
//! per-lane views on the evaluation's [`Frames`], which the root owns and
//! every node claims its own frame out of. Kernels route them as opaque
//! [`RecordValue`]s that carry
//! their provenance. Only generated or wiring code touches offsets, so a
//! safe kernel cannot misalign a field.
mod access;
mod frames;
mod input;
mod layout;
mod owned;
mod promote;
mod route;
mod run;
mod serve;
#[cfg(test)]
mod test_support;
mod testkit;
pub use access::{Rec, RecordValue, apply_plan, borrow_element, erase_static, read_at, read_element, token_only, write_element, write_element_sized, write_field};
pub use frames::{FrameArena, FrameScope, Frames};
pub use input::{DerivedLazyInput, DerivedRecordInput, ElementInput, ElementLazyInput, LevelStatus, RecordExtract, RecordInput, RecordLazyInput, fill_frames, materialize_batch, materialize_level};
pub use layout::{
ElToken, ElementSpec, ElementWrite, ElementWritePick, ElementWritePickHashed, ElementWritePickPlain, FieldDesc, FieldOffset, FieldWrite, InputReads, Layout, LayoutMeta, RecordLayout, copy_plan,
element_dims, element_parked, element_write, element_write_hashed, empty_layout,
};
pub use owned::{OwnedRecord, deepen_field_value, has_deep_element_glue, register_deep_element_clone, register_deep_field_value, replay_field_value};
pub use promote::{Promotion, assert_promoted, register_element_promote, register_field_promote, register_retained_heap};
pub use route::{RecordSource, SourcePlan};
pub use run::{Group, GroupItem, RunBuilder, RunColumn, RunView};
pub use serve::{FrameClaim, MaterializedSpan, Served, SlotRun, serve_input};
pub use testkit::{LiftedSource, ServedRecord, capture, test_frames};
@@ -0,0 +1,236 @@
//! The owned crossing: deep copies that outlive the evaluation their content borrowed.
use super::access::Rec;
use super::layout::Layout;
use super::serve::FrameClaim;
/// Deep-copy overrides for element types whose plain clone borrows the
/// evaluation's arena (a `Graphic` holding a group interior). The generic
/// element glue consults this registry, so every layout carrying such an
/// element deep-copies at memo and capture seams regardless of which
/// constructor built the glue. The clone-out must produce a value of the
/// element's own type that owns all of its content; the re-park restores that
/// value's arena-resident form before parking it.
#[derive(Clone, Copy)]
pub(in crate::record) struct DeepElementGlue {
pub(in crate::record) clone_out: unsafe fn(*const u8) -> Box<dyn std::any::Any + Send + Sync>,
pub(in crate::record) repark: unsafe fn(&(dyn std::any::Any + Send + Sync), *mut u8, &crate::arena::Arena) -> Option<()>,
}
static DEEP_ELEMENT_CLONES: std::sync::LazyLock<std::sync::Mutex<std::collections::HashMap<std::any::TypeId, DeepElementGlue>>> = std::sync::LazyLock::new(Default::default);
/// Registers the deep copy-out and re-park pair for elements of `T`. Called
/// at startup from the crate that owns the type.
pub fn register_deep_element_clone<T: dyn_any::StaticTypeSized>(
clone_out: unsafe fn(*const u8) -> Box<dyn std::any::Any + Send + Sync>,
repark: unsafe fn(&(dyn std::any::Any + Send + Sync), *mut u8, &crate::arena::Arena) -> Option<()>,
) {
DEEP_ELEMENT_CLONES.lock().unwrap().insert(std::any::TypeId::of::<T::Static>(), DeepElementGlue { clone_out, repark });
}
pub(in crate::record) fn deep_element_glue(type_id: std::any::TypeId) -> Option<DeepElementGlue> {
DEEP_ELEMENT_CLONES.lock().unwrap().get(&type_id).copied()
}
/// Whether elements of a type registered deep glue. The shallow clone path is
/// only sound for types that did not need to, so a host that drives the
/// registration itself checks the types it owes before it evaluates anything.
pub fn has_deep_element_glue(type_id: std::any::TypeId) -> bool {
DEEP_ELEMENT_CLONES.lock().unwrap().contains_key(&type_id)
}
/// Deep-copy overrides for field values whose content borrows the
/// evaluation's arena (a graphic list holding native groups), keyed by the
/// field's owned value form. Consulted at the persistence seams only:
/// `read_erased` itself stays shallow, since introspection reads captures in
/// generation. Both halves decline when the value already owns all of its
/// content, so group-free values pay no extra clone: `copy_out` returns
/// `None` for unchanged, `replay` returns `Some(None)` for unchanged and
/// `None` for arena exhaustion.
#[derive(Clone, Copy)]
pub(in crate::record) struct DeepFieldGlue {
pub(in crate::record) copy_out: fn(&dyn crate::list::AnyAttributeValue) -> Option<Box<dyn crate::list::AnyAttributeValue>>,
pub(in crate::record) replay: crate::list::FieldReplayFn,
}
static DEEP_FIELD_VALUES: std::sync::LazyLock<std::sync::Mutex<std::collections::HashMap<std::any::TypeId, DeepFieldGlue>>> = std::sync::LazyLock::new(Default::default);
/// Registers the deep copy-out and replay pair for field values of `T`.
/// Called at startup from the crate that owns the type.
pub fn register_deep_field_value<T: 'static>(
copy_out: fn(&dyn crate::list::AnyAttributeValue) -> Option<Box<dyn crate::list::AnyAttributeValue>>,
replay: crate::list::FieldReplayFn,
) {
DEEP_FIELD_VALUES.lock().unwrap().insert(std::any::TypeId::of::<T>(), DeepFieldGlue { copy_out, replay });
}
pub(in crate::record) fn deep_field_glue(type_id: std::any::TypeId) -> Option<DeepFieldGlue> {
DEEP_FIELD_VALUES.lock().unwrap().get(&type_id).copied()
}
/// The copy-out half over an erased field value: the owned form a value takes
/// when it crosses out of the evaluation whose arena its content borrows. A
/// value with no registered glue already owns everything and passes through.
pub fn deepen_field_value(value: Box<dyn crate::list::AnyAttributeValue>) -> Box<dyn crate::list::AnyAttributeValue> {
match deep_field_glue(value.as_any().type_id()) {
Some(glue) => (glue.copy_out)(&*value).unwrap_or(value),
None => value,
}
}
/// The replay half over an erased field value: `Some(None)` where the value
/// already owns its content, `None` on arena exhaustion.
pub fn replay_field_value(value: &dyn crate::list::AnyAttributeValue, arena: &crate::arena::Arena) -> Option<Option<Box<dyn crate::list::AnyAttributeValue>>> {
match deep_field_glue(value.as_any().type_id()) {
Some(glue) => (glue.replay)(value, arena),
None => Some(None),
}
}
/// A record deep-copied out of its evaluation: the packed bytes plus owned
/// clones of every parked payload, replayable into a later evaluation's
/// storage through the layout's erased glue. The layout stays with the
/// holder, which proved it at wiring.
pub struct OwnedRecord {
pub(in crate::record) bytes: Box<[u8]>,
pub(in crate::record) element: Option<Box<dyn std::any::Any + Send + Sync>>,
/// Each copied field's index in the copy's layout, that field's declared
/// value type, and the owned value. The type rides along so a replay can
/// re-check the field it resolves rather than trusting the caller's claim.
fields: Vec<(usize, std::any::TypeId, Box<dyn crate::list::AnyAttributeValue>)>,
}
impl std::fmt::Debug for OwnedRecord {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("OwnedRecord(..)")
}
}
impl OwnedRecord {
/// # Safety
/// `rec` must be a live record of `layout`.
pub unsafe fn copy_out(layout: &Layout, rec: Rec<'_>) -> OwnedRecord {
// The element-to-field seam is never written, so the copy stays untyped:
// a `&[u8]` over the frame would read those bytes.
let mut staged = Vec::<u8>::with_capacity(layout.size);
// SAFETY: the caller's contract sizes the record at `layout.size`, which
// is the capacity just reserved, so the copy fills exactly the staging.
let bytes = unsafe {
std::ptr::copy_nonoverlapping(rec.ptr(), staged.as_mut_ptr(), layout.size);
staged.set_len(layout.size);
staged.into_boxed_slice()
};
// SAFETY: the caller's contract; a parked element sits at offset 0.
let element = layout.element.parked.then(|| unsafe { (layout.element.clone_out)(rec.ptr()) });
let fields = layout
.fields
.iter()
.enumerate()
.filter(|(_, field)| field.repark.is_some())
// SAFETY: the caller's contract; each field reads its own descriptor's offset.
.map(|(index, field)| (index, field.type_id, deepen_field_value(unsafe { (field.read_erased)(rec.ptr().add(field.offset)) })))
.collect();
OwnedRecord { bytes, element, fields }
}
/// Replays the copy into a caller's claim, re-parking droppable payloads
/// against `arena`; `None` reports arena exhaustion. The claim's layout must
/// be the one the copy was taken at, which the checks below establish rather
/// than assume: the copy carries no layout of its own, so a mismatched claim
/// would otherwise write past the frame or re-park a field through another
/// field's glue.
pub fn replay_into(&self, slot: &mut FrameClaim<'_, '_>, arena: &crate::arena::Arena) -> Option<()> {
let layout = slot.layout;
assert_eq!(self.bytes.len(), layout.size, "a replay lands in the layout the copy was taken at");
assert_eq!(self.element.is_some(), layout.element.parked, "a replay lands in the layout the copy was taken at");
for &(index, type_id, _) in &self.fields {
let field = layout.fields.get(index).expect("a replay lands in the layout the copy was taken at");
assert_eq!(field.type_id, type_id, "a replay lands in the layout the copy was taken at");
}
self.write_into(layout, slot.dst(), arena)
}
/// `dst` is a claimed frame of `layout`, and `replay_into`'s asserts have
/// established that `layout` is the one the copy was taken at.
fn write_into(&self, layout: &Layout, dst: *mut u8, arena: &crate::arena::Arena) -> Option<()> {
// SAFETY: the copy is `layout.size` bytes and the claim is a frame of it.
unsafe { std::ptr::copy_nonoverlapping(self.bytes.as_ptr(), dst, self.bytes.len()) };
if let Some(element) = &self.element {
// SAFETY: the element was cloned out of this layout's own slot at offset 0.
unsafe { (layout.element.repark)(&**element, dst, arena) }?;
}
for (index, _, value) in &self.fields {
let field = &layout.fields[*index];
let repark = field.repark.expect("copied fields carry re-park glue");
let resident = replay_field_value(&**value, arena)?;
// SAFETY: the asserts matched this index's field type, so the glue and
// the value agree; the write lands in that field's own region.
unsafe { repark(resident.as_deref().unwrap_or(&**value), dst.add(field.offset), arena) }?;
}
Some(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::access::{read_element, write_element, write_field};
use crate::record::frames::FrameArena;
use crate::record::layout::{FieldWrite, element_write};
#[test]
fn a_padded_layout_copies_out_without_reading_its_seam() {
// A 4-byte element before an 8-aligned field leaves [4, 8) unwritten.
let layout = Layout::default().with_writes(0, element_write::<u32>(), &[FieldWrite::of::<crate::attribute::Opacity>(0)]);
let offset = layout.offset_of("opacity", 0).unwrap();
assert_eq!((layout.element.size, offset), (4, 8), "the fixture needs the element-to-field seam");
let arena = crate::arena::Arena::new(1024).unwrap();
let scratch = arena.alloc_scratch::<u64>(layout.frame_bytes().div_ceil(8)).unwrap();
let base: *mut u8 = scratch.as_mut_ptr().cast();
unsafe { write_element(base, 7u32, &arena) }.unwrap();
unsafe { write_field::<f64>(base, offset, 0.5) };
let copy = unsafe { OwnedRecord::copy_out(&layout, Rec::new(base.cast_const())) };
let replay_arena = crate::arena::Arena::new(1024).unwrap();
let mut frame_arena = FrameArena::new();
frame_arena.reserve(layout.frame_bytes());
let frames = frame_arena.frames();
let mut slot = frames.claim(&layout);
copy.replay_into(&mut slot, &replay_arena).unwrap();
// SAFETY: the replay completes the record in the claimed frame.
let value = unsafe { slot.finish() };
let rec = layout.rec(&value);
assert_eq!(unsafe { read_element::<u32>(rec) }, 7);
assert_eq!(unsafe { rec.read::<f64>(offset) }, 0.5);
}
#[test]
fn owned_records_replay_re_parked_payloads_after_the_source_dies() {
let layout = Layout::default().with_writes(0, element_write::<String>(), &[FieldWrite::of::<crate::attribute::Name>(0)]);
let mut buffer = vec![0u64; layout.size.div_ceil(8)];
let base: *mut u8 = buffer.as_mut_ptr().cast();
let copy = {
let arena = crate::arena::Arena::new(1024).unwrap();
unsafe { write_element(base, String::from("element"), &arena) }.unwrap();
let (name, _) = arena.alloc(String::from("field")).unwrap();
unsafe { write_field::<&str>(base, layout.offset_of("name", 0).unwrap(), name.as_str()) };
unsafe { OwnedRecord::copy_out(&layout, Rec::new(base)) }
};
buffer.fill(u64::MAX);
let replay_arena = crate::arena::Arena::new(1024).unwrap();
let mut frame_arena = FrameArena::new();
frame_arena.reserve(layout.frame_bytes());
let frames = frame_arena.frames();
let mut slot = frames.claim(&layout);
copy.replay_into(&mut slot, &replay_arena).unwrap();
// SAFETY: the replay completes the record in the claimed frame.
let value = unsafe { slot.finish() };
let rec = layout.rec(&value);
assert_eq!(unsafe { read_element::<String>(rec) }, "element");
assert_eq!(unsafe { rec.read::<&str>(layout.offset_of("name", 0).unwrap()) }, "field");
}
}
@@ -0,0 +1,433 @@
//! Transient-to-persistent promotion of records and their parked payloads.
use super::layout::Layout;
use super::owned::{deepen_field_value, replay_field_value};
use super::serve::MaterializedSpan;
/// The regions a promote dispatches on. A payload already living in the
/// persistent region outlives every entry promoted into it and is shared; a
/// payload in the transient arena or the frame buffer dies at the next reset
/// and is cloned.
///
/// The dispatch is decidable only where the reference addresses the payload
/// itself, which holds for parked elements and for a group interior's frames.
/// A reference-valued attribute instead names storage whose owner the address
/// alone does not identify, so those are never provenance-shared; they clone,
/// or move where [`Arena::move_park`](crate::arena::Arena::move_park) confirms
/// the reference is the transient arena's own park.
///
/// THE SHARING LAW: sharing a payload between persistent entries is sound
/// because persistent invalidation is epochal, so every entry dies at one
/// flush and no entry can outlive a payload another still names. Per-entry
/// eviction would have to refcount the shared payloads before it could
/// reclaim one entry's storage.
#[derive(Clone, Copy)]
pub struct Promotion<'a> {
transient: &'a crate::arena::Arena,
frames: (usize, usize),
persistent: &'a crate::arena::Arena,
}
impl<'a> Promotion<'a> {
/// `frames` is the whole frame buffer as (address, bytes), from
/// [`Frames::bounds`].
pub fn new(transient: &'a crate::arena::Arena, frames: (usize, usize), persistent: &'a crate::arena::Arena) -> Self {
Promotion { transient, frames, persistent }
}
pub fn persistent(&self) -> &'a crate::arena::Arena {
self.persistent
}
/// Whether the reference dies with the evaluation, which is the promote's
/// clone-or-share question. A null or byte-carried slot reads as neither
/// region's and shares.
pub fn evaluation_lived(&self, ptr: *const u8) -> bool {
self.transient.contains(ptr) || (ptr as usize).wrapping_sub(self.frames.0) < self.frames.1
}
/// Moves a transient payload's header into the persistent region instead of
/// cloning the heap it owns: the heap travels with the drop obligation and
/// is freed at the persistent flush, never at the transient reset. `None`
/// where the header is not the transient arena's own park keyed to `T`'s
/// static type or the region refused it, which leaves the caller its clone
/// path.
///
/// The forwarding is the evaluation's, so a payload two records share moves
/// once and both reach the one persistent header. The source header stays
/// readable to the evaluation's remaining sharers, which are the only reads
/// the move keeps sound: no read of it may outlive the persistent flush.
///
/// # Safety
/// `parked` must address a live `T`, and `T` must own all of its content, a
/// persistent header being allowed to reference no transient storage, which
/// is also what lets it be republished at `T::Static`. The park's key
/// settles the type, so the caller owes no identity argument.
pub unsafe fn move_park<T: dyn_any::StaticTypeSized>(&self, parked: *const u8, retained: usize) -> Option<*const T::Static>
where
T::Static: Send + Sync,
{
// SAFETY: the caller's contract, forwarded to the parking arena.
unsafe { self.transient.move_park::<T>(parked, self.persistent, retained) }
}
}
/// Rewrites one promoted record's parked references in place: the lane bytes
/// are already the persistent region's, and each reference whose payload dies
/// with the evaluation is replaced by a clone parked there.
///
/// A parked element rides the arena slot its payload was written into, so the
/// region holding it decides share against clone. A parked field's reference
/// names storage the promote cannot attribute from the address alone, so it is
/// never provenance-shared: it clones, or takes the route a registered
/// [`FieldPromote`] states under the two-level sharing law.
///
/// # Safety
/// `dst` must be a persistent image of a live record of `layout`.
pub(in crate::record) unsafe fn promote_record(layout: &Layout, dst: *mut u8, promotion: &Promotion<'_>) -> Option<()> {
if layout.element.parked {
// SAFETY: a parked element slot holds one reference at offset 0.
let parked = unsafe { dst.cast::<*const u8>().read() };
if !promotion.persistent.contains(parked) {
match element_promote_glue(layout.element.type_id) {
// SAFETY: the slot images a parked element of this type.
Some(promote) => unsafe { promote(dst.cast_const(), dst, promotion) }?,
// SAFETY: as above; the header is the payload's own.
None => match unsafe { (layout.element.park_move)(parked, promotion) } {
// SAFETY: a parked element slot holds one reference at offset 0.
Some(moved) => unsafe { dst.cast::<*const u8>().write(moved) },
None => {
// SAFETY: as above, and the clone owns its content.
let owned = unsafe { (layout.element.clone_out)(dst.cast_const()) };
// SAFETY: the clone is this element's own type, back into its slot.
unsafe { (layout.element.repark)(&*owned, dst, promotion.persistent) }?;
}
},
}
}
}
for field in &layout.fields {
let Some(repark) = field.repark else { continue };
// SAFETY: a parked field slot holds one reference at its offset.
let slot = unsafe { dst.add(field.offset) };
if let Some(promote) = field_promote_glue(field.type_id) {
// SAFETY: the slot images a parked field of this descriptor, and the
// promoted reference is written back into that same slot.
unsafe { promote(slot.cast_const(), slot, promotion) }?;
continue;
}
// SAFETY: the slot images a parked field of this descriptor.
let value = deepen_field_value(unsafe { (field.read_erased)(slot.cast_const()) });
let resident = replay_field_value(&*value, promotion.persistent)?;
// SAFETY: the replay produced this field's own value type.
unsafe { repark(resident.as_deref().unwrap_or(&*value), slot, promotion.persistent) }?;
}
Some(())
}
/// Re-walks a promoted record and asserts no reference into the evaluation's
/// storage survived, which is the postcondition every later hit and every
/// shared interior relies on. The element's payload must sit in the persistent
/// region itself; a field's payload is heap its owner holds, so the weaker
/// range check is all that is decidable there.
///
/// # Safety
/// `ptr` must be a live record of `layout`.
pub unsafe fn assert_promoted(layout: &Layout, ptr: *const u8, promotion: &Promotion<'_>) {
if layout.element.parked {
// SAFETY: a parked element slot holds one reference at offset 0.
let parked = unsafe { ptr.cast::<*const u8>().read() };
assert!(promotion.persistent.contains(parked), "a promoted element kept a reference outside the persistent region");
}
for field in &layout.fields {
if field.repark.is_none() {
continue;
}
// SAFETY: a parked field slot holds one reference at its offset.
let parked = unsafe { ptr.add(field.offset).cast::<*const u8>().read() };
assert!(!promotion.evaluation_lived(parked), "a promoted field kept a reference into the evaluation");
}
}
/// The promote override for element types whose payload holds arena-resident
/// interiors: the generic path clones through an owned intermediate, while a
/// registered promote shares the interiors already living in the persistent
/// region and copies only the rest.
type ElementPromote = unsafe fn(*const u8, *mut u8, &Promotion<'_>) -> Option<()>;
static ELEMENT_PROMOTES: std::sync::LazyLock<std::sync::Mutex<std::collections::HashMap<std::any::TypeId, ElementPromote>>> = std::sync::LazyLock::new(Default::default);
/// Registers the promote for elements of `T`. Called at startup from the crate
/// that owns the type. The promote must leave no reference the promotion calls
/// evaluation-lived.
pub fn register_element_promote<T: dyn_any::StaticTypeSized>(promote: ElementPromote) {
ELEMENT_PROMOTES.lock().unwrap().insert(std::any::TypeId::of::<T::Static>(), promote);
}
fn element_promote_glue(type_id: std::any::TypeId) -> Option<ElementPromote> {
ELEMENT_PROMOTES.lock().unwrap().get(&type_id).copied()
}
/// The deep field glue's third half, opt-in beside [`register_deep_field_value`]'s
/// pair: a registered promote routes the field payload transient-to-persistent
/// at the slot itself, so the promote never builds the owned form the other two
/// halves round-trip through. Keyed by the field's stored type rather than its
/// owned one, since the route runs before any erased read. The owned halves keep
/// serving every other seam, [`OwnedRecord::copy_out`] and [`GroupItem::copy_out`]
/// included.
///
/// THE TWO-LEVEL SHARING LAW, which a registrant must hold to: the field's own
/// header is not provenance-shared, since a stored reference names storage whose
/// owner the promote cannot attribute from the address alone; it clones, or moves
/// where the payload owns all of its content and the transient arena confirms the
/// reference is its own park. One level inside the payload, an interior that is
/// arena-resident has decidable provenance and takes the Cow dispatch: an interior
/// the persistent region already holds is shared pointer for pointer, and one that
/// dies with the evaluation is copied or moved.
type FieldPromote = unsafe fn(*const u8, *mut u8, &Promotion<'_>) -> Option<()>;
static FIELD_PROMOTES: std::sync::LazyLock<std::sync::Mutex<std::collections::HashMap<std::any::TypeId, FieldPromote>>> = std::sync::LazyLock::new(Default::default);
/// Registers the promote for fields stored as `T`. Called at startup from the
/// crate that owns the type. The promote must leave no reference the promotion
/// calls evaluation-lived.
pub fn register_field_promote<T: 'static>(promote: FieldPromote) {
FIELD_PROMOTES.lock().unwrap().insert(std::any::TypeId::of::<T>(), promote);
}
fn field_promote_glue(type_id: std::any::TypeId) -> Option<FieldPromote> {
FIELD_PROMOTES.lock().unwrap().get(&type_id).copied()
}
/// The park glue's heap estimate for values of a type, keyed as the deep glue
/// is. Consulted where a payload parks, so a region's retained heap is known
/// without walking it.
type RetainedMeasure = fn(&(dyn std::any::Any + Send + Sync)) -> usize;
static RETAINED_MEASURES: std::sync::LazyLock<std::sync::Mutex<std::collections::HashMap<std::any::TypeId, RetainedMeasure>>> = std::sync::LazyLock::new(Default::default);
/// Registers the retained-heap estimate for values of `T`. Called at startup
/// from the crate that owns the type. The estimate is a hint: an unregistered
/// type contributes 0, so a region's counter is a lower bound.
pub fn register_retained_heap<T: dyn_any::StaticTypeSized>(measure: RetainedMeasure) {
RETAINED_MEASURES.lock().unwrap().insert(std::any::TypeId::of::<T::Static>(), measure);
}
pub(in crate::record) fn retained_measure(type_id: std::any::TypeId) -> Option<RetainedMeasure> {
RETAINED_MEASURES.lock().unwrap().get(&type_id).copied()
}
impl MaterializedSpan {
/// Copies the batch into the persistent region as a copy-on-write over
/// provenance: the frame bytes memcpy, and each parked reference is cloned
/// only where it dies with the evaluation, so a layout carrying no parked
/// slot reduces to the memcpy and a level whose payloads an upstream memo
/// already published costs nothing beyond it. `None` where the region
/// could not hold the copy, which leaves the caller with nothing to cache.
///
/// # Safety
/// The batch's lanes must be live records of its layout.
pub unsafe fn to_persistent(batch: &crate::node::RecordBatch<'_>, promotion: &Promotion<'_>) -> Option<MaterializedSpan> {
let layout = batch.layout();
let stride = layout.lane_stride();
let len = batch.len();
if len == 0 {
return Some(MaterializedSpan {
base: crate::arena::ArenaWeak::NULL,
len: 0,
});
}
let persistent = promotion.persistent();
let slab = persistent.alloc_scratch::<u64>((len * stride).div_ceil(8))?;
let base: *mut u8 = slab.as_mut_ptr().cast();
for lane in 0..len {
// SAFETY: the caller's contract on the lane, into the lane's own
// region of the freshly reserved slab.
let dst = unsafe { base.add(lane * stride) };
// SAFETY: as above; `layout.size` bytes of a lane fit its own stride.
unsafe { std::ptr::copy_nonoverlapping(batch.get(lane).rec().ptr(), dst, layout.size) };
// SAFETY: the copy images a record of this layout.
unsafe { promote_record(layout, dst, promotion) }?;
}
#[cfg(debug_assertions)]
for lane in 0..len {
// SAFETY: every lane was imaged and promoted above.
unsafe { assert_promoted(layout, base.add(lane * stride).cast_const(), promotion) };
}
Some(MaterializedSpan {
base: persistent.handle_at(base.cast_const())?,
len,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::access::{borrow_element, write_element, write_element_sized};
use crate::record::layout::element_write;
use crate::record::test_support::f64_field;
#[test]
fn a_pod_level_promotes_as_a_bare_memcpy() {
let transient = crate::arena::Arena::new(4096).unwrap();
let persistent = crate::arena::Arena::new(4096).unwrap();
let layout = Layout::default().with_writes(1, element_write::<f64>(), &[f64_field("opacity")]);
let stride = layout.lane_stride();
let mut buffer = vec![0u64; (4 * stride).div_ceil(8)];
let base = buffer.as_mut_ptr().cast::<u8>();
let bounds = (base as usize, buffer.len() * 8);
for lane in 0..4usize {
unsafe { base.add(lane * stride).cast::<f64>().write(lane as f64) };
}
let batch = unsafe { crate::node::RecordBatch::new(base.cast_const(), 4, &layout) };
let promotion = Promotion::new(&transient, bounds, &persistent);
let span = unsafe { MaterializedSpan::to_persistent(&batch, &promotion) }.unwrap();
let slab = (4 * stride).div_ceil(8) * 8;
assert_eq!(persistent.occupancy(), slab, "a layout with no parked slot allocates the slab and nothing else");
assert_eq!(persistent.retained_heap(), 0, "no payload parked, so nothing is retained");
let published = span.batch(&persistent, &layout).unwrap();
for lane in 0..4usize {
assert_eq!(unsafe { published.get(lane).rec().element::<f64>() }, lane as f64, "lane {lane}");
}
}
#[test]
fn a_promoted_element_lands_in_the_persistent_region() {
let transient = crate::arena::Arena::new(4096).unwrap();
let persistent = crate::arena::Arena::new(4096).unwrap();
let layout = Layout::default().with_writes(0, element_write::<String>(), &[]);
let mut buffer = vec![0u64; layout.frame_bytes().div_ceil(8)];
let base = buffer.as_mut_ptr().cast::<u8>();
let bounds = (base as usize, buffer.len() * 8);
unsafe { write_element(base, String::from("parked in the evaluation"), &transient) }.unwrap();
let source = unsafe { base.cast::<*const u8>().read() };
assert!(transient.contains(source));
let batch = unsafe { crate::node::RecordBatch::new(base.cast_const(), 1, &layout) };
let promotion = Promotion::new(&transient, bounds, &persistent);
let span = unsafe { MaterializedSpan::to_persistent(&batch, &promotion) }.unwrap();
let published = span.batch(&persistent, &layout).unwrap();
let promoted = unsafe { published.get(0).rec().ptr().cast::<*const u8>().read() };
assert!(persistent.contains(promoted), "the promote re-parked the payload into the persistent region");
assert_ne!(source, promoted, "an evaluation-lived payload gets its own persistent header");
assert_eq!(unsafe { borrow_element::<String>(published.get(0).rec()) }, "parked in the evaluation");
}
/// The measure production registers for `String`, without which a promote
/// credits the region 0 and the counters cannot be observed to transfer.
fn measure_strings() {
register_retained_heap::<String>(|value| value.downcast_ref::<String>().map_or(0, String::len));
}
#[test]
fn a_promoted_payload_moves_its_heap_rather_than_cloning_it() {
measure_strings();
let mut transient = crate::arena::Arena::new(4096).unwrap();
let persistent = crate::arena::Arena::new(4096).unwrap();
let layout = Layout::default().with_writes(0, element_write::<String>(), &[]);
let mut buffer = vec![0u64; layout.frame_bytes().div_ceil(8)];
let base = buffer.as_mut_ptr().cast::<u8>();
let bounds = (base as usize, buffer.len() * 8);
let owned = String::from("the obligation travels with the header");
let (heap, length) = (owned.as_ptr(), owned.len());
unsafe { write_element_sized(base, owned, &transient, length) }.unwrap();
assert_eq!(transient.retained_heap(), length);
let batch = unsafe { crate::node::RecordBatch::new(base.cast_const(), 1, &layout) };
let promotion = Promotion::new(&transient, bounds, &persistent);
let span = unsafe { MaterializedSpan::to_persistent(&batch, &promotion) }.unwrap();
let published = span.batch(&persistent, &layout).unwrap();
let served = unsafe { borrow_element::<String>(published.get(0).rec()) };
assert_eq!(served.as_ptr(), heap, "the promote moved the header, so the served view names the pre-promote heap");
assert_eq!(transient.retained_heap(), 0, "the transient counter gave the hint up");
assert_eq!(persistent.retained_heap(), length, "and the persistent counter took it");
transient.reset();
let published = span.batch(&persistent, &layout).unwrap();
assert_eq!(
unsafe { borrow_element::<String>(published.get(0).rec()) },
"the obligation travels with the header",
"the moved payload survives the transient reset"
);
}
#[test]
fn a_payload_two_records_share_promotes_to_one_persistent_header() {
measure_strings();
let transient = crate::arena::Arena::new(4096).unwrap();
let mut persistent = crate::arena::Arena::new(4096).unwrap();
let layout = Layout::default().with_writes(0, element_write::<String>(), &[]);
let stride = layout.lane_stride();
let mut buffer = vec![0u64; (2 * stride).div_ceil(8)];
let base = buffer.as_mut_ptr().cast::<u8>();
let bounds = (base as usize, buffer.len() * 8);
let length = "shared across lanes".len();
unsafe { write_element_sized(base, String::from("shared across lanes"), &transient, length) }.unwrap();
// A carried element byte-copies its park reference, so both lanes name the one park.
let shared = unsafe { base.cast::<*const u8>().read() };
unsafe { base.add(stride).cast::<*const u8>().write(shared) };
let batch = unsafe { crate::node::RecordBatch::new(base.cast_const(), 2, &layout) };
let promotion = Promotion::new(&transient, bounds, &persistent);
let span = unsafe { MaterializedSpan::to_persistent(&batch, &promotion) }.unwrap();
let published = span.batch(&persistent, &layout).unwrap();
let first = unsafe { published.get(0).rec().ptr().cast::<*const u8>().read() };
let second = unsafe { published.get(1).rec().ptr().cast::<*const u8>().read() };
assert_eq!(first, second, "a payload two records share moves once");
assert_eq!(persistent.retained_heap(), length, "and its hint transfers once");
persistent.reset();
assert_eq!(persistent.retained_heap(), 0, "the flush frees the one header exactly once");
}
#[test]
fn a_persistent_element_is_shared_by_the_promote() {
let transient = crate::arena::Arena::new(4096).unwrap();
let persistent = crate::arena::Arena::new(4096).unwrap();
let layout = Layout::default().with_writes(0, element_write::<String>(), &[]);
let mut buffer = vec![0u64; layout.frame_bytes().div_ceil(8)];
let base = buffer.as_mut_ptr().cast::<u8>();
let bounds = (base as usize, buffer.len() * 8);
// The payload an upstream memo already published: the promote must
// name it rather than copy it.
unsafe { write_element(base, String::from("published upstream"), &persistent) }.unwrap();
let upstream = unsafe { base.cast::<*const u8>().read() };
let occupied = persistent.occupancy();
let batch = unsafe { crate::node::RecordBatch::new(base.cast_const(), 1, &layout) };
let promotion = Promotion::new(&transient, bounds, &persistent);
let span = unsafe { MaterializedSpan::to_persistent(&batch, &promotion) }.unwrap();
let published = span.batch(&persistent, &layout).unwrap();
let promoted = unsafe { published.get(0).rec().ptr().cast::<*const u8>().read() };
assert_eq!(upstream, promoted, "an already persistent payload is shared, pointer for pointer");
assert_eq!(persistent.occupancy() - occupied, layout.frame_bytes(), "only the lane slab was allocated");
}
#[test]
#[should_panic(expected = "a promoted element kept a reference outside the persistent region")]
fn the_rewalk_catches_a_reference_the_promote_left_behind() {
let transient = crate::arena::Arena::new(4096).unwrap();
let persistent = crate::arena::Arena::new(4096).unwrap();
let layout = Layout::default().with_writes(0, element_write::<String>(), &[]);
let mut buffer = vec![0u64; layout.frame_bytes().div_ceil(8)];
let base = buffer.as_mut_ptr().cast::<u8>();
let bounds = (base as usize, buffer.len() * 8);
unsafe { write_element(base, String::from("never promoted"), &transient) }.unwrap();
let promotion = Promotion::new(&transient, bounds, &persistent);
unsafe { assert_promoted(&layout, base.cast_const(), &promotion) };
}
}
@@ -0,0 +1,165 @@
//! Producer-side routing: a source's translation into the union layout.
use super::access::{Rec, apply_plan};
use super::frames::Frames;
use super::layout::{Layout, copy_plan};
use super::serve::{FrameClaim, Served, serve_input};
use crate::attribute;
use crate::gpoll::GPoll;
use crate::node::Node;
/// The default bytes for a union field the source does not carry: the census
/// default for declared names, zeroes otherwise.
fn default_fill_bytes(name: &str, size: usize) -> Box<[u8]> {
let mut bytes = vec![0u8; size].into_boxed_slice();
if let Some(info) = attribute::info(name)
&& info.size == size
{
(info.write_default_bytes)(&mut bytes);
}
bytes
}
/// A routing source's wiring-resolved translation: field moves into the
/// union layout plus census default fill for union fields the source lacks.
/// Absent when the source's layout already equals the union, in which case
/// the record pointer forwards untouched.
#[derive(Debug)]
pub struct SourcePlan {
moves: Vec<(usize, usize, usize)>,
fills: Vec<(usize, Box<[u8]>)>,
source: Layout,
}
impl SourcePlan {
pub fn new(source: &Layout, union: &Layout) -> Option<SourcePlan> {
if source == union {
return None;
}
let moves = copy_plan(source, union, true, &[]);
let fills = union
.fields
.iter()
.filter(|field| source.offset_of(field.name, field.level).is_none())
.map(|field| (field.offset, default_fill_bytes(field.name, field.size)))
.collect();
Some(SourcePlan { moves, fills, source: source.clone() })
}
/// # Safety
/// `src` must be a record of this plan's source layout and `dst` a
/// buffer of the plan's union layout that does not overlap `src`. The
/// returned view borrows `dst`, so `'d` must not outlive it.
pub unsafe fn translate<'d>(&self, src: Rec<'_>, dst: *mut u8) -> Rec<'d> {
// SAFETY: the caller's contract; the plan's moves and fills name only
// offsets of the layouts it was built from.
unsafe {
apply_plan(src, dst, &self.moves);
for (offset, bytes) in &self.fills {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst.add(*offset), bytes.len());
}
Rec::new(dst)
}
}
}
/// A routing input's claimed source plus its wiring-resolved [`SourcePlan`].
/// Evaluating it yields the source's record translated to the union layout
/// (or forwarded untouched when the layouts already agree), so the kernel
/// holds and returns record values without ever seeing the representation.
/// A translation lands in the claim the caller minted, so the value survives
/// sibling evaluations and its region is free again with that claim, which
/// bounds claims at one per source evaluation the kernel performs.
pub struct RecordSource<N> {
edge: N,
plan: Option<SourcePlan>,
union: Layout,
}
impl<N> RecordSource<N> {
pub fn new(edge: N, source: &Layout, union: &Layout) -> Self {
Self {
edge,
plan: SourcePlan::new(source, union),
union: union.clone(),
}
}
}
impl<C, N> Node<C> for RecordSource<N>
where
N: Node<C>,
{
fn serve<'e, 'l>(&self, input: &C, mut slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
// The source's frame is claimed beyond this one and dies with the
// claim; the translated union record stays.
let Some(plan) = &self.plan else {
return self.edge.serve(input, slot);
};
match serve_input(&self.edge, input, &slot.frames().reborrow()) {
GPoll::Final(value) => {
// SAFETY: the value came from this source, so it carries the
// plan's source layout.
unsafe { slot.translate(plan.source.rec(&value), plan) };
// SAFETY: the translation completes the union record.
GPoll::Final(unsafe { slot.finish_served() })
}
GPoll::Partial(value) => {
// SAFETY: as for the final arm.
unsafe { slot.translate(plan.source.rec(&value), plan) };
// SAFETY: as for the final arm.
GPoll::Partial(unsafe { slot.finish_served() })
}
GPoll::Fallback(boxed) => {
let (value, error) = *boxed;
// SAFETY: as for the final arm.
unsafe { slot.translate(plan.source.rec(&value), plan) };
// SAFETY: as for the final arm.
GPoll::Fallback(Box::new((unsafe { slot.finish_served() }, error)))
}
GPoll::Pending => GPoll::Pending,
GPoll::Error(error) => GPoll::Error(error),
}
}
fn extent_at<'x>(&self, input: &C, level: u8, frames: &Frames<'x>) -> GPoll<crate::gpoll::Extent>
where
C: crate::context::ExtractArena<ArenaRef = &'x crate::arena::Arena>,
{
self.edge.extent_at(input, level, frames)
}
fn layout(&self) -> &Layout {
&self.union
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::layout::element_write;
use crate::record::test_support::f64_field;
#[test]
fn translation_moves_fields_and_fills_census_defaults() {
let source = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("length")]);
let union = Layout::union(&[&source, &Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")])]);
let plan = SourcePlan::new(&source, &union).unwrap();
let record = [5f64, 7f64];
let mut buffer = vec![0u64; union.size.div_ceil(8)];
let translated = unsafe { plan.translate(Rec::new(record.as_ptr().cast()), buffer.as_mut_ptr().cast()) };
assert_eq!(unsafe { translated.element::<f64>() }, 5.);
assert_eq!(unsafe { translated.read::<f64>(union.offset_of("length", 0).unwrap()) }, 7.);
assert_eq!(unsafe { translated.read::<f64>(union.offset_of("opacity", 0).unwrap()) }, 1.);
}
#[test]
fn identity_layouts_forward() {
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[f64_field("opacity")]);
assert!(SourcePlan::new(&layout, &layout.clone()).is_none());
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,389 @@
//! The serving protocol: a node's own frame claim and the proof it closes with.
use super::access::{Rec, RecordValue, apply_plan, lift_poll_into, write_element, write_field};
use super::frames::Frames;
use super::layout::{Layout, element_dims, element_parked};
use super::route::SourcePlan;
use crate::gpoll::GPoll;
use crate::node::Node;
/// A claimed run of same-layout slots over the caller's scratch: [`Self::slot`]
/// backs an ordinary claim's own region by the lane's region of the slab, so a
/// lane serves in place with no staging copy, and [`Self::served`] records the
/// proof. The filled prefix is the only readable part, which is what makes
/// [`Self::finish`] safe.
pub struct SlotRun<'a> {
scratch: &'a mut [std::mem::MaybeUninit<u64>],
layout: &'a Layout,
len: usize,
filled: usize,
}
impl<'a> SlotRun<'a> {
/// `None` where the scratch cannot hold `len` lanes. The products are
/// checked: the stride is a multiple of 8, so a wrapped one would pass the
/// capacity test vacuously.
pub(in crate::record) fn new(scratch: &'a mut [std::mem::MaybeUninit<u64>], len: usize, layout: &'a Layout) -> Option<SlotRun<'a>> {
let need = len.checked_mul(layout.lane_stride())?;
let capacity = scratch.len().checked_mul(8)?;
(capacity >= need).then_some(SlotRun { scratch, layout, len, filled: 0 })
}
pub fn layout(&self) -> &'a Layout {
self.layout
}
/// Lane `lane`'s claim: its own frame is the lane's region of the slab and
/// its free space is `frames`, so the lane's inputs claim beyond it.
pub fn slot<'e>(&mut self, lane: usize, frames: &Frames<'e>) -> FrameClaim<'e, 'a> {
assert!(lane < self.len, "lane {lane} out of bounds for a run of {}", self.len);
let stride = self.layout.lane_stride();
// SAFETY: in-bounds by the assert against the capacity check `new` made.
let frame = unsafe { self.scratch.as_mut_ptr().cast::<u8>().add(lane * stride) };
FrameClaim {
layout: self.layout,
inline: RecordValue::zeroed(),
frame: (self.layout.frame_bytes() != 0).then_some(frame),
free: frames.reborrow(),
filled_fields: false,
}
}
/// Records lane `lane` as served; the proof came from that lane's slot. An
/// inline record rides the value's own storage, so it takes the one copy
/// the run makes. Lanes serve in ascending order with no gaps, so the
/// filled count is an initialized prefix rather than a high-water mark.
pub fn served(&mut self, lane: usize, proof: &Served<'_>) {
assert!(lane < self.len, "lane {lane} out of bounds for a run of {}", self.len);
assert_eq!(lane, self.filled, "lane {lane} serves out of order after {} filled lanes", self.filled);
if self.layout.size == 0 {
let stride = self.layout.lane_stride();
// SAFETY: in-bounds by the assert against the capacity check `new`
// made, and the lane takes the whole inline record.
unsafe { std::ptr::copy_nonoverlapping(self.layout.rec(proof.record()).ptr(), self.scratch.as_mut_ptr().cast::<u8>().add(lane * stride), stride) };
}
self.filled = lane + 1;
}
/// The served lanes as the caller's exclusive batch.
pub fn finish(self) -> crate::node::RecordBatchMut<'a> {
crate::node::RecordBatchMut::new(self.scratch, self.filled, self.layout)
}
}
/// A node's own output frame, minted by its caller from the caller's frame
/// space: the one closing surface for every exit. Writes land through it,
/// [`Self::lift`] and [`Self::finish`] serve the record, and it carries the
/// free space beyond the frame, so a node's inputs claim past it and their
/// space is free again when the claim dies, on value, error, and pending exits
/// alike with no per-exit ritual.
pub struct FrameClaim<'e, 'l> {
pub(in crate::record) layout: &'l Layout,
pub(in crate::record) inline: RecordValue<'static>,
pub(in crate::record) frame: Option<*mut u8>,
pub(in crate::record) free: Frames<'e>,
/// Set by the writes that fill the declared fields, so the safe closers can
/// refuse a field-bearing frame that was never filled.
pub(in crate::record) filled_fields: bool,
}
impl<'e, 'l> FrameClaim<'e, 'l> {
/// The free space beyond this claim's frame, which the node's own inputs
/// claim from.
pub fn frames(&mut self) -> &Frames<'e> {
&mut self.free
}
pub(in crate::record) fn dst(&mut self) -> *mut u8 {
match self.frame {
Some(frame) => frame,
None => (&raw mut self.inline).cast(),
}
}
/// Asserts the served element matches the wired layout, so a node whose
/// layout never resolved (or resolved at another type) panics here
/// instead of writing past its frame.
fn check_element<T: dyn_any::StaticTypeSized>(&self) {
let (size, _) = element_dims::<T>();
assert!(
self.layout.element.size == size && self.layout.element.parked == element_parked::<T>() && self.layout.element.type_id == std::any::TypeId::of::<T::Static>(),
"the served element `{}` ({size} bytes) must match the wired layout ({} bytes)",
std::any::type_name::<T>(),
self.layout.element.size,
);
}
/// Carries the plan's fields from a source record into the frame.
///
/// # Safety
/// `src` must be a live record of the plan's source layout, and the plan
/// must be the wiring-resolved plan of this frame's layout. `src` must not
/// overlap this frame; an input's frame is split off beyond the claim, so
/// serving the source through [`Self::frames`] establishes it.
pub unsafe fn carry(&mut self, src: Rec<'_>, plan: &[(usize, usize, usize)]) {
unsafe { apply_plan(src, self.dst(), plan) };
self.filled_fields = true;
}
/// Writes a field at its wiring-resolved offset.
///
/// # Safety
/// `offset` must be this layout's resolved offset for a field of `T`. A
/// generated write offset defaults to 0 until `set_layout` installs it, so
/// the debug assertion catches a serve that ran before the install.
pub unsafe fn attr_at<T>(&mut self, offset: usize, value: T) {
debug_assert!(
self.layout.fields.iter().any(|field| field.offset == offset),
"attribute write at an offset this layout does not carry; the layout must install before serving"
);
unsafe { write_field(self.dst(), offset, value) };
self.filled_fields = true;
}
/// Writes the element; `None` reports arena exhaustion for a parked
/// element. Panics where the element does not match the wired layout.
pub fn element<T: Send + Sync + dyn_any::StaticTypeSized>(&mut self, value: T, arena: &crate::arena::Arena) -> Option<()> {
self.check_element::<T>();
// SAFETY: the frame is this layout's fresh claim and the element
// check pinned `T` to the layout's element slot.
unsafe { write_element(self.dst(), value, arena) }
}
/// Lifts a kernel's poll into the frame and closes it: the element
/// writes on value polls, every poll keeps the frame claimed, and arena
/// exhaustion of a parked element reports as an error poll. Panics where
/// the element does not match the wired layout, or where a field-bearing
/// layout closes on a frame no carry or field write ever filled, since the
/// [`Served`] proof certifies the whole record and the fields would be the
/// prior frame's bytes.
pub fn lift<T: Send + Sync + dyn_any::StaticTypeSized>(mut self, poll: GPoll<T>, arena: &'e crate::arena::Arena) -> GPoll<RecordValue<'e>> {
self.check_element::<T>();
assert!(
self.layout.fields.is_empty() || self.filled_fields,
"a layout with {} fields must carry or write them before lifting",
self.layout.fields.len()
);
let frame_bytes = self.layout.frame_bytes();
let dst = self.dst();
// SAFETY: the frame is this layout's fresh claim, the element check
// pinned `T`, and the drop keeps the frame contract on every poll.
unsafe { lift_poll_into(poll, dst, frame_bytes, arena) }
}
/// Copies a complete record of this layout into the frame, for serving
/// cached or published bytes.
///
/// # Safety
/// `src` must point at a live record of this layout whose parked
/// references outlive the serving evaluation.
pub unsafe fn fill_copy(&mut self, src: *const u8) {
unsafe { std::ptr::copy_nonoverlapping(src, self.dst(), self.layout.size) };
self.filled_fields = true;
}
/// The served record. The frame stays claimed for the consumer; the drop
/// releases only what was claimed above it.
///
/// # Safety
/// The frame must hold a complete record of the layout, written through
/// the carry, element, and field writes.
pub unsafe fn finish(mut self) -> RecordValue<'e> {
match self.frame {
Some(frame) => RecordValue::spilled(unsafe { Rec::new(frame.cast_const()) }),
// SAFETY: the inline record is the value's own bytes.
None => unsafe { (&raw mut self.inline).cast::<RecordValue<'e>>().read() },
}
}
/// [`Self::lift`] with the proof-bearing return for [`Node::serve`].
pub fn lift_served<T: Send + Sync + dyn_any::StaticTypeSized>(self, poll: GPoll<T>, arena: &'e crate::arena::Arena) -> GPoll<Served<'e>> {
self.lift(poll, arena).map(|value| Served { value })
}
/// [`Self::finish`] with the proof-bearing return for [`Node::serve`].
///
/// # Safety
/// As [`Self::finish`].
pub unsafe fn finish_served(self) -> Served<'e> {
Served { value: unsafe { self.finish() } }
}
/// Fills the frame from a record a forwarded input already served, and
/// closes it: the source's frame sits above this claim and dies with its
/// drop, so the served record is this claim's own.
///
/// # Safety
/// `value` must be a live record of this frame's layout.
pub unsafe fn forward(mut self, value: &RecordValue<'_>) -> Served<'e> {
let src = self.layout.rec(value).ptr();
unsafe {
self.fill_copy(src);
self.finish_served()
}
}
/// Translates a source record into the frame through a wiring-resolved plan.
///
/// # Safety
/// `src` must be a live record of `plan`'s source layout, `plan` must
/// translate into this frame's layout, and `src` must not overlap this
/// frame, as [`Self::carry`] requires.
pub unsafe fn translate(&mut self, src: Rec<'_>, plan: &SourcePlan) {
unsafe { plan.translate(src, self.dst()) };
self.filled_fields = true;
}
}
/// A materialized run of lanes as the arena region its frames live in: the
/// handle keeps the provenance the region was allocated with and carries the
/// generation, so resolving it re-checks liveness where an address would have
/// been trusted. The layout stays with the holder, which proved it at wiring.
#[derive(Clone, Copy)]
pub struct MaterializedSpan {
pub(in crate::record) base: crate::arena::ArenaWeak<u8>,
pub(in crate::record) len: usize,
}
impl std::fmt::Debug for MaterializedSpan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MaterializedSpan").field("len", &self.len).finish()
}
}
impl MaterializedSpan {
/// `None` where the batch's frames are not this arena's, which is the
/// caller's cue to re-materialize rather than cache.
pub fn of(batch: &crate::node::RecordBatch<'_>, arena: &crate::arena::Arena) -> Option<MaterializedSpan> {
match batch.len() {
0 => Some(MaterializedSpan {
base: crate::arena::ArenaWeak::NULL,
len: 0,
}),
len => Some(MaterializedSpan {
base: arena.handle_at(batch.get(0).rec().ptr())?,
len,
}),
}
}
/// The span's lanes at `layout`, or `None` once the generation moved on.
pub fn batch<'a>(&self, arena: &'a crate::arena::Arena, layout: &'a Layout) -> Option<crate::node::RecordBatch<'a>> {
let base: *const u8 = match self.len {
0 => std::ptr::NonNull::<u8>::dangling().as_ptr(),
_ => self.base.upgrade(arena)?,
};
// SAFETY: the handle resolved in generation, so the region still holds
// the lanes it was published with, packed at the layout's stride; an
// empty span reads no lane.
Some(unsafe { crate::node::RecordBatch::new(base, self.len, layout) })
}
/// Lane `lane`'s record, or `None` past the span or once the generation
/// moved on.
pub fn lane(&self, arena: &crate::arena::Arena, lane: usize, layout: &Layout) -> Option<*const u8> {
(lane < self.len).then_some(())?;
let base: *const u8 = self.base.upgrade(arena)?;
// SAFETY: in-bounds by the length check, at the layout the span was
// published under.
Some(unsafe { base.add(lane * layout.lane_stride()) })
}
}
/// The proof a record was served through a frame claim: mintable only by the
/// claim's closing methods, so holding one means the record is of the
/// claimed layout.
pub struct Served<'e> {
value: RecordValue<'e>,
}
impl<'e> Served<'e> {
pub fn value(self) -> RecordValue<'e> {
self.value
}
/// The served record in place, for producers that read it before passing
/// the proof on.
pub fn record(&self) -> &RecordValue<'e> {
&self.value
}
}
/// Claims `node`'s own frame from `frames` and serves through it: the
/// caller-side half of [`Node::serve`], for drivers that want the record
/// rather than the proof.
pub fn serve_input<'e, C, N>(node: &N, input: &C, frames: &Frames<'e>) -> GPoll<RecordValue<'e>>
where
N: Node<C> + ?Sized,
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
let slot = frames.claim(node.layout());
node.serve(input, slot).map(Served::value)
}
/// A claim shortens onto a derived context's arena lifetime.
#[cfg(test)]
fn claim_shortens<'long: 'short, 'short, 'l>(claim: FrameClaim<'long, 'l>) -> FrameClaim<'short, 'l> {
claim
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::frames::FrameArena;
use crate::record::layout::element_write;
#[test]
#[should_panic(expected = "must match the wired layout")]
fn a_mistyped_element_is_rejected_at_the_write() {
let arena = crate::arena::Arena::new(256).unwrap();
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[]);
let mut frame_arena = FrameArena::new();
frame_arena.reserve(1 << 10);
let frames = frame_arena.frames();
frames.claim(&layout).element(1u32, &arena);
}
#[test]
fn a_run_refuses_a_lane_count_whose_stride_product_overflows() {
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[]);
let mut scratch = [std::mem::MaybeUninit::<u64>::uninit(); 4];
let mut frame_arena = FrameArena::new();
frame_arena.reserve(64);
let frames = frame_arena.frames();
let wrapping = usize::MAX / layout.lane_stride() + 1;
assert!(frames.run(&mut scratch, wrapping, &layout).is_none(), "a wrapped capacity product must not pass the check");
}
#[test]
#[should_panic(expected = "serves out of order")]
fn a_run_refuses_a_gapped_serve() {
let arena = crate::arena::Arena::new(1024).unwrap();
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[]);
let mut scratch = [std::mem::MaybeUninit::<u64>::uninit(); 8];
let mut frame_arena = FrameArena::new();
frame_arena.reserve(1 << 10);
let frames = frame_arena.frames();
let mut run = frames.run(&mut scratch, 3, &layout).unwrap();
let lane_frames = frames.scope();
let slot = run.slot(2, &lane_frames);
let GPoll::Final(proof) = slot.lift_served(GPoll::Final(1.0f64), &arena) else {
panic!("expected a final record");
};
run.served(2, &proof);
}
#[test]
fn a_claim_shortens_onto_a_derived_lifetime() {
fn shorten<'long: 'short, 'short, 'l>(claim: FrameClaim<'long, 'l>) -> FrameClaim<'short, 'l> {
claim_shortens(claim)
}
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[]);
let mut frame_arena = FrameArena::new();
frame_arena.reserve(64);
let frames = frame_arena.frames();
let claim = shorten(frames.claim(&layout));
assert_eq!(claim.layout, &layout);
}
}
@@ -0,0 +1,25 @@
//! Shared fixtures for the record module family's tests.
use super::layout::FieldWrite;
unsafe fn unread(_: *const u8) -> Box<dyn crate::list::AnyAttributeValue> {
unreachable!("layout-only test field")
}
pub(super) fn sized_field(name: &'static str, size: usize, align: usize) -> FieldWrite {
FieldWrite {
name,
level: 0,
size,
align,
type_id: std::any::TypeId::of::<()>(),
read_erased: unread,
repark: None,
content_hash: None,
content_eq: None,
}
}
pub(super) fn f64_field(name: &'static str) -> FieldWrite {
sized_field(name, 8, 8)
}
@@ -0,0 +1,182 @@
//! Law-test scaffolding over the record tier.
use super::frames::{FrameArena, Frames};
use super::layout::{Layout, element_write};
use super::owned::OwnedRecord;
use super::serve::{FrameClaim, Served, serve_input};
use crate::gpoll::GPoll;
use crate::node::Node;
/// Law-test scaffolding: a frame space of `bytes`, leaked so a fixture holds
/// it for the whole test without threading the buffer's own borrow. Production
/// roots own their buffer and lend it by `&mut`.
#[doc(hidden)]
pub fn test_frames(bytes: usize) -> Frames<'static> {
let arena: &'static mut FrameArena = Box::leak(Box::new(FrameArena::new()));
arena.reserve(bytes);
arena.frames()
}
/// One record captured out of a poll: the deep copy plus the layout it was
/// served at, so assertions read owned storage with no tie to the record
/// stack. [`capture`] is the only constructor.
pub struct ServedRecord {
layout: Layout,
record: OwnedRecord,
}
impl ServedRecord {
pub fn layout(&self) -> &Layout {
&self.layout
}
/// The element, cloned out of the capture. Panics unless `T` is the
/// layout's element type.
pub fn element<T: Clone + 'static>(&self) -> T {
assert_eq!(std::any::TypeId::of::<T>(), self.layout.element.type_id, "the read type must match the layout's element type");
match &self.record.element {
Some(parked) => parked.downcast_ref::<T>().expect("the element parked at its own type").clone(),
// SAFETY: the captured bytes image a record of this layout; a
// byte-carried element is a `T` with no drop glue.
None => unsafe { self.record.bytes.as_ptr().cast::<T>().read_unaligned() },
}
}
/// The marker's level-0 field value. Panics unless the layout declares
/// the marker at its value type.
pub fn attr<A: crate::attribute::Attribute>(&self) -> A::Value<'static>
where
A::Value<'static>: Copy + 'static,
{
self.field(A::NAME, 0)
}
/// A field by name and level, for layouts whose fields are not census
/// markers. Panics unless the field is declared at `T` and byte-carried;
/// a parked field reads through replay, not the captured bytes.
pub fn field<T: Copy + 'static>(&self, name: &str, level: u8) -> T {
let field = self
.layout
.fields
.iter()
.find(|field| field.name == name && field.level == level)
.expect("the layout carries the read field");
assert_eq!(field.type_id, std::any::TypeId::of::<T>(), "the field was declared at this value type");
assert!(field.repark.is_none(), "a parked field reads through replay, not the captured bytes");
// SAFETY: the captured bytes image a record of this layout and the
// field is byte-carried at `T`.
unsafe { self.record.bytes.as_ptr().add(field.offset).cast::<T>().read_unaligned() }
}
}
/// Polls `node` once and captures any served record: the record is
/// deep-copied at the node's own declared layout inside a frame scope, so the
/// result is owned and every claimed frame is free again. Assertion
/// scaffolding for law tests; production consumers read served records in
/// place.
pub fn capture<'e, C, N>(node: &N, ctx: &C, frames: &Frames<'e>) -> GPoll<ServedRecord>
where
N: Node<C> + ?Sized,
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
let scope = frames.scope();
let layout = node.layout().clone();
serve_input(node, ctx, &scope).map(|value| ServedRecord {
// SAFETY: the poll served `value` at the node's declared layout and
// nothing has claimed frames since.
record: unsafe { OwnedRecord::copy_out(&layout, layout.rec(&value)) },
layout: layout.clone(),
})
}
/// Law-test scaffolding: a kernel closure served onto an element-only record
/// input (the element lands at offset 0, parked when it carries drop glue). No
/// production path constructs one; value sources are
/// [`crate::value::ValueSource`].
pub struct LiftedSource<El, F> {
kernel: F,
layout: Layout,
_marker: std::marker::PhantomData<fn() -> El>,
}
impl<El: Clone + Send + Sync + dyn_any::StaticTypeSized, F> LiftedSource<El, F>
where
El::Static: Clone + Send + Sync,
{
pub fn new(kernel: F) -> Self {
Self {
kernel,
layout: Layout::default().with_writes(0, element_write::<El>(), &[]),
_marker: std::marker::PhantomData,
}
}
}
impl<C, El, F> Node<C> for LiftedSource<El, F>
where
El: Send + Sync + dyn_any::StaticTypeSized,
F: Fn(&C) -> GPoll<El>,
{
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
slot.lift_served((self.kernel)(input), input.arena())
}
fn layout(&self) -> &Layout {
&self.layout
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::layout::FieldWrite;
#[test]
fn a_captured_frame_serves_its_writes() {
use crate::attribute::{Attribute, Transform};
use glam::{DAffine2, DVec2};
struct Fixture {
layout: Layout,
}
impl<C> Node<C> for Fixture {
fn serve<'e, 'l>(&self, input: &C, mut slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
let offset = self.layout.offset_of(Transform::NAME, 0).expect("the fixture's layout carries the transform");
if slot.element(String::from("parked"), crate::context::ExtractArena::arena(input)).is_none() {
return GPoll::error("arena exhausted");
}
// SAFETY: the offset is this layout's own, at the marker's value type.
unsafe { slot.attr_at(offset, DAffine2::from_translation(DVec2::new(3., 4.))) };
// SAFETY: the writes above complete the record.
GPoll::Final(unsafe { slot.finish_served() })
}
fn layout(&self) -> &Layout {
&self.layout
}
}
let layout = Layout::default().with_writes(0, element_write::<String>(), &[FieldWrite::of::<Transform>(0)]);
let mut frame_arena = FrameArena::new();
frame_arena.reserve(1 << 10);
let frames = frame_arena.frames();
let arena = crate::arena::Arena::new(1024).unwrap();
let generations = [];
let scope = crate::context::EvalScope::new(None, None, None, &generations, &arena);
let ctx = crate::context::ContextImpl::root(&scope);
let free = frames.free_words();
let GPoll::Final(served) = capture(&Fixture { layout }, &ctx, &frames) else {
panic!("the fixture serves finally");
};
assert_eq!(frames.free_words(), free, "capture returns every claimed slot");
assert_eq!(served.element::<String>(), "parked");
assert_eq!(served.attr::<Transform>(), DAffine2::from_translation(DVec2::new(3., 4.)));
}
}
@@ -0,0 +1,620 @@
use crate::concrete;
use crate::context::{Context, ContextImpl};
use crate::node::Node;
use crate::{ContextFeature, ProtoNodeIdentifier, Type, WasmNotSend, WasmNotSync};
use dyn_any::DynAny;
use graphene_hash::CacheHash;
pub use no_std_types::registry::types;
use std::collections::HashMap;
use std::hash::Hasher;
use std::sync::{LazyLock, Mutex};
// Translation struct between macro and definition
#[derive(Clone, Debug)]
pub struct NodeMetadata {
pub display_name: &'static str,
pub category: &'static str,
pub fields: Vec<FieldMetadata>,
pub description: &'static str,
pub properties: Option<&'static str>,
pub context_features: Vec<ContextFeature>,
pub memoize: bool,
pub inject_scope: bool,
/// Set where `_runtime` and `_source` are the last two entries of `fields`.
pub async_source_fields: bool,
}
// Translation struct between macro and definition
#[derive(Clone, Debug)]
pub struct FieldMetadata {
pub name: &'static str,
pub description: &'static str,
/// Index levels the node pushes when evaluating this input.
pub pushed_levels: u8,
pub hidden: bool,
pub exposed: bool,
pub widget_override: RegistryWidgetOverride,
pub value_source: RegistryValueSource,
pub default_type: Option<Type>,
/// The slider's suggested extent, from `#[soft(a..b)]`. Typed values may exceed it.
pub number_soft_min: Option<f64>,
pub number_soft_max: Option<f64>,
/// The enforced clamp, from `#[hard(a..b)]`. Applied to typed values and at eval time.
pub number_hard_min: Option<f64>,
pub number_hard_max: Option<f64>,
pub number_mode_range: bool,
pub number_display_decimal_places: Option<u32>,
pub number_step: Option<f64>,
pub unit: Option<&'static str>,
}
#[derive(Clone, Debug)]
pub enum RegistryWidgetOverride {
None,
Hidden,
String(&'static str),
Custom(&'static str),
}
#[derive(Clone, Debug)]
pub enum RegistryValueSource {
None,
Default(&'static str),
Scope(&'static str),
SourceId,
}
type NodeRegistry = LazyLock<Mutex<HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>>>>;
pub static NODE_REGISTRY: NodeRegistry = LazyLock::new(|| Mutex::new(HashMap::new()));
pub static NODE_METADATA: LazyLock<Mutex<HashMap<ProtoNodeIdentifier, NodeMetadata>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
pub use crate::NodeIOTypes;
/// Element-independent by erasure; the input's `Type::Record(El)` keeps element reads proven at wiring.
#[cfg(not(target_family = "wasm"))]
pub type ErasedRecordNode = dyn for<'c> Node<ContextImpl<'c>> + Send + Sync;
#[cfg(target_family = "wasm")]
pub type ErasedRecordNode = dyn for<'c> Node<ContextImpl<'c>>;
#[cfg(not(target_family = "wasm"))]
type DynSource = dyn std::any::Any + Send + Sync;
#[cfg(target_family = "wasm")]
type DynSource = dyn std::any::Any;
pub fn record_type<T: 'static>() -> Type {
Type::Record(Box::new(concrete!(T)))
}
pub fn record_source_type<T: 'static>() -> Type {
Type::Fn(Box::new(concrete!(Context)), Box::new(record_type::<T>()))
}
/// The record source type of a token row, generic over the element.
pub fn generic_record_source_type(name: &'static str) -> Type {
Type::Fn(Box::new(concrete!(Context)), Box::new(Type::Record(Box::new(Type::Generic(std::borrow::Cow::Borrowed(name))))))
}
pub fn cache_key<C: CacheHash + ?Sized>(ctx: &C) -> u64 {
let mut hasher = graphene_hash::FxHasher64::new();
ctx.cache_hash(&mut hasher);
hasher.finish()
}
#[derive(Debug, PartialEq)]
pub enum ConstructionError {
Arity { expected: usize, got: usize },
Type { expected: Box<Type>, found: Box<Type> },
}
pub struct SharedSource<N: ?Sized> {
ptr: std::ptr::NonNull<N>,
own: std::sync::Arc<N>,
}
impl<N: ?Sized> SharedSource<N> {
pub fn new(own: std::sync::Arc<N>) -> Self {
Self {
ptr: std::ptr::NonNull::from(&*own),
own,
}
}
pub fn share(&self) -> Self {
Self { ptr: self.ptr, own: self.own.clone() }
}
/// Re-derives the cached pointer from the owned payload. An exclusive
/// re-borrow of the payload invalidates the pointer taken before it, so
/// every mutation through `own` ends here, an unwinding one included.
pub fn rederive(&mut self) {
self.ptr = std::ptr::NonNull::from(&*self.own);
}
}
/// Re-derives on the way out of an exclusive re-borrow, so a mutation that
/// panics cannot leave the cached pointer retired for a caller that catches.
struct Rederive<'a, N: ?Sized>(&'a mut SharedSource<N>);
impl<N: ?Sized> Drop for Rederive<'_, N> {
fn drop(&mut self) {
self.0.rederive();
}
}
// SAFETY: `ptr` is derived from the owned Arc and never mutated through, so the source is exactly as
// thread safe as the payload it shares.
unsafe impl<N: ?Sized + Send + Sync> Send for SharedSource<N> {}
// SAFETY: as in Send.
unsafe impl<N: ?Sized + Send + Sync> Sync for SharedSource<N> {}
impl<Input, N> Node<Input> for SharedSource<N>
where
N: Node<Input> + ?Sized,
{
fn serve<'e, 'l>(&self, input: &Input, slot: crate::record::FrameClaim<'e, 'l>) -> crate::gpoll::GPoll<crate::record::Served<'e>>
where
Input: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
{
// SAFETY: `own` keeps the payload alive for `self`'s lifetime and Arc
// payloads are address stable.
unsafe { self.ptr.as_ref() }.serve(input, slot)
}
fn extent_at<'x>(&self, input: &Input, level: u8, frames: &crate::record::Frames<'x>) -> crate::gpoll::GPoll<crate::gpoll::Extent>
where
Input: crate::context::ExtractArena<ArenaRef = &'x crate::arena::Arena>,
{
// SAFETY: as in serve.
unsafe { self.ptr.as_ref() }.extent_at(input, level, frames)
}
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
// SAFETY: as in serve.
unsafe { self.ptr.as_ref() }.serialize()
}
fn layout(&self) -> &crate::record::Layout {
// SAFETY: as in serve.
unsafe { self.ptr.as_ref() }.layout()
}
fn eval_batch<'a, 'x>(
&'a self,
input: &'a Input,
range: std::ops::Range<u64>,
scratch: Option<&'a mut [std::mem::MaybeUninit<u64>]>,
frames: &crate::record::Frames<'x>,
) -> crate::node::BatchStatus<'a>
where
Input: crate::context::InjectIndex + Copy + crate::context::ExtractArena<ArenaRef = &'x crate::arena::Arena>,
{
// SAFETY: as in serve.
unsafe { self.ptr.as_ref() }.eval_batch(input, range, scratch, frames)
}
}
pub struct SourceHandle {
node: Box<DynSource>,
share: fn(&DynSource) -> Box<DynSource>,
serialize: fn(&DynSource) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
layout: fn(&DynSource) -> &crate::record::Layout,
set_layout: fn(&mut DynSource, crate::record::RecordLayout),
ty: Type,
}
impl std::fmt::Debug for SourceHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SourceHandle").field("ty", &self.ty).finish_non_exhaustive()
}
}
// SAFETY: wasm is single threaded, so the marker-free payload never actually crosses a thread.
#[cfg(target_family = "wasm")]
unsafe impl Send for SourceHandle {}
// SAFETY: as in Send.
#[cfg(target_family = "wasm")]
unsafe impl Sync for SourceHandle {}
#[cfg(all(target_family = "wasm", target_feature = "atomics"))]
compile_error!("SourceHandle's wasm Send/Sync rest on a single-threaded build; a +atomics target needs the stored node bounded instead");
impl SourceHandle {
pub fn new_record<T: 'static>(node: std::sync::Arc<ErasedRecordNode>) -> Self {
Self::new_erased(node, record_source_type::<T>())
}
pub fn new_erased<N>(node: std::sync::Arc<N>, ty: Type) -> Self
where
N: ?Sized + 'static + for<'c> Node<ContextImpl<'c>>,
SharedSource<N>: WasmNotSend + WasmNotSync,
{
Self {
node: Box::new(SharedSource::new(node)),
share: |edge| Box::new(edge.downcast_ref::<SharedSource<N>>().expect("share hook matches the stored edge type").share()),
serialize: |edge| Node::<ContextImpl>::serialize(edge.downcast_ref::<SharedSource<N>>().expect("serialize hook matches the stored edge type")),
layout: |edge| Node::<ContextImpl>::layout(edge.downcast_ref::<SharedSource<N>>().expect("layout hook matches the stored edge type")),
set_layout: |edge, layout| {
let shared = edge.downcast_mut::<SharedSource<N>>().expect("set_layout hook matches the stored edge type");
// The re-borrow below retires the cached pointer, so the rederive
// is a guard: a panicking `set_layout` must not leave it stale.
let guard = Rederive(shared);
let node = std::sync::Arc::get_mut(&mut guard.0.own).expect("layout is installed before the node is shared");
Node::<ContextImpl>::set_layout(node, layout);
},
ty,
}
}
pub fn ty(&self) -> &Type {
&self.ty
}
pub fn duplicate(&self) -> Self {
Self {
node: (self.share)(&*self.node),
share: self.share,
serialize: self.serialize,
layout: self.layout,
set_layout: self.set_layout,
ty: self.ty.clone(),
}
}
pub fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
(self.serialize)(&*self.node)
}
pub fn layout(&self) -> &crate::record::Layout {
(self.layout)(&*self.node)
}
pub fn set_layout(&mut self, layout: crate::record::RecordLayout) {
(self.set_layout)(&mut *self.node, layout);
}
pub fn downcast_record<T: 'static>(self) -> Result<SharedSource<ErasedRecordNode>, ConstructionError> {
self.downcast_erased(record_source_type::<T>())
}
/// The erased record source, for callers that dispatch on the layout rather
/// than a static element type. `None` for a source erased to another node type.
pub fn record_source(self) -> Option<SharedSource<ErasedRecordNode>> {
self.node.downcast::<SharedSource<ErasedRecordNode>>().ok().map(|edge| *edge)
}
pub fn downcast_erased<N: ?Sized + 'static>(self, expected: Type) -> Result<SharedSource<N>, ConstructionError> {
let found = self.ty;
self.node.downcast::<SharedSource<N>>().map(|edge| *edge).map_err(|_| ConstructionError::Type {
expected: Box::new(expected),
found: Box::new(found),
})
}
}
pub type NodeConstructor = fn(Vec<SourceHandle>) -> Result<SourceHandle, ConstructionError>;
#[derive(Clone)]
pub struct RegistryEntry {
pub io: NodeIOTypes,
pub constructor: NodeConstructor,
/// Declarative record-io metadata for the compiler layout pass; `None` for
/// nodes whose layout the pass does not fold (routing/opaque, hand-written
/// rows), which keep the construction-time path.
pub layout_meta: Option<crate::record::LayoutMeta>,
}
pub fn construct(entry: &RegistryEntry, inputs: Vec<SourceHandle>) -> Result<SourceHandle, ConstructionError> {
if inputs.len() != entry.io.inputs.len() {
return Err(ConstructionError::Arity {
expected: entry.io.inputs.len(),
got: inputs.len(),
});
}
for (handle, expected) in inputs.iter().zip(&entry.io.inputs) {
if handle.ty() != expected {
return Err(ConstructionError::Type {
expected: Box::new(expected.clone()),
found: Box::new(handle.ty().clone()),
});
}
}
(entry.constructor)(inputs)
}
#[cfg(not(target_family = "wasm"))]
pub type Any<'n> = Box<dyn DynAny<'n> + 'n + Send>;
#[cfg(target_family = "wasm")]
pub type Any<'n> = Box<dyn DynAny<'n> + 'n>;
#[cfg(test)]
mod tests {
use super::*;
use crate::SourceId;
use crate::arena::Arena;
use crate::context::{Ctx, EvalScope, ExtractArena, ExtractIndices};
use crate::gpoll::GPoll;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use crate::record::{FrameClaim, Layout, LiftedSource, Served, element_write, read_element, serve_input};
fn counting() -> LiftedSource<u32, impl for<'c> Fn(&ContextImpl<'c>) -> GPoll<u32>> {
let count = AtomicU32::new(0);
LiftedSource::new(move |_: &ContextImpl<'_>| GPoll::Final(count.fetch_add(1, Ordering::Relaxed) + 1))
}
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
EvalScope::new(Some(0.5), None, None, generations, arena)
}
/// Serves a parked borrow of its own value: the element is a reference into
/// the evaluation's arena, so its lifetime is the serving one.
struct LendNode {
value: String,
layout: Layout,
}
impl LendNode {
fn new(value: &str) -> Self {
Self {
value: value.to_string(),
layout: Layout::default().with_writes(0, element_write::<&'static String>(), &[]),
}
}
}
impl<Input: Ctx> Node<Input> for LendNode {
fn serve<'e, 'l>(&self, input: &Input, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
Input: ExtractArena<ArenaRef = &'e Arena>,
{
match input.arena().alloc(self.value.clone()) {
Some((parked, _)) => slot.lift_served(GPoll::Final(parked), input.arena()),
None => GPoll::arena_exhausted(),
}
}
fn layout(&self) -> &Layout {
&self.layout
}
}
#[test]
fn borrow_carrying_value_types_wire_through_the_general_constructor() {
let arena = Arena::new(4096).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let frames = crate::record::test_frames(1 << 12);
let node = LendNode::new("held");
let layout = Node::<ContextImpl>::layout(&node).clone();
let handle = SourceHandle::new_erased(Arc::new(node) as Arc<ErasedRecordNode>, concrete!(String));
assert_eq!(*handle.ty(), concrete!(String));
let wired = handle.downcast_erased::<ErasedRecordNode>(concrete!(String)).unwrap();
let GPoll::Final(value) = serve_input(&wired, &ctx, &frames) else {
panic!("borrow-carrying output must serve through the erased edge");
};
// SAFETY: the record was served at `layout`, whose element is the borrow.
let held = unsafe { read_element::<&String>(layout.rec(&value)) };
assert_eq!(held, "held");
assert_eq!(held.len(), 4);
}
/// Evaluates its content at three indices of one promoted level and serves
/// the collected elements.
struct RepeatNode<Node0, T> {
content: Node0,
inner: Layout,
layout: Layout,
_marker: std::marker::PhantomData<fn() -> T>,
}
impl<Node0, T: Clone + Send + Sync + dyn_any::StaticTypeSized> RepeatNode<Node0, T>
where
Vec<T>: Clone + Send + Sync + dyn_any::StaticTypeSized,
<Vec<T> as dyn_any::StaticTypeSized>::Static: Clone + Send + Sync,
{
fn new(content: Node0, inner: Layout) -> Self {
Self {
content,
inner,
layout: Layout::default().with_writes(0, element_write::<Vec<T>>(), &[]),
_marker: std::marker::PhantomData,
}
}
}
impl<C, T, Node0> Node<C> for RepeatNode<Node0, T>
where
C: Ctx + crate::context::DeriveCtx,
T: Clone + 'static,
Vec<T>: Send + Sync + dyn_any::StaticTypeSized,
Node0: for<'x> crate::record::DerivedRecordInput<'x, crate::context::Derived<'x, C>>,
{
fn serve<'e, 'l>(&self, input: &C, mut slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
let cell = crate::node::StatusCell::new();
let spilled = input.index_head();
let mut result = Vec::new();
for index in 0..3 {
// The element copies out by value, so the content's frame is
// dead when the scope ends.
let scope = slot.frames().scope();
let derived = input.promoted(&spilled, index);
match self.content.eval_derived(&cell, 0, &derived, &scope) {
// SAFETY: the content served at its own layout, whose
// element is `T`.
Ok(value) => result.push(unsafe { read_element::<T>(self.inner.rec(&value)) }),
Err(interrupt) => return interrupt.into(),
}
}
slot.lift_served(cell.finish(result), input.arena())
}
fn layout(&self) -> &Layout {
&self.layout
}
}
#[test]
fn derive_ctx_repeat_pushes_index_levels_through_the_erased_edge() {
let arena = Arena::new(1024).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let levels = LiftedSource::<Vec<usize>, _>::new(|input: &ContextImpl| GPoll::Final(input.try_index().map(|levels| levels.collect()).unwrap_or_default()));
let levels_layout = Node::<ContextImpl>::layout(&levels).clone();
let inner = RepeatNode::<_, Vec<usize>>::new(levels, levels_layout);
let inner_layout = Node::<ContextImpl>::layout(&inner).clone();
let nested = RepeatNode::<_, Vec<Vec<usize>>>::new(inner, inner_layout);
let layout = Node::<ContextImpl>::layout(&nested).clone();
let erased: Box<ErasedRecordNode> = Box::new(nested);
let frames = crate::record::test_frames(1 << 12);
let GPoll::Final(value) = serve_input(&*erased, &ctx, &frames) else {
panic!("nested repeat must evaluate");
};
// SAFETY: the record was served at `layout`, whose element is the output.
let outer = unsafe { read_element::<Vec<Vec<Vec<usize>>>>(layout.rec(&value)) };
assert_eq!(outer.len(), 3);
assert_eq!(outer[2][1], vec![1, 2, 0]);
assert_eq!(outer[0][0], vec![0, 0, 0]);
}
/// Shifts the footprint's resolution and serves its content's element under
/// the derived context.
struct ShiftFootprintNode<Node0> {
content: Node0,
inner: Layout,
layout: Layout,
}
impl<Node0> ShiftFootprintNode<Node0> {
fn new(content: Node0, inner: Layout) -> Self {
Self {
content,
inner,
layout: Layout::default().with_writes(0, element_write::<u32>(), &[]),
}
}
}
impl<C, Node0> Node<C> for ShiftFootprintNode<Node0>
where
C: Ctx + crate::context::DeriveCtx + crate::context::ExtractFootprint,
Node0: for<'x> crate::record::DerivedRecordInput<'x, crate::context::Derived<'x, C>>,
{
fn serve<'e, 'l>(&self, input: &C, mut slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
where
C: ExtractArena<ArenaRef = &'e Arena>,
{
use crate::transform::Footprint;
let cell = crate::node::StatusCell::new();
let mut footprint = input.try_footprint().copied().unwrap_or(Footprint::DEFAULT);
footprint.resolution.x += 7;
// The element copies out by value, so the content's frame is dead
// when the scope ends.
let value = {
let scope = slot.frames().scope();
let derived = input.with_footprint(&footprint);
match self.content.eval_derived(&cell, 0, &derived, &scope) {
// SAFETY: the content served at its own layout, whose
// element is the resolution.
Ok(value) => unsafe { read_element::<u32>(self.inner.rec(&value)) },
Err(interrupt) => return interrupt.into(),
}
};
slot.lift_served(cell.finish(value), input.arena())
}
fn layout(&self) -> &Layout {
&self.layout
}
}
#[test]
fn derive_ctx_footprint_replace_reaches_the_content() {
use crate::context::ExtractFootprint;
use crate::transform::Footprint;
let arena = Arena::new(1024).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let frames = crate::record::test_frames(1 << 12);
let resolution = LiftedSource::<u32, _>::new(|input: &ContextImpl| GPoll::Final(input.try_footprint().map(|footprint| footprint.resolution.x).unwrap_or(0)));
let resolution_layout = Node::<ContextImpl>::layout(&resolution).clone();
let shifted = ShiftFootprintNode::new(resolution, resolution_layout);
let shifted_layout = Node::<ContextImpl>::layout(&shifted).clone();
let graph = ShiftFootprintNode::new(shifted, shifted_layout);
let layout = Node::<ContextImpl>::layout(&graph).clone();
let GPoll::Final(value) = serve_input(&graph, &ctx, &frames) else {
panic!("the footprint shift must reach the content");
};
// SAFETY: the record was served at `layout`, whose element is the resolution.
assert_eq!(unsafe { read_element::<u32>(layout.rec(&value)) }, Footprint::DEFAULT.resolution.x + 14);
}
#[test]
fn construct_checks_arity_and_types() {
fn construct_strlen(args: Vec<SourceHandle>) -> Result<SourceHandle, ConstructionError> {
let mut args = args.into_iter();
let value = args.next().ok_or(ConstructionError::Arity { expected: 1, got: 0 })?.downcast_record::<String>()?;
drop(value);
Ok(crate::value::record_value_source(0u32))
}
let entry = RegistryEntry {
layout_meta: None,
io: NodeIOTypes::new(concrete!(Context), record_type::<u32>(), vec![record_source_type::<String>()]),
constructor: construct_strlen,
};
let owned = crate::value::record_value_source("typed".to_string());
assert!(construct(&entry, vec![owned]).is_ok());
assert_eq!(construct(&entry, vec![]).unwrap_err(), ConstructionError::Arity { expected: 1, got: 0 });
let mistyped = crate::value::record_value_source(1.0f64);
assert_eq!(
construct(&entry, vec![mistyped]).unwrap_err(),
ConstructionError::Type {
expected: Box::new(record_source_type::<String>()),
found: Box::new(record_source_type::<f64>()),
}
);
}
#[test]
fn duplicated_edges_share_one_instance_and_outlive_each_other() {
let arena = Arena::new(1024).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let counting = counting();
let layout = Node::<ContextImpl>::layout(&counting).clone();
let handle = SourceHandle::new_record::<u32>(Arc::new(counting) as Arc<ErasedRecordNode>);
let duplicate = handle.duplicate();
assert_eq!(*duplicate.ty(), record_source_type::<u32>());
let frames = crate::record::test_frames(1 << 12);
let first = handle.downcast_record::<u32>().unwrap();
let second = duplicate.downcast_record::<u32>().unwrap();
// SAFETY: each record was served at `layout`, whose element is the count.
let count = |value| unsafe { layout.rec(&value).element::<u32>() };
assert_eq!(serve_input(&first, &ctx, &frames).map(count), GPoll::Final(1));
assert_eq!(serve_input(&second, &ctx, &frames).map(count), GPoll::Final(2));
drop(first);
assert_eq!(serve_input(&second, &ctx, &frames).map(count), GPoll::Final(3));
}
}
@@ -0,0 +1,27 @@
// Raster types moved to raster-types crate
use crate::Color;
use crate::list::List;
pub trait RenderComplexity {
fn render_complexity(&self) -> usize {
0
}
}
impl<T: RenderComplexity> RenderComplexity for List<T> {
fn render_complexity(&self) -> usize {
self.iter_element_values().map(|element| element.render_complexity()).fold(0, usize::saturating_add)
}
}
impl RenderComplexity for Color {
fn render_complexity(&self) -> usize {
1
}
}
impl RenderComplexity for String {
fn render_complexity(&self) -> usize {
self.chars().count()
}
}
@@ -0,0 +1,679 @@
use crate::SourceId;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
#[cfg(not(target_family = "wasm"))]
pub type SourceFuture<T = ()> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
#[cfg(target_family = "wasm")]
pub type SourceFuture<T = ()> = Pin<Box<dyn Future<Output = T> + 'static>>;
#[cfg(not(target_family = "wasm"))]
pub type DynRuntime = dyn Runtime + Send + Sync;
#[cfg(target_family = "wasm")]
pub type DynRuntime = dyn Runtime;
pub trait Runtime {
/// Returns true when the future completed during the call, so its result is already observable.
fn spawn(&self, source: SourceId, future: SourceFuture) -> bool;
}
#[derive(Clone, dyn_any::DynAny)]
pub struct RuntimeHandle(pub Arc<DynRuntime>);
// SAFETY: wasm is single threaded, so the handle never actually crosses a thread.
#[cfg(target_family = "wasm")]
unsafe impl Send for RuntimeHandle {}
// SAFETY: as in Send.
#[cfg(target_family = "wasm")]
unsafe impl Sync for RuntimeHandle {}
#[cfg(all(target_family = "wasm", target_feature = "atomics"))]
compile_error!("RuntimeHandle's wasm Send/Sync rest on a single-threaded build; a +atomics target needs the payload bounded instead");
impl std::fmt::Debug for RuntimeHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RuntimeHandle").finish_non_exhaustive()
}
}
impl graphene_hash::CacheHash for RuntimeHandle {
fn cache_hash<H: std::hash::Hasher>(&self, _state: &mut H) {}
}
pub trait Spawner {
/// Returns true when the task completed during the call, so its result is already observable.
fn spawn(&self, task: SourceFuture) -> bool;
}
/// Polls `task` once with a no-op waker, returning true if it completed.
pub fn poll_once(task: &mut SourceFuture) -> bool {
let mut context = std::task::Context::from_waker(std::task::Waker::noop());
task.as_mut().poll(&mut context).is_ready()
}
#[cfg(not(target_family = "wasm"))]
pub type DynSpawner = dyn Spawner + Send + Sync;
#[cfg(target_family = "wasm")]
pub type DynSpawner = dyn Spawner;
#[cfg(not(target_family = "wasm"))]
pub type DynNotifier = dyn Fn() + Send + Sync;
#[cfg(target_family = "wasm")]
pub type DynNotifier = dyn Fn();
impl<S: Spawner + ?Sized> Spawner for Box<S> {
fn spawn(&self, task: SourceFuture) -> bool {
(**self).spawn(task)
}
}
/// Polls each task once inline. Tasks that are not immediately ready never complete.
pub struct NoopSpawner;
impl Spawner for NoopSpawner {
fn spawn(&self, mut task: SourceFuture) -> bool {
if poll_once(&mut task) {
return true;
}
log::warn!("async source is not immediately ready and no host spawner is wired; the task is dropped");
false
}
}
pub type DynGraphRuntime = GraphRuntime<Box<DynSpawner>>;
impl Default for RuntimeHandle {
fn default() -> Self {
Self(Arc::new(GraphRuntime::new(Box::new(NoopSpawner) as Box<DynSpawner>)))
}
}
pub struct GraphRuntime<S> {
generations: Arc<Mutex<HashMap<SourceId, u64>>>,
dirty: Arc<AtomicBool>,
notifier: Arc<Mutex<Arc<DynNotifier>>>,
spawner: S,
}
// SAFETY: wasm is single threaded, so the runtime never actually crosses a thread.
#[cfg(target_family = "wasm")]
unsafe impl<S> Send for GraphRuntime<S> {}
// SAFETY: as in Send.
#[cfg(target_family = "wasm")]
unsafe impl<S> Sync for GraphRuntime<S> {}
#[cfg(all(target_family = "wasm", target_feature = "atomics"))]
compile_error!("GraphRuntime's wasm Send/Sync are unbounded in S and rest on a single-threaded build; a +atomics target needs S bounded instead");
impl<S> GraphRuntime<S> {
pub fn new(spawner: S) -> Self {
Self {
generations: Arc::default(),
dirty: Arc::default(),
notifier: Arc::new(Mutex::new(Arc::new(|| {}))),
spawner,
}
}
pub fn set_notifier(&self, notifier: Arc<DynNotifier>) {
*self.notifier.lock().unwrap_or_else(PoisonError::into_inner) = notifier;
}
pub fn retain_sources(&self, live: &[SourceId]) {
let mut generations = self.generations.lock().unwrap_or_else(PoisonError::into_inner);
generations.retain(|source, _| live.contains(source));
for source in live {
generations.entry(*source).or_insert(0);
}
}
pub fn snapshot(&self) -> Vec<(SourceId, u64)> {
let generations = self.generations.lock().unwrap_or_else(PoisonError::into_inner);
let mut snapshot: Vec<_> = generations.iter().map(|(source, generation)| (*source, *generation)).collect();
snapshot.sort_unstable();
snapshot
}
pub fn take_dirty(&self) -> bool {
self.dirty.swap(false, Ordering::Acquire)
}
pub fn spawner(&self) -> &S {
&self.spawner
}
}
impl<S: Spawner> Runtime for GraphRuntime<S> {
fn spawn(&self, source: SourceId, mut future: SourceFuture) -> bool {
let generations = Arc::clone(&self.generations);
let dirty = Arc::clone(&self.dirty);
let notifier = Arc::clone(&self.notifier);
let mut first = true;
self.spawner.spawn(Box::pin(std::future::poll_fn(move |task_context| {
let poll = future.as_mut().poll(task_context);
if poll.is_ready() && !first {
let mut generations = generations.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(generation) = generations.get_mut(&source) {
*generation += 1;
dirty.store(true, Ordering::Release);
drop(generations);
let notifier = Arc::clone(&notifier.lock().unwrap_or_else(PoisonError::into_inner));
notifier();
}
}
first = false;
poll
})))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::arena::Arena;
use crate::context::{ContextImpl, Ctx, EvalScope, ExtractFootprint, ExtractVarArgs, VarArgLink, VarArgSlots};
use crate::gpoll::GPoll;
use crate::node::Node;
use crate::record::{Layout, LiftedSource, RecordExtract, element_write};
use crate::transform::Footprint;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU32, Ordering};
#[derive(Default)]
struct MockRuntime {
futures: Mutex<Vec<(SourceId, SourceFuture)>>,
}
impl Runtime for MockRuntime {
fn spawn(&self, source: SourceId, future: SourceFuture) -> bool {
self.futures.lock().unwrap().push((source, future));
false
}
}
impl MockRuntime {
fn drain(&self) -> Vec<SourceId> {
let futures = std::mem::take(&mut *self.futures.lock().unwrap());
let mut task_ctx = std::task::Context::from_waker(std::task::Waker::noop());
futures
.into_iter()
.map(|(source, mut future)| {
assert!(future.as_mut().poll(&mut task_ctx).is_ready());
source
})
.collect()
}
}
#[derive(Default)]
struct CollectSpawner {
tasks: Mutex<Vec<SourceFuture>>,
}
impl Spawner for CollectSpawner {
fn spawn(&self, mut task: SourceFuture) -> bool {
if poll_once(&mut task) {
return true;
}
self.tasks.lock().unwrap().push(task);
false
}
}
struct YieldOnce(bool);
impl Future for YieldOnce {
type Output = ();
fn poll(mut self: Pin<&mut Self>, task_context: &mut std::task::Context<'_>) -> std::task::Poll<()> {
if self.0 {
return std::task::Poll::Ready(());
}
self.0 = true;
task_context.waker().wake_by_ref();
std::task::Poll::Pending
}
}
fn yield_once() -> YieldOnce {
YieldOnce(false)
}
impl CollectSpawner {
fn drain(&self) -> usize {
let tasks = std::mem::take(&mut *self.tasks.lock().unwrap());
let mut task_ctx = std::task::Context::from_waker(std::task::Waker::noop());
let count = tasks.len();
for mut task in tasks {
assert!(task.as_mut().poll(&mut task_ctx).is_ready());
}
count
}
}
fn element_layout<T: Clone + Send + Sync + dyn_any::StaticTypeSized>() -> Layout
where
T::Static: Clone + Send + Sync,
{
Layout::default().with_writes(0, element_write::<T>(), &[])
}
fn lifted<T: Clone + Send + Sync + dyn_any::StaticTypeSized>(value: T) -> LiftedSource<T, impl for<'c> Fn(&ContextImpl<'c>) -> GPoll<T>>
where
T::Static: Clone + Send + Sync,
{
LiftedSource::new(move |_: &ContextImpl<'_>| GPoll::Final(value.clone()))
}
fn extract<El: Clone + Send + Sync + dyn_any::StaticTypeSized, N: Node<ContextImpl<'static>>>(mut graph: N) -> RecordExtract<El, N>
where
El::Static: Clone + Send + Sync,
{
let layout = element_layout::<El>();
graph.set_layout(crate::record::RecordLayout {
frame_bytes: layout.frame_bytes(),
plan: Vec::new(),
layout: layout.clone(),
lane_invariant: u32::MAX,
});
RecordExtract::new(graph, &layout)
}
static SLOW_DOUBLE_RUNS: AtomicU32 = AtomicU32::new(0);
#[node_macro::node(category(""))]
async fn slow_double(_: impl Ctx, value: f64) -> f64 {
SLOW_DOUBLE_RUNS.fetch_add(1, Ordering::Relaxed);
value * 2.
}
fn stand_in(_value: &f64) -> f64 {
-1.
}
#[node_macro::node(category(""), placeholder(stand_in))]
async fn preview_double(_: impl Ctx, value: f64) -> f64 {
value * 2.
}
#[node_macro::node(category(""), placeholder(stand_in), no_partial)]
async fn strict_double(_: impl Ctx, value: f64) -> f64 {
value * 2.
}
#[node_macro::node(category(""))]
async fn snapshot_resolution(ctx: CtxSnapshot, _primary: ()) -> u32 {
ctx.try_footprint().map(|footprint| footprint.resolution.x).unwrap_or(0)
}
#[node_macro::node(category(""))]
async fn snapshot_vararg(ctx: CtxSnapshot, _primary: ()) -> f64 {
ctx.vararg(0).ok().and_then(|slot| slot.downcast_ref::<f64>()).copied().unwrap_or(0.)
}
static STAGED_RUNS: AtomicU32 = AtomicU32::new(0);
#[node_macro::node(category(""))]
fn staged_double(_: impl Ctx, value: f64) -> SourceFuture<f64> {
STAGED_RUNS.fetch_add(1, Ordering::Relaxed);
Box::pin(async move { value * 2. })
}
#[node_macro::node(category(""))]
fn staged_sum(ctx: impl Ctx, value: f64, addend: impl Node<Context<'_>, Output = f64>) -> Result<SourceFuture<f64>, crate::gpoll::Interrupt> {
let addend = addend.eval(ctx)?;
Ok(Box::pin(async move { value + addend }))
}
fn gated(gate: Arc<std::sync::atomic::AtomicBool>, value: f64) -> LiftedSource<f64, impl for<'c> Fn(&ContextImpl<'c>) -> GPoll<f64>> {
LiftedSource::new(move |_: &ContextImpl<'_>| match gate.load(Ordering::Relaxed) {
true => GPoll::Final(value),
false => GPoll::Pending,
})
}
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
EvalScope::new(None, None, None, generations, arena)
}
#[test]
fn async_source_spawns_once_and_lands_via_the_slot() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let runtime = Arc::new(MockRuntime::default());
let graph = extract::<f64, _>(SlowDoubleNode::new(
lifted(21.0f64),
lifted(RuntimeHandle(runtime.clone())),
lifted(7u64),
&element_layout::<f64>(),
&element_layout::<RuntimeHandle>(),
&element_layout::<u64>(),
));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
assert_eq!(SLOW_DOUBLE_RUNS.load(Ordering::Relaxed), 0);
assert_eq!(runtime.drain(), vec![7]);
assert_eq!(SLOW_DOUBLE_RUNS.load(Ordering::Relaxed), 1);
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(42.0));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(42.0));
assert_eq!(SLOW_DOUBLE_RUNS.load(Ordering::Relaxed), 1);
}
#[test]
fn async_source_reports_the_placeholder_while_in_flight() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let runtime = Arc::new(MockRuntime::default());
let graph = extract::<f64, _>(PreviewDoubleNode::new(
lifted(21.0f64),
lifted(RuntimeHandle(runtime.clone())),
lifted(1u64),
&element_layout::<f64>(),
&element_layout::<RuntimeHandle>(),
&element_layout::<u64>(),
));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Partial(-1.0));
runtime.drain();
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(42.0));
}
#[test]
fn no_partial_maps_the_placeholder_frame_to_pending() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let runtime = Arc::new(MockRuntime::default());
let graph = extract::<f64, _>(StrictDoubleNode::new(
lifted(21.0f64),
lifted(RuntimeHandle(runtime.clone())),
lifted(2u64),
&element_layout::<f64>(),
&element_layout::<RuntimeHandle>(),
&element_layout::<u64>(),
));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
runtime.drain();
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(42.0));
}
#[test]
fn prologue_runs_sync_and_spawns_once() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let runtime = Arc::new(MockRuntime::default());
let graph = extract::<f64, _>(StagedDoubleNode::new(
lifted(21.0f64),
lifted(RuntimeHandle(runtime.clone())),
lifted(8u64),
&element_layout::<f64>(),
&element_layout::<RuntimeHandle>(),
&element_layout::<u64>(),
));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
assert_eq!(STAGED_RUNS.load(Ordering::Relaxed), 1, "the prologue runs synchronously on the miss");
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
assert_eq!(STAGED_RUNS.load(Ordering::Relaxed), 1, "in flight must not rerun the prologue");
assert_eq!(runtime.drain(), vec![8]);
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(42.0));
assert_eq!(STAGED_RUNS.load(Ordering::Relaxed), 1);
}
#[test]
fn prologue_interrupt_defers_the_spawn() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let gate = Arc::new(std::sync::atomic::AtomicBool::new(false));
let runtime = Arc::new(MockRuntime::default());
let graph = extract::<f64, _>(StagedSumNode::new(
lifted(40.0f64),
gated(gate.clone(), 2.0),
lifted(RuntimeHandle(runtime.clone())),
lifted(9u64),
&element_layout::<f64>(),
&element_layout::<f64>(),
&element_layout::<RuntimeHandle>(),
&element_layout::<u64>(),
));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
assert_eq!(runtime.drain(), Vec::<SourceId>::new(), "an interrupted prologue must not spawn or claim the slot");
gate.store(true, Ordering::Relaxed);
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
assert_eq!(runtime.drain(), vec![9]);
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(42.0));
}
#[test]
fn async_kernels_read_captured_varargs() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let root = ContextImpl::root(&scope);
let payload = 21.5f64;
let link = VarArgLink {
args: VarArgSlots::Single(&payload),
outer: None,
};
let ctx = root.with_varargs(&link);
let runtime = Arc::new(MockRuntime::default());
let graph = extract::<f64, _>(SnapshotVarargNode::new(
lifted(()),
lifted(RuntimeHandle(runtime.clone())),
lifted(5u64),
&element_layout::<()>(),
&element_layout::<RuntimeHandle>(),
&element_layout::<u64>(),
));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
runtime.drain();
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(21.5));
}
#[test]
fn async_kernels_read_the_captured_context_snapshot() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let root = ContextImpl::root(&scope);
let footprint = Footprint::DEFAULT;
let ctx = root.with_footprint(&footprint);
let runtime = Arc::new(MockRuntime::default());
let graph = extract::<u32, _>(SnapshotResolutionNode::new(
lifted(()),
lifted(RuntimeHandle(runtime.clone())),
lifted(3u64),
&element_layout::<()>(),
&element_layout::<RuntimeHandle>(),
&element_layout::<u64>(),
));
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
runtime.drain();
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(Footprint::DEFAULT.resolution.x));
}
#[test]
fn the_epilogue_bumps_the_generation_and_sets_dirty() {
let runtime = GraphRuntime::new(CollectSpawner::default());
runtime.retain_sources(&[7]);
Runtime::spawn(&runtime, 7, Box::pin(yield_once()));
assert_eq!(runtime.snapshot(), vec![(7, 0)], "no bump before the future completes");
assert!(!runtime.take_dirty());
assert_eq!(runtime.spawner().drain(), 1);
assert_eq!(runtime.snapshot(), vec![(7, 1)]);
assert!(runtime.take_dirty());
assert!(!runtime.take_dirty(), "take_dirty drains the flag");
}
#[test]
fn the_epilogue_notifies_after_setting_dirty() {
let runtime = GraphRuntime::new(CollectSpawner::default());
runtime.retain_sources(&[7]);
let observed_dirty = Arc::new(AtomicBool::new(false));
let dirty_at_notify = Arc::clone(&runtime.dirty);
let observed = Arc::clone(&observed_dirty);
runtime.set_notifier(Arc::new(move || {
observed.store(dirty_at_notify.load(Ordering::Acquire), Ordering::Relaxed);
}));
Runtime::spawn(&runtime, 7, Box::pin(yield_once()));
assert_eq!(runtime.spawner().drain(), 1);
assert!(observed_dirty.load(Ordering::Relaxed), "the notifier must observe the dirty flag already set");
}
#[test]
fn the_epilogue_of_a_removed_source_does_not_notify() {
let runtime = GraphRuntime::new(CollectSpawner::default());
runtime.retain_sources(&[7]);
let notified = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&notified);
runtime.set_notifier(Arc::new(move || flag.store(true, Ordering::Relaxed)));
Runtime::spawn(&runtime, 7, Box::pin(yield_once()));
runtime.retain_sources(&[]);
assert_eq!(runtime.spawner().drain(), 1);
assert!(!notified.load(Ordering::Relaxed));
}
#[test]
fn the_epilogue_of_a_removed_source_is_inert() {
let runtime = GraphRuntime::new(CollectSpawner::default());
runtime.retain_sources(&[7]);
Runtime::spawn(&runtime, 7, Box::pin(yield_once()));
runtime.retain_sources(&[]);
assert_eq!(runtime.spawner().drain(), 1);
assert_eq!(runtime.snapshot(), Vec::<(SourceId, u64)>::new());
assert!(!runtime.take_dirty(), "a removed source must not invalidate");
}
#[test]
fn retain_sources_preserves_live_generations() {
let runtime = GraphRuntime::new(CollectSpawner::default());
runtime.retain_sources(&[7]);
Runtime::spawn(&runtime, 7, Box::pin(yield_once()));
runtime.spawner().drain();
runtime.retain_sources(&[7, 9]);
assert_eq!(runtime.snapshot(), vec![(7, 1), (9, 0)]);
runtime.retain_sources(&[9]);
assert_eq!(runtime.snapshot(), vec![(9, 0)]);
}
#[node_macro::node(category(""))]
async fn epilogue_double(_: impl Ctx, value: f64) -> f64 {
yield_once().await;
value * 2.
}
#[node_macro::node(category(""))]
async fn inline_double(_: impl Ctx, value: f64) -> f64 {
value * 2.
}
#[test]
fn an_immediately_ready_task_completes_inline_without_invalidating() {
let runtime = GraphRuntime::new(CollectSpawner::default());
runtime.retain_sources(&[7]);
assert!(Runtime::spawn(&runtime, 7, Box::pin(async {})));
assert_eq!(runtime.spawner().drain(), 0);
assert_eq!(runtime.snapshot(), vec![(7, 0)], "inline completion must not bump the generation");
assert!(!runtime.take_dirty());
}
#[test]
fn an_immediately_ready_kernel_returns_final_on_the_first_eval() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let runtime = Arc::new(GraphRuntime::new(CollectSpawner::default()));
runtime.retain_sources(&[13]);
let graph = extract::<f64, _>(InlineDoubleNode::new(
lifted(21.0f64),
lifted(RuntimeHandle(runtime.clone())),
lifted(13u64),
&element_layout::<f64>(),
&element_layout::<RuntimeHandle>(),
&element_layout::<u64>(),
));
let snapshot = runtime.snapshot();
let scope = EvalScope::new(None, None, None, &snapshot, &arena);
let ctx = ContextImpl::root(&scope);
assert_eq!(graph.eval(&ctx, &frames), GPoll::Final(42.0));
assert!(!runtime.take_dirty());
assert_eq!(runtime.snapshot(), vec![(13, 0)]);
assert_eq!(runtime.spawner().drain(), 0);
}
#[test]
fn a_source_slot_lands_through_the_runtime_while_downstream_keys_invalidate() {
let frames = crate::record::test_frames(1 << 16);
let arena = Arena::new(64).unwrap();
let runtime = Arc::new(GraphRuntime::new(CollectSpawner::default()));
runtime.retain_sources(&[11]);
let graph = extract::<f64, _>(EpilogueDoubleNode::new(
lifted(21.0f64),
lifted(RuntimeHandle(runtime.clone())),
lifted(11u64),
&element_layout::<f64>(),
&element_layout::<RuntimeHandle>(),
&element_layout::<u64>(),
));
let snapshot = runtime.snapshot();
let scope = EvalScope::new(None, None, None, &snapshot, &arena);
let ctx = ContextImpl::root(&scope);
assert_eq!(graph.eval(&ctx, &frames), GPoll::Pending);
assert!(!runtime.take_dirty());
assert_eq!(runtime.spawner().drain(), 1);
assert!(runtime.take_dirty());
let bumped = runtime.snapshot();
assert_eq!(bumped, vec![(11, 1)]);
let bumped_scope = EvalScope::new(None, None, None, &bumped, &arena);
let bumped_ctx = ContextImpl::root(&bumped_scope);
assert_eq!(graph.eval(&bumped_ctx, &frames), GPoll::Final(42.0), "the own-generation-excluded key replays the landed slot");
assert_eq!(runtime.spawner().drain(), 0, "a slot hit must not respawn");
let downstream_key = crate::registry::cache_key(&ContextImpl::root(&scope));
let bumped_downstream_key = crate::registry::cache_key(&ContextImpl::root(&bumped_scope));
assert_ne!(downstream_key, bumped_downstream_key, "unretained keys see the bump");
}
}
@@ -0,0 +1,256 @@
use crate::math::bbox::AxisAlignedBbox;
use core::f64;
use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2, UVec2};
/// Controls whether the Decompose Scale node returns axis-length magnitudes or pure scale factors.
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum ScaleType {
/// The visual length of each axis (always positive, includes any skew contribution).
#[default]
Magnitude,
/// The isolated scale factors with rotation and skew stripped away (can be negative for flipped axes).
Pure,
}
pub trait Transform {
fn transform(&self) -> DAffine2;
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
pivot
}
/// Decomposes the full transform into `(rotation, signed_scale, skew)` using a TRS+Skew factorization.
///
/// - `rotation`: angle in radians
/// - `signed_scale`: the algebraic scale factors (can be negative for reflections, excludes skew)
/// - `skew`: the horizontal shear coefficient (the raw matrix value, not an angle)
///
/// The original transform can be reconstructed as:
/// ```ignore
/// DAffine2::from_scale_angle_translation(scale, rotation, translation) * DAffine2::from_cols_array(&[1., 0., skew, 1., 0., 0.])
/// ```
#[inline(always)]
fn decompose_rotation_scale_skew(&self) -> (f64, DVec2, f64) {
let t = self.transform();
let x_axis = t.matrix2.x_axis;
let y_axis = t.matrix2.y_axis;
let angle = x_axis.y.atan2(x_axis.x);
let (sin, cos) = angle.sin_cos();
let scale_x = if cos.abs() > 1e-10 { x_axis.x / cos } else { x_axis.y / sin };
let mut skew = (sin * y_axis.y + cos * y_axis.x) / scale_x;
if !skew.is_finite() {
skew = 0.;
}
let scale_y = if cos.abs() > 1e-10 {
(y_axis.y - scale_x * sin * skew) / cos
} else {
(scale_x * cos * skew - y_axis.x) / sin
};
(angle, DVec2::new(scale_x, scale_y), skew)
}
/// Extracts the rotation angle (in radians) from the transform.
/// This is the angle of the x-axis and is correct regardless of skew, negative scale, or non-uniform scale.
fn decompose_rotation(&self) -> f64 {
let x_axis = self.transform().matrix2.x_axis;
let rotation = x_axis.y.atan2(x_axis.x);
if rotation == -0. { 0. } else { rotation }
}
/// Returns the signed scale components from the TRS+Skew decomposition.
/// Unlike [`Self::scale_magnitudes`] which returns positive axis-length magnitudes,
/// this returns the algebraic scale factors which can be negative for reflections and exclude skew.
fn decompose_scale(&self) -> DVec2 {
self.decompose_rotation_scale_skew().1
}
/// Returns the unsigned scale as the lengths of each axis (always positive, includes skew contribution).
/// Use this for magnitude-based queries like stroke width scaling, zoom level, or bounding box inflation.
fn scale_magnitudes(&self) -> DVec2 {
DVec2::new(self.transform().transform_vector2(DVec2::X).length(), self.transform().transform_vector2(DVec2::Y).length())
}
/// Returns the horizontal skew (shear) coefficient from the TRS+Skew decomposition.
/// This is the raw matrix coefficient. To convert to degrees: `skew.atan().to_degrees()`.
fn decompose_skew(&self) -> f64 {
self.decompose_rotation_scale_skew().2
}
/// Detects if the transform contains skew by checking if the transformation matrix
/// deviates from a pure rotation + uniform scale + translation.
///
/// Returns true if the matrix columns are not orthogonal or have different lengths,
/// indicating the presence of skew or non-uniform scaling.
fn has_skew(&self) -> bool {
let mat2 = self.transform().matrix2;
let col0 = mat2.x_axis;
let col1 = mat2.y_axis;
const EPSILON: f64 = 1e-10;
// Check if columns are orthogonal (dot product should be ~0) and equal length
// Non-orthogonal columns or different lengths indicate skew/non-uniform scaling
col0.dot(col1).abs() > EPSILON || (col0.length() - col1.length()).abs() > EPSILON
}
}
pub trait TransformMut: Transform {
fn transform_mut(&mut self) -> &mut DAffine2;
fn translate(&mut self, offset: DVec2) {
*self.transform_mut() = DAffine2::from_translation(offset) * self.transform();
}
}
// Implementation for references to anything that implements Transform
impl<T: Transform> Transform for &T {
fn transform(&self) -> DAffine2 {
(*self).transform()
}
}
// Implementations for DAffine2
impl Transform for DAffine2 {
fn transform(&self) -> DAffine2 {
*self
}
}
impl TransformMut for DAffine2 {
fn transform_mut(&mut self) -> &mut DAffine2 {
self
}
}
// Implementations for Footprint
impl Transform for Footprint {
fn transform(&self) -> DAffine2 {
self.transform
}
}
impl TransformMut for Footprint {
fn transform_mut(&mut self) -> &mut DAffine2 {
&mut self.transform
}
}
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RenderQuality {
/// Low quality, fast rendering
Preview,
/// Ensure that the render is available with at least the specified quality
/// A value of 0.5 means that the render is available with at least 50% of the final image resolution
Scale(f32),
/// Flip a coin to decide if the render should be available with the current quality or done at full quality
/// This should be used to gradually update the render quality of a cached node
Probability(f32),
/// Render at full quality
Full,
}
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Footprint {
/// Inverse of the transform which will be applied to the node output during the rendering process
pub transform: DAffine2,
/// Resolution of the target output area in pixels
pub resolution: UVec2,
/// Quality of the render, this may be used by caching nodes to decide if the cached render is sufficient
pub quality: RenderQuality,
}
impl Default for Footprint {
fn default() -> Self {
Self::DEFAULT
}
}
impl Footprint {
pub const DEFAULT: Self = Self {
transform: DAffine2::IDENTITY,
resolution: UVec2::ONE,
quality: RenderQuality::Full,
};
pub const BOUNDLESS: Self = Self {
transform: DAffine2 {
matrix2: DMat2::from_diagonal(DVec2::splat(f64::INFINITY)),
translation: DVec2::ZERO,
},
resolution: UVec2::ZERO,
quality: RenderQuality::Full,
};
pub fn viewport_bounds_in_local_space(&self) -> AxisAlignedBbox {
let inverse = self.transform.inverse();
let res = self.resolution.as_dvec2();
let c0 = inverse.transform_point2(DVec2::ZERO);
let c1 = inverse.transform_point2(DVec2::new(res.x, 0.));
let c2 = inverse.transform_point2(res);
let c3 = inverse.transform_point2(DVec2::new(0., res.y));
AxisAlignedBbox {
start: c0.min(c1).min(c2).min(c3),
end: c0.max(c1).max(c2).max(c3),
}
}
pub fn scale(&self) -> DVec2 {
self.transform.scale_magnitudes()
}
pub fn offset(&self) -> DVec2 {
self.transform.transform_point2(DVec2::ZERO)
}
}
impl From<()> for Footprint {
fn from(_: ()) -> Self {
Footprint::default()
}
}
/// Consumes an item's `transform` attribute by baking it into the underlying value itself.
pub trait BakeTransform {
fn bake_transform(&mut self, transform: &DAffine2);
}
impl BakeTransform for DAffine2 {
fn bake_transform(&mut self, transform: &DAffine2) {
*self = *transform * *self;
}
}
impl BakeTransform for DVec2 {
fn bake_transform(&mut self, transform: &DAffine2) {
*self = transform.transform_point2(*self);
}
}
pub trait ApplyTransform {
fn apply_transform(&mut self, modification: &DAffine2);
fn left_apply_transform(&mut self, modification: &DAffine2);
}
impl<T: TransformMut> ApplyTransform for T {
fn apply_transform(&mut self, &modification: &DAffine2) {
*self.transform_mut() = self.transform() * modification
}
fn left_apply_transform(&mut self, &modification: &DAffine2) {
*self.transform_mut() = modification * self.transform()
}
}
impl ApplyTransform for DVec2 {
fn apply_transform(&mut self, modification: &DAffine2) {
*self = modification.transform_point2(*self);
}
fn left_apply_transform(&mut self, modification: &DAffine2) {
*self = modification.transform_point2(*self);
}
}
@@ -0,0 +1,621 @@
use std::any::TypeId;
pub use std::borrow::Cow;
use std::fmt::{Display, Formatter};
#[macro_export]
macro_rules! concrete {
($type:ty) => {
$crate::Type::Concrete($crate::descriptor!($type))
};
($type:ty, $name:ty) => {
$crate::Type::Concrete($crate::descriptor!($type, $name))
};
}
#[macro_export]
macro_rules! descriptor {
($type:ty) => {
$crate::TypeDescriptor {
id: Some(std::any::TypeId::of::<$type>()),
name: $crate::Cow::Borrowed(std::any::type_name::<$type>()),
alias: None,
size: std::mem::size_of::<$type>(),
align: std::mem::align_of::<$type>(),
}
};
($type:ty, $name:ty) => {
$crate::TypeDescriptor {
id: Some(std::any::TypeId::of::<$type>()),
name: $crate::Cow::Borrowed(std::any::type_name::<$type>()),
alias: Some($crate::Cow::Borrowed(stringify!($name))),
size: std::mem::size_of::<$type>(),
align: std::mem::align_of::<$type>(),
}
};
}
#[macro_export]
macro_rules! concrete_with_name {
($type:ty, $name:expr_2021) => {
$crate::Type::Concrete($crate::TypeDescriptor {
id: Some(std::any::TypeId::of::<$type>()),
name: $crate::Cow::Borrowed($name),
alias: None,
size: std::mem::size_of::<$type>(),
align: std::mem::align_of::<$type>(),
})
};
}
#[macro_export]
macro_rules! generic {
($type:ty) => {{ $crate::Type::Generic($crate::Cow::Borrowed(stringify!($type))) }};
}
/// Constructs the [`Type`] of an `Item` holding the given element type, e.g. `item!(f64)` is the type of an `Item<f64>`.
/// The two-argument form tags the element descriptor with an alias, preserving the source spelling for widget dispatch.
#[macro_export]
macro_rules! item {
(Item<$inner:ty>) => {
$crate::Type::Item(Box::new($crate::item!($inner)))
};
($element:ty) => {
$crate::Type::Item(Box::new($crate::concrete!($element)))
};
($element:ty, $alias:ty) => {
$crate::Type::Item(Box::new($crate::concrete!($element, $alias)))
};
}
/// Constructs the [`Type`] of a `List` holding the given element type, e.g. `list!(f64)` is the type of a `List<f64>`.
#[macro_export]
macro_rules! list {
(List<$inner:ty>) => {
$crate::Type::List(Box::new($crate::list!($inner)))
};
($element:ty) => {
$crate::Type::List(Box::new($crate::concrete!($element)))
};
}
// The `List<...>`/`Item<...>` rules must appear before the generic `$type:ty` rules, and in each macro that sees the literal tokens,
// because a type captured as `ty` becomes opaque to any inner macro's ranked pattern
#[macro_export]
macro_rules! future {
(List<$inner:ty>) => {
$crate::Type::Future(Box::new($crate::list!($inner)))
};
(List<$inner:ty>, $name:ty) => {
$crate::Type::Future(Box::new($crate::list!($inner)))
};
(Item<$inner:ty>) => {
$crate::Type::Future(Box::new($crate::item!($inner)))
};
(Item<$inner:ty>, $name:ty) => {
$crate::Type::Future(Box::new($crate::item!($inner, $name)))
};
($type:ty) => {{ $crate::Type::Future(Box::new(concrete!($type))) }};
($type:ty, $name:ty) => {
$crate::Type::Future(Box::new(concrete!($type, $name)))
};
}
#[macro_export]
macro_rules! fn_type {
(List<$inner:ty>) => {
$crate::Type::Fn(Box::new(concrete!(())), Box::new($crate::list!($inner)))
};
(Item<$inner:ty>) => {
$crate::Type::Fn(Box::new(concrete!(())), Box::new($crate::item!($inner)))
};
($type:ty) => {
$crate::Type::Fn(Box::new(concrete!(())), Box::new(concrete!($type)))
};
($in_type:ty, List<$inner:ty>, alias: $outname:ty) => {
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::list!($inner)))
};
($in_type:ty, List<$inner:ty>) => {
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::list!($inner)))
};
($in_type:ty, Item<$inner:ty>, alias: $outname:ty) => {
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::item!($inner, $inner)))
};
($in_type:ty, Item<$inner:ty>) => {
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::item!($inner)))
};
($in_type:ty, $type:ty, alias: $outname:ty) => {
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(concrete!($type, $outname)))
};
($in_type:ty, $type:ty) => {
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(concrete!($type)))
};
}
#[macro_export]
macro_rules! fn_type_fut {
(List<$inner:ty>) => {
$crate::Type::Fn(Box::new(concrete!(())), Box::new($crate::Type::Future(Box::new($crate::list!($inner)))))
};
(Item<$inner:ty>) => {
$crate::Type::Fn(Box::new(concrete!(())), Box::new($crate::Type::Future(Box::new($crate::item!($inner)))))
};
($type:ty) => {
$crate::Type::Fn(Box::new(concrete!(())), Box::new(future!($type)))
};
($in_type:ty, List<$inner:ty>, alias: $outname:ty) => {
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::Type::Future(Box::new($crate::list!($inner)))))
};
($in_type:ty, List<$inner:ty>) => {
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::Type::Future(Box::new($crate::list!($inner)))))
};
($in_type:ty, Item<$inner:ty>, alias: $outname:ty) => {
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::Type::Future(Box::new($crate::item!($inner, $inner)))))
};
($in_type:ty, Item<$inner:ty>) => {
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::Type::Future(Box::new($crate::item!($inner)))))
};
($in_type:ty, $type:ty, alias: $outname:ty) => {
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(future!($type, $outname)))
};
($in_type:ty, $type:ty) => {
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(future!($type)))
};
}
// TODO: Rename to NodeSignatureMonomorphization
#[derive(Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NodeIOTypes {
pub call_argument: Type,
pub return_value: Type,
pub inputs: Vec<Type>,
}
impl NodeIOTypes {
pub const fn new(call_argument: Type, return_value: Type, inputs: Vec<Type>) -> Self {
Self { call_argument, return_value, inputs }
}
/// Applies [`Type::normalize_rank`] to every type in the signature.
pub fn normalize_rank(self) -> Self {
Self {
call_argument: self.call_argument.normalize_rank(),
return_value: self.return_value.normalize_rank(),
inputs: self.inputs.into_iter().map(Type::normalize_rank).collect(),
}
}
pub const fn empty() -> Self {
let tds1 = TypeDescriptor {
id: None,
name: Cow::Borrowed("()"),
alias: None,
size: 0,
align: 0,
};
let tds2 = TypeDescriptor {
id: None,
name: Cow::Borrowed("()"),
alias: None,
size: 0,
align: 0,
};
Self {
call_argument: Type::Concrete(tds1),
return_value: Type::Concrete(tds2),
inputs: Vec::new(),
}
}
pub fn ty(&self) -> Type {
Type::Fn(Box::new(self.call_argument.clone()), Box::new(self.return_value.clone()))
}
}
impl std::fmt::Debug for NodeIOTypes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let inputs = self.inputs.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ");
let return_value = &self.return_value;
let call_argument = &self.call_argument;
f.write_fmt(format_args!("({inputs}) → {return_value} called with {call_argument}"))
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ProtoNodeIdentifier {
name: Cow<'static, str>,
}
impl ProtoNodeIdentifier {
pub const fn new(name: &'static str) -> Self {
ProtoNodeIdentifier { name: Cow::Borrowed(name) }
}
pub const fn with_owned_string(name: String) -> Self {
ProtoNodeIdentifier { name: Cow::Owned(name) }
}
pub fn as_str(&self) -> &str {
self.name.as_ref()
}
pub const fn as_static_str(&self) -> &'static str {
match self.name {
Cow::Borrowed(name) => name,
Cow::Owned(_) => panic!("`as_static_str` called on a `ProtoNodeIdentifier` backed by an owned string"),
}
}
}
impl From<ProtoNodeIdentifier> for Cow<'static, str> {
fn from(val: ProtoNodeIdentifier) -> Self {
val.name
}
}
impl Display for ProtoNodeIdentifier {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("ProtoNodeIdentifier").field(&self.name).finish()
}
}
#[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>,
pub name: Cow<'static, str>,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
pub alias: Option<Cow<'static, str>>,
#[cfg_attr(feature = "serde", serde(skip))]
pub size: usize,
#[cfg_attr(feature = "serde", serde(skip))]
pub align: usize,
}
impl std::hash::Hash for TypeDescriptor {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl graphene_hash::CacheHash for TypeDescriptor {
fn cache_hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
graphene_hash::CacheHash::cache_hash(&self.name, state);
}
}
impl std::fmt::Display for TypeDescriptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let text = make_type_user_readable(&simplify_identifier_name(&self.name));
write!(f, "{text}")
}
}
impl PartialEq for TypeDescriptor {
fn eq(&self, other: &Self) -> bool {
match (self.id, other.id) {
(Some(id), Some(other_id)) => id == other_id,
_ => {
// TODO: Add a flag to disable this warning
// warn!("TypeDescriptor::eq: comparing types without ids based on name");
self.name == other.name
}
}
}
}
/// Graph runtime type information used for type inference.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Type {
/// A wrapper for some type variable used within the inference system. Resolved at inference time and replaced with a concrete type.
Generic(Cow<'static, str>),
/// A wrapper around the Rust type id for any concrete Rust type. Allows us to do equality comparisons, like checking if a String == a String.
Concrete(TypeDescriptor),
/// Runtime type information for a function. Given some input, gives some output.
Fn(Box<Type>, Box<Type>),
/// Represents a future which promises to return the inner type.
Future(Box<Type>),
/// Represents a recursive [Type] allowing nested levels of types to represent the type of an Item<T>.
Item(Box<Type>),
/// Represents a list of this recursive [Type] allowing nested levels of types to represent the type of a List<T>.
List(Box<Type>),
}
impl Default for Type {
fn default() -> Self {
concrete!(())
}
}
unsafe impl dyn_any::StaticType for Type {
type Static = Self;
}
impl Type {
pub fn is_generic(&self) -> bool {
matches!(self, Type::Generic(_))
}
pub fn is_concrete(&self) -> bool {
matches!(self, Type::Concrete(_))
}
pub fn is_fn(&self) -> bool {
matches!(self, Type::Fn(_, _))
}
pub fn is_value(&self) -> bool {
matches!(self, Type::Fn(_, _) | Type::Concrete(_))
}
pub fn is_unit(&self) -> bool {
matches!(self, Type::Fn(_, _) | Type::Concrete(_))
}
pub fn is_generic_or_fn(&self) -> bool {
matches!(self, Type::Fn(_, _) | Type::Generic(_))
}
pub fn fn_input(&self) -> Option<&Type> {
match self {
Type::Fn(first, _) => Some(first),
_ => None,
}
}
pub fn fn_output(&self) -> Option<&Type> {
match self {
Type::Fn(_, second) => Some(second),
_ => None,
}
}
pub fn function(input: &Type, output: &Type) -> Type {
Type::Fn(Box::new(input.clone()), Box::new(output.clone()))
}
}
impl Type {
pub fn new<T: dyn_any::StaticType + Sized>() -> Self {
Self::Concrete(TypeDescriptor {
id: Some(TypeId::of::<T::Static>()),
name: Cow::Borrowed(std::any::type_name::<T::Static>()),
alias: None,
size: size_of::<T>(),
align: align_of::<T>(),
})
}
pub fn size(&self) -> Option<usize> {
match self {
Self::Generic(_) => None,
Self::Concrete(ty) => Some(ty.size),
Self::Fn(_, _) => None,
Self::Future(_) => None,
Self::Item(_) => None,
Self::List(_) => None,
}
}
pub fn align(&self) -> Option<usize> {
match self {
Self::Generic(_) => None,
Self::Concrete(ty) => Some(ty.align),
Self::Fn(_, _) => None,
Self::Future(_) => None,
Self::Item(_) => None,
Self::List(_) => None,
}
}
pub fn nested_type(&self) -> &Type {
match self {
Self::Generic(_) => self,
Self::Concrete(_) => self,
Self::Fn(_, output) => output.nested_type(),
Self::Future(output) => output.nested_type(),
Self::Item(_) => self,
Self::List(_) => self,
}
}
pub fn replace_nested(&mut self, f: impl Fn(&Type) -> Option<Type>) -> Option<Type> {
if let Some(replacement) = f(self) {
return Some(std::mem::replace(self, replacement));
}
match self {
Self::Generic(_) => None,
Self::Concrete(_) => None,
Self::Fn(_, output) => output.replace_nested(f),
Self::Future(output) => output.replace_nested(f),
Self::Item(_) => None,
Self::List(_) => None,
}
}
pub fn identifier_name(&self) -> String {
match self {
Type::Generic(name) => name.to_string(),
Type::Concrete(ty) => simplify_identifier_name(&ty.name),
Type::Fn(call_arg, return_value) => format!("{} called with {}", return_value.identifier_name(), call_arg.identifier_name()),
Type::Future(ty) => ty.identifier_name(),
Type::Item(element) => element.identifier_name(),
Type::List(element) => format!("{}[]", element.identifier_name()),
}
}
/// Constructs the [`Type`] of a `List` holding elements of the given type, the expression-position counterpart of [`list!`].
pub fn list_of(element: Type) -> Type {
Type::List(Box::new(element))
}
/// The element type if this is a rank-1 `List` wire type.
pub fn list_element(&self) -> Option<&Type> {
match self {
Type::List(element) => Some(element),
_ => None,
}
}
/// The element name if this is the type of an `Item<Bundle<X>>` cell carrying a whole list, e.g. `f64` from `Item<Bundle<f64>>`.
/// The `Bundle` layer stays name-encoded inside the structural `Item` since it has no structural variant.
pub fn bundle_element_name(&self) -> Option<&str> {
let Type::Item(element) = self else { return None };
let Type::Concrete(descriptor) = element.as_ref() else { return None };
descriptor.name.strip_prefix("core_types::list::Bundle<")?.strip_suffix('>')
}
/// Converts a name-encoded `List` or `Item` concrete type into its structural form, recursively.
/// Structurally-built types pass through unchanged, so sources which cannot construct ranked types
/// (reflection and opaque macro captures) converge with macro-built ones at this single point.
pub fn normalize_rank(self) -> Type {
fn parse_element(element_name: &str) -> Type {
let element = Type::Concrete(TypeDescriptor {
id: None,
name: Cow::Owned(element_name.to_string()),
alias: None,
size: 0,
align: 0,
});
element.normalize_rank()
}
match self {
Type::Concrete(descriptor) => {
if let Some(element_name) = descriptor.name.strip_prefix("core_types::list::List<").and_then(|rest| rest.strip_suffix('>')) {
return Type::List(Box::new(parse_element(element_name)));
}
if let Some(element_name) = descriptor.name.strip_prefix("core_types::list::Item<").and_then(|rest| rest.strip_suffix('>')) {
return Type::Item(Box::new(parse_element(element_name)));
}
Type::Concrete(descriptor)
}
Type::Fn(input, output) => Type::Fn(Box::new(input.normalize_rank()), Box::new(output.normalize_rank())),
Type::Future(inner) => Type::Future(Box::new(inner.normalize_rank())),
Type::Item(element) => Type::Item(Box::new(element.normalize_rank())),
Type::List(element) => Type::List(Box::new(element.normalize_rank())),
Type::Generic(_) => self,
}
}
}
pub fn simplify_identifier_name(ty: &str) -> String {
ty.split('<')
.map(|path| path.split(',').map(|path| path.split("::").last().unwrap_or(path)).collect::<Vec<_>>().join(","))
.collect::<Vec<_>>()
.join("<")
}
/// Converts a Rust-internal type name to its user-facing form.
pub fn make_type_user_readable(ty: &str) -> String {
let ty = ty
.replace("Option<Arc<OwnedContextImpl>>", "Context")
.replace("Raster<CPU>", "Raster")
.replace("Raster<GPU>", "Raster")
.replace("DAffine2", "Transform")
.replace("Affine2", "Transform")
.replace("DVec2", "Vec2")
.replace("IVec2", "Vec2")
.replace("UVec2", "Vec2")
.replace("&str", "String");
rewrite_ranked_type_wrappers(&ty)
}
/// Rewrites `List<T>` and the whole-collection `Bundle<T>` as `T[]`, and unwraps `Item<T>` to `T`, so ranked wires read as their element type.
/// Handles nesting (e.g. `List<List<Vector>>` becomes `Vector[][]`).
/// Respects word boundaries so unrelated identifiers that happen to end in `List` or `Item` are not affected.
fn rewrite_ranked_type_wrappers(input: &str) -> String {
let bytes = input.as_bytes();
let mut result = String::with_capacity(input.len());
let mut i = 0;
while i < bytes.len() {
let at_word_boundary = i == 0 || !is_identifier_byte(bytes[i - 1]);
if at_word_boundary && bytes[i..].starts_with(b"List<") {
let inner_start = i + b"List<".len();
if let Some(close) = find_matching_angle_bracket(bytes, inner_start) {
let inner = &input[inner_start..close];
result.push_str(&rewrite_ranked_type_wrappers(inner));
result.push_str("[]");
i = close + 1;
continue;
}
}
if at_word_boundary && bytes[i..].starts_with(b"Bundle<") {
let inner_start = i + b"Bundle<".len();
if let Some(close) = find_matching_angle_bracket(bytes, inner_start) {
let inner = &input[inner_start..close];
result.push_str(&rewrite_ranked_type_wrappers(inner));
result.push_str("[]");
i = close + 1;
continue;
}
}
if at_word_boundary && bytes[i..].starts_with(b"Item<") {
let inner_start = i + b"Item<".len();
if let Some(close) = find_matching_angle_bracket(bytes, inner_start) {
let inner = &input[inner_start..close];
result.push_str(&rewrite_ranked_type_wrappers(inner));
i = close + 1;
continue;
}
}
if bytes[i].is_ascii() {
result.push(bytes[i] as char);
i += 1;
} else {
let ch = input[i..].chars().next().unwrap();
result.push(ch);
i += ch.len_utf8();
}
}
result
}
fn is_identifier_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_'
}
fn find_matching_angle_bracket(bytes: &[u8], start: usize) -> Option<usize> {
let mut depth = 1_usize;
for (offset, &byte) in bytes[start..].iter().enumerate() {
match byte {
b'<' => depth += 1,
b'>' => {
depth -= 1;
if depth == 0 {
return Some(start + offset);
}
}
_ => {}
}
}
None
}
impl std::fmt::Debug for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self}")
}
}
// Display
impl std::fmt::Display for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Type::Generic(name) => write!(f, "{}", make_type_user_readable(name)),
Type::Concrete(ty) => write!(f, "{ty}"),
Type::Fn(_, return_value) => write!(f, "{return_value}"),
Type::Future(ty) => write!(f, "{ty}"),
Type::Item(element) => write!(f, "{element}"),
Type::List(element) => write!(f, "{element}[]"),
}
}
}
@@ -0,0 +1,86 @@
use dyn_any::DynAny;
pub use uuid_generation::*;
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Uuid(#[serde(with = "u64_string")] u64);
mod u64_string {
use serde::{self, Deserialize, Deserializer, Serializer};
use std::str::FromStr;
// The signature of a serialize_with function must follow the pattern:
//
// fn serialize<S>(&T, S) -> Result<S::Ok, S::Error>
// where
// S: Serializer
//
// although it may also be generic over the input types T.
pub fn serialize<S>(value: &u64, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&value.to_string())
}
// The signature of a deserialize_with function must follow the pattern:
//
// fn deserialize<'de, D>(D) -> Result<T, D::Error>
// where
// D: Deserializer<'de>
//
// although it may also be generic over the output types T.
pub fn deserialize<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
u64::from_str(&s).map_err(serde::de::Error::custom)
}
}
mod uuid_generation {
use rand_chacha::ChaCha20Rng;
use rand_chacha::rand_core::{RngCore, SeedableRng};
use std::cell::Cell;
use std::sync::Mutex;
static RNG: Mutex<Option<ChaCha20Rng>> = Mutex::new(None);
thread_local! {
pub static UUID_SEED: Cell<Option<u64>> = const { Cell::new(None) };
}
pub fn set_uuid_seed(random_seed: u64) {
UUID_SEED.with(|seed| seed.set(Some(random_seed)))
}
pub fn generate_uuid() -> u64 {
let Ok(mut lock) = RNG.lock() else { panic!("UUID mutex poisoned") };
if lock.is_none() {
UUID_SEED.with(|seed| {
let random_seed = seed.get().unwrap_or(42);
*lock = Some(ChaCha20Rng::seed_from_u64(random_seed));
})
}
lock.as_mut().map(ChaCha20Rng::next_u64).expect("UUID mutex poisoned")
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, PartialOrd, Ord, DynAny)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NodeId(pub u64);
impl NodeId {
pub fn new() -> Self {
Self(generate_uuid())
}
}
impl std::fmt::Display for NodeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
@@ -0,0 +1,233 @@
use crate::list::Item;
use crate::registry::DynFuture;
use crate::{Node, WasmNotSend};
use std::cell::{Cell, RefCell, RefMut};
use std::marker::PhantomData;
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct IntNode<const N: u32>;
impl<'i, const N: u32, I> Node<'i, I> for IntNode<N> {
type Output = u32;
#[inline(always)]
fn eval(&'i self, _input: I) -> Self::Output {
N
}
}
#[derive(Default, Debug, Clone, Copy)]
pub struct ValueNode<T>(pub T);
impl<'i, T: 'i, I> Node<'i, I> for ValueNode<T> {
type Output = &'i T;
#[inline(always)]
fn eval(&'i self, _input: I) -> Self::Output {
&self.0
}
}
impl<T> ValueNode<T> {
pub const fn new(value: T) -> ValueNode<T> {
ValueNode(value)
}
}
impl<T> From<T> for ValueNode<T> {
fn from(value: T) -> Self {
ValueNode::new(value)
}
}
#[derive(Default, Debug, Clone, Copy)]
pub struct AsRefNode<T: AsRef<U>, U>(pub T, PhantomData<U>);
impl<'i, T: 'i + AsRef<U>, U: 'i> Node<'i, ()> for AsRefNode<T, U> {
type Output = &'i U;
#[inline(always)]
fn eval(&'i self, _input: ()) -> Self::Output {
self.0.as_ref()
}
}
impl<T: AsRef<U>, U> AsRefNode<T, U> {
pub const fn new(value: T) -> AsRefNode<T, U> {
AsRefNode(value, PhantomData)
}
}
#[derive(Default, Debug, Clone)]
pub struct RefCellMutNode<T>(pub RefCell<T>);
impl<'i, T: 'i> Node<'i, ()> for RefCellMutNode<T> {
type Output = RefMut<'i, T>;
#[inline(always)]
fn eval(&'i self, _input: ()) -> Self::Output {
self.0.borrow_mut()
}
}
impl<T> RefCellMutNode<T> {
pub const fn new(value: T) -> RefCellMutNode<T> {
RefCellMutNode(RefCell::new(value))
}
}
#[derive(Default)]
pub struct OnceCellNode<T>(pub Cell<T>);
impl<'i, T: Default + 'i, I> Node<'i, I> for OnceCellNode<T> {
type Output = T;
#[inline(always)]
fn eval(&'i self, _input: I) -> Self::Output {
self.0.replace(T::default())
}
}
impl<T> OnceCellNode<T> {
pub const fn new(value: T) -> OnceCellNode<T> {
OnceCellNode(Cell::new(value))
}
}
#[derive(Clone, Copy)]
pub struct ClonedNode<T: Clone>(pub T);
impl<'i, T: Clone + 'i, I> Node<'i, I> for ClonedNode<T> {
type Output = T;
#[inline(always)]
fn eval(&'i self, _input: I) -> Self::Output {
self.0.clone()
}
}
impl<T: Clone> ClonedNode<T> {
pub const fn new(value: T) -> ClonedNode<T> {
ClonedNode(value)
}
}
impl<T: Clone> From<T> for ClonedNode<T> {
fn from(value: T) -> Self {
ClonedNode::new(value)
}
}
/// Yields a precomputed `Item<T>` as a ready future, ignoring its input context.
/// Generated list-content variants feed each already-evaluated content slot through this so the kernel's own
/// context modifications become no-ops (the slot was evaluated once, up front, at the ambient footprint).
pub struct PrecomputedItemNode<T>(pub Item<T>);
impl<'i, T: Clone + WasmNotSend + 'i, I: 'i> Node<'i, I> for PrecomputedItemNode<T> {
type Output = DynFuture<'i, Item<T>>;
#[inline(always)]
fn eval(&'i self, _input: I) -> Self::Output {
let item = self.0.clone();
Box::pin(async move { item })
}
}
impl<T> PrecomputedItemNode<T> {
pub const fn new(item: Item<T>) -> Self {
Self(item)
}
}
#[derive(Clone, Copy)]
/// The DebugClonedNode logs every time it is evaluated.
/// This is useful for debugging.
pub struct DebugClonedNode<T: Clone>(pub T);
impl<'i, T: Clone + 'i> Node<'i, ()> for DebugClonedNode<T> {
type Output = T;
#[inline(always)]
fn eval(&'i self, _input: ()) -> Self::Output {
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
log::debug!("DebugClonedNode::eval");
self.0.clone()
}
}
impl<T: Clone> DebugClonedNode<T> {
pub const fn new(value: T) -> DebugClonedNode<T> {
DebugClonedNode(value)
}
}
#[derive(Clone, Copy)]
pub struct CopiedNode<T: Copy>(pub T);
impl<'i, T: Copy + 'i, I> Node<'i, I> for CopiedNode<T> {
type Output = T;
#[inline(always)]
fn eval(&'i self, _input: I) -> Self::Output {
self.0
}
}
impl<T: Copy> CopiedNode<T> {
pub const fn new(value: T) -> CopiedNode<T> {
CopiedNode(value)
}
}
#[derive(Default)]
pub struct DefaultNode<T>(PhantomData<T>);
impl<'i, T: Default + 'i, I> Node<'i, I> for DefaultNode<T> {
type Output = T;
fn eval(&'i self, _input: I) -> Self::Output {
T::default()
}
}
impl<T> DefaultNode<T> {
pub fn new() -> Self {
Self(PhantomData)
}
}
#[repr(C)]
/// Return the unit value
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct ForgetNode;
impl<'i, T: 'i> Node<'i, T> for ForgetNode {
type Output = ();
fn eval(&'i self, _input: T) -> Self::Output {}
}
impl ForgetNode {
pub const fn new() -> Self {
ForgetNode
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_int_node() {
let node = IntNode::<5>;
assert_eq!(node.eval(()), 5);
}
#[test]
fn test_value_node() {
let node = ValueNode::new(5);
assert_eq!(node.eval(()), &5);
let type_erased = &node as &dyn for<'a> Node<'a, (), Output = &'a i32>;
assert_eq!(type_erased.eval(()), &5);
}
#[test]
fn test_default_node() {
let node = DefaultNode::<u32>::new();
assert_eq!(node.eval(42), 0);
}
#[test]
#[allow(clippy::unit_cmp)]
fn test_unit_node() {
let node = ForgetNode::new();
assert_eq!(node.eval(()), ());
}
}
@@ -0,0 +1,17 @@
[package]
name = "graphene-hash"
version = "0.0.0"
edition = "2024"
authors = ["Graphite Authors <contact@graphite.art>"]
description = "CacheHash trait and derive macro for cache invalidation hashing in Graphite"
license = "MIT OR Apache-2.0"
publish = false
[features]
default = ["std"]
std = []
derive = ["graphene-hash-derive"]
[dependencies]
graphene-hash-derive = { path = "derive", optional = true }
glam = { workspace = true }
@@ -0,0 +1,19 @@
[package]
name = "graphene-hash-derive"
version = "0.0.0"
edition = "2024"
authors = ["Graphite Authors <contact@graphite.art>"]
description = "#[derive(CacheHash)]"
license = "MIT OR Apache-2.0"
publish = false
[lib]
proc-macro = true
[dependencies]
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true }
[dev-dependencies]
graphene-hash = { path = "..", features = ["derive"] }
@@ -0,0 +1,129 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{Data, DeriveInput, Fields, parse_macro_input};
/// Derives `CacheHash` for a struct or enum.
///
/// All fields must implement `CacheHash`. Fields annotated with `#[cache_hash(skip)]`
/// are excluded from hashing.
///
/// # Example
///
/// ```
/// # use graphene_hash::CacheHash;
/// #[derive(CacheHash)]
/// pub struct MyNode {
/// pub value: f64,
/// pub count: u32,
/// #[cache_hash(skip)]
/// pub debug_label: String,
/// }
/// ```
#[proc_macro_derive(CacheHash, attributes(cache_hash))]
pub fn derive_cache_hash(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let mut generics = ast.generics.clone();
for param in &mut generics.params {
if let syn::GenericParam::Type(type_param) = param {
type_param.bounds.push(syn::parse_quote!(graphene_hash::CacheHash));
}
}
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let body = match &ast.data {
Data::Struct(s) => hash_fields(&s.fields, quote! { self }),
Data::Enum(e) => {
let arms = e.variants.iter().map(|variant| {
let variant_name = &variant.ident;
let (pattern, hash_body) = match &variant.fields {
Fields::Unit => (quote! {}, quote! {}),
Fields::Unnamed(fields) => {
let bindings: Vec<_> = (0..fields.unnamed.len())
.map(|i| {
let ident = proc_macro2::Ident::new(&format!("f{i}"), proc_macro2::Span::call_site());
quote! { #ident }
})
.collect();
let hash_stmts = fields.unnamed.iter().enumerate().filter_map(|(i, field)| {
if has_skip_attr(&field.attrs) {
return None;
}
let ident = proc_macro2::Ident::new(&format!("f{i}"), proc_macro2::Span::call_site());
Some(quote! { graphene_hash::CacheHash::cache_hash(#ident, state); })
});
(quote! { (#(#bindings,)*) }, quote! { #(#hash_stmts)* })
}
Fields::Named(fields) => {
let names: Vec<_> = fields.named.iter().map(|f| f.ident.as_ref().unwrap()).collect();
let hash_stmts = fields.named.iter().filter_map(|field| {
if has_skip_attr(&field.attrs) {
return None;
}
let ident = field.ident.as_ref().unwrap();
Some(quote! { graphene_hash::CacheHash::cache_hash(#ident, state); })
});
(quote! { { #(#names,)* } }, quote! { #(#hash_stmts)* })
}
};
quote! {
Self::#variant_name #pattern => { #hash_body }
}
});
quote! {
::core::hash::Hash::hash(&::core::mem::discriminant(self), state);
match self {
#(#arms)*
}
}
}
Data::Union(_) => return syn::Error::new(ast.ident.span(), "CacheHash cannot be derived for unions").to_compile_error().into(),
};
quote! {
impl #impl_generics graphene_hash::CacheHash for #name #ty_generics #where_clause {
fn cache_hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
#body
}
}
}
.into()
}
fn hash_fields(fields: &Fields, self_expr: TokenStream2) -> TokenStream2 {
match fields {
Fields::Unit => quote! {},
Fields::Unnamed(fields) => {
let stmts = fields.unnamed.iter().enumerate().filter_map(|(i, field)| {
if has_skip_attr(&field.attrs) {
return None;
}
let index = syn::Index::from(i);
Some(quote! { graphene_hash::CacheHash::cache_hash(&#self_expr.#index, state); })
});
quote! { #(#stmts)* }
}
Fields::Named(fields) => {
let stmts = fields.named.iter().filter_map(|field| {
if has_skip_attr(&field.attrs) {
return None;
}
let ident = field.ident.as_ref().unwrap();
Some(quote! { graphene_hash::CacheHash::cache_hash(&#self_expr.#ident, state); })
});
quote! { #(#stmts)* }
}
}
}
fn has_skip_attr(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|attr| {
if !attr.path().is_ident("cache_hash") {
return false;
}
attr.parse_args::<syn::Ident>().map(|id| id == "skip").unwrap_or(false)
})
}
@@ -0,0 +1,377 @@
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "std")]
extern crate std;
#[cfg(feature = "derive")]
pub use graphene_hash_derive::CacheHash;
pub trait CacheHash {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H);
}
/// Wrapper that implements `std::hash::Hash` by delegating to `CacheHash`.
///
/// Use this to store `CacheHash` types in `HashMap`/`HashSet` keys,
/// making it explicit that float fields are hashed via bit patterns.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CacheHashWrapper<T>(pub T);
impl<T: CacheHash> core::hash::Hash for CacheHashWrapper<T> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.0.cache_hash(state);
}
}
impl<T: CacheHash> CacheHash for core::ops::RangeInclusive<T> {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.start().cache_hash(state);
self.end().cache_hash(state);
}
}
impl<T> core::ops::Deref for CacheHashWrapper<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
// Bulk impl for types that already implement std::hash::Hash — delegates directly.
#[macro_export]
macro_rules! impl_via_hash {
($($t:ty),* $(,)?) => {
$(
impl $crate::CacheHash for $t {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(self, state);
}
}
)*
};
}
impl_via_hash! {
bool, char,
u8, u16, u32, u64, u128, usize,
i8, i16, i32, i64, i128, isize,
core::time::Duration,
// glam integer vector types have Hash
glam::UVec2, glam::UVec3, glam::UVec4,
glam::IVec2, glam::IVec3, glam::IVec4,
glam::I64Vec2, glam::I64Vec3, glam::I64Vec4,
glam::U64Vec2, glam::U64Vec3, glam::U64Vec4,
glam::BVec2, glam::BVec3, glam::BVec4,
}
#[cfg(feature = "std")]
impl_via_hash! {
String,
core::time::Duration,
}
impl<'a> CacheHash for std::borrow::Cow<'a, str> {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(self, state);
}
}
impl CacheHash for str {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(self, state);
}
}
impl CacheHash for () {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, _state: &mut H) {}
}
// f32 and f64: hash via bit pattern so NaN is handled deterministically.
impl CacheHash for f32 {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(&self.to_bits(), state);
}
}
impl CacheHash for f64 {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(&self.to_bits(), state);
}
}
// glam float vector/matrix types: hash each component via to_bits().
macro_rules! impl_glam_array {
($($t:ty),* $(,)?) => {
$(
impl CacheHash for $t {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
for v in self.to_array() {
CacheHash::cache_hash(&v, state);
}
}
}
)*
};
}
macro_rules! impl_glam_cols {
($($t:ty),* $(,)?) => {
$(
impl CacheHash for $t {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
for v in self.to_cols_array() {
CacheHash::cache_hash(&v, state);
}
}
}
)*
};
}
impl_glam_array! {
glam::Vec2, glam::Vec3, glam::Vec3A, glam::Vec4,
glam::DVec2, glam::DVec3, glam::DVec4,
}
impl_glam_cols! {
glam::Mat2, glam::Mat3, glam::Mat3A, glam::Mat4,
glam::DMat2, glam::DMat3, glam::DMat4,
glam::Affine2, glam::Affine3A,
glam::DAffine2, glam::DAffine3,
}
// Quat / DQuat — to_array gives [x, y, z, w] as floats
impl_glam_array! {
glam::Quat, glam::DQuat,
}
// Generic container impls.
impl<T: CacheHash> CacheHash for Option<T> {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
match self {
None => core::hash::Hash::hash(&0u8, state),
Some(v) => {
core::hash::Hash::hash(&1u8, state);
v.cache_hash(state);
}
}
}
}
impl<T: CacheHash> CacheHash for [T] {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(&self.len(), state);
for item in self {
item.cache_hash(state);
}
}
}
impl<T: CacheHash, const N: usize> CacheHash for [T; N] {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
for item in self {
item.cache_hash(state);
}
}
}
#[cfg(feature = "std")]
impl<T: CacheHash> CacheHash for Vec<T> {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.as_slice().cache_hash(state);
}
}
#[cfg(feature = "std")]
impl<T: CacheHash + ?Sized> CacheHash for Box<T> {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
(**self).cache_hash(state);
}
}
#[cfg(feature = "std")]
impl<T: CacheHash + ?Sized> CacheHash for std::sync::Arc<T> {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
(**self).cache_hash(state);
}
}
impl<T: CacheHash + ?Sized> CacheHash for &T {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
(**self).cache_hash(state);
}
}
// Tuple impls.
macro_rules! impl_tuple {
($($T:ident),+) => {
impl<$($T: CacheHash),+> CacheHash for ($($T,)+) {
#[inline]
#[allow(non_snake_case)]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
let ($($T,)+) = self;
$($T.cache_hash(state);)+
}
}
};
}
impl_tuple!(A, B);
impl_tuple!(A, B, C);
impl_tuple!(A, B, C, D);
impl_tuple!(A, B, C, D, E);
impl_tuple!(A, B, C, D, E, F);
/// rustc-hash's polynomial hash with the state pinned to u64, so keys match across native and wasm targets.
/// The state starts at a nonzero seed, since zero-initialized fx absorbs leading zero words.
#[derive(Clone)]
pub struct FxHasher64 {
hash: u64,
}
const K: u64 = 0xf1357aea2e62a9c5;
const SEED: u64 = 0x517cc1b727220a95;
const SEED1: u64 = 0x243f6a8885a308d3;
const SEED2: u64 = 0x13198a2e03707344;
const PREVENT_TRIVIAL_ZERO_COLLAPSE: u64 = 0xa4093822299f31d0;
impl Default for FxHasher64 {
fn default() -> Self {
Self::new()
}
}
impl FxHasher64 {
pub const fn new() -> Self {
Self { hash: SEED }
}
#[inline]
fn add_to_hash(&mut self, i: u64) {
self.hash = self.hash.wrapping_add(i).wrapping_mul(K);
}
}
impl core::hash::Hasher for FxHasher64 {
#[inline]
fn write(&mut self, bytes: &[u8]) {
self.add_to_hash(hash_bytes(bytes));
}
#[inline]
fn write_u8(&mut self, i: u8) {
self.add_to_hash(i as u64);
}
#[inline]
fn write_u16(&mut self, i: u16) {
self.add_to_hash(i as u64);
}
#[inline]
fn write_u32(&mut self, i: u32) {
self.add_to_hash(i as u64);
}
#[inline]
fn write_u64(&mut self, i: u64) {
self.add_to_hash(i);
}
#[inline]
fn write_u128(&mut self, i: u128) {
self.add_to_hash(i as u64);
self.add_to_hash((i >> 64) as u64);
}
#[inline]
fn write_usize(&mut self, i: usize) {
self.add_to_hash(i as u64);
}
#[inline]
fn finish(&self) -> u64 {
self.hash.rotate_left(26)
}
}
#[inline]
fn multiply_mix(x: u64, y: u64) -> u64 {
let full = (x as u128) * (y as u128);
(full as u64) ^ ((full >> 64) as u64)
}
#[inline]
fn hash_bytes(bytes: &[u8]) -> u64 {
let len = bytes.len();
let mut s0 = SEED1;
let mut s1 = SEED2;
if len <= 16 {
if len >= 8 {
s0 ^= u64::from_le_bytes(bytes[0..8].try_into().unwrap());
s1 ^= u64::from_le_bytes(bytes[len - 8..].try_into().unwrap());
} else if len >= 4 {
s0 ^= u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as u64;
s1 ^= u32::from_le_bytes(bytes[len - 4..].try_into().unwrap()) as u64;
} else if len > 0 {
let lo = bytes[0];
let mid = bytes[len / 2];
let hi = bytes[len - 1];
s0 ^= lo as u64;
s1 ^= ((hi as u64) << 8) | mid as u64;
}
} else {
let mut off = 0;
while off < len - 16 {
let x = u64::from_le_bytes(bytes[off..off + 8].try_into().unwrap());
let y = u64::from_le_bytes(bytes[off + 8..off + 16].try_into().unwrap());
let t = multiply_mix(s0 ^ x, PREVENT_TRIVIAL_ZERO_COLLAPSE ^ y);
s0 = s1;
s1 = t;
off += 16;
}
let suffix = &bytes[len - 16..];
s0 ^= u64::from_le_bytes(suffix[0..8].try_into().unwrap());
s1 ^= u64::from_le_bytes(suffix[8..16].try_into().unwrap());
}
multiply_mix(s0, s1) ^ (len as u64)
}
#[cfg(test)]
mod tests {
use super::FxHasher64;
use core::hash::Hasher;
#[test]
fn leading_zero_words_are_not_absorbed() {
let hash_words = |words: &[u64]| {
let mut hasher = FxHasher64::new();
for &word in words {
hasher.write_u64(word);
}
hasher.finish()
};
assert_ne!(hash_words(&[]), hash_words(&[0]), "a zero word must change the hash of the empty input");
assert_ne!(hash_words(&[0]), hash_words(&[0, 0]), "zero words must accumulate distinct states");
assert_ne!(hash_words(&[0, 7]), hash_words(&[7]), "a leading zero word must not be absorbed");
}
}
@@ -0,0 +1,34 @@
[package]
name = "graphic-types"
version = "0.1.0"
edition = "2024"
description = "Graphic types for Graphene - combines vector types with core infrastructure"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde", "core-types/serde", "vector-types/serde", "raster-types/serde"]
wasm = [
"core-types/wasm",
"vector-types/wasm",
"raster-types/wasm",
"wasm-bindgen",
]
[dependencies]
# Local dependencies
core-types = { workspace = true }
graphene-hash = { workspace = true }
raster-types = { workspace = true, features = ["wgpu"] }
vector-types = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies
dyn-any = { workspace = true }
glam = { workspace = true }
serde_json = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true }
wasm-bindgen = { workspace = true, optional = true }
@@ -0,0 +1,157 @@
use crate::graphic::Graphic;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash;
use core_types::list::List;
use core_types::render_complexity::RenderComplexity;
use dyn_any::DynAny;
use glam::DAffine2;
/// Nominal wrapper around `List<Graphic>` representing a single artboard's content.
///
/// Per-artboard metadata (location, dimensions, background, clip) lives as attributes on the
/// enclosing `List<Artboard>`, not as fields here. This keeps `Artboard` a pure type-system boundary
/// that prevents arbitrary `List<List<...<Graphic>>>` nesting.
#[derive(Clone, Debug, Default, CacheHash, PartialEq, DynAny)]
pub struct Artboard<'e>(List<Graphic<'e>>);
impl<'e> Artboard<'e> {
pub fn new(content: List<Graphic<'e>>) -> Self {
Self(content)
}
pub fn as_graphic_list(&self) -> &List<Graphic<'_>> {
&self.0
}
pub fn as_graphic_list_mut(&mut self) -> &mut List<Graphic<'e>> {
&mut self.0
}
pub fn into_graphic_list(self) -> List<Graphic<'e>> {
self.0
}
/// The artboard with every content group converted to its legacy form, so
/// the value owns all of its content free of arena borrows.
pub fn with_legacy_groups(&self) -> Artboard<'e> {
let mut content = self.0.clone();
for element in content.iter_element_values_mut() {
*element = crate::graphic::map_groups_to_legacy(element);
}
crate::graphic::map_paint_attrs_to_legacy(&mut content);
Artboard(content)
}
}
/// The deep copy-out for `Artboard` elements: as for `Graphic`, a plain
/// clone of group content would carry frame pointers into the evaluation's
/// arena, so memo and capture seams copy out the owned-group form.
///
/// # Safety
/// `ptr` must point at a live parked `Artboard` element field.
unsafe fn deep_clone_artboard(ptr: *const u8) -> Box<dyn std::any::Any + Send + Sync> {
// SAFETY: the caller's contract.
let artboard = unsafe { core_types::record::borrow_element::<Artboard>(core_types::record::Rec::new(ptr)) };
let mut content = artboard.0.clone();
for element in content.iter_element_values_mut() {
*element = crate::graphic::map_groups_to_owned(element);
}
crate::graphic::map_attribute_groups_to_owned(&mut content);
Box::new(Artboard(content))
}
/// The deep re-park for `Artboard` elements: owned content groups replay into
/// the serving arena before the artboard parks.
///
/// # Safety
/// `value` must hold an `Artboard` and `dst` must be a live `Artboard`
/// element field.
unsafe fn deep_repark_artboard(value: &(dyn std::any::Any + Send + Sync), dst: *mut u8, arena: &core_types::arena::Arena) -> Option<()> {
let artboard = value.downcast_ref::<Artboard>().expect("an element replays at its own type");
let mut content = artboard.0.clone();
for element in content.iter_element_values_mut() {
*element = crate::graphic::map_groups_to_resident(element, arena)?;
}
crate::graphic::map_attribute_groups_to_resident(&mut content, arena)?;
// SAFETY: the caller's contract on `dst`; the replayed content is an `Artboard`.
unsafe { core_types::record::write_element(dst, Artboard(content), arena) }
}
/// The promote for `Artboard` elements: as for `Graphic`, content groups the
/// persistent region already holds are shared rather than copied, and content
/// no group is reachable from moves its header instead of copying its heap.
///
/// # Safety
/// `src` must point at a live parked `Artboard` element field, and `dst` at
/// the element field the promoted reference is written to.
unsafe fn promote_artboard(src: *const u8, dst: *mut u8, promotion: &core_types::record::Promotion<'_>) -> Option<()> {
// SAFETY: the caller's contract on `src`.
let artboard = unsafe { core_types::record::borrow_element::<Artboard>(core_types::record::Rec::new(src)) };
if !crate::graphic::list_contains_groups(&artboard.0) {
// SAFETY: a parked element slot holds one reference at offset 0, and
// content no group is reachable from, elements and item attribute values
// alike, owns all of itself.
let header = unsafe { src.cast::<*const u8>().read() };
// SAFETY: as above; the group-free check establishes the own-all-content half.
if let Some(moved) = unsafe { promotion.move_park::<Artboard>(header, 0) } {
// SAFETY: as above, into the promoted image's own element slot.
unsafe { dst.cast::<*const Artboard>().write(moved) };
return Some(());
}
}
let mut content = List::new();
for item in artboard.0.clone().into_iter() {
let (element, attributes) = item.into_parts();
content.push(core_types::list::Item::from_parts(crate::graphic::map_groups_to_persistent(&element, promotion)?, attributes));
}
crate::graphic::map_attribute_groups_to_persistent(&mut content, promotion)?;
// SAFETY: the caller's contract on `dst`; the promoted content is an `Artboard`.
unsafe { core_types::record::write_element(dst, Artboard(content), promotion.persistent()) }
}
const _: () = {
fn register_all() {
core_types::record::register_deep_element_clone::<Artboard>(deep_clone_artboard, deep_repark_artboard);
core_types::record::register_element_promote::<Artboard>(promote_artboard);
}
#[cfg(not(target_family = "wasm"))]
#[core_types::ctor::ctor]
fn register() {
register_all();
}
#[cfg(target_family = "wasm")]
#[unsafe(export_name = "__node_registry_deep_element_artboard")]
extern "C" fn register() {
register_all();
}
};
impl<'e> From<List<Graphic<'e>>> for Artboard<'e> {
fn from(content: List<Graphic<'e>>) -> Self {
Self(content)
}
}
impl<'e> From<Artboard<'e>> for List<Graphic<'e>> {
fn from(artboard: Artboard<'e>) -> 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()
}
}
@@ -0,0 +1,105 @@
//! Boundary helpers between leveled wires and the legacy editor surface:
//! the renderer's flip form materializes a wire into a group, and captured
//! wires convert to the legacy values the editor's downcasts expect.
use crate::graphic::{Graphic, group_to_legacy_list, run_to_legacy_list};
use crate::raster_types::{CPU, GPU, Raster};
use crate::{Artboard, Vector};
use core_types::Color;
use core_types::arena::Arena;
use core_types::context::InjectIndex;
use core_types::gpoll::{Finality, GraphError};
use core_types::node::Node;
use core_types::record::{Group, GroupItem, LevelStatus, materialize_level};
use core_types::uuid::NodeId;
use glam::{DAffine2, DVec2};
use vector_types::GradientStops;
/// The outcome of materializing a leveled wire into a group.
// The group is the render path's success payload; boxing it would add a heap allocation per materialized level.
#[allow(clippy::large_enum_variant)]
pub enum LevelGroup<'e> {
Group(Group<'e>, Finality),
Pending,
Error(GraphError),
}
/// The renderer's flip form: the wire's whole extent materialized into a
/// group over the level's records, ready for the group render bridge.
pub fn materialize_group<'a, 'e, C, N>(node: &'a N, input: &'a C, arena: &'a Arena, frames: &core_types::record::Frames<'e>) -> LevelGroup<'a>
where
C: InjectIndex + Copy + core_types::context::ExtractArena<ArenaRef = &'e Arena>,
N: Node<C>,
{
match materialize_level(node, input, arena, frames) {
LevelStatus::Batch(batch, finality) => {
// SAFETY: a materialized batch's frames are arena-resident.
let item = unsafe { GroupItem::from_resident(batch) };
LevelGroup::Group(Group { row: None, content: item }, finality)
}
LevelStatus::Pending => LevelGroup::Pending,
LevelStatus::Error(error) => LevelGroup::Error(error),
}
}
/// The resident batch as the legacy value the editor's downcasts expect: a
/// rank-0 wire is its element, a leveled `Graphic` wire becomes its legacy
/// list through the group bridge, and another element type becomes a legacy
/// list of that element. `None` for an element type outside the legacy
/// vocabulary or an empty rank-0 batch.
pub fn batch_to_legacy(layout: &core_types::record::Layout, batch: core_types::node::RecordBatch<'_>, _arena: &Arena) -> Option<Box<dyn std::any::Any + Send + Sync>> {
if layout.depth == 0 {
// The group-carrying types legacy-convert while the batch is
// resident; the deep clone-out would hand back the unreadable owned
// form.
if batch.is_empty() {
return None;
}
let element = &layout.element;
if element.type_id == std::any::TypeId::of::<Graphic>() {
// SAFETY: the layout records the element type, and a parked
// element stores its reference at offset 0.
let graphic = unsafe { core_types::record::borrow_element::<Graphic>(batch.get(0).rec()) };
return Some(Box::new(crate::graphic::map_groups_to_legacy(graphic)));
}
if element.type_id == std::any::TypeId::of::<Artboard>() {
// SAFETY: as for the graphic arm.
let artboard = unsafe { core_types::record::borrow_element::<Artboard>(batch.get(0).rec()) };
return Some(Box::new(artboard.with_legacy_groups()));
}
// SAFETY: lane 0 is a live record of `layout`.
return Some(unsafe { (element.clone_out)(batch.get(0).rec().ptr()) });
}
// SAFETY: the caller's batch is resident for the read.
let item = unsafe { GroupItem::from_resident(batch) };
fn typed<T: Clone + Send + Sync + dyn_any::StaticTypeSized + 'static>(item: &GroupItem) -> Option<Box<dyn std::any::Any + Send + Sync>> {
run_to_legacy_list::<T>(item).map(|list| Box::new(list) as Box<dyn std::any::Any + Send + Sync>)
}
if item.typed_lanes::<Graphic>().is_some() {
let group = Group { row: None, content: item };
return Some(Box::new(group_to_legacy_list(&group)));
}
fn typed_artboards(item: &GroupItem) -> Option<Box<dyn std::any::Any + Send + Sync>> {
let mut list = run_to_legacy_list::<Artboard>(item)?;
// The capture outlives the arena generation, so group content must
// leave in its owned legacy form.
for artboard in list.iter_element_values_mut() {
*artboard = artboard.with_legacy_groups();
}
Some(Box::new(list))
}
None.or_else(|| typed_artboards(&item))
.or_else(|| typed::<Vector>(&item))
.or_else(|| typed::<Raster<CPU>>(&item))
.or_else(|| typed::<Raster<GPU>>(&item))
.or_else(|| typed::<Color>(&item))
.or_else(|| typed::<GradientStops>(&item))
.or_else(|| typed::<String>(&item))
.or_else(|| typed::<f64>(&item))
.or_else(|| typed::<u64>(&item))
.or_else(|| typed::<u32>(&item))
.or_else(|| typed::<bool>(&item))
.or_else(|| typed::<NodeId>(&item))
.or_else(|| typed::<DAffine2>(&item))
.or_else(|| typed::<DVec2>(&item))
}
@@ -0,0 +1,742 @@
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash;
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, NodeIdPath};
use core_types::ops::FromAnchorPosition;
use core_types::render_complexity::RenderComplexity;
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use raster_types::{CPU, GPU, Raster};
use std::borrow::Cow;
use vector_types::Gradient;
pub use vector_types::Vector;
/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
#[derive(Clone, Debug, Default, CacheHash, PartialEq, DynAny)]
pub enum Graphic {
/// The absence of graphical content, like CSS's `none` keyword: painting it produces nothing.
#[default]
None,
Graphic(List<Graphic>),
Vector(List<Vector>),
RasterCPU(List<Raster<CPU>>),
RasterGPU(List<Raster<GPU>>),
Color(List<Color>),
Gradient(List<Gradient>),
Text(List<String>),
}
// Graphic
impl From<List<Graphic>> for Graphic {
fn from(graphic: List<Graphic>) -> Self {
Graphic::Graphic(graphic)
}
}
// Vector
impl From<Vector> for Graphic {
fn from(vector: Vector) -> Self {
Graphic::Vector(List::new_from_element(vector))
}
}
impl From<List<Vector>> for Graphic {
fn from(vector: List<Vector>) -> Self {
Graphic::Vector(vector)
}
}
// Note: List<Vector> -> List<Graphic> conversion handled by blanket impl in gcore
// Raster<CPU>
impl From<Raster<CPU>> for Graphic {
fn from(raster: Raster<CPU>) -> Self {
Graphic::RasterCPU(List::new_from_element(raster))
}
}
impl From<List<Raster<CPU>>> for Graphic {
fn from(raster: List<Raster<CPU>>) -> Self {
Graphic::RasterCPU(raster)
}
}
// Note: List conversions handled by blanket impl in gcore
// Raster<GPU>
impl From<Raster<GPU>> for Graphic {
fn from(raster: Raster<GPU>) -> Self {
Graphic::RasterGPU(List::new_from_element(raster))
}
}
impl From<List<Raster<GPU>>> for Graphic {
fn from(raster: List<Raster<GPU>>) -> Self {
Graphic::RasterGPU(raster)
}
}
// Note: List conversions handled by blanket impl in gcore
// Color
impl From<Color> for Graphic {
fn from(color: Color) -> Self {
Graphic::Color(List::new_from_element(color))
}
}
impl From<List<Color>> for Graphic {
fn from(color: List<Color>) -> Self {
Graphic::Color(color)
}
}
// Note: List conversions handled by blanket impl in gcore
// Note: List<Color> -> Option<Color> is in gcore (Color is defined there)
// Gradient
impl From<Gradient> for Graphic {
fn from(gradient: Gradient) -> Self {
Graphic::Gradient(List::new_from_element(gradient))
}
}
impl From<List<Gradient>> for Graphic {
fn from(gradient: List<Gradient>) -> Self {
Graphic::Gradient(gradient)
}
}
// String
impl From<String> for Graphic {
fn from(text: String) -> Self {
Graphic::Text(List::new_from_element(text))
}
}
impl From<List<String>> for Graphic {
fn from(text: List<String>) -> Self {
Graphic::Text(text)
}
}
/// Deeply flattens a `List<Graphic>`, collecting only elements matching a specific variant (extracted by `extract_variant`)
/// and discarding all other non-matching content. Recursion through `Graphic::Graphic` sub-`List`s composes transforms and opacity.
fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic) -> Option<List<T>>) -> List<T> {
fn flatten_recursive<T>(output: &mut List<T>, current_graphic_list: List<Graphic>, extract_variant: fn(Graphic) -> Option<List<T>>) {
for current_graphic_item in current_graphic_list.into_iter() {
// Whether the parent carries each attribute: a structural fact (column presence), never a value comparison.
// Flattening composes a parent attribute onto its children only when the parent has it,
// so an absent parent attribute never invents a column the children didn't already have.
let parent_has_transform = current_graphic_item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some();
let parent_has_opacity = current_graphic_item.attribute::<f64>(ATTR_OPACITY).is_some();
let parent_has_fill = current_graphic_item.attribute::<f64>(ATTR_OPACITY_FILL).is_some();
let parent_has_layer_path = current_graphic_item.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH).is_some();
let layer_path: NodeIdPath = current_graphic_item.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH);
let current_transform: DAffine2 = current_graphic_item.attribute_cloned_or_default(ATTR_TRANSFORM);
let current_opacity: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY, 1.);
let current_fill: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);
match current_graphic_item.into_element() {
// Compose the parent's transform/opacity/fill onto each child, but only for attributes the parent carries.
// A child lacking one is padded with the composition identity (`1.` for opacity/fill, identity for transform), so composing through it is a no-op.
Graphic::Graphic(mut sub_list) => {
if parent_has_transform {
for v in sub_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
*v = current_transform * *v;
}
}
if parent_has_opacity {
for v in sub_list.iter_attribute_values_mut_or_default::<f64>(ATTR_OPACITY) {
*v *= current_opacity;
}
}
if parent_has_fill {
for v in sub_list.iter_attribute_values_mut_or_default::<f64>(ATTR_OPACITY_FILL) {
*v *= current_fill;
}
}
flatten_recursive(output, sub_list, extract_variant);
}
// Extract the target variant and push its items, composing the parent's attributes onto each
other => {
if let Some(typed_list) = extract_variant(other) {
for mut item in typed_list.into_iter() {
// Each `|| item.attribute(...)` keeps an attribute the item itself carries
// (recomposed with the parent's identity value) even when the parent lacks it
if parent_has_transform || item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some() {
let item_transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM);
item.set_attribute(ATTR_TRANSFORM, current_transform * item_transform);
}
if parent_has_opacity || item.attribute::<f64>(ATTR_OPACITY).is_some() {
let item_opacity: f64 = item.attribute_cloned_or(ATTR_OPACITY, 1.);
item.set_attribute(ATTR_OPACITY, current_opacity * item_opacity);
}
if parent_has_fill || item.attribute::<f64>(ATTR_OPACITY_FILL).is_some() {
let item_fill: f64 = item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);
item.set_attribute(ATTR_OPACITY_FILL, current_fill * item_fill);
}
if parent_has_layer_path {
item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
}
output.push(item);
}
}
}
}
}
}
let mut output = List::new();
flatten_recursive(&mut output, content, extract_variant);
output
}
/// Whether a normalized paint graphic list actually carries renderable paint.
/// A 0-item list, or a list whose first graphic is empty, is treated as no paint.
pub fn is_paint_present(graphic_list: &List<Graphic>) -> bool {
graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty())
}
/// Look up the paint graphics stored under attribute for a vector item, in the canonical `List<Graphic>` form.
pub fn graphic_list_at<'a>(list: &'a List<Vector>, index: usize, attribute: &str) -> Option<Cow<'a, List<Graphic>>> {
list.attribute::<List<Graphic>>(attribute, index)
.map(Cow::Borrowed)
// Treat a blank paint attribute as absent so an empty attribute doesn't count as painted
.filter(|graphic_list| is_paint_present(graphic_list))
}
/// Whether the item carries a non-blank canonical `List<Graphic>` paint attribute,
/// checked by borrowing without cloning the renderable list.
pub fn has_paint_at(list: &List<Vector>, index: usize, attribute: &str) -> bool {
list.attribute::<List<Graphic>>(attribute, index).is_some_and(is_paint_present)
}
/// Stores a paint attribute in its canonical `List<Graphic>` form, the only representation paint readers accept.
pub fn set_paint_attribute(attributes: &mut ItemAttributeValues, key: &str, paint: impl IntoGraphicList) {
attributes.insert(key, paint.into_graphic_list());
}
/// Stores a paint attribute at a list index in its canonical `List<Graphic>` form, the only representation paint readers accept.
pub fn set_paint_attribute_at<T>(list: &mut List<T>, index: usize, key: &str, paint: impl IntoGraphicList) {
list.set_attribute(key, index, paint.into_graphic_list());
}
/// Bake the provided transform into the per-item transforms of the paint graphics stored under the
/// canonical `List<Graphic>` fill and stroke attributes.
pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) {
fn bake_list_transform<T>(list: &mut List<T>, transform: DAffine2) {
for item_transform in list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
*item_transform = transform * *item_transform;
}
}
fn bake_graphic_paint_transform(graphics: &mut List<Graphic>, transform: DAffine2) {
for graphic in graphics.iter_element_values_mut() {
match graphic {
Graphic::None => {}
Graphic::Graphic(list) => bake_list_transform(list, transform),
Graphic::Vector(list) => bake_list_transform(list, transform),
Graphic::RasterCPU(list) => bake_list_transform(list, transform),
Graphic::RasterGPU(list) => bake_list_transform(list, transform),
Graphic::Gradient(list) => bake_list_transform(list, transform),
Graphic::Text(list) => bake_list_transform(list, transform),
Graphic::Color(_) => {}
}
}
}
for paint_key in [ATTR_FILL, ATTR_STROKE] {
if let Some(graphics) = attributes.get_mut::<List<Graphic>>(paint_key) {
bake_graphic_paint_transform(graphics, transform);
}
}
}
/// Maps from a concrete element type to its corresponding `Graphic` enum variant,
/// enabling type-directed casting of typed `List`s from a `Graphic` value.
pub trait TryFromGraphic: Clone + Sized {
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>>;
}
impl TryFromGraphic for Vector {
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
if let Graphic::Vector(t) = graphic { Some(t) } else { None }
}
}
impl TryFromGraphic for Raster<CPU> {
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
if let Graphic::RasterCPU(t) = graphic { Some(t) } else { None }
}
}
impl TryFromGraphic for Color {
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
if let Graphic::Color(t) = graphic { Some(t) } else { None }
}
}
impl TryFromGraphic for Gradient {
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
if let Graphic::Gradient(t) = graphic { Some(t) } else { None }
}
}
impl TryFromGraphic for String {
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
if let Graphic::Text(t) = graphic { Some(t) } else { None }
}
}
// Local trait to convert types to List<Graphic> (avoids orphan rule issues)
pub trait IntoGraphicList: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static {
fn into_graphic_list(self) -> List<Graphic>;
/// Deeply flattens any content of type `T` within a `List<Graphic>`, discarding all other content, and returning a flat `List<T>`.
fn into_flattened_list<T: TryFromGraphic>(self) -> List<T>
where
Self: std::marker::Sized,
{
flatten_graphic_list(self.into_graphic_list(), T::try_from_graphic)
}
}
impl IntoGraphicList for List<Graphic> {
fn into_graphic_list(self) -> List<Graphic> {
self
}
}
impl IntoGraphicList for List<Vector> {
fn into_graphic_list(self) -> List<Graphic> {
// Propagate the `editor:layer_path` column (if present) from item 0 onto the wrapper Graphic item so a
// subsequent `flatten_graphic_list` doesn't drop the inner Vector's layer stamp
let layer_path = self.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, 0).cloned();
let mut graphic_list = List::new_from_element(Graphic::Vector(self));
if let Some(layer_path) = layer_path {
graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path);
}
graphic_list
}
}
impl IntoGraphicList for List<Raster<CPU>> {
fn into_graphic_list(self) -> List<Graphic> {
List::new_from_element(Graphic::RasterCPU(self))
}
}
impl IntoGraphicList for List<Raster<GPU>> {
fn into_graphic_list(self) -> List<Graphic> {
List::new_from_element(Graphic::RasterGPU(self))
}
}
impl IntoGraphicList for List<Color> {
fn into_graphic_list(self) -> List<Graphic> {
List::new_from_element(Graphic::Color(self))
}
}
impl IntoGraphicList for List<Gradient> {
fn into_graphic_list(self) -> List<Graphic> {
List::new_from_element(Graphic::Gradient(self))
}
}
impl IntoGraphicList for List<String> {
fn into_graphic_list(self) -> List<Graphic> {
let layer_path = self.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, 0).cloned();
let mut graphic_list = List::new_from_element(Graphic::Text(self));
if let Some(layer_path) = layer_path {
graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path);
}
graphic_list
}
}
impl IntoGraphicList for Item<DAffine2> {
fn into_graphic_list(self) -> List<Graphic> {
List::new_from_element(Graphic::default())
}
}
// DAffine2
impl From<Item<DAffine2>> for Graphic {
fn from(_: Item<DAffine2>) -> Self {
Graphic::default()
}
}
// DVec2
impl From<Item<DVec2>> for Graphic {
fn from(position: Item<DVec2>) -> Self {
Graphic::Vector(List::new_from_element(Vector::from_anchor_position(position.into_element())))
}
}
// Note: List conversions handled by blanket impl in gcore
impl Graphic {
pub fn as_graphic(&self) -> Option<&List<Graphic>> {
match self {
Graphic::Graphic(graphic) => Some(graphic),
_ => None,
}
}
pub fn as_graphic_mut(&mut self) -> Option<&mut List<Graphic>> {
match self {
Graphic::Graphic(graphic) => Some(graphic),
_ => None,
}
}
pub fn as_vector(&self) -> Option<&List<Vector>> {
match self {
Graphic::Vector(vector) => Some(vector),
_ => None,
}
}
pub fn as_vector_mut(&mut self) -> Option<&mut List<Vector>> {
match self {
Graphic::Vector(vector) => Some(vector),
_ => None,
}
}
pub fn as_raster(&self) -> Option<&List<Raster<CPU>>> {
match self {
Graphic::RasterCPU(raster) => Some(raster),
_ => None,
}
}
pub fn as_raster_mut(&mut self) -> Option<&mut List<Raster<CPU>>> {
match self {
Graphic::RasterCPU(raster) => Some(raster),
_ => None,
}
}
pub fn had_clip_enabled(&self) -> bool {
fn all_clipped<T>(list: &List<T>) -> bool {
list.iter_attribute_values_or_default::<bool>(ATTR_CLIPPING_MASK).all(|clip| clip)
}
match self {
Graphic::None => true,
Graphic::Vector(list) => all_clipped(list),
Graphic::Graphic(list) => all_clipped(list),
Graphic::RasterCPU(list) => all_clipped(list),
Graphic::RasterGPU(list) => all_clipped(list),
Graphic::Color(list) => all_clipped(list),
Graphic::Gradient(list) => all_clipped(list),
Graphic::Text(list) => all_clipped(list),
}
}
pub fn can_reduce_to_clip_path(&self) -> bool {
match self {
Graphic::Vector(vector) => (0..vector.len()).all(|index| {
let Some(element) = vector.element(index) else { return false };
let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let fill_opaque_or_absent = graphic_list_at(vector, index, ATTR_FILL).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque()));
let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke())
|| graphic_list_at(vector, index, ATTR_STROKE).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent
}),
_ => false,
}
}
pub fn is_opaque(&self) -> bool {
match self {
Graphic::None => false,
Graphic::Graphic(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque),
Graphic::Vector(list) => {
let is_paint_opaque_at = |key: &str, index: usize| graphic_list_at(list, index, key).is_some_and(|graphic_list| graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque()));
!list.is_empty()
&& (0..list.len()).all(|i| {
let Some(vector) = list.element(i) else { return false };
let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.);
let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
let fill_opaque = opacity_fill >= 1. - f64::EPSILON && is_paint_opaque_at(ATTR_FILL, i);
let stroke_opaque_or_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_opaque_at(ATTR_STROKE, i);
opacity >= 1. - f64::EPSILON && fill_opaque && stroke_opaque_or_invisible
})
}
Graphic::Color(list) => list.element(0).is_some_and(|color| color.is_opaque()),
Graphic::Gradient(list) => list.element(0).is_some_and(|stops| stops.iter().all(|stop| stop.color.is_opaque())),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false,
}
}
pub fn is_fully_transparent(&self) -> bool {
match self {
Graphic::None => true,
Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent),
Graphic::Vector(list) => (0..list.len()).all(|i| {
let Some(vector) = list.element(i) else { return false };
let is_paint_fully_transparent_at =
|key: &str, index: usize| graphic_list_at(list, index, key).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.);
if opacity <= f64::EPSILON {
return true;
}
let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
let fill_invisible = opacity_fill <= f64::EPSILON || is_paint_fully_transparent_at(ATTR_FILL, i);
let stroke_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_fully_transparent_at(ATTR_STROKE, i);
fill_invisible && stroke_invisible
}),
Graphic::Color(list) => list.iter_element_values().all(|color| color.a() == 0.),
Graphic::Gradient(list) => list.iter_element_values().all(|stops| stops.iter().all(|stop| stop.color.a() == 0.)),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false,
}
}
/// True if this paint opaquely covers the entire fill region.
/// Vector, Raster, and a nested Graphic may leave gaps, so they return false.
pub fn covers_opaquely(&self) -> bool {
matches!(self, Graphic::Color(_) | Graphic::Gradient(_)) && self.is_opaque()
}
/// Returns true if this graphic contains no content.
pub fn is_empty(&self) -> bool {
match self {
Graphic::None => true,
Graphic::Graphic(list) => list.is_empty(),
Graphic::Vector(list) => list.is_empty(),
Graphic::Color(list) => list.is_empty(),
Graphic::Gradient(list) => list.is_empty(),
Graphic::RasterCPU(list) => list.is_empty(),
Graphic::RasterGPU(list) => list.is_empty(),
Graphic::Text(list) => list.is_empty(),
}
}
}
impl BoundingBox for Graphic {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
match self {
Graphic::None => RenderBoundingBox::None,
Graphic::Vector(list) => list.bounding_box(transform, include_stroke),
Graphic::RasterCPU(list) => list.bounding_box(transform, include_stroke),
Graphic::RasterGPU(list) => list.bounding_box(transform, include_stroke),
Graphic::Graphic(list) => list.bounding_box(transform, include_stroke),
Graphic::Color(list) => list.bounding_box(transform, include_stroke),
Graphic::Gradient(list) => list.bounding_box(transform, include_stroke),
Graphic::Text(list) => list.bounding_box(transform, include_stroke),
}
}
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
match self {
Graphic::None => RenderBoundingBox::None,
Graphic::Vector(vector) => vector.thumbnail_bounding_box(transform, include_stroke),
Graphic::RasterCPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
Graphic::RasterGPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
Graphic::Graphic(graphic) => graphic.thumbnail_bounding_box(transform, include_stroke),
Graphic::Color(color) => color.thumbnail_bounding_box(transform, include_stroke),
Graphic::Gradient(gradient) => gradient.thumbnail_bounding_box(transform, include_stroke),
Graphic::Text(list) => list.thumbnail_bounding_box(transform, include_stroke),
}
}
}
impl RenderComplexity for Graphic {
fn render_complexity(&self) -> usize {
match self {
Self::None => 0,
Self::Graphic(list) => list.render_complexity(),
Self::Vector(list) => list.render_complexity(),
Self::RasterCPU(list) => list.render_complexity(),
Self::RasterGPU(list) => list.render_complexity(),
Self::Color(list) => list.render_complexity(),
Self::Gradient(list) => list.render_complexity(),
Self::Text(list) => list.render_complexity(),
}
}
}
// Node definitions moved to graphic-nodes crate
pub trait AtIndex {
type Output;
fn at_index(&self, index: usize) -> Option<Self::Output>;
fn at_index_from_end(&self, index: usize) -> Option<Self::Output>;
}
impl<T: Clone> AtIndex for Vec<T> {
type Output = T;
fn at_index(&self, index: usize) -> Option<Self::Output> {
self.get(index).cloned()
}
fn at_index_from_end(&self, index: usize) -> Option<Self::Output> {
if index == 0 || index > self.len() { None } else { self.get(self.len() - index).cloned() }
}
}
impl<T: Clone> AtIndex for List<T> {
type Output = List<T>;
fn at_index(&self, index: usize) -> Option<Self::Output> {
self.clone_item(index).map(|item| {
let mut result_list = Self::default();
result_list.push(item);
result_list
})
}
fn at_index_from_end(&self, index: usize) -> Option<Self::Output> {
if index == 0 || index > self.len() { None } else { self.at_index(self.len() - index) }
}
}
pub trait OmitIndex {
fn omit_index(&self, index: usize) -> Self;
fn omit_index_from_end(&self, index: usize) -> Self;
}
impl<T: Clone> OmitIndex for Vec<T> {
fn omit_index(&self, index: usize) -> Self {
self.iter().enumerate().filter(|(i, _)| *i != index).map(|(_, v)| v.clone()).collect()
}
fn omit_index_from_end(&self, index: usize) -> Self {
if index == 0 || index > self.len() {
return self.clone();
}
self.omit_index(self.len() - index)
}
}
impl<T: Clone> OmitIndex for List<T> {
fn omit_index(&self, index: usize) -> Self {
let mut result = Self::default();
for i in 0..self.len() {
if i != index
&& let Some(item) = self.clone_item(i)
{
result.push(item);
}
}
result
}
fn omit_index_from_end(&self, index: usize) -> Self {
if index == 0 || index > self.len() {
return self.clone();
}
self.omit_index(self.len() - index)
}
}
#[cfg(test)]
mod tests {
use super::*;
use core_types::list::List;
fn vector_graphic() -> Graphic {
Graphic::Vector(List::new_from_element(Vector::default()))
}
// Flattening must not invent attribute columns that neither the parent graphic nor the child carried
#[test]
fn flatten_does_not_invent_attributes() {
let graphics = List::new_from_element(vector_graphic());
let flattened: List<Vector> = graphics.into_flattened_list();
for key in [ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, ATTR_EDITOR_LAYER_PATH] {
assert!(!flattened.attribute_keys().any(|k| k == key), "flatten invented the `{key}` attribute");
}
}
// A parent attribute that is present must compose onto the flattened children
#[test]
fn flatten_propagates_present_attributes() {
let mut graphics = List::new_from_element(vector_graphic());
graphics.set_attribute(ATTR_OPACITY, 0, 0.5_f64);
let flattened: List<Vector> = graphics.into_flattened_list();
assert_eq!(flattened.attribute_cloned_or_default::<f64>(ATTR_OPACITY, 0), 0.5);
let mut group = List::new_from_element(Graphic::Graphic(List::new_from_element(vector_graphic())));
group.set_attribute(ATTR_OPACITY, 0, 0.5_f64);
let flattened: List<Vector> = group.into_flattened_list();
assert_eq!(flattened.attribute_cloned_or_default::<f64>(ATTR_OPACITY, 0), 0.5);
}
}
#[cfg(test)]
mod graphic_is_opaque_tests {
use core_types::ATTR_SPREAD_METHOD;
use vector_types::{GradientSpreadMethod, GradientStop};
use super::*;
fn color_graphic(alpha: f64) -> Graphic {
let color = Color::from_rgbaf32(1., 0., 0., alpha as f32).unwrap();
Graphic::Color(List::new_from_element(color))
}
fn gradient_graphic(gradient: Gradient) -> Graphic {
let mut gradient_list = List::new_from_element(gradient);
gradient_list.set_attribute(ATTR_SPREAD_METHOD, 0, GradientSpreadMethod::Pad);
Graphic::Gradient(gradient_list)
}
#[test]
fn opaque_color_is_opaque() {
let g = color_graphic(1.);
assert!(g.is_opaque());
}
#[test]
fn transparent_color_is_not_opaque() {
let g = color_graphic(0.5);
assert!(!g.is_opaque());
}
#[test]
fn vector_is_not_opaque() {
let g = Graphic::Vector(List::default());
assert!(!g.is_opaque());
}
#[test]
fn gradient_with_all_opaque_stops_is_opaque() {
let color_1 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
let color_2 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
let gradient = Gradient::new(vec![
GradientStop {
position: 0.,
midpoint: 0.5,
color: color_1,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: color_2,
},
]);
let g = gradient_graphic(gradient);
assert!(g.is_opaque());
}
#[test]
fn gradient_with_transparent_stop_is_not_opaque() {
let color_1 = Color::from_rgbaf32(1., 0., 0., 0.5).unwrap();
let color_2 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
let gradient = Gradient::new(vec![
GradientStop {
position: 0.,
midpoint: 0.5,
color: color_1,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: color_2,
},
]);
let g = gradient_graphic(gradient);
assert!(!g.is_opaque());
}
}
@@ -0,0 +1,660 @@
//! The record-crossing glue: group interiors carried between the owned, resident, and persistent regions.
use super::Graphic;
use core_types::Color;
use core_types::list::{Item, List};
use raster_types::{CPU, Raster};
use vector_types::Vector;
/// The graphic with every `Group` deep-copied to its owned form, which
/// survives the arena generation but cannot be read until
/// [`map_groups_to_resident`] re-parks it into a serving arena.
pub fn map_groups_to_owned<'out>(graphic: &Graphic<'_>) -> Graphic<'out> {
match graphic {
Graphic::Group(group) => Graphic::Group(group.copy_out()),
Graphic::Graphic(children) => {
let mut out = List::new();
for item in children.clone().into_iter() {
let (element, attributes) = item.into_parts();
out.push(Item::from_parts(map_groups_to_owned(&element), attributes));
}
map_attribute_groups_to_owned(&mut out);
Graphic::Graphic(out)
}
Graphic::Vector(vector) => Graphic::Vector(vector.clone()),
Graphic::RasterCPU(raster) => Graphic::RasterCPU(raster.clone()),
Graphic::RasterGPU(raster) => Graphic::RasterGPU(raster.clone()),
Graphic::Color(color) => Graphic::Color(*color),
Graphic::Gradient(gradient) => Graphic::Gradient(gradient.clone()),
Graphic::Text(text) => Graphic::Text(text.clone()),
}
}
/// The graphic with every owned `Group` re-parked into `arena`; `None`
/// reports arena exhaustion.
pub fn map_groups_to_resident<'a>(graphic: &Graphic<'a>, arena: &'a core_types::arena::Arena) -> Option<Graphic<'a>> {
match graphic {
Graphic::Group(group) => group.replay(arena).map(Graphic::Group),
Graphic::Graphic(children) => {
let mut children = children.clone();
for child in children.iter_element_values_mut() {
*child = map_groups_to_resident(child, arena)?;
}
map_attribute_groups_to_resident(&mut children, arena)?;
Some(Graphic::Graphic(children))
}
other => Some(other.clone()),
}
}
/// The deep copy-out for `Graphic` elements: a plain clone of a group
/// interior would carry frame pointers into the evaluation's arena, so memo
/// and capture seams copy out the owned-group form.
///
/// # Safety
/// `ptr` must point at a live parked `Graphic` element field.
unsafe fn deep_clone_graphic(ptr: *const u8) -> Box<dyn std::any::Any + Send + Sync> {
// SAFETY: the caller's contract.
let graphic = unsafe { core_types::record::borrow_element::<Graphic>(core_types::record::Rec::new(ptr)) };
Box::new(map_groups_to_owned(graphic))
}
/// The deep re-park for `Graphic` elements: owned groups replay into the
/// serving arena before the graphic parks.
///
/// # Safety
/// `value` must hold a `Graphic` and `dst` must be a live `Graphic` element
/// field.
unsafe fn deep_repark_graphic(value: &(dyn std::any::Any + Send + Sync), dst: *mut u8, arena: &core_types::arena::Arena) -> Option<()> {
let graphic = value.downcast_ref::<Graphic>().expect("an element replays at its own type");
let resident = map_groups_to_resident(graphic, arena)?;
let retained = graphic_retained_heap(&resident);
// SAFETY: the caller's contract; the resident form is a `Graphic` for `dst`.
unsafe { core_types::record::write_element_sized(dst, resident, arena, retained) }
}
/// The graphic with every `Group` promoted into the persistent region: an
/// interior already living there is shared rather than copied, so the cost is
/// what this level newly produced. `None` reports arena exhaustion.
pub fn map_groups_to_persistent<'p>(graphic: &Graphic<'_>, promotion: &core_types::record::Promotion<'p>) -> Option<Graphic<'p>> {
match graphic {
Graphic::Group(group) => group.to_persistent(promotion).map(Graphic::Group),
Graphic::Graphic(children) => {
let mut out = List::new();
for item in children.clone().into_iter() {
let (element, attributes) = item.into_parts();
out.push(Item::from_parts(map_groups_to_persistent(&element, promotion)?, attributes));
}
map_attribute_groups_to_persistent(&mut out, promotion)?;
Some(Graphic::Graphic(out))
}
Graphic::Vector(vector) => Some(Graphic::Vector(vector.clone())),
Graphic::RasterCPU(raster) => Some(Graphic::RasterCPU(raster.clone())),
Graphic::RasterGPU(raster) => Some(Graphic::RasterGPU(raster.clone())),
Graphic::Color(color) => Some(Graphic::Color(*color)),
Graphic::Gradient(gradient) => Some(Graphic::Gradient(gradient.clone())),
Graphic::Text(text) => Some(Graphic::Text(text.clone())),
}
}
/// The list's attribute keys, owned so the columns can be walked mutably.
fn attribute_keys(list: &List<Graphic>) -> Vec<String> {
list.attribute_keys().map(str::to_string).collect()
}
/// Every group held in the list's item attribute values, deep-copied to its
/// owned form. Element values are the caller's own pass; a value list's own
/// item attributes recurse here.
pub(crate) fn map_attribute_groups_to_owned(list: &mut List<Graphic<'_>>) {
for key in attribute_keys(list) {
// A column of a type that cannot hold groups is skipped whole.
let Some(values) = list.iter_attribute_values_mut::<Option<List<Graphic>>>(&key) else { continue };
for value in values.flatten() {
for element in value.iter_element_values_mut() {
*element = map_groups_to_owned(element);
}
map_attribute_groups_to_owned(value);
}
}
}
/// Every owned group held in the list's item attribute values, re-parked into
/// `arena`. `None` reports arena exhaustion.
pub(crate) fn map_attribute_groups_to_resident(list: &mut List<Graphic<'_>>, arena: &core_types::arena::Arena) -> Option<()> {
for key in attribute_keys(list) {
// A column of a type that cannot hold groups is skipped whole.
let Some(values) = list.iter_attribute_values_mut::<Option<List<Graphic>>>(&key) else { continue };
for value in values.flatten() {
for element in value.iter_element_values_mut() {
let resident = map_groups_to_resident(element, arena)?;
// SAFETY: the attribute store is erased, and the replay serves as
// long as `arena`, which the store's reader outlives.
*element = unsafe { core_types::record::erase_static(resident) };
}
map_attribute_groups_to_resident(value, arena)?;
}
}
Some(())
}
/// Every group held in the list's item attribute values, promoted into the
/// persistent region on the same Cow dispatch the elements take. `None` reports
/// arena exhaustion.
pub(crate) fn map_attribute_groups_to_persistent(list: &mut List<Graphic<'_>>, promotion: &core_types::record::Promotion<'_>) -> Option<()> {
for key in attribute_keys(list) {
// A column of a type that cannot hold groups is skipped whole.
let Some(values) = list.iter_attribute_values_mut::<Option<List<Graphic>>>(&key) else { continue };
for value in values.flatten() {
for element in value.iter_element_values_mut() {
let promoted = map_groups_to_persistent(element, promotion)?;
// SAFETY: the attribute store is erased, and persistent content
// outlives the evaluation.
*element = unsafe { core_types::record::erase_static(promoted) };
}
map_attribute_groups_to_persistent(value, promotion)?;
}
}
Some(())
}
/// The promote for `Graphic` elements: the generic path would deep-copy every
/// interior through an owned intermediate, while this shares the interiors the
/// persistent region already holds. A graphic no group is reachable from
/// references nothing the evaluation owns, so its header moves and its heap is
/// never copied.
///
/// # Safety
/// `src` must point at a live parked `Graphic` element field, and `dst` at the
/// element field the promoted reference is written to.
unsafe fn promote_graphic(src: *const u8, dst: *mut u8, promotion: &core_types::record::Promotion<'_>) -> Option<()> {
// SAFETY: the caller's contract on `src`.
let graphic = unsafe { core_types::record::borrow_element::<Graphic>(core_types::record::Rec::new(src)) };
if !graphic_contains_groups(graphic) {
// SAFETY: a parked element slot holds one reference at offset 0, and a
// graphic no group is reachable from, elements and item attribute values
// alike, owns all of its content.
let header = unsafe { src.cast::<*const u8>().read() };
// SAFETY: as above; the group-free check establishes the own-all-content half.
if let Some(moved) = unsafe { promotion.move_park::<Graphic<'static>>(header, graphic_retained_heap(graphic)) } {
// SAFETY: as above, into the promoted image's own element slot.
unsafe { dst.cast::<*const Graphic<'static>>().write(moved) };
return Some(());
}
}
let promoted = map_groups_to_persistent(graphic, promotion)?;
let retained = graphic_retained_heap(&promoted);
// SAFETY: the caller's contract on `dst`; the promoted form is a `Graphic`.
unsafe { core_types::record::write_element_sized(dst, promoted, promotion.persistent(), retained) }
}
/// The heap a graphic's own payload owns. Group interiors are excluded: their
/// lanes park through this same glue and are counted as they land.
fn graphic_retained_heap(graphic: &Graphic<'_>) -> usize {
match graphic {
Graphic::Vector(vector) => vector_retained_heap(vector),
Graphic::RasterCPU(raster) => raster.data.len() * size_of::<Color>(),
Graphic::Text(text) => text.len(),
Graphic::Gradient(gradient) => gradient.len() * size_of::<(f64, Color)>(),
Graphic::Graphic(children) => (0..children.len()).filter_map(|index| children.element(index)).map(graphic_retained_heap).sum(),
Graphic::Group(_) | Graphic::RasterGPU(_) | Graphic::Color(_) => 0,
}
}
/// The heap a vector's domain columns own, summed over the columns it
/// exposes, so the segment domain's private parallel columns are undercounted.
fn vector_retained_heap(vector: &Vector) -> usize {
size_of_val(vector.point_domain.ids())
+ size_of_val(vector.point_domain.positions())
+ size_of_val(vector.segment_domain.ids())
+ size_of_val(vector.region_domain.ids())
+ size_of_val(vector.colinear_manipulators.as_slice())
}
/// Whether any group is reachable from the graphic, so it does not own all of
/// its content.
fn graphic_contains_groups(graphic: &Graphic) -> bool {
match graphic {
Graphic::Group(_) => true,
Graphic::Graphic(children) => list_contains_groups(children),
_ => false,
}
}
/// Whether any group is reachable from the list, so it does not own all of its
/// content. An item attribute value holds its groups exactly as an element
/// does: a cloned group there keeps a resident interior borrowing the
/// evaluation's arena, so the elements alone cannot decide ownership.
pub(crate) fn list_contains_groups(list: &List<Graphic>) -> bool {
(0..list.len()).any(|index| list.element(index).is_some_and(graphic_contains_groups)) || attribute_values_contain_groups(list)
}
/// Whether any group hides in the list's item attribute values. Only the
/// group-capable columns are scanned: those are the value types the deep field
/// glue is registered for, today `Option<List<Graphic>>` alone. A list with no
/// attribute columns costs nothing, and a column of any other type is decided
/// by its one downcast rather than per value.
fn attribute_values_contain_groups(list: &List<Graphic>) -> bool {
list.attribute_keys().any(|key| {
list.iter_attribute_values::<Option<List<Graphic>>>(key)
.is_some_and(|mut values| values.any(|value| value.as_ref().is_some_and(list_contains_groups)))
})
}
/// The heap a graphic list's elements own, group interiors excluded as
/// [`graphic_retained_heap`] excludes them.
fn list_retained_heap(list: &List<Graphic>) -> usize {
(0..list.len()).filter_map(|index| list.element(index)).map(graphic_retained_heap).sum()
}
/// The deep copy-out for graphic-list field values (the paint markers' owned
/// form): content groups leave in their owned form, whether an element or an
/// item attribute value holds them. Declines (`None`) for group-free content,
/// which already owns everything.
fn deep_clone_graphic_list(value: &dyn core_types::list::AnyAttributeValue) -> Option<Box<dyn core_types::list::AnyAttributeValue>> {
let list = value.as_any().downcast_ref::<Option<List<Graphic>>>().expect("a graphic list field deep-copies at its own type");
let list = list.as_ref().filter(|list| list_contains_groups(list))?;
let mut list = list.clone();
for element in list.iter_element_values_mut() {
*element = map_groups_to_owned(element);
}
map_attribute_groups_to_owned(&mut list);
Some(Box::new(Some(list)))
}
/// The deep replay for graphic-list field values: owned content groups replay
/// into the serving arena before the field re-parks, whether an element or an
/// item attribute value holds them. `Some(None)` declines for group-free
/// content; `None` reports arena exhaustion.
fn deep_repark_graphic_list(value: &dyn core_types::list::AnyAttributeValue, arena: &core_types::arena::Arena) -> Option<Option<Box<dyn core_types::list::AnyAttributeValue>>> {
let list = value.as_any().downcast_ref::<Option<List<Graphic>>>().expect("a graphic list field replays at its own type");
let Some(list) = list.as_ref().filter(|list| list_contains_groups(list)) else {
return Some(None);
};
let mut list = list.clone();
for element in list.iter_element_values_mut() {
*element = map_groups_to_resident(element, arena)?;
}
map_attribute_groups_to_resident(&mut list, arena)?;
// SAFETY: every group the clone carried, in an element or an item attribute
// value, now names `arena`, whose borrow the replayed field is read at.
let list = unsafe { core_types::record::erase_static(list) };
Some(Some(Box::new(Some(list))))
}
/// The promote for graphic-list fields, the deep field glue's third half: the
/// owned halves copy content groups out to the owned form and replay them back,
/// while this maps the content transient-to-persistent in one pass.
///
/// THE TWO-LEVEL SHARING LAW: the field's own header is not provenance-shared.
/// It moves where no group is reachable from the list, elements and item
/// attribute values alike, since the payload then owns all of its content and
/// the transient arena confirms the reference is its own park, and otherwise it
/// clones into a fresh persistent park. One level inside, a content group's
/// interior is arena-resident and its provenance is decidable, so it takes
/// [`map_groups_to_persistent`]'s Cow dispatch: an interior the persistent
/// region already holds is shared pointer for pointer.
///
/// # Safety
/// `src` must point at a live parked graphic-list field, and `dst` at the field
/// slot the promoted reference is written to.
unsafe fn promote_graphic_list(src: *const u8, dst: *mut u8, promotion: &core_types::record::Promotion<'_>) -> Option<()> {
// SAFETY: the caller's contract; the slot holds one optional reference.
let Some(list) = (unsafe { src.cast::<Option<&List<Graphic<'static>>>>().read() }) else {
// SAFETY: as above, into the promoted image's own field slot.
unsafe { dst.cast::<Option<&List<Graphic<'static>>>>().write(None) };
return Some(());
};
let retained = list_retained_heap(list);
if !list_contains_groups(list) {
// SAFETY: a list no group is reachable from, elements and item attribute
// values alike, owns all of its content, and the arena declines a
// reference that is not a park at the list's own address and size.
if let Some(moved) = unsafe { promotion.move_park::<List<Graphic<'static>>>(std::ptr::from_ref(list).cast(), retained) } {
// SAFETY: the move published a live list in the persistent region.
unsafe { dst.cast::<Option<&List<Graphic<'static>>>>().write(Some(&*moved)) };
return Some(());
}
}
let mut promoted = list.clone();
for element in promoted.iter_element_values_mut() {
*element = map_groups_to_persistent(element, promotion)?;
}
map_attribute_groups_to_persistent(&mut promoted, promotion)?;
// SAFETY: every borrow the clone carried, in an element or an item attribute
// value, was replaced by persistent content above, so the erased form
// outlives the evaluation.
let promoted = unsafe { core_types::record::erase_static(promoted) };
let (parked, _) = promotion.persistent().alloc_sized(promoted, retained)?;
// SAFETY: the slot holds one optional reference.
unsafe { dst.cast::<Option<&List<Graphic<'static>>>>().write(Some(parked)) };
Some(())
}
const _: () = {
fn register_all() {
core_types::record::register_deep_element_clone::<Graphic>(deep_clone_graphic, deep_repark_graphic);
core_types::record::register_deep_field_value::<Option<List<Graphic>>>(deep_clone_graphic_list, deep_repark_graphic_list);
core_types::record::register_field_promote::<Option<&'static List<Graphic<'static>>>>(promote_graphic_list);
core_types::record::register_element_promote::<Graphic>(promote_graphic);
core_types::record::register_retained_heap::<Graphic>(|value| value.downcast_ref::<Graphic>().map_or(0, graphic_retained_heap));
core_types::record::register_retained_heap::<Vector>(|value| value.downcast_ref::<Vector>().map_or(0, vector_retained_heap));
core_types::record::register_retained_heap::<Raster<CPU>>(|value| value.downcast_ref::<Raster<CPU>>().map_or(0, |raster| raster.data.len() * size_of::<Color>()));
core_types::record::register_retained_heap::<String>(|value| value.downcast_ref::<String>().map_or(0, String::len));
}
#[cfg(not(target_family = "wasm"))]
#[core_types::ctor::ctor]
fn register() {
register_all();
}
#[cfg(target_family = "wasm")]
#[unsafe(export_name = "__node_registry_deep_element_graphic")]
extern "C" fn register() {
register_all();
}
};
#[cfg(test)]
mod run_tests {
use super::*;
use crate::graphic::test_support::{native_group_paint, unit_square_at};
use crate::graphic::{group_to_legacy_list, map_groups_to_legacy};
use crate::markers::{Fill, Stroke};
use core_types::attribute::Attribute;
use core_types::lane::LaneSource;
use core_types::record::{FieldWrite, RunBuilder, RunView, element_write_hashed};
use glam::DVec2;
#[test]
fn an_owned_group_replays_content_equal_after_the_source_dies() {
let paint = List::new_from_element(Graphic::Color(Color::BLACK));
let vector = unit_square_at(DVec2::ZERO);
let source = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
let lane = builder.push(vector.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint));
let group = core_types::record::Group { row: None, content: builder.finish() };
let expected = group_to_legacy_list(&group);
let owned = map_groups_to_owned(&Graphic::Group(group));
drop(source);
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let resident = map_groups_to_resident(&owned, &arena).expect("the arena holds the replay");
let Graphic::Group(group) = &resident else { panic!("the replay keeps the group form") };
assert_eq!(group_to_legacy_list(group), expected);
}
#[test]
fn an_owned_group_replays_nested_groups_through_the_element_glue() {
let vector = unit_square_at(DVec2::ZERO);
let source = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[], 1).unwrap();
builder.push(vector.clone()).unwrap();
let nested = Graphic::Group(core_types::record::Group { row: None, content: builder.finish() });
let mut builder = RunBuilder::new(&source, element_write_hashed::<Graphic>(), &[], 1).unwrap();
builder.push(nested).unwrap();
let group = core_types::record::Group { row: None, content: builder.finish() };
let expected = group_to_legacy_list(&group);
let owned = map_groups_to_owned(&Graphic::Group(group));
drop(source);
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let resident = map_groups_to_resident(&owned, &arena).expect("the arena holds the replay");
let Graphic::Group(group) = &resident else { panic!("the replay keeps the group form") };
assert_eq!(group_to_legacy_list(group), expected);
}
#[test]
fn an_owned_run_deep_copies_graphic_list_fields() {
let inner_vector = unit_square_at(DVec2::ZERO);
let source = core_types::arena::Arena::new(1 << 16).unwrap();
// SAFETY: the erased native list serves only while `source` is live; the
// deep glue under test replaces its borrows at the copy-out seam.
let paint = unsafe { core_types::record::erase_static(native_group_paint(&inner_vector, &source)) };
let vector = unit_square_at(DVec2::new(4., 4.));
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
let lane = builder.push(vector.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint));
let item = builder.finish();
let owned = item.copy_out();
let expected = map_groups_to_legacy(paint.element(0).unwrap());
drop(item);
drop(paint);
drop(source);
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let replayed = owned.replay(&arena).expect("the arena holds the replay");
let run = RunView::<Vector>::new(&replayed).expect("the run holds vector elements");
let served = run.attr::<Fill>(0).expect("the fill replays present");
assert_eq!(map_groups_to_legacy(served.element(0).unwrap()), expected);
}
#[test]
fn an_owned_record_deep_copies_graphic_list_fields() {
let inner_vector = unit_square_at(DVec2::ZERO);
let source = core_types::arena::Arena::new(1 << 16).unwrap();
// SAFETY: the erased native list serves only while `source` is live; the
// deep glue under test replaces its borrows at the copy-out seam.
let paint = unsafe { core_types::record::erase_static(native_group_paint(&inner_vector, &source)) };
let vector = unit_square_at(DVec2::new(4., 4.));
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
let lane = builder.push(vector.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint));
let item = builder.finish();
let layout = item.layout().clone();
let offset = layout.offset_of(Fill::NAME, 0).unwrap();
// SAFETY: the item's lane is a live record of `layout`.
let owned = unsafe { core_types::record::OwnedRecord::copy_out(&layout, item.lanes().get(0).rec()) };
let expected = map_groups_to_legacy(paint.element(0).unwrap());
drop(item);
drop(paint);
drop(source);
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let frames = core_types::record::test_frames(layout.frame_bytes());
let mut slot = frames.claim(&layout);
owned.replay_into(&mut slot, &arena).expect("the arena holds the replay");
// SAFETY: the replay completes the record in the claimed frame.
let value = unsafe { slot.finish() };
// SAFETY: the replay wrote a record of `layout`.
let served = unsafe { layout.rec(&value).read::<Option<&List<Graphic>>>(offset) }.expect("the fill replays present");
assert_eq!(map_groups_to_legacy(served.element(0).unwrap()), expected);
}
/// A `lanes`-long frame promoted out of `transient` into `persistent`, with
/// `fill` written into the paint field of every lane. The frame buffer comes
/// back so it outlives the promote's reads.
fn promote_paint_field(
fill: Option<&List<Graphic<'static>>>,
lanes: usize,
transient: &core_types::arena::Arena,
persistent: &core_types::arena::Arena,
) -> (core_types::record::Layout, core_types::record::MaterializedSpan, Vec<u64>) {
use core_types::record::{Layout, MaterializedSpan, Promotion, element_write, write_field};
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[FieldWrite::of::<Fill>(0)]);
let offset = layout.offset_of(Fill::NAME, 0).unwrap();
let stride = layout.lane_stride();
let mut buffer = vec![0u64; (lanes * stride).div_ceil(8)];
let base = buffer.as_mut_ptr().cast::<u8>();
let bounds = (base as usize, buffer.len() * 8);
for lane in 0..lanes {
// SAFETY: the frame is this layout's, written at the element slot and
// at the paint field's own offset.
unsafe {
base.add(lane * stride).cast::<f64>().write(lane as f64);
write_field::<Option<&List<Graphic<'static>>>>(base.add(lane * stride), offset, fill);
}
}
// SAFETY: the frames hold `lanes` live records of `layout`.
let batch = unsafe { core_types::node::RecordBatch::new(base.cast_const(), lanes, &layout) };
let promotion = Promotion::new(transient, bounds, persistent);
// SAFETY: as above.
let span = unsafe { MaterializedSpan::to_persistent(&batch, &promotion) }.expect("the region holds the promote");
(layout, span, buffer)
}
/// The promoted paint of one lane, at the layout the promote published.
fn promoted_paint<'p>(
span: &core_types::record::MaterializedSpan,
layout: &core_types::record::Layout,
lane: usize,
persistent: &'p core_types::arena::Arena,
) -> &'p List<Graphic<'p>> {
let offset = layout.offset_of(Fill::NAME, 0).unwrap();
let batch = span.batch(persistent, layout).expect("the span resolves in its own region");
// SAFETY: the promote wrote a record of `layout` into every lane.
unsafe { batch.get(lane).rec().read::<Option<&List<Graphic>>>(offset) }.expect("the paint promotes present")
}
#[test]
fn a_promoted_paint_field_shares_persistent_interiors() {
let inner_vector = unit_square_at(DVec2::ZERO);
let transient = core_types::arena::Arena::new(1 << 16).unwrap();
let persistent = core_types::arena::Arena::new(1 << 16).unwrap();
// The interior an upstream promote already published, named by a paint
// list the evaluation parked.
let published = native_group_paint(&inner_vector, &persistent);
let interior = {
let Some(Graphic::Group(group)) = published.element(0) else { panic!("the paint carries a native group") };
group.content.lanes().get(0).rec().ptr()
};
// SAFETY: the list serves only while `persistent` is live, and the
// promote under test replaces every borrow it carries.
let (paint, _) = transient.alloc_sized_keyed(unsafe { core_types::record::erase_static(published) }, 0).unwrap();
let occupied = persistent.occupancy();
let (layout, span, _frames) = promote_paint_field(Some(paint), 1, &transient, &persistent);
let served = promoted_paint(&span, &layout, 0, &persistent);
let Some(Graphic::Group(group)) = served.element(0) else { panic!("the promote keeps the group form") };
assert_eq!(group.content.lanes().get(0).rec().ptr(), interior, "a persistent interior is shared pointer for pointer");
assert!(
persistent.occupancy() - occupied <= layout.frame_bytes() + size_of::<List<Graphic>>() + align_of::<List<Graphic>>(),
"the promote allocated the lane slab and the field's own header, never the owned form of the shared interior"
);
}
#[test]
fn a_group_free_paint_field_moves_its_parked_header() {
let mut transient = core_types::arena::Arena::new(1 << 16).unwrap();
let persistent = core_types::arena::Arena::new(1 << 16).unwrap();
let paint = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::ZERO)));
let heap = {
let Some(Graphic::Vector(vector)) = paint.element(0) else { panic!("the paint carries a vector") };
vector.point_domain.positions().as_ptr()
};
let (paint, _) = transient.alloc_sized_keyed(paint, 0).unwrap();
let (layout, span, _frames) = promote_paint_field(Some(paint), 2, &transient, &persistent);
let served = promoted_paint(&span, &layout, 0, &persistent);
let Some(Graphic::Vector(vector)) = served.element(0) else { panic!("the promote keeps the vector") };
assert_eq!(vector.point_domain.positions().as_ptr(), heap, "the promote moved the header, so the served paint names the pre-promote heap");
assert!(std::ptr::eq(served, promoted_paint(&span, &layout, 1, &persistent)), "a paint two lanes share moves once");
transient.reset();
let served = promoted_paint(&span, &layout, 0, &persistent);
assert!(matches!(served.element(0), Some(Graphic::Vector(_))), "the moved paint survives the transient reset");
}
#[test]
fn a_paint_field_whose_attributes_hold_groups_never_moves() {
let inner_vector = unit_square_at(DVec2::ZERO);
let mut transient = core_types::arena::Arena::new(1 << 16).unwrap();
let persistent = core_types::arena::Arena::new(1 << 16).unwrap();
// Group-free elements, with the resident group hidden in an item attribute.
let native = native_group_paint(&inner_vector, &transient);
let expected = map_groups_to_legacy(native.element(0).unwrap());
let mut paint = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(4., 4.))));
// SAFETY: the erased native list serves only while `transient` is live; the
// promote under test replaces its borrows.
paint.set_attribute::<Option<List<Graphic>>>(Stroke::NAME, 0, Some(unsafe { core_types::record::erase_static(native) }));
let (paint, _) = transient.alloc_sized_keyed(paint, 0).unwrap();
let (layout, span, _frames) = promote_paint_field(Some(paint), 1, &transient, &persistent);
let served = promoted_paint(&span, &layout, 0, &persistent);
let moved = std::ptr::eq(std::ptr::from_ref(served).cast::<u8>(), std::ptr::from_ref(paint).cast::<u8>());
assert!(!moved, "an attribute-held group denies the move, so the promote parks a header of its own");
transient.reset();
let served = promoted_paint(&span, &layout, 0, &persistent);
let held = served.attribute::<Option<List<Graphic>>>(Stroke::NAME, 0).expect("the stroke attribute rides the promoted list");
let held = held.as_ref().expect("the stroke is present");
assert_eq!(map_groups_to_legacy(held.element(0).unwrap()), expected, "the attribute-held group serves from persistent storage after the reset");
}
#[test]
fn a_group_free_paint_field_moves_past_its_attribute_columns() {
let mut transient = core_types::arena::Arena::new(1 << 16).unwrap();
let persistent = core_types::arena::Arena::new(1 << 16).unwrap();
let mut paint = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::ZERO)));
// Columns of a type that cannot hold groups, and a group-capable column
// whose value holds none: neither denies the move.
paint.set_attribute::<f64>("opacity", 0, 0.5);
paint.set_attribute::<Color>("probe:color", 0, Color::BLACK);
paint.set_attribute::<Option<List<Graphic>>>(Stroke::NAME, 0, Some(List::new_from_element(Graphic::Color(Color::WHITE))));
let heap = {
let Some(Graphic::Vector(vector)) = paint.element(0) else { panic!("the paint carries a vector") };
vector.point_domain.positions().as_ptr()
};
let (paint, _) = transient.alloc_sized_keyed(paint, 0).unwrap();
let (layout, span, _frames) = promote_paint_field(Some(paint), 1, &transient, &persistent);
let served = promoted_paint(&span, &layout, 0, &persistent);
let Some(Graphic::Vector(vector)) = served.element(0) else { panic!("the promote keeps the vector") };
assert_eq!(vector.point_domain.positions().as_ptr(), heap, "the promote moved the header, so the served paint names the pre-promote heap");
transient.reset();
let served = promoted_paint(&span, &layout, 0, &persistent);
assert!(matches!(served.element(0), Some(Graphic::Vector(_))), "the moved paint survives the transient reset");
}
#[test]
fn an_owned_record_deep_copies_attribute_held_groups() {
let inner_vector = unit_square_at(DVec2::ZERO);
let source = core_types::arena::Arena::new(1 << 16).unwrap();
let native = native_group_paint(&inner_vector, &source);
let expected = map_groups_to_legacy(native.element(0).unwrap());
// The field's elements are group-free; the group rides an item attribute,
// which the shallow read alone would leave borrowing `source`.
let mut paint = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(2., 2.))));
// SAFETY: the erased native list serves only while `source` is live; the
// deep glue under test replaces its borrows at the copy-out seam.
paint.set_attribute::<Option<List<Graphic>>>(Stroke::NAME, 0, Some(unsafe { core_types::record::erase_static(native) }));
let vector = unit_square_at(DVec2::new(4., 4.));
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
let lane = builder.push(vector.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint));
let item = builder.finish();
let layout = item.layout().clone();
let offset = layout.offset_of(Fill::NAME, 0).unwrap();
// SAFETY: the item's lane is a live record of `layout`.
let owned = unsafe { core_types::record::OwnedRecord::copy_out(&layout, item.lanes().get(0).rec()) };
drop(item);
drop(paint);
drop(source);
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let frames = core_types::record::test_frames(layout.frame_bytes());
let mut slot = frames.claim(&layout);
owned.replay_into(&mut slot, &arena).expect("the arena holds the replay");
// SAFETY: the replay completes the record in the claimed frame.
let value = unsafe { slot.finish() };
// SAFETY: the replay wrote a record of `layout`.
let served = unsafe { layout.rec(&value).read::<Option<&List<Graphic>>>(offset) }.expect("the fill replays present");
let held = served.attribute::<Option<List<Graphic>>>(Stroke::NAME, 0).expect("the stroke attribute rides the replayed list");
let held = held.as_ref().expect("the stroke is present");
assert_eq!(map_groups_to_legacy(held.element(0).unwrap()), expected, "the attribute-held group replayed into the serving arena");
}
}
@@ -0,0 +1,159 @@
//! The legacy bridge: record-backed groups rebuilt as owned legacy lists.
use super::walk::push_lane_paint_into_interiors;
use super::{Graphic, detable_items};
use crate::markers::{ATTR_FILL, ATTR_STROKE};
use core_types::Color;
use core_types::list::{AttributeValueDyn, Item, List};
use raster_types::{CPU, GPU, Raster};
use vector_types::{GradientStops, Vector};
/// One typed run as an owned list, elements cloned and every attribute copied
/// through its erased read. Content keeps its native form; the legacy
/// conversions layer their mapping on top.
pub fn run_to_list<T: Clone + Send + Sync + dyn_any::StaticTypeSized>(item: &core_types::record::GroupItem) -> Option<List<T>> {
let lanes = item.typed_lanes::<T>()?;
let mut list = List::new();
for lane in 0..lanes.len() {
list.push(Item::new_from_element(lanes.element_ref(lane).clone()));
}
for field in &item.layout().fields {
for lane in 0..lanes.len() {
// SAFETY: the offset comes from the item's own layout.
let value = unsafe { (field.read_erased)(item.lanes().get(lane).rec().ptr().add(field.offset)) };
list.set_attribute_value_dyn(field.name, lane, AttributeValueDyn(value));
}
}
Some(list)
}
/// Converts the group content of the list's paint attribute values to legacy
/// form, so a legacy product owns everything its attributes reach.
pub fn map_paint_attrs_to_legacy<T>(list: &mut List<T>) {
for key in [ATTR_FILL, ATTR_STROKE, crate::markers::ATTR_EDITOR_MERGED_LAYERS] {
let Some(values) = list.iter_attribute_values_mut::<Option<List<Graphic>>>(key) else { continue };
for value in values.flatten() {
for element in value.iter_element_values_mut() {
*element = map_groups_to_legacy(element);
}
}
}
}
/// One typed run as a legacy list: [`run_to_list`] with the paint attribute
/// contents converted to their legacy form.
pub(crate) fn run_to_legacy_list<T: Clone + Send + Sync + dyn_any::StaticTypeSized>(item: &core_types::record::GroupItem) -> Option<List<T>> {
let mut list = run_to_list::<T>(item)?;
map_paint_attrs_to_legacy(&mut list);
Some(list)
}
/// The graphic with every `Group` converted to its legacy form.
pub fn map_groups_to_legacy<'out>(graphic: &Graphic<'_>) -> Graphic<'out> {
match graphic {
Graphic::Group(group) => group_to_legacy_graphic(group),
Graphic::Graphic(children) => {
let mut out = List::new();
for item in children.clone().into_iter() {
let (element, attributes) = item.into_parts();
out.push(Item::from_parts(map_groups_to_legacy(&element), attributes));
}
map_paint_attrs_to_legacy(&mut out);
Graphic::Graphic(out)
}
Graphic::Vector(vector) => Graphic::Vector(vector.clone()),
Graphic::RasterCPU(raster) => Graphic::RasterCPU(raster.clone()),
Graphic::RasterGPU(raster) => Graphic::RasterGPU(raster.clone()),
Graphic::Color(color) => Graphic::Color(*color),
Graphic::Gradient(gradient) => Graphic::Gradient(gradient.clone()),
Graphic::Text(text) => Graphic::Text(text.clone()),
}
}
/// The group as one legacy graphic. A bare (row-less) wrap of a single typed
/// run keeps the run's typed variant, matching the `Into<Graphic>` the
/// pre-flip wrap applied; everything else becomes the legacy group list.
pub fn group_to_legacy_graphic(group: &core_types::record::Group) -> Graphic<'static> {
if group.row.is_none() {
let item = &group.content;
let typed = None
.or_else(|| run_to_legacy_list::<Vector>(item).map(|list| detable_items(list, Graphic::Vector)))
.or_else(|| run_to_legacy_list::<Raster<CPU>>(item).map(|list| detable_items(list, Graphic::RasterCPU)))
.or_else(|| run_to_legacy_list::<Raster<GPU>>(item).map(|list| detable_items(list, Graphic::RasterGPU)))
.or_else(|| run_to_legacy_list::<Color>(item).map(|list| detable_items(list, Graphic::Color)))
.or_else(|| run_to_legacy_list::<GradientStops>(item).map(|list| detable_items(list, Graphic::Gradient)))
.or_else(|| run_to_legacy_list::<String>(item).map(|list| detable_items(list, Graphic::Text)));
if let Some(typed) = typed {
return Graphic::Graphic(typed);
}
}
Graphic::Graphic(group_to_legacy_list(group))
}
/// The group as a legacy `List<Graphic>`: a `Graphic` run becomes the items,
/// another typed run becomes one de-tabled leaf item per lane.
pub fn group_to_legacy_list(group: &core_types::record::Group) -> List<Graphic<'static>> {
let item = &group.content;
if let Some(mut list) = run_to_legacy_list::<Graphic>(item) {
for element in list.iter_element_values_mut() {
*element = map_groups_to_legacy(element);
}
push_lane_paint_into_interiors(&mut list);
return list;
}
None.or_else(|| run_to_legacy_list::<Vector>(item).map(|list| detable_items(list, Graphic::Vector)))
.or_else(|| run_to_legacy_list::<Raster<CPU>>(item).map(|list| detable_items(list, Graphic::RasterCPU)))
.or_else(|| run_to_legacy_list::<Raster<GPU>>(item).map(|list| detable_items(list, Graphic::RasterGPU)))
.or_else(|| run_to_legacy_list::<Color>(item).map(|list| detable_items(list, Graphic::Color)))
.or_else(|| run_to_legacy_list::<GradientStops>(item).map(|list| detable_items(list, Graphic::Gradient)))
.or_else(|| run_to_legacy_list::<String>(item).map(|list| detable_items(list, Graphic::Text)))
.unwrap_or_default()
}
#[cfg(test)]
mod run_tests {
use super::*;
use crate::graphic::test_support::{native_group_paint, unit_square_at};
use crate::markers::Fill;
use core_types::attribute::Attribute;
use core_types::record::{FieldWrite, RunBuilder, element_write_hashed};
use glam::DVec2;
#[test]
fn a_legacy_list_owns_its_paint_attr_content() {
let inner_vector = unit_square_at(DVec2::ZERO);
let source = core_types::arena::Arena::new(1 << 16).unwrap();
// SAFETY: the erased native list serves only while `source` is live; the
// deep glue under test replaces its borrows at the copy-out seam.
let paint = unsafe { core_types::record::erase_static(native_group_paint(&inner_vector, &source)) };
let vector = unit_square_at(DVec2::new(4., 4.));
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
let lane = builder.push(vector.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint));
let item = builder.finish();
let legacy = run_to_legacy_list::<Vector>(&item).expect("the run lowers to a legacy vector list");
let expected = map_groups_to_legacy(paint.element(0).unwrap());
drop(item);
drop(paint);
drop(source);
let served = legacy.attribute::<Option<List<Graphic>>>(Fill::NAME, 0).expect("the fill attribute rides the list");
let served = served.as_ref().expect("the fill is present");
assert_eq!(served.element(0).unwrap(), &expected);
}
#[test]
fn a_run_list_keeps_native_group_elements() {
let inner_vector = unit_square_at(DVec2::ZERO);
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let content = native_group_paint(&inner_vector, &arena);
let element = content.element(0).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Graphic>(), &[], 1).unwrap();
builder.push(element.clone()).unwrap();
let item = builder.finish();
let list = run_to_list::<Graphic>(&item).expect("the run holds graphic lanes");
assert!(matches!(list.element(0), Some(Graphic::Group(_))), "the list keeps the native group form");
}
}
@@ -0,0 +1,697 @@
mod glue;
mod legacy;
mod paint;
mod walk;
pub(crate) use glue::{list_contains_groups, map_attribute_groups_to_owned, map_attribute_groups_to_persistent, map_attribute_groups_to_resident};
pub use glue::{map_groups_to_owned, map_groups_to_persistent, map_groups_to_resident};
pub(crate) use legacy::run_to_legacy_list;
pub use legacy::{group_to_legacy_graphic, group_to_legacy_list, map_groups_to_legacy, map_paint_attrs_to_legacy, run_to_list};
pub use paint::{
LanePaint, PaintColumns, PaintOverlay, PaintOverlayColumn, PaintReach, bake_paint_transforms, has_paint, is_paint_present, paint_graphics, set_paint_attribute, set_paint_attribute_at,
vector_can_reduce_to_clip_path,
};
pub use walk::{GraphicLevel, GraphicLevelColumn, RowStep, VectorRow, direct_vector_len, flatten_vector_rows, group_is_empty, lane_attributes, run_lane_attributes, walk_vector_rows};
use walk::{group_all_clipped, group_bounding_box, group_is_fully_transparent, group_is_opaque, group_render_complexity};
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash;
use core_types::list::{Item, List};
use core_types::ops::{FromAnchorPosition, ListConvert};
use core_types::render_complexity::RenderComplexity;
use core_types::uuid::NodeId;
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use raster_types::{CPU, GPU, Raster};
use vector_types::GradientStops;
pub use vector_types::Vector;
/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
/// A leaf holds its element directly; its attributes ride the containing
/// lane. Multi-element content is a [`core_types::record::Group`] run, or
/// transitionally the legacy `Graphic` list.
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny)]
pub enum Graphic<'e> {
Graphic(List<Graphic<'e>>),
Vector(Vector),
RasterCPU(Raster<CPU>),
RasterGPU(Raster<GPU>),
Color(Color),
Gradient(GradientStops),
Text(String),
Group(core_types::record::Group<'e>),
}
impl Default for Graphic<'_> {
fn default() -> Self {
Self::Graphic(List::new())
}
}
/// A typed legacy list as a legacy graphic list: each item de-tables to a
/// leaf element, keeping its attributes on the containing lane.
pub(in crate::graphic) fn detable_items<'e, T: Clone + Send + Sync + 'static>(list: List<T>, leaf: fn(T) -> Graphic<'e>) -> List<Graphic<'e>> {
let mut out = List::new();
for item in list.into_iter() {
let (element, attributes) = item.into_parts();
out.push(Item::from_parts(leaf(element), attributes));
}
out
}
/// The element-space coercion into `Graphic`: a leaf converts in place and a
/// legacy list becomes a native group built over the arena, so the coercion
/// never constructs a legacy interior.
pub trait IntoGraphicElement: Clone + Send + Sync + CacheHash + 'static {
/// `None` reports arena exhaustion.
fn into_graphic_element(self, arena: &core_types::arena::Arena) -> Option<Graphic<'_>>;
}
fn list_group<T: Clone + Send + Sync + CacheHash + PartialEq + dyn_any::StaticTypeSized>(list: List<T>, arena: &core_types::arena::Arena) -> Option<Graphic<'_>>
where
T::Static: Clone + Send + Sync,
{
Some(Graphic::Group(core_types::record::Group {
row: None,
content: core_types::record::GroupItem::from_list(list, arena)?,
}))
}
macro_rules! into_graphic_element {
($($leaf:ident: $element:ty;)*) => {
$(
impl IntoGraphicElement for $element {
fn into_graphic_element(self, _arena: &core_types::arena::Arena) -> Option<Graphic<'_>> {
Some(Graphic::$leaf(self))
}
}
impl IntoGraphicElement for List<$element> {
fn into_graphic_element(self, arena: &core_types::arena::Arena) -> Option<Graphic<'_>> {
list_group(self, arena)
}
}
)*
};
}
into_graphic_element! {
Vector: Vector;
RasterCPU: Raster<CPU>;
RasterGPU: Raster<GPU>;
Color: Color;
Gradient: GradientStops;
Text: String;
}
impl IntoGraphicElement for Graphic<'static> {
fn into_graphic_element(self, _arena: &core_types::arena::Arena) -> Option<Graphic<'_>> {
Some(self)
}
}
impl IntoGraphicElement for List<Graphic<'static>> {
fn into_graphic_element(self, arena: &core_types::arena::Arena) -> Option<Graphic<'_>> {
list_group(self, arena)
}
}
// Vector
impl From<Vector> for Graphic<'_> {
fn from(vector: Vector) -> Self {
Graphic::Vector(vector)
}
}
// Raster<CPU>
impl From<Raster<CPU>> for Graphic<'_> {
fn from(raster: Raster<CPU>) -> Self {
Graphic::RasterCPU(raster)
}
}
// Raster<GPU>
impl From<Raster<GPU>> for Graphic<'_> {
fn from(raster: Raster<GPU>) -> Self {
Graphic::RasterGPU(raster)
}
}
// Color
impl From<Color> for Graphic<'_> {
fn from(color: Color) -> Self {
Graphic::Color(color)
}
}
// Note: List<Color> -> Option<Color> is in gcore (Color is defined there)
// GradientStops
impl From<GradientStops> for Graphic<'_> {
fn from(gradient: GradientStops) -> Self {
Graphic::Gradient(gradient)
}
}
// String
impl From<String> for Graphic<'_> {
fn from(text: String) -> Self {
Graphic::Text(text)
}
}
/// Deeply flattens a `List<Graphic>`, collecting only elements matching a specific variant (extracted by `extract_variant`)
/// and discarding all other non-matching content. Recursion through `Graphic::Graphic` sub-`List`s composes transforms and opacity.
fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic) -> Option<List<T>>) -> List<T> {
fn flatten_recursive<T>(output: &mut List<T>, current_graphic_list: List<Graphic>, extract_variant: fn(Graphic) -> Option<List<T>>, parent_layer_path: Option<&[NodeId]>) {
for current_graphic_item in current_graphic_list.into_iter() {
// Whether the parent carries each attribute: a structural fact (column presence), never a value comparison.
// Flattening composes a parent attribute onto its children only when the parent has it,
// so an absent parent attribute never invents a column the children didn't already have.
let parent_has_transform = current_graphic_item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some();
let parent_has_opacity = current_graphic_item.attribute::<f64>(ATTR_OPACITY).is_some();
let parent_has_fill = current_graphic_item.attribute::<f64>(ATTR_OPACITY_FILL).is_some();
let current_transform: DAffine2 = current_graphic_item.attribute_cloned_or_default(ATTR_TRANSFORM);
let current_opacity: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY, 1.);
let current_fill: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);
let lane_layer_path: Option<Vec<NodeId>> = current_graphic_item.attribute::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH).cloned();
let (element, attributes) = current_graphic_item.into_parts();
match element {
// Compose the parent's transform/opacity/fill onto each child, but only for attributes the parent carries.
// A child lacking one is padded with the composition identity (`1.` for opacity/fill, identity for transform), so composing through it is a no-op.
Graphic::Graphic(mut sub_list) => {
if parent_has_transform {
for v in sub_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
*v = current_transform * *v;
}
}
if parent_has_opacity {
for v in sub_list.iter_attribute_values_mut_or_default::<f64>(ATTR_OPACITY) {
*v *= current_opacity;
}
}
if parent_has_fill {
for v in sub_list.iter_attribute_values_mut_or_default::<f64>(ATTR_OPACITY_FILL) {
*v *= current_fill;
}
}
flatten_recursive(output, sub_list, extract_variant, lane_layer_path.as_deref());
}
// A bridge row's native group flattens through its legacy lowering.
Graphic::Group(group) => {
let lowered = List::new_from_item(Item::from_parts(group_to_legacy_graphic(&group), attributes.clone()));
flatten_recursive(output, lowered, extract_variant, parent_layer_path);
}
// A de-tabled leaf is one attr-less element; the extracted row rides with its containing lane's full attributes, paint included.
// The enclosing group lane's own layer path overrides, one hop only, matching the native walk.
other => {
if let Some(typed_list) = extract_variant(other) {
for item in typed_list.into_iter() {
let mut row = Item::from_parts(item.into_element(), attributes.clone());
if let Some(layer_path) = parent_layer_path {
row.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.to_vec());
}
output.push(row);
}
}
}
}
}
}
let mut output = List::new();
flatten_recursive(&mut output, content, extract_variant, None);
output
}
/// Maps from a concrete element type to its corresponding `Graphic` enum variant,
/// enabling type-directed casting of typed `List`s from a `Graphic` value.
pub trait TryFromGraphic: Clone + Sized {
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>>;
}
impl TryFromGraphic for Vector {
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
if let Graphic::Vector(t) = graphic { Some(List::new_from_element(t)) } else { None }
}
}
impl TryFromGraphic for Raster<CPU> {
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
if let Graphic::RasterCPU(t) = graphic { Some(List::new_from_element(t)) } else { None }
}
}
impl TryFromGraphic for Color {
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
if let Graphic::Color(t) = graphic { Some(List::new_from_element(t)) } else { None }
}
}
impl TryFromGraphic for GradientStops {
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
if let Graphic::Gradient(t) = graphic { Some(List::new_from_element(t)) } else { None }
}
}
impl TryFromGraphic for String {
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
if let Graphic::Text(t) = graphic { Some(List::new_from_element(t)) } else { None }
}
}
// Local trait to convert types to List<Graphic> (avoids orphan rule issues)
pub trait IntoGraphicList: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static {
fn into_graphic_list(self) -> List<Graphic<'static>>;
/// Deeply flattens any content of type `T` within a `List<Graphic>`, discarding all other content, and returning a flat `List<T>`.
fn into_flattened_list<T: TryFromGraphic>(self) -> List<T>
where
Self: std::marker::Sized,
{
flatten_graphic_list(self.into_graphic_list(), T::try_from_graphic)
}
}
impl IntoGraphicList for List<Graphic<'static>> {
fn into_graphic_list(self) -> List<Graphic<'static>> {
self
}
}
impl IntoGraphicList for List<Vector> {
fn into_graphic_list(self) -> List<Graphic<'static>> {
detable_items(self, Graphic::Vector)
}
}
impl IntoGraphicList for List<Raster<CPU>> {
fn into_graphic_list(self) -> List<Graphic<'static>> {
detable_items(self, Graphic::RasterCPU)
}
}
impl IntoGraphicList for List<Raster<GPU>> {
fn into_graphic_list(self) -> List<Graphic<'static>> {
detable_items(self, Graphic::RasterGPU)
}
}
impl IntoGraphicList for List<Color> {
fn into_graphic_list(self) -> List<Graphic<'static>> {
detable_items(self, Graphic::Color)
}
}
impl IntoGraphicList for List<GradientStops> {
fn into_graphic_list(self) -> List<Graphic<'static>> {
detable_items(self, Graphic::Gradient)
}
}
impl IntoGraphicList for List<String> {
fn into_graphic_list(self) -> List<Graphic<'static>> {
detable_items(self, Graphic::Text)
}
}
impl IntoGraphicList for DAffine2 {
fn into_graphic_list(self) -> List<Graphic<'static>> {
List::new_from_element(Graphic::default())
}
}
// DAffine2
impl From<DAffine2> for Graphic<'_> {
fn from(_: DAffine2) -> Self {
Graphic::default()
}
}
// DVec2
impl From<DVec2> for Graphic<'_> {
fn from(position: DVec2) -> Self {
Graphic::Vector(Vector::from_anchor_position(position))
}
}
// Note: List conversions handled by blanket impl in gcore
impl<'e> Graphic<'e> {
pub fn as_graphic(&self) -> Option<&List<Graphic<'_>>> {
match self {
Graphic::Graphic(graphic) => Some(graphic),
_ => None,
}
}
pub fn as_graphic_mut(&mut self) -> Option<&mut List<Graphic<'e>>> {
match self {
Graphic::Graphic(graphic) => Some(graphic),
_ => None,
}
}
pub fn as_vector(&self) -> Option<&Vector> {
match self {
Graphic::Vector(vector) => Some(vector),
_ => None,
}
}
pub fn as_raster(&self) -> Option<&Raster<CPU>> {
match self {
Graphic::RasterCPU(raster) => Some(raster),
_ => None,
}
}
pub fn as_raster_mut(&mut self) -> Option<&mut Raster<CPU>> {
match self {
Graphic::RasterCPU(raster) => Some(raster),
_ => None,
}
}
/// A leaf carries no clipping attribute, which rides its containing lane.
pub fn had_clip_enabled(&self) -> bool {
fn all_clipped<T>(list: &List<T>) -> bool {
list.iter_attribute_values_or_default::<bool>(ATTR_CLIPPING_MASK).all(|clip| clip)
}
match self {
Graphic::Graphic(list) => all_clipped(list),
Graphic::Group(group) => group_all_clipped(group),
_ => false,
}
}
pub fn can_reduce_to_clip_path(&self) -> bool {
match self {
Graphic::Vector(vector) => vector_can_reduce_to_clip_path(&core_types::lane::Single(vector)),
_ => false,
}
}
pub fn is_opaque(&self) -> bool {
match self {
Graphic::Graphic(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque),
// A bare leaf carries no paint attribute, which rides its lane, so
// nothing here claims opacity.
Graphic::Vector(_) => false,
Graphic::Color(color) => color.is_opaque(),
Graphic::Gradient(stops) => stops.iter().all(|stop| stop.color.is_opaque()),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false,
Graphic::Group(group) => group_is_opaque(group),
}
}
pub fn is_fully_transparent(&self) -> bool {
match self {
Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent),
// A bare leaf carries no paint attribute, so only an unstroked
// vector is invisible on its own.
Graphic::Vector(vector) => vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()),
Graphic::Color(color) => color.a() == 0.,
Graphic::Gradient(stops) => stops.iter().all(|stop| stop.color.a() == 0.),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false,
Graphic::Group(group) => group_is_fully_transparent(group),
}
}
/// True if this paint opaquely covers the entire fill region.
/// Vector, Raster, and a nested Graphic may leave gaps, so they return false.
pub fn covers_opaquely(&self) -> bool {
matches!(self, Graphic::Color(_) | Graphic::Gradient(_)) && self.is_opaque()
}
/// Whether the graphic holds no content: a leaf always holds its element.
pub fn is_empty(&self) -> bool {
match self {
Graphic::Graphic(list) => list.is_empty(),
Graphic::Group(group) => group_is_empty(group),
_ => false,
}
}
}
impl BoundingBox for Graphic<'_> {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
match self {
Graphic::Vector(vector) => BoundingBox::bounding_box(vector, transform, include_stroke),
Graphic::RasterCPU(raster) => raster.bounding_box(transform, include_stroke),
Graphic::RasterGPU(raster) => raster.bounding_box(transform, include_stroke),
Graphic::Graphic(list) => list.bounding_box(transform, include_stroke),
Graphic::Color(color) => color.bounding_box(transform, include_stroke),
Graphic::Gradient(gradient) => gradient.bounding_box(transform, include_stroke),
Graphic::Text(text) => text.bounding_box(transform, include_stroke),
Graphic::Group(group) => group_bounding_box(group, transform, include_stroke, false),
}
}
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
match self {
Graphic::Vector(vector) => vector.thumbnail_bounding_box(transform, include_stroke),
Graphic::RasterCPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
Graphic::RasterGPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
Graphic::Graphic(graphic) => graphic.thumbnail_bounding_box(transform, include_stroke),
Graphic::Color(color) => color.thumbnail_bounding_box(transform, include_stroke),
Graphic::Gradient(gradient) => gradient.thumbnail_bounding_box(transform, include_stroke),
Graphic::Text(list) => list.thumbnail_bounding_box(transform, include_stroke),
Graphic::Group(group) => group_bounding_box(group, transform, include_stroke, true),
}
}
}
impl<'e> ListConvert<Graphic<'e>> for Vector {
fn convert_item(self) -> Graphic<'e> {
Graphic::Vector(self)
}
}
impl<'e> ListConvert<Graphic<'e>> for Raster<CPU> {
fn convert_item(self) -> Graphic<'e> {
Graphic::RasterCPU(self)
}
}
impl<'e> ListConvert<Graphic<'e>> for Raster<GPU> {
fn convert_item(self) -> Graphic<'e> {
Graphic::RasterGPU(self)
}
}
impl RenderComplexity for Graphic<'_> {
fn render_complexity(&self) -> usize {
match self {
Self::Graphic(list) => list.render_complexity(),
Self::Vector(list) => list.render_complexity(),
Self::RasterCPU(list) => list.render_complexity(),
Self::RasterGPU(list) => list.render_complexity(),
Self::Color(list) => list.render_complexity(),
Self::Gradient(list) => list.render_complexity(),
Self::Text(list) => list.render_complexity(),
Self::Group(group) => group_render_complexity(group),
}
}
}
// Node definitions moved to graphic-nodes crate
pub trait AtIndex {
type Output;
fn at_index(&self, index: usize) -> Option<Self::Output>;
fn at_index_from_end(&self, index: usize) -> Option<Self::Output>;
}
impl<T: Clone> AtIndex for Vec<T> {
type Output = T;
fn at_index(&self, index: usize) -> Option<Self::Output> {
self.get(index).cloned()
}
fn at_index_from_end(&self, index: usize) -> Option<Self::Output> {
if index == 0 || index > self.len() { None } else { self.get(self.len() - index).cloned() }
}
}
impl<T: Clone> AtIndex for List<T> {
type Output = List<T>;
fn at_index(&self, index: usize) -> Option<Self::Output> {
self.clone_item(index).map(|item| {
let mut result_list = Self::default();
result_list.push(item);
result_list
})
}
fn at_index_from_end(&self, index: usize) -> Option<Self::Output> {
if index == 0 || index > self.len() { None } else { self.at_index(self.len() - index) }
}
}
pub trait OmitIndex {
fn omit_index(&self, index: usize) -> Self;
fn omit_index_from_end(&self, index: usize) -> Self;
}
impl<T: Clone> OmitIndex for Vec<T> {
fn omit_index(&self, index: usize) -> Self {
self.iter().enumerate().filter(|(i, _)| *i != index).map(|(_, v)| v.clone()).collect()
}
fn omit_index_from_end(&self, index: usize) -> Self {
if index == 0 || index > self.len() {
return self.clone();
}
self.omit_index(self.len() - index)
}
}
impl<T: Clone> OmitIndex for List<T> {
fn omit_index(&self, index: usize) -> Self {
let mut result = Self::default();
for i in 0..self.len() {
if i != index
&& let Some(item) = self.clone_item(i)
{
result.push(item);
}
}
result
}
fn omit_index_from_end(&self, index: usize) -> Self {
if index == 0 || index > self.len() {
return self.clone();
}
self.omit_index(self.len() - index)
}
}
#[cfg(test)]
mod tests {
use super::*;
use core_types::list::List;
fn vector_graphic() -> Graphic<'static> {
Graphic::Vector(Vector::default())
}
// Flattening must not invent attribute columns that neither the parent graphic nor the child carried
#[test]
fn flatten_does_not_invent_attributes() {
let graphics = List::new_from_element(vector_graphic());
let flattened: List<Vector> = graphics.into_flattened_list();
for key in [ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, ATTR_EDITOR_LAYER_PATH] {
assert!(!flattened.attribute_keys().any(|k| k == key), "flatten invented the `{key}` attribute");
}
}
// A parent attribute that is present must compose onto the flattened children
#[test]
fn flatten_propagates_present_attributes() {
let mut graphics = List::new_from_element(vector_graphic());
graphics.set_attribute(ATTR_OPACITY, 0, 0.5_f64);
let flattened: List<Vector> = graphics.into_flattened_list();
assert_eq!(flattened.attribute_cloned_or_default::<f64>(ATTR_OPACITY, 0), 0.5);
let mut group = List::new_from_element(Graphic::Graphic(List::new_from_element(vector_graphic())));
group.set_attribute(ATTR_OPACITY, 0, 0.5_f64);
let flattened: List<Vector> = group.into_flattened_list();
assert_eq!(flattened.attribute_cloned_or_default::<f64>(ATTR_OPACITY, 0), 0.5);
}
}
#[cfg(test)]
mod graphic_is_opaque_tests {
use vector_types::GradientStop;
use super::*;
fn color_graphic(alpha: f64) -> Graphic<'static> {
let color = Color::from_rgbaf32(1., 0., 0., alpha as f32).unwrap();
Graphic::Color(color)
}
fn gradient_graphic(gradient: GradientStops) -> Graphic<'static> {
Graphic::Gradient(gradient)
}
#[test]
fn opaque_color_is_opaque() {
let g = color_graphic(1.);
assert!(g.is_opaque());
}
#[test]
fn transparent_color_is_not_opaque() {
let g = color_graphic(0.5);
assert!(!g.is_opaque());
}
#[test]
fn vector_is_not_opaque() {
let g = Graphic::Vector(Vector::default());
assert!(!g.is_opaque());
}
#[test]
fn gradient_with_all_opaque_stops_is_opaque() {
let color_1 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
let color_2 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
let gradient = GradientStops::new(vec![
GradientStop {
position: 0.,
midpoint: 0.5,
color: color_1,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: color_2,
},
]);
let g = gradient_graphic(gradient);
assert!(g.is_opaque());
}
#[test]
fn gradient_with_transparent_stop_is_not_opaque() {
let color_1 = Color::from_rgbaf32(1., 0., 0., 0.5).unwrap();
let color_2 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
let gradient = GradientStops::new(vec![
GradientStop {
position: 0.,
midpoint: 0.5,
color: color_1,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: color_2,
},
]);
let g = gradient_graphic(gradient);
assert!(!g.is_opaque());
}
}
#[cfg(test)]
mod test_support {
use super::Graphic;
use core_types::list::List;
use core_types::record::{RunBuilder, element_write_hashed};
use glam::DVec2;
use vector_types::Vector;
use vector_types::subpath::Subpath;
use vector_types::vector::PointId;
pub(in crate::graphic) fn unit_square_at(corner: DVec2) -> Vector {
Vector::from_subpath(Subpath::<PointId>::new_rectangle(corner, corner + DVec2::ONE))
}
pub(in crate::graphic) fn native_group_paint<'a>(vector: &Vector, arena: &'a core_types::arena::Arena) -> List<Graphic<'a>> {
let mut builder = RunBuilder::new(arena, element_write_hashed::<Vector>(), &[], 1).unwrap();
builder.push(vector.clone()).unwrap();
List::new_from_element(Graphic::Group(core_types::record::Group { row: None, content: builder.finish() }))
}
}
@@ -0,0 +1,272 @@
//! The paint column level: fill and stroke read as lane columns and threaded down to the elements they reach.
use super::{Graphic, IntoGraphicList};
use crate::markers::{ATTR_FILL, ATTR_STROKE, Fill, Stroke};
use core_types::ATTR_TRANSFORM;
use core_types::attribute::{Attribute, Opacity};
use core_types::lane::{LaneColumn, LaneSource};
use core_types::list::{ItemAttributeValues, List};
use glam::DAffine2;
use vector_types::Vector;
/// Whether a normalized paint graphic list actually carries renderable paint.
/// A 0-item list, or a list whose first graphic is empty, is treated as no paint.
pub fn is_paint_present(graphic_list: &List<Graphic>) -> bool {
graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty())
}
/// Look up the paint graphics stored under the marker `A`, in the canonical `List<Graphic>` form.
pub fn paint_graphics<'a, A, S>(source: &'a S, index: usize) -> Option<&'a List<Graphic<'static>>>
where
S: LaneSource,
A: Attribute<Value<'a> = Option<&'a List<Graphic<'static>>>>,
{
source
.attr::<A>(index)
// Treat a blank paint attribute as absent so an empty attribute doesn't count as painted
.filter(|graphic_list| is_paint_present(graphic_list))
}
/// Whether the item carries a non-blank canonical `List<Graphic>` paint under the marker `A`,
/// checked by borrowing without cloning the renderable list.
pub fn has_paint<'a, A, S>(source: &'a S, index: usize) -> bool
where
S: LaneSource,
A: Attribute<Value<'a> = Option<&'a List<Graphic<'static>>>>,
{
paint_graphics::<A, S>(source, index).is_some()
}
/// Whether every lane of a vector source draws as a plain clip path: fully
/// opaque, fill absent or opaque, stroke invisible or fully transparent.
pub fn vector_can_reduce_to_clip_path<S: LaneSource<Element = Vector>>(source: &S) -> bool {
(0..source.lane_count()).all(|index| {
let Some(element) = source.element(index) else { return false };
let opacity: f64 = source.attr::<Opacity>(index);
let fill_opaque_or_absent = paint_graphics::<Fill, _>(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque()));
let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke())
|| paint_graphics::<Stroke, _>(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent
})
}
/// The paint a lane carries for its interiors, in the reference form
/// [`PaintOverlay`] threads down.
#[derive(Clone, Copy, Default)]
pub struct LanePaint<'a> {
pub fill: Option<&'a List<Graphic<'static>>>,
pub stroke: Option<&'a List<Graphic<'static>>>,
}
impl<'a> LanePaint<'a> {
pub const NONE: Self = Self { fill: None, stroke: None };
pub fn is_present(&self) -> bool {
self.fill.is_some() || self.stroke.is_some()
}
}
/// A source's fill and stroke columns, resolved once for per-lane reads.
pub struct PaintColumns<'a, S: LaneSource + 'a> {
fill: S::Column<'a, Fill>,
stroke: S::Column<'a, Stroke>,
}
impl<'a, S: LaneSource> PaintColumns<'a, S> {
pub fn new(source: &'a S) -> Self {
Self {
fill: source.column::<Fill>(),
stroke: source.column::<Stroke>(),
}
}
/// The lane's present, non-blank paint.
pub fn read(&self, lane: usize) -> LanePaint<'a> {
let present = |value: Option<Option<&'a List<Graphic<'static>>>>| value.flatten().filter(|list| is_paint_present(list));
LanePaint {
fill: present(self.fill.try_get(lane)),
stroke: present(self.stroke.try_get(lane)),
}
}
}
/// How far a lane's paint reaches into the element beneath it, mirroring the
/// legacy conversion's paint push: vector interiors directly and vector
/// children of a nested graphic list, one level deep.
#[derive(Clone, Copy)]
pub struct PaintReach<'a> {
pub paint: LanePaint<'a>,
hops: u8,
}
impl<'a> PaintReach<'a> {
pub const NONE: Self = Self { paint: LanePaint::NONE, hops: 0 };
/// The lane's effective reach: an inherited paint stays authoritative
/// (lane paint below a push's origin is inert in the legacy model), an
/// absent one reads the lane's own paint.
pub fn for_lane<S: LaneSource>(self, columns: &PaintColumns<'a, S>, index: usize) -> Self {
match self.paint.is_present() {
true => self,
false => Self { paint: columns.read(index), hops: 2 },
}
}
pub fn applies(&self) -> bool {
self.hops > 0 && self.paint.is_present()
}
/// The reach one graphic nesting level further down.
pub fn nested(self) -> Self {
Self {
paint: self.paint,
hops: self.hops.saturating_sub(1),
}
}
/// The reach entering a group's own graphic run: a spent or absent reach
/// resets so the group's own lane paint applies at its own boundary.
pub fn into_group_graphics(self) -> Self {
match self.applies() {
true => self.nested(),
false => Self::NONE,
}
}
}
/// A source with a lane's paint forced over its fill and stroke columns,
/// reaching the interiors the legacy conversion's paint push reached.
pub struct PaintOverlay<'a, S> {
inner: &'a S,
paint: LanePaint<'a>,
}
impl<'a, S> PaintOverlay<'a, S> {
pub fn new(inner: &'a S, paint: LanePaint<'a>) -> Self {
Self { inner, paint }
}
}
pub struct PaintOverlayColumn<'a, S: LaneSource + 'a, A: Attribute> {
inner: S::Column<'a, A>,
forced: Option<A::Value<'a>>,
}
impl<'a, S: LaneSource, A: Attribute> LaneColumn<'a, A> for PaintOverlayColumn<'a, S, A> {
fn try_get(&self, lane: usize) -> Option<A::Value<'a>> {
match self.forced {
Some(forced) => Some(forced),
None => self.inner.try_get(lane),
}
}
}
/// The forced value for the marker `A`: the lane paint where `A` is this
/// crate's fill or stroke marker, absent otherwise.
fn forced_paint<'a, A: Attribute>(paint: LanePaint<'a>) -> Option<A::Value<'a>> {
let slot = match A::NAME {
name if name == Fill::NAME => paint.fill,
name if name == Stroke::NAME => paint.stroke,
_ => None,
}?;
assert_eq!(
std::any::TypeId::of::<A::Value<'static>>(),
std::any::TypeId::of::<Option<&'static List<Graphic<'static>>>>(),
"attribute `{}` is declared at another value type than this crate's paint form",
A::NAME
);
assert_eq!(
size_of::<A::Value<'a>>(),
size_of::<Option<&'a List<Graphic<'a>>>>(),
"the paint value form must span the marker's value"
);
// SAFETY: the census admits one value type per attribute name, so a `fill` or `stroke` marker carries this crate's `Option<&List<Graphic>>` at the asserted size.
Some(unsafe { std::mem::transmute_copy::<Option<&'a List<Graphic>>, A::Value<'a>>(&Some(slot)) })
}
impl<'a, S: LaneSource> LaneSource for PaintOverlay<'a, S> {
type Element = S::Element;
type Column<'b, A: Attribute>
= PaintOverlayColumn<'b, S, A>
where
Self: 'b;
fn lane_count(&self) -> usize {
self.inner.lane_count()
}
fn element(&self, lane: usize) -> Option<&S::Element> {
self.inner.element(lane)
}
fn column<A: Attribute>(&self) -> PaintOverlayColumn<'_, S, A> {
PaintOverlayColumn {
inner: self.inner.column::<A>(),
forced: forced_paint::<A>(self.paint),
}
}
}
/// Stores a paint attribute in the paint marker's owned form, the only representation paint readers accept.
pub fn set_paint_attribute(attributes: &mut ItemAttributeValues, key: &str, paint: impl IntoGraphicList) {
attributes.insert(key, Some(paint.into_graphic_list()));
}
/// Stores a paint attribute at a list index in the paint marker's owned form, the only representation paint readers accept.
pub fn set_paint_attribute_at<T>(list: &mut List<T>, index: usize, key: &str, paint: impl IntoGraphicList) {
list.set_attribute(key, index, Some(paint.into_graphic_list()));
}
/// Bake the provided transform into the per-item transforms of the paint graphics stored under the
/// canonical `List<Graphic>` fill and stroke attributes.
pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) {
fn bake_graphic_paint_transform(graphics: &mut List<Graphic>, transform: DAffine2) {
for item_transform in graphics.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
*item_transform = transform * *item_transform;
}
for graphic in graphics.iter_element_values_mut() {
if let Graphic::Graphic(list) = graphic {
bake_graphic_paint_transform(list, transform);
}
}
}
for paint_key in [ATTR_FILL, ATTR_STROKE] {
if let Some(Some(graphics)) = attributes.get_mut::<Option<List<Graphic>>>(paint_key) {
bake_graphic_paint_transform(graphics, transform);
}
}
}
#[cfg(test)]
mod run_tests {
use super::*;
use crate::graphic::run_to_legacy_list;
use crate::graphic::test_support::unit_square_at;
use core_types::Color;
use core_types::record::{FieldWrite, RunBuilder, RunView, element_write_hashed};
use glam::DVec2;
#[test]
fn a_run_serves_the_parked_paint_reference() {
let paint = List::new_from_element(Graphic::Color(Color::BLACK));
let vector = unit_square_at(DVec2::ZERO);
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
let lane = builder.push(vector.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint));
let item = builder.finish();
let run = RunView::<Vector>::new(&item).expect("the run holds vector elements");
assert_eq!(run.attr::<Fill>(0), Some(&paint));
assert_eq!(paint_graphics::<Fill, _>(&run, 0), Some(&paint));
assert_eq!(paint_graphics::<Stroke, _>(&run, 0), None);
let legacy = run_to_legacy_list::<Vector>(&item).expect("the run lowers to a legacy vector list");
assert_eq!(paint_graphics::<Fill, _>(&legacy, 0), paint_graphics::<Fill, _>(&run, 0));
}
}
@@ -0,0 +1,572 @@
//! The native-content walk: vector rows reached through a graphic's own storage, with no legacy conversion.
use super::Graphic;
use super::paint::{LanePaint, PaintColumns, PaintReach, is_paint_present, paint_graphics, set_paint_attribute_at};
use crate::markers::{ATTR_FILL, ATTR_STROKE, Fill};
use core_types::attribute::{Attribute, ClippingMask, EditorLayerPath, Opacity, OpacityFill, Transform};
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::lane::LaneSource;
use core_types::list::{AttributeValueDyn, Item, ItemAttributeValues, List};
use core_types::record::FieldOffset;
use core_types::render_complexity::RenderComplexity;
use core_types::uuid::NodeId;
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
use glam::{DAffine2, DVec2};
use raster_types::{CPU, GPU, Raster};
use vector_types::{GradientStops, Vector};
/// One run's attribute tokens, minted once so the lane loops read at an offset.
struct RunAttrs {
transform: Option<FieldOffset<Transform>>,
opacity: Option<FieldOffset<Opacity>>,
opacity_fill: Option<FieldOffset<OpacityFill>>,
clipping_mask: Option<FieldOffset<ClippingMask>>,
}
impl RunAttrs {
fn of(item: &core_types::record::GroupItem) -> Self {
let layout = item.layout();
Self {
transform: FieldOffset::of(layout, 0),
opacity: FieldOffset::of(layout, 0),
opacity_fill: FieldOffset::of(layout, 0),
clipping_mask: FieldOffset::of(layout, 0),
}
}
fn read_or<'i, A: Attribute>(item: &'i core_types::record::GroupItem, field: Option<FieldOffset<A>>, lane: usize, default: A::Value<'i>) -> A::Value<'i> {
match field.and_then(|field| item.lanes().get(lane).try_attr_at(field)) {
Some(value) => value,
None => default,
}
}
}
pub fn group_is_empty(group: &core_types::record::Group) -> bool {
group.content.is_empty()
}
pub(in crate::graphic) fn group_all_clipped(group: &core_types::record::Group) -> bool {
let item = &group.content;
let attrs = RunAttrs::of(item);
(0..item.len()).all(|lane| RunAttrs::read_or(item, attrs.clipping_mask, lane, false))
}
pub(in crate::graphic) fn group_is_opaque(group: &core_types::record::Group) -> bool {
let item = &group.content;
let attrs = RunAttrs::of(item);
let lanes = item.typed_lanes::<Graphic>();
!item.is_empty()
&& (0..item.len()).all(|lane| {
RunAttrs::read_or(item, attrs.opacity, lane, 1.) >= 1.
&& RunAttrs::read_or(item, attrs.opacity_fill, lane, 1.) >= 1.
&& lanes.as_ref().is_some_and(|lanes| lanes.element_ref(lane).is_opaque())
})
}
pub(in crate::graphic) fn group_is_fully_transparent(group: &core_types::record::Group) -> bool {
let item = &group.content;
let attrs = RunAttrs::of(item);
let lanes = item.typed_lanes::<Graphic>();
(0..item.len()).all(|lane| RunAttrs::read_or(item, attrs.opacity, lane, 1.) <= 0. || lanes.as_ref().is_some_and(|lanes| lanes.element_ref(lane).is_fully_transparent()))
}
pub(in crate::graphic) fn group_bounding_box(group: &core_types::record::Group, transform: DAffine2, include_stroke: bool, thumbnail: bool) -> RenderBoundingBox {
fn combine(combined: &mut Option<[DVec2; 2]>, any_infinite: &mut bool, bounds: RenderBoundingBox, thumbnail: bool) -> Option<RenderBoundingBox> {
match bounds {
RenderBoundingBox::None => None,
RenderBoundingBox::Infinite if thumbnail => {
*any_infinite = true;
None
}
RenderBoundingBox::Infinite => Some(RenderBoundingBox::Infinite),
RenderBoundingBox::Rectangle(bounds) => {
*combined = Some(match *combined {
Some(existing) => core_types::math::quad::Quad::combine_bounds(existing, bounds),
None => bounds,
});
None
}
}
}
fn typed_run<T: dyn_any::StaticTypeSized + BoundingBox>(item: &core_types::record::GroupItem, transform: DAffine2, include_stroke: bool, thumbnail: bool) -> Option<RenderBoundingBox> {
let lanes = item.typed_lanes::<T>()?;
let transform_offset = RunAttrs::of(item).transform;
let mut combined = None;
let mut any_infinite = false;
for lane in 0..lanes.len() {
let lane_transform = transform * RunAttrs::read_or(item, transform_offset, lane, DAffine2::IDENTITY);
let element = lanes.element_ref(lane);
let bounds = match thumbnail {
true => element.thumbnail_bounding_box(lane_transform, include_stroke),
false => element.bounding_box(lane_transform, include_stroke),
};
if let Some(short_circuit) = combine(&mut combined, &mut any_infinite, bounds, thumbnail) {
return Some(short_circuit);
}
}
Some(match (combined, any_infinite) {
(Some(bounds), _) => RenderBoundingBox::Rectangle(bounds),
(None, true) => RenderBoundingBox::Infinite,
(None, false) => RenderBoundingBox::None,
})
}
fn run_bounding_box(item: &core_types::record::GroupItem, transform: DAffine2, include_stroke: bool, thumbnail: bool) -> RenderBoundingBox {
None.or_else(|| typed_run::<Graphic>(item, transform, include_stroke, thumbnail))
.or_else(|| typed_run::<Vector>(item, transform, include_stroke, thumbnail))
.or_else(|| typed_run::<Raster<CPU>>(item, transform, include_stroke, thumbnail))
.or_else(|| typed_run::<Raster<GPU>>(item, transform, include_stroke, thumbnail))
.or_else(|| typed_run::<Color>(item, transform, include_stroke, thumbnail))
.or_else(|| typed_run::<GradientStops>(item, transform, include_stroke, thumbnail))
.or_else(|| typed_run::<String>(item, transform, include_stroke, thumbnail))
.unwrap_or(RenderBoundingBox::Infinite)
}
run_bounding_box(&group.content, transform, include_stroke, thumbnail)
}
/// One step of the vector-row walk: continue to the next row or stop early.
pub enum RowStep {
Continue,
Stop,
}
/// The ancestor composition a flattened row inherits: transform, opacity and
/// fill opacity multiply down, each composing only where some ancestor
/// carries the attribute, matching the legacy flatten.
#[derive(Clone, Copy)]
struct FlattenScale {
has_transform: bool,
transform: DAffine2,
has_opacity: bool,
opacity: f64,
has_fill_opacity: bool,
fill_opacity: f64,
}
impl FlattenScale {
const ROOT: Self = Self {
has_transform: false,
transform: DAffine2::IDENTITY,
has_opacity: false,
opacity: 1.,
has_fill_opacity: false,
fill_opacity: 1.,
};
fn composed<S: LaneSource>(self, source: &S, lane: usize) -> Self {
let transform = source.try_attr::<Transform>(lane);
let opacity = source.try_attr::<Opacity>(lane);
let fill_opacity = source.try_attr::<OpacityFill>(lane);
Self {
has_transform: self.has_transform || transform.is_some(),
transform: self.transform * transform.unwrap_or(DAffine2::IDENTITY),
has_opacity: self.has_opacity || opacity.is_some(),
opacity: self.opacity * opacity.unwrap_or(1.),
has_fill_opacity: self.has_fill_opacity || fill_opacity.is_some(),
fill_opacity: self.fill_opacity * fill_opacity.unwrap_or(1.),
}
}
}
/// A graphic level in either of its two storages, as one lane source.
#[derive(Clone, Copy)]
pub enum GraphicLevel<'a> {
Legacy(&'a List<Graphic<'a>>),
Run(&'a core_types::record::GroupItem<'a>),
}
pub enum GraphicLevelColumn<'a, A: Attribute> {
Legacy(core_types::list::ListColumn<'a, A>),
Run(core_types::record::RunColumn<'a, A>),
}
impl<'a, A: Attribute> core_types::lane::LaneColumn<'a, A> for GraphicLevelColumn<'a, A> {
fn try_get(&self, lane: usize) -> Option<A::Value<'a>> {
match self {
GraphicLevelColumn::Legacy(column) => column.try_get(lane),
GraphicLevelColumn::Run(column) => column.try_get(lane),
}
}
}
impl<'a> LaneSource for GraphicLevel<'a> {
type Element = Graphic<'a>;
type Column<'b, A: Attribute>
= GraphicLevelColumn<'b, A>
where
Self: 'b;
fn lane_count(&self) -> usize {
match self {
GraphicLevel::Legacy(list) => list.len(),
GraphicLevel::Run(item) => item.len(),
}
}
fn element(&self, lane: usize) -> Option<&Graphic<'a>> {
match self {
GraphicLevel::Legacy(list) => list.element(lane),
GraphicLevel::Run(item) => {
let lanes = item.typed_lanes::<Graphic>()?;
if lane >= lanes.len() {
return None;
}
// SAFETY: the layout records the element type, and a parked
// element stores its reference at offset 0.
Some(unsafe { core_types::record::borrow_element::<Graphic>(item.lanes().get(lane).rec()) })
}
}
}
fn column<A: Attribute>(&self) -> GraphicLevelColumn<'_, A> {
match self {
GraphicLevel::Legacy(list) => GraphicLevelColumn::Legacy(list.column::<A>()),
GraphicLevel::Run(item) => GraphicLevelColumn::Run(core_types::record::RunColumn::of(item)),
}
}
}
/// The lane's attributes as an owned set, read through the erased glue.
pub fn run_lane_attributes(item: &core_types::record::GroupItem, lane: usize) -> ItemAttributeValues {
let mut scratch: List<Vector> = List::new_from_element(Vector::default());
for field in &item.layout().fields {
// SAFETY: the offset comes from the item's own layout.
let value = unsafe { (field.read_erased)(item.lanes().get(lane).rec().ptr().add(field.offset)) };
scratch.set_attribute_value_dyn(field.name, 0, AttributeValueDyn(value));
}
scratch.clone_item_attributes(0)
}
/// The lane's attributes as an owned set, from either level storage.
pub fn lane_attributes(level: GraphicLevel<'_>, lane: usize) -> ItemAttributeValues {
match level {
GraphicLevel::Legacy(list) => list.clone_item_attributes(lane),
GraphicLevel::Run(item) => run_lane_attributes(item, lane),
}
}
/// One flattened vector row served by [`walk_vector_rows`]: cheap probes
/// first, the full row built on demand.
pub struct VectorRow<'w> {
source: RowSourceRef<'w>,
scale: FlattenScale,
layer_path: Option<&'w [NodeId]>,
paint: LanePaint<'w>,
}
enum RowSourceRef<'w> {
/// A de-tabled vector leaf on a graphic lane: the lane is the row.
Lane(GraphicLevel<'w>, usize),
/// A lane of a vector run.
Run(&'w core_types::record::RunView<'w, Vector>, &'w core_types::record::GroupItem<'w>, usize),
}
impl VectorRow<'_> {
/// The row's vector, borrowed.
pub fn element(&self) -> &Vector {
match &self.source {
RowSourceRef::Lane(level, index) => level.element(*index).and_then(Graphic::as_vector).expect("the walk visits vector lanes"),
RowSourceRef::Run(run, _, index) => LaneSource::element(*run, *index).expect("the walk visits held lanes"),
}
}
/// Whether the built row will carry fill paint: the reaching lane paint,
/// else the row's own.
pub fn has_fill(&self) -> bool {
if self.paint.fill.is_some() {
return true;
}
match &self.source {
RowSourceRef::Lane(level, index) => paint_graphics::<Fill, _>(level, *index).is_some(),
RowSourceRef::Run(run, _, index) => paint_graphics::<Fill, _>(*run, *index).is_some(),
}
}
/// Builds the row at the end of `out`, applying the reach paint and the
/// inherited composition.
pub fn build_into(&self, out: &mut List<Vector>) {
let index = out.len();
match &self.source {
RowSourceRef::Lane(level, lane) => {
let vector = self.element().clone();
out.push(Item::from_parts(vector, lane_attributes(*level, *lane)));
}
RowSourceRef::Run(run, item, lane) => {
let vector = LaneSource::element(*run, *lane).expect("the walk visits held lanes").clone();
out.push(Item::from_parts(vector, run_lane_attributes(item, *lane)));
}
}
for (key, slot) in [(ATTR_FILL, self.paint.fill), (ATTR_STROKE, self.paint.stroke)] {
if let Some(paint) = slot {
set_paint_attribute_at(out, index, key, paint.clone());
}
}
if self.scale.has_transform || out.attribute::<DAffine2>(ATTR_TRANSFORM, index).is_some() {
let row_transform: DAffine2 = out.attribute_cloned_or_default(ATTR_TRANSFORM, index);
out.set_attribute(ATTR_TRANSFORM, index, self.scale.transform * row_transform);
}
if self.scale.has_opacity || out.attribute::<f64>(ATTR_OPACITY, index).is_some() {
let row_opacity: f64 = out.attribute_cloned_or(ATTR_OPACITY, index, 1.);
out.set_attribute(ATTR_OPACITY, index, self.scale.opacity * row_opacity);
}
if self.scale.has_fill_opacity || out.attribute::<f64>(ATTR_OPACITY_FILL, index).is_some() {
let row_fill: f64 = out.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
out.set_attribute(ATTR_OPACITY_FILL, index, self.scale.fill_opacity * row_fill);
}
if let Some(layer_path) = self.layer_path {
out.set_attribute(ATTR_EDITOR_LAYER_PATH, index, layer_path.to_vec());
}
}
}
fn walk_rows_of_run(item: &core_types::record::GroupItem, scale: FlattenScale, layer_path: Option<&[NodeId]>, paint: LanePaint<'_>, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep) -> RowStep {
let Some(run) = core_types::record::RunView::<Vector>::new(item) else {
return RowStep::Continue;
};
for lane in 0..item.len() {
if let RowStep::Stop = visit(VectorRow {
source: RowSourceRef::Run(&run, item, lane),
scale,
layer_path,
paint,
}) {
return RowStep::Stop;
}
}
RowStep::Continue
}
/// Walks a graphic level into its flattened vector rows, matching the legacy
/// push-then-flatten lowering: lane paint threads with [`PaintReach`],
/// ancestor transform, opacity and fill opacity compose down, the containing
/// level's parent layer path overwrites its rows, and non-vector content is
/// discarded. A de-tabled leaf's row is its lane, attributes included.
pub fn walk_vector_rows(level: GraphicLevel<'_>, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep) {
walk_vector_rows_impl(level, FlattenScale::ROOT, None, PaintReach::NONE, visit);
}
fn walk_vector_rows_impl<'a>(
level: GraphicLevel<'a>,
scale: FlattenScale,
parent_layer_path: Option<&'a [NodeId]>,
inherited: PaintReach<'a>,
visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep,
) -> RowStep {
if let GraphicLevel::Run(item) = level {
// A vector-typed run is already its rows.
if item.typed_lanes::<Vector>().is_some() {
let paint = match inherited.applies() {
true => inherited.paint,
false => LanePaint::NONE,
};
return walk_rows_of_run(item, scale, parent_layer_path, paint, visit);
}
}
let columns = PaintColumns::new(&level);
for index in 0..level.lane_count() {
let Some(element) = level.element(index) else { continue };
let reach = inherited.for_lane(&columns, index);
let row_paint = match reach.applies() {
true => reach.paint,
false => LanePaint::NONE,
};
let step = match element {
Graphic::Vector(_) => visit(VectorRow {
source: RowSourceRef::Lane(level, index),
scale,
layer_path: parent_layer_path,
paint: row_paint,
}),
Graphic::Graphic(children) => walk_vector_rows_impl(
GraphicLevel::Legacy(children),
scale.composed(&level, index),
level.try_attr::<EditorLayerPath>(index),
reach.nested(),
visit,
),
Graphic::Group(group) => {
let item = &group.content;
if item.typed_lanes::<Vector>().is_some() {
walk_rows_of_run(item, scale.composed(&level, index), level.try_attr::<EditorLayerPath>(index), row_paint, visit)
} else if item.typed_lanes::<Graphic>().is_some() {
walk_vector_rows_impl(
GraphicLevel::Run(item),
scale.composed(&level, index),
level.try_attr::<EditorLayerPath>(index),
reach.into_group_graphics(),
visit,
)
} else {
RowStep::Continue
}
}
_ => RowStep::Continue,
};
if let RowStep::Stop = step {
return RowStep::Stop;
}
}
RowStep::Continue
}
/// The level's flattened vector rows as one owned list, the walk's collect
/// form.
pub fn flatten_vector_rows(level: GraphicLevel<'_>) -> List<Vector> {
let mut out = List::new();
walk_vector_rows(level, &mut |row| {
row.build_into(&mut out);
RowStep::Continue
});
out
}
/// The transitional paint placement: a lane-level fill or stroke paint
/// attribute moves onto the vector interiors the legacy paint readers
/// inspect, reaching as far as the pre-flip broadcast did.
pub(in crate::graphic) fn push_lane_paint_into_interiors(list: &mut List<Graphic>) {
for index in 0..list.len() {
for key in [ATTR_FILL, ATTR_STROKE] {
let stored = list.attribute::<Option<List<Graphic>>>(key, index).and_then(|optional| optional.as_ref());
let Some(paint) = stored.filter(|paint| is_paint_present(paint)).cloned() else {
continue;
};
let Some(Graphic::Graphic(children)) = list.element_mut(index) else { continue };
for child in 0..children.len() {
if matches!(children.element(child), Some(Graphic::Vector(_))) {
set_paint_attribute_at(children, child, key, paint.clone());
}
}
}
}
}
/// The count [`map_groups_to_legacy`] would expose through [`Graphic::as_vector`],
/// read from the run's lanes instead of materializing the legacy list. Mirrors
/// [`group_to_legacy_graphic`]'s typed-run path, where `Vector` is tried first.
pub fn direct_vector_len(graphic: &Graphic) -> usize {
match graphic {
Graphic::Vector(_) => 1,
Graphic::Group(group) => match &group.row {
None => group.content.typed_lanes::<Vector>().map_or(0, |lanes| lanes.len()),
_ => 0,
},
_ => 0,
}
}
pub(in crate::graphic) fn group_render_complexity(group: &core_types::record::Group) -> usize {
fn typed_run<T: dyn_any::StaticTypeSized + RenderComplexity>(item: &core_types::record::GroupItem) -> Option<usize> {
let lanes = item.typed_lanes::<T>()?;
Some((0..lanes.len()).map(|lane| lanes.element_ref(lane).render_complexity()).sum())
}
let item = &group.content;
None.or_else(|| typed_run::<Graphic>(item))
.or_else(|| typed_run::<Vector>(item))
.or_else(|| typed_run::<Raster<CPU>>(item))
.or_else(|| typed_run::<Raster<GPU>>(item))
.or_else(|| typed_run::<Color>(item))
.or_else(|| typed_run::<GradientStops>(item))
.or_else(|| typed_run::<String>(item))
.unwrap_or(item.len())
}
#[cfg(test)]
mod run_tests {
use super::*;
use crate::graphic::test_support::unit_square_at;
use crate::graphic::{IntoGraphicList, map_groups_to_legacy, run_to_legacy_list};
use core_types::record::{FieldWrite, RunBuilder, RunView, element_write_hashed};
#[test]
fn the_vector_row_walk_matches_the_legacy_flatten() {
let inner_vector = unit_square_at(DVec2::ZERO);
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[], 1).unwrap();
builder.push(inner_vector.clone()).unwrap();
let inner_item = builder.finish();
let mut painted = List::new();
painted.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::ZERO))));
painted.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::ONE))));
painted.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(1., 0.)));
painted.set_attribute(core_types::ATTR_TRANSFORM, 1, DAffine2::from_translation(DVec2::new(0., 1.)));
set_paint_attribute_at(&mut painted, 1, ATTR_FILL, List::new_from_element(Graphic::Color(Color::WHITE)));
let mut nested = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(2., 2.))));
nested.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_scale(DVec2::splat(2.)));
let mut top = List::new();
top.push(Item::new_from_element(Graphic::Graphic(painted)));
top.push(Item::new_from_element(Graphic::Graphic(nested)));
top.push(Item::new_from_element(Graphic::Group(core_types::record::Group { row: None, content: inner_item })));
top.push(Item::new_from_element(Graphic::Color(Color::BLACK)));
top.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(6., 0.)))));
top.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(5., 5.)));
top.set_attribute(core_types::ATTR_EDITOR_LAYER_PATH, 0, vec![core_types::uuid::NodeId(7)]);
set_paint_attribute_at(&mut top, 0, ATTR_FILL, List::new_from_element(Graphic::Color(Color::BLACK)));
top.set_attribute(core_types::ATTR_OPACITY, 1, 0.5);
top.set_attribute(core_types::ATTR_TRANSFORM, 2, DAffine2::from_scale(DVec2::splat(3.)));
top.set_attribute(core_types::ATTR_TRANSFORM, 4, DAffine2::from_translation(DVec2::new(0., 7.)));
top.set_attribute(core_types::ATTR_EDITOR_LAYER_PATH, 4, vec![core_types::uuid::NodeId(9)]);
set_paint_attribute_at(&mut top, 4, ATTR_FILL, List::new_from_element(Graphic::Color(Color::WHITE)));
let legacy = {
let mut list = List::new();
for item in top.clone().into_iter() {
let (element, attributes) = item.into_parts();
list.push(Item::from_parts(map_groups_to_legacy(&element), attributes));
}
push_lane_paint_into_interiors(&mut list);
list.into_flattened_list::<Vector>()
};
let native = flatten_vector_rows(GraphicLevel::Legacy(&top));
assert_eq!(native.len(), legacy.len());
for row in 0..native.len() {
assert_eq!(
native.attribute::<DAffine2>(core_types::ATTR_TRANSFORM, row),
legacy.attribute::<DAffine2>(core_types::ATTR_TRANSFORM, row),
"transform, row {row}"
);
assert_eq!(
native.attribute::<f64>(core_types::ATTR_OPACITY, row),
legacy.attribute::<f64>(core_types::ATTR_OPACITY, row),
"opacity, row {row}"
);
assert_eq!(
native.attribute::<Vec<core_types::uuid::NodeId>>(core_types::ATTR_EDITOR_LAYER_PATH, row),
legacy.attribute::<Vec<core_types::uuid::NodeId>>(core_types::ATTR_EDITOR_LAYER_PATH, row),
"layer path, row {row}"
);
assert_eq!(
native.attribute::<Option<List<Graphic>>>(ATTR_FILL, row),
legacy.attribute::<Option<List<Graphic>>>(ATTR_FILL, row),
"fill, row {row}"
);
}
assert_eq!(native, legacy);
}
#[test]
fn a_run_and_its_legacy_list_agree_on_bounding_boxes() {
let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(4., 4.))];
let transforms = [DAffine2::from_translation(DVec2::new(1., 2.)), DAffine2::from_scale(DVec2::splat(3.))];
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<core_types::attribute::Transform>(0)], 2).unwrap();
for (vector, transform) in vectors.iter().zip(transforms) {
let lane = builder.push(vector.clone()).unwrap();
builder.attr::<core_types::attribute::Transform>(lane, transform);
}
let item = builder.finish();
let run = RunView::<Vector>::new(&item).expect("the run holds vector elements");
let legacy = run_to_legacy_list::<Vector>(&item).expect("the run lowers to a legacy vector list");
let outer = DAffine2::from_angle(0.3);
for include_stroke in [false, true] {
let bounds = run.bounding_box(outer, include_stroke);
assert_eq!(bounds, legacy.bounding_box(outer, include_stroke));
assert!(matches!(bounds, RenderBoundingBox::Rectangle(_)));
assert_eq!(run.thumbnail_bounding_box(outer, include_stroke), legacy.thumbnail_bounding_box(outer, include_stroke));
}
}
}
@@ -0,0 +1,187 @@
pub mod artboard;
pub mod boundary;
pub mod graphic;
pub mod markers;
// Re-export all transitive dependencies so downstream crates only need to depend on graphic-types
pub use core_types;
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, IntoGraphicList, TryFromGraphic, Vector};
pub use markers::{ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, ATTR_STROKE};
pub mod migrations {
use crate::Vector;
// Storing legacy structs that are only used in document migration.
// TODO: Eventually remove this migration document upgrade code
pub mod legacy {
use core_types::Color;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use vector_types::vector::{PointDomain, RegionDomain, SegmentDomain, misc::HandleId, style::Stroke};
use vector_types::{Gradient, Vector, vector};
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
pub struct LegacyGradient {
pub stops: Gradient,
pub gradient_type: vector::style::GradientType,
pub start: DVec2,
pub end: DVec2,
#[serde(default)]
pub spread_method: vector::style::GradientSpreadMethod,
#[serde(default)]
pub absolute: bool,
#[serde(default)]
pub transform: DAffine2,
}
impl LegacyGradient {
/// Converts a legacy bounding-box-relative gradient (`start`/`end` in [0,1]) into an absolute one in the geometry's local space.
/// `bounding_box` maps [0,1] onto the geometry's bounding box; `layer_transform` is the layer's own transform,
/// used to bake the elliptical adjustment that reproduces the legacy isotropic radial through a non-uniform layer.
pub fn to_absolute(&self, bounding_box: DAffine2, layer_transform: DAffine2) -> LegacyGradient {
let start = bounding_box.transform_point2(self.start);
let end = bounding_box.transform_point2(self.end);
let direction = end - start;
// The legacy radial drew as a circle in the layer's own space; bake the adjustment that, composed with the
// endpoint frame, makes the new pipeline reproduce that circle through the (possibly non-uniform) layer transform.
let radial_invertible = self.gradient_type == vector::style::GradientType::Radial
&& layer_transform.is_finite()
&& layer_transform.matrix2.determinant().recip().is_finite()
&& direction.length_squared() > 1e-20;
let transform = if radial_invertible {
let radius = (layer_transform.matrix2 * direction).length();
let circle = DAffine2 {
matrix2: glam::DMat2::from_diagonal(DVec2::splat(radius)),
translation: layer_transform.transform_point2(start),
};
let base = DAffine2::from_cols(direction, direction.perp(), start);
(layer_transform.inverse() * circle) * base.inverse()
} else {
DAffine2::IDENTITY
};
LegacyGradient {
start,
end,
transform,
absolute: true,
..self.clone()
}
}
/// Builds the affine that places the gradient endpoints at `start` and `end` when applied to canonical gradient space (0, 0) -> (1, 0).
pub fn to_transform(&self) -> DAffine2 {
let direction = self.end - self.start;
DAffine2::from_cols(direction, direction.perp(), self.start)
}
}
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
pub enum LegacyFill {
#[default]
None,
Solid(Color),
Gradient(LegacyGradient),
}
/// The legacy `fill` field is intentionally omitted because vector payload migration only
/// recovers editable vector data. The fill/stroke paints are migrated from the node inputs.
#[derive(serde::Deserialize)]
#[cfg_attr(test, derive(Default, serde::Serialize))]
pub(super) struct PathStyle {
pub stroke: Option<Stroke>,
}
/// Old documents stored a `Vector` flattened with list attributes (`transform`, `alpha_blending`, `upstream_graphic_group`); only the geometry fields are recovered.
#[derive(serde::Deserialize)]
#[cfg_attr(test, derive(Default, serde::Serialize))]
pub(super) struct VectorData {
pub style: PathStyle,
pub colinear_manipulators: Vec<[HandleId; 2]>,
pub point_domain: PointDomain,
pub segment_domain: SegmentDomain,
pub region_domain: RegionDomain,
}
#[derive(serde::Deserialize)]
pub(super) struct Table {
#[serde(alias = "instances", alias = "instance")]
pub element: Vec<Vector>,
}
}
// TODO: Eventually remove this migration document upgrade code
/// Returns the first `Vector` recovered from any of the legacy on-disk shapes (the legacy `VectorData` flat struct, a single `Vector`, or any of the historical `List<Vector>` variants).
pub fn migrate_to_optional_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<Vector>, D::Error> {
use serde::Deserialize;
#[derive(serde::Deserialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
enum VectorFormat {
// Old vector data must be tried first. Serde would otherwise ignore its `style` field and
// deserialize the missing optional `stroke` field as `None` in the current `Vector`.
OldVectorData(legacy::VectorData),
Vector(Vector),
List(legacy::Table),
}
Ok(match VectorFormat::deserialize(deserializer)? {
VectorFormat::OldVectorData(old) => Some(Vector {
stroke: old.style.stroke,
colinear_manipulators: old.colinear_manipulators,
point_domain: old.point_domain,
segment_domain: old.segment_domain,
region_domain: old.region_domain,
}),
VectorFormat::Vector(vector) => Some(vector),
VectorFormat::List(list) => list.element.into_iter().next(),
})
}
#[cfg(test)]
mod migration_tests {
use super::*;
use vector_types::vector::style::Stroke;
#[test]
fn preserves_stroke_from_old_vector_data_style() {
let old_vector = legacy::VectorData {
style: legacy::PathStyle { stroke: Some(Stroke::new(12.)) },
..Default::default()
};
let mut value = serde_json::to_value(old_vector).unwrap();
value
.as_object_mut()
.unwrap()
.get_mut("style")
.unwrap()
.as_object_mut()
.unwrap()
.insert("fill".into(), serde_json::to_value(legacy::LegacyFill::default()).unwrap());
let migrated = migrate_to_optional_vector(value).unwrap().unwrap();
assert_eq!(migrated.stroke.unwrap().weight, 12.);
}
#[test]
fn preserves_stroke_from_current_vector_data() {
let vector = Vector {
stroke: Some(Stroke::new(12.)),
..Default::default()
};
let value = serde_json::to_value(&vector).unwrap();
let migrated = migrate_to_optional_vector(value).unwrap().unwrap();
assert_eq!(migrated.stroke.unwrap().weight, 12.);
}
}
}
@@ -0,0 +1,58 @@
//! Attribute markers whose value types live in this crate, with their name
//! constants for the string-keyed legacy readers and writers.
//!
//! The list-valued markers may carry native [`Graphic::Group`] content: the
//! registered deep field glue owns it across persistence seams, and legacy
//! products convert it through [`crate::graphic::map_paint_attrs_to_legacy`].
use crate::Graphic;
use core_types::attribute::Attribute;
use core_types::list::List;
core_types::attribute! {
/// Vector graphics object's filled area paint, a graphic list in the canonical paint form.
/// An absent value means no fill.
pub Fill("fill"): Option<&List<Graphic<'static>>>;
/// Vector graphics object's stroke paint, a graphic list in the canonical paint form.
/// An absent value means no stroke paint.
pub Stroke("stroke"): Option<&List<Graphic<'static>>>;
/// Snapshot of the upstream content that fed into a destructive merge (Boolean Operation,
/// Rasterize, etc.), so the editor can still surface click targets for the original child
/// layers after their content has been collapsed.
pub EditorMergedLayers("editor:merged_layers"): Option<&List<Graphic<'static>>>;
}
pub const ATTR_FILL: &str = Fill::NAME;
pub const ATTR_STROKE: &str = Stroke::NAME;
pub const ATTR_EDITOR_MERGED_LAYERS: &str = EditorMergedLayers::NAME;
#[cfg(test)]
mod tests {
use super::*;
use core_types::attribute::info;
use std::any::TypeId;
#[test]
fn the_census_carries_this_crates_names() {
for name in ["fill", "stroke", "editor:merged_layers"] {
assert_eq!(info(name).unwrap().value_type, TypeId::of::<Option<&'static List<Graphic>>>());
}
}
#[test]
fn an_absent_paint_defaults_to_none() {
assert_eq!(<Fill as Attribute>::default(), None);
}
#[test]
fn a_paint_marker_reads_back_what_the_paint_writer_stored() {
use core_types::lane::LaneSource;
let paint = List::new_from_element(Graphic::default());
let mut list = List::new_from_element(Graphic::default());
crate::graphic::set_paint_attribute_at(&mut list, 0, ATTR_FILL, paint.clone());
assert_eq!(list.attr::<Fill>(0), Some(&paint));
assert_eq!(list.attr::<Stroke>(0), None);
}
}
@@ -0,0 +1,65 @@
[package]
name = "no-std-types"
version = "0.1.0"
edition = "2024"
description = "no_std types for Graphene (shader-compatible)"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "MIT OR Apache-2.0"
[features]
# any feature that
# * must be usable in shaders
# * but requires std
# * and should be on by default
# should be in this list instead of `[workspace.dependency]`
std = [
"dep:dyn-any",
"dep:graphene-hash",
"dep:serde",
"dep:log",
"glam/debug-glam-assert",
"glam/std",
"glam/serde",
"half/std",
"half/serde",
"num-traits/std",
"num_enum/std",
]
wasm = ["tsify", "wasm-bindgen"]
[dependencies]
# Local dependencies
node-macro = { workspace = true }
# Local std dependencies
dyn-any = { workspace = true, optional = true }
graphene-hash = { workspace = true, optional = true }
# Workspace dependencies
bytemuck = { workspace = true }
glam = { workspace = true }
half = { workspace = true, default-features = false }
num-derive = { workspace = true }
num-traits = { workspace = true }
num_enum = { workspace = true }
spirv-std = { workspace = true }
# Workspace std dependencies
serde = { workspace = true, optional = true }
log = { workspace = true, optional = true }
# Workspace wasm dependencies
tsify = { workspace = true, optional = true }
wasm-bindgen = { workspace = true, optional = true }
[dev-dependencies]
core-types = { workspace = true }
[lints.rust]
# the spirv target is not in the list of common cfgs so must be added manually
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(target_arch, values("spirv"))',
] }
[package.metadata.cargo-shear]
ignored = ["core-types"]
@@ -0,0 +1,187 @@
use core::fmt::Display;
use node_macro::BufferStruct;
use num_enum::{FromPrimitive, IntoPrimitive};
#[repr(i32)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(graphene_hash::CacheHash))]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, BufferStruct, FromPrimitive, IntoPrimitive)]
pub enum BlendMode {
// Basic group
#[default]
Normal,
// Darken group
Darken,
Multiply,
ColorBurn,
LinearBurn,
DarkerColor,
// Lighten group
Lighten,
Screen,
ColorDodge,
LinearDodge,
LighterColor,
// Contrast group
Overlay,
SoftLight,
HardLight,
VividLight,
LinearLight,
PinLight,
HardMix,
// Inversion group
Difference,
Exclusion,
Subtract,
Divide,
// Component group
Hue,
Saturation,
Color,
Luminosity,
// Other stuff
Erase,
Restore,
MultiplyAlpha,
}
impl BlendMode {
/// All standard blend modes ordered by group.
pub fn list() -> [&'static [BlendMode]; 6] {
use BlendMode::*;
[
// Normal group
&[Normal],
// Darken group
&[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor],
// Lighten group
&[Lighten, Screen, ColorDodge, LinearDodge, LighterColor],
// Contrast group
&[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix],
// Inversion group
&[Difference, Exclusion, Subtract, Divide],
// Component group
&[Hue, Saturation, Color, Luminosity],
]
}
/// The subset of [`BlendMode::list()`] that is supported by SVG.
pub fn list_svg_subset() -> [&'static [BlendMode]; 6] {
use BlendMode::*;
[
// Normal group
&[Normal],
// Darken group
&[Darken, Multiply, ColorBurn],
// Lighten group
&[Lighten, Screen, ColorDodge],
// Contrast group
&[Overlay, SoftLight, HardLight],
// Inversion group
&[Difference, Exclusion],
// Component group
&[Hue, Saturation, Color, Luminosity],
]
}
pub fn index_in_list(&self) -> Option<usize> {
Self::list().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
pub fn index_in_list_svg_subset(&self) -> Option<usize> {
Self::list_svg_subset().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
/// Convert the enum to the CSS string for the blend mode.
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
pub fn to_svg_style_name(&self) -> Option<&'static str> {
match self {
// Normal group
BlendMode::Normal => Some("normal"),
// Darken group
BlendMode::Darken => Some("darken"),
BlendMode::Multiply => Some("multiply"),
BlendMode::ColorBurn => Some("color-burn"),
// Lighten group
BlendMode::Lighten => Some("lighten"),
BlendMode::Screen => Some("screen"),
BlendMode::ColorDodge => Some("color-dodge"),
// Contrast group
BlendMode::Overlay => Some("overlay"),
BlendMode::SoftLight => Some("soft-light"),
BlendMode::HardLight => Some("hard-light"),
// Inversion group
BlendMode::Difference => Some("difference"),
BlendMode::Exclusion => Some("exclusion"),
// Component group
BlendMode::Hue => Some("hue"),
BlendMode::Saturation => Some("saturation"),
BlendMode::Color => Some("color"),
BlendMode::Luminosity => Some("luminosity"),
_ => None,
}
}
/// Renders the blend mode CSS style declaration.
#[cfg(feature = "std")]
pub fn render(&self) -> String {
format!(
r#" mix-blend-mode: {};"#,
self.to_svg_style_name().unwrap_or_else(|| {
log::warn!("Unsupported blend mode {self:?}");
"normal"
})
)
}
}
impl Display for BlendMode {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
// Normal group
BlendMode::Normal => write!(f, "Normal"),
// Darken group
BlendMode::Darken => write!(f, "Darken"),
BlendMode::Multiply => write!(f, "Multiply"),
BlendMode::ColorBurn => write!(f, "Color Burn"),
BlendMode::LinearBurn => write!(f, "Linear Burn"),
BlendMode::DarkerColor => write!(f, "Darker Color"),
// Lighten group
BlendMode::Lighten => write!(f, "Lighten"),
BlendMode::Screen => write!(f, "Screen"),
BlendMode::ColorDodge => write!(f, "Color Dodge"),
BlendMode::LinearDodge => write!(f, "Linear Dodge"),
BlendMode::LighterColor => write!(f, "Lighter Color"),
// Contrast group
BlendMode::Overlay => write!(f, "Overlay"),
BlendMode::SoftLight => write!(f, "Soft Light"),
BlendMode::HardLight => write!(f, "Hard Light"),
BlendMode::VividLight => write!(f, "Vivid Light"),
BlendMode::LinearLight => write!(f, "Linear Light"),
BlendMode::PinLight => write!(f, "Pin Light"),
BlendMode::HardMix => write!(f, "Hard Mix"),
// Inversion group
BlendMode::Difference => write!(f, "Difference"),
BlendMode::Exclusion => write!(f, "Exclusion"),
BlendMode::Subtract => write!(f, "Subtract"),
BlendMode::Divide => write!(f, "Divide"),
// Component group
BlendMode::Hue => write!(f, "Hue"),
BlendMode::Saturation => write!(f, "Saturation"),
BlendMode::Color => write!(f, "Color"),
BlendMode::Luminosity => write!(f, "Luminosity"),
// Other utility blend modes (hidden from the normal list)
BlendMode::Erase => write!(f, "Erase"),
BlendMode::Restore => write!(f, "Restore"),
BlendMode::MultiplyAlpha => write!(f, "Multiply Alpha"),
}
}
}
@@ -0,0 +1,26 @@
pub trait ChoiceTypeStatic: Sized + Copy + crate::AsU32 + Send + Sync {
const WIDGET_HINT: ChoiceWidgetHint;
const DESCRIPTION: Option<&'static str>;
fn list() -> &'static [&'static [(Self, VariantMetadata)]];
}
pub enum ChoiceWidgetHint {
Dropdown,
RadioButtons,
}
/// Translation struct between macro and definition.
#[derive(Clone, Debug)]
pub struct VariantMetadata {
/// Name as declared in source code.
pub name: &'static str,
/// Name to be displayed in UI.
pub label: &'static str,
/// User-facing documentation text.
pub description: Option<&'static str>,
/// Name of icon to display in radio buttons and such.
pub icon: Option<&'static str>,
}
@@ -0,0 +1,182 @@
pub use crate::blending::*;
use bytemuck::{Pod, Zeroable};
use core::fmt::Debug;
use glam::DVec2;
use num_derive::*;
#[cfg(not(feature = "std"))]
use num_traits::float::Float;
pub trait Linear {
fn from_f32(x: f32) -> Self;
fn to_f32(self) -> f32;
fn from_f64(x: f64) -> Self;
fn to_f64(self) -> f64;
}
#[rustfmt::skip]
impl Linear for f32 {
#[inline(always)] fn from_f32(x: f32) -> Self { x }
#[inline(always)] fn to_f32(self) -> f32 { self }
#[inline(always)] fn from_f64(x: f64) -> Self { x as f32 }
#[inline(always)] fn to_f64(self) -> f64 { self as f64 }
}
#[rustfmt::skip]
impl Linear for f64 {
#[inline(always)] fn from_f32(x: f32) -> Self { x as f64 }
#[inline(always)] fn to_f32(self) -> f32 { self as f32 }
#[inline(always)] fn from_f64(x: f64) -> Self { x }
#[inline(always)] fn to_f64(self) -> f64 { self }
}
pub trait Channel: Copy + Debug {
fn to_linear<Out: Linear>(self) -> Out;
fn from_linear<In: Linear>(linear: In) -> Self;
}
pub trait LinearChannel: Channel {
fn cast_linear_channel<Out: LinearChannel>(self) -> Out {
Out::from_linear(self.to_linear::<f64>())
}
}
impl<T: Linear + Debug + Copy> Channel for T {
#[inline(always)]
fn to_linear<Out: Linear>(self) -> Out {
Out::from_f64(self.to_f64())
}
#[inline(always)]
fn from_linear<In: Linear>(linear: In) -> Self {
Self::from_f64(linear.to_f64())
}
}
impl<T: Linear + Debug + Copy> LinearChannel for T {}
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Num, NumCast, NumOps, One, Zero, ToPrimitive, FromPrimitive)]
pub struct SRGBGammaFloat(f32);
impl Channel for SRGBGammaFloat {
#[inline(always)]
fn to_linear<Out: Linear>(self) -> Out {
let x = self.0;
Out::from_f32(if x <= 0.04045 { x / 12.92 } else { ((x + 0.055) / 1.055).powf(2.4) })
}
#[inline(always)]
fn from_linear<In: Linear>(linear: In) -> Self {
let x = linear.to_f32();
if x <= 0.0031308 { Self(x * 12.92) } else { Self(1.055 * x.powf(1. / 2.4) - 0.055) }
}
}
pub trait RGBPrimaries {
const RED: DVec2;
const GREEN: DVec2;
const BLUE: DVec2;
const WHITE: DVec2;
}
pub trait Rec709Primaries {}
impl<T: Rec709Primaries> RGBPrimaries for T {
const RED: DVec2 = DVec2::new(0.64, 0.33);
const GREEN: DVec2 = DVec2::new(0.3, 0.6);
const BLUE: DVec2 = DVec2::new(0.15, 0.06);
const WHITE: DVec2 = DVec2::new(0.3127, 0.329);
}
pub trait SRGB: Rec709Primaries {}
// TODO: Come up with a better name for this trait
pub trait Pixel: Clone + Pod + Zeroable + Default {
#[cfg(feature = "std")]
fn to_bytes(&self) -> Vec<u8> {
bytemuck::bytes_of(self).to_vec()
}
// TODO: use u8 for Color
fn from_bytes(bytes: &[u8]) -> Self {
*bytemuck::try_from_bytes(bytes).expect("Failed to convert bytes to pixel")
}
fn byte_size() -> usize {
size_of::<Self>()
}
}
pub trait RGB: Pixel {
type ColorChannel: Channel;
fn red(&self) -> Self::ColorChannel;
fn r(&self) -> Self::ColorChannel {
self.red()
}
fn green(&self) -> Self::ColorChannel;
fn g(&self) -> Self::ColorChannel {
self.green()
}
fn blue(&self) -> Self::ColorChannel;
fn b(&self) -> Self::ColorChannel {
self.blue()
}
}
pub trait RGBMut: RGB {
fn set_red(&mut self, red: Self::ColorChannel);
fn set_green(&mut self, green: Self::ColorChannel);
fn set_blue(&mut self, blue: Self::ColorChannel);
}
pub trait AssociatedAlpha: RGB + Alpha {
fn to_unassociated<Out: UnassociatedAlpha>(&self) -> Out;
}
pub trait UnassociatedAlpha: RGB + Alpha {
fn to_associated<Out: AssociatedAlpha>(&self) -> Out;
}
pub trait Alpha {
type AlphaChannel: LinearChannel;
const TRANSPARENT: Self;
fn alpha(&self) -> Self::AlphaChannel;
fn a(&self) -> Self::AlphaChannel {
self.alpha()
}
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self;
}
pub trait AlphaMut: Alpha {
fn set_alpha(&mut self, value: Self::AlphaChannel);
}
pub trait Depth {
type DepthChannel: Channel;
fn depth(&self) -> Self::DepthChannel;
fn d(&self) -> Self::DepthChannel {
self.depth()
}
}
pub trait ExtraChannels<const NUM: usize> {
type ChannelType: Channel;
fn extra_channels(&self) -> [Self::ChannelType; NUM];
}
pub trait Luminance {
type LuminanceChannel: LinearChannel;
fn luminance(&self) -> Self::LuminanceChannel;
fn l(&self) -> Self::LuminanceChannel {
self.luminance()
}
}
// TODO: We might rename this to Raster at some point
pub trait Sample {
type Pixel: Pixel;
// TODO: Add an area parameter
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel>;
}
impl<T: Sample> Sample for &T {
type Pixel = T::Pixel;
#[inline(always)]
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel> {
(**self).sample(pos, area)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,178 @@
#![allow(clippy::neg_cmp_op_on_partial_ord)]
//! Fast conversions between u8 sRGB and linear float.
// Inspired by https://gist.github.com/rygorous/2203834, but with a slightly
// modified method, custom derived constants and error correction for perfect
// accuracy in accordance with the D3D11 spec:
// https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm#FLOATtoSRGB.
/// CRITICAL_POINTS[i] is the last float value such that it maps to i after
/// conversion to integer sRGB. So if x > CRITICAL_POINTS[i] you know you need
/// to increment i.
#[rustfmt::skip]
const CRITICAL_POINTS: [f32; 256] = [
0.00015176347, 0.00045529046, 0.0007588174, 0.0010623443, 0.0013658714, 0.0016693983, 0.0019729252, 0.0022764523,
0.0025799791, 0.0028835062, 0.0031883009, 0.003509259, 0.003848315, 0.004205748, 0.0045818323, 0.0049768374,
0.005391024, 0.00582465, 0.0062779686, 0.0067512267, 0.0072446675, 0.0077585294, 0.008293047, 0.008848451,
0.0094249705, 0.010022825, 0.010642236, 0.01128342, 0.011946591, 0.012631957, 0.013339729, 0.014070111,
0.0148233045, 0.015599505, 0.01639891, 0.017221717, 0.018068114, 0.018938294, 0.019832445, 0.020750746,
0.021693384, 0.022660539, 0.02365239, 0.024669115, 0.025710886, 0.026777886, 0.027870273, 0.028988222,
0.030131903, 0.03130148, 0.032497127, 0.033718992, 0.034967244, 0.03624204, 0.03754355, 0.03887192,
0.040227327, 0.041609894, 0.04301979, 0.044457167, 0.04592218, 0.04741497, 0.04893569, 0.050484486,
0.05206151, 0.053666897, 0.055300802, 0.056963358, 0.058654714, 0.060375024, 0.062124394, 0.06390298,
0.065710925, 0.06754836, 0.06941542, 0.07131224, 0.07323896, 0.07519571, 0.07718261, 0.07919981,
0.08124744, 0.08332562, 0.08543448, 0.08757417, 0.08974478, 0.091946445, 0.09417931, 0.09644348,
0.098739095, 0.10106628, 0.10342514, 0.105815805, 0.1082384, 0.110693045, 0.11317986, 0.11569896,
0.118250474, 0.12083454, 0.12345121, 0.12610064, 0.12878296, 0.13149826, 0.13424668, 0.1370283,
0.13984327, 0.14269169, 0.14557366, 0.1484893, 0.15143873, 0.15442204, 0.15743938, 0.16049084,
0.1635765, 0.16669647, 0.16985092, 0.1730399, 0.17626354, 0.17952198, 0.18281525, 0.1861435,
0.18950681, 0.19290532, 0.19633913, 0.19980833, 0.20331302, 0.20685332, 0.21042931, 0.21404111,
0.21768881, 0.22137253, 0.22509235, 0.22884844, 0.23264077, 0.23646952, 0.24033478, 0.24423665,
0.24817522, 0.25215057, 0.25616285, 0.26021212, 0.26429847, 0.26842204, 0.27258286, 0.27678108,
0.2810168, 0.28529006, 0.289601, 0.2939497, 0.29833627, 0.30276078, 0.30722332, 0.311724,
0.31626293, 0.32084015, 0.32545578, 0.33010995, 0.3348027, 0.3395341, 0.34430432, 0.34911346,
0.3539615, 0.35884857, 0.3637748, 0.36874023, 0.373745, 0.37878913, 0.38387278, 0.388996,
0.39415887, 0.39936152, 0.404604, 0.4098864, 0.41520882, 0.42057133, 0.425974, 0.431417,
0.43690032, 0.4424241, 0.44798836, 0.45359328, 0.45923886, 0.46492523, 0.47065246, 0.47642064,
0.48222986, 0.48808017, 0.4939718, 0.49990457, 0.5058787, 0.5118943, 0.5179514, 0.5240501,
0.5301905, 0.5363727, 0.5425967, 0.54886264, 0.5551706, 0.56152064, 0.5679129, 0.5743473,
0.5808241, 0.5873433, 0.593905, 0.60050917, 0.60715604, 0.61384565, 0.62057805, 0.6273533,
0.63417155, 0.6410328, 0.6479372, 0.65488476, 0.66187555, 0.6689097, 0.6759874, 0.68310845,
0.6902731, 0.6974814, 0.7047334, 0.71202916, 0.7193688, 0.7267524, 0.73418003, 0.7416518,
0.7491677, 0.7567278, 0.76433223, 0.7719811, 0.7796744, 0.7874122, 0.7951947, 0.80302185,
0.8108938, 0.81881046, 0.82677215, 0.8347787, 0.8428304, 0.8509272, 0.85906917, 0.8672564,
0.875489, 0.8837671, 0.89209044, 0.9004596, 0.9088741, 0.91733456, 0.9258405, 0.9343926,
0.94299024, 0.95163417, 0.96032387, 0.96906, 0.977842, 0.9866705, 0.9955452, 1.,
];
#[rustfmt::skip]
const FLOAT_SRGB_LERP: [u32; 27] = [
0x66f, 0x66f063b, 0xcaa0515, 0x11c00773, 0x193305dc, 0x1f1004f3, 0x24030481, 0x28850773,
0x2ff9065e, 0x365805a1, 0x3bfa0547, 0x414108f7, 0x4a3907d8, 0x52110709, 0x591b06aa, 0x5fc50b70,
0x6b350a18, 0x754e091c, 0x7e6b08aa, 0x87160ef1, 0x96070d3e, 0xa3460bfc, 0xaf430b6c, 0xbaaf13bd,
0xce6d1187, 0xdff40fe3, 0xefd70f28,
];
#[inline]
pub fn float_to_srgb_u8(mut f: f32) -> u8 {
// Clamp f to [0, 1], with a negated condition to handle NaNs as 0.
if !(f >= 0.) {
f = 0.;
} else if f > 1. {
f = 1.;
}
// Shift away slightly from 0 to reduce exponent range.
const C: f32 = 0.009842521f32;
let u = (f + C).to_bits() - C.to_bits();
if u > (1. + C).to_bits() - C.to_bits() {
// We clamped f to [0, 1], and the integer representations
// of the positive finite non-NaN floats are monotonic.
// This makes the later LUT lookup panicless.
unsafe { core::hint::unreachable_unchecked() }
}
// Compute a piecewise linear interpolation that is always
// the correct answer, or one less than it.
let u16mask = (1 << 16) - 1;
let lut_idx = u >> 21;
let lerp_idx = (u >> 5) & u16mask;
let bias_mult = FLOAT_SRGB_LERP[lut_idx as usize];
let bias = (bias_mult >> 16) << 16;
let mult = bias_mult & u16mask;
// I don't believe this wraps, but since we test in release mode,
// better make sure debug mode behaves the same.
let lerp = bias.wrapping_add(mult * lerp_idx) >> 24;
// Adjust linear interpolation to the correct value.
if f > CRITICAL_POINTS[lerp as usize] { lerp as u8 + 1 } else { lerp as u8 }
}
#[rustfmt::skip]
const FROM_SRGB_U8: [f32; 256] = [
0., 0.000303527, 0.000607054, 0.00091058103, 0.001214108, 0.001517635, 0.0018211621, 0.002124689,
0.002428216, 0.002731743, 0.00303527, 0.0033465356, 0.003676507, 0.004024717, 0.004391442,
0.0047769533, 0.005181517, 0.0056053917, 0.0060488326, 0.006512091, 0.00699541, 0.0074990317,
0.008023192, 0.008568125, 0.009134057, 0.009721218, 0.010329823, 0.010960094, 0.011612245,
0.012286487, 0.012983031, 0.013702081, 0.014443844, 0.015208514, 0.015996292, 0.016807375,
0.017641952, 0.018500218, 0.019382361, 0.020288562, 0.02121901, 0.022173883, 0.023153365,
0.02415763, 0.025186857, 0.026241222, 0.027320892, 0.028426038, 0.029556843, 0.03071345, 0.03189604,
0.033104774, 0.03433981, 0.035601325, 0.036889452, 0.038204376, 0.039546248, 0.04091521, 0.042311423,
0.043735042, 0.045186214, 0.046665095, 0.048171833, 0.049706575, 0.051269468, 0.052860655, 0.05448028,
0.056128494, 0.057805434, 0.05951124, 0.06124607, 0.06301003, 0.06480328, 0.06662595, 0.06847818,
0.07036011, 0.07227186, 0.07421358, 0.07618539, 0.07818743, 0.08021983, 0.082282715, 0.084376216,
0.086500466, 0.088655606, 0.09084173, 0.09305898, 0.095307484, 0.09758736, 0.09989874, 0.10224175,
0.10461649, 0.10702311, 0.10946172, 0.111932434, 0.11443538, 0.116970696, 0.11953845, 0.12213881,
0.12477186, 0.12743773, 0.13013652, 0.13286836, 0.13563336, 0.13843165, 0.14126332, 0.1441285,
0.1470273, 0.14995982, 0.15292618, 0.1559265, 0.15896086, 0.16202943, 0.16513224, 0.16826946,
0.17144115, 0.17464745, 0.17788847, 0.1811643, 0.18447503, 0.1878208, 0.19120172, 0.19461787,
0.19806935, 0.2015563, 0.20507877, 0.2086369, 0.21223079, 0.21586053, 0.21952623, 0.22322798,
0.22696589, 0.23074007, 0.23455065, 0.23839766, 0.2422812, 0.2462014, 0.25015837, 0.25415218,
0.2581829, 0.26225072, 0.26635566, 0.27049786, 0.27467737, 0.27889434, 0.2831488, 0.2874409,
0.2917707, 0.29613832, 0.30054384, 0.30498737, 0.30946895, 0.31398875, 0.31854683, 0.32314324,
0.32777813, 0.33245158, 0.33716366, 0.34191445, 0.3467041, 0.3515327, 0.35640025, 0.36130688,
0.3662527, 0.37123778, 0.37626222, 0.3813261, 0.38642952, 0.39157256, 0.3967553, 0.40197787,
0.4072403, 0.4125427, 0.41788515, 0.42326775, 0.42869055, 0.4341537, 0.43965724, 0.44520125,
0.45078585, 0.45641106, 0.46207705, 0.46778384, 0.47353154, 0.47932023, 0.48514998, 0.4910209,
0.49693304, 0.5028866, 0.50888145, 0.5149178, 0.5209957, 0.52711535, 0.5332766, 0.5394797,
0.5457247, 0.5520116, 0.5583406, 0.5647117, 0.57112503, 0.57758063, 0.5840786, 0.590619, 0.597202,
0.60382754, 0.61049575, 0.61720675, 0.62396055, 0.63075733, 0.637597, 0.6444799, 0.6514058,
0.65837497, 0.66538745, 0.67244333, 0.6795426, 0.68668544, 0.69387203, 0.70110214, 0.70837605,
0.7156938, 0.72305536, 0.730461, 0.7379107, 0.7454045, 0.75294244, 0.76052475, 0.7681514, 0.77582246,
0.78353804, 0.79129815, 0.79910296, 0.8069525, 0.8148468, 0.822786, 0.8307701, 0.83879924, 0.84687346,
0.8549928, 0.8631574, 0.87136734, 0.8796226, 0.8879232, 0.89626956, 0.90466136, 0.913099, 0.92158204,
0.93011117, 0.9386859, 0.9473069, 0.9559735, 0.9646866, 0.9734455, 0.98225087, 0.9911022, 1.,
];
#[inline]
pub fn srgb_u8_to_float(c: u8) -> f32 {
FROM_SRGB_U8[c as usize]
}
#[cfg(test)]
mod tests {
use super::*;
// https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm#FLOATtoSRGB
fn float_to_srgb_ref(f: f32) -> f32 {
if !(f > 0_f32) {
0_f32
} else if f <= 0.0031308f32 {
12.92_f32 * f
} else if f < 1_f32 {
1.055f32 * f.powf(1_f32 / 2.4_f32) - 0.055f32
} else {
1_f32
}
}
fn float_to_srgb_u8_ref(f: f32) -> u8 {
(float_to_srgb_ref(f) * 255_f32 + 0.5_f32) as u8
}
// https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm#SRGBtoFLOAT
fn srgb_to_float_ref(f: f32) -> f32 {
if f <= 0.04045f32 { f / 12.92f32 } else { ((f + 0.055f32) / 1.055f32).powf(2.4_f32) }
}
fn srgb_u8_to_float_ref(c: u8) -> f32 {
srgb_to_float_ref(c as f32 * (1_f32 / 255_f32))
}
#[test]
fn test_float_to_srgb_u8() {
for u in 0..=u8::MAX {
assert!(srgb_u8_to_float(u) == srgb_u8_to_float_ref(u));
}
}
#[ignore = "expensive, test in release mode"]
#[test]
fn test_srgb_u8_to_float() {
// Simply... check all float values.
for u in 0..=u32::MAX {
let f = f32::from_bits(u);
assert!(float_to_srgb_u8(f) == float_to_srgb_u8_ref(f));
}
}
}
@@ -0,0 +1,9 @@
mod color_traits;
mod color_types;
mod discrete_srgb;
mod transfer;
pub use color_traits::*;
pub use color_types::*;
pub use discrete_srgb::*;
pub use transfer::*;
@@ -0,0 +1,19 @@
//! Analytic per-channel sRGB transfer functions (gamma encoding/decoding).
//!
//! These work in `f32` at full precision. For round-trip-exact `u8` ⇄ `f32` conversion at the
//! display byte boundary, use the lookup tables in [`super::discrete_srgb`] instead.
#[cfg(not(feature = "std"))]
use num_traits::float::Float;
/// Decode an sRGB gamma-encoded channel value to linear-light.
#[inline(always)]
pub fn srgb_to_linear(channel: f32) -> f32 {
if channel <= 0.04045 { channel / 12.92 } else { ((channel + 0.055) / 1.055).powf(2.4) }
}
/// Encode a linear-light channel value to sRGB gamma-encoded.
#[inline(always)]
pub fn linear_to_srgb(channel: f32) -> f32 {
if channel <= 0.0031308 { channel * 12.92 } else { 1.055 * channel.powf(1. / 2.4) - 0.055 }
}
@@ -0,0 +1,18 @@
pub trait Ctx: Clone + Send {}
impl<T: Ctx> Ctx for Option<T> {}
impl<T: Ctx + Sync> Ctx for &T {}
impl Ctx for () {}
pub trait ArcCtx: Send + Sync {}
#[cfg(feature = "std")]
impl<T: ArcCtx> Ctx for std::sync::Arc<T> {}
// The cache-hash bound record kernels place on their element generics; the
// shader build compiles the same signatures without the hashing machinery.
#[cfg(feature = "std")]
pub use graphene_hash::CacheHash;
#[cfg(not(feature = "std"))]
pub trait CacheHash {}
#[cfg(not(feature = "std"))]
impl<T> CacheHash for T {}
@@ -0,0 +1,21 @@
#![cfg_attr(not(feature = "std"), no_std)]
pub mod blending;
pub mod choice_type;
pub mod color;
pub mod context;
pub mod list;
pub mod registry;
pub mod shaders;
pub use context::Ctx;
pub use glam;
pub trait AsU32 {
fn as_u32(&self) -> u32;
}
impl AsU32 for u32 {
fn as_u32(&self) -> u32 {
*self
}
}
@@ -0,0 +1,41 @@
//! A zero-cost stand-in for `core_types::list::Item` used when node kernels are compiled for the GPU.
//!
//! Shader node kernels compile twice: under `std` against the real attribute-carrying `Item`, and under
//! `no_std` (SPIR-V) against this transparent wrapper, imported as `Item`. Only the element-access surface is
//! provided, since rust-gpu cannot allocate and attributes have no per-pixel meaning; attribute use fails the
//! shader build. It is named distinctly from `Item` so a search for the canonical type finds only that one.
/// A rank-0 wire value holding a single element, mirroring the element-access API of the real `Item`.
#[repr(transparent)]
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct ShaderItem<T> {
element: T,
}
impl<T> ShaderItem<T> {
/// Constructs an item with the given element.
pub fn new_from_element(element: T) -> Self {
Self { element }
}
/// Returns a shared reference to this item's element.
pub fn element(&self) -> &T {
&self.element
}
/// Returns a mutable reference to this item's element.
pub fn element_mut(&mut self) -> &mut T {
&mut self.element
}
/// Consumes this item and returns the owned element.
pub fn into_element(self) -> T {
self.element
}
}
impl<T> From<T> for ShaderItem<T> {
fn from(element: T) -> Self {
Self::new_from_element(element)
}
}
@@ -0,0 +1,33 @@
pub mod types {
/// 0% - 100%
pub type Percentage = f64;
/// 0% - 100%
pub type PercentageF32 = f32;
/// -100% - 100%
pub type SignedPercentage = f64;
/// -100% - 100%
pub type SignedPercentageF32 = f32;
/// -180° - 180°
pub type Angle = f64;
/// -180° - 180°
pub type AngleF32 = f32;
/// Ends in the unit of x
pub type Multiplier = f64;
/// Non-negative integer with px unit
pub type PixelLength = f64;
/// Non-negative
pub type Length = f64;
/// 0 to 1
pub type Fraction = f64;
/// Non-negative number broken into whole and fractional parts
pub type Progression = f64;
/// Signed integer that's actually a float because we don't handle type conversions very well yet
pub type SignedInteger = f64;
/// Unsigned integer to be used for random seeds
pub type SeedValue = u32;
/// DVec2 with px unit
pub type PixelSize = glam::DVec2;
/// String with one or more than one line
#[cfg(feature = "std")]
pub type TextArea = String;
}
@@ -0,0 +1,112 @@
use crate::shaders::buffer_struct::BufferStruct;
macro_rules! glam_array {
($t:ty, $a:ty) => {
unsafe impl BufferStruct for $t {
type Buffer = $a;
#[inline]
fn write(from: Self) -> Self::Buffer {
<$t>::to_array(&from)
}
#[inline]
fn read(from: Self::Buffer) -> Self {
<$t>::from_array(from)
}
}
};
}
macro_rules! glam_cols_array {
($t:ty, $a:ty) => {
unsafe impl BufferStruct for $t {
type Buffer = $a;
#[inline]
fn write(from: Self) -> Self::Buffer {
<$t>::to_cols_array(&from)
}
#[inline]
fn read(from: Self::Buffer) -> Self {
<$t>::from_cols_array(&from)
}
}
};
}
glam_array!(glam::Vec2, [f32; 2]);
glam_array!(glam::Vec3, [f32; 3]);
// glam_array!(Vec3A, [f32; 4]);
glam_array!(glam::Vec4, [f32; 4]);
glam_cols_array!(glam::Mat2, [f32; 4]);
glam_cols_array!(glam::Mat3, [f32; 9]);
// glam_cols_array!(Mat3A, [f32; 4]);
glam_cols_array!(glam::Mat4, [f32; 16]);
glam_cols_array!(glam::Affine2, [f32; 6]);
glam_cols_array!(glam::Affine3A, [f32; 12]);
glam_array!(glam::DVec2, [f64; 2]);
glam_array!(glam::DVec3, [f64; 3]);
glam_array!(glam::DVec4, [f64; 4]);
glam_cols_array!(glam::DMat2, [f64; 4]);
glam_cols_array!(glam::DMat3, [f64; 9]);
glam_cols_array!(glam::DMat4, [f64; 16]);
glam_cols_array!(glam::DAffine2, [f64; 6]);
glam_cols_array!(glam::DAffine3, [f64; 12]);
glam_array!(glam::I16Vec2, [i16; 2]);
glam_array!(glam::I16Vec3, [i16; 3]);
glam_array!(glam::I16Vec4, [i16; 4]);
glam_array!(glam::U16Vec2, [u16; 2]);
glam_array!(glam::U16Vec3, [u16; 3]);
glam_array!(glam::U16Vec4, [u16; 4]);
glam_array!(glam::IVec2, [i32; 2]);
glam_array!(glam::IVec3, [i32; 3]);
glam_array!(glam::IVec4, [i32; 4]);
glam_array!(glam::UVec2, [u32; 2]);
glam_array!(glam::UVec3, [u32; 3]);
glam_array!(glam::UVec4, [u32; 4]);
glam_array!(glam::I64Vec2, [i64; 2]);
glam_array!(glam::I64Vec3, [i64; 3]);
glam_array!(glam::I64Vec4, [i64; 4]);
glam_array!(glam::U64Vec2, [u64; 2]);
glam_array!(glam::U64Vec3, [u64; 3]);
glam_array!(glam::U64Vec4, [u64; 4]);
unsafe impl BufferStruct for glam::Vec3A {
type Buffer = [f32; 4];
#[inline]
fn write(from: Self) -> Self::Buffer {
glam::Vec4::to_array(&from.extend(0.))
}
#[inline]
fn read(from: Self::Buffer) -> Self {
glam::Vec3A::from_vec4(glam::Vec4::from_array(from))
}
}
/// do NOT use slices, otherwise spirv will fail to compile
unsafe impl BufferStruct for glam::Mat3A {
type Buffer = [f32; 12];
#[inline]
fn write(from: Self) -> Self::Buffer {
let a = from.to_cols_array();
[a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], 0., 0., 0.]
}
#[inline]
fn read(from: Self::Buffer) -> Self {
let a = from;
glam::Mat3A::from_cols_array(&[a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]])
}
}
@@ -0,0 +1,63 @@
//! I (@firestar99) copied this entire mod from one of my projects, as I haven't uploaded that lib to crates. Hopefully
//! rust-gpu improves and this entire thing becomes unnecessary in the future.
//!
//! https://github.com/Firestar99/nanite-at-home/tree/008dac8df656959c71efeddd2d3ddabcb801771c/rust-gpu-bindless/crates/buffer-content
use bytemuck::Pod;
mod glam;
mod primitive;
/// A BufferStruct is a "parallel representation" of the original struct with some fundamental types remapped. This
/// struct hierarchy represents how data is stored in GPU Buffers, where all types must be [`Pod`] to allow
/// transmuting them to `&[u8]` with [`bytemuck`].
///
/// Notable type remappings (original: buffer):
/// * bool: u32 of 0 or 1
/// * any repr(u32) enum: u32 with remapping via [`num_enum`]
///
/// By adding `#[derive(ShaderStruct)]` to your struct (or enum), a parallel `{name}Buffer` struct is created with all
/// the members of the original struct, but with their types using the associated remapped types as specified by this
/// trait.
///
/// # Origin
/// I (@firestar99) copied this entire mod from my [Nanite-at-home] project, specifically the [buffer-content] crate
/// and the [buffer_struct] proc macro. The variant here has quite some modifications, to both cleaned up some of the
/// mistakes my implementation has and to customize it a bit for graphite.
///
/// Hopefully rust-gpu improves to the point where this remapping becomes unnecessary.
///
/// [Nanite-at-home]: https://github.com/Firestar99/nanite-at-home
/// [buffer-content]: https://github.com/Firestar99/nanite-at-home/tree/008dac8df656959c71efeddd2d3ddabcb801771c/rust-gpu-bindless/crates/buffer-content
/// [buffer_struct]: https://github.com/Firestar99/nanite-at-home/blob/008dac8df656959c71efeddd2d3ddabcb801771c/rust-gpu-bindless/crates/macros/src/buffer_struct.rs
///
/// # Safety
/// The associated type Transfer must be the same on all targets. Writing followed by reading back a value must result
/// in the same value.
pub unsafe trait BufferStruct: Copy + Send + Sync + 'static {
type Buffer: Pod + Send + Sync;
fn write(from: Self) -> Self::Buffer;
fn read(from: Self::Buffer) -> Self;
}
/// Trait marking all [`BufferStruct`] whose read and write methods are identity. While [`BufferStruct`] only
/// requires `t == read(write(t))`, this trait additionally requires `t == read(t) == write(t)`. As this removes the
/// conversion requirement for writing to or reading from a buffer, one can acquire slices from buffers created of these
/// types.
///
/// Implementing this type is completely safe due to the [`Pod`] requirement.
pub trait BufferStructIdentity: Pod + Send + Sync {}
unsafe impl<T: BufferStructIdentity> BufferStruct for T {
type Buffer = Self;
fn write(from: Self) -> Self::Buffer {
from
}
fn read(from: Self::Buffer) -> Self {
from
}
}
@@ -0,0 +1,134 @@
use crate::shaders::buffer_struct::{BufferStruct, BufferStructIdentity};
use bytemuck::Pod;
use core::marker::PhantomData;
use core::num::Wrapping;
use spirv_std::arch::IndexUnchecked;
macro_rules! identity {
($t:ty) => {
impl BufferStructIdentity for $t {}
};
}
identity!(());
identity!(u8);
identity!(u16);
identity!(u32);
identity!(u64);
identity!(u128);
identity!(usize);
identity!(i8);
identity!(i16);
identity!(i32);
identity!(i64);
identity!(i128);
identity!(isize);
identity!(f32);
identity!(f64);
identity!(spirv_std::memory::Semantics);
identity!(spirv_std::ray_tracing::RayFlags);
identity!(spirv_std::indirect_command::DrawIndirectCommand);
identity!(spirv_std::indirect_command::DrawIndexedIndirectCommand);
identity!(spirv_std::indirect_command::DispatchIndirectCommand);
identity!(spirv_std::indirect_command::DrawMeshTasksIndirectCommandEXT);
identity!(spirv_std::indirect_command::TraceRaysIndirectCommandKHR);
// not pod
// identity!(spirv_std::indirect_command::TraceRaysIndirectCommand2KHR);
unsafe impl BufferStruct for bool {
type Buffer = u32;
#[inline]
fn write(from: Self) -> Self::Buffer {
from as u32
}
#[inline]
fn read(from: Self::Buffer) -> Self {
from != 0
}
}
unsafe impl<T: BufferStruct> BufferStruct for Wrapping<T>
where
// unfortunately has to be Pod, even though AnyBitPattern would be sufficient,
// due to bytemuck doing `impl<T: Pod> AnyBitPattern for T {}`
// see https://github.com/Lokathor/bytemuck/issues/164
T::Buffer: Pod,
{
type Buffer = Wrapping<T::Buffer>;
#[inline]
fn write(from: Self) -> Self::Buffer {
Wrapping(T::write(from.0))
}
#[inline]
fn read(from: Self::Buffer) -> Self {
Wrapping(T::read(from.0))
}
}
unsafe impl<T: BufferStruct + 'static> BufferStruct for PhantomData<T> {
type Buffer = PhantomData<T>;
#[inline]
fn write(_: Self) -> Self::Buffer {
PhantomData {}
}
#[inline]
fn read(_: Self::Buffer) -> Self {
PhantomData {}
}
}
/// Potential problem: you can't impl this for an array of BufferStruct, as it'll conflict with this impl due to the
/// blanket impl on all BufferStructPlain types.
unsafe impl<T: BufferStruct, const N: usize> BufferStruct for [T; N]
where
// rust-gpu does not like `[T; N].map()` nor `core::array::from_fn()` nor transmuting arrays with a const generic
// length, so for now we need to require T: Default and T::Transfer: Default for all arrays.
T: Default,
// unfortunately has to be Pod, even though AnyBitPattern would be sufficient,
// due to bytemuck doing `impl<T: Pod> AnyBitPattern for T {}`
// see https://github.com/Lokathor/bytemuck/issues/164
T::Buffer: Pod + Default,
{
type Buffer = [T::Buffer; N];
#[inline]
fn write(from: Self) -> Self::Buffer {
unsafe {
let mut ret = [T::Buffer::default(); N];
for i in 0..N {
*ret.index_unchecked_mut(i) = T::write(*from.index_unchecked(i));
}
ret
}
}
#[inline]
fn read(from: Self::Buffer) -> Self {
unsafe {
let mut ret = [T::default(); N];
for i in 0..N {
*ret.index_unchecked_mut(i) = T::read(*from.index_unchecked(i));
}
ret
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_bool() {
for x in [false, true] {
assert_eq!(x, <bool as BufferStruct>::read(<bool as BufferStruct>::write(x)));
}
}
}
@@ -0,0 +1,10 @@
//! supporting infrastructure for shaders
pub mod buffer_struct;
pub mod __private {
pub use bytemuck;
pub use glam;
pub use num_enum;
pub use spirv_std;
}
@@ -0,0 +1,33 @@
[package]
name = "raster-types"
version = "0.1.0"
edition = "2024"
description = "Raster data types for Graphene node system"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde", "core-types/serde"]
wgpu = ["dep:wgpu"]
wasm = ["core-types/wasm", "tsify", "wasm-bindgen"]
[dependencies]
# Local dependencies
core-types = { workspace = true }
graphene-hash = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies
dyn-any = { workspace = true }
glam = { workspace = true }
base64 = { workspace = true }
bytemuck = { workspace = true }
image = { workspace = true }
serde_json = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true }
tsify = { workspace = true, optional = true }
wgpu = { workspace = true, optional = true }
wasm-bindgen = { workspace = true, optional = true }
@@ -0,0 +1,290 @@
use crate::{Bitmap, BitmapMut};
use core_types::Color;
use core_types::color::float_to_srgb_u8;
// use crate::vector::Vector; // TODO: Check if Vector is actually used, if so handle differently
use core_types::color::*;
use dyn_any::StaticType;
use glam::{DAffine2, DVec2};
use std::vec::Vec;
mod base64_serde {
//! Basic wrapper for [`serde`] to perform [`base64`] encoding
use base64::Engine;
use core_types::color::*;
use serde::{Deserializer, Serialize, Serializer};
pub fn as_base64<S: Serializer, P: Pixel>(key: &[P], serializer: S) -> Result<S::Ok, S::Error> {
let u8_data = bytemuck::cast_slice(key);
let string = base64::engine::general_purpose::STANDARD.encode(u8_data);
(key.len() as u64, string).serialize(serializer)
}
pub fn from_base64<'a, D: Deserializer<'a>, P: Pixel>(deserializer: D) -> Result<Vec<P>, D::Error> {
use serde::de::Error;
// Use a small visitor that accepts both borrowed bytes (from a streaming JSON deserializer) and owned strings (from an intermediate like `serde_json::Value`, which can't preserve the borrow).
// The migration loader takes the second path, so without this allowance documents containing image base64 data fail with `expected a borrowed byte array`.
struct LenAndBase64Visitor<P: Pixel>(std::marker::PhantomData<P>);
impl<'de, P: Pixel> serde::de::Visitor<'de> for LenAndBase64Visitor<P> {
type Value = Vec<P>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a tuple of (length, base64-encoded data)")
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let len: u64 = seq.next_element()?.ok_or_else(|| A::Error::missing_field("length"))?;
let base64_string: std::borrow::Cow<'de, str> = seq.next_element()?.ok_or_else(|| A::Error::missing_field("base64 data"))?;
let mut output: Vec<P> = vec![P::zeroed(); len as usize];
base64::engine::general_purpose::STANDARD
.decode_slice(base64_string.as_bytes(), bytemuck::cast_slice_mut(output.as_mut_slice()))
.map_err(|err| A::Error::custom(err.to_string()))?;
Ok(output)
}
}
deserializer.deserialize_tuple(2, LenAndBase64Visitor::<P>(std::marker::PhantomData))
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Image<P: Pixel> {
pub width: u32,
pub height: u32,
#[cfg_attr(feature = "serde", serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64"))]
pub data: Vec<P>,
/// Optional: Stores a base64 string representation of the image which can be used to speed up the conversion
/// to an svg string. This is used as a cache in order to not have to encode the data on every graph evaluation.
#[cfg_attr(feature = "serde", serde(skip))]
pub base64_string: Option<String>,
// TODO: Add an `origin` field to store where in the local space the image is anchored.
// TODO: Currently it is always anchored at the top left corner at (0, 0). The bottom right corner of the new origin field would correspond to (1, 1).
}
impl<P: Pixel + PartialEq> PartialEq for Image<P> {
fn eq(&self, other: &Self) -> bool {
self.width == other.width && self.height == other.height && self.data == other.data
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TransformImage(pub DAffine2);
impl core_types::CacheHash for TransformImage {
fn cache_hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
core_types::CacheHash::cache_hash(&self.0, state);
}
}
impl<P: Pixel + std::fmt::Debug> std::fmt::Debug for Image<P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let length = self.data.len();
f.debug_struct("Image")
.field("width", &self.width)
.field("height", &self.height)
.field("data", if length < 100 { &self.data } else { &length })
.finish()
}
}
unsafe impl<P> StaticType for Image<P>
where
P: dyn_any::StaticTypeSized + Pixel,
P::Static: Pixel,
{
type Static = Image<P::Static>;
}
impl<P: Copy + Pixel> Bitmap for Image<P> {
type Pixel = P;
#[inline(always)]
fn get_pixel(&self, x: u32, y: u32) -> Option<P> {
self.data.get((x + y * self.width) as usize).copied()
}
#[inline(always)]
fn width(&self) -> u32 {
self.width
}
#[inline(always)]
fn height(&self) -> u32 {
self.height
}
}
impl<P: Copy + Pixel> BitmapMut for Image<P> {
fn get_pixel_mut(&mut self, x: u32, y: u32) -> Option<&mut P> {
self.data.get_mut((x + y * self.width) as usize)
}
}
impl<P: core_types::CacheHash + Pixel> core_types::CacheHash for Image<P> {
fn cache_hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
core_types::CacheHash::cache_hash(&self.width, state);
core_types::CacheHash::cache_hash(&self.height, state);
core_types::CacheHash::cache_hash(&self.data, state);
}
}
impl<P: Pixel> Image<P> {
pub fn new(width: u32, height: u32, color: P) -> Self {
Self {
width,
height,
data: vec![color; (width * height) as usize],
base64_string: None,
}
}
}
impl Image<Color> {
/// Generate Image from some frontend image data (the canvas pixels as u8s in a flat array)
pub fn from_image_data(image_data: &[u8], width: u32, height: u32) -> Self {
let data = image_data
.chunks_exact(4)
.map(|v| {
// `Image<Color>` pixels are stored linear-light with premultiplied alpha
let srgba = SRGBA8::new(v[0], v[1], v[2], v[3]);
Color::from(srgba).apply_opacity(v[3] as f32 / 255.)
})
.collect();
Image {
width,
height,
data,
base64_string: None,
}
}
pub fn to_png(&self) -> Vec<u8> {
use ::image::ImageEncoder;
let (data, width, height) = self.to_flat_u8();
let mut png = Vec::new();
let encoder = ::image::codecs::png::PngEncoder::new(&mut png);
encoder.write_image(&data, width, height, ::image::ExtendedColorType::Rgba8).expect("failed to encode image as png");
png
}
}
use super::*;
impl<P: Alpha + RGB + AssociatedAlpha> Image<P>
where
P::ColorChannel: Linear,
<P as Alpha>::AlphaChannel: Linear,
{
/// Flattens each channel cast to a u8
pub fn to_flat_u8(&self) -> (Vec<u8>, u32, u32) {
let Image { width, height, data, .. } = self;
assert_eq!(data.len(), *width as usize * *height as usize);
// Cache the last sRGB value we computed, speeds up fills.
let mut last_r = 0.;
let mut last_r_srgb = 0u8;
let mut last_g = 0.;
let mut last_g_srgb = 0u8;
let mut last_b = 0.;
let mut last_b_srgb = 0u8;
let mut result = vec![0; data.len() * 4];
for (color, out) in data.iter().zip(result.chunks_exact_mut(4)) {
let a = color.a().to_f32();
// Smaller alpha values than this would map to fully transparent
// anyway, avoid expensive encoding.
if a >= 0.5 / 255. {
let undo_premultiply = 1. / a;
let r = color.r().to_f32() * undo_premultiply;
let g = color.g().to_f32() * undo_premultiply;
let b = color.b().to_f32() * undo_premultiply;
// Compute new sRGB value if necessary.
if r != last_r {
last_r = r;
last_r_srgb = float_to_srgb_u8(r);
}
if g != last_g {
last_g = g;
last_g_srgb = float_to_srgb_u8(g);
}
if b != last_b {
last_b = b;
last_b_srgb = float_to_srgb_u8(b);
}
out[0] = last_r_srgb;
out[1] = last_g_srgb;
out[2] = last_b_srgb;
out[3] = (a * 255. + 0.5) as u8;
}
}
(result, *width, *height)
}
}
impl<P: Pixel> IntoIterator for Image<P> {
type Item = P;
type IntoIter = std::vec::IntoIter<P>;
fn into_iter(self) -> Self::IntoIter {
self.data.into_iter()
}
}
impl<P: std::fmt::Debug + Copy + Pixel> Sample for Image<P> {
type Pixel = P;
// TODO: Improve sampling logic
#[inline(always)]
fn sample(&self, pos: DVec2, _area: DVec2) -> Option<Self::Pixel> {
let image_size = DVec2::new(self.width() as f64, self.height() as f64);
if pos.x < 0. || pos.y < 0. || pos.x >= image_size.x || pos.y >= image_size.y {
return None;
}
self.get_pixel(pos.x as u32, pos.y as u32)
}
}
impl<P: Copy + Pixel> Image<P> {
pub fn get_mut(&mut self, x: usize, y: usize) -> &mut P {
&mut self.data[y * (self.width as usize) + x]
}
/// Clamps the provided point to ((0, 0), (ImageSize.x, ImageSize.y)) and returns the closest pixel
pub fn sample(&self, position: DVec2) -> P {
let x = position.x.clamp(0., self.width as f64 - 1.) as usize;
let y = position.y.clamp(0., self.height as f64 - 1.) as usize;
self.data[x + y * self.width as usize]
}
}
impl<P: Pixel> AsRef<Image<P>> for Image<P> {
fn as_ref(&self) -> &Image<P> {
self
}
}
#[cfg(test)]
mod test {
#[test]
fn test_image_serialization_roundtrip() {
use super::*;
use crate::Color;
let image = Image {
width: 2,
height: 2,
data: vec![Color::WHITE, Color::BLACK, Color::RED, Color::GREEN],
base64_string: None,
};
let serialized = serde_json::to_string(&image).unwrap();
println!("{serialized}");
let deserialized: Image<Color> = serde_json::from_str(&serialized).unwrap();
println!("{deserialized:?}");
assert_eq!(image, deserialized);
}
}
@@ -0,0 +1,82 @@
pub mod image;
pub mod raster_types;
// Re-exports for convenience
pub use image::Image;
pub use raster_types::*;
// Re-export color types from no-std-types
pub use core_types::color::*;
/// as to not yet rename all references
pub mod color {
pub use super::*;
}
use std::fmt::Debug;
pub trait Bitmap {
type Pixel: Pixel;
fn width(&self) -> u32;
fn height(&self) -> u32;
fn dimensions(&self) -> (u32, u32) {
(self.width(), self.height())
}
fn dim(&self) -> (u32, u32) {
self.dimensions()
}
fn get_pixel(&self, x: u32, y: u32) -> Option<Self::Pixel>;
}
impl<T: Bitmap> Bitmap for &T {
type Pixel = T::Pixel;
fn width(&self) -> u32 {
(**self).width()
}
fn height(&self) -> u32 {
(**self).height()
}
fn get_pixel(&self, x: u32, y: u32) -> Option<Self::Pixel> {
(**self).get_pixel(x, y)
}
}
impl<T: Bitmap> Bitmap for &mut T {
type Pixel = T::Pixel;
fn width(&self) -> u32 {
(**self).width()
}
fn height(&self) -> u32 {
(**self).height()
}
fn get_pixel(&self, x: u32, y: u32) -> Option<Self::Pixel> {
(**self).get_pixel(x, y)
}
}
pub trait BitmapMut: Bitmap {
fn get_pixel_mut(&mut self, x: u32, y: u32) -> Option<&mut Self::Pixel>;
fn set_pixel(&mut self, x: u32, y: u32, pixel: Self::Pixel) {
*self.get_pixel_mut(x, y).unwrap() = pixel;
}
fn map_pixels<F: Fn(Self::Pixel) -> Self::Pixel>(&mut self, map_fn: F) {
for y in 0..self.height() {
for x in 0..self.width() {
let pixel = self.get_pixel(x, y).unwrap();
self.set_pixel(x, y, map_fn(pixel));
}
}
}
}
impl<T: BitmapMut + Bitmap> BitmapMut for &mut T {
fn get_pixel_mut(&mut self, x: u32, y: u32) -> Option<&mut Self::Pixel> {
(*self).get_pixel_mut(x, y)
}
}
@@ -0,0 +1,293 @@
use crate::image::Image;
use core::ops::Deref;
use core_types::Color;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::math::quad::Quad;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use std::fmt::Debug;
use std::ops::DerefMut;
mod __private {
pub trait Sealed {}
}
pub trait Storage: __private::Sealed + Clone + Debug + 'static {
fn is_empty(&self) -> bool;
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct Raster<T>
where
Raster<T>: Storage,
{
storage: T,
}
unsafe impl<T> dyn_any::StaticType for Raster<T>
where
Raster<T>: Storage,
{
type Static = Raster<T>;
}
impl<T> Raster<T>
where
Raster<T>: Storage,
{
pub fn new(t: T) -> Self {
Self { storage: t }
}
}
impl<T> Deref for Raster<T>
where
Raster<T>: Storage,
{
type Target = T;
fn deref(&self) -> &Self::Target {
&self.storage
}
}
impl<T> DerefMut for Raster<T>
where
Raster<T>: Storage,
{
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.storage
}
}
impl<T> core_types::CacheHash for Raster<T>
where
Raster<T>: Storage,
T: core_types::CacheHash,
{
fn cache_hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
core_types::CacheHash::cache_hash(&self.storage, state);
}
}
pub use cpu::CPU;
mod cpu {
use super::*;
use crate::raster_types::__private::Sealed;
#[derive(Clone, Debug, Default, PartialEq, core_types::CacheHash, DynAny)]
pub struct CPU(Image<Color>);
impl Sealed for Raster<CPU> {}
impl Storage for Raster<CPU> {
fn is_empty(&self) -> bool {
self.0.height == 0 || self.0.width == 0
}
}
impl Raster<CPU> {
pub fn new_cpu(image: Image<Color>) -> Self {
Self::new(CPU(image))
}
pub fn data(&self) -> &Image<Color> {
self
}
pub fn data_mut(&mut self) -> &mut Image<Color> {
self
}
pub fn into_data(self) -> Image<Color> {
self.storage.0
}
}
impl Deref for CPU {
type Target = Image<Color>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for CPU {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'de> serde::Deserialize<'de> for Raster<CPU> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Raster::new_cpu(Image::deserialize(deserializer)?))
}
}
impl serde::Serialize for Raster<CPU> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
}
pub use gpu::GPU;
#[cfg(feature = "wgpu")]
pub use gpu::Texture;
#[cfg(feature = "wgpu")]
mod gpu {
use super::*;
use crate::raster_types::__private::Sealed;
use std::sync::Arc;
#[derive(Clone, Debug, PartialEq, Eq, Hash, DynAny)]
pub struct Texture(Arc<wgpu::Texture>);
impl Deref for Texture {
type Target = wgpu::Texture;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<wgpu::Texture> for Texture {
fn as_ref(&self) -> &wgpu::Texture {
&self.0
}
}
impl From<Arc<wgpu::Texture>> for Texture {
fn from(texture: Arc<wgpu::Texture>) -> Self {
Self(texture)
}
}
impl From<wgpu::Texture> for Texture {
fn from(texture: wgpu::Texture) -> Self {
Self(Arc::new(texture))
}
}
impl From<Texture> for Arc<wgpu::Texture> {
fn from(texture: Texture) -> Self {
texture.0
}
}
impl core_types::CacheHash for Texture {
fn cache_hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
use ::core::hash::Hash;
self.hash(state);
}
}
#[derive(Clone, Debug, PartialEq, Hash)]
pub struct GPU {
pub texture: Texture,
}
impl core_types::CacheHash for GPU {
fn cache_hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
use ::core::hash::Hash;
self.texture.hash(state);
}
}
impl Sealed for Raster<GPU> {}
impl Storage for Raster<GPU> {
fn is_empty(&self) -> bool {
self.texture.width() == 0 || self.texture.height() == 0
}
}
impl Raster<GPU> {
pub fn new_gpu(texture: impl Into<Texture>) -> Self {
Self::new(GPU { texture: texture.into() })
}
pub fn data(&self) -> &wgpu::Texture {
&self.texture
}
}
}
#[cfg(not(feature = "wgpu"))]
mod gpu {
use super::*;
use crate::raster_types::__private::Sealed;
#[derive(Clone, Debug, PartialEq, Hash, core_types::CacheHash)]
pub struct GPU;
impl Sealed for Raster<GPU> {}
impl Storage for Raster<GPU> {
fn is_empty(&self) -> bool {
true
}
}
}
mod gpu_common {
use super::*;
impl<'de> serde::Deserialize<'de> for Raster<GPU> {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
unimplemented!()
}
}
impl serde::Serialize for Raster<GPU> {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
unimplemented!()
}
}
}
impl<T> BoundingBox for Raster<T>
where
Raster<T>: Storage,
{
fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
if self.is_empty() || transform.matrix2.determinant() == 0. {
return RenderBoundingBox::None;
}
let unit_rectangle = Quad::from_box([DVec2::ZERO, DVec2::ONE]);
RenderBoundingBox::Rectangle((transform * unit_rectangle).bounding_box())
}
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
self.bounding_box(transform, include_stroke)
}
}
// RenderComplexity trait implementations
impl core_types::render_complexity::RenderComplexity for Raster<CPU> {
fn render_complexity(&self) -> usize {
(self.width * self.height / 500) as usize
}
}
impl core_types::render_complexity::RenderComplexity for Raster<GPU> {
fn render_complexity(&self) -> usize {
// GPU textures currently can't have a thumbnail
usize::MAX
}
}
@@ -0,0 +1,36 @@
[package]
name = "rendering"
version = "0.1.0"
edition = "2024"
description = "SVG rendering for Graphene"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde", "core-types/serde", "vector-types/serde", "graphic-types/serde"]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
core-types = { workspace = true }
graphene-hash = { workspace = true }
graphene-resource = { workspace = true }
text-nodes = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
base64 = { workspace = true }
log = { workspace = true }
num-traits = { workspace = true }
usvg = { workspace = true }
kurbo = { workspace = true }
vector-types = { workspace = true }
graphic-types = { workspace = true }
vello = { workspace = true }
vello_encoding = { workspace = true }
parley = { workspace = true }
skrifa = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true }
@@ -0,0 +1,47 @@
use glam::DVec2;
use vector_types::subpath::{ManipulatorGroup, Subpath};
use vector_types::vector::PointId;
pub fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<PointId>> {
let mut subpaths = Vec::new();
let mut manipulators_list = Vec::new();
let mut points = path.data().points().iter();
let to_vec = |p: &usvg::tiny_skia_path::Point| DVec2::new(p.x as f64, p.y as f64);
for verb in path.data().verbs() {
match verb {
usvg::tiny_skia_path::PathVerb::Move => {
subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), false));
let Some(start) = points.next().map(to_vec) else { continue };
manipulators_list.push(ManipulatorGroup::new(start, Some(start), Some(start)));
}
usvg::tiny_skia_path::PathVerb::Line => {
let Some(end) = points.next().map(to_vec) else { continue };
manipulators_list.push(ManipulatorGroup::new(end, Some(end), Some(end)));
}
usvg::tiny_skia_path::PathVerb::Quad => {
let Some(handle) = points.next().map(to_vec) else { continue };
let Some(end) = points.next().map(to_vec) else { continue };
if let Some(last) = manipulators_list.last_mut() {
last.out_handle = Some(last.anchor + (2. / 3.) * (handle - last.anchor));
}
manipulators_list.push(ManipulatorGroup::new(end, Some(end + (2. / 3.) * (handle - end)), Some(end)));
}
usvg::tiny_skia_path::PathVerb::Cubic => {
let Some(first_handle) = points.next().map(to_vec) else { continue };
let Some(second_handle) = points.next().map(to_vec) else { continue };
let Some(end) = points.next().map(to_vec) else { continue };
if let Some(last) = manipulators_list.last_mut() {
last.out_handle = Some(first_handle);
}
manipulators_list.push(ManipulatorGroup::new(end, Some(second_handle), Some(end)));
}
usvg::tiny_skia_path::PathVerb::Close => {
subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), true));
}
}
}
subpaths.push(Subpath::new(manipulators_list, false));
subpaths
}
@@ -0,0 +1,6 @@
pub mod convert_usvg_path;
pub mod render_ext;
mod renderer;
pub mod to_peniko;
pub use renderer::*;
@@ -0,0 +1,316 @@
use crate::renderer::{RenderParams, format_transform_matrix, gradient_placement, transform_is_invertible};
use crate::{Render, RenderSvgSegmentList, SvgRender};
use core_types::Color;
use core_types::attribute::Transform;
use core_types::color::SRGBA8;
use core_types::list::List;
use core_types::uuid::generate_uuid;
use glam::{DAffine2, DVec2};
use graphic_types::Graphic;
use graphic_types::vector_types::gradient::GradientType;
use graphic_types::vector_types::markers::{GradientType as GradientTypeAttr, SpreadMethod};
use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use std::fmt::Write;
use vector_types::Gradient;
use vector_types::gradient::GradientSpreadMethod;
#[derive(Copy, Clone, PartialEq)]
pub enum PaintTarget {
Fill,
Stroke,
}
impl PaintTarget {
fn paint_attr(self) -> &'static str {
match self {
Self::Fill => "fill",
Self::Stroke => "stroke",
}
}
fn opacity_attr(self) -> &'static str {
match self {
Self::Fill => "fill-opacity",
Self::Stroke => "stroke-opacity",
}
}
}
pub trait RenderExt {
type Output;
#[allow(clippy::too_many_arguments)]
fn render(
&self,
svg_defs: &mut String,
item_transform: DAffine2,
element_transform: DAffine2,
stroke_transform: DAffine2,
bounds: DAffine2,
render_params: &RenderParams,
target: PaintTarget,
) -> Self::Output;
}
/// The color paint attribute over any color lane source.
pub fn render_color_paint<S: core_types::lane::LaneSource<Element = Color>>(source: &S, target: PaintTarget) -> String {
let Some(color) = source.element(0) else {
return format!(r#" {}="none""#, target.paint_attr());
};
let mut result = format!(r##" {}="#{}""##, target.paint_attr(), SRGBA8::from(*color).to_rgb_hex());
if color.a() < 1. {
let _ = write!(result, r#" {}="{}""#, target.opacity_attr(), (color.a() * 1000.).round() / 1000.);
}
result
}
impl RenderExt for List<Color> {
type Output = String;
fn render(
&self,
_svg_defs: &mut String,
_item_transform: DAffine2,
_element_transform: DAffine2,
_stroke_transform: DAffine2,
_bounds: DAffine2,
_render_params: &RenderParams,
target: PaintTarget,
) -> Self::Output {
render_color_paint(self, target)
}
}
impl RenderExt for List<Gradient> {
type Output = u64;
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
fn render(
&self,
svg_defs: &mut String,
item_transform: DAffine2,
element_transform: DAffine2,
_stroke_transform: DAffine2,
_bounds: DAffine2,
_render_params: &RenderParams,
_target: PaintTarget,
) -> Self::Output {
render_gradient_paint(self, svg_defs, item_transform, element_transform)
}
}
/// Adds the gradient def through mutating `svg_defs`, returning the gradient
/// ID, over any gradient lane source.
pub fn render_gradient_paint<S: core_types::lane::LaneSource<Element = GradientStops>>(source: &S, svg_defs: &mut String, item_transform: DAffine2, element_transform: DAffine2) -> u64 {
let mut stop = String::new();
{
let Some(stops) = source.element(0) else { return 0 };
let gradient_type: GradientType = source.attr::<GradientTypeAttr>(0);
let local_gradient_transform: DAffine2 = source.attr::<Transform>(0);
let spread_method: GradientSpreadMethod = source.attr::<SpreadMethod>(0);
for (position, color, original_midpoint) in stops.interpolated_samples() {
stop.push_str("<stop");
if position != 0. {
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
}
let _ = write!(stop, r##" stop-color="#{}""##, SRGBA8::from(color).to_rgb_hex());
if color.a() < 1. {
let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
if let Some(midpoint) = original_midpoint {
let _ = write!(stop, r#" graphite:midpoint="{}""#, (midpoint * 1000.).round() / 1000.);
}
stop.push_str(" />")
}
// Need to cancel out the element's transform as it is already applied to the path itself.
let element_transform_inverse = if transform_is_invertible(element_transform) {
element_transform.inverse()
} else {
DAffine2::IDENTITY
};
let document_transform = item_transform * local_gradient_transform;
let placement = gradient_placement(document_transform, gradient_type);
let gradient_transform = format_transform_matrix(element_transform_inverse * placement);
let gradient_transform = if gradient_transform.is_empty() {
String::new()
} else {
format!(r#" gradientTransform="{gradient_transform}""#)
};
let spread_method = if spread_method == GradientSpreadMethod::Pad {
String::new()
} else {
format!(r#" spreadMethod="{}""#, spread_method.svg_name())
};
let gradient_id = generate_uuid();
match gradient_type {
GradientType::Linear => {
let _ = write!(
svg_defs,
r#"<linearGradient id="{}" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0"{spread_method}{gradient_transform}>{}</linearGradient>"#,
gradient_id, stop
);
}
GradientType::Radial => {
let _ = write!(
svg_defs,
r#"<radialGradient id="{}" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="1"{spread_method}{gradient_transform}>{}</radialGradient>"#,
gradient_id, stop
);
}
}
gradient_id
}
}
impl RenderExt for Stroke {
type Output = String;
/// Provide the shape-related SVG attributes for the stroke. The paint-related attributes for the stroke are generated from `List<Graphic>.render` with `PaintTarget::Stroke`.
fn render(
&self,
_svg_defs: &mut String,
_item_transform: DAffine2,
_element_transform: DAffine2,
_stroke_transform: DAffine2,
_bounds: DAffine2,
render_params: &RenderParams,
_target: PaintTarget,
) -> Self::Output {
// Don't render a stroke at all if it would be invisible
if !self.has_renderable_stroke() {
return String::new();
}
let default_weight = if self.align != StrokeAlign::Center && render_params.aligned_strokes { 1. / 2. } else { 1. };
// Set to None if the value is the SVG default
let weight = (self.weight != default_weight).then_some(self.weight);
let dash_array = (!self.dash_lengths.is_empty()).then_some(self.dash_lengths());
let dash_offset = (self.dash_offset != 0.).then_some(self.dash_offset);
let stroke_cap = (self.cap != StrokeCap::Butt).then_some(self.cap);
let stroke_join = (self.join != StrokeJoin::Miter).then_some(self.join);
let stroke_join_miter_limit = (self.join_miter_limit != 4.).then_some(self.join_miter_limit);
let stroke_align = (self.align != StrokeAlign::Center).then_some(self.align);
let paint_order = (self.paint_order != PaintOrder::StrokeAbove || render_params.override_paint_order).then_some(PaintOrder::StrokeBelow);
// Render the needed stroke attributes
let mut attributes = String::new();
if let Some(mut weight) = weight {
if stroke_align.is_some() && render_params.aligned_strokes {
weight *= 2.;
}
let _ = write!(&mut attributes, r#" stroke-width="{weight}""#);
}
if let Some(dash_array) = dash_array {
let _ = write!(&mut attributes, r#" stroke-dasharray="{dash_array}""#);
}
if let Some(dash_offset) = dash_offset {
let _ = write!(&mut attributes, r#" stroke-dashoffset="{dash_offset}""#);
}
if let Some(stroke_cap) = stroke_cap {
let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, stroke_cap.svg_name());
}
if let Some(stroke_join) = stroke_join {
let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, stroke_join.svg_name());
}
if let Some(stroke_join_miter_limit) = stroke_join_miter_limit {
let _ = write!(&mut attributes, r#" stroke-miterlimit="{stroke_join_miter_limit}""#);
}
if paint_order.is_some() {
let _ = write!(&mut attributes, r#" style="paint-order: stroke;" "#);
}
attributes
}
}
impl RenderExt for List<Graphic<'_>> {
type Output = String;
fn render(
&self,
svg_defs: &mut String,
item_transform: DAffine2,
element_transform: DAffine2,
stroke_transform: DAffine2,
bounds: DAffine2,
render_params: &RenderParams,
target: PaintTarget,
) -> Self::Output {
let fill_graphic = self.element(0);
let paint_attr = target.paint_attr();
match fill_graphic {
Some(Graphic::Color(color)) => render_color_paint(&core_types::lane::LeafLane::new(self, 0, color), target),
Some(Graphic::Gradient(gradient)) => {
let gradient_id = render_gradient_paint(&core_types::lane::LeafLane::new(self, 0, gradient), svg_defs, item_transform, element_transform);
format!(r##" {paint_attr}="url(#{gradient_id})""##)
}
Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) | Some(Graphic::Text(_)) | Some(Graphic::Group(_)) => {
let bounds = if target == PaintTarget::Stroke {
// To prevent a wraparound artefact occurring when the tile boundary and the stroke region are perfectly aligned, the local coordinate is expanded slightly.
let inverse = |len: f64| if len > 0. { 1. / len } else { 0. };
let inflate = DVec2::new(inverse(item_transform.matrix2.x_axis.length()), inverse(item_transform.matrix2.y_axis.length()));
let min = bounds.transform_point2(DVec2::ZERO) - inflate;
let max = bounds.transform_point2(DVec2::ONE) + inflate;
DAffine2::from_scale_angle_translation(max - min, 0., min)
} else {
bounds
};
render_svg_pattern(svg_defs, self, stroke_transform, bounds, render_params)
.map(|id| format!(r##" {paint_attr}="url(#{id})""##))
.unwrap_or_else(|| format!(r#" {paint_attr}="none""#))
}
None => format!(r#" {paint_attr}="none""#),
}
}
}
/// Emits an SVG `<pattern>` paint server into `svg_defs` that renders the given graphic list as the paint content, and returns the pattern ID.
/// Currently, this function is only used for clipping-based filling and stroking, not considering tiling yet.
fn render_svg_pattern(svg_defs: &mut String, fill_graphic_list: &List<Graphic>, stroke_transform: DAffine2, bounds: DAffine2, render_params: &RenderParams) -> Option<String> {
let min = bounds.transform_point2(DVec2::ZERO);
let max = bounds.transform_point2(DVec2::ONE);
let size = max - min;
if size.x <= 0. || size.y <= 0. {
return None;
}
// Render the pattern content recursively
let mut content = SvgRender::new();
fill_graphic_list.render_svg(&mut content, &render_params.for_pattern());
// Unwrap the inner def element
write!(svg_defs, "{}", content.svg_defs).unwrap();
let pattern_transform = stroke_transform * DAffine2::from_translation(min);
let transform_str = format_transform_matrix(pattern_transform);
let transform_attr = if transform_str.is_empty() {
String::new()
} else {
format!(r#" patternTransform="{transform_str}""#)
};
let pattern_id = format!("pattern-{}", generate_uuid());
write!(
svg_defs,
r##"<pattern id="{pattern_id}" patternUnits="userSpaceOnUse" x="0" y="0" width="{}" height="{}"{transform_attr}>"##,
size.x, size.y,
)
.unwrap();
let content_shift = format_transform_matrix(DAffine2::from_translation(-min));
write!(svg_defs, r##"<g transform="{content_shift}">{}</g></pattern>"##, content.svg.to_svg_string()).unwrap();
Some(pattern_id)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,48 @@
use core_types::BlendMode;
use core_types::color::SRGBA8;
use vello::peniko;
pub trait BlendModeExt {
fn to_peniko(&self) -> peniko::Mix;
}
pub trait ToPenikoColor {
fn to_peniko_color(&self) -> peniko::Color;
}
impl ToPenikoColor for SRGBA8 {
#[inline(always)]
fn to_peniko_color(&self) -> peniko::Color {
peniko::Color::from_rgba8(self.red, self.green, self.blue, self.alpha)
}
}
impl BlendModeExt for BlendMode {
fn to_peniko(&self) -> peniko::Mix {
match self {
// Normal group
BlendMode::Normal => peniko::Mix::Normal,
// Darken group
BlendMode::Darken => peniko::Mix::Darken,
BlendMode::Multiply => peniko::Mix::Multiply,
BlendMode::ColorBurn => peniko::Mix::ColorBurn,
// Lighten group
BlendMode::Lighten => peniko::Mix::Lighten,
BlendMode::Screen => peniko::Mix::Screen,
BlendMode::ColorDodge => peniko::Mix::ColorDodge,
// Contrast group
BlendMode::Overlay => peniko::Mix::Overlay,
BlendMode::SoftLight => peniko::Mix::SoftLight,
BlendMode::HardLight => peniko::Mix::HardLight,
// Inversion group
BlendMode::Difference => peniko::Mix::Difference,
BlendMode::Exclusion => peniko::Mix::Exclusion,
// Component group
BlendMode::Hue => peniko::Mix::Hue,
BlendMode::Saturation => peniko::Mix::Saturation,
BlendMode::Color => peniko::Mix::Color,
BlendMode::Luminosity => peniko::Mix::Luminosity,
_ => todo!(),
}
}
}
@@ -0,0 +1,23 @@
[package]
name = "graphene-resource"
version = "0.1.0"
edition = "2024"
description = "graphene resource interface"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde", "url/serde"]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
core-types = { workspace = true }
# Workspace dependencies
blake3 = { workspace = true }
url = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true }
@@ -0,0 +1,373 @@
use core_types::{CacheHash, graphene_hash};
use dyn_any::DynAny;
use std::collections::HashMap;
use std::future::Future;
use std::hash::Hash;
use std::ops::Deref;
use std::pin::Pin;
use std::sync::Arc;
#[derive(Clone, DynAny)]
pub struct Resource {
inner: Arc<dyn AsRef<[u8]> + Send + Sync>,
hash: ResourceHash,
}
impl Resource {
pub fn new<T: AsRef<[u8]> + Send + Sync + 'static>(data: T) -> Self {
let hash = ResourceHash::from(data.as_ref());
Self { inner: Arc::new(data), hash }
}
pub fn new_unchecked<T: AsRef<[u8]> + Send + Sync + 'static>(data: T, hash: ResourceHash) -> Self {
Self { inner: Arc::new(data), hash }
}
pub fn hash(&self) -> ResourceHash {
self.hash
}
pub fn empty() -> Self {
Self::new([])
}
}
impl Default for Resource {
fn default() -> Self {
Self::empty()
}
}
impl Default for &Resource {
fn default() -> Self {
static EMPTY: std::sync::LazyLock<Resource> = std::sync::LazyLock::new(Resource::empty);
std::sync::LazyLock::force(&EMPTY)
}
}
impl From<&Resource> for Arc<dyn AsRef<[u8]> + Send + Sync> {
fn from(val: &Resource) -> Self {
val.inner.clone()
}
}
impl Deref for Resource {
type Target = [u8];
fn deref(&self) -> &[u8] {
(*self.inner).as_ref()
}
}
impl AsRef<[u8]> for Resource {
fn as_ref(&self) -> &[u8] {
(*self.inner).as_ref()
}
}
impl std::fmt::Debug for Resource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Resource").field("len", &self.len()).finish()
}
}
impl PartialEq for Resource {
fn eq(&self, other: &Self) -> bool {
self.hash == other.hash
}
}
impl CacheHash for Resource {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.hash.cache_hash(state);
}
}
/// Blake3 content hash of a resource, represented as 32 bytes
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Ord, DynAny)]
pub struct ResourceHash([u8; 32]);
impl From<&[u8]> for ResourceHash {
fn from(data: &[u8]) -> Self {
Self(blake3::hash(data).into())
}
}
impl From<[u8; 32]> for ResourceHash {
fn from(bytes: [u8; 32]) -> Self {
Self(bytes)
}
}
impl From<&ResourceHash> for [u8; 32] {
fn from(hash: &ResourceHash) -> Self {
hash.0
}
}
impl From<&ResourceHash> for String {
fn from(hash: &ResourceHash) -> Self {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(hash.0.len() * 2);
for byte in &hash.0 {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}
}
impl std::fmt::Display for ResourceHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&String::from(self))
}
}
impl std::str::FromStr for ResourceHash {
type Err = ResourceHashParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
fn decode_hex_nibble(byte: u8, position: usize) -> Result<u8, ResourceHashParseError> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
b'A'..=b'F' => Ok(byte - b'A' + 10),
_ => Err(ResourceHashParseError::InvalidCharacter { byte, position }),
}
}
let bytes = s.as_bytes();
if bytes.len() != 64 {
return Err(ResourceHashParseError::InvalidLength { found: bytes.len() });
}
let mut out = [0u8; 32];
for (index, chunk) in bytes.chunks_exact(2).enumerate() {
let high = decode_hex_nibble(chunk[0], index * 2)?;
let low = decode_hex_nibble(chunk[1], index * 2 + 1)?;
out[index] = (high << 4) | low;
}
Ok(Self(out))
}
}
impl std::fmt::Debug for ResourceHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&String::from(self))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceHashParseError {
InvalidLength { found: usize },
InvalidCharacter { byte: u8, position: usize },
}
impl std::fmt::Display for ResourceHashParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidLength { found } => write!(f, "resource hash must be 64 hex characters, got {found}"),
Self::InvalidCharacter { byte, position } => write!(f, "resource hash contains non-hex byte {byte:#04x} at position {position}"),
}
}
}
impl std::error::Error for ResourceHashParseError {}
impl TryFrom<&str> for ResourceHash {
type Error = ResourceHashParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for ResourceHash {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if serializer.is_human_readable() {
serializer.serialize_str(&String::from(self))
} else {
serializer.serialize_bytes(&self.0)
}
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for ResourceHash {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct ResourceHashVisitor;
impl<'de> serde::de::Visitor<'de> for ResourceHashVisitor {
type Value = ResourceHash;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a 64-character hex string or 32 raw bytes")
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
ResourceHash::try_from(value).map_err(E::custom)
}
fn visit_bytes<E: serde::de::Error>(self, value: &[u8]) -> Result<Self::Value, E> {
let bytes: [u8; 32] = value.try_into().map_err(|_| E::invalid_length(value.len(), &"32 bytes"))?;
Ok(ResourceHash(bytes))
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut bytes = [0u8; 32];
for (i, slot) in bytes.iter_mut().enumerate() {
*slot = seq.next_element()?.ok_or_else(|| serde::de::Error::invalid_length(i, &"32 bytes"))?;
}
Ok(ResourceHash(bytes))
}
}
if deserializer.is_human_readable() {
deserializer.deserialize_str(ResourceHashVisitor)
} else {
deserializer.deserialize_bytes(ResourceHashVisitor)
}
}
}
impl CacheHash for ResourceHash {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(self, state);
}
}
pub trait LoadResource: Send + Sync {
fn load(&self, hash: ResourceHash) -> ResourceFuture<'_>;
}
#[cfg(not(target_family = "wasm"))]
pub type ResourceFuture<'a> = Pin<Box<dyn Future<Output = Option<Resource>> + Send + 'a>>;
#[cfg(target_family = "wasm")]
pub type ResourceFuture<'a> = Pin<Box<dyn Future<Output = Option<Resource>> + 'a>>;
pub trait ResourceStorage: LoadResource {
fn store(&self, data: &[u8]) -> ResourceHash;
fn contains(&self, hash: &ResourceHash) -> bool;
fn garbage_collect(&self, used: &[ResourceHash]);
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, PartialOrd, Ord, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ResourceId(u64);
impl ResourceId {
pub fn new() -> Self {
Self(core_types::uuid::generate_uuid())
}
/// Derive a deterministic ID from a content hash (first 8 bytes, little-endian). Used when
/// bootstrapping resources from an existing document so re-conversion is stable and identical
/// content maps to one ID. New resources created live should use [`ResourceId::new`] instead.
pub fn from_hash(hash: &ResourceHash) -> Self {
let bytes: [u8; 32] = hash.into();
let mut truncated = [0u8; 8];
truncated.copy_from_slice(&bytes[..8]);
Self(u64::from_le_bytes(truncated))
}
}
impl From<u64> for ResourceId {
fn from(value: u64) -> Self {
Self(value)
}
}
impl From<ResourceId> for u64 {
fn from(id: ResourceId) -> Self {
id.0
}
}
impl std::fmt::Display for ResourceId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
pub type DataSources = Box<[DataSource]>;
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DataSource {
Embedded,
Url(url::Url),
Font { family: String, style: Option<String> },
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ResourceRegistry {
hashes: HashMap<ResourceId, ResourceHash>,
sources: HashMap<ResourceId, Vec<DataSource>>,
}
impl ResourceRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.hashes.is_empty() && self.sources.is_empty()
}
pub fn contains(&self, id: &ResourceId) -> bool {
self.hashes.contains_key(id) || self.sources.contains_key(id)
}
pub fn ids(&self) -> impl Iterator<Item = ResourceId> + '_ {
self.hashes.keys().chain(self.sources.keys().filter(|id| !self.hashes.contains_key(id))).copied()
}
pub fn info(&self, id: &ResourceId) -> Option<ResourceInfo<'_>> {
self.contains(id).then(|| ResourceInfo {
id: *id,
hash: self.hashes.get(id),
sources: self.sources.get(id).map(|sources| sources.as_slice()).unwrap_or(&[]),
})
}
pub fn push_source_back(&mut self, id: &ResourceId, source: DataSource) {
self.sources.entry(*id).or_default().push(source);
}
pub fn push_source_front(&mut self, id: &ResourceId, source: DataSource) {
self.sources.entry(*id).or_default().insert(0, source);
}
pub fn delete(&mut self, id: &ResourceId) -> bool {
let hash = self.hashes.remove(id);
let sources = self.sources.remove(id);
!(hash.is_none() && sources.is_none())
}
pub fn resolve(&mut self, id: &ResourceId, hash: ResourceHash) -> Option<ResourceHash> {
self.hashes.insert(*id, hash)
}
pub fn hash(&self, id: &ResourceId) -> Option<ResourceHash> {
self.hashes.get(id).copied()
}
pub fn unresolved(&self) -> impl Iterator<Item = ResourceInfo<'_>> + '_ {
self.sources.keys().filter(|id| !self.hashes.contains_key(id)).filter_map(|id| self.info(id))
}
pub fn resolved(&self) -> impl Iterator<Item = ResourceInfo<'_>> + '_ {
self.hashes.keys().filter_map(|id| self.info(id))
}
}
#[derive(Clone, Debug)]
pub struct ResourceInfo<'a> {
pub id: ResourceId,
pub hash: Option<&'a ResourceHash>,
pub sources: &'a [DataSource],
}
@@ -0,0 +1,38 @@
[package]
name = "vector-types"
version = "0.1.0"
edition = "2024"
description = "Vector graphics types and algorithms for Graphene"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde", "core-types/serde"]
wasm = ["core-types/wasm", "tsify", "wasm-bindgen"]
[dependencies]
# Local dependencies
core-types = { workspace = true }
graphene-hash = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies
bitflags = { workspace = true }
bytemuck = { workspace = true }
num-traits = { workspace = true }
glam = { workspace = true }
kurbo = { workspace = true }
lyon_geom = { workspace = true }
dyn-any = { workspace = true }
log = { workspace = true }
petgraph = { workspace = true }
rustc-hash = { workspace = true }
polycool = { workspace = true }
tinyvec = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true }
tsify = { workspace = true, optional = true }
wasm-bindgen = { workspace = true, optional = true }
fixedbitset = "0.5.7"
@@ -0,0 +1,577 @@
use core_types::Color;
use core_types::color::SRGBA8;
use core_types::render_complexity::RenderComplexity;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum GradientType {
#[default]
Linear,
Radial,
}
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
/// A list of colors (linear, unassociated alpha) associated with positions (in the range 0 to 1) along a gradient.
///
/// Not exposed via Tsify; use [`GradientUI`] at the JS boundary.
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Gradient {
/// The position of this stop, a factor from 0-1 along the length of the full gradient.
pub position: Vec<f64>,
/// The midpoint to the right of this stop, a factor from 0-1 along the distance to the next stop. The final stop's midpoint is ignored.
pub midpoint: Vec<f64>,
/// The color at this stop.
pub color: Vec<Color>,
}
/// JS-boundary version of [`Gradient`] where stop colors are [`SRGBA8`] byte triples instead of linear-light [`Color`].
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
#[derive(Debug, Clone, PartialEq, Default, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GradientUI {
pub position: Vec<f64>,
pub midpoint: Vec<f64>,
pub color: Vec<SRGBA8>,
}
impl From<&Gradient> for GradientUI {
fn from(s: &Gradient) -> Self {
Self {
position: s.position.clone(),
midpoint: s.midpoint.clone(),
color: s.color.iter().map(|c| SRGBA8::from(*c)).collect(),
}
}
}
impl From<&GradientUI> for Gradient {
fn from(s: &GradientUI) -> Self {
Self {
position: s.position.clone(),
midpoint: s.midpoint.clone(),
color: s.color.iter().map(|c| Color::from(*c)).collect(),
}
}
}
impl GradientUI {
/// CSS `linear-gradient(...)` string. Stops are emitted as `#rrggbbaa` hex (already gamma-encoded bytes).
pub fn to_css_linear_gradient(&self) -> String {
if self.position.len() <= 1 {
let hex = self.color.first().map(|c| c.to_rgba_hex()).unwrap_or_else(|| "000000ff".to_string());
return format!("linear-gradient(to right, #{hex} 0%, #{hex} 100%)");
}
// Sample via the midpoint-aware subdivision used for SVG/Vello stops so browser interpolation matches
let stops: Gradient = self.into();
let pieces = stops
.interpolated_samples()
.into_iter()
.map(|(position, color, _)| {
let percent = ((position * 100.) * 1e2).round() / 1e2;
let hex = SRGBA8::from(color).to_rgba_hex();
format!("#{hex} {percent}%")
})
.collect::<Vec<_>>()
.join(", ");
format!("linear-gradient(to right, {pieces})")
}
}
// TODO: Eventually remove this migration document upgrade code
impl<'de> serde::Deserialize<'de> for Gradient {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
struct NewFormat {
position: Vec<f64>,
midpoint: Vec<f64>,
color: Vec<Color>,
}
#[derive(serde::Deserialize)]
#[cfg_attr(feature = "serde", serde(untagged))]
enum GradientStopsFormat {
New(NewFormat),
Old(Vec<(f64, Color)>),
}
Ok(match GradientStopsFormat::deserialize(deserializer)? {
GradientStopsFormat::New(new) => Self {
position: new.position,
midpoint: new.midpoint,
color: new.color,
},
GradientStopsFormat::Old(stops) => {
let count = stops.len();
Self {
position: stops.iter().map(|(p, _)| *p).collect(),
midpoint: vec![0.5; count],
color: stops.into_iter().map(|(_, c)| c).collect(),
}
}
})
}
}
impl Default for Gradient {
fn default() -> Self {
Self {
position: vec![0., 1.],
midpoint: vec![0.5, 0.5],
color: vec![Color::BLACK, Color::WHITE],
}
}
}
impl RenderComplexity for Gradient {
fn render_complexity(&self) -> usize {
1
}
}
/// Apply the midpoint curve to a normalized parameter `t` (0 to 1) given a `midpoint` (0 to 1, where 0.5 is linear).
fn apply_midpoint(t: f64, midpoint: f64) -> f64 {
if (midpoint - 0.5).abs() < 1e-6 {
return t;
}
let midpoint = midpoint.clamp(f64::EPSILON, 1. - f64::EPSILON);
if midpoint < 0.5 {
let q = -1. / (1. - midpoint).log2();
1. - (1. - t).powf(q)
} else {
let p = -1. / midpoint.log2();
t.powf(p)
}
}
#[derive(Debug, Clone, Copy)]
pub struct GradientStop {
pub position: f64,
pub midpoint: f64,
pub color: Color,
}
pub struct GradientStopsIter<'a> {
stops: &'a Gradient,
index: usize,
}
impl<'a> Iterator for GradientStopsIter<'a> {
type Item = GradientStop;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.stops.position.len() {
return None;
}
let stop = GradientStop {
position: self.stops.position[self.index],
midpoint: self.stops.midpoint[self.index],
color: self.stops.color[self.index],
};
self.index += 1;
Some(stop)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.stops.position.len() - self.index;
(remaining, Some(remaining))
}
}
impl ExactSizeIterator for GradientStopsIter<'_> {}
impl<'a> IntoIterator for &'a Gradient {
type Item = GradientStop;
type IntoIter = GradientStopsIter<'a>;
fn into_iter(self) -> Self::IntoIter {
GradientStopsIter { stops: self, index: 0 }
}
}
impl IntoIterator for Gradient {
type Item = GradientStop;
type IntoIter = std::vec::IntoIter<GradientStop>;
fn into_iter(self) -> Self::IntoIter {
self.position
.into_iter()
.zip(self.midpoint)
.zip(self.color)
.map(|((position, midpoint), color)| GradientStop { position, midpoint, color })
.collect::<Vec<_>>()
.into_iter()
}
}
impl Gradient {
pub fn new(stops: impl IntoIterator<Item = GradientStop>) -> Self {
let mut position = Vec::new();
let mut midpoint = Vec::new();
let mut color = Vec::new();
for stop in stops {
position.push(stop.position);
midpoint.push(stop.midpoint);
color.push(stop.color);
}
Self { position, midpoint, color }
}
pub fn len(&self) -> usize {
self.position.len()
}
pub fn is_empty(&self) -> bool {
self.position.is_empty()
}
pub fn iter(&self) -> GradientStopsIter<'_> {
self.into_iter()
}
/// Remove a stop at the given index.
pub fn remove(&mut self, index: usize) {
self.position.remove(index);
self.midpoint.remove(index);
self.color.remove(index);
}
/// Remove and return the last stop's color, or `None` if empty.
pub fn pop(&mut self) -> Option<Color> {
self.position.pop();
self.midpoint.pop();
self.color.pop()
}
/// Move the stop at `index` to a new position, re-sorting the stops by position. Returns the new index of the moved stop.
pub fn move_stop(&mut self, index: usize, position: f64) -> usize {
if index >= self.position.len() {
return index;
}
self.position[index] = position;
self.sort_returning_new_index(index)
}
/// Insert a new stop at the given position, sampling the gradient at that position to determine the new stop's color.
/// The new stop's midpoint is inherited from the interval it splits (or `0.5` if inserting at the very start).
/// Returns the index where the new stop was inserted.
pub fn insert_stop(&mut self, position: f64) -> usize {
let color = self.evaluate(position);
let index = self.position.iter().position(|p| *p > position).unwrap_or(self.position.len());
let midpoint = index.checked_sub(1).and_then(|i| self.midpoint.get(i).copied()).unwrap_or(0.5);
self.position.insert(index, position);
self.midpoint.insert(index, midpoint);
self.color.insert(index, color);
index
}
/// Insert a copy of the stop at `source_index` (same color and midpoint) at `position`, keeping the stops sorted by position.
/// Returns the index where the copy was inserted, or `None` if `source_index` is out of range.
pub fn duplicate_stop(&mut self, source_index: usize, position: f64) -> Option<usize> {
let color = *self.color.get(source_index)?;
let midpoint = *self.midpoint.get(source_index)?;
let index = self.position.iter().position(|p| *p > position).unwrap_or(self.position.len());
self.position.insert(index, position);
self.midpoint.insert(index, midpoint);
self.color.insert(index, color);
Some(index)
}
/// Reset the midpoint for the interval starting at `index` to its default `0.5`.
pub fn reset_midpoint(&mut self, index: usize) {
if let Some(midpoint) = self.midpoint.get_mut(index) {
*midpoint = 0.5;
}
}
/// Sort the stops in place by position; returns the new index of the stop that was at `previous_index` before sorting.
fn sort_returning_new_index(&mut self, previous_index: usize) -> usize {
let len = self.position.len();
let mut indices: Vec<usize> = (0..len).collect();
indices.sort_by(|&a, &b| self.position[a].total_cmp(&self.position[b]));
let new_index = indices.iter().position(|&i| i == previous_index).unwrap_or(previous_index);
self.position = indices.iter().map(|&i| self.position[i]).collect();
self.midpoint = indices.iter().map(|&i| self.midpoint[i]).collect();
self.color = indices.iter().map(|&i| self.color[i]).collect();
new_index
}
pub fn evaluate(&self, t: f64) -> Color {
if self.position.is_empty() {
return Color::BLACK;
}
if t <= self.position[0] {
return self.color[0];
}
let last = self.position.len() - 1;
if t >= self.position[last] {
return self.color[last];
}
for i in 0..self.position.len() - 1 {
let (t1, c1) = (self.position[i], self.color[i]);
let (t2, c2) = (self.position[i + 1], self.color[i + 1]);
if t >= t1 && t <= t2 {
let normalized_t = (t - t1) / (t2 - t1);
let adjusted_t = apply_midpoint(normalized_t, self.midpoint[i]);
return c1.lerp(&c2, adjusted_t as f32);
}
}
Color::BLACK
}
pub fn sort(&mut self) {
let mut indices: Vec<usize> = (0..self.position.len()).collect();
indices.sort_unstable_by(|&a, &b| self.position[a].total_cmp(&self.position[b]));
self.position = indices.iter().map(|&i| self.position[i]).collect();
self.midpoint = indices.iter().map(|&i| self.midpoint[i]).collect();
self.color = indices.iter().map(|&i| self.color[i]).collect();
}
pub fn reversed(&self) -> Self {
let position: Vec<f64> = self.position.iter().rev().map(|&p| 1. - p).collect();
let count = self.midpoint.len();
let midpoint = (0..count).map(|i| if i < count - 1 { 1. - self.midpoint[count - 2 - i] } else { 0.5 }).collect::<Vec<_>>();
let color: Vec<Color> = self.color.iter().rev().cloned().collect();
Self { position, midpoint, color }
}
pub fn map_colors<F: Fn(&Color) -> Color>(&self, f: F) -> Self {
Self {
position: self.position.clone(),
midpoint: self.midpoint.clone(),
color: self.color.iter().map(f).collect(),
}
}
/// Build a CSS `linear-gradient(...)` string suitable for use as a `background-image`. Samples the midpoint curves so the rendered gradient matches Graphite's interpolation rather than browser defaults.
pub fn to_css_linear_gradient(&self) -> String {
if self.position.len() <= 1 {
let hex = self.color.first().map(|c| SRGBA8::from(*c).to_rgba_hex()).unwrap_or_else(|| "000000ff".to_string());
return format!("linear-gradient(to right, #{hex} 0%, #{hex} 100%)");
}
let pieces = self
.interpolated_samples()
.into_iter()
.map(|(position, color, _)| {
let percent = ((position * 100.) * 1e2).round() / 1e2;
format!("#{} {percent}%", SRGBA8::from(color).to_rgba_hex())
})
.collect::<Vec<_>>()
.join(", ");
format!("linear-gradient(to right, {pieces})")
}
/// Produce a set of linearly-interpolated color samples that approximate the gradient's midpoint curves.
///
/// Each sample is `(position, color, original_midpoint)` where `original_midpoint` is `Some(f64)` with the corresponding
/// midpoint for actual gradient stops, and `None` for interpolated samples added to approximate midpoint curves.
///
/// Interpolation is performed in sRGB gamma space (then lifted back to linear-light for output) because the downstream SVG/CSS
/// renderer interpolates between adjacent `<stop>` colors in gamma space; doing the subdivision math in the same space ensures
/// the chosen samples actually match the curve the browser will draw.
pub fn interpolated_samples(&self) -> Vec<(f64, Color, Option<f64>)> {
/// Controls accuracy vs. number of samples tradeoff.
/// 2/255 means the linear approximation will deviate by no more than 2 gradations of 8-bit color from the theoretically perfect curve with this midpoint bias.
const THRESHOLD: f64 = 2. / 255.;
#[allow(clippy::too_many_arguments)]
fn subdivide(left: f64, right: f64, midpoint: f64, pos_a: f64, pos_b: f64, color_a_gamma: [f32; 4], color_b_gamma: [f32; 4], result: &mut Vec<(f64, Color, Option<f64>)>, depth: u32) {
const MAX_DEPTH: u32 = 20;
if depth >= MAX_DEPTH {
return;
}
let mid = (left + right) / 2.;
let y_actual = apply_midpoint(mid, midpoint);
let y_left = apply_midpoint(left, midpoint);
let y_right = apply_midpoint(right, midpoint);
let y_linear = (y_left + y_right) / 2.;
if (y_actual - y_linear).abs() > THRESHOLD {
subdivide(left, mid, midpoint, pos_a, pos_b, color_a_gamma, color_b_gamma, result, depth + 1);
let global_pos = pos_a + mid * (pos_b - pos_a);
let t = y_actual as f32;
let r = color_a_gamma[0] + (color_b_gamma[0] - color_a_gamma[0]) * t;
let g = color_a_gamma[1] + (color_b_gamma[1] - color_a_gamma[1]) * t;
let b = color_a_gamma[2] + (color_b_gamma[2] - color_a_gamma[2]) * t;
let a = color_a_gamma[3] + (color_b_gamma[3] - color_a_gamma[3]) * t;
let color = Color::from_gamma_srgb_channels(r, g, b, a);
result.push((global_pos, color, None));
subdivide(mid, right, midpoint, pos_a, pos_b, color_a_gamma, color_b_gamma, result, depth + 1);
}
}
if self.position.is_empty() {
return vec![];
}
if self.position.len() == 1 {
return vec![(self.position[0], self.color[0], Some(self.midpoint[0]))];
}
let mut result = Vec::new();
for i in 0..self.position.len() - 1 {
let pos_a = self.position[i];
let pos_b = self.position[i + 1];
let color_a = self.color[i];
let color_b = self.color[i + 1];
let midpoint = self.midpoint[i].clamp(0.01, 0.99);
let next_midpoint = self.midpoint[i + 1].clamp(0.01, 0.99);
// Add the start stop (subsequent segments share the previous end stop)
if i == 0 {
result.push((pos_a, color_a, Some(midpoint)));
}
// Only subdivide if midpoint deviates from linear (0.5)
if (midpoint - 0.5).abs() >= 1e-6 {
subdivide(0., 1., midpoint, pos_a, pos_b, color_a.to_gamma_srgb_channels(), color_b.to_gamma_srgb_channels(), &mut result, 0);
}
// Add the end stop
result.push((pos_b, color_b, Some(next_midpoint)));
}
// If every midpoint is 0.5 (or within epsilon), turn all midpoints to None
if result.iter().all(|(_, _, midpoint)| matches!(midpoint, Some(m) if (m - 0.5).abs() < 1e-6)) {
result.iter_mut().for_each(|(_, _, midpoint)| *midpoint = None);
}
result
}
pub fn lerp(&self, other: &Self, time: f64) -> Self {
let stops = self.iter().zip(other.iter()).map(|(a, b)| {
let position = a.position + (b.position - a.position) * time;
let color = a.color.lerp(&b.color, time as f32);
GradientStop { position, midpoint: 0.5, color }
});
Gradient::new(stops)
}
}
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum GradientSpreadMethod {
#[default]
Pad,
Reflect,
Repeat,
}
impl GradientSpreadMethod {
pub fn svg_name(&self) -> &'static str {
match self {
GradientSpreadMethod::Pad => "pad",
GradientSpreadMethod::Reflect => "reflect",
GradientSpreadMethod::Repeat => "repeat",
}
}
}
/// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both
/// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint
/// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks.
/// Falls back to a +90° rotation of `new_x` when `old_x` is degenerate.
fn scale_y_axis_to_match_new_x(old_x: DVec2, old_y: DVec2, new_x: DVec2) -> DVec2 {
let old_x_length = old_x.length();
if old_x_length < 1e-9 {
return DVec2::new(-new_x.y, new_x.x);
}
let ex_old = old_x / old_x_length;
let ey_old = DVec2::new(-ex_old.y, ex_old.x);
let new_x_length = new_x.length();
if new_x_length < 1e-9 {
return DVec2::ZERO;
}
let ex_new = new_x / new_x_length;
let ey_new = DVec2::new(-ex_new.y, ex_new.x);
let parallel = old_y.dot(ex_old);
let perpendicular = old_y.dot(ey_old);
let scale = new_x_length / old_x_length;
scale * (parallel * ex_new + perpendicular * ey_new)
}
/// Build a new affine that maps canonical (0,0) -> (1,0) to (new_start, new_end), preserving the y-axis
/// shape of `old` proportionally to the x-axis length change.
pub fn build_transform_with_y_preservation(old: DAffine2, new_start: DVec2, new_end: DVec2) -> DAffine2 {
let new_x_axis = new_end - new_start;
let preserved_y_axis = scale_y_axis_to_match_new_x(old.matrix2.x_axis, old.matrix2.y_axis, new_x_axis);
DAffine2 {
matrix2: glam::DMat2::from_cols(new_x_axis, preserved_y_axis),
translation: new_start,
}
}
/// Build the default transform for a gradient not yet given one: a horizontal gradient spanning the
/// bounding box's width, running through its vertical middle.
pub fn initial_gradient_transform_for_bounding_box(bounds: [DVec2; 2]) -> DAffine2 {
let [min, max] = bounds;
let x_axis = DVec2::new(max.x - min.x, 0.);
DAffine2 {
matrix2: glam::DMat2::from_cols(x_axis, x_axis.perp()),
translation: DVec2::new(min.x, (min.y + max.y) / 2.),
}
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_to_gradient<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Gradient, D::Error> {
use serde::Deserialize;
#[derive(serde::Deserialize)]
struct LegacyTable {
#[serde(alias = "instances", alias = "instance")]
element: Vec<Gradient>,
}
#[derive(serde::Deserialize)]
#[cfg_attr(feature = "serde", serde(untagged))]
enum GradientStopsFormat {
Stops(Gradient),
List(LegacyTable),
}
Ok(match GradientStopsFormat::deserialize(deserializer)? {
GradientStopsFormat::Stops(stops) => stops,
GradientStopsFormat::List(list) => list.element.into_iter().next().unwrap_or_default(),
})
}
impl core_types::bounds::BoundingBox for Gradient {
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox {
core_types::bounds::RenderBoundingBox::Infinite
}
fn thumbnail_bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox {
// AABB of the gradient line itself, leaving aspect padding and sub-pixel fallbacks to the runtime so this stays
// a clean per-item geometric bound that combines naturally with siblings
let start = transform.transform_point2(DVec2::ZERO);
let end = transform.transform_point2(DVec2::X);
core_types::bounds::RenderBoundingBox::Rectangle([start.min(end), start.max(end)])
}
}
@@ -0,0 +1,22 @@
#[macro_use]
extern crate log;
pub mod gradient;
pub mod markers;
pub mod math;
pub mod subpath;
pub mod vector;
// Re-export commonly used types at the crate root
pub use core_types as gcore;
pub use gradient::{Gradient, GradientSpreadMethod, GradientStop, GradientType};
pub use markers::{ATTR_EDITOR_CLICK_TARGET, ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD};
pub use math::{QuadExt, RectExt};
pub use subpath::Subpath;
pub use vector::Vector;
pub use vector::reference_point::ReferencePoint;
// Re-export dependencies that users of this crate will need
pub use dyn_any;
pub use glam;
pub use kurbo;
@@ -0,0 +1,39 @@
//! Attribute markers whose value types live in this crate, with their name
//! constants for the string-keyed legacy readers and writers.
use core_types::attribute::Attribute;
core_types::attribute! {
/// Gradient's spread behavior past its endpoints (`Pad`, `Reflect`, or `Repeat`).
pub SpreadMethod("spread_method"): crate::gradient::GradientSpreadMethod;
/// Gradient's shape (`Linear` or `Radial`).
pub GradientType("gradient_type"): crate::gradient::GradientType;
/// Optional `Vector` that overrides the item's own geometry for click-target generation.
/// Used by the 'Text' node for per-glyph bounding-box rectangles so glyphs are selectable
/// by clicking anywhere within their bounds, not just the filled letterform. An absent
/// value means the item's own geometry is the click target.
pub EditorClickTarget("editor:click_target"): Option<&crate::Vector>;
}
pub const ATTR_SPREAD_METHOD: &str = SpreadMethod::NAME;
pub const ATTR_GRADIENT_TYPE: &str = GradientType::NAME;
pub const ATTR_EDITOR_CLICK_TARGET: &str = EditorClickTarget::NAME;
#[cfg(test)]
mod tests {
use super::*;
use core_types::attribute::info;
use std::any::TypeId;
#[test]
fn the_census_carries_this_crates_names() {
assert_eq!(info("gradient_type").unwrap().value_type, TypeId::of::<crate::gradient::GradientType>());
assert_eq!(info("spread_method").unwrap().value_type, TypeId::of::<crate::gradient::GradientSpreadMethod>());
assert_eq!(info("editor:click_target").unwrap().value_type, TypeId::of::<Option<&'static crate::Vector>>());
}
#[test]
fn an_absent_click_target_defaults_to_none() {
assert_eq!(<EditorClickTarget as Attribute>::default(), None);
}
}
@@ -0,0 +1,32 @@
use crate::subpath::Bezier;
use crate::vector::misc::dvec2_to_point;
use core_types::math::quad::Quad;
use core_types::math::rect::Rect;
use kurbo::{Line, PathSeg};
pub trait QuadExt {
/// Get all the edges in the rect as linear bezier curves
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_;
fn to_lines(&self) -> impl Iterator<Item = PathSeg>;
}
impl QuadExt for Quad {
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_ {
self.all_edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end))
}
fn to_lines(&self) -> impl Iterator<Item = PathSeg> {
self.all_edges().into_iter().map(|[start, end]| PathSeg::Line(Line::new(dvec2_to_point(start), dvec2_to_point(end))))
}
}
pub trait RectExt {
/// Get all the edges in the quad as linear bezier curves
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_;
}
impl RectExt for Rect {
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_ {
self.edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end))
}
}
@@ -0,0 +1,4 @@
// Implementation constants
/// Constant used to determine if `f64`s are equivalent.
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
@@ -0,0 +1,469 @@
use super::consts::*;
use super::*;
use crate::vector::misc::{SpiralType, point_to_dvec2};
use glam::DVec2;
use kurbo::PathSeg;
use std::f64::consts::TAU;
pub struct PathSegPoints {
pub p0: DVec2,
pub p1: Option<DVec2>,
pub p2: Option<DVec2>,
pub p3: DVec2,
}
impl PathSegPoints {
pub fn new(p0: DVec2, p1: Option<DVec2>, p2: Option<DVec2>, p3: DVec2) -> Self {
Self { p0, p1, p2, p3 }
}
}
pub fn pathseg_points(segment: PathSeg) -> PathSegPoints {
match segment {
PathSeg::Line(line) => PathSegPoints::new(point_to_dvec2(line.p0), None, None, point_to_dvec2(line.p1)),
PathSeg::Quad(quad) => PathSegPoints::new(point_to_dvec2(quad.p0), None, Some(point_to_dvec2(quad.p1)), point_to_dvec2(quad.p2)),
PathSeg::Cubic(cube) => PathSegPoints::new(point_to_dvec2(cube.p0), Some(point_to_dvec2(cube.p1)), Some(point_to_dvec2(cube.p2)), point_to_dvec2(cube.p3)),
}
}
/// Functionality relating to core `Subpath` operations, such as constructors and `iter`.
impl<PointId: Identifier> Subpath<PointId> {
/// Create a new `Subpath` using a list of [ManipulatorGroup]s.
/// A `Subpath` with less than 2 [ManipulatorGroup]s may not be closed.
#[track_caller]
pub fn new(manipulator_groups: Vec<ManipulatorGroup<PointId>>, closed: bool) -> Self {
assert!(!closed || !manipulator_groups.is_empty(), "A closed Subpath must contain more than 0 ManipulatorGroups.");
Self { manipulator_groups, closed }
}
/// Create a `Subpath` consisting of 2 manipulator groups from a `Bezier`.
pub fn from_bezier(segment: PathSeg) -> Self {
let PathSegPoints { p0, p1, p2, p3 } = pathseg_points(segment);
Subpath::new(vec![ManipulatorGroup::new(p0, None, p1), ManipulatorGroup::new(p3, p2, None)], false)
}
/// Creates a subpath from a slice of [Bezier]. When two consecutive Beziers do not share an end and start point, this function
/// resolves the discrepancy by simply taking the start-point of the second Bezier as the anchor of the Manipulator Group.
pub fn from_beziers(beziers: &[PathSeg], closed: bool) -> Self {
assert!(!closed || beziers.len() > 1, "A closed Subpath must contain at least 1 Bezier.");
if beziers.is_empty() {
return Subpath::new(vec![], closed);
}
let beziers: Vec<_> = beziers.iter().map(|b| pathseg_points(*b)).collect();
let first = beziers.first().unwrap();
let mut manipulator_groups = vec![ManipulatorGroup {
anchor: first.p0,
in_handle: None,
out_handle: first.p1,
id: PointId::new(),
}];
let mut inner_groups: Vec<ManipulatorGroup<PointId>> = beziers
.windows(2)
.map(|bezier_pair| ManipulatorGroup {
anchor: bezier_pair[1].p0,
in_handle: bezier_pair[0].p2,
out_handle: bezier_pair[1].p1,
id: PointId::new(),
})
.collect::<Vec<ManipulatorGroup<PointId>>>();
manipulator_groups.append(&mut inner_groups);
let last = beziers.last().unwrap();
if !closed {
manipulator_groups.push(ManipulatorGroup {
anchor: last.p3,
in_handle: last.p2,
out_handle: None,
id: PointId::new(),
});
return Subpath::new(manipulator_groups, false);
}
manipulator_groups[0].in_handle = last.p2;
Subpath::new(manipulator_groups, true)
}
/// Returns true if the `Subpath` contains no [ManipulatorGroup].
pub fn is_empty(&self) -> bool {
self.manipulator_groups.is_empty()
}
/// Returns the number of [ManipulatorGroup]s contained within the `Subpath`.
pub fn len(&self) -> usize {
self.manipulator_groups.len()
}
/// Returns the number of segments contained within the `Subpath`.
pub fn len_segments(&self) -> usize {
let mut number_of_curves = self.len();
if !self.closed && number_of_curves > 0 {
number_of_curves -= 1
}
number_of_curves
}
/// Returns a copy of the bezier segment at the given segment index, if this segment exists.
pub fn get_segment(&self, segment_index: usize) -> Option<PathSeg> {
if segment_index >= self.len_segments() {
return None;
}
Some(self[segment_index].to_bezier(&self[(segment_index + 1) % self.len()]))
}
/// Returns an iterator of the [Bezier]s along the `Subpath`.
pub fn iter(&self) -> SubpathIter<'_, PointId> {
SubpathIter {
subpath: self,
index: 0,
is_always_closed: false,
}
}
/// Returns an iterator of the [Bezier]s along the `Subpath` always considering it as a closed subpath.
pub fn iter_closed(&self) -> SubpathIter<'_, PointId> {
SubpathIter {
subpath: self,
index: 0,
is_always_closed: true,
}
}
/// Returns a slice of the [ManipulatorGroup]s in the `Subpath`.
pub fn manipulator_groups(&self) -> &[ManipulatorGroup<PointId>] {
&self.manipulator_groups
}
/// Returns a mutable reference to the [ManipulatorGroup]s in the `Subpath`.
pub fn manipulator_groups_mut(&mut self) -> &mut Vec<ManipulatorGroup<PointId>> {
&mut self.manipulator_groups
}
/// Returns a vector of all the anchors (DVec2) for this `Subpath`.
pub fn anchors(&self) -> Vec<DVec2> {
self.manipulator_groups().iter().map(|group| group.anchor).collect()
}
/// Returns if the Subpath is equivalent to a single point.
pub fn is_point(&self) -> bool {
if self.is_empty() {
return false;
}
let point = self.manipulator_groups[0].anchor;
self.manipulator_groups
.iter()
.all(|manipulator_group| manipulator_group.anchor.abs_diff_eq(point, MAX_ABSOLUTE_DIFFERENCE))
}
pub fn from_anchors(anchor_positions: impl IntoIterator<Item = DVec2>, closed: bool) -> Self {
Self::new(anchor_positions.into_iter().map(|anchor| ManipulatorGroup::new_anchor(anchor)).collect(), closed)
}
/// Constructs a rectangle with `corner1` and `corner2` as the two corners.
pub fn new_rectangle(corner1: DVec2, corner2: DVec2) -> Self {
Self::from_anchors([corner1, DVec2::new(corner2.x, corner1.y), corner2, DVec2::new(corner1.x, corner2.y)], true)
}
/// Constructs a rounded rectangle with `corner1` and `corner2` as the two corners and `corner_radii` as the radii of the corners: `[top_left, top_right, bottom_right, bottom_left]`.
pub fn new_rounded_rectangle(corner1: DVec2, corner2: DVec2, corner_radii: [f64; 4]) -> Self {
if corner_radii.iter().all(|radii| radii.abs() < f64::EPSILON * 100.) {
return Self::new_rectangle(corner1, corner2);
}
use std::f64::consts::{FRAC_1_SQRT_2, PI};
let new_arc = |center: DVec2, corner: DVec2, radius: f64| -> Vec<ManipulatorGroup<PointId>> {
let point1 = center + DVec2::from_angle(-PI * 0.25).rotate(corner - center) * FRAC_1_SQRT_2;
let point2 = center + DVec2::from_angle(PI * 0.25).rotate(corner - center) * FRAC_1_SQRT_2;
if radius == 0. {
return vec![ManipulatorGroup::new_anchor(point1), ManipulatorGroup::new_anchor(point2)];
}
// Constant from https://pomax.github.io/bezierinfo/#circles_cubic
const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014;
let handle_offset = radius * HANDLE_OFFSET_FACTOR;
vec![
ManipulatorGroup::new(point1, None, Some(point1 + handle_offset * (corner - point1).normalize())),
ManipulatorGroup::new(point2, Some(point2 + handle_offset * (corner - point2).normalize()), None),
]
};
Self::new(
[
new_arc(DVec2::new(corner1.x + corner_radii[0], corner1.y + corner_radii[0]), DVec2::new(corner1.x, corner1.y), corner_radii[0]),
new_arc(DVec2::new(corner2.x - corner_radii[1], corner1.y + corner_radii[1]), DVec2::new(corner2.x, corner1.y), corner_radii[1]),
new_arc(DVec2::new(corner2.x - corner_radii[2], corner2.y - corner_radii[2]), DVec2::new(corner2.x, corner2.y), corner_radii[2]),
new_arc(DVec2::new(corner1.x + corner_radii[3], corner2.y - corner_radii[3]), DVec2::new(corner1.x, corner2.y), corner_radii[3]),
]
.concat(),
true,
)
}
/// Constructs an ellipse with `corner1` and `corner2` as the two corners of the bounding box.
pub fn new_ellipse(corner1: DVec2, corner2: DVec2) -> Self {
let size = (corner1 - corner2).abs();
let center = (corner1 + corner2) / 2.;
let top = DVec2::new(center.x, corner1.y);
let bottom = DVec2::new(center.x, corner2.y);
let left = DVec2::new(corner1.x, center.y);
let right = DVec2::new(corner2.x, center.y);
// Based on https://pomax.github.io/bezierinfo/#circles_cubic
const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014;
let handle_offset = size * HANDLE_OFFSET_FACTOR * 0.5;
let manipulator_groups = vec![
ManipulatorGroup::new(top, Some(top - handle_offset * DVec2::X), Some(top + handle_offset * DVec2::X)),
ManipulatorGroup::new(right, Some(right - handle_offset * DVec2::Y), Some(right + handle_offset * DVec2::Y)),
ManipulatorGroup::new(bottom, Some(bottom + handle_offset * DVec2::X), Some(bottom - handle_offset * DVec2::X)),
ManipulatorGroup::new(left, Some(left + handle_offset * DVec2::Y), Some(left - handle_offset * DVec2::Y)),
];
Self::new(manipulator_groups, true)
}
/// Constructs an arc by a `radius`, `angle_start` and `angle_size`. Angles must be in radians. Slice option makes it look like pie or pacman.
pub fn new_arc(radius: f64, start_angle: f64, sweep_angle: f64, arc_type: ArcType) -> Self {
// Prevents glitches from numerical imprecision that have been observed during animation playback after about a minute
let start_angle = start_angle % (std::f64::consts::TAU * 2.);
let sweep_angle = sweep_angle % (std::f64::consts::TAU * 2.);
let original_start_angle = start_angle;
let sweep_angle_sign = sweep_angle.signum();
let mut start_angle = 0.;
let mut sweep_angle = sweep_angle.abs();
if ((sweep_angle / std::f64::consts::TAU).floor() as u32).is_multiple_of(2) {
sweep_angle %= std::f64::consts::TAU;
} else {
start_angle = sweep_angle % std::f64::consts::TAU;
sweep_angle = std::f64::consts::TAU - start_angle;
}
sweep_angle *= sweep_angle_sign;
start_angle *= sweep_angle_sign;
start_angle += original_start_angle;
let closed = arc_type == ArcType::Closed;
let slice = arc_type == ArcType::PieSlice;
let center = DVec2::new(0., 0.);
let segments = (sweep_angle.abs() / (std::f64::consts::PI / 4.)).ceil().max(1.) as usize;
let step = sweep_angle / segments as f64;
let factor = 4. / 3. * (step / 2.).sin() / (1. + (step / 2.).cos());
let mut manipulator_groups = Vec::with_capacity(segments);
let mut prev_in_handle = None;
let mut prev_end = DVec2::new(0., 0.);
for i in 0..segments {
let start_angle = start_angle + step * i as f64;
let end_angle = start_angle + step;
let start_vec = DVec2::from_angle(start_angle);
let end_vec = DVec2::from_angle(end_angle);
let start = center + radius * start_vec;
let end = center + radius * end_vec;
let handle_start = start + start_vec.perp() * radius * factor;
let handle_end = end - end_vec.perp() * radius * factor;
manipulator_groups.push(ManipulatorGroup::new(start, prev_in_handle, Some(handle_start)));
prev_in_handle = Some(handle_end);
prev_end = end;
}
manipulator_groups.push(ManipulatorGroup::new(prev_end, prev_in_handle, None));
if slice {
manipulator_groups.push(ManipulatorGroup::new(center, None, None));
}
Self::new(manipulator_groups, closed || slice)
}
/// Constructs a regular polygon (ngon). Based on `sides` and `radius`, which is the distance from the center to any vertex.
pub fn new_regular_polygon(center: DVec2, sides: u64, radius: f64) -> Self {
let sides = sides.max(3);
let angle_increment = std::f64::consts::TAU / (sides as f64);
let anchor_positions = (0..sides).map(|i| {
let angle = (i as f64) * angle_increment - std::f64::consts::FRAC_PI_2;
let center = center + DVec2::ONE * radius;
DVec2::new(center.x + radius * f64::cos(angle), center.y + radius * f64::sin(angle)) * 0.5
});
Self::from_anchors(anchor_positions, true)
}
/// Constructs a star polygon (n-star). See [new_regular_polygon], but with interspersed vertices at an `inner_radius`.
pub fn new_star_polygon(center: DVec2, sides: u64, radius: f64, inner_radius: f64) -> Self {
let sides = sides.max(2);
let angle_increment = 0.5 * std::f64::consts::TAU / (sides as f64);
let anchor_positions = (0..sides * 2).map(|i| {
let angle = (i as f64) * angle_increment - std::f64::consts::FRAC_PI_2;
let center = center + DVec2::ONE * radius;
let r = if i % 2 == 0 { radius } else { inner_radius };
DVec2::new(center.x + r * f64::cos(angle), center.y + r * f64::sin(angle)) * 0.5
});
Self::from_anchors(anchor_positions, true)
}
/// Constructs a line from `p1` to `p2`
pub fn new_line(p1: DVec2, p2: DVec2) -> Self {
Self::from_anchors([p1, p2], false)
}
/// Constructs an arrow shape from start and end points with parametric control over dimensions
pub fn new_arrow(start: DVec2, end: DVec2, shaft_width: f64, head_width: f64, head_length: f64) -> Self {
let delta = end - start;
let length = delta.length();
if length < 1e-10 {
// Degenerate case: return a point
return Self::from_anchors([start], true);
}
let direction = delta / length;
let perpendicular = DVec2::new(-direction.y, direction.x);
let half_shaft = shaft_width * 0.5;
let half_head = head_width * 0.5;
let head_base_distance = (length - head_length).max(0.);
let head_base = start + direction * head_base_distance;
// Arrow path starts at the tail, traces around the shape, and returns to the tail
let anchors = [
start, // Tail center (origin)
start + perpendicular * half_shaft, // Tail top
head_base + perpendicular * half_shaft, // Head base top (shaft)
head_base + perpendicular * half_head, // Head base top (wide)
end, // Tip
head_base - perpendicular * half_head, // Head base bottom (wide)
head_base - perpendicular * half_shaft, // Head base bottom (shaft)
start - perpendicular * half_shaft, // Tail bottom
];
Self::from_anchors(anchors, true)
}
pub fn new_spiral(a: f64, outer_radius: f64, turns: f64, start_angle: f64, delta_theta: f64, spiral_type: SpiralType) -> Self {
let mut manipulator_groups = Vec::new();
let mut prev_in_handle = None;
let theta_end = turns * std::f64::consts::TAU + start_angle;
let a = if spiral_type == SpiralType::Logarithmic { a.max(1e-10) } else { a };
let b = calculate_growth_factor(a, turns, outer_radius, spiral_type);
let mut theta = start_angle;
while theta < theta_end {
let theta_next = f64::min(theta + delta_theta, theta_end);
let p0 = spiral_point(theta, a, b, spiral_type);
let p3 = spiral_point(theta_next, a, b, spiral_type);
let t0 = spiral_tangent(theta, a, b, spiral_type);
let t1 = spiral_tangent(theta_next, a, b, spiral_type);
let arc_len = spiral_arc_length(theta, theta_next, a, b, spiral_type);
let d = arc_len / 3.;
let p1 = p0 + d * t0;
let p2 = p3 - d * t1;
manipulator_groups.push(ManipulatorGroup::new(p0, prev_in_handle, Some(p1)));
prev_in_handle = Some(p2);
// If final segment, end with anchor at theta_end
if (theta_next - theta_end).abs() < f64::EPSILON {
manipulator_groups.push(ManipulatorGroup::new(p3, prev_in_handle, None));
break;
}
theta = theta_next;
}
Self::new(manipulator_groups, false)
}
}
pub fn calculate_growth_factor(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 {
match spiral_type {
SpiralType::Archimedean => {
let total_theta = turns * TAU;
(outer_radius - a) / total_theta
}
SpiralType::Logarithmic => {
let total_theta = turns * TAU;
((outer_radius.abs() / a).ln()) / total_theta
}
}
}
/// Returns a point on the given spiral type at angle `theta`.
pub fn spiral_point(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 {
match spiral_type {
SpiralType::Archimedean => archimedean_spiral_point(theta, a, b),
SpiralType::Logarithmic => log_spiral_point(theta, a, b),
}
}
/// Returns the tangent direction at angle `theta` for the given spiral type.
pub fn spiral_tangent(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 {
match spiral_type {
SpiralType::Archimedean => archimedean_spiral_tangent(theta, a, b),
SpiralType::Logarithmic => log_spiral_tangent(theta, a, b),
}
}
/// Computes arc length between two angles for the given spiral type.
pub fn spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64, spiral_type: SpiralType) -> f64 {
match spiral_type {
SpiralType::Archimedean => archimedean_spiral_arc_length(theta_start, theta_end, a, b),
SpiralType::Logarithmic => log_spiral_arc_length(theta_start, theta_end, a, b),
}
}
/// Returns a point on a logarithmic spiral at angle `theta`.
pub fn log_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 {
let r = a * (b * theta).exp(); // a * e^(bθ)
DVec2::new(r * theta.cos(), -r * theta.sin())
}
/// Computes arc length along a logarithmic spiral between two angles.
pub fn log_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 {
let factor = (1. + b * b).sqrt();
(a / b) * factor * ((b * theta_end).exp() - (b * theta_start).exp())
}
/// Returns the tangent direction of a logarithmic spiral at angle `theta`.
pub fn log_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 {
let r = a * (b * theta).exp();
let dx = r * (b * theta.cos() - theta.sin());
let dy = r * (b * theta.sin() + theta.cos());
DVec2::new(dx, -dy).normalize_or(DVec2::X)
}
/// Returns a point on an Archimedean spiral at angle `theta`.
pub fn archimedean_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 {
let r = a + b * theta;
DVec2::new(r * theta.cos(), -r * theta.sin())
}
/// Returns the tangent direction of an Archimedean spiral at angle `theta`.
pub fn archimedean_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 {
let r = a + b * theta;
let dx = b * theta.cos() - r * theta.sin();
let dy = b * theta.sin() + r * theta.cos();
DVec2::new(dx, -dy).normalize_or(DVec2::X)
}
/// Computes arc length along an Archimedean spiral between two angles.
pub fn archimedean_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 {
archimedean_spiral_arc_length_origin(theta_end, a, b) - archimedean_spiral_arc_length_origin(theta_start, a, b)
}
/// Computes arc length from origin to a point on Archimedean spiral at angle `theta`.
pub fn archimedean_spiral_arc_length_origin(theta: f64, a: f64, b: f64) -> f64 {
let r = a + b * theta;
let sqrt_term = (r * r + b * b).sqrt();
(r * sqrt_term + b * b * ((r + sqrt_term).ln())) / (2. * b)
}
@@ -0,0 +1,128 @@
use super::consts::MAX_ABSOLUTE_DIFFERENCE;
use super::*;
use crate::vector::algorithms::bezpath_algorithms::pathseg_length_centroid_and_length;
use crate::vector::algorithms::intersection::{filtered_all_segment_intersections, pathseg_self_intersections};
use core_types::math::polynomial::pathseg_to_parametric_polynomial;
use glam::DVec2;
impl<PointId: Identifier> Subpath<PointId> {
/// Returns a list of `t` values that correspond to all the self intersection points of the subpath always considering it as a closed subpath. The index and `t` value of both will be returned that corresponds to a point.
/// The points will be sorted based on their index and `t` repsectively.
/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point.
/// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
///
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two
///
/// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out.
pub fn all_self_intersections(&self, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
let mut intersections_vec = Vec::new();
let err = accuracy.unwrap_or(MAX_ABSOLUTE_DIFFERENCE);
let num_curves = self.len();
// TODO: optimization opportunity - this for-loop currently compares all intersections with all curve-segments in the subpath list
self.iter_closed().enumerate().for_each(|(i, other)| {
intersections_vec.extend(pathseg_self_intersections(other, accuracy, minimum_separation).iter().flat_map(|value| [(i, value.0), (i, value.1)]));
self.iter_closed().enumerate().skip(i + 1).for_each(|(j, curve)| {
intersections_vec.extend(
filtered_all_segment_intersections(curve, other, accuracy, minimum_separation)
.iter()
.filter(|&value| (j != i + 1 || value.0 > err || (1. - value.1) > err) && (j != num_curves - 1 || i != 0 || value.1 > err || (1. - value.0) > err))
.flat_map(|value| [(j, value.0), (i, value.1)]),
);
});
});
intersections_vec.sort_by(|a, b| a.partial_cmp(b).unwrap());
intersections_vec
}
/// Return the area centroid, together with the area, of the `Subpath` always considering it as a closed subpath. The area will always be a positive value.
///
/// The area centroid is the center of mass for the area of a solid shape's interior.
/// An infinitely flat material forming the subpath's closed shape would balance at this point.
///
/// It will return `None` if no manipulator is present. If the area is less than `error`, it will return `Some((DVec2::NAN, 0.))`.
///
/// Because the calculation of area and centroid for self-intersecting path requires finding the intersections, the following parameters are used:
/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point.
/// - `minimum_separation` - the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
///
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two.
///
/// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out.
pub fn area_centroid_and_area(&self, error: Option<f64>, minimum_separation: Option<f64>) -> Option<(DVec2, f64)> {
let all_intersections = self.all_self_intersections(error, minimum_separation);
let mut current_sign: f64 = 1.;
let (x_sum, y_sum, area) = self
.iter_closed()
.enumerate()
.map(|(index, bezier)| {
let (f_x, f_y) = pathseg_to_parametric_polynomial(bezier);
let (f_x, f_y) = (f_x.as_size::<10>().unwrap(), f_y.as_size::<10>().unwrap());
let f_y_prime = f_y.derivative();
let f_x_prime = f_x.derivative();
let f_xy = &f_x * &f_y;
let mut x_part = &f_xy * &f_x_prime;
let mut y_part = &f_xy * &f_y_prime;
let mut area_part = &f_x * &f_y_prime;
x_part.antiderivative_mut();
y_part.antiderivative_mut();
area_part.antiderivative_mut();
let mut curve_sum_x = -current_sign * x_part.eval(0.);
let mut curve_sum_y = -current_sign * y_part.eval(0.);
let mut curve_sum_area = -current_sign * area_part.eval(0.);
for (_, t) in all_intersections.iter().filter(|(i, _)| *i == index) {
curve_sum_x += 2. * current_sign * x_part.eval(*t);
curve_sum_y += 2. * current_sign * y_part.eval(*t);
curve_sum_area += 2. * current_sign * area_part.eval(*t);
current_sign *= -1.;
}
curve_sum_x += current_sign * x_part.eval(1.);
curve_sum_y += current_sign * y_part.eval(1.);
curve_sum_area += current_sign * area_part.eval(1.);
(-curve_sum_x, curve_sum_y, curve_sum_area)
})
.reduce(|(x1, y1, area1), (x2, y2, area2)| (x1 + x2, y1 + y2, area1 + area2))?;
if area.abs() < error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE) {
return Some((DVec2::NAN, 0.));
}
Some((DVec2::new(x_sum / area, y_sum / area), area.abs()))
}
/// Return the approximation of the length centroid, together with the length, of the `Subpath`.
///
/// The length centroid is the center of mass for the arc length of the solid shape's perimeter.
/// An infinitely thin wire forming the subpath's closed shape would balance at this point.
///
/// It will return `None` if no manipulator is present.
/// - `accuracy` is used to approximate the curve.
/// - `always_closed` is to consider the subpath as closed always.
pub fn length_centroid_and_length(&self, accuracy: Option<f64>, always_closed: bool) -> Option<(DVec2, f64)> {
if always_closed { self.iter_closed() } else { self.iter() }
.map(|bezier| pathseg_length_centroid_and_length(bezier, accuracy))
.map(|(centroid, length)| (centroid * length, length))
.reduce(|(centroid_part1, length1), (centroid_part2, length2)| (centroid_part1 + centroid_part2, length1 + length2))
.map(|(centroid_part, length)| (centroid_part / length, length))
.map(|(centroid_part, length)| (DVec2::new(centroid_part.x, centroid_part.y), length))
}
}
#[cfg(test)]
mod test_centroid {
use crate::vector::PointId;
use super::*;
#[test]
fn centroid_rect() {
let rect = Subpath::<PointId>::new_rectangle(DVec2::new(100., 100.), DVec2::new(300., 200.));
let (center, area) = rect.area_centroid_and_area(Some(1e-3), Some(1e-3)).unwrap();
assert_eq!(area, 200. * 100.);
assert_eq!(center, DVec2::new(200., 150.))
}
}
@@ -0,0 +1,52 @@
// use super::consts::MAX_ABSOLUTE_DIFFERENCE;
// use super::utils::{SubpathTValue};
use super::*;
impl<PointId: super::structs::Identifier> Subpath<PointId> {
/// Get whether the subpath is closed.
pub fn closed(&self) -> bool {
self.closed
}
/// Set whether the subpath is closed.
pub fn set_closed(&mut self, new_closed: bool) {
self.closed = new_closed;
}
/// Access a [ManipulatorGroup] from a PointId.
pub fn manipulator_from_id(&self, id: PointId) -> Option<&ManipulatorGroup<PointId>> {
self.manipulator_groups.iter().find(|manipulator_group| manipulator_group.id == id)
}
/// Access a mutable [ManipulatorGroup] from a PointId.
pub fn manipulator_mut_from_id(&mut self, id: PointId) -> Option<&mut ManipulatorGroup<PointId>> {
self.manipulator_groups.iter_mut().find(|manipulator_group| manipulator_group.id == id)
}
/// Access the index of a [ManipulatorGroup] from a PointId.
pub fn manipulator_index_from_id(&self, id: PointId) -> Option<usize> {
self.manipulator_groups.iter().position(|manipulator_group| manipulator_group.id == id)
}
/// Insert a manipulator group at an index.
pub fn insert_manipulator_group(&mut self, index: usize, group: ManipulatorGroup<PointId>) {
assert!(group.is_finite(), "Inserting non finite manipulator group");
self.manipulator_groups.insert(index, group)
}
/// Push a manipulator group to the end.
pub fn push_manipulator_group(&mut self, group: ManipulatorGroup<PointId>) {
assert!(group.is_finite(), "Pushing non finite manipulator group");
self.manipulator_groups.push(group)
}
/// Get a mutable reference to the last manipulator
pub fn last_manipulator_group_mut(&mut self) -> Option<&mut ManipulatorGroup<PointId>> {
self.manipulator_groups.last_mut()
}
/// Remove a manipulator group at an index.
pub fn remove_manipulator_group(&mut self, index: usize) -> ManipulatorGroup<PointId> {
self.manipulator_groups.remove(index)
}
}
@@ -0,0 +1,71 @@
mod consts;
mod core;
mod lookup;
mod manipulators;
mod solvers;
mod structs;
mod transform;
pub use core::*;
use kurbo::PathSeg;
use std::fmt::{Debug, Formatter, Result};
use std::ops::{Index, IndexMut};
pub use structs::*;
/// Structure used to represent a path composed of [Bezier] curves.
#[derive(Clone, PartialEq, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Subpath<PointId: Identifier> {
manipulator_groups: Vec<ManipulatorGroup<PointId>>,
pub closed: bool,
}
/// Iteration structure for iterating across each curve of a `Subpath`, using an intermediate `Bezier` representation.
pub struct SubpathIter<'a, PointId: Identifier> {
index: usize,
subpath: &'a Subpath<PointId>,
is_always_closed: bool,
}
impl<PointId: Identifier> Index<usize> for Subpath<PointId> {
type Output = ManipulatorGroup<PointId>;
fn index(&self, index: usize) -> &Self::Output {
assert!(index < self.len(), "Index out of bounds in trait Index of SubPath.");
&self.manipulator_groups[index]
}
}
impl<PointId: Identifier> IndexMut<usize> for Subpath<PointId> {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
assert!(index < self.len(), "Index out of bounds in trait IndexMut of SubPath.");
&mut self.manipulator_groups[index]
}
}
impl<PointId: Identifier> Iterator for SubpathIter<'_, PointId> {
type Item = PathSeg;
// Returns the Bezier representation of each `Subpath` segment, defined between a pair of adjacent manipulator points.
fn next(&mut self) -> Option<Self::Item> {
if self.subpath.is_empty() {
return None;
}
let closed = if self.is_always_closed { true } else { self.subpath.closed };
let len = self.subpath.len() - 1 + if closed { 1 } else { 0 };
if self.index >= len {
return None;
}
let start_index = self.index;
let end_index = (self.index + 1) % self.subpath.len();
self.index += 1;
Some(self.subpath[start_index].to_bezier(&self.subpath[end_index]))
}
}
impl<PointId: Identifier> Debug for Subpath<PointId> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.debug_struct("Subpath").field("closed", &self.closed).field("manipulator_groups", &self.manipulator_groups).finish()
}
}
@@ -0,0 +1,83 @@
use crate::subpath::{Identifier, Subpath};
use crate::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath;
use crate::vector::misc::dvec2_to_point;
use glam::DVec2;
use kurbo::{Affine, BezPath, Shape};
impl<PointId: Identifier> Subpath<PointId> {
pub fn contains_point(&self, point: DVec2) -> bool {
self.to_bezpath().contains(dvec2_to_point(point))
}
pub fn to_bezpath(&self) -> BezPath {
let mut bezpath = kurbo::BezPath::new();
let mut out_handle;
let Some(first) = self.manipulator_groups.first() else { return bezpath };
bezpath.move_to(dvec2_to_point(first.anchor));
out_handle = first.out_handle;
for manipulator in self.manipulator_groups.iter().skip(1) {
match (out_handle, manipulator.in_handle) {
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(manipulator.anchor)),
(None, None) => bezpath.line_to(dvec2_to_point(manipulator.anchor)),
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
}
out_handle = manipulator.out_handle;
}
if self.closed {
match (out_handle, first.in_handle) {
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(first.anchor)),
(None, None) => bezpath.line_to(dvec2_to_point(first.anchor)),
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
}
bezpath.close_path();
}
bezpath
}
/// Returns `true` if this subpath is completely inside the `other` subpath.
pub fn is_inside_subpath(&self, other: &Subpath<PointId>, accuracy: Option<f64>, minimum_separation: Option<f64>) -> bool {
bezpath_is_inside_bezpath(&self.to_bezpath(), &other.to_bezpath(), accuracy, minimum_separation)
}
/// Return the min and max corners that represent the bounding box of the subpath. Return `None` if the subpath is empty.
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
self.iter()
.map(|bezier| bezier.bounding_box())
.map(|bbox| [DVec2::new(bbox.min_x(), bbox.min_y()), DVec2::new(bbox.max_x(), bbox.max_y())])
.reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
}
/// Return the min and max corners that represent the bounding box of the subpath, after a given affine transform.
pub fn bounding_box_with_transform(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
self.iter()
.map(|bezier| (Affine::new(transform.to_cols_array()) * bezier).bounding_box())
.map(|bbox| [DVec2::new(bbox.min_x(), bbox.min_y()), DVec2::new(bbox.max_x(), bbox.max_y())])
.reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
}
/// Return the min and max corners that represent the loose bounding box of the subpath (bounding box of all handles and anchors).
pub fn loose_bounding_box(&self) -> Option<[DVec2; 2]> {
self.manipulator_groups
.iter()
.flat_map(|group| [group.in_handle, group.out_handle, Some(group.anchor)])
.flatten()
.map(|pos| [pos, pos])
.reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
}
/// Return the min and max corners that represent the loose bounding box of the subpath, after a given affine transform.
pub fn loose_bounding_box_with_transform(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
self.manipulator_groups
.iter()
.flat_map(|group| [group.in_handle, group.out_handle, Some(group.anchor)])
.flatten()
.map(|pos| transform.transform_point2(pos))
.map(|pos| [pos, pos])
.reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
}
}
@@ -0,0 +1,384 @@
use crate::vector::algorithms::intersection::filtered_segment_intersections;
use crate::vector::misc::{dvec2_to_point, handles_to_segment};
use glam::{DAffine2, DVec2};
use kurbo::{CubicBez, Line, PathSeg, QuadBez, Shape};
use std::fmt::{Debug, Formatter, Result};
use std::hash::Hash;
/// An id type used for each [ManipulatorGroup].
pub trait Identifier: Sized + Clone + PartialEq + Hash + graphene_hash::CacheHash + 'static {
fn new() -> Self;
}
/// Structure used to represent a single anchor with up to two optional associated handles along a `Subpath`
#[derive(Copy, Clone, PartialEq, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ManipulatorGroup<PointId: Identifier> {
pub anchor: DVec2,
pub in_handle: Option<DVec2>,
pub out_handle: Option<DVec2>,
pub id: PointId,
}
impl<PointId: Identifier> Debug for ManipulatorGroup<PointId> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.debug_struct("ManipulatorGroup")
.field("anchor", &self.anchor)
.field("in_handle", &self.in_handle)
.field("out_handle", &self.out_handle)
.finish()
}
}
impl<PointId: Identifier> ManipulatorGroup<PointId> {
/// Construct a new manipulator group from an anchor, in handle and out handle
pub fn new(anchor: DVec2, in_handle: Option<DVec2>, out_handle: Option<DVec2>) -> Self {
let id = PointId::new();
Self { anchor, in_handle, out_handle, id }
}
/// Construct a new manipulator point with just an anchor position
pub fn new_anchor(anchor: DVec2) -> Self {
Self::new(anchor, None, None)
}
/// Construct a new manipulator group from an anchor, in handle, out handle and an id
pub fn new_with_id(anchor: DVec2, in_handle: Option<DVec2>, out_handle: Option<DVec2>, id: PointId) -> Self {
Self { anchor, in_handle, out_handle, id }
}
/// Construct a new manipulator point with just an anchor position and an id
pub fn new_anchor_with_id(anchor: DVec2, id: PointId) -> Self {
Self::new_with_id(anchor, Some(anchor), Some(anchor), id)
}
/// Create a bezier curve that starts at the current manipulator group and finishes in the `end_group` manipulator group.
pub fn to_bezier(&self, end_group: &ManipulatorGroup<PointId>) -> PathSeg {
let start = self.anchor;
let end = end_group.anchor;
let out_handle = self.out_handle;
let in_handle = end_group.in_handle;
match (out_handle, in_handle) {
(Some(handle1), Some(handle2)) => PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(handle1), dvec2_to_point(handle2), dvec2_to_point(end))),
(Some(handle), None) | (None, Some(handle)) => PathSeg::Quad(QuadBez::new(dvec2_to_point(start), dvec2_to_point(handle), dvec2_to_point(end))),
(None, None) => PathSeg::Line(Line::new(dvec2_to_point(start), dvec2_to_point(end))),
}
}
/// Apply a transformation to all of the [ManipulatorGroup] points
pub fn apply_transform(&mut self, affine_transform: DAffine2) {
self.anchor = affine_transform.transform_point2(self.anchor);
self.in_handle = self.in_handle.map(|in_handle| affine_transform.transform_point2(in_handle));
self.out_handle = self.out_handle.map(|out_handle| affine_transform.transform_point2(out_handle));
}
/// Are all handles at finite positions
pub fn is_finite(&self) -> bool {
self.anchor.is_finite() && self.in_handle.is_none_or(|handle| handle.is_finite()) && self.out_handle.is_none_or(|handle| handle.is_finite())
}
/// Reverse directions of handles
pub fn flip(mut self) -> Self {
std::mem::swap(&mut self.in_handle, &mut self.out_handle);
self
}
pub fn has_in_handle(&self) -> bool {
self.in_handle.map(|handle| Self::has_handle(self.anchor, handle)).unwrap_or(false)
}
pub fn has_out_handle(&self) -> bool {
self.out_handle.map(|handle| Self::has_handle(self.anchor, handle)).unwrap_or(false)
}
fn has_handle(anchor: DVec2, handle: DVec2) -> bool {
!((handle.x - anchor.x).abs() < f64::EPSILON && (handle.y - anchor.y).abs() < f64::EPSILON)
}
}
#[derive(Copy, Clone)]
pub enum AppendType {
IgnoreStart,
SmoothJoin(f64),
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, graphene_hash::CacheHash)]
pub enum ArcType {
Open,
Closed,
PieSlice,
}
/// Representation of the handle point(s) in a bezier segment.
#[derive(Copy, Clone, PartialEq, Debug, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BezierHandles {
Linear,
/// Handles for a quadratic curve.
Quadratic {
/// Point representing the location of the single handle.
handle: DVec2,
},
/// Handles for a cubic curve.
Cubic {
/// Point representing the location of the handle associated to the start point.
handle_start: DVec2,
/// Point representing the location of the handle associated to the end point.
handle_end: DVec2,
},
}
impl BezierHandles {
pub fn is_cubic(&self) -> bool {
matches!(self, Self::Cubic { .. })
}
pub fn is_finite(&self) -> bool {
match self {
BezierHandles::Linear => true,
BezierHandles::Quadratic { handle } => handle.is_finite(),
BezierHandles::Cubic { handle_start, handle_end } => handle_start.is_finite() && handle_end.is_finite(),
}
}
/// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment.
pub fn start(&self) -> Option<DVec2> {
match *self {
BezierHandles::Cubic { handle_start, .. } | BezierHandles::Quadratic { handle: handle_start } => Some(handle_start),
_ => None,
}
}
/// Get the coordinates of the second handle point. This will return `None` for a quadratic segment.
pub fn end(&self) -> Option<DVec2> {
match *self {
BezierHandles::Cubic { handle_end, .. } => Some(handle_end),
_ => None,
}
}
pub fn move_start(&mut self, delta: DVec2) {
if let BezierHandles::Cubic { handle_start, .. } | BezierHandles::Quadratic { handle: handle_start } = self {
*handle_start += delta
}
}
pub fn move_end(&mut self, delta: DVec2) {
if let BezierHandles::Cubic { handle_end, .. } = self {
*handle_end += delta
}
}
/// Returns a Bezier curve that results from applying the transformation function to each handle point in the Bezier.
#[must_use]
pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Self {
match *self {
BezierHandles::Linear => Self::Linear,
BezierHandles::Quadratic { handle } => {
let handle = transformation_function(handle);
Self::Quadratic { handle }
}
BezierHandles::Cubic { handle_start, handle_end } => {
let handle_start = transformation_function(handle_start);
let handle_end = transformation_function(handle_end);
Self::Cubic { handle_start, handle_end }
}
}
}
#[must_use]
pub fn reversed(self) -> Self {
match self {
BezierHandles::Cubic { handle_start, handle_end } => Self::Cubic {
handle_start: handle_end,
handle_end: handle_start,
},
_ => self,
}
}
}
/// Representation of a bezier curve with 2D points.
#[derive(Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Bezier {
/// Start point of the bezier curve.
pub start: DVec2,
/// End point of the bezier curve.
pub end: DVec2,
/// Handles of the bezier curve.
pub handles: BezierHandles,
}
impl Debug for Bezier {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let mut debug_struct = f.debug_struct("Bezier");
let mut debug_struct_ref = debug_struct.field("start", &self.start);
debug_struct_ref = match self.handles {
BezierHandles::Linear => debug_struct_ref,
BezierHandles::Quadratic { handle } => debug_struct_ref.field("handle", &handle),
BezierHandles::Cubic { handle_start, handle_end } => debug_struct_ref.field("handle_start", &handle_start).field("handle_end", &handle_end),
};
debug_struct_ref.field("end", &self.end).finish()
}
}
/// Functionality for the getters and setters of the various points in a Bezier
impl Bezier {
/// Set the coordinates of the start point.
pub fn set_start(&mut self, s: DVec2) {
self.start = s;
}
/// Set the coordinates of the end point.
pub fn set_end(&mut self, e: DVec2) {
self.end = e;
}
/// Set the coordinates of the first handle point. This represents the only handle in a quadratic segment. If used on a linear segment, it will be changed to a quadratic.
pub fn set_handle_start(&mut self, h1: DVec2) {
match self.handles {
BezierHandles::Linear => {
self.handles = BezierHandles::Quadratic { handle: h1 };
}
BezierHandles::Quadratic { ref mut handle } => {
*handle = h1;
}
BezierHandles::Cubic { ref mut handle_start, .. } => {
*handle_start = h1;
}
};
}
/// Set the coordinates of the second handle point. This will convert both linear and quadratic segments into cubic ones. For a linear segment, the first handle will be set to the start point.
pub fn set_handle_end(&mut self, h2: DVec2) {
match self.handles {
BezierHandles::Linear => {
self.handles = BezierHandles::Cubic {
handle_start: self.start,
handle_end: h2,
};
}
BezierHandles::Quadratic { handle } => {
self.handles = BezierHandles::Cubic { handle_start: handle, handle_end: h2 };
}
BezierHandles::Cubic { ref mut handle_end, .. } => {
*handle_end = h2;
}
};
}
/// Get the coordinates of the bezier segment's start point.
pub fn start(&self) -> DVec2 {
self.start
}
/// Get the coordinates of the bezier segment's end point.
pub fn end(&self) -> DVec2 {
self.end
}
/// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment.
pub fn handle_start(&self) -> Option<DVec2> {
self.handles.start()
}
/// Get the coordinates of the second handle point. This will return `None` for a quadratic segment.
pub fn handle_end(&self) -> Option<DVec2> {
self.handles.end()
}
/// Get an iterator over the coordinates of all points in a vector.
/// - For a linear segment, the order of the points will be: `start`, `end`.
/// - For a quadratic segment, the order of the points will be: `start`, `handle`, `end`.
/// - For a cubic segment, the order of the points will be: `start`, `handle_start`, `handle_end`, `end`.
pub fn get_points(&self) -> impl Iterator<Item = DVec2> + use<> {
match self.handles {
BezierHandles::Linear => [self.start, self.end, DVec2::ZERO, DVec2::ZERO].into_iter().take(2),
BezierHandles::Quadratic { handle } => [self.start, handle, self.end, DVec2::ZERO].into_iter().take(3),
BezierHandles::Cubic { handle_start, handle_end } => [self.start, handle_start, handle_end, self.end].into_iter().take(4),
}
}
// TODO: Consider removing this function
/// Create a linear bezier using the provided coordinates as the start and end points.
pub fn from_linear_coordinates(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
Bezier {
start: DVec2::new(x1, y1),
handles: BezierHandles::Linear,
end: DVec2::new(x2, y2),
}
}
/// Create a linear bezier using the provided DVec2s as the start and end points.
pub fn from_linear_dvec2(p1: DVec2, p2: DVec2) -> Self {
Bezier {
start: p1,
handles: BezierHandles::Linear,
end: p2,
}
}
// TODO: Consider removing this function
/// Create a quadratic bezier using the provided coordinates as the start, handle, and end points.
pub fn from_quadratic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self {
Bezier {
start: DVec2::new(x1, y1),
handles: BezierHandles::Quadratic { handle: DVec2::new(x2, y2) },
end: DVec2::new(x3, y3),
}
}
/// Create a quadratic bezier using the provided DVec2s as the start, handle, and end points.
pub fn from_quadratic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2) -> Self {
Bezier {
start: p1,
handles: BezierHandles::Quadratic { handle: p2 },
end: p3,
}
}
// TODO: Consider removing this function
/// Create a cubic bezier using the provided coordinates as the start, handles, and end points.
#[allow(clippy::too_many_arguments)]
pub fn from_cubic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> Self {
Bezier {
start: DVec2::new(x1, y1),
handles: BezierHandles::Cubic {
handle_start: DVec2::new(x2, y2),
handle_end: DVec2::new(x3, y3),
},
end: DVec2::new(x4, y4),
}
}
/// Create a cubic bezier using the provided DVec2s as the start, handles, and end points.
pub fn from_cubic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2, p4: DVec2) -> Self {
Bezier {
start: p1,
handles: BezierHandles::Cubic { handle_start: p2, handle_end: p3 },
end: p4,
}
}
/// Returns a Bezier curve that results from applying the transformation function to each point in the Bezier.
pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Bezier {
Self {
start: transformation_function(self.start),
end: transformation_function(self.end),
handles: self.handles.apply_transformation(transformation_function),
}
}
pub fn intersections(&self, other: &Bezier, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
let this = handles_to_segment(self.start, self.handles, self.end);
let other = handles_to_segment(other.start, other.handles, other.end);
filtered_segment_intersections(this, other, accuracy, minimum_separation)
}
pub fn winding(&self, point: DVec2) -> i32 {
let this = handles_to_segment(self.start, self.handles, self.end);
this.winding(dvec2_to_point(point))
}
}
@@ -0,0 +1,62 @@
use super::structs::Identifier;
use super::*;
use glam::{DAffine2, DVec2};
/// Functionality that transforms Subpaths, such as split, reduce, offset, etc.
impl<PointId: Identifier> Subpath<PointId> {
/// Returns [ManipulatorGroup]s with a reversed winding order.
fn reverse_manipulator_groups(manipulator_groups: &[ManipulatorGroup<PointId>]) -> Vec<ManipulatorGroup<PointId>> {
manipulator_groups
.iter()
.rev()
.map(|group| ManipulatorGroup {
anchor: group.anchor,
in_handle: group.out_handle,
out_handle: group.in_handle,
id: PointId::new(),
})
.collect::<Vec<ManipulatorGroup<PointId>>>()
}
/// Returns a [Subpath] with a reversed winding order.
/// Note that a reversed closed subpath will start on the same manipulator group and simply wind the other direction
pub fn reverse(&self) -> Subpath<PointId> {
let mut reversed = Subpath::reverse_manipulator_groups(self.manipulator_groups());
if self.closed {
reversed.rotate_right(1);
};
Subpath {
manipulator_groups: reversed,
closed: self.closed,
}
}
/// Apply a transformation to all of the [ManipulatorGroup]s in the [Subpath].
pub fn apply_transform(&mut self, affine_transform: DAffine2) {
for manipulator_group in &mut self.manipulator_groups {
manipulator_group.apply_transform(affine_transform);
}
}
/// Returns a subpath that results from rotating this subpath around the origin by the given angle (in radians).
pub fn rotate(&self, angle: f64) -> Subpath<PointId> {
let mut rotated_subpath = self.clone();
let affine_transform: DAffine2 = DAffine2::from_angle(angle);
rotated_subpath.apply_transform(affine_transform);
rotated_subpath
}
/// Returns a subpath that results from rotating this subpath around the provided point by the given angle (in radians).
pub fn rotate_about_point(&self, angle: f64, pivot: DVec2) -> Subpath<PointId> {
// Translate before and after the rotation to account for the pivot
let translate: DAffine2 = DAffine2::from_translation(pivot);
let rotate: DAffine2 = DAffine2::from_angle(angle);
let translate_inverse = translate.inverse();
let mut rotated_subpath = self.clone();
rotated_subpath.apply_transform(translate * rotate * translate_inverse);
rotated_subpath
}
}

Some files were not shown because too many files have changed in this diff Show More