mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
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:
committed by
Keavon Chambers
parent
ad0dc3276e
commit
08f9be6aaf
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]>, _>>>>)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user