mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 23:08:05 +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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user