Add nondestructive vector editing (#1676)

* Initial vector modify node

* Initial extraction of data from monitor nodes

* Migrate to point id

* Start converting to modify node

* Non destructive spline tool (tout le reste est cassé)

* Fix unconnected modify node

* Fix freehand tool

* Pen tool

* Migrate demo art

* Select points

* Fix the demo artwork

* Fix the X and Y inputs for path tool

* G1 continous toggle

* Delete points

* Fix test

* Insert point

* Improve robustness of handles

* Fix GRS shortcuts on path

* Dragging points

* Fix build

* Preserve opposing handle lengths

* Update demo art and snapping

* Fix polygon tool

* Double click end anchor

* Improve dragging

* Fix text shifting

* Select only connected verts

* Colinear alt

* Cleanup

* Fix imports

* Improve pen tool avoiding handle placement

* Improve disolve

* Remove pivot widget from Transform node properties

* Fix demo art

* Fix bugs

* Re-save demo artwork

* Code review

* Serialize hashmap as tuple vec to enable deserialize_inputs

* Fix migrate

* Add document upgrade function to editor_api.rs

* Finalize document upgrading

* Rename to the Path node

* Remove smoothing from Freehand tool

* Upgrade demo artwork

* Propertly disable raw-rs tests

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: Adam <adamgerhant@gmail.com>
Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
James Lindsay
2024-07-05 13:42:40 -07:00
committed by GitHub
co-authored by Keavon Chambers Adam Dennis Kobert
parent fd3613018a
commit 1652c713a6
96 changed files with 3343 additions and 2622 deletions
@@ -37,11 +37,10 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context:
} else {
DVec2::new(secondary_pos, primary_end)
};
overlay_context.line(
overlay_context.colored_line(
document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())),
None,
&("#".to_string() + &grid_color.rgba_hex()),
);
}
}
@@ -112,11 +111,10 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m
let x_pos = (((min_x - origin.x) / spacing).ceil() + line_index as f64) * spacing + origin.x;
let start = DVec2::new(x_pos, min_y);
let end = DVec2::new(x_pos, max_y);
overlay_context.line(
overlay_context.colored_line(
document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())),
None,
&("#".to_string() + &grid_color.rgba_hex()),
);
}
@@ -131,11 +129,10 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m
let y_pos = (((inverse_project(&min_y) - origin.y) / spacing).ceil() + line_index as f64) * spacing + origin.y;
let start = DVec2::new(min_x, project(&DVec2::new(min_x, y_pos)));
let end = DVec2::new(max_x, project(&DVec2::new(max_x, y_pos)));
overlay_context.line(
overlay_context.colored_line(
document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())),
None,
&("#".to_string() + &grid_color.rgba_hex()),
);
}
}
@@ -178,7 +175,7 @@ fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context
let start = DVec2::new(min_x + x_offset, project(&DVec2::new(min_x + x_offset, y_pos)));
let end = DVec2::new(max_x + x_offset, project(&DVec2::new(max_x + x_offset, y_pos)));
overlay_context.line(
overlay_context.dashed_line(
document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())),
@@ -1,10 +1,9 @@
use super::utility_types::OverlayContext;
use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_groups, get_subpaths};
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use graphene_core::vector::ManipulatorPointId;
use glam::DVec2;
use wasm_bindgen::JsCast;
@@ -26,54 +25,55 @@ pub fn overlay_canvas_context() -> web_sys::CanvasRenderingContext2d {
pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext) {
for layer in document.selected_nodes.selected_layers(document.metadata()) {
let Some(subpaths) = get_subpaths(layer, &document.network) else { continue };
let Some(vector_data) = document.metadata.compute_modified_vector(layer, &document.network) else {
continue;
};
let transform = document.metadata().transform_to_viewport(layer);
let selected = shape_editor.selected_shape_state.get(&layer);
let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point));
overlay_context.outline(subpaths.iter(), transform);
overlay_context.outline_vector(&vector_data, transform);
for manipulator_group in get_manipulator_groups(subpaths) {
let anchor = manipulator_group.anchor;
let anchor_position = transform.transform_point2(anchor);
let not_under_anchor = |&position: &DVec2| transform.transform_point2(position).distance_squared(anchor_position) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
if let Some(in_handle) = manipulator_group.in_handle.filter(not_under_anchor) {
let handle_position = transform.transform_point2(in_handle);
overlay_context.line(handle_position, anchor_position, None, None);
overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::InHandle)));
for (segment_id, bezier, _start, _end) in vector_data.segment_bezier_iter() {
let bezier = bezier.apply_transformation(|point| transform.transform_point2(point));
let not_under_anchor = |position: DVec2, anchor: DVec2| position.distance_squared(anchor) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
match bezier.handles {
bezier_rs::BezierHandles::Quadratic { handle } if not_under_anchor(handle, bezier.start) && not_under_anchor(handle, bezier.end) => {
overlay_context.line(handle, bezier.start);
overlay_context.line(handle, bezier.end);
overlay_context.manipulator_handle(handle, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id)));
}
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
if not_under_anchor(handle_start, bezier.start) {
overlay_context.line(handle_start, bezier.start);
overlay_context.manipulator_handle(handle_start, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id)));
}
if not_under_anchor(handle_end, bezier.end) {
overlay_context.line(handle_end, bezier.end);
overlay_context.manipulator_handle(handle_end, is_selected(selected, ManipulatorPointId::EndHandle(segment_id)));
}
}
_ => {}
}
if let Some(out_handle) = manipulator_group.out_handle.filter(not_under_anchor) {
let handle_position = transform.transform_point2(out_handle);
overlay_context.line(handle_position, anchor_position, None, None);
overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle)));
}
overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::Anchor)), None);
}
for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) {
overlay_context.manipulator_anchor(transform.transform_point2(position), is_selected(selected, ManipulatorPointId::Anchor(id)), None);
}
}
}
pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext) {
for layer in document.selected_nodes.selected_layers(document.metadata()) {
let Some(subpaths) = get_subpaths(layer, &document.network) else { continue };
let Some(vector_data) = document.metadata.compute_modified_vector(layer, &document.network) else {
continue;
};
let transform = document.metadata().transform_to_viewport(layer);
let selected = shape_editor.selected_shape_state.get(&layer);
let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point));
let mut manipulator_groups = get_manipulator_groups(subpaths);
if let Some(first_manipulator) = manipulator_groups.next() {
let anchor = first_manipulator.anchor;
let anchor_position = transform.transform_point2(anchor);
overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(first_manipulator.id, SelectedType::Anchor)), None);
};
if let Some(last_manipulator) = manipulator_groups.last() {
let anchor = last_manipulator.anchor;
let anchor_position = transform.transform_point2(anchor);
overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(last_manipulator.id, SelectedType::Anchor)), None);
};
for point in vector_data.single_connected_points() {
let Some(position) = vector_data.point_domain.position_from_id(point) else { continue };
let position = transform.transform_point2(position);
overlay_context.manipulator_anchor(position, is_selected(selected, ManipulatorPointId::Anchor(point)), None);
}
}
}
@@ -2,12 +2,14 @@ use super::utility_functions::overlay_canvas_context;
use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER};
use crate::messages::prelude::Message;
use bezier_rs::Subpath;
use bezier_rs::{Bezier, Subpath};
use graphene_core::renderer::Quad;
use wasm_bindgen::JsValue;
use graphene_std::vector::{PointId, VectorData};
use core::borrow::Borrow;
use core::f64::consts::TAU;
use glam::{DAffine2, DVec2};
use wasm_bindgen::JsValue;
pub type OverlayProvider = fn(OverlayContext) -> Message;
@@ -39,7 +41,15 @@ impl OverlayContext {
self.render_context.stroke();
}
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, dash_width: Option<f64>) {
pub fn line(&mut self, start: DVec2, end: DVec2) {
self.dashed_line(start, end, None, None)
}
pub fn colored_line(&mut self, start: DVec2, end: DVec2, color: &str) {
self.dashed_line(start, end, Some(color), None)
}
pub fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, dash_width: Option<f64>) {
let start = start.round() - DVec2::splat(0.5);
let end = end.round() - DVec2::splat(0.5);
if let Some(dash_width) = dash_width {
@@ -152,9 +162,44 @@ impl OverlayContext {
self.render_context.stroke();
}
pub fn outline<'a, Id: bezier_rs::Identifier>(&mut self, subpaths: impl Iterator<Item = &'a Subpath<Id>>, transform: DAffine2) {
pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) {
self.render_context.begin_path();
let mut last_point = None;
for (_, bezier, start_id, end_id) in vector_data.segment_bezier_iter() {
let move_to = last_point != Some(start_id);
last_point = Some(end_id);
self.bezier_command(bezier, transform, move_to);
}
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE));
self.render_context.stroke();
}
pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
self.render_context.begin_path();
self.bezier_command(bezier, transform, true);
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE));
self.render_context.stroke();
}
fn bezier_command(&self, bezier: Bezier, transform: DAffine2, move_to: bool) {
let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point));
if move_to {
self.render_context.move_to(start.x, start.y);
}
match handles {
bezier_rs::BezierHandles::Linear => self.render_context.line_to(end.x, end.y),
bezier_rs::BezierHandles::Quadratic { handle } => self.render_context.quadratic_curve_to(handle.x, handle.y, end.x, end.y),
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => self.render_context.bezier_curve_to(handle_start.x, handle_start.y, handle_end.x, handle_end.y, end.x, end.y),
}
}
pub fn outline<'a>(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) {
self.render_context.begin_path();
for subpath in subpaths {
let subpath = subpath.borrow();
let mut curves = subpath.iter().peekable();
let Some(first) = curves.peek() else {