mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-27 07:28:12 +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
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user