mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
The fill's gradient placement input splits from one optional transform into the has-transform toggle and a plain transform, so the toggle's input name is fixed in the document format ahead of the transform toggle's own arrival. An unset placement keeps the toggle's false default and derives from the content's bounds at evaluation, which is also the unbaked placeholder the deferred gradient bake probes for. Documents saved with the optional form migrate by decomposing it, a wired transform source keeping its connection marked explicit, and the legacy fill migration now lands on the split shape directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1028 lines
40 KiB
Rust
1028 lines
40 KiB
Rust
use crate::messages::frontend::utility_types::{ExportBounds, FileType, RasterizedImage};
|
|
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
|
use crate::messages::prelude::*;
|
|
use glam::{DAffine2, DVec2, UVec2};
|
|
use graph_craft::application_io::EditorPreferences;
|
|
use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue};
|
|
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork};
|
|
use graph_craft::proto::GraphErrors;
|
|
use graphene_std::application_io::{ExportFormat, NodeGraphUpdateMessage, RenderConfig, TimingInformation};
|
|
use graphene_std::bounds::RenderBoundingBox;
|
|
use graphene_std::color::SRGBA8;
|
|
use graphene_std::list::List;
|
|
use graphene_std::raster::{CPU, Raster};
|
|
use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box};
|
|
use graphene_std::transform::Footprint;
|
|
use graphene_std::vector::style::HasTransform;
|
|
use graphene_std::vector::{Vector, graphic_types};
|
|
use graphene_std::{ATTR_TRANSFORM, Graphic, NodeInputDecleration};
|
|
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
|
|
use std::any::Any;
|
|
use std::sync::Arc;
|
|
|
|
mod runtime_io;
|
|
pub use runtime_io::NodeRuntimeIO;
|
|
|
|
mod runtime;
|
|
pub use runtime::*;
|
|
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
pub struct ExecutionRequest {
|
|
execution_id: u64,
|
|
render_config: RenderConfig,
|
|
}
|
|
|
|
pub struct ExecutionResponse {
|
|
execution_id: u64,
|
|
result: Result<TaggedValue, String>,
|
|
responses: VecDeque<FrontendMessage>,
|
|
vector_modify: HashMap<NodeId, Vector>,
|
|
/// The resulting value from the temporary inspected during execution
|
|
inspect_result: Option<InspectResult>,
|
|
}
|
|
|
|
#[derive(serde::Serialize, serde::Deserialize)]
|
|
pub struct CompilationResponse {
|
|
result: Result<ResolvedDocumentNodeTypesDelta, (ResolvedDocumentNodeTypesDelta, String)>,
|
|
node_graph_errors: GraphErrors,
|
|
}
|
|
|
|
pub enum NodeGraphUpdate {
|
|
ExecutionResponse(Box<ExecutionResponse>),
|
|
CompilationResponse(CompilationResponse),
|
|
EyedropperPreview(Raster<CPU>),
|
|
NodeGraphUpdateMessage(NodeGraphUpdateMessage),
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct NodeGraphExecutor {
|
|
runtime_io: NodeRuntimeIO,
|
|
current_execution_id: u64,
|
|
futures: VecDeque<(u64, ExecutionContext)>,
|
|
/// The most recently consumed plain render execution, kept so a runtime-replayed response with the same id
|
|
/// (sent after an async source completion) finds its context again.
|
|
last_execution_context: Option<(u64, ExecutionContext)>,
|
|
node_graph_hash: u64,
|
|
/// Full path from the root document network to the node currently being inspected by the Data panel, or empty if nothing is selected.
|
|
/// The last element is the inspect target itself; preceding elements identify the nested subnetwork the node lives in,
|
|
/// so the runtime can splice its monitor node alongside the target rather than only at the top level.
|
|
/// Tracking the previously-sent value lets `update_node_graph` re-send the network when the inspection target changes.
|
|
previous_node_to_inspect: Vec<NodeId>,
|
|
// TODO: Eventually remove this document upgrade code
|
|
/// In-progress one-time pre-pass that converts legacy bounding-box-relative gradients to absolute space, if any.
|
|
gradient_migration: Option<GradientMigration>,
|
|
// TODO: Eventually remove this document upgrade code
|
|
/// Documents whose gradient migration pass already ran this session, so entries that failed to measure
|
|
/// (kept pending for a retry on the next open) don't re-run the pass on every render request.
|
|
gradient_migration_attempted: HashSet<DocumentId>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct ExecutionContext {
|
|
export_config: Option<ExportConfig>,
|
|
document_id: DocumentId,
|
|
// TODO: Eventually remove this document upgrade code
|
|
/// Set when this execution is a gradient-migration measurement run, carrying the "Fill" node (addressed by its enclosing
|
|
/// network path) and its original relative gradient. The evaluated geometry is read back from the inspect result to size the
|
|
/// gradient; such runs never touch the visible artwork. Carrying the entry keeps a stale re-dispatched response paired with the fill it measured.
|
|
measure_fill: Option<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient)>,
|
|
}
|
|
|
|
// TODO: Eventually remove this document upgrade code
|
|
/// State for the deferred legacy-gradient migration: a queue of "Fill" nodes (each addressed by its enclosing network path)
|
|
/// whose decomposed gradient still needs its transform baked, each paired with its original relative gradient and measured
|
|
/// one at a time by redirecting the document export so even hidden/orphaned/nested branches evaluate.
|
|
#[derive(Debug, Clone)]
|
|
struct GradientMigration {
|
|
document_id: DocumentId,
|
|
remaining: VecDeque<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient)>,
|
|
resolution: UVec2,
|
|
scale: f64,
|
|
}
|
|
|
|
impl NodeGraphExecutor {
|
|
/// A local runtime is useful on threads since having global state causes flakes
|
|
#[cfg(test)]
|
|
pub(crate) fn new_with_local_runtime() -> (NodeRuntime, Self) {
|
|
let (request_sender, request_receiver) = std::sync::mpsc::channel();
|
|
let (response_sender, response_receiver) = std::sync::mpsc::channel();
|
|
let node_runtime = NodeRuntime::new(request_receiver, response_sender);
|
|
|
|
let node_executor = Self {
|
|
futures: Default::default(),
|
|
runtime_io: NodeRuntimeIO::with_channels(request_sender, response_receiver),
|
|
last_execution_context: None,
|
|
node_graph_hash: 0,
|
|
current_execution_id: 0,
|
|
previous_node_to_inspect: Vec::new(),
|
|
gradient_migration: None,
|
|
gradient_migration_attempted: HashSet::new(),
|
|
};
|
|
(node_runtime, node_executor)
|
|
}
|
|
|
|
/// Execute the network by flattening it and creating a borrow stack.
|
|
fn queue_execution(&mut self, render_config: RenderConfig) -> u64 {
|
|
let execution_id = self.current_execution_id;
|
|
self.current_execution_id += 1;
|
|
let request = ExecutionRequest { execution_id, render_config };
|
|
self.runtime_io.send(GraphRuntimeRequest::ExecutionRequest(request)).expect("Failed to send generation request");
|
|
|
|
execution_id
|
|
}
|
|
|
|
pub fn update_editor_preferences(&self, editor_preferences: EditorPreferences) {
|
|
self.runtime_io
|
|
.send(GraphRuntimeRequest::EditorPreferencesUpdate(editor_preferences))
|
|
.expect("Failed to send editor preferences");
|
|
}
|
|
|
|
/// Updates the network to monitor all inputs. Useful for the testing.
|
|
#[cfg(test)]
|
|
pub(crate) fn update_node_graph_instrumented(&mut self, document: &mut DocumentMessageHandler) -> Result<Instrumented, String> {
|
|
// We should always invalidate the cache.
|
|
self.node_graph_hash = crate::application::generate_uuid();
|
|
let mut network = document.network_interface.document_network().clone();
|
|
let instrumented = Instrumented::new(&mut network);
|
|
|
|
let resources = document.resources.registry.clone();
|
|
|
|
self.runtime_io
|
|
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate {
|
|
network,
|
|
resources,
|
|
node_to_inspect: Vec::new(),
|
|
}))
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(instrumented)
|
|
}
|
|
|
|
/// Update the cached network if necessary.
|
|
fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, node_to_inspect: Vec<NodeId>, ignore_hash: bool) -> Result<(), String> {
|
|
let network_hash = document.network_interface.network_hash();
|
|
// Refresh the graph when it changes or the inspect node changes
|
|
if network_hash != self.node_graph_hash || self.previous_node_to_inspect != node_to_inspect || ignore_hash {
|
|
let network = document.network_interface.document_network().clone();
|
|
self.previous_node_to_inspect.clone_from(&node_to_inspect);
|
|
self.node_graph_hash = network_hash;
|
|
|
|
let resources = document.resources.registry.clone();
|
|
|
|
self.runtime_io
|
|
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, resources, node_to_inspect }))
|
|
.map_err(|e| e.to_string())?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Adds an evaluate request for whatever current network is cached.
|
|
pub(crate) fn submit_current_node_graph_evaluation(
|
|
&mut self,
|
|
document: &mut DocumentMessageHandler,
|
|
document_id: DocumentId,
|
|
viewport_resolution: UVec2,
|
|
viewport_scale: f64,
|
|
time: TimingInformation,
|
|
pointer: DVec2,
|
|
) -> Result<Message, String> {
|
|
let viewport = Footprint {
|
|
transform: document.metadata().document_to_viewport,
|
|
resolution: viewport_resolution,
|
|
..Default::default()
|
|
};
|
|
let render_config = RenderConfig {
|
|
viewport,
|
|
scale: viewport_scale,
|
|
time,
|
|
pointer,
|
|
export_format: graphene_std::application_io::ExportFormat::Raster,
|
|
render_mode: document.render_mode,
|
|
for_export: false,
|
|
for_eyedropper: false,
|
|
};
|
|
|
|
// Execute the node graph
|
|
let execution_id = self.queue_execution(render_config);
|
|
|
|
self.futures.push_back((
|
|
execution_id,
|
|
ExecutionContext {
|
|
export_config: None,
|
|
document_id,
|
|
measure_fill: None,
|
|
},
|
|
));
|
|
|
|
Ok(DeferMessage::SetGraphSubmissionIndex { execution_id }.into())
|
|
}
|
|
|
|
/// Evaluates a node graph, computing the entire graph
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn submit_node_graph_evaluation(
|
|
&mut self,
|
|
document: &mut DocumentMessageHandler,
|
|
document_id: DocumentId,
|
|
viewport_resolution: UVec2,
|
|
viewport_scale: f64,
|
|
time: TimingInformation,
|
|
node_to_inspect: Vec<NodeId>,
|
|
ignore_hash: bool,
|
|
pointer: DVec2,
|
|
) -> Result<Message, String> {
|
|
self.update_node_graph(document, node_to_inspect, ignore_hash)?;
|
|
self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, viewport_scale, time, pointer)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(crate) fn submit_eyedropper_preview(
|
|
&mut self,
|
|
document: &DocumentMessageHandler,
|
|
document_id: DocumentId,
|
|
transform: DAffine2,
|
|
pointer: DVec2,
|
|
viewport_resolution: UVec2,
|
|
viewport_scale: f64,
|
|
time: TimingInformation,
|
|
) -> Result<Message, String> {
|
|
let viewport = Footprint {
|
|
transform,
|
|
resolution: viewport_resolution,
|
|
..Default::default()
|
|
};
|
|
|
|
// TODO: On desktop, SVG Preview mode cannot work with the Eyedropper tool until <https://github.com/GraphiteEditor/Graphite/issues/3796> is implemented.
|
|
// TODO: So for now, we fall back to the Eyedropper using Normal mode (Vello) rendering, which looks similar enough to SVG Preview.
|
|
#[cfg(not(target_family = "wasm"))]
|
|
let render_mode = match document.render_mode {
|
|
graphene_std::vector::style::RenderMode::SvgPreview => graphene_std::vector::style::RenderMode::Normal,
|
|
other => other,
|
|
};
|
|
// On web, SVG Preview is handled by the frontend's SVG rasterization path instead, producing the correct result, so we keep it enabled.
|
|
#[cfg(target_family = "wasm")]
|
|
let render_mode = document.render_mode;
|
|
|
|
let render_config = RenderConfig {
|
|
viewport,
|
|
scale: viewport_scale,
|
|
time,
|
|
pointer,
|
|
export_format: graphene_std::application_io::ExportFormat::Raster,
|
|
render_mode,
|
|
for_export: false,
|
|
for_eyedropper: true,
|
|
};
|
|
|
|
// Execute the node graph
|
|
let execution_id = self.queue_execution(render_config);
|
|
|
|
self.futures.push_back((
|
|
execution_id,
|
|
ExecutionContext {
|
|
export_config: None,
|
|
document_id,
|
|
measure_fill: None,
|
|
},
|
|
));
|
|
|
|
Ok(DeferMessage::SetGraphSubmissionIndex { execution_id }.into())
|
|
}
|
|
|
|
/// Evaluates a node graph for export
|
|
pub fn submit_document_export(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, mut export_config: ExportConfig) -> Result<(), String> {
|
|
let network = document.network_interface.document_network().clone();
|
|
let resources = document.resources.registry.clone();
|
|
|
|
let export_format = if export_config.file_type == FileType::Svg {
|
|
graphene_std::application_io::ExportFormat::Svg
|
|
} else {
|
|
graphene_std::application_io::ExportFormat::Raster
|
|
};
|
|
|
|
// Calculate the bounding box of the region to be exported (artboard bounds always contribute).
|
|
// `AllArtwork` and `Selection` expand vector layer bounds by the rendered stroke width so strokes
|
|
// drawn at render-time (without a `Solidify Stroke`) aren't clipped at the export canvas edge.
|
|
let bounds = match export_config.bounds {
|
|
ExportBounds::AllArtwork => document.network_interface.document_bounds_document_space_with_stroke(true),
|
|
ExportBounds::Selection => document.network_interface.selected_bounds_document_space_with_stroke(true, &[]),
|
|
ExportBounds::Artboard(id) => document.metadata().bounding_box_document(id),
|
|
}
|
|
.ok_or_else(|| "No bounding box".to_string())?;
|
|
|
|
let resolution_in_document_space = bounds[1] - bounds[0];
|
|
let export_resolution = resolution_in_document_space * export_config.scale_factor;
|
|
let resolution = export_resolution.round().as_uvec2();
|
|
let transform = DAffine2::from_translation(bounds[0]).inverse();
|
|
let viewport = Footprint {
|
|
resolution,
|
|
transform,
|
|
..Default::default()
|
|
};
|
|
|
|
let render_config = RenderConfig {
|
|
viewport,
|
|
scale: export_config.scale_factor,
|
|
time: Default::default(),
|
|
pointer: DVec2::ZERO,
|
|
export_format,
|
|
render_mode: document.render_mode,
|
|
for_export: true,
|
|
for_eyedropper: false,
|
|
};
|
|
export_config.size = resolution;
|
|
|
|
// Execute the node graph
|
|
self.runtime_io
|
|
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate {
|
|
network,
|
|
resources,
|
|
node_to_inspect: Vec::new(),
|
|
}))
|
|
.map_err(|e| e.to_string())?;
|
|
let execution_id = self.queue_execution(render_config);
|
|
self.futures.push_back((
|
|
execution_id,
|
|
ExecutionContext {
|
|
export_config: Some(export_config),
|
|
document_id,
|
|
measure_fill: None,
|
|
},
|
|
));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn poll_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
|
// TODO: Eventually remove this document upgrade code
|
|
// A gradient migration belongs to one document; if the active document changed away from it, cancel so its stale in-flight response is ignored and revisiting restarts a fresh pass
|
|
if self.gradient_migration.as_ref().is_some_and(|migration| migration.document_id != document_id) {
|
|
self.cancel_gradient_migration();
|
|
}
|
|
|
|
let results = self.runtime_io.receive().collect::<Vec<_>>();
|
|
for response in results {
|
|
match response {
|
|
NodeGraphUpdate::ExecutionResponse(execution_response) => {
|
|
let ExecutionResponse {
|
|
execution_id,
|
|
result,
|
|
responses: existing_responses,
|
|
vector_modify,
|
|
inspect_result,
|
|
} = *execution_response;
|
|
|
|
while let Some(&(queued_execution_id, _)) = self.futures.front() {
|
|
if queued_execution_id < execution_id {
|
|
self.futures.pop_front();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let execution_context = if self.futures.front().is_some_and(|&(queued_execution_id, _)| queued_execution_id == execution_id) {
|
|
let (_, execution_context) = self.futures.pop_front().expect("front was just matched");
|
|
self.last_execution_context = Some((execution_id, execution_context.clone()));
|
|
execution_context
|
|
} else {
|
|
// A runtime-replayed response re-uses an already consumed id; only plain renders may re-apply.
|
|
match &self.last_execution_context {
|
|
Some((last_execution_id, execution_context)) if *last_execution_id == execution_id => {
|
|
if execution_context.export_config.is_some() || execution_context.measure_fill.is_some() {
|
|
continue;
|
|
}
|
|
execution_context.clone()
|
|
}
|
|
_ => panic!("InvalidGenerationId"),
|
|
}
|
|
};
|
|
|
|
// TODO: Eventually remove this document upgrade code
|
|
// Gradient-migration measurement runs only read back the fill's evaluated geometry; they never render to the artwork.
|
|
// Apply only to the active document's in-progress migration, dropping responses left over from a document switch.
|
|
if let Some(bake_target) = execution_context.measure_fill {
|
|
if execution_context.document_id == document_id && self.gradient_migration.as_ref().is_some_and(|migration| migration.document_id == document_id) {
|
|
self.handle_gradient_measurement(document, document_id, bake_target, result.ok().and(inspect_result), responses);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
responses.add(OverlaysMessage::Draw);
|
|
|
|
let node_graph_output = match result {
|
|
Ok(output) => output,
|
|
Err(e) => {
|
|
// Clear the click targets while the graph is in an un-renderable state
|
|
document.network_interface.update_click_targets(HashMap::new());
|
|
document.network_interface.update_outlines(HashMap::new());
|
|
document.network_interface.update_vector_modify(HashMap::new());
|
|
return Err(format!("Node graph evaluation failed:\n{e}"));
|
|
}
|
|
};
|
|
|
|
responses.extend(existing_responses.into_iter().map(Into::into));
|
|
document.network_interface.update_vector_modify(vector_modify);
|
|
|
|
if let Some(export_config) = execution_context.export_config {
|
|
// Special handling for exporting the artwork
|
|
self.process_export(node_graph_output, export_config, document, responses)?;
|
|
} else {
|
|
self.process_node_graph_output(node_graph_output, responses)?;
|
|
}
|
|
responses.add(DeferMessage::TriggerGraphRun {
|
|
execution_id,
|
|
document_id: execution_context.document_id,
|
|
});
|
|
|
|
// Update the Data panel on the frontend using the value of the inspect result.
|
|
if let Some(inspect_result) = (!self.previous_node_to_inspect.is_empty()).then_some(inspect_result).flatten() {
|
|
responses.add(DataPanelMessage::UpdateLayout { inspect_result });
|
|
} else {
|
|
responses.add(DataPanelMessage::ClearLayout);
|
|
}
|
|
}
|
|
NodeGraphUpdate::CompilationResponse(execution_response) => {
|
|
let CompilationResponse { node_graph_errors, result } = execution_response;
|
|
|
|
// TODO: Eventually remove this document upgrade code
|
|
// The migration's temporary redirected-export compilations must not push types or a graph view to the frontend
|
|
if self.gradient_migration.is_some() {
|
|
if let Err((_, e)) = &result {
|
|
log::trace!("Gradient migration measurement compile: {e}");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let type_delta = match result {
|
|
Err((incomplete_delta, e)) => {
|
|
// Clear the click targets while the graph is in an un-renderable state
|
|
|
|
document.network_interface.update_click_targets(HashMap::new());
|
|
document.network_interface.update_outlines(HashMap::new());
|
|
document.network_interface.update_vector_modify(HashMap::new());
|
|
|
|
log::trace!("{e}");
|
|
|
|
responses.add(NodeGraphMessage::UpdateTypes {
|
|
resolved_types: incomplete_delta,
|
|
node_graph_errors,
|
|
});
|
|
responses.add(NodeGraphMessage::SendGraph);
|
|
|
|
return Err(format!("Node graph evaluation failed:\n{e}"));
|
|
}
|
|
Ok(result) => result,
|
|
};
|
|
|
|
responses.add(NodeGraphMessage::UpdateTypes {
|
|
resolved_types: type_delta,
|
|
node_graph_errors,
|
|
});
|
|
responses.add(NodeGraphMessage::SendGraph);
|
|
}
|
|
NodeGraphUpdate::EyedropperPreview(raster) => {
|
|
let (data, width, height) = raster.to_flat_u8();
|
|
responses.add(EyedropperToolMessage::PreviewImage { data, width, height });
|
|
}
|
|
NodeGraphUpdate::NodeGraphUpdateMessage(_) => {}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// TODO: Eventually remove this document upgrade code
|
|
/// Kick off the one-time pre-pass converting legacy bounding-box gradients to absolute space, or no-op if already running.
|
|
/// Returns false when the render should proceed normally instead: the pass already ran this session, so any entries still
|
|
/// pending are unmeasurable ones kept to retry on the next open.
|
|
pub(crate) fn drive_gradient_migration(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, resolution: UVec2, scale: f64, responses: &mut VecDeque<Message>) -> bool {
|
|
if self.gradient_migration_attempted.contains(&document_id) {
|
|
return false;
|
|
}
|
|
|
|
match &self.gradient_migration {
|
|
// Already running for this document
|
|
Some(migration) if migration.document_id == document_id => return true,
|
|
// A different document's migration is in progress; cancel it so this one can run (the other restarts when revisited)
|
|
Some(_) => self.gradient_migration = None,
|
|
None => {}
|
|
}
|
|
|
|
// Snapshot the queue but leave `pending_gradient_bbox_bake` populated, so subsequent render requests keep deferring here (and hit the guard above); each entry is removed from the document as its bake lands.
|
|
let remaining: VecDeque<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient)> = document.pending_gradient_bbox_bake.iter().cloned().collect();
|
|
let Some((first_network_path, first_fill, first_gradient)) = remaining.front().cloned() else {
|
|
return false;
|
|
};
|
|
|
|
self.gradient_migration = Some(GradientMigration {
|
|
document_id,
|
|
remaining,
|
|
resolution,
|
|
scale,
|
|
});
|
|
self.dispatch_gradient_measurement(document, document_id, first_network_path, first_fill, first_gradient, resolution, scale, responses);
|
|
|
|
true
|
|
}
|
|
|
|
// TODO: Eventually remove this document upgrade code
|
|
/// Run the graph with its export chain redirected down to the (possibly nested) Fill at `network_path`/`fill_node_id`
|
|
/// (so its branch evaluates even when hidden or orphaned) and that node inspected, so its evaluated geometry returns
|
|
/// in the execution response without touching the visible artwork.
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn dispatch_gradient_measurement(
|
|
&mut self,
|
|
document: &mut DocumentMessageHandler,
|
|
document_id: DocumentId,
|
|
network_path: Vec<NodeId>,
|
|
fill_node_id: NodeId,
|
|
gradient: graphic_types::migrations::legacy::Gradient,
|
|
resolution: UVec2,
|
|
scale: f64,
|
|
responses: &mut VecDeque<Message>,
|
|
) {
|
|
let mut network = document.network_interface.document_network().clone();
|
|
|
|
// On this throwaway clone, redirect each level's export down to the Fill, un-hiding the Fill and its enclosing subnetworks so it's measured as a real Fill rather than a passthrough.
|
|
// But upstream generators keep their visibility, so a hidden one intentionally contributes no geometry.
|
|
let full_path: Vec<NodeId> = network_path.iter().copied().chain(std::iter::once(fill_node_id)).collect();
|
|
if !redirect_export_chain(&mut network, &full_path) {
|
|
// The fill was deleted or is otherwise unreachable, so drop its stale entry instead of retrying it on every open
|
|
log::warn!("Gradient migration: could not redirect the document network's export chain to fill node {fill_node_id:?}; dropping its pending gradient bake");
|
|
remove_pending_gradient_bake(document, &network_path, fill_node_id);
|
|
self.advance_gradient_migration(document, document_id, responses);
|
|
return;
|
|
}
|
|
|
|
let resources = document.resources.registry.clone();
|
|
if self
|
|
.runtime_io
|
|
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate {
|
|
network,
|
|
resources,
|
|
node_to_inspect: full_path.clone(),
|
|
}))
|
|
.is_err()
|
|
{
|
|
log::error!("Gradient migration: failed to send measurement graph update");
|
|
self.advance_gradient_migration(document, document_id, responses);
|
|
return;
|
|
}
|
|
|
|
// Force the next normal render to recompile and re-send the real, non-redirected network
|
|
self.node_graph_hash = 0;
|
|
self.previous_node_to_inspect = full_path;
|
|
|
|
let viewport = Footprint {
|
|
transform: document.metadata().document_to_viewport,
|
|
resolution,
|
|
..Default::default()
|
|
};
|
|
let render_config = RenderConfig {
|
|
viewport,
|
|
scale,
|
|
time: Default::default(),
|
|
pointer: DVec2::ZERO,
|
|
export_format: ExportFormat::Svg,
|
|
render_mode: document.render_mode,
|
|
for_export: false,
|
|
for_eyedropper: false,
|
|
};
|
|
let execution_id = self.queue_execution(render_config);
|
|
self.futures.push_back((
|
|
execution_id,
|
|
ExecutionContext {
|
|
export_config: None,
|
|
document_id,
|
|
measure_fill: Some((network_path, fill_node_id, gradient)),
|
|
},
|
|
));
|
|
}
|
|
|
|
// TODO: Eventually remove this document upgrade code
|
|
/// Bake the just-measured fill's gradient transform into absolute space using its evaluated geometry, then advance the queue.
|
|
fn handle_gradient_measurement(
|
|
&mut self,
|
|
document: &mut DocumentMessageHandler,
|
|
document_id: DocumentId,
|
|
bake_target: (Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient),
|
|
inspect_result: Option<InspectResult>,
|
|
responses: &mut VecDeque<Message>,
|
|
) {
|
|
let (network_path, fill_node_id, gradient) = bake_target;
|
|
|
|
// Ignore a stale response whose fill is no longer the one being measured (a re-dispatch after a document switch can leave two measurements in flight for the same entry)
|
|
let is_current = self
|
|
.gradient_migration
|
|
.as_ref()
|
|
.and_then(|migration| migration.remaining.front())
|
|
.is_some_and(|(front_path, front_fill, _)| *front_fill == fill_node_id && *front_path == network_path);
|
|
if !is_current {
|
|
return;
|
|
}
|
|
|
|
match inspect_result.and_then(|mut result| result.take_data()).and_then(|data| measure_fill_geometry(&data)) {
|
|
Some((bounding_box, item_transform)) => {
|
|
// Skip the bake if the user already placed this gradient themselves, but drop the now-superseded entry either way
|
|
if fill_transform_unbaked(document, &network_path, fill_node_id) {
|
|
let absolute_gradient = gradient.to_absolute(bounding_box, item_transform);
|
|
let gradient_transform = absolute_gradient.transform * absolute_gradient.to_transform();
|
|
let transform_input = InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX);
|
|
let has_transform_input = InputConnector::node(fill_node_id, graphene_std::vector::fill::HasTransformInput::INDEX);
|
|
document
|
|
.network_interface
|
|
.set_input(&transform_input, NodeInput::value(TaggedValue::DAffine2(gradient_transform), false), &network_path);
|
|
document
|
|
.network_interface
|
|
.set_input(&has_transform_input, NodeInput::value(TaggedValue::HasTransform(HasTransform(true)), false), &network_path);
|
|
}
|
|
|
|
// The transform is settled, so its entry no longer needs to persist for a retry on the next open
|
|
remove_pending_gradient_bake(document, &network_path, fill_node_id);
|
|
}
|
|
// Leave the entry pending so the next open retries it
|
|
None => log::warn!("Gradient migration could not measure geometry for fill node {fill_node_id:?}; leaving its transform unbaked and pending a retry on the next open"),
|
|
}
|
|
|
|
self.advance_gradient_migration(document, document_id, responses);
|
|
}
|
|
|
|
// TODO: Eventually remove this document upgrade code
|
|
/// Move to the next queued fill, or finish the migration and trigger a normal render once the queue is empty.
|
|
fn advance_gradient_migration(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) {
|
|
let next = self.gradient_migration.as_mut().and_then(|migration| {
|
|
migration.remaining.pop_front();
|
|
migration.remaining.front().cloned()
|
|
});
|
|
|
|
if let Some((next_network_path, next_fill, next_gradient)) = next {
|
|
let (resolution, scale) = self.gradient_migration.as_ref().map(|migration| (migration.resolution, migration.scale)).unwrap_or((UVec2::ONE, 1.));
|
|
self.dispatch_gradient_measurement(document, document_id, next_network_path, next_fill, next_gradient, resolution, scale, responses);
|
|
} else {
|
|
// Entries still pending failed to measure; they stay persisted for the next open while this marker stops re-runs this session
|
|
self.cancel_gradient_migration();
|
|
self.gradient_migration_attempted.insert(document_id);
|
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
|
}
|
|
}
|
|
|
|
// TODO: Eventually remove this document upgrade code
|
|
/// Tear down the in-progress migration and clear the stale inspect target and hash so the next normal render recompiles the real network.
|
|
fn cancel_gradient_migration(&mut self) {
|
|
self.gradient_migration = None;
|
|
self.previous_node_to_inspect = Vec::new();
|
|
self.node_graph_hash = 0;
|
|
}
|
|
|
|
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
|
let TaggedValue::RenderOutput(render_output) = node_graph_output else {
|
|
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
|
|
};
|
|
|
|
match render_output.data {
|
|
RenderOutputType::Svg { svg, image_data } => {
|
|
// Convert each linear-light `Image<Color>` into gamma sRGB bytes (the DOM boundary form) before dispatching.
|
|
let rgba8_bytes = |&color: &_| <[u8; 4]>::from(SRGBA8::from(color));
|
|
let image_data = image_data
|
|
.into_iter()
|
|
.map(|(id, image)| RasterizedImage {
|
|
id,
|
|
width: image.width,
|
|
height: image.height,
|
|
pixels: image.data.iter().flat_map(rgba8_bytes).collect::<Vec<u8>>().into(),
|
|
})
|
|
.collect();
|
|
responses.add(FrontendMessage::UpdateImageData { image_data });
|
|
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
|
}
|
|
#[cfg(target_family = "wasm")]
|
|
RenderOutputType::CanvasFrame { canvas_id, resolution } => {
|
|
let svg = format!(
|
|
r#"<svg><foreignObject width="{}" height="{}"><div data-canvas-placeholder="{}" data-is-viewport="true"></div></foreignObject></svg>"#,
|
|
resolution.x, resolution.y, canvas_id,
|
|
);
|
|
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
|
}
|
|
RenderOutputType::Texture { .. } => {}
|
|
_ => return Err(format!("Invalid node graph output type: {:#?}", render_output.data)),
|
|
}
|
|
|
|
let RenderMetadata {
|
|
upstream_footprints,
|
|
local_transforms,
|
|
first_element_source_id,
|
|
click_targets,
|
|
outlines,
|
|
text_frames,
|
|
clip_targets,
|
|
vector_data,
|
|
fill_attributes,
|
|
stroke_attributes,
|
|
backgrounds: _,
|
|
} = render_output.metadata;
|
|
|
|
// Run these update state messages immediately
|
|
responses.add(DocumentMessage::UpdateUpstreamTransforms {
|
|
upstream_footprints,
|
|
local_transforms,
|
|
first_element_source_id,
|
|
});
|
|
responses.add(DocumentMessage::UpdateClickTargets { click_targets });
|
|
responses.add(DocumentMessage::UpdateOutlines { outlines });
|
|
responses.add(DocumentMessage::UpdateTextFrames { text_frames });
|
|
responses.add(DocumentMessage::UpdateClipTargets { clip_targets });
|
|
responses.add(DocumentMessage::UpdateVectorData { vector_data });
|
|
responses.add(DocumentMessage::UpdateFillAttributes { fill_attributes });
|
|
responses.add(DocumentMessage::UpdateStrokeAttributes { stroke_attributes });
|
|
responses.add(DocumentMessage::RenderScrollbars);
|
|
responses.add(DocumentMessage::RenderRulers);
|
|
responses.add(OverlaysMessage::Draw);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn process_export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
|
let ExportConfig {
|
|
file_type,
|
|
name,
|
|
size,
|
|
artboard_name,
|
|
artboard_count,
|
|
..
|
|
} = export_config;
|
|
|
|
let file_extension = match file_type {
|
|
FileType::Svg => "svg",
|
|
FileType::Png => "png",
|
|
FileType::Jpg => "jpg",
|
|
};
|
|
let base_name = match (artboard_name, artboard_count) {
|
|
(Some(artboard_name), count) if count > 1 => format!("{name} - {artboard_name}"),
|
|
_ => name,
|
|
};
|
|
let name = format!("{base_name}.{file_extension}");
|
|
let folder = document.path.as_ref().and_then(|path| path.parent()).map(|parent| parent.to_path_buf());
|
|
|
|
match node_graph_output {
|
|
TaggedValue::RenderOutput(RenderOutput {
|
|
data: RenderOutputType::Svg { svg, .. },
|
|
..
|
|
}) => {
|
|
if file_type == FileType::Svg {
|
|
responses.add(FrontendMessage::TriggerSaveFile {
|
|
name,
|
|
folder,
|
|
content: svg.into_bytes().into(),
|
|
});
|
|
} else {
|
|
let mime = file_type.to_mime().to_string();
|
|
let size = size.as_dvec2().into();
|
|
responses.add(FrontendMessage::TriggerExportImage { svg, name, mime, size });
|
|
}
|
|
}
|
|
#[cfg(feature = "gpu")]
|
|
TaggedValue::RenderOutput(RenderOutput {
|
|
data: RenderOutputType::Buffer { data, width, height },
|
|
..
|
|
}) if file_type != FileType::Svg => {
|
|
use image::buffer::ConvertBuffer;
|
|
use image::{ImageFormat, RgbImage, RgbaImage};
|
|
|
|
let Some(mut image) = RgbaImage::from_raw(width, height, data) else {
|
|
return Err("Failed to create image buffer for export".to_string());
|
|
};
|
|
|
|
let mut encoded = Vec::new();
|
|
let mut cursor = std::io::Cursor::new(&mut encoded);
|
|
|
|
match file_type {
|
|
FileType::Png => {
|
|
let result = image.write_to(&mut cursor, ImageFormat::Png);
|
|
if let Err(err) = result {
|
|
return Err(format!("Failed to encode PNG: {err}"));
|
|
}
|
|
}
|
|
FileType::Jpg => {
|
|
// Composite onto a white background since JPG doesn't support transparency
|
|
for pixel in image.pixels_mut() {
|
|
let [r, g, b, a] = pixel.0;
|
|
let alpha = a as f32 / 255.;
|
|
let blend = |channel: u8| (channel as f32 * alpha + 255. * (1. - alpha)).round() as u8;
|
|
*pixel = image::Rgba([blend(r), blend(g), blend(b), 255]);
|
|
}
|
|
|
|
let image: RgbImage = image.convert();
|
|
let result = image.write_to(&mut cursor, ImageFormat::Jpeg);
|
|
if let Err(err) = result {
|
|
return Err(format!("Failed to encode JPG: {err}"));
|
|
}
|
|
}
|
|
FileType::Svg => {
|
|
return Err("SVG cannot be exported from an image buffer".to_string());
|
|
}
|
|
}
|
|
|
|
responses.add(FrontendMessage::TriggerSaveFile {
|
|
name,
|
|
folder,
|
|
content: encoded.into(),
|
|
});
|
|
}
|
|
_ => {
|
|
return Err(format!("Incorrect render type for exporting to an SVG ({file_type:?}, {node_graph_output})"));
|
|
}
|
|
};
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// TODO: Eventually remove this document upgrade code
|
|
/// Whether the fill node's gradient placement is still the unset `HasTransform(false)` placeholder that the migration leaves
|
|
/// behind, meaning it has not yet been baked (or set by the user), so a measured bake may safely be written.
|
|
fn fill_transform_unbaked(document: &DocumentMessageHandler, network_path: &[NodeId], fill_node_id: NodeId) -> bool {
|
|
let Some(network) = document.network_interface.document_network().nested_network(network_path) else {
|
|
return false;
|
|
};
|
|
let Some(node) = network.nodes.get(&fill_node_id) else { return false };
|
|
matches!(
|
|
node.inputs.get(graphene_std::vector::fill::HasTransformInput::INDEX).and_then(|input| input.as_value()),
|
|
Some(TaggedValue::HasTransform(HasTransform(false)))
|
|
)
|
|
}
|
|
|
|
// TODO: Eventually remove this document upgrade code
|
|
/// Remove a fill's entry from the document's persisted pending-bake list, either because its bake landed or because it is stale.
|
|
fn remove_pending_gradient_bake(document: &mut DocumentMessageHandler, network_path: &[NodeId], fill_node_id: NodeId) {
|
|
if let Some(index) = document
|
|
.pending_gradient_bbox_bake
|
|
.iter()
|
|
.position(|(path, node_id, _)| *node_id == fill_node_id && path == network_path)
|
|
{
|
|
document.pending_gradient_bbox_bake.remove(index);
|
|
}
|
|
}
|
|
|
|
// TODO: Eventually remove this document upgrade code
|
|
/// Redirect the primary export at each level of `network` down through the subnetwork nodes named by `full_path`,
|
|
/// un-hiding each so the innermost node's branch evaluates. Returns false if any step is missing or not a subnetwork.
|
|
fn redirect_export_chain(network: &mut NodeNetwork, full_path: &[NodeId]) -> bool {
|
|
let Some((node_id, deeper_path)) = full_path.split_first() else { return true };
|
|
|
|
let Some(export) = network.exports.first_mut() else { return false };
|
|
*export = NodeInput::node(*node_id, 0);
|
|
|
|
let Some(node) = network.nodes.get_mut(node_id) else { return false };
|
|
node.visible = true;
|
|
|
|
if deeper_path.is_empty() {
|
|
return true;
|
|
}
|
|
match &mut node.implementation {
|
|
DocumentNodeImplementation::Network(nested) => redirect_export_chain(nested, deeper_path),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
// TODO: Eventually remove this document upgrade code
|
|
/// Turn a measured fill node's evaluated geometry into a `[0,1]² -> bounding box` affine plus the geometry's item transform.
|
|
fn measure_fill_geometry(data: &Arc<dyn Any + Send + Sync>) -> Option<(DAffine2, DAffine2)> {
|
|
if let Some(list) = introspected_output::<List<Vector>>(data) {
|
|
let vector = list.element(0)?;
|
|
let item_transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
|
let bounds = vector.nonzero_bounding_box();
|
|
let bounding_box_affine = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
|
return Some((bounding_box_affine, item_transform));
|
|
}
|
|
if let Some(list) = introspected_output::<List<Graphic>>(data) {
|
|
let RenderBoundingBox::Rectangle(bounds) = graphic_list_bounding_box(&list, DAffine2::IDENTITY) else {
|
|
return None;
|
|
};
|
|
let bounding_box_affine = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
|
return Some((bounding_box_affine, DAffine2::IDENTITY));
|
|
}
|
|
None
|
|
}
|
|
|
|
// TODO: Eventually remove this document upgrade code
|
|
/// Extract a monitor node's captured output element.
|
|
fn introspected_output<T: Clone + Send + Sync + 'static>(data: &Arc<dyn Any + Send + Sync>) -> Option<T> {
|
|
data.downcast_ref::<T>().cloned()
|
|
}
|
|
|
|
// Re-export for usage by tests in other modules
|
|
#[cfg(test)]
|
|
pub use test::Instrumented;
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use std::sync::Arc;
|
|
|
|
use super::*;
|
|
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
|
use crate::test_utils::test_prelude::{self, NodeGraphLayer};
|
|
use graph_craft::ProtoNodeIdentifier;
|
|
use graph_craft::document::NodeNetwork;
|
|
use graphene_std::NodeInputDecleration;
|
|
use test_prelude::LayerNodeIdentifier;
|
|
|
|
/// Stores all of the monitor nodes that have been attached to a graph
|
|
#[derive(Default)]
|
|
pub struct Instrumented {
|
|
protonodes_by_name: HashMap<ProtoNodeIdentifier, Vec<Vec<Vec<NodeId>>>>,
|
|
protonodes_by_path: HashMap<Vec<NodeId>, Vec<Vec<NodeId>>>,
|
|
}
|
|
|
|
impl Instrumented {
|
|
/// Adds montior nodes to the network
|
|
fn add(&mut self, network: &mut NodeNetwork, path: &mut Vec<NodeId>) {
|
|
// Required to do seperately to satiate the borrow checker.
|
|
let mut monitor_nodes = Vec::new();
|
|
for (id, node) in network.nodes.iter_mut() {
|
|
// Recursively instrument
|
|
if let DocumentNodeImplementation::Network(nested) = &mut node.implementation {
|
|
path.push(*id);
|
|
self.add(nested, path);
|
|
path.pop();
|
|
}
|
|
let mut monitor_node_ids = Vec::with_capacity(node.inputs.len());
|
|
for input in &mut node.inputs {
|
|
let node_id = NodeId::new();
|
|
let old_input = std::mem::replace(input, NodeInput::node(node_id, 0));
|
|
monitor_nodes.push((old_input, node_id));
|
|
path.push(node_id);
|
|
monitor_node_ids.push(path.clone());
|
|
path.pop();
|
|
}
|
|
if let DocumentNodeImplementation::ProtoNode(identifier) = &mut node.implementation {
|
|
path.push(*id);
|
|
self.protonodes_by_name.entry(identifier.clone()).or_default().push(monitor_node_ids.clone());
|
|
self.protonodes_by_path.insert(path.clone(), monitor_node_ids);
|
|
path.pop();
|
|
}
|
|
}
|
|
for (input, monitor_id) in monitor_nodes {
|
|
let monitor_node = DocumentNode {
|
|
inputs: vec![input],
|
|
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::memo::monitor::IDENTIFIER),
|
|
call_argument: graph_craft::generic!(T),
|
|
skip_deduplication: true,
|
|
..Default::default()
|
|
};
|
|
network.nodes.insert(monitor_id, monitor_node);
|
|
}
|
|
}
|
|
|
|
/// Instrument a graph and return a new [Instrumented] state.
|
|
pub fn new(network: &mut NodeNetwork) -> Self {
|
|
let mut instrumented = Self::default();
|
|
instrumented.add(network, &mut Vec::new());
|
|
instrumented
|
|
}
|
|
|
|
fn downcast<Input: NodeInputDecleration>(dynamic: Arc<dyn std::any::Any + Send + Sync>) -> Option<Input::Result>
|
|
where
|
|
Input::Result: Send + Sync + Clone + 'static,
|
|
{
|
|
let element = dynamic.downcast_ref::<Input::Result>().cloned();
|
|
if element.is_none() {
|
|
warn!("cannot downcast type for introspection");
|
|
}
|
|
element
|
|
}
|
|
|
|
/// Grab all of the values of a LEVELED input, which introspects as its
|
|
/// whole legacy list rather than as one element. `T` is the introspected
|
|
/// element type, which differs from the declared one where a conversion
|
|
/// sits downstream of the monitor.
|
|
pub fn grab_all_input_level<'a, Input: NodeInputDecleration + 'a, T: Send + Sync + Clone + 'static>(&'a self, runtime: &'a NodeRuntime) -> impl Iterator<Item = List<T>> + 'a {
|
|
self.protonodes_by_name
|
|
.get(&Input::identifier())
|
|
.map_or([].as_slice(), |x| x.as_slice())
|
|
.iter()
|
|
.filter_map(|inputs| inputs.get(Input::INDEX))
|
|
.filter_map(|input_monitor_node| runtime.executor.introspect(input_monitor_node).ok())
|
|
.filter_map(|dynamic| dynamic.downcast_ref::<List<T>>().cloned())
|
|
}
|
|
|
|
pub fn grab_protonode_input<Input: NodeInputDecleration>(&self, path: &Vec<NodeId>, runtime: &NodeRuntime) -> Option<Input::Result>
|
|
where
|
|
Input::Result: Send + Sync + Clone + 'static,
|
|
{
|
|
let input_monitor_node = self.protonodes_by_path.get(path)?.get(Input::INDEX)?;
|
|
|
|
let dynamic = runtime.executor.introspect(input_monitor_node).ok()?;
|
|
|
|
Self::downcast::<Input>(dynamic)
|
|
}
|
|
|
|
pub fn grab_input_from_layer<Input: NodeInputDecleration>(&self, layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface, runtime: &NodeRuntime) -> Option<Input::Result>
|
|
where
|
|
Input::Result: Send + Sync + Clone + 'static,
|
|
{
|
|
let node_graph_layer = NodeGraphLayer::new(layer, network_interface);
|
|
let node = node_graph_layer.upstream_node_id_from_protonode(Input::identifier())?;
|
|
self.grab_protonode_input::<Input>(&vec![node], runtime)
|
|
}
|
|
}
|
|
}
|