mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Fix corrupted font resource loading in older documents
This commit is contained in:
@@ -299,7 +299,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
graph_operation_message_handler.process_message(message, responses, context);
|
||||
}
|
||||
DocumentMessage::Resource(message) => {
|
||||
let context = ResourceMessageContext { document_id, fonts };
|
||||
let context = ResourceMessageContext { document_id, fonts, resource_storage };
|
||||
self.resources.process_message(message, responses, context);
|
||||
}
|
||||
DocumentMessage::AlignSelectedLayers { axis, aggregate } => {
|
||||
|
||||
@@ -12,6 +12,7 @@ use url::Url;
|
||||
pub struct ResourceMessageContext<'a> {
|
||||
pub document_id: DocumentId,
|
||||
pub fonts: &'a FontsMessageHandler,
|
||||
pub resource_storage: &'a ResourceStorageMessageHandler,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, ExtractField)]
|
||||
@@ -25,7 +26,7 @@ pub struct ResourceMessageHandler {
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<ResourceMessage, ResourceMessageContext<'_>> for ResourceMessageHandler {
|
||||
fn process_message(&mut self, message: ResourceMessage, responses: &mut VecDeque<Message>, context: ResourceMessageContext) {
|
||||
let ResourceMessageContext { document_id, fonts } = context;
|
||||
let ResourceMessageContext { document_id, fonts, resource_storage } = context;
|
||||
|
||||
match message {
|
||||
ResourceMessage::StoreEmbedded { resource_id, data } => {
|
||||
@@ -48,8 +49,16 @@ impl MessageHandler<ResourceMessage, ResourceMessageContext<'_>> for ResourceMes
|
||||
responses.add(ResourceMessage::Resolve { resource_id });
|
||||
}
|
||||
ResourceMessage::ResolveAll => {
|
||||
let unresolved_ids: Vec<ResourceId> = self.registry.unresolved().map(|info| info.id).collect();
|
||||
for id in unresolved_ids {
|
||||
// A resource keeps its hash when storage evicts its data, so only a fetchable source can repair it
|
||||
let refetchable = self
|
||||
.registry
|
||||
.resolved()
|
||||
.filter(|info| info.hash.is_some_and(|hash| !resource_storage.contains(hash)))
|
||||
.filter(|info| info.sources.iter().any(|source| matches!(source, DataSource::Url(_) | DataSource::Font { .. })))
|
||||
.map(|info| info.id);
|
||||
let ids: Vec<ResourceId> = self.registry.unresolved().map(|info| info.id).chain(refetchable).collect();
|
||||
|
||||
for id in ids {
|
||||
if self.pending_resolves.contains(&id) {
|
||||
continue;
|
||||
}
|
||||
@@ -65,7 +74,9 @@ impl MessageHandler<ResourceMessage, ResourceMessageContext<'_>> for ResourceMes
|
||||
log::error!("Resolve for {resource_id}: no registry entry");
|
||||
return;
|
||||
};
|
||||
if info.hash.is_some() {
|
||||
// This hash names the very data that is missing, so it cannot stand in for fetching that data
|
||||
let data_missing = info.hash.is_some_and(|hash| !resource_storage.contains(hash));
|
||||
if info.hash.is_some() && !data_missing {
|
||||
log::warn!("Resource {resource_id} already resolved");
|
||||
return;
|
||||
}
|
||||
@@ -78,7 +89,7 @@ impl MessageHandler<ResourceMessage, ResourceMessageContext<'_>> for ResourceMes
|
||||
.sources
|
||||
.iter()
|
||||
.map(|source| match source {
|
||||
DataSource::Font { family, style } => {
|
||||
DataSource::Font { family, style } if !data_missing => {
|
||||
let font = match style {
|
||||
Some(style) => Font::new(family.clone(), style.clone()),
|
||||
None => Font::new_with_default_style(family.clone()),
|
||||
@@ -214,16 +225,20 @@ impl ResourceMessageHandler {
|
||||
.resolved()
|
||||
.filter(|info| info.sources.contains(&DataSource::Embedded))
|
||||
.filter_map(|info| {
|
||||
if let Some(hash) = info.hash {
|
||||
let resource = resources_load_handle.load(*hash);
|
||||
Some(async move { resource.await.map(|resource| (*hash, resource)) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let (id, hash) = (info.id, *info.hash?);
|
||||
let resource = resources_load_handle.load(hash);
|
||||
Some(async move { (id, hash, resource.await) })
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.embedded = EmbeddedResources::from_iter(futures::future::join_all(embedded).await.into_iter().flatten());
|
||||
let loaded = futures::future::join_all(embedded).await;
|
||||
|
||||
// Saving without these bytes writes a document whose registry claims to carry them
|
||||
for (id, hash, _) in loaded.iter().filter(|(_, _, resource)| resource.is_none()) {
|
||||
log::error!("Resource {id} ({hash}) is marked as embedded but its data is missing from storage, so the saved document will not contain it");
|
||||
}
|
||||
|
||||
self.embedded = EmbeddedResources::from_iter(loaded.into_iter().filter_map(|(_, hash, resource)| resource.map(|resource| (hash, resource))));
|
||||
}
|
||||
|
||||
pub fn collect_garbage(&mut self, used: &[ResourceId]) {
|
||||
@@ -297,3 +312,65 @@ impl<'de> serde::Deserialize<'de> for ResourceMessageHandler {
|
||||
deserializer.deserialize_map(EmbeddedResourcesVisitor { human_readable })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use graph_craft::application_io::resource::ResourceStorage;
|
||||
|
||||
/// Storage can lose a resource's data while the document keeps the hash naming it, which leaves the graph
|
||||
/// pointing at bytes that are gone. Only sources that can be fetched again are worth re-resolving.
|
||||
#[test]
|
||||
fn resolve_all_refetches_resources_whose_data_is_missing() {
|
||||
let mut handler = ResourceMessageHandler::default();
|
||||
let storage = ResourceStorageMessageHandler::default();
|
||||
|
||||
// Present: its bytes are in storage, so it is already usable
|
||||
let present = ResourceId::new();
|
||||
let present_hash = storage.resources_mut().store(b"stored font bytes");
|
||||
handler.registry.resolve(&present, present_hash);
|
||||
handler.registry.push_source_back(&present, DataSource::Embedded);
|
||||
handler.registry.push_source_back(
|
||||
&present,
|
||||
DataSource::Font {
|
||||
family: "Lato".into(),
|
||||
style: Some("Regular (400)".into()),
|
||||
},
|
||||
);
|
||||
|
||||
// Recoverable: its bytes are gone, but the font it came from can be downloaded again
|
||||
let recoverable = ResourceId::new();
|
||||
handler.registry.resolve(&recoverable, ResourceHash::from(b"evicted font bytes".as_slice()));
|
||||
handler.registry.push_source_back(&recoverable, DataSource::Embedded);
|
||||
handler.registry.push_source_back(
|
||||
&recoverable,
|
||||
DataSource::Font {
|
||||
family: "Lato".into(),
|
||||
style: Some("Black (900)".into()),
|
||||
},
|
||||
);
|
||||
|
||||
// Unrecoverable: its bytes are gone and nothing records where to fetch them from
|
||||
let unrecoverable = ResourceId::new();
|
||||
handler.registry.resolve(&unrecoverable, ResourceHash::from(b"evicted image bytes".as_slice()));
|
||||
handler.registry.push_source_back(&unrecoverable, DataSource::Embedded);
|
||||
|
||||
let mut responses = VecDeque::new();
|
||||
let fonts = FontsMessageHandler::default();
|
||||
handler.process_message(
|
||||
ResourceMessage::ResolveAll,
|
||||
&mut responses,
|
||||
ResourceMessageContext {
|
||||
document_id: DocumentId(0),
|
||||
fonts: &fonts,
|
||||
resource_storage: &storage,
|
||||
},
|
||||
);
|
||||
|
||||
let resolve_requested = |id: ResourceId| responses.contains(&Message::from(ResourceMessage::Resolve { resource_id: id }));
|
||||
|
||||
assert!(resolve_requested(recoverable), "a missing resource with a font source should be fetched again");
|
||||
assert!(!resolve_requested(present), "a resource whose data is in storage should be left alone");
|
||||
assert!(!resolve_requested(unrecoverable), "a missing resource with no fetchable source has nowhere to fetch from");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1979,79 +1979,38 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
|
||||
if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")) && inputs_count == 8 {
|
||||
// Every Text node era before alignment only appended inputs, so each is a prefix of the 11-input layout.
|
||||
// Alignment (#2920) is the exception: it landed at index 9 and pushed Per-Glyph Instances out to 10.
|
||||
if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")) && (4..=10).contains(&inputs_count) {
|
||||
let mut template: NodeTemplate = legacy_text_node_template()?;
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut template);
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?;
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 3), old_inputs[3].clone(), network_path);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node_at_index(*node_id, 4),
|
||||
if inputs_count == 6 {
|
||||
old_inputs[4].clone()
|
||||
} else {
|
||||
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().line_height_ratio), false)
|
||||
},
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node_at_index(*node_id, 5),
|
||||
if inputs_count == 6 {
|
||||
old_inputs[5].clone()
|
||||
} else {
|
||||
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().letter_spacing), false)
|
||||
},
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node_at_index(*node_id, 6),
|
||||
if inputs_count >= 7 {
|
||||
old_inputs[6].clone()
|
||||
} else {
|
||||
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().max_width.unwrap_or_default()), false)
|
||||
},
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node_at_index(*node_id, 7),
|
||||
if inputs_count >= 8 {
|
||||
old_inputs[7].clone()
|
||||
} else {
|
||||
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().max_width.unwrap_or_default()), false)
|
||||
},
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node_at_index(*node_id, 8),
|
||||
if inputs_count >= 9 {
|
||||
old_inputs[8].clone()
|
||||
} else {
|
||||
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().letter_tilt), false)
|
||||
},
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node_at_index(*node_id, 9),
|
||||
if inputs_count >= 10 {
|
||||
old_inputs[9].clone()
|
||||
} else {
|
||||
NodeInput::value(TaggedValue::TextAlign(TextAlign::default()), false)
|
||||
},
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node_at_index(*node_id, 10),
|
||||
if inputs_count >= 11 {
|
||||
old_inputs[10].clone()
|
||||
} else {
|
||||
NodeInput::value(TaggedValue::Bool(false), false)
|
||||
},
|
||||
network_path,
|
||||
);
|
||||
// Line height and character spacing were hardcoded to 1 in the era before they became inputs
|
||||
let hardcoded_to_one = || NodeInput::value(TaggedValue::F64(1.), false);
|
||||
// Zero is how an absent `Option<f64>` maximum reads to the split below
|
||||
let unset_maximum = || NodeInput::value(TaggedValue::F64(0.), false);
|
||||
|
||||
let upgraded_inputs = [
|
||||
old_inputs[0].clone(),
|
||||
old_inputs[1].clone(),
|
||||
old_inputs[2].clone(),
|
||||
old_inputs[3].clone(),
|
||||
old_inputs.get(4).cloned().unwrap_or_else(hardcoded_to_one),
|
||||
old_inputs.get(5).cloned().unwrap_or_else(hardcoded_to_one),
|
||||
old_inputs.get(6).cloned().unwrap_or_else(unset_maximum),
|
||||
old_inputs.get(7).cloned().unwrap_or_else(unset_maximum),
|
||||
old_inputs
|
||||
.get(8)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| NodeInput::value(TaggedValue::F64(TypesettingConfig::default().letter_tilt), false)),
|
||||
NodeInput::value(TaggedValue::TextAlign(TextAlign::default()), false),
|
||||
old_inputs.get(9).cloned().unwrap_or_else(|| NodeInput::value(TaggedValue::Bool(false), false)),
|
||||
];
|
||||
for (index, input) in upgraded_inputs.into_iter().enumerate() {
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input, network_path);
|
||||
}
|
||||
|
||||
inputs_count = 11
|
||||
}
|
||||
|
||||
@@ -2068,31 +2027,26 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, i), old_inputs[i].clone(), network_path);
|
||||
}
|
||||
|
||||
// The old `Option<f64>` maximum becomes a bool plus a value, with zero standing in for the absent option.
|
||||
// A wired maximum has no value to read, so it keeps its connection and counts as present.
|
||||
let split_maximum = |input: &NodeInput| match input.as_value() {
|
||||
Some(&TaggedValue::F64(maximum)) => (maximum != 0., NodeInput::value(TaggedValue::F64(if maximum == 0. { 100. } else { maximum }), false)),
|
||||
_ => (true, input.clone()),
|
||||
};
|
||||
|
||||
// Max Width
|
||||
let Some(&TaggedValue::F64(old_max_width)) = old_inputs[6].as_value() else { return None };
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node_at_index(*node_id, 6),
|
||||
NodeInput::value(TaggedValue::Bool(old_max_width != 0.), false),
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node_at_index(*node_id, 7),
|
||||
NodeInput::value(TaggedValue::F64(if old_max_width == 0. { 100. } else { old_max_width }), false),
|
||||
network_path,
|
||||
);
|
||||
let (has_max_width, max_width) = split_maximum(&old_inputs[6]);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node_at_index(*node_id, 6), NodeInput::value(TaggedValue::Bool(has_max_width), false), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 7), max_width, network_path);
|
||||
|
||||
// Max Height
|
||||
let Some(&TaggedValue::F64(old_max_height)) = old_inputs[7].as_value() else { return None };
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node_at_index(*node_id, 8),
|
||||
NodeInput::value(TaggedValue::Bool(old_max_height != 0.), false),
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node_at_index(*node_id, 9),
|
||||
NodeInput::value(TaggedValue::F64(if old_max_height == 0. { 100. } else { old_max_height }), false),
|
||||
network_path,
|
||||
);
|
||||
let (has_max_height, max_height) = split_maximum(&old_inputs[7]);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node_at_index(*node_id, 8), NodeInput::value(TaggedValue::Bool(has_max_height), false), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 9), max_height, network_path);
|
||||
|
||||
// Copy over old inputs
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
@@ -2441,6 +2395,39 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
document.network_interface.add_import(TaggedValue::U32(0), false, 1, "Loop Level", "TODO", &node_path);
|
||||
}
|
||||
|
||||
// Drop the placeholder primary input the "Read Vector" node used to carry, since it reads its value from the context
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::context::read_vector::IDENTIFIER) && inputs_count > 0 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||
document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||
}
|
||||
|
||||
// The "Dot Product" node gained a "Normalize" toggle, which older nodes predate by always taking the raw dot product
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::dot_product::IDENTIFIER) && inputs_count == 2 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||
|
||||
for (index, input) in old_inputs.iter().take(2).enumerate() {
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input.clone(), network_path);
|
||||
}
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node_at_index(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path);
|
||||
}
|
||||
|
||||
// The "Query JSON" node succeeded "JSON Get", whose object lookups always returned their strings unquoted
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::text_nodes::json::query_json::IDENTIFIER) && inputs_count == 2 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||
|
||||
for (index, input) in old_inputs.iter().take(2).enumerate() {
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input.clone(), network_path);
|
||||
}
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node_at_index(*node_id, 2), NodeInput::value(TaggedValue::Bool(true), false), network_path);
|
||||
}
|
||||
|
||||
// Upgrade the "Animation" node to add the "Rate" input
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::animation::animation_time::IDENTIFIER) && inputs_count < 2 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
@@ -2883,19 +2870,39 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
/// definition by its old reference name, swaps it to a still-supported implementation, and preserves the user's inputs.
|
||||
/// After this runs, the node's reference resolves cleanly so the rest of `migrate_node` proceeds normally.
|
||||
fn migrate_removed_catalog_definitions(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document: &mut DocumentMessageHandler) -> Option<()> {
|
||||
// Collapse the legacy "Sample Polyline" wrapper network into the standalone `sample_polyline` proto node.
|
||||
// The proto node now computes per-bezpath segment lengths inline, so the wrapper's separate `subpath_segment_lengths`
|
||||
// and `Memoize` nodes are no longer needed. The 7 user-facing inputs are positionally identical between the
|
||||
// old wrapper and the new proto node.
|
||||
if let Some(DefinitionIdentifier::Network(name)) = document.network_interface.reference(node_id, network_path)
|
||||
&& name == "Sample Polyline"
|
||||
&& node.inputs.len() == 7
|
||||
// Collapse the legacy "Sample Points" and "Sample Polyline" wrapper networks into the standalone `sample_polyline`
|
||||
// proto node, which now computes per-bezpath segment lengths inline instead of through the wrapper's helper nodes.
|
||||
// The oldest documents lose their stored reference on load, so the wrapper is recognized by the nodes it encloses.
|
||||
let wrapper_inputs = match &node.implementation {
|
||||
DocumentNodeImplementation::Network(inner) => {
|
||||
let helpers = [graphene_std::ops::passthrough::IDENTIFIER, graphene_std::memo::memoize::IDENTIFIER];
|
||||
let mut sample_nodes = 0;
|
||||
let only_helpers = inner.nodes.values().all(|inner_node| match &inner_node.implementation {
|
||||
DocumentNodeImplementation::ProtoNode(identifier) if *identifier == graphene_std::vector::sample_polyline::IDENTIFIER => {
|
||||
sample_nodes += 1;
|
||||
true
|
||||
}
|
||||
DocumentNodeImplementation::ProtoNode(identifier) => helpers.contains(identifier),
|
||||
_ => false,
|
||||
});
|
||||
(only_helpers && sample_nodes == 1).then_some(node.inputs.len())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(wrapper_inputs) = wrapper_inputs
|
||||
&& (wrapper_inputs == 5 || wrapper_inputs == 7)
|
||||
{
|
||||
let mut node_template = resolve_proto_node_type(graphene_std::vector::sample_polyline::IDENTIFIER)?.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||
for (index, input) in old_inputs.iter().take(7).enumerate() {
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input.clone(), network_path);
|
||||
|
||||
// The 5-input era predates the separation/quantity choice, so its lone spacing distance becomes the separation
|
||||
let upgraded_inputs: Vec<(usize, NodeInput)> = match wrapper_inputs {
|
||||
5 => [0, 2, 4, 5, 6].into_iter().zip(old_inputs.iter().cloned()).collect(),
|
||||
_ => old_inputs.iter().take(7).cloned().enumerate().collect(),
|
||||
};
|
||||
for (index, input) in upgraded_inputs {
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input, network_path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2984,6 +2991,93 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// The Text node produced geometry until it became a string source, so every shape it ever had must reach the
|
||||
// current one and gain the converter that turns its string back into geometry
|
||||
#[test]
|
||||
fn every_legacy_text_shape_gains_its_geometry_converter() {
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeTemplate;
|
||||
use graphene_std::NodeParameter;
|
||||
use graphene_std::text::Font;
|
||||
|
||||
// Each era only appended to the one before it, so the shorter shapes are prefixes of this longest pre-alignment one
|
||||
let legacy_inputs = [
|
||||
NodeInput::scope("editor-api"),
|
||||
NodeInput::value(TaggedValue::String("Lorem".into()), false),
|
||||
NodeInput::value(TaggedValue::Font(Font::new("Lato".to_string(), "Regular (400)".to_string())), false),
|
||||
NodeInput::value(TaggedValue::F64(48.), false),
|
||||
NodeInput::value(TaggedValue::F64(1.5), false),
|
||||
NodeInput::value(TaggedValue::F64(2.), false),
|
||||
NodeInput::value(TaggedValue::F64(0.), false),
|
||||
NodeInput::value(TaggedValue::F64(0.), false),
|
||||
NodeInput::value(TaggedValue::F64(10.), false),
|
||||
NodeInput::value(TaggedValue::Bool(false), false),
|
||||
];
|
||||
|
||||
for shape in [4, 6, 8, 9, 10] {
|
||||
let (text_id, consumer_id) = (NodeId(1), NodeId(2));
|
||||
let mut document = DocumentMessageHandler::default();
|
||||
document.network_interface.insert_node(
|
||||
text_id,
|
||||
NodeTemplate {
|
||||
implementation: NodeTemplateImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")),
|
||||
inputs: legacy_inputs[..shape].to_vec(),
|
||||
..Default::default()
|
||||
},
|
||||
&[],
|
||||
);
|
||||
document.network_interface.insert_node(
|
||||
consumer_id,
|
||||
NodeTemplate {
|
||||
inputs: vec![NodeInput::value(TaggedValue::None, false)],
|
||||
..Default::default()
|
||||
},
|
||||
&[],
|
||||
);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(consumer_id, 0), NodeInput::node(text_id, 0), &[]);
|
||||
|
||||
document_migration_upgrades(&mut document, false);
|
||||
|
||||
let network = document.network_interface.document_network();
|
||||
let text_node = network.nodes.get(&text_id).expect("the upgraded text node should keep its ID");
|
||||
assert_eq!(text_node.inputs.len(), 12, "a {shape}-input text node should reach the current shape");
|
||||
|
||||
// The converter is a new node, so it is found by identity rather than by ID
|
||||
let converter = network
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|(_, node)| matches!(&node.implementation, DocumentNodeImplementation::ProtoNode(identifier) if *identifier == graphene_std::text::text_to_vector::IDENTIFIER))
|
||||
.map(|(converter_id, _)| *converter_id)
|
||||
.unwrap_or_else(|| panic!("a {shape}-input text node should gain a string converter"));
|
||||
assert_eq!(
|
||||
network.nodes[&consumer_id].inputs.first(),
|
||||
Some(&NodeInput::node(converter, 0)),
|
||||
"the converter should be spliced onto the wire leaving a {shape}-input text node"
|
||||
);
|
||||
|
||||
let input_value = |index: usize| text_node.inputs.get(index).and_then(|input| input.as_value()).cloned();
|
||||
assert_eq!(input_value(graphene_std::text::text::SizeInput::INDEX), Some(TaggedValue::F64(48.)), "shape {shape} lost its size");
|
||||
if shape >= 6 {
|
||||
assert_eq!(
|
||||
input_value(graphene_std::text::text::LineHeightInput::INDEX),
|
||||
Some(TaggedValue::F64(1.5)),
|
||||
"shape {shape} lost its line height"
|
||||
);
|
||||
assert_eq!(
|
||||
input_value(graphene_std::text::text::LetterSpacingInput::INDEX),
|
||||
Some(TaggedValue::F64(2.)),
|
||||
"shape {shape} lost its letter spacing"
|
||||
);
|
||||
}
|
||||
if shape >= 9 {
|
||||
assert_eq!(
|
||||
input_value(graphene_std::text::text::LetterTiltInput::INDEX),
|
||||
Some(TaggedValue::F64(10.)),
|
||||
"shape {shape} lost its letter tilt"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_duplicate_node_replacements() {
|
||||
let mut hashmap = HashMap::<ProtoNodeIdentifier, u32>::new();
|
||||
|
||||
@@ -48,6 +48,11 @@ impl ResourceStorageMessageHandler {
|
||||
inner: self.storage.clone().expect("Resource storage not initialized"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the resource's data is held in storage, assuming it is until storage is initialized.
|
||||
pub fn contains(&self, hash: &ResourceHash) -> bool {
|
||||
self.storage.as_ref().is_none_or(|storage| storage.contains(hash))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ResourceStorageMessageHandler {
|
||||
|
||||
@@ -153,6 +153,9 @@ async fn drain_queue(inner: Arc<Mutex<Inner>>) {
|
||||
Mutation::Write { hash, bytes } => {
|
||||
if let Err(error) = write_file(&directory, &hash, &bytes).await {
|
||||
log::error!("OPFS write for {hash} failed: {error:?}");
|
||||
|
||||
// Nothing reached disk, so leaving the hash listed would claim a file that later sessions cannot read
|
||||
inner.lock().unwrap().on_disk.remove(&hash);
|
||||
}
|
||||
}
|
||||
Mutation::Delete { hash } => {
|
||||
|
||||
@@ -269,8 +269,19 @@ pub async fn resource<'a: 'n>(
|
||||
hash: Item<ResourceHash>,
|
||||
) -> Item<Resource> {
|
||||
let hash = hash.into_element();
|
||||
let application_io = editor_api.into_element().application_io.as_ref().expect("ApplicationIo must be available when using resources");
|
||||
let resource = application_io.load_resource(hash).await.unwrap_or_else(|| panic!("Resource {hash} not found"));
|
||||
let placeholder = || -> Item<Resource> { Item::new_from_element(Resource::empty()) };
|
||||
|
||||
let Some(application_io) = editor_api.into_element().application_io.as_ref() else {
|
||||
log::error!("Resource {hash} is unavailable because the platform's application IO is missing");
|
||||
return placeholder();
|
||||
};
|
||||
|
||||
// Stored bytes go missing when the browser evicts its storage or a write is interrupted
|
||||
let Some(resource) = application_io.load_resource(hash).await else {
|
||||
log::error!("Resource {hash} was not found in storage");
|
||||
return placeholder();
|
||||
};
|
||||
|
||||
Item::new_from_element(resource)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user