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
@@ -47,9 +47,9 @@
// Gradient color stops
$: gradient = colorOrGradient instanceof Gradient ? colorOrGradient : undefined;
let activeIndex = 0 as number | undefined;
$: selectedGradientColour = (activeIndex !== undefined && gradient?.atIndex(activeIndex)?.color) || (Color.fromCSS("black") as Color);
$: selectedGradientColor = (activeIndex !== undefined && gradient?.atIndex(activeIndex)?.color) || (Color.fromCSS("black") as Color);
// Currently viewed color
$: color = colorOrGradient instanceof Color ? colorOrGradient : selectedGradientColour;
$: color = colorOrGradient instanceof Color ? colorOrGradient : selectedGradientColor;
// New color components
let hue = hsva.h;
let saturation = hsva.s;
@@ -38,7 +38,7 @@
<input
type="checkbox"
id={`checkbox-input-${id}`}
{checked}
bind:checked
on:change={(_) => dispatch("checked", inputElement?.checked || false)}
{disabled}
tabindex={disabled ? -1 : 0}
@@ -331,7 +331,8 @@
}
// If no buttons are down, we are stuck in the drag state after having released the mouse, so we should exit.
if (e.buttons === 0) {
// For some reason on firefox in wayland the button is -1 and the buttons is 0.
if (e.buttons === 0 && e.button !== -1) {
document.exitPointerLock();
return;
}
@@ -16,6 +16,7 @@ import {
TriggerImport,
TriggerOpenDocument,
TriggerRevokeBlobUrl,
TriggerUpgradeDocumentToVectorManipulationFormat,
UpdateActiveDocument,
UpdateOpenDocumentsList,
} from "@graphite/wasm-communication/messages";
@@ -110,6 +111,11 @@ export function createPortfolioState(editor: Editor) {
editor.subscriptions.subscribeJsMessage(TriggerRevokeBlobUrl, async (triggerRevokeBlobUrl) => {
URL.revokeObjectURL(triggerRevokeBlobUrl.url);
});
editor.subscriptions.subscribeJsMessage(TriggerUpgradeDocumentToVectorManipulationFormat, async (triggerUpgradeDocumentToVectorManipulationFormat) => {
// TODO: Eventually remove this (probably starting late 2024)
const { documentId, documentName, documentIsAutoSaved, documentIsSaved, documentSerializedContent } = triggerUpgradeDocumentToVectorManipulationFormat;
editor.handle.triggerUpgradeDocumentToVectorManipulationFormat(documentId, documentName, documentIsAutoSaved, documentIsSaved, documentSerializedContent);
});
return {
subscribe,
@@ -788,6 +788,15 @@ export class TriggerAboutGraphiteLocalizedCommitDate extends JsMessage {
export class TriggerViewportResize extends JsMessage {}
// TODO: Eventually remove this (probably starting late 2024)
export class TriggerUpgradeDocumentToVectorManipulationFormat extends JsMessage {
readonly documentId!: bigint;
readonly documentName!: string;
readonly documentIsAutoSaved!: boolean;
readonly documentIsSaved!: boolean;
readonly documentSerializedContent!: string;
}
// WIDGET PROPS
export abstract class WidgetProps {
@@ -1439,6 +1448,7 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerSavePreferences,
TriggerTextCommit,
TriggerTextCopy,
TriggerUpgradeDocumentToVectorManipulationFormat,
TriggerViewportResize,
TriggerVisitLink,
UpdateActiveDocument,
+1
View File
@@ -30,6 +30,7 @@ js-sys = { workspace = true }
wasm-bindgen-futures = { workspace = true }
ron = { workspace = true, optional = true }
bezier-rs = { workspace = true }
glam = { workspace = true }
# We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm
wgpu = { workspace = true, features = ["fragile-send-sync-non-atomic-wasm"] }
meval = "0.2.0"
+166 -1
View File
@@ -636,6 +636,166 @@ impl EditorHandle {
pub fn inject_imaginate_poll_server_status(&self) {
self.dispatch(PortfolioMessage::ImaginatePollServerStatus);
}
// TODO: Eventually remove this (probably starting late 2024)
#[wasm_bindgen(js_name = triggerUpgradeDocumentToVectorManipulationFormat)]
pub async fn upgrade_document_to_vector_manipulation_format(
&self,
document_id: u64,
document_name: String,
document_is_auto_saved: bool,
document_is_saved: bool,
document_serialized_content: String,
) {
use editor::messages::portfolio::document::graph_operation::transform_utils::*;
use editor::messages::portfolio::document::graph_operation::utility_types::*;
use editor::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use editor::node_graph_executor::replace_node_runtime;
use editor::node_graph_executor::NodeRuntime;
use graph_craft::document::NodeInput;
use graph_craft::document::{value::TaggedValue, DocumentNodeImplementation};
use graphene_core::vector::*;
let (_, request_receiver) = std::sync::mpsc::channel();
let (response_sender, _) = std::sync::mpsc::channel();
let old_runtime = replace_node_runtime(NodeRuntime::new(request_receiver, response_sender));
let document_serialized_content = document_serialized_content.replace("\"ManipulatorGroupIds\"", "\"PointIds\"");
let mut editor = Editor::new();
let document_id = DocumentId(document_id);
editor.handle_message(PortfolioMessage::OpenDocumentFileWithId {
document_id,
document_name: document_name.clone(),
document_is_auto_saved,
document_is_saved,
document_serialized_content: document_serialized_content.clone(),
});
let document = editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap();
for node in document.network.nodes.values_mut().filter(|d| d.name == "Artboard") {
if let Some(network) = node.implementation.get_network_mut() {
for node in network.nodes.values_mut() {
if node.name == "To Artboard" {
node.implementation = DocumentNodeImplementation::proto("graphene_core::ConstructArtboardNode<_, _, _, _, _, _>");
if node.inputs.len() != 6 {
node.inputs.insert(2, NodeInput::value(TaggedValue::IVec2(glam::IVec2::default()), false));
}
}
}
}
}
let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler;
portfolio
.executor
.submit_node_graph_evaluation(portfolio.documents.get_mut(&portfolio.active_document_id().unwrap()).unwrap(), glam::UVec2::ONE)
.unwrap();
editor::node_graph_executor::run_node_graph().await;
let mut messages = VecDeque::new();
if let Err(err) = editor.poll_node_graph_evaluation(&mut messages) {
log::warn!(
"While attempting to upgrade the old document format, the graph evaluation failed which is necessary for the upgrade process:\n{:#?}",
err
);
replace_node_runtime(old_runtime.unwrap());
let document_name = document_name.clone() + "__DO_NOT_UPGRADE__";
self.dispatch(PortfolioMessage::OpenDocumentFileWithId {
document_id,
document_name,
document_is_auto_saved,
document_is_saved,
document_serialized_content,
});
return;
}
let mut updated_nodes = HashSet::new();
let document = editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap();
document.metadata.load_structure(&document.network);
for node in document.network.nodes.iter().filter(|(_, d)| d.name == "Merge").map(|(id, _)| *id).collect::<Vec<_>>() {
let layer = LayerNodeIdentifier::new(node, &document.network);
if document.metadata.is_folder(layer) {
continue;
}
let bounds = LayerBounds::new(&document.metadata, layer);
let mut responses = VecDeque::new();
let mut shape = None;
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer.to_node(), &mut document.network, &mut document.metadata, &mut document.node_graph_handler, &mut responses) {
modify_inputs.modify_existing_inputs("Transform", |inputs, node_id, metadata| {
if !updated_nodes.insert(node_id) {
return;
}
let transform = get_current_transform(&inputs);
let upstream_transform = metadata.upstream_transform(node_id);
let pivot_transform = glam::DAffine2::from_translation(upstream_transform.transform_point2(bounds.local_pivot(get_current_normalized_pivot(&inputs))));
update_transform(inputs, pivot_transform * transform * pivot_transform.inverse());
});
modify_inputs.modify_existing_inputs("Shape", |inputs, node_id, _metadata| {
if !updated_nodes.insert(node_id) {
return;
}
let empty_vec = Vec::new();
let path_data = if let NodeInput::Value {
tagged_value: TaggedValue::Subpaths(translation),
..
} = &inputs[0]
{
translation
} else {
&empty_vec
};
let empty_vec = Vec::new();
let colinear_manipulators = if let NodeInput::Value {
tagged_value: TaggedValue::PointIds(translation),
..
} = &inputs[1]
{
translation
} else {
&empty_vec
};
let mut vector_data = VectorData::from_subpaths(path_data, false);
vector_data.colinear_manipulators = colinear_manipulators
.iter()
.filter_map(|&point| ManipulatorPointId::Anchor(point).get_handle_pair(&vector_data))
.collect();
shape = Some((node_id, VectorModification::create_from_vector(&vector_data)));
});
}
if let Some((id, modification)) = shape {
let metadata = document.network.nodes.remove(&id).map(|node| node.metadata).unwrap_or_default();
let node_type = resolve_document_node_type("Path").unwrap();
let document_node = node_type.to_document_node_default_inputs([None, Some(NodeInput::value(TaggedValue::VectorModification(modification), false))], metadata);
document.network.nodes.insert(id, document_node);
}
}
let document_serialized_content = editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap().serialize_document();
replace_node_runtime(old_runtime.unwrap());
self.dispatch(PortfolioMessage::OpenDocumentFileWithId {
document_id,
document_name,
document_is_auto_saved,
document_is_saved,
document_serialized_content,
});
}
}
// ============================================================================
@@ -765,7 +925,12 @@ async fn poll_node_graph_evaluation() {
editor_and_handle(|editor, handle| {
let mut messages = VecDeque::new();
editor.poll_node_graph_evaluation(&mut messages);
if let Err(e) = editor.poll_node_graph_evaluation(&mut messages) {
// TODO: This is a hacky way to suppress the error, but it shouldn't be generated in the first place
if e != "No active document" {
error!("Error evaluating node graph:\n{e}");
}
}
// Send each `FrontendMessage` to the JavaScript frontend
for response in messages.into_iter().flat_map(|message| editor.handle_message(message)) {