mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-22 08:08:11 +08:00
Restructure project directories (#333)
`/client/web` -> `/frontend` `/client/cli` -> *delete for now* `/client/native` -> *delete for now* `/core/editor` -> `/editor` `/core/document` -> `/graphene` `/core/renderer` -> `/charcoal` `/core/proc-macro` -> `/proc-macros` *(now plural)*
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum BlendMode {
|
||||
Normal,
|
||||
Multiply,
|
||||
Darken,
|
||||
ColorBurn,
|
||||
Screen,
|
||||
Lighten,
|
||||
ColorDodge,
|
||||
Overlay,
|
||||
SoftLight,
|
||||
HardLight,
|
||||
Difference,
|
||||
Exclusion,
|
||||
Hue,
|
||||
Saturation,
|
||||
Color,
|
||||
Luminosity,
|
||||
}
|
||||
|
||||
impl BlendMode {
|
||||
pub fn to_svg_style_name(&self) -> &str {
|
||||
match self {
|
||||
BlendMode::Normal => "normal",
|
||||
BlendMode::Multiply => "multiply",
|
||||
BlendMode::Darken => "darken",
|
||||
BlendMode::ColorBurn => "color-burn",
|
||||
BlendMode::Screen => "screen",
|
||||
BlendMode::Lighten => "lighten",
|
||||
BlendMode::ColorDodge => "color-dodge",
|
||||
BlendMode::Overlay => "overlay",
|
||||
BlendMode::SoftLight => "soft-light",
|
||||
BlendMode::HardLight => "hard-light",
|
||||
BlendMode::Difference => "difference",
|
||||
BlendMode::Exclusion => "exclusion",
|
||||
BlendMode::Hue => "hue",
|
||||
BlendMode::Saturation => "saturation",
|
||||
BlendMode::Color => "color",
|
||||
BlendMode::Luminosity => "luminosity",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use glam::DVec2;
|
||||
|
||||
use crate::{DocumentError, LayerId, Quad};
|
||||
|
||||
use super::{Layer, LayerData, LayerDataType};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
|
||||
pub struct Folder {
|
||||
next_assignment_id: LayerId,
|
||||
pub layer_ids: Vec<LayerId>,
|
||||
layers: Vec<Layer>,
|
||||
}
|
||||
|
||||
impl LayerData for Folder {
|
||||
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>) {
|
||||
for layer in &mut self.layers {
|
||||
let _ = writeln!(svg, "{}", layer.render(transforms));
|
||||
}
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
for (layer, layer_id) in self.layers().iter().zip(&self.layer_ids) {
|
||||
path.push(*layer_id);
|
||||
layer.intersects_quad(quad, path, intersections);
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.layers
|
||||
.iter()
|
||||
.filter_map(|layer| layer.data.bounding_box(transform * layer.transform))
|
||||
.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])])
|
||||
}
|
||||
}
|
||||
|
||||
impl Folder {
|
||||
/// When a insertion id is provided, try to insert the layer with the given id.
|
||||
/// If that id is already used, return None.
|
||||
/// When no insertion id is provided, search for the next free id and insert it with that.
|
||||
pub fn add_layer(&mut self, layer: Layer, id: Option<LayerId>, insert_index: isize) -> Option<LayerId> {
|
||||
let mut insert_index = insert_index as i128;
|
||||
if insert_index < 0 {
|
||||
insert_index = self.layers.len() as i128 + insert_index as i128 + 1;
|
||||
}
|
||||
|
||||
if insert_index <= self.layers.len() as i128 && insert_index >= 0 {
|
||||
if let Some(id) = id {
|
||||
self.next_assignment_id = id;
|
||||
}
|
||||
if self.layer_ids.contains(&self.next_assignment_id) {
|
||||
return None;
|
||||
}
|
||||
let id = self.next_assignment_id;
|
||||
self.layers.insert(insert_index as usize, layer);
|
||||
self.layer_ids.insert(insert_index as usize, id);
|
||||
// Linear probing for collision avoidance
|
||||
while self.layer_ids.contains(&self.next_assignment_id) {
|
||||
self.next_assignment_id += 1;
|
||||
}
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_layer(&mut self, id: LayerId) -> Result<(), DocumentError> {
|
||||
let pos = self.position_of_layer(id)?;
|
||||
self.layers.remove(pos);
|
||||
self.layer_ids.remove(pos);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a list of layers in the folder
|
||||
pub fn list_layers(&self) -> &[LayerId] {
|
||||
self.layer_ids.as_slice()
|
||||
}
|
||||
|
||||
pub fn layers(&self) -> &[Layer] {
|
||||
self.layers.as_slice()
|
||||
}
|
||||
|
||||
pub fn layers_mut(&mut self) -> &mut [Layer] {
|
||||
self.layers.as_mut_slice()
|
||||
}
|
||||
|
||||
pub fn layer(&self, id: LayerId) -> Option<&Layer> {
|
||||
let pos = self.position_of_layer(id).ok()?;
|
||||
Some(&self.layers[pos])
|
||||
}
|
||||
|
||||
pub fn layer_mut(&mut self, id: LayerId) -> Option<&mut Layer> {
|
||||
let pos = self.position_of_layer(id).ok()?;
|
||||
Some(&mut self.layers[pos])
|
||||
}
|
||||
|
||||
pub fn position_of_layer(&self, layer_id: LayerId) -> Result<usize, DocumentError> {
|
||||
self.layer_ids.iter().position(|x| *x == layer_id).ok_or(DocumentError::LayerNotFound)
|
||||
}
|
||||
|
||||
pub fn folder(&self, id: LayerId) -> Option<&Folder> {
|
||||
match self.layer(id) {
|
||||
Some(Layer {
|
||||
data: LayerDataType::Folder(folder), ..
|
||||
}) => Some(folder),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn folder_mut(&mut self, id: LayerId) -> Option<&mut Folder> {
|
||||
match self.layer_mut(id) {
|
||||
Some(Layer {
|
||||
data: LayerDataType::Folder(folder), ..
|
||||
}) => Some(folder),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
pub mod style;
|
||||
|
||||
use glam::DAffine2;
|
||||
use glam::{DMat2, DVec2};
|
||||
|
||||
pub mod blend_mode;
|
||||
pub use blend_mode::BlendMode;
|
||||
|
||||
pub mod simple_shape;
|
||||
pub use simple_shape::Shape;
|
||||
|
||||
pub mod folder;
|
||||
use crate::LayerId;
|
||||
use crate::{DocumentError, Quad};
|
||||
pub use folder::Folder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
pub trait LayerData {
|
||||
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>);
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>);
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub enum LayerDataType {
|
||||
Folder(Folder),
|
||||
Shape(Shape),
|
||||
}
|
||||
|
||||
impl LayerDataType {
|
||||
pub fn inner(&self) -> &dyn LayerData {
|
||||
match self {
|
||||
LayerDataType::Shape(s) => s,
|
||||
LayerDataType::Folder(f) => f,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner_mut(&mut self) -> &mut dyn LayerData {
|
||||
match self {
|
||||
LayerDataType::Shape(s) => s,
|
||||
LayerDataType::Folder(f) => f,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayerData for LayerDataType {
|
||||
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>) {
|
||||
self.inner_mut().render(svg, transforms)
|
||||
}
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
self.inner().intersects_quad(quad, path, intersections)
|
||||
}
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.inner().bounding_box(transform)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(remote = "glam::DAffine2")]
|
||||
struct DAffine2Ref {
|
||||
pub matrix2: DMat2,
|
||||
pub translation: DVec2,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Layer {
|
||||
pub visible: bool,
|
||||
pub name: Option<String>,
|
||||
pub data: LayerDataType,
|
||||
#[serde(with = "DAffine2Ref")]
|
||||
pub transform: glam::DAffine2,
|
||||
pub cache: String,
|
||||
pub thumbnail_cache: String,
|
||||
pub cache_dirty: bool,
|
||||
pub blend_mode: BlendMode,
|
||||
pub opacity: f64,
|
||||
}
|
||||
|
||||
impl Layer {
|
||||
pub fn new(data: LayerDataType, transform: [f64; 6]) -> Self {
|
||||
Self {
|
||||
visible: true,
|
||||
name: None,
|
||||
data,
|
||||
transform: glam::DAffine2::from_cols_array(&transform),
|
||||
cache: String::new(),
|
||||
thumbnail_cache: String::new(),
|
||||
cache_dirty: true,
|
||||
blend_mode: BlendMode::Normal,
|
||||
opacity: 1.,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self, transforms: &mut Vec<DAffine2>) -> &str {
|
||||
if !self.visible {
|
||||
return "";
|
||||
}
|
||||
if self.cache_dirty {
|
||||
transforms.push(self.transform);
|
||||
self.thumbnail_cache.clear();
|
||||
self.data.render(&mut self.thumbnail_cache, transforms);
|
||||
|
||||
self.cache.clear();
|
||||
let _ = writeln!(self.cache, r#"<g transform="matrix("#);
|
||||
self.transform.to_cols_array().iter().enumerate().for_each(|(i, f)| {
|
||||
let _ = self.cache.write_str(&(f.to_string() + if i != 5 { "," } else { "" }));
|
||||
});
|
||||
let _ = write!(
|
||||
self.cache,
|
||||
r#")" style="mix-blend-mode: {}; opacity: {}">{}</g>"#,
|
||||
self.blend_mode.to_svg_style_name(),
|
||||
self.opacity,
|
||||
self.thumbnail_cache.as_str()
|
||||
);
|
||||
transforms.pop();
|
||||
self.cache_dirty = false;
|
||||
}
|
||||
self.cache.as_str()
|
||||
}
|
||||
|
||||
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
if !self.visible {
|
||||
return;
|
||||
}
|
||||
let transformed_quad = self.transform.inverse() * quad;
|
||||
self.data.intersects_quad(transformed_quad, path, intersections)
|
||||
}
|
||||
|
||||
pub fn current_bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
self.data.bounding_box(self.transform)
|
||||
}
|
||||
|
||||
pub fn as_folder_mut(&mut self) -> Result<&mut Folder, DocumentError> {
|
||||
match &mut self.data {
|
||||
LayerDataType::Folder(f) => Ok(f),
|
||||
_ => Err(DocumentError::NotAFolder),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_folder(&self) -> Result<&Folder, DocumentError> {
|
||||
match &self.data {
|
||||
LayerDataType::Folder(f) => Ok(f),
|
||||
_ => Err(DocumentError::NotAFolder),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Layer {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
visible: self.visible,
|
||||
name: self.name.clone(),
|
||||
data: self.data.clone(),
|
||||
transform: self.transform,
|
||||
cache: String::new(),
|
||||
thumbnail_cache: String::new(),
|
||||
cache_dirty: true,
|
||||
blend_mode: self.blend_mode,
|
||||
opacity: self.opacity,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
use glam::DAffine2;
|
||||
use glam::DMat2;
|
||||
use glam::DVec2;
|
||||
|
||||
use kurbo::Affine;
|
||||
use kurbo::Shape as KurboShape;
|
||||
|
||||
use crate::intersection::intersect_quad_bez_path;
|
||||
use crate::LayerId;
|
||||
use crate::Quad;
|
||||
use kurbo::BezPath;
|
||||
|
||||
use super::style;
|
||||
use super::style::PathStyle;
|
||||
use super::LayerData;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
fn glam_to_kurbo(transform: DAffine2) -> Affine {
|
||||
Affine::new(transform.to_cols_array())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Shape {
|
||||
pub path: BezPath,
|
||||
pub style: style::PathStyle,
|
||||
pub render_index: i32,
|
||||
pub solid: bool,
|
||||
}
|
||||
|
||||
impl LayerData for Shape {
|
||||
fn render(&mut self, svg: &mut String, transforms: &mut Vec<DAffine2>) {
|
||||
let mut path = self.path.clone();
|
||||
let transform = self.transform(transforms);
|
||||
let inverse = transform.inverse();
|
||||
if !inverse.is_finite() {
|
||||
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
|
||||
return;
|
||||
}
|
||||
path.apply_affine(glam_to_kurbo(transform));
|
||||
|
||||
let _ = writeln!(svg, r#"<g transform="matrix("#);
|
||||
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
|
||||
let _ = svg.write_str(&(entry.to_string() + if i != 5 { "," } else { "" }));
|
||||
});
|
||||
let _ = svg.write_str(r#")">"#);
|
||||
let _ = write!(svg, r#"<path d="{}" {} />"#, path.to_svg(), self.style.render());
|
||||
let _ = svg.write_str("</g>");
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
let mut path = self.path.clone();
|
||||
if transform.matrix2 == DMat2::ZERO {
|
||||
return None;
|
||||
}
|
||||
path.apply_affine(glam_to_kurbo(transform));
|
||||
|
||||
use kurbo::Shape;
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
Some([(x0, y0).into(), (x1, y1).into()])
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
if intersect_quad_bez_path(quad, &self.path, self.solid) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
pub fn transform(&self, transforms: &[DAffine2]) -> DAffine2 {
|
||||
let start = match self.render_index {
|
||||
-1 => 0,
|
||||
x => (transforms.len() as i32 - x).max(0) as usize,
|
||||
};
|
||||
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
|
||||
}
|
||||
|
||||
pub fn shape(sides: u8, style: PathStyle) -> Self {
|
||||
use std::f64::consts::{FRAC_PI_2, TAU};
|
||||
fn unit_rotation(theta: f64) -> DVec2 {
|
||||
DVec2::new(theta.sin(), theta.cos())
|
||||
}
|
||||
let mut path = kurbo::BezPath::new();
|
||||
let apothem_offset_angle = TAU / (sides as f64);
|
||||
// Rotate odd sided shapes by 90 degrees
|
||||
let offset = ((sides + 1) % 2) as f64 * FRAC_PI_2;
|
||||
|
||||
let relative_points = (0..sides).map(|i| apothem_offset_angle * i as f64 + offset).map(unit_rotation);
|
||||
let min = relative_points.clone().reduce(|a, b| a.min(b)).unwrap_or_default();
|
||||
|
||||
let transform = DAffine2::from_scale_angle_translation(DVec2::ONE / 2., 0., -min / 2.);
|
||||
let point = |vec: DVec2| kurbo::Point::new(vec.x, vec.y);
|
||||
let mut relative_points = relative_points.map(|p| point(transform.transform_point2(p)));
|
||||
path.move_to(relative_points.next().expect("Tried to create an ngon with 0 sides"));
|
||||
relative_points.for_each(|p| path.line_to(p));
|
||||
|
||||
path.close_path();
|
||||
Self {
|
||||
path,
|
||||
style,
|
||||
render_index: 1,
|
||||
solid: true,
|
||||
}
|
||||
}
|
||||
pub fn rectangle(style: PathStyle) -> Self {
|
||||
Self {
|
||||
path: kurbo::Rect::new(0., 0., 1., 1.).to_path(0.01),
|
||||
style,
|
||||
render_index: 1,
|
||||
solid: true,
|
||||
}
|
||||
}
|
||||
pub fn ellipse(style: PathStyle) -> Self {
|
||||
Self {
|
||||
path: kurbo::Ellipse::from_rect(kurbo::Rect::new(0., 0., 1., 1.)).to_path(0.01),
|
||||
style,
|
||||
render_index: 1,
|
||||
solid: true,
|
||||
}
|
||||
}
|
||||
pub fn line(style: PathStyle) -> Self {
|
||||
Self {
|
||||
path: kurbo::Line::new((0., 0.), (1., 0.)).to_path(0.01),
|
||||
style,
|
||||
render_index: 1,
|
||||
solid: true,
|
||||
}
|
||||
}
|
||||
pub fn poly_line(points: Vec<impl Into<glam::DVec2>>, style: PathStyle) -> Self {
|
||||
let mut path = kurbo::BezPath::new();
|
||||
points
|
||||
.into_iter()
|
||||
.map(|v| v.into())
|
||||
.map(|v: DVec2| kurbo::Point { x: v.x, y: v.y })
|
||||
.enumerate()
|
||||
.for_each(|(i, p)| if i == 0 { path.move_to(p) } else { path.line_to(p) });
|
||||
Self {
|
||||
path,
|
||||
style,
|
||||
render_index: 0,
|
||||
solid: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
use crate::color::Color;
|
||||
use serde::{Deserialize, Serialize};
|
||||
const OPACITY_PRECISION: usize = 3;
|
||||
|
||||
fn format_opacity(name: &str, opacity: f32) -> String {
|
||||
if (opacity - 1.).abs() > 10f32.powi(-(OPACITY_PRECISION as i32)) {
|
||||
format!(r#" {}-opacity="{:.precision$}""#, name, opacity, precision = OPACITY_PRECISION)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct Fill {
|
||||
color: Option<Color>,
|
||||
}
|
||||
impl Fill {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self { color: Some(color) }
|
||||
}
|
||||
pub fn color(&self) -> Option<Color> {
|
||||
self.color
|
||||
}
|
||||
pub fn none() -> Self {
|
||||
Self { color: None }
|
||||
}
|
||||
pub fn render(&self) -> String {
|
||||
match self.color {
|
||||
Some(c) => format!(r##" fill="#{}"{}"##, c.rgb_hex(), format_opacity("fill", c.a())),
|
||||
None => r#" fill="none""#.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct Stroke {
|
||||
color: Color,
|
||||
width: f32,
|
||||
}
|
||||
|
||||
impl Stroke {
|
||||
pub fn new(color: Color, width: f32) -> Self {
|
||||
Self { color, width }
|
||||
}
|
||||
pub fn color(&self) -> Color {
|
||||
self.color
|
||||
}
|
||||
pub fn width(&self) -> f32 {
|
||||
self.width
|
||||
}
|
||||
pub fn render(&self) -> String {
|
||||
format!(r##" stroke="#{}"{} stroke-width="{}""##, self.color.rgb_hex(), format_opacity("stroke", self.color.a()), self.width)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct PathStyle {
|
||||
stroke: Option<Stroke>,
|
||||
fill: Option<Fill>,
|
||||
}
|
||||
impl PathStyle {
|
||||
pub fn new(stroke: Option<Stroke>, fill: Option<Fill>) -> Self {
|
||||
Self { stroke, fill }
|
||||
}
|
||||
pub fn fill(&self) -> Option<Fill> {
|
||||
self.fill
|
||||
}
|
||||
pub fn stroke(&self) -> Option<Stroke> {
|
||||
self.stroke
|
||||
}
|
||||
pub fn set_fill(&mut self, fill: Fill) {
|
||||
self.fill = Some(fill);
|
||||
}
|
||||
pub fn set_stroke(&mut self, stroke: Stroke) {
|
||||
self.stroke = Some(stroke);
|
||||
}
|
||||
pub fn clear_fill(&mut self) {
|
||||
self.fill = None;
|
||||
}
|
||||
pub fn clear_stroke(&mut self) {
|
||||
self.stroke = None;
|
||||
}
|
||||
pub fn render(&self) -> String {
|
||||
format!(
|
||||
"{}{}",
|
||||
match self.fill {
|
||||
Some(fill) => fill.render(),
|
||||
None => String::new(),
|
||||
},
|
||||
match self.stroke {
|
||||
Some(stroke) => stroke.render(),
|
||||
None => String::new(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user