mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-05 15:18:11 +08:00
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:
co-authored by
Keavon Chambers
Adam
Dennis Kobert
parent
fd3613018a
commit
1652c713a6
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user