Artboard nodes (#1328)

* Create artboard nodes

* Update node when resizing artboard

* Render clipped artboards

* More stable feature

* Do not render old artboards

* Fix some issues with transforms

* Fix crash when drawing rectangle

* Format

* Allow renaming document from Properties panel

* Adjust artboard label styling

* Fix document graph refresh so artboards show up

* Make "Clear Artboards" coming soon

* Fix displaying an infinite canvas

* Show document name in node graph options bar

* info!() to debug!()

* Fix Properties panel not being cleared when all docs closed

* Remove dead code

* Remove debug logs added in this branch

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2023-07-27 07:35:17 +01:00
committed by Keavon Chambers
parent ad0dc3276e
commit 08f9be6aaf
26 changed files with 636 additions and 182 deletions

View File

@@ -49,6 +49,19 @@ pub struct Artboard {
pub location: IVec2,
pub dimensions: IVec2,
pub background: Color,
pub clip: bool,
}
impl Artboard {
pub fn new(location: IVec2, dimensions: IVec2) -> Self {
Self {
graphic_group: GraphicGroup::EMPTY,
location: location.min(location + dimensions),
dimensions: dimensions.abs(),
background: Color::WHITE,
clip: false,
}
}
}
pub struct ConstructLayerNode<Name, BlendMode, Opacity, Visible, Locked, Collapsed, Stack> {
@@ -84,19 +97,21 @@ fn construct_layer<Data: Into<GraphicElementData>>(
stack
}
pub struct ConstructArtboardNode<Location, Dimensions, Background> {
pub struct ConstructArtboardNode<Location, Dimensions, Background, Clip> {
location: Location,
dimensions: Dimensions,
background: Background,
clip: Clip,
}
#[node_fn(ConstructArtboardNode)]
fn construct_artboard(graphic_group: GraphicGroup, location: IVec2, dimensions: IVec2, background: Color) -> Artboard {
fn construct_artboard(graphic_group: GraphicGroup, location: IVec2, dimensions: IVec2, background: Color, clip: bool) -> Artboard {
Artboard {
graphic_group,
location,
dimensions,
location: location.min(location + dimensions),
dimensions: dimensions.abs(),
background,
clip,
}
}

View File

@@ -12,6 +12,7 @@ pub struct SvgRender {
pub svg_defs: String,
pub transform: DAffine2,
pub image_data: Vec<(u64, Image<Color>)>,
indent: usize,
}
impl SvgRender {
@@ -21,9 +22,15 @@ impl SvgRender {
svg_defs: String::new(),
transform: DAffine2::IDENTITY,
image_data: Vec::new(),
indent: 0,
}
}
pub fn indent(&mut self) {
self.svg.push("\n");
self.svg.push("\t".repeat(self.indent));
}
/// Add an outer `<svg />` tag with a `viewBox` and the `<defs />`
pub fn format_svg(&mut self, bounds_min: DVec2, bounds_max: DVec2) {
let (x, y) = bounds_min.into();
@@ -31,7 +38,38 @@ impl SvgRender {
let defs = &self.svg_defs;
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{x} {y} {size_x} {size_y}"><defs>{defs}</defs>"#,);
self.svg.insert(0, svg_header.into());
self.svg.push("</svg>".into());
self.svg.push("</svg>");
}
pub fn leaf_tag(&mut self, name: impl Into<SvgSegment>, attributes: impl FnOnce(&mut SvgRenderAttrs)) {
self.indent();
self.svg.push("<");
self.svg.push(name);
attributes(&mut SvgRenderAttrs(self));
self.svg.push("/>");
}
pub fn parent_tag(&mut self, name: impl Into<SvgSegment>, attributes: impl FnOnce(&mut SvgRenderAttrs), inner: impl FnOnce(&mut Self)) {
let name = name.into();
self.indent();
self.svg.push("<");
self.svg.push(name.clone());
attributes(&mut SvgRenderAttrs(self));
self.svg.push(">");
let length = self.svg.len();
self.indent += 1;
inner(self);
self.indent -= 1;
if self.svg.len() != length {
self.indent();
self.svg.push("</");
self.svg.push(name);
self.svg.push(">");
} else {
self.svg.pop();
self.svg.push("/>");
}
}
}
@@ -54,8 +92,18 @@ impl RenderParams {
}
}
fn format_transform_matrix(transform: DAffine2) -> String {
transform.to_cols_array().iter().map(ToString::to_string).collect::<Vec<_>>().join(",")
pub fn format_transform_matrix(transform: DAffine2) -> String {
use std::fmt::Write;
let mut result = "matrix(".to_string();
let cols = transform.to_cols_array();
for (index, item) in cols.iter().enumerate() {
write!(result, "{}", item).unwrap();
if index != cols.len() - 1 {
result.push_str(", ");
}
}
result.push(')');
result
}
pub trait GraphicElementRendered {
@@ -77,17 +125,17 @@ impl GraphicElementRendered for VectorData {
let layer_bounds = self.bounding_box().unwrap_or_default();
let transformed_bounds = self.bounding_box_with_transform(render.transform).unwrap_or_default();
render.svg.push("<path d=\"".into());
let mut path = String::new();
for subpath in &self.subpaths {
let _ = subpath.subpath_to_svg(&mut path, self.transform * render.transform);
}
render.svg.push(path.into());
render.svg.push("\"".into());
let style = self.style.render(render_params.view_mode, &mut render.svg_defs, render.transform, layer_bounds, transformed_bounds);
render.svg.push(style.into());
render.svg.push("/>".into());
render.leaf_tag("path", |attributes| {
attributes.push("class", "vector-data");
attributes.push("d", path);
let render = &mut attributes.0;
let style = self.style.render(render_params.view_mode, &mut render.svg_defs, render.transform, layer_bounds, transformed_bounds);
attributes.push_val(style);
});
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.bounding_box_with_transform(self.transform * transform)
@@ -96,7 +144,54 @@ impl GraphicElementRendered for VectorData {
impl GraphicElementRendered for Artboard {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
self.graphic_group.render_svg(render, render_params)
// Background
render.leaf_tag("rect", |attributes| {
attributes.push("class", "artboard-bg");
attributes.push("fill", format!("#{}", self.background.rgba_hex()));
attributes.push("x", self.location.x.min(self.location.x + self.dimensions.x).to_string());
attributes.push("y", self.location.y.min(self.location.y + self.dimensions.y).to_string());
attributes.push("width", self.dimensions.x.abs().to_string());
attributes.push("height", self.dimensions.y.abs().to_string());
});
// Label
render.parent_tag(
"text",
|attributes| {
attributes.push("class", "artboard-label");
attributes.push("fill", "white");
attributes.push("x", (self.location.x.min(self.location.x + self.dimensions.x)).to_string());
attributes.push("y", (self.location.y.min(self.location.y + self.dimensions.y) - 4).to_string());
attributes.push("font-size", "14px");
},
|render| {
render.svg.push("Artboard");
},
);
// Contents group
render.parent_tag(
"g",
|attributes| {
attributes.push("class", "artboard");
if self.clip {
let id = format!("artboard-{}", generate_uuid());
let selector = format!("url(#{id})");
use std::fmt::Write;
write!(
&mut attributes.0.svg_defs,
r##"<clipPath id="{id}"><rect x="{}" y="{}" width="{}" height="{}"/></clipPath>"##,
self.location.x, self.location.y, self.dimensions.x, self.dimensions.y
)
.unwrap();
attributes.push("clip-path", selector);
}
},
|render| {
// Contents
self.graphic_group.render_svg(render, render_params);
},
);
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let artboard_bounds = (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box();
@@ -107,12 +202,14 @@ impl GraphicElementRendered for Artboard {
impl GraphicElementRendered for ImageFrame<Color> {
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
let transform: String = format_transform_matrix(self.transform * render.transform);
render
.svg
.push(format!(r#"<image width="1" height="1" preserveAspectRatio="none" transform="matrix({transform})" href=""#).into());
let uuid = generate_uuid();
render.svg.push(SvgSegment::BlobUrl(uuid));
render.svg.push("\" />".into());
render.leaf_tag("image", |attributes| {
attributes.push("width", 1.to_string());
attributes.push("height", 1.to_string());
attributes.push("preserveAspectRatio", "none");
attributes.push("transform", transform);
attributes.push("href", SvgSegment::BlobUrl(uuid))
});
render.image_data.push((uuid, self.image.clone()))
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
@@ -193,3 +290,27 @@ impl core::fmt::Display for SvgSegmentList {
Ok(())
}
}
pub struct SvgRenderAttrs<'a>(&'a mut SvgRender);
impl<'a> SvgRenderAttrs<'a> {
pub fn push_complex(&mut self, name: impl Into<SvgSegment>, value: impl FnOnce(&mut SvgRender)) {
self.0.svg.push(" ");
self.0.svg.push(name);
self.0.svg.push("=\"");
value(self.0);
self.0.svg.push("\"");
}
pub fn push(&mut self, name: impl Into<SvgSegment>, value: impl Into<SvgSegment>) {
self.push_complex(name, move |renderer| renderer.svg.push(value));
}
pub fn push_val(&mut self, value: impl Into<SvgSegment>) {
self.0.svg.push(value);
}
}
impl SvgSegmentList {
pub fn push(&mut self, value: impl Into<SvgSegment>) {
self.0.push(value.into());
}
}

View File

@@ -496,34 +496,19 @@ impl NodeNetwork {
///
/// Used for the properties panel and tools.
pub fn primary_flow(&self) -> impl Iterator<Item = (&DocumentNode, u64)> {
struct FlowIter<'a> {
stack: Vec<NodeId>,
network: &'a NodeNetwork,
}
impl<'a> Iterator for FlowIter<'a> {
type Item = (&'a DocumentNode, NodeId);
fn next(&mut self) -> Option<Self::Item> {
loop {
let node_id = self.stack.pop()?;
if let Some(document_node) = self.network.nodes.get(&node_id) {
self.stack.extend(
document_node
.inputs
.iter()
.take(1) // Only show the primary input
.filter_map(|input| if let NodeInput::Node { node_id: ref_id, .. } = input { Some(*ref_id) } else { None }),
);
return Some((document_node, node_id));
};
}
}
}
FlowIter {
stack: self.outputs.iter().map(|output| output.node_id).collect(),
network: self,
}
}
pub fn primary_flow_from_opt(&self, id: Option<NodeId>) -> impl Iterator<Item = (&DocumentNode, u64)> {
FlowIter {
stack: id.map_or_else(|| self.outputs.iter().map(|output| output.node_id).collect(), |id| vec![id]),
network: self,
}
}
pub fn is_acyclic(&self) -> bool {
let mut dependencies: HashMap<u64, Vec<u64>> = HashMap::new();
for (node_id, node) in &self.nodes {
@@ -549,6 +534,29 @@ impl NodeNetwork {
}
}
struct FlowIter<'a> {
stack: Vec<NodeId>,
network: &'a NodeNetwork,
}
impl<'a> Iterator for FlowIter<'a> {
type Item = (&'a DocumentNode, NodeId);
fn next(&mut self) -> Option<Self::Item> {
loop {
let node_id = self.stack.pop()?;
if let Some(document_node) = self.network.nodes.get(&node_id) {
self.stack.extend(
document_node
.inputs
.iter()
.take(1) // Only show the primary input
.filter_map(|input| if let NodeInput::Node { node_id: ref_id, .. } = input { Some(*ref_id) } else { None }),
);
return Some((document_node, node_id));
};
}
}
}
/// Functions for compiling the network
impl NodeNetwork {
pub fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId + Copy) {
@@ -835,7 +843,7 @@ impl NodeNetwork {
self.nodes.retain(|_, node| !matches!(node.implementation, DocumentNodeImplementation::Extract));
for (_, node) in &mut extraction_nodes {
log::info!("extraction network: {:#?}", &self);
log::debug!("extraction network: {:#?}", &self);
if let DocumentNodeImplementation::Extract = node.implementation {
assert_eq!(node.inputs.len(), 1);
log::debug!("Resolving extract node {:?}", node);

View File

@@ -169,7 +169,7 @@ impl ApplicationIo for WasmApplicationIo {
fn load_resource(&self, url: impl AsRef<str>) -> Result<ResourceFuture, ApplicationError> {
let url = url::Url::parse(url.as_ref()).map_err(|_| ApplicationError::InvalidUrl)?;
log::info!("Loading resource: {:?}", url);
log::trace!("Loading resource: {:?}", url);
match url.scheme() {
#[cfg(feature = "tokio")]
"file" => {
@@ -196,7 +196,7 @@ impl ApplicationIo for WasmApplicationIo {
"graphite" => {
let path = url.path();
let path = path.to_owned();
log::info!("Loading resource: {}", path);
log::trace!("Loading local resource: {}", path);
let data = self.resources.get(&path).ok_or(ApplicationError::NotFound)?.clone();
Ok(Box::pin(async move { Ok(data.clone()) }) as Pin<Box<dyn Future<Output = Result<Arc<[u8]>, _>>>>)
}

View File

@@ -539,7 +539,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, input: ImageFrame<Color>, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]),
register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, input: graphene_core::GraphicGroup, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]),
register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, input: graphene_core::Artboard, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]),
register_node!(graphene_core::ConstructArtboardNode<_, _, _>, input: graphene_core::GraphicGroup, params: [glam::IVec2, glam::IVec2, Color]),
register_node!(graphene_core::ConstructArtboardNode<_, _, _, _>, input: graphene_core::GraphicGroup, params: [glam::IVec2, glam::IVec2, Color, bool]),
];
let mut map: HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();
for (id, c, types) in node_types.into_iter().flatten() {