Implement anchor and handle point rendering with the Path Tool (#353)

* Implement Path Tool

* Draw a red rectangle where the first point on the shape is

* Correctly render anchors, handles, and connecting lines

* Fix drain() which can panic

* Refactor frontend messages to work as return values not callbacks

* Reduce the number of unnecessary frontend updates

* Fix stack overflow by using a loop

* Group Document Render calls and put them at the end

* Speed hacks for dirtification

* Add performance

* Bunch folder changed updates

* Add triggers to redraw overlays to movement_handler

* Polish the pixel-perfect rendering of vector manipulators

* Restore scrollbars that were disabled

* Cleanup

* WIP Add shape outline rendering

* Fix compiling

* Add outlines to selected shapes

* Fix outlines rendering over handles and anchors

* Fix dirtification

* Add a comment

* Address code review feedback

* Formatting

* Small tweaks

Co-authored-by: Oliver Davies <oliver@psyfer.io>
Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
Keavon Chambers
2021-08-29 00:10:54 -07:00
committed by GitHub
co-authored by Oliver Davies Dennis Kobert
parent 8336b3e809
commit 913b9365e2
41 changed files with 759 additions and 282 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ impl Color {
}
/// Return an opaque `Color` from given `f32` RGB channels.
const fn from_unsafe(red: f32, green: f32, blue: f32) -> Color {
pub const fn from_unsafe(red: f32, green: f32, blue: f32) -> Color {
Color { red, green, blue, alpha: 1. }
}
+89 -13
View File
@@ -215,7 +215,6 @@ impl Document {
}
pub fn mark_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
self.mark_downstream_as_dirty(path)?;
self.mark_upstream_as_dirty(path)?;
Ok(())
}
@@ -264,6 +263,10 @@ impl Document {
Ok(())
}
pub fn generate_transform_relative_to_viewport(&self, from: &[LayerId]) -> Result<DAffine2, DocumentError> {
self.generate_transform_across_scope(from, None)
}
pub fn apply_transform_relative_to_viewport(&mut self, layer: &[LayerId], transform: DAffine2) -> Result<(), DocumentError> {
self.transform_relative_to_scope(layer, None, transform)
}
@@ -297,33 +300,79 @@ impl Document {
let responses = match &operation {
Operation::AddEllipse { path, insert_index, transform, style } => {
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::ellipse(*style)), *transform), *insert_index)?;
let layer = Layer::new(LayerDataType::Shape(Shape::ellipse(*style)), *transform);
self.set_layer(path, layer, *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
}
Operation::AddOverlayEllipse { path, transform, style } => {
let mut ellipse = Shape::ellipse(*style);
ellipse.render_index = -1;
let mut layer = Layer::new(LayerDataType::Shape(ellipse), *transform);
layer.overlay = true;
self.set_layer(path, layer, -1)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
}
Operation::AddRect { path, insert_index, transform, style } => {
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::rectangle(*style)), *transform), *insert_index)?;
let layer = Layer::new(LayerDataType::Shape(Shape::rectangle(*style)), *transform);
self.set_layer(path, layer, *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
}
Operation::AddBoundingBox { path, transform, style } => {
Operation::AddOverlayRect { path, transform, style } => {
let mut rect = Shape::rectangle(*style);
rect.render_index = -1;
let mut layer = Layer::new(LayerDataType::Shape(rect), *transform);
layer.overlay = true;
self.set_layer(path, layer, -1)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
}
Operation::AddShape {
Operation::AddLine { path, insert_index, transform, style } => {
let layer = Layer::new(LayerDataType::Shape(Shape::line(*style)), *transform);
self.set_layer(path, layer, *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
}
Operation::AddOverlayLine { path, transform, style } => {
let mut line = Shape::line(*style);
line.render_index = -1;
let mut layer = Layer::new(LayerDataType::Shape(line), *transform);
layer.overlay = true;
self.set_layer(path, layer, -1)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
}
Operation::AddNgon {
path,
insert_index,
transform,
style,
sides,
} => {
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::shape(*sides, *style)), *transform), *insert_index)?;
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::ngon(*sides, *style)), *transform), *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
}
Operation::AddLine { path, insert_index, transform, style } => {
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::line(*style)), *transform), *insert_index)?;
Operation::AddOverlayShape { path, style, bez_path } => {
let mut shape = Shape::from_bez_path(bez_path.clone(), *style, false);
shape.render_index = -1;
let mut layer = Layer::new(LayerDataType::Shape(shape), DAffine2::IDENTITY.to_cols_array());
layer.overlay = true;
self.set_layer(path, layer, -1)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
}
Operation::AddPen {
@@ -395,6 +444,19 @@ impl Document {
self.mark_as_dirty(path)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::SetShapePathInViewport { path, bez_path, transform } => {
let transform = DAffine2::from_cols_array(transform);
self.set_transform_relative_to_viewport(path, transform)?;
self.mark_as_dirty(path)?;
match &mut self.layer_mut(path)?.data {
LayerDataType::Shape(shape) => {
shape.path = bez_path.clone();
}
LayerDataType::Folder(_) => (),
}
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::TransformLayerInScope { path, transform, scope } => {
let transform = DAffine2::from_cols_array(transform);
let scope = DAffine2::from_cols_array(scope);
@@ -416,11 +478,16 @@ impl Document {
self.mark_as_dirty(path)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::ToggleVisibility { path } => {
Operation::ToggleLayerVisibility { path } => {
self.mark_as_dirty(path)?;
if let Ok(layer) = self.layer_mut(path) {
layer.visible = !layer.visible;
}
let layer = self.layer_mut(path)?;
layer.visible = !layer.visible;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::SetLayerVisibility { path, visible } => {
self.mark_as_dirty(path)?;
let layer = self.layer_mut(path)?;
layer.visible = *visible;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::SetLayerBlendMode { path, blend_mode } => {
@@ -435,7 +502,16 @@ impl Document {
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::FillLayer { path, color } => {
Operation::SetLayerStyle { path, style } => {
let layer = self.layer_mut(path)?;
match &mut layer.data {
LayerDataType::Shape(s) => s.style = *style,
_ => return Err(DocumentError::NotAShape),
}
self.mark_as_dirty(path)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::SetLayerFill { path, color } => {
let layer = self.layer_mut(path)?;
match &mut layer.data {
LayerDataType::Shape(s) => s.style.set_fill(layers::style::Fill::new(*color)),
+10 -1
View File
@@ -77,7 +77,16 @@ impl Shape {
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
}
pub fn shape(sides: u8, style: PathStyle) -> Self {
pub fn from_bez_path(bez_path: BezPath, style: PathStyle, solid: bool) -> Self {
Self {
path: bez_path,
style,
render_index: 1,
solid: solid,
}
}
pub fn ngon(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())
+32 -4
View File
@@ -20,13 +20,18 @@ pub enum Operation {
transform: [f64; 6],
style: style::PathStyle,
},
AddOverlayEllipse {
path: Vec<LayerId>,
transform: [f64; 6],
style: style::PathStyle,
},
AddRect {
path: Vec<LayerId>,
insert_index: isize,
transform: [f64; 6],
style: style::PathStyle,
},
AddBoundingBox {
AddOverlayRect {
path: Vec<LayerId>,
transform: [f64; 6],
style: style::PathStyle,
@@ -37,6 +42,11 @@ pub enum Operation {
transform: [f64; 6],
style: style::PathStyle,
},
AddOverlayLine {
path: Vec<LayerId>,
transform: [f64; 6],
style: style::PathStyle,
},
AddPen {
path: Vec<LayerId>,
transform: [f64; 6],
@@ -44,13 +54,18 @@ pub enum Operation {
points: Vec<(f64, f64)>,
style: style::PathStyle,
},
AddShape {
AddNgon {
path: Vec<LayerId>,
insert_index: isize,
transform: [f64; 6],
sides: u8,
style: style::PathStyle,
},
AddOverlayShape {
path: Vec<LayerId>,
bez_path: kurbo::BezPath,
style: style::PathStyle,
},
DeleteLayer {
path: Vec<LayerId>,
},
@@ -81,6 +96,11 @@ pub enum Operation {
path: Vec<LayerId>,
transform: [f64; 6],
},
SetShapePathInViewport {
path: Vec<LayerId>,
bez_path: kurbo::BezPath,
transform: [f64; 6],
},
TransformLayerInScope {
path: Vec<LayerId>,
transform: [f64; 6],
@@ -95,9 +115,13 @@ pub enum Operation {
path: Vec<LayerId>,
transform: [f64; 6],
},
ToggleVisibility {
ToggleLayerVisibility {
path: Vec<LayerId>,
},
SetLayerVisibility {
path: Vec<LayerId>,
visible: bool,
},
SetLayerBlendMode {
path: Vec<LayerId>,
blend_mode: BlendMode,
@@ -106,7 +130,11 @@ pub enum Operation {
path: Vec<LayerId>,
opacity: f64,
},
FillLayer {
SetLayerStyle {
path: Vec<LayerId>,
style: style::PathStyle,
},
SetLayerFill {
path: Vec<LayerId>,
color: Color,
},