mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Migrate legacy gradients via an isolated measurement pre-pass (#4268)
* Migrate legacy gradients via an isolated measurement pre-pass Convert legacy bounding-box-relative Fill::Gradient values to absolute space on document load by measuring each fill's evaluated geometry, rather than walking each layer's primary flow. The pre-pass scans the whole root network so fills on secondary branches (e.g. a Copy to Points instance) and fills in hidden, disabled, or orphaned branches are all found, redirecting the document export to each fill in turn to force its branch to evaluate and reading its geometry back from the inspect result. With every legacy gradient converted before any render, the legacy bounding-box render path is removed: the ATTR_GRADIENT_LEGACY marker, the renderer's legacy brush branch, and fill_to_graphic_list's bbox bake all go away. Gradients nested inside subgraph node networks are out of scope and warned about. * Address review: harden gradient migration against stalls and document switches - Advance the queue on dispatch early returns (missing export or channel send error) so a failure can't leave the migration permanently stuck with the document unable to render. - Tag the migration with its DocumentId: cancel-and-restart when a different document's migration is requested, and only apply a measurement to the active document's in-progress migration, so switching documents mid-migration can't stall or cross-apply. * Remove nested network non-migration warning * Cleanup * Also add migrations for Fill node backup colors * Apply migrations to the demo art
This commit is contained in:
2
demo-artwork/changing-seasons.graphite
generated
2
demo-artwork/changing-seasons.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/isometric-fountain.graphite
generated
2
demo-artwork/isometric-fountain.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/painted-dreams.graphite
generated
2
demo-artwork/painted-dreams.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/parametric-dunescape.graphite
generated
2
demo-artwork/parametric-dunescape.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/procedural-string-lights.graphite
generated
2
demo-artwork/procedural-string-lights.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/red-dress.graphite
generated
2
demo-artwork/red-dress.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/valley-of-spires.graphite
generated
2
demo-artwork/valley-of-spires.graphite
generated
File diff suppressed because one or more lines are too long
@@ -225,8 +225,6 @@ pub enum DocumentMessage {
|
||||
UpdateClickTargets {
|
||||
click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>>,
|
||||
},
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
MigrateLegacyGradients,
|
||||
UpdateOutlines {
|
||||
outlines: HashMap<NodeId, Vec<Arc<ClickTarget>>>,
|
||||
},
|
||||
|
||||
@@ -1372,38 +1372,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
.collect();
|
||||
self.network_interface.update_click_targets(layer_click_targets);
|
||||
}
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
DocumentMessage::MigrateLegacyGradients => {
|
||||
if self.pending_gradient_migration {
|
||||
self.pending_gradient_migration = false;
|
||||
|
||||
// Read each layer's legacy gradient and compute its absolute form from the now-available local bounds
|
||||
let layers: Vec<_> = self.metadata().all_layers().collect();
|
||||
let conversions: Vec<(NodeId, Fill)> = layers
|
||||
.into_iter()
|
||||
.filter_map(|layer| {
|
||||
let gradient = graph_modification_utils::get_gradient(layer, &self.network_interface)?;
|
||||
if gradient.absolute {
|
||||
return None;
|
||||
}
|
||||
let fill_node_id = graph_modification_utils::get_fill_node_id(layer, &self.network_interface)?;
|
||||
let bounds = self.metadata().nonzero_bounding_box(layer);
|
||||
let bounding_box = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
let layer_transform = self.metadata().upstream_transform(layer.to_node());
|
||||
Some((fill_node_id, Fill::Gradient(gradient.to_absolute(bounding_box, layer_transform))))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let converted_any = !conversions.is_empty();
|
||||
for (fill_node_id, fill) in conversions {
|
||||
self.network_interface
|
||||
.set_input(&InputConnector::node(fill_node_id, 1), NodeInput::value(TaggedValue::Fill(fill), false), &[]);
|
||||
}
|
||||
if converted_any {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
}
|
||||
DocumentMessage::UpdateOutlines { outlines } => {
|
||||
let layer_outlines = outlines
|
||||
.into_iter()
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector};
|
||||
use crate::messages::prelude::DocumentMessageHandler;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use glam::{DVec2, IVec2};
|
||||
use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId};
|
||||
use graph_craft::descriptor;
|
||||
@@ -1121,8 +1122,8 @@ pub fn document_migration_replace_resources_referenced_by_hash(document_serializ
|
||||
pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) {
|
||||
document.network_interface.migrate_path_modify_node();
|
||||
|
||||
// Legacy `Fill::Gradient`s are converted to absolute by the deferred migration pass once the first graph run yields geometry bounds
|
||||
document.pending_gradient_migration = true;
|
||||
// Legacy `Fill::Gradient`s are converted to absolute by the deferred migration pre-pass that measures each fill's geometry
|
||||
document.pending_gradient_migration = !graph_modification_utils::legacy_gradient_fill_nodes(&document.network_interface).is_empty();
|
||||
|
||||
let network = document.network_interface.document_network().clone();
|
||||
|
||||
|
||||
@@ -1621,6 +1621,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
// Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize)
|
||||
let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2();
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// A freshly-opened document with legacy gradients runs a one-time measurement pre-pass instead of rendering, until every gradient is converted to absolute space
|
||||
if document.pending_gradient_migration {
|
||||
self.executor.drive_gradient_migration(document, document_id, physical_resolution, scale, responses);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Remove this when we do the SVG rendering with a separate library on desktop, thus avoiding a need for the hole punch.
|
||||
// TODO: See #3796. There is a second instance of this todo comment and code block (be sure to remove both).
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
@@ -1999,7 +2006,7 @@ impl PortfolioMessageHandler {
|
||||
return Err("No active document".to_string());
|
||||
};
|
||||
|
||||
let result = self.executor.poll_node_graph_evaluation(active_document, responses);
|
||||
let result = self.executor.poll_node_graph_evaluation(active_document, document_id, responses);
|
||||
if result.is_err() {
|
||||
let error = r#"
|
||||
<rect x="50%" y="50%" width="460" height="100" transform="translate(-230 -50)" rx="4" fill="var(--color-warning-yellow)" />
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, NodeTemplate};
|
||||
use crate::messages::prelude::*;
|
||||
use glam::DVec2;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graph_craft::{ProtoNodeIdentifier, concrete};
|
||||
@@ -319,6 +319,60 @@ pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
|
||||
Some(gradient.clone())
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// The legacy bounding-box-relative gradient (`absolute == false`) in a "Fill" node's active `fill` input, if any.
|
||||
fn legacy_active_gradient_in_fill_node(fill_node_id: NodeId, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
|
||||
let node = network_interface.document_network().nodes.get(&fill_node_id)?;
|
||||
let TaggedValue::Fill(Fill::Gradient(gradient)) = node.inputs.get(graphene_std::vector::fill::FillInput::<Fill>::INDEX)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
(!gradient.absolute).then(|| gradient.clone())
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// The legacy bounding-box-relative gradient (`absolute == false`) stashed in a "Fill" node's `_backup_gradient` input, if any.
|
||||
/// The backup is inert until the fill is toggled back to a gradient, at which point it becomes the active fill, so it needs converting too.
|
||||
fn legacy_backup_gradient_in_fill_node(fill_node_id: NodeId, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
|
||||
let node = network_interface.document_network().nodes.get(&fill_node_id)?;
|
||||
let TaggedValue::FillGradient(gradient) = node.inputs.get(graphene_std::vector::fill::BackupGradientInput::INDEX)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
(!gradient.absolute).then(|| gradient.clone())
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// Convert a "Fill" node's legacy gradients (the active `fill` and/or the stashed `_backup_gradient`) to absolute space using
|
||||
/// the geometry's measured bounding box, writing each back in place. The active fill is written as a `Fill`, the backup as a bare `FillGradient`.
|
||||
pub fn migrate_fill_node_gradients_to_absolute(fill_node_id: NodeId, network_interface: &mut NodeNetworkInterface, bounding_box: DAffine2, layer_transform: DAffine2) {
|
||||
if let Some(gradient) = legacy_active_gradient_in_fill_node(fill_node_id, network_interface) {
|
||||
let absolute = gradient.to_absolute(bounding_box, layer_transform);
|
||||
let input = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<Fill>::INDEX);
|
||||
network_interface.set_input(&input, NodeInput::value(TaggedValue::Fill(Fill::Gradient(absolute)), false), &[]);
|
||||
}
|
||||
if let Some(gradient) = legacy_backup_gradient_in_fill_node(fill_node_id, network_interface) {
|
||||
let absolute = gradient.to_absolute(bounding_box, layer_transform);
|
||||
let input = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX);
|
||||
network_interface.set_input(&input, NodeInput::value(TaggedValue::FillGradient(absolute), false), &[]);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// Find every root-network "Fill" node holding a legacy bounding-box-relative gradient, either as its active `fill` or as its `_backup_gradient`.
|
||||
///
|
||||
/// Scans the document network structurally instead of walking each layer's primary flow, so it also catches fills on
|
||||
/// secondary inputs and in hidden, disabled, or orphaned branches. Fills nested inside subgraph node networks are skipped.
|
||||
pub fn legacy_gradient_fill_nodes(network_interface: &NodeNetworkInterface) -> Vec<NodeId> {
|
||||
let fill_identifier = DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER);
|
||||
network_interface
|
||||
.document_network()
|
||||
.nodes
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&fill_identifier))
|
||||
.filter(|&node_id| legacy_active_gradient_in_fill_node(node_id, network_interface).is_some() || legacy_backup_gradient_in_fill_node(node_id, network_interface).is_some())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get the gradient stops of a layer, if any.
|
||||
pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientStops> {
|
||||
let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?;
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
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};
|
||||
use graph_craft::proto::GraphErrors;
|
||||
use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig, TimingInformation};
|
||||
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::memo::IORecord;
|
||||
use graphene_std::raster::{CPU, Raster};
|
||||
use graphene_std::renderer::RenderMetadata;
|
||||
use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box};
|
||||
use graphene_std::transform::Footprint;
|
||||
use graphene_std::vector::Vector;
|
||||
use graphene_std::{ATTR_TRANSFORM, Context, Graphic};
|
||||
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
mod runtime_io;
|
||||
pub use runtime_io::NodeRuntimeIO;
|
||||
@@ -58,12 +65,30 @@ pub struct NodeGraphExecutor {
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
#[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 for the given "Fill" node, whose evaluated geometry
|
||||
/// is read back from the inspect result to size the gradient; such runs never touch the visible artwork.
|
||||
measure_fill: Option<NodeId>,
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// State for the deferred legacy-gradient migration: a queue of root-network "Fill" nodes still holding bounding-box-relative
|
||||
/// gradients, measured one at a time by redirecting the document export to each so even hidden/orphaned branches evaluate.
|
||||
#[derive(Debug, Clone)]
|
||||
struct GradientMigration {
|
||||
document_id: DocumentId,
|
||||
remaining: VecDeque<NodeId>,
|
||||
resolution: UVec2,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl NodeGraphExecutor {
|
||||
@@ -80,6 +105,7 @@ impl NodeGraphExecutor {
|
||||
node_graph_hash: 0,
|
||||
current_execution_id: 0,
|
||||
previous_node_to_inspect: Vec::new(),
|
||||
gradient_migration: None,
|
||||
};
|
||||
(node_runtime, node_executor)
|
||||
}
|
||||
@@ -168,7 +194,14 @@ impl NodeGraphExecutor {
|
||||
// Execute the node graph
|
||||
let execution_id = self.queue_execution(render_config);
|
||||
|
||||
self.futures.push_back((execution_id, ExecutionContext { export_config: None, document_id }));
|
||||
self.futures.push_back((
|
||||
execution_id,
|
||||
ExecutionContext {
|
||||
export_config: None,
|
||||
document_id,
|
||||
measure_fill: None,
|
||||
},
|
||||
));
|
||||
|
||||
Ok(DeferMessage::SetGraphSubmissionIndex { execution_id }.into())
|
||||
}
|
||||
@@ -232,7 +265,14 @@ impl NodeGraphExecutor {
|
||||
// Execute the node graph
|
||||
let execution_id = self.queue_execution(render_config);
|
||||
|
||||
self.futures.push_back((execution_id, ExecutionContext { export_config: None, document_id }));
|
||||
self.futures.push_back((
|
||||
execution_id,
|
||||
ExecutionContext {
|
||||
export_config: None,
|
||||
document_id,
|
||||
measure_fill: None,
|
||||
},
|
||||
));
|
||||
|
||||
Ok(DeferMessage::SetGraphSubmissionIndex { execution_id }.into())
|
||||
}
|
||||
@@ -294,13 +334,14 @@ impl NodeGraphExecutor {
|
||||
ExecutionContext {
|
||||
export_config: Some(export_config),
|
||||
document_id,
|
||||
measure_fill: None,
|
||||
},
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn poll_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
||||
pub fn poll_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
||||
let results = self.runtime_io.receive().collect::<Vec<_>>();
|
||||
for response in results {
|
||||
match response {
|
||||
@@ -313,6 +354,29 @@ impl NodeGraphExecutor {
|
||||
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 Some((queued_execution_id, execution_context)) = self.futures.pop_front() else {
|
||||
panic!("InvalidGenerationId")
|
||||
};
|
||||
assert_eq!(queued_execution_id, execution_id, "Missmatch in execution id");
|
||||
|
||||
// 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(fill_node_id) = 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, fill_node_id, result.ok().and(inspect_result), responses);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
let node_graph_output = match result {
|
||||
@@ -329,19 +393,6 @@ impl NodeGraphExecutor {
|
||||
responses.extend(existing_responses.into_iter().map(Into::into));
|
||||
document.network_interface.update_vector_modify(vector_modify);
|
||||
|
||||
while let Some(&(fid, _)) = self.futures.front() {
|
||||
if fid < execution_id {
|
||||
self.futures.pop_front();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let Some((fid, execution_context)) = self.futures.pop_front() else {
|
||||
panic!("InvalidGenerationId")
|
||||
};
|
||||
assert_eq!(fid, execution_id, "Missmatch in execution id");
|
||||
|
||||
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)?;
|
||||
@@ -362,6 +413,16 @@ impl NodeGraphExecutor {
|
||||
}
|
||||
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
|
||||
@@ -400,6 +461,146 @@ impl NodeGraphExecutor {
|
||||
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.
|
||||
pub(crate) fn drive_gradient_migration(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, resolution: UVec2, scale: f64, responses: &mut VecDeque<Message>) {
|
||||
match &self.gradient_migration {
|
||||
// Already running for this document
|
||||
Some(migration) if migration.document_id == document_id => return,
|
||||
// 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 => {}
|
||||
}
|
||||
|
||||
let remaining: VecDeque<NodeId> = graph_modification_utils::legacy_gradient_fill_nodes(&document.network_interface).into_iter().collect();
|
||||
let Some(&first_fill) = remaining.front() else {
|
||||
document.pending_gradient_migration = false;
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
return;
|
||||
};
|
||||
|
||||
self.gradient_migration = Some(GradientMigration {
|
||||
document_id,
|
||||
remaining,
|
||||
resolution,
|
||||
scale,
|
||||
});
|
||||
self.dispatch_gradient_measurement(document, document_id, first_fill, resolution, scale, responses);
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// Run the graph with its export redirected to `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.
|
||||
fn dispatch_gradient_measurement(
|
||||
&mut self,
|
||||
document: &mut DocumentMessageHandler,
|
||||
document_id: DocumentId,
|
||||
fill_node_id: NodeId,
|
||||
resolution: UVec2,
|
||||
scale: f64,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let mut network = document.network_interface.document_network().clone();
|
||||
|
||||
// On this throwaway clone, un-hide just the Fill 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.
|
||||
if let Some(node) = network.nodes.get_mut(&fill_node_id) {
|
||||
node.visible = true;
|
||||
}
|
||||
|
||||
let Some(export) = network.exports.first_mut() else {
|
||||
// No export to redirect through, so skip this Fill rather than leaving the migration stuck
|
||||
log::warn!("Gradient migration: document network has no export to redirect");
|
||||
self.advance_gradient_migration(document, document_id, responses);
|
||||
return;
|
||||
};
|
||||
*export = NodeInput::node(fill_node_id, 0);
|
||||
|
||||
let resources = document.resources.registry.clone();
|
||||
if self
|
||||
.runtime_io
|
||||
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate {
|
||||
network,
|
||||
resources,
|
||||
node_to_inspect: vec![fill_node_id],
|
||||
}))
|
||||
.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 = vec![fill_node_id];
|
||||
|
||||
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(fill_node_id),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// Convert the just-measured fill's legacy gradients to absolute space using its evaluated geometry, then advance the queue.
|
||||
fn handle_gradient_measurement(
|
||||
&mut self,
|
||||
document: &mut DocumentMessageHandler,
|
||||
document_id: DocumentId,
|
||||
fill_node_id: NodeId,
|
||||
inspect_result: Option<InspectResult>,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let measured = inspect_result.and_then(|mut result| result.take_data()).and_then(|data| measure_fill_geometry(&data));
|
||||
|
||||
match measured {
|
||||
Some((bounding_box, item_transform)) => graph_modification_utils::migrate_fill_node_gradients_to_absolute(fill_node_id, &mut document.network_interface, bounding_box, item_transform),
|
||||
None => log::warn!("Gradient migration could not measure geometry for fill node {fill_node_id:?}; leaving it in legacy space"),
|
||||
}
|
||||
|
||||
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_fill = self.gradient_migration.as_mut().and_then(|migration| {
|
||||
migration.remaining.pop_front();
|
||||
migration.remaining.front().copied()
|
||||
});
|
||||
|
||||
if let Some(next_fill) = next_fill {
|
||||
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_fill, resolution, scale, responses);
|
||||
} else {
|
||||
self.gradient_migration = None;
|
||||
self.previous_node_to_inspect = Vec::new();
|
||||
self.node_graph_hash = 0;
|
||||
document.pending_gradient_migration = false;
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
|
||||
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:#?}"));
|
||||
@@ -458,11 +659,6 @@ impl NodeGraphExecutor {
|
||||
first_element_source_id,
|
||||
});
|
||||
responses.add(DocumentMessage::UpdateClickTargets { click_targets });
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// Runs after click targets land (this graph run's geometry bounds) so the deferred gradient migration can use them.
|
||||
responses.add(DocumentMessage::MigrateLegacyGradients);
|
||||
|
||||
responses.add(DocumentMessage::UpdateOutlines { outlines });
|
||||
responses.add(DocumentMessage::UpdateTextFrames { text_frames });
|
||||
responses.add(DocumentMessage::UpdateClipTargets { clip_targets });
|
||||
@@ -572,6 +768,41 @@ impl NodeGraphExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 recorded output, trying each context type the runtime may have evaluated it under.
|
||||
fn introspected_output<T: Clone + Send + Sync + 'static>(data: &Arc<dyn Any + Send + Sync>) -> Option<T> {
|
||||
if let Some(io) = data.downcast_ref::<IORecord<(), T>>() {
|
||||
return Some(io.output.clone());
|
||||
}
|
||||
if let Some(io) = data.downcast_ref::<IORecord<Footprint, T>>() {
|
||||
return Some(io.output.clone());
|
||||
}
|
||||
if let Some(io) = data.downcast_ref::<IORecord<Context, T>>() {
|
||||
return Some(io.output.clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// Re-export for usage by tests in other modules
|
||||
#[cfg(test)]
|
||||
pub use test::Instrumented;
|
||||
|
||||
@@ -25,8 +25,8 @@ pub use graphene_hash;
|
||||
pub use graphene_hash::CacheHash;
|
||||
pub use list::{
|
||||
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END,
|
||||
ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_LEGACY, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME,
|
||||
ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE,
|
||||
ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL,
|
||||
ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE,
|
||||
};
|
||||
pub use memo::MemoHash;
|
||||
pub use no_std_types::AsU32;
|
||||
|
||||
@@ -58,10 +58,6 @@ pub const ATTR_CLIP: &str = "clip";
|
||||
pub const ATTR_SPREAD_METHOD: &str = "spread_method";
|
||||
/// Gradient's `GradientType` (`Linear` or `Radial`).
|
||||
pub const ATTR_GRADIENT_TYPE: &str = "gradient_type";
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// `bool` runtime marker (never serialized) flagging a gradient that came from the legacy bounding-box-relative `Fill::Gradient`,
|
||||
/// so the renderer reproduces the pre-#4241 positioning instead of the new absolute path.
|
||||
pub const ATTR_GRADIENT_LEGACY: &str = "gradient_legacy";
|
||||
/// Vector graphics object's filled area paint, of type List<T> where T is any graphic type.
|
||||
pub const ATTR_FILL: &str = "fill";
|
||||
/// Vector graphics object's stroke paint, of type List<T> where T is any graphic type.
|
||||
|
||||
@@ -4,7 +4,7 @@ use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List};
|
||||
use core_types::ops::{FromAnchorPosition, ListConvert};
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use core_types::uuid::NodeId;
|
||||
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_GRADIENT_LEGACY, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
|
||||
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use raster_types::{CPU, GPU, Raster};
|
||||
@@ -193,26 +193,15 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
|
||||
|
||||
/// Converts a `Fill` enum into the `List<Graphic>` representation used as paint storage.
|
||||
/// TODO: Remove once all fill paint sources flow through `List<Graphic>` directly without going through the `Fill` enum.
|
||||
pub fn fill_to_graphic_list(fill: &Fill, bounding_box_transform: DAffine2) -> Option<List<Graphic>> {
|
||||
pub fn fill_to_graphic_list(fill: &Fill) -> Option<List<Graphic>> {
|
||||
match fill {
|
||||
Fill::None => None,
|
||||
Fill::Solid(color) => Some(List::new_from_element((*color).into())),
|
||||
Fill::Gradient(gradient) => {
|
||||
let gradient_transform = gradient.transform * gradient.to_transform();
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// Absolute gradients carry their effective frame into the new pipeline; legacy bounding-box gradients bake the bbox
|
||||
// in and flag the legacy render path until the deferred migration converts them.
|
||||
let (transform, legacy) = if gradient.absolute {
|
||||
(gradient_transform, false)
|
||||
} else {
|
||||
(bounding_box_transform * gradient_transform, true)
|
||||
};
|
||||
let gradient_item = Item::new_from_element(gradient.stops.clone())
|
||||
.with_attribute(ATTR_TRANSFORM, transform)
|
||||
.with_attribute(ATTR_TRANSFORM, gradient.transform * gradient.to_transform())
|
||||
.with_attribute(ATTR_GRADIENT_TYPE, gradient.gradient_type)
|
||||
.with_attribute(ATTR_SPREAD_METHOD, gradient.spread_method)
|
||||
.with_attribute(ATTR_GRADIENT_LEGACY, legacy);
|
||||
.with_attribute(ATTR_SPREAD_METHOD, gradient.spread_method);
|
||||
let gradient_list = List::new_from_item(gradient_item);
|
||||
|
||||
Some(List::new_from_element(Graphic::Gradient(gradient_list)))
|
||||
@@ -257,9 +246,7 @@ pub fn has_paint_at(list: &List<Vector>, index: usize, attribute: &str) -> bool
|
||||
pub fn fill_graphic_list_at(list: &List<Vector>, index: usize) -> Option<Cow<'_, List<Graphic>>> {
|
||||
graphic_list_at(list, index, ATTR_FILL).or_else(|| {
|
||||
let vector = list.element(index)?;
|
||||
let bounds = vector.nonzero_bounding_box();
|
||||
let bounding_box_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
fill_to_graphic_list(vector.style.fill(), bounding_box_transform).map(Cow::Owned)
|
||||
fill_to_graphic_list(vector.style.fill()).map(Cow::Owned)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ use core_types::transform::Footprint;
|
||||
use core_types::uuid::{NodeId, generate_uuid};
|
||||
use core_types::{
|
||||
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_FONT,
|
||||
ATTR_FONT_SIZE, ATTR_GRADIENT_LEGACY, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL,
|
||||
ATTR_SPREAD_METHOD, ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
|
||||
ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD,
|
||||
ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
|
||||
};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
@@ -396,14 +396,12 @@ pub(crate) fn gradient_placement(transform: DAffine2, gradient_type: GradientTyp
|
||||
}
|
||||
}
|
||||
|
||||
fn create_peniko_gradient_brush(gradient_list: &List<GradientStops>, parent_transform: &DAffine2, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> {
|
||||
fn create_peniko_gradient_brush(gradient_list: &List<GradientStops>, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> {
|
||||
let stops = gradient_list.element(0)?;
|
||||
|
||||
let gradient_type: GradientType = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0);
|
||||
let gradient_transform: DAffine2 = gradient_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||
let spread_method: GradientSpreadMethod = gradient_list.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0);
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
let legacy_bounding_box: bool = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_LEGACY, 0);
|
||||
|
||||
let mut peniko_stops = peniko::ColorStops::new();
|
||||
for (position, color, _) in stops.interpolated_samples() {
|
||||
@@ -413,20 +411,8 @@ fn create_peniko_gradient_brush(gradient_list: &List<GradientStops>, parent_tran
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// Legacy bounding-box gradients reproduce the pre-#4241 renderer: literal endpoints in the layer's own space, with the
|
||||
// parent transform applied as the brush so its shear bends the bands. New gradients use the unit gradient placed by the desheared frame.
|
||||
let (start, end, gradient_to_device) = if legacy_bounding_box {
|
||||
let inverse_parent_transform = if transform_is_invertible(*parent_transform) {
|
||||
parent_transform.inverse()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let mod_points = inverse_parent_transform * *multiplied_transform * gradient_transform;
|
||||
(mod_points.transform_point2(DVec2::ZERO), mod_points.transform_point2(DVec2::X), *parent_transform)
|
||||
} else {
|
||||
(DVec2::ZERO, DVec2::X, gradient_placement(multiplied_transform * gradient_transform, gradient_type))
|
||||
};
|
||||
// The unit gradient is placed by the desheared frame so a non-uniform transform produces the intended ellipse
|
||||
let (start, end, gradient_to_device) = (DVec2::ZERO, DVec2::X, gradient_placement(multiplied_transform * gradient_transform, gradient_type));
|
||||
|
||||
let brush = peniko::Brush::Gradient(peniko::Gradient {
|
||||
kind: match gradient_type {
|
||||
@@ -1372,7 +1358,7 @@ impl Render for List<Vector> {
|
||||
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path);
|
||||
}
|
||||
Graphic::Gradient(list) => {
|
||||
let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(list, &parent_transform, &multiplied_transform) else {
|
||||
let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(list, &multiplied_transform) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -1454,7 +1440,7 @@ impl Render for List<Vector> {
|
||||
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, None, &path);
|
||||
}
|
||||
Graphic::Gradient(list) => {
|
||||
let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(list, &parent_transform, &multiplied_transform) else {
|
||||
let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(list, &multiplied_transform) else {
|
||||
continue;
|
||||
};
|
||||
let inverse_element_transform = if transform_is_invertible(element_transform) {
|
||||
|
||||
Reference in New Issue
Block a user