Implement the Brush without relying on a stamp texture

Test Plan: Test the BrushNode in the editor

Reviewers: Keavon

Reviewed By: Keavon

Pull Request: https://github.com/GraphiteEditor/Graphite/pull/1184
This commit is contained in:
Dennis Kobert 2023-04-29 01:31:14 +02:00 committed by Keavon Chambers
parent 5d9c0cb4d5
commit 1020eb6835
31 changed files with 221 additions and 178 deletions

74
Cargo.lock generated
View File

@ -232,9 +232,9 @@ dependencies = [
[[package]]
name = "axum"
version = "0.6.16"
version = "0.6.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "113713495a32dd0ab52baf5c10044725aa3aec00b31beda84218e469029b72a3"
checksum = "b70caf9f1b0c045f7da350636435b775a9733adf2df56e8aa2a29210fbc335d4"
dependencies = [
"async-trait",
"axum-core",
@ -1122,12 +1122,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flate2"
version = "1.0.25"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
dependencies = [
"crc32fast",
"miniz_oxide 0.6.2",
"miniz_oxide",
]
[[package]]
@ -2266,8 +2266,8 @@ dependencies = [
[[package]]
name = "kurbo"
version = "0.9.3"
source = "git+https://github.com/linebender/kurbo.git#615ed7ede2d80ae0f3509165c5a2dee6cef69251"
version = "0.9.4"
source = "git+https://github.com/linebender/kurbo.git#01b52cd85c3a9b1be2c6e39ab97fcf401a8911c4"
dependencies = [
"arrayvec",
"serde",
@ -2342,9 +2342,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
version = "0.3.4"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf"
checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c"
[[package]]
name = "litrs"
@ -2493,15 +2493,6 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "miniz_oxide"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
dependencies = [
"adler",
]
[[package]]
name = "miniz_oxide"
version = "0.7.1"
@ -2864,9 +2855,9 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.51"
version = "0.10.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97ea2d98598bf9ada7ea6ee8a30fb74f9156b63bbe495d64ec2b87c269d2dda3"
checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56"
dependencies = [
"bitflags",
"cfg-if",
@ -2896,9 +2887,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.86"
version = "0.9.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "992bac49bdbab4423199c654a5515bd2a6c6a23bf03f2dd3bdb7e5ae6259bc69"
checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e"
dependencies = [
"cc",
"libc",
@ -3169,7 +3160,7 @@ dependencies = [
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.7.1",
"miniz_oxide",
]
[[package]]
@ -3457,9 +3448,9 @@ checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157"
[[package]]
name = "reqwest"
version = "0.11.16"
version = "0.11.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254"
checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91"
dependencies = [
"base64 0.21.0",
"bytes",
@ -3564,9 +3555,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.37.14"
version = "0.37.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b864d3c18a5785a05953adeed93e2dca37ed30f18e69bba9f30079d51f363f"
checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece"
dependencies = [
"bitflags",
"errno",
@ -4319,9 +4310,9 @@ dependencies = [
[[package]]
name = "target-lexicon"
version = "0.12.6"
version = "0.12.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae9980cab1db3fceee2f6c6f643d5d8de2997c58ee8d25fb0cc8a9e9e7348e5"
checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5"
[[package]]
name = "tauri"
@ -4670,9 +4661,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.27.0"
version = "1.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001"
checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f"
dependencies = [
"autocfg",
"bytes",
@ -4684,14 +4675,14 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.45.0",
"windows-sys 0.48.0",
]
[[package]]
name = "tokio-macros"
version = "2.0.0"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
@ -4721,9 +4712,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.7"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
dependencies = [
"bytes",
"futures-core",
@ -4806,11 +4797,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]]
name = "tracing"
version = "0.1.37"
version = "0.1.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
checksum = "cf9cf6a813d3f40c88b0b6b6f29a5c95c6cdbf97c1f9cc53fb820200f5ad814d"
dependencies = [
"cfg-if",
"log",
"pin-project-lite",
"tracing-attributes",
@ -4819,13 +4809,13 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.23"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.15",
]
[[package]]

View File

@ -33,7 +33,6 @@ specta = { git = "https://github.com/oscartbeaumont/rspc", rev = "9725ddbfe40183
] }
xxhash-rust = { version = "0.8.4", features = ["xxh3"] }
[profile.dev.package.graphite-editor]
opt-level = 1
@ -54,6 +53,9 @@ opt-level = 3
[profile.dev.package.image]
opt-level = 3
[profile.dev.package.png]
opt-level = 3
[profile.dev.package.xxhash-rust]
opt-level = 3

View File

@ -24,7 +24,7 @@ struct ModifyInputsContext<'a> {
impl<'a> ModifyInputsContext<'a> {
/// Get the node network from the document
fn new(layer: &'a [LayerId], document: &'a mut Document, node_graph: &'a mut NodeGraphMessageHandler, responses: &'a mut VecDeque<Message>) -> Option<Self> {
document.layer_mut(&layer).ok().and_then(|layer| layer.as_node_graph_mut().ok()).map(|network| Self {
document.layer_mut(layer).ok().and_then(|layer| layer.as_node_graph_mut().ok()).map(|network| Self {
network,
node_graph,
responses,

View File

@ -1048,19 +1048,19 @@ pub fn wrap_network_in_scope(network: NodeNetwork) -> NodeNetwork {
let nodes = vec![
resolve_document_node_type("Begin Scope")
.expect("Begin Scope node type not found")
.to_document_node(vec![input_type.clone()], DocumentNodeMetadata::default()),
.to_document_node(vec![input_type], DocumentNodeMetadata::default()),
inner_network,
resolve_document_node_type("End Scope")
.expect("End Scope node type not found")
.to_document_node(vec![NodeInput::node(0, 0), NodeInput::node(1, 0)], DocumentNodeMetadata::default()),
];
let network = NodeNetwork {
NodeNetwork {
inputs: vec![0],
outputs: vec![NodeOutput::new(2, 0)],
nodes: nodes.into_iter().enumerate().map(|(id, node)| (id as NodeId, node)).collect(),
..Default::default()
};
network
}
}
pub fn new_image_network(output_offset: i32, output_node_id: NodeId) -> NodeNetwork {
@ -1121,8 +1121,8 @@ pub fn new_text_network(text: String, font: Font, size: f64) -> NodeNetwork {
text_generator.to_document_node(
[
NodeInput::Network(concrete!(graphene_core::EditorApi)),
NodeInput::value(TaggedValue::String(text.clone()), false),
NodeInput::value(TaggedValue::Font(font.clone()), false),
NodeInput::value(TaggedValue::String(text), false),
NodeInput::value(TaggedValue::Font(font), false),
NodeInput::value(TaggedValue::F64(size), false),
],
DocumentNodeMetadata::position((0, 4)),

View File

@ -149,10 +149,10 @@ impl ShapeState {
/// Move the selected points by dragging the mouse.
pub fn move_selected_points(&self, document: &Document, delta: DVec2, mirror_distance: bool, responses: &mut VecDeque<Message>) {
for (layer_path, state) in &self.selected_shape_state {
let Ok(layer) = document.layer(&layer_path) else { continue };
let Ok(layer) = document.layer(layer_path) else { continue };
let Some(vector_data) = layer.as_vector_data() else { continue };
let transform = document.multiply_transforms(&layer_path).unwrap_or_default();
let transform = document.multiply_transforms(layer_path).unwrap_or_default();
let delta = transform.inverse().transform_vector2(delta);
for &point in state.selected_points.iter() {

View File

@ -315,7 +315,7 @@ fn add_brush_render(data: &BrushToolData, tool_data: &DocumentToolData, response
};
let mut network = NodeNetwork::value_network(brush_node);
network.push_output_node();
graph_modification_utils::new_custom_layer(network, layer_path.clone(), responses);
graph_modification_utils::new_custom_layer(network, layer_path, responses);
}
fn load_existing_points(document: &DocumentMessageHandler) -> Option<(Vec<LayerId>, Vec<DVec2>)> {

View File

@ -223,7 +223,7 @@ fn add_polyline(data: &FreehandToolData, tool_data: &DocumentToolData, responses
graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses);
responses.add(GraphOperationMessage::StrokeSet {
layer: layer_path.clone(),
layer: layer_path,
stroke: Stroke::new(tool_data.primary_color, data.weight),
});
}

View File

@ -164,7 +164,7 @@ impl Fsm for LineToolFsmState {
tool_data.path = Some(layer_path.clone());
graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses);
responses.add(GraphOperationMessage::StrokeSet {
layer: layer_path.clone(),
layer: layer_path,
stroke: Stroke::new(global_tool_data.primary_color, tool_options.line_weight),
});

View File

@ -273,11 +273,9 @@ impl Fsm for PathToolFsmState {
if tool_data.opposing_handle_lengths.is_none() {
tool_data.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(&document.document_legacy));
}
} else {
if let Some(opposing_handle_lengths) = &tool_data.opposing_handle_lengths {
shape_editor.reset_opposing_handle_lengths(&document.document_legacy, opposing_handle_lengths, responses);
tool_data.opposing_handle_lengths = None;
}
} else if let Some(opposing_handle_lengths) = &tool_data.opposing_handle_lengths {
shape_editor.reset_opposing_handle_lengths(&document.document_legacy, opposing_handle_lengths, responses);
tool_data.opposing_handle_lengths = None;
}
// Move the selected points by the mouse position
@ -291,8 +289,7 @@ impl Fsm for PathToolFsmState {
(_, PathToolMessage::DragStop { shift_mirror_distance }) => {
let nearest_point = shape_editor
.find_nearest_point_indices(&document.document_legacy, input.mouse.position, SELECTION_THRESHOLD)
.map(|(_, nearest_point)| nearest_point)
.clone();
.map(|(_, nearest_point)| nearest_point);
let shift_pressed = input.keyboard.get(shift_mirror_distance as usize);
if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD && !shift_pressed {

View File

@ -475,7 +475,7 @@ impl PenToolData {
modification: VectorDataModification::SetManipulatorPosition { point, position },
});
return Some(DocumentMessage::CommitTransaction);
Some(DocumentMessage::CommitTransaction)
}
}

View File

@ -762,7 +762,7 @@ impl Fsm for SelectToolFsmState {
if let Some(path) = intersection.last() {
// let folders: Vec<_> = (1..path.len() + 1).map(|i| &path[0..i]).collect();
// let replacement_selected_layers: Vec<_> = document.selected_layers().filter(|&layer| !folders.contains(&layer)).map(|path| path.to_vec()).collect();
let replacement_selected_layers: Vec<_> = document.selected_layers().filter(|&layer| !path.starts_with(&layer)).map(|path| path.to_vec()).collect();
let replacement_selected_layers: Vec<_> = document.selected_layers().filter(|&layer| !path.starts_with(layer)).map(|path| path.to_vec()).collect();
tool_data.layers_dragging.clear();
tool_data.layers_dragging.append(replacement_selected_layers.clone().as_mut());
@ -999,7 +999,7 @@ fn drag_shallowest_manipulation(
// Checks if the incoming layer's root parent is already selected
// If so we need to update the selected layer to the deeper of the two
let mut layers_without_incoming_parent: Vec<Vec<u64>> = document.selected_layers().filter(|&layer| layer != [incoming_parent].as_slice()).map(|path| path.to_vec()).collect();
if layers.contains(&&[incoming_parent].as_slice()) {
if layers.contains(&[incoming_parent].as_slice()) {
// Add incoming layer
tool_data.layers_dragging.clear();
responses.add(DocumentMessage::DeselectAllLayers);
@ -1028,7 +1028,7 @@ fn drag_shallowest_manipulation(
// Check if the intersected layer path is already selected
let previous_parents: Vec<_> = (0..layers.len()).map(|i| &layers.get(i).unwrap()[..1]).collect();
let already_selected_parent = previous_parents.contains(&&[incoming_parent].as_slice());
let already_selected_parent = previous_parents.contains(&[incoming_parent].as_slice());
let selected_layers: Vec<_> = document.selected_layers().collect();
let mut search = previous_layer_path.to_vec();
@ -1042,7 +1042,7 @@ fn drag_shallowest_manipulation(
selected_layer_path_parent = selected_layer_path_parent[..selected_layer_path_parent.len() - 1].to_vec();
}
while selected_layer_path_parent.len() > 0 && !is_parent && !recursive_found {
while !selected_layer_path_parent.is_empty() && !is_parent && !recursive_found {
let selected_children_layer_paths = document.document_legacy.folder_children_paths(&selected_layer_path_parent);
for child in selected_children_layer_paths {
if child == *incoming_layer_path_vector {
@ -1062,7 +1062,7 @@ fn drag_shallowest_manipulation(
// Check if new layer is already selected
let mut already_selected = false;
if selected_layers.contains(&search.clone().as_slice()) {
if selected_layers.contains(&search.as_slice()) {
already_selected = true;
}
@ -1086,7 +1086,7 @@ fn drag_shallowest_manipulation(
} else {
// Previous selected layers with the intersect layer path appended to it
let mut combined_layers = selected_layers.clone();
let intersection_temp = intersection.clone();
let intersection_temp = intersection;
let intersection_temp_slice = intersection_temp.as_slice();
combined_layers.push(intersection_temp_slice);
let layers_iter = combined_layers.into_iter();
@ -1162,7 +1162,7 @@ fn edit_layer_shallowest_manipulation(document: &DocumentMessageHandler, interse
let incoming_parent = *intersect_layer_path.first().unwrap();
let previous_parents: Vec<_> = (0..selected_layers.len()).map(|i| &selected_layers.get(i).unwrap()[..1]).collect();
let mut incoming_parent_selected = false;
if previous_parents.contains(&&[incoming_parent].as_slice()) {
if previous_parents.contains(&[incoming_parent].as_slice()) {
incoming_parent_selected = true;
}
if incoming_parent_selected {
@ -1225,11 +1225,9 @@ fn recursive_search(document: &DocumentMessageHandler, layer_path: &Vec<u64>, in
for path in layer_paths {
if path == *incoming_layer_path_vector {
return true;
} else if document.document_legacy.is_folder(path.clone()) {
if recursive_search(document, &path, incoming_layer_path_vector) {
return true;
}
} else if document.document_legacy.is_folder(path.clone()) && recursive_search(document, &path, incoming_layer_path_vector) {
return true;
}
}
return false;
false
}

View File

@ -280,7 +280,7 @@ impl TextToolData {
else if let Some(editing_text) = self.editing_text.as_ref().filter(|_| state == TextToolFsmState::Ready) {
responses.add(DocumentMessage::StartTransaction);
let network = new_text_network(String::new(), editing_text.font.clone(), editing_text.font_size as f64);
let network = new_text_network(String::new(), editing_text.font.clone(), editing_text.font_size);
responses.add(Operation::AddFrame {
path: self.layer_path.clone(),
@ -320,7 +320,7 @@ impl TextToolData {
resize_overlays(&mut self.overlays, responses, 1);
let editing_text = self.editing_text.as_ref()?;
let buzz_face = render_data.font_cache.get(&editing_text.font).map(|data| load_face(&data));
let buzz_face = render_data.font_cache.get(&editing_text.font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(&self.new_text, buzz_face, editing_text.font_size, None);
let quad = Quad::from_box([DVec2::ZERO, far]);
@ -337,8 +337,8 @@ impl TextToolData {
fn get_bounds(&self, text: &str, render_data: &RenderData) -> Option<[DVec2; 2]> {
let editing_text = self.editing_text.as_ref()?;
let buzz_face = render_data.font_cache.get(&editing_text.font).map(|data| load_face(&data));
let subpaths = graphene_core::text::to_path(&text, buzz_face, editing_text.font_size, None);
let buzz_face = render_data.font_cache.get(&editing_text.font).map(|data| load_face(data));
let subpaths = graphene_core::text::to_path(text, buzz_face, editing_text.font_size, None);
let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box());
let combined_bounds = bounds.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])]).unwrap_or_default();
Some(combined_bounds)
@ -383,8 +383,8 @@ fn update_overlays(document: &DocumentMessageHandler, tool_data: &mut TextToolDa
let node_id = get_text_node_id(node_graph)?;
let document_node = node_graph.nodes.get(&node_id)?;
let (text, font, font_size) = TextToolData::extract_text_node_inputs(document_node)?;
let buzz_face = render_data.font_cache.get(font).map(|data| load_face(&data));
let far = graphene_core::text::bounding_box(&text, buzz_face, font_size, None);
let buzz_face = render_data.font_cache.get(font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(text, buzz_face, font_size, None);
let quad = Quad::from_box([DVec2::ZERO, far]);
let multiplied = document.document_legacy.multiply_transforms(path).ok()? * quad;
Some(multiplied.bounding_box())
@ -474,7 +474,7 @@ impl Fsm for TextToolFsmState {
tool_data.new_text = String::new();
tool_data.layer_path = document.get_path_for_new_layer();
tool_data.interact(state, input.mouse.position, document, &render_data, responses)
tool_data.interact(state, input.mouse.position, document, render_data, responses)
}
(state, TextToolMessage::EditSelected) => {
if let Some(layer_path) = can_edit_selected(document) {

View File

@ -65,10 +65,10 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
}
if using_path_tool {
if let Ok(layer) = document.document_legacy.layer(&selected_layers[0]) {
if let Ok(layer) = document.document_legacy.layer(selected_layers[0]) {
if let Some(vector_data) = layer.as_vector_data() {
*selected.original_transforms = OriginalTransforms::default();
let viewspace = &mut document.document_legacy.generate_transform_relative_to_viewport(&selected_layers[0]).ok().unwrap_or_default();
let viewspace = &mut document.document_legacy.generate_transform_relative_to_viewport(selected_layers[0]).ok().unwrap_or_default();
let mut point_count: usize = 0;
let count_point = |position| {

View File

@ -98,7 +98,7 @@ fn handle_message(message: String) -> String {
for image in image_data {
let path = image.path.clone();
let mime = image.mime.clone();
let transform = image.transform.clone();
let transform = image.transform;
images.insert(format!("{:?}_{}", &image.path, document_id), image);
stub_data.push(FrontendImageData {
path,

View File

@ -38,9 +38,13 @@ impl<ManipulatorGroupId: crate::Identifier> Hash for ManipulatorGroup<Manipulato
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.anchor.to_array().iter().for_each(|x| x.to_bits().hash(state));
self.in_handle.is_some().hash(state);
self.in_handle.map(|in_handle| in_handle.to_array().iter().for_each(|x| x.to_bits().hash(state)));
if let Some(in_handle) = self.in_handle {
in_handle.to_array().iter().for_each(|x| x.to_bits().hash(state));
}
self.out_handle.is_some().hash(state);
self.out_handle.map(|out_handle| out_handle.to_array().iter().for_each(|x| x.to_bits().hash(state)));
if let Some(out_handle) = self.out_handle {
out_handle.to_array().iter().for_each(|x| x.to_bits().hash(state));
}
self.id.hash(state);
}
}

View File

@ -13,7 +13,7 @@ pub struct PushConstants {
impl Sample for SampledImage<Image2d> {
type Pixel = Color;
fn sample(&self, pos: glam::DVec2) -> Option<Self::Pixel> {
fn sample(&self, pos: glam::DVec2, _area: glam::DVec2) -> Option<Self::Pixel> {
let color = self.sample(pos);
Color::from_rgbaf32(color.x, color.y, color.z, color.w)
}

View File

@ -252,7 +252,7 @@ mod test {
let value: ClonedNode<Result<&u32, ()>> = ClonedNode(Ok(&4u32));
assert_eq!(value.eval(()), Ok(&4u32));
//let type_erased_clone = clone as &dyn for<'a> Node<'a, &'a u32, Output = u32>;
let map_result = MapResultNode::new(ValueNode::new(FnNode::new(|x: &u32| x.clone())));
let map_result = MapResultNode::new(ValueNode::new(FnNode::new(|x: &u32| *x)));
//et type_erased = &map_result as &dyn for<'a> Node<'a, Result<&'a u32, ()>, Output = Result<u32, ()>>;
assert_eq!(map_result.eval(Ok(&4u32)), Ok(4u32));
let fst = value.then(map_result);

View File

@ -171,7 +171,7 @@ pub trait Luminance {
pub trait Sample {
type Pixel: Pixel;
// TODO: Add an area parameter
fn sample(&self, pos: DVec2) -> Option<Self::Pixel>;
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel>;
}
// TODO: We might rename this to Bitmap at some point

View File

@ -715,6 +715,9 @@ impl Color {
}
pub fn to_unassociated_alpha(&self) -> Self {
if self.alpha == 0. {
return *self;
}
let unmultiply = 1. / self.alpha;
Self {
red: self.red * unmultiply,

View File

@ -26,7 +26,7 @@ mod base64_serde {
{
use serde::de::Error;
let color_from_chunk = |chunk: &[u8]| P::from_bytes(chunk.try_into().unwrap()).clone();
let color_from_chunk = |chunk: &[u8]| P::from_bytes(chunk.try_into().unwrap());
let colors_from_bytes = |bytes: Vec<u8>| bytes.chunks_exact(P::byte_size()).map(color_from_chunk).collect();
@ -129,7 +129,7 @@ where
pub fn into_flat_u8(self) -> (Vec<u8>, u32, u32) {
let Image { width, height, data } = self;
let to_gamma = |x| SRGBGammaFloat::from_linear(x);
let to_gamma = SRGBGammaFloat::from_linear;
let to_u8 = |x| (num_cast::<_, f32>(x).unwrap() * 255.) as u8;
let result_bytes = data
@ -201,7 +201,8 @@ pub struct ImageFrame<P: Pixel> {
impl<P: Debug + Copy + Pixel> Sample for ImageFrame<P> {
type Pixel = P;
fn sample(&self, pos: DVec2) -> Option<Self::Pixel> {
// TODO: Improve sampling logic
fn sample(&self, pos: DVec2, _area: DVec2) -> Option<Self::Pixel> {
let image_size = DVec2::new(self.image.width() as f64, self.image.height() as f64);
let pos = (DAffine2::from_scale(image_size) * self.transform.inverse()).transform_point2(pos);
if pos.x < 0. || pos.y < 0. || pos.x >= image_size.x || pos.y >= image_size.y {

View File

@ -81,7 +81,7 @@ mod test {
fn test_ref_eval() {
let value = ValueNode::new(5);
assert_eq!((&value).eval(()), &5);
assert_eq!(value.eval(()), &5);
let id = IdNode::new();
let compose = ComposeNode::new(&value, &id);

View File

@ -7,6 +7,7 @@ pub struct IntNode<const N: u32>;
impl<'i, const N: u32> Node<'i, ()> for IntNode<N> {
type Output = u32;
#[inline(always)]
fn eval(&'i self, _input: ()) -> Self::Output {
N
}
@ -17,6 +18,7 @@ pub struct ValueNode<T>(pub T);
impl<'i, T: 'i> Node<'i, ()> for ValueNode<T> {
type Output = &'i T;
#[inline(always)]
fn eval(&'i self, _input: ()) -> Self::Output {
&self.0
}
@ -45,6 +47,7 @@ pub struct ClonedNode<T: Clone>(pub T);
impl<'i, T: Clone + 'i> Node<'i, ()> for ClonedNode<T> {
type Output = T;
#[inline(always)]
fn eval(&'i self, _input: ()) -> Self::Output {
self.0.clone()
}
@ -62,11 +65,34 @@ impl<T: Clone> From<T> for ClonedNode<T> {
}
}
#[derive(Clone, Copy)]
/// The DebugClonedNode logs every time it is evaluated.
/// This is useful for debugging.
pub struct DebugClonedNode<T: Clone>(pub T);
impl<'i, T: Clone + 'i> Node<'i, ()> for DebugClonedNode<T> {
type Output = T;
#[inline(always)]
fn eval(&'i self, _input: ()) -> Self::Output {
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
log::debug!("DebugClonedNode::eval");
self.0.clone()
}
}
impl<T: Clone> DebugClonedNode<T> {
pub const fn new(value: T) -> ClonedNode<T> {
ClonedNode(value)
}
}
#[derive(Clone, Copy)]
pub struct CopiedNode<T: Copy>(pub T);
impl<'i, T: Copy + 'i> Node<'i, ()> for CopiedNode<T> {
type Output = T;
#[inline(always)]
fn eval(&'i self, _input: ()) -> Self::Output {
self.0
}

View File

@ -26,7 +26,7 @@ fn set_vector_data_fill(
positions: Vec<(f64, Option<Color>)>,
) -> VectorData {
vector_data.style.set_fill(match fill_type {
FillType::None | FillType::Solid => solid_color.map_or(Fill::None, |solid_color| Fill::Solid(solid_color)),
FillType::None | FillType::Solid => solid_color.map_or(Fill::None, Fill::Solid),
FillType::Gradient => Fill::Gradient(Gradient {
start,
end,

View File

@ -169,8 +169,7 @@ pub struct UniformNode<Executor> {
#[node_macro::node_fn(UniformNode)]
fn uniform_node<T: ToUniformBuffer, E: GpuExecutor>(data: T, executor: &'any_input E) -> ShaderInput<E::BufferHandle> {
let handle = executor.create_uniform_buffer(data).unwrap();
handle
executor.create_uniform_buffer(data).unwrap()
}
pub struct StorageNode<Executor> {
@ -179,7 +178,7 @@ pub struct StorageNode<Executor> {
#[node_macro::node_fn(StorageNode)]
fn storage_node<T: ToStorageBuffer, E: GpuExecutor>(data: T, executor: &'any_input E) -> ShaderInput<E::BufferHandle> {
let handle = executor
executor
.create_storage_buffer(
data,
StorageBufferOptions {
@ -188,8 +187,7 @@ fn storage_node<T: ToStorageBuffer, E: GpuExecutor>(data: T, executor: &'any_inp
cpu_readable: false,
},
)
.unwrap();
handle
.unwrap()
}
pub struct PushNode<Value> {

View File

@ -292,7 +292,7 @@ impl NodeNetwork {
}
pub fn input_types<'a>(&'a self) -> impl Iterator<Item = Type> + 'a {
self.inputs.iter().map(move |id| self.nodes[id].inputs.get(0).map(|i| i.ty().clone()).unwrap_or(concrete!(())))
self.inputs.iter().map(move |id| self.nodes[id].inputs.get(0).map(|i| i.ty()).unwrap_or(concrete!(())))
}
/// An empty graph
@ -500,7 +500,7 @@ impl NodeNetwork {
}
FlowIter {
stack: self.outputs.iter().map(|output| output.node_id).collect(),
network: &self,
network: self,
}
}
}

View File

@ -1,8 +1,8 @@
use std::marker::PhantomData;
use glam::{DAffine2, DVec2};
use graphene_core::raster::{Color, Image, ImageFrame, RasterMut};
use graphene_core::transform::TransformMut;
use graphene_core::raster::{Alpha, Color, Pixel, Sample};
use graphene_core::transform::{Transform, TransformMut};
use graphene_core::vector::VectorData;
use graphene_core::Node;
use node_macro::node_fn;
@ -73,8 +73,49 @@ fn vector_points(vector: VectorData) -> Vec<DVec2> {
vector.subpaths.iter().flat_map(|subpath| subpath.manipulator_groups().iter().map(|group| group.anchor)).collect()
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BrushStampGenerator<P: Pixel + Alpha> {
color: P,
feather_exponent: f32,
transform: DAffine2,
}
impl<P: Pixel + Alpha> Transform for BrushStampGenerator<P> {
fn transform(&self) -> DAffine2 {
self.transform
}
}
impl<P: Pixel + Alpha> TransformMut for BrushStampGenerator<P> {
fn transform_mut(&mut self) -> &mut DAffine2 {
&mut self.transform
}
}
impl<P: Pixel + Alpha> Sample for BrushStampGenerator<P> {
type Pixel = P;
#[inline]
fn sample(&self, position: DVec2, area: DVec2) -> Option<P> {
let position = self.transform.inverse().transform_point2(position);
let area = self.transform.inverse().transform_vector2(area);
let center = DVec2::splat(0.5);
let distance = (position + area / 2. - center).length() as f32 * 2.;
let result = if distance < 1. {
1. - distance.powf(self.feather_exponent)
} else {
return None;
};
use graphene_core::raster::Channel;
Some(self.color.multiplied_alpha(P::AlphaChannel::from_f32(result)))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct BrushTextureNode<ColorNode, Hardness, Flow> {
pub struct BrushStampGeneratorNode<ColorNode, Hardness, Flow> {
pub color: ColorNode,
pub hardness: Hardness,
pub flow: Flow,
@ -92,17 +133,14 @@ fn erase(input: (Color, Color), flow: f64) -> Color {
Color::from_unassociated_alpha(input.r(), input.g(), input.b(), alpha)
}
#[node_fn(BrushTextureNode)]
fn brush_texture(diameter: f64, color: Color, hardness: f64, flow: f64) -> ImageFrame<Color> {
#[node_fn(BrushStampGeneratorNode)]
fn brush_stamp_generator_node(diameter: f64, color: Color, hardness: f64, flow: f64) -> BrushStampGenerator<Color> {
// Diameter
let radius = diameter / 2.;
// TODO: Remove the 4px padding after figuring out why the brush stamp gets randomly offset by 1px up/down/left/right when clicking with the Brush tool
let dimension = diameter.ceil() as u32 + 4;
let center = DVec2::splat(radius + (dimension as f64 - diameter) / 2.);
// Hardness
let hardness = hardness / 100.;
let feather_exponent = 1. / (1. - hardness);
let feather_exponent = 1. / (1. - hardness) as f32;
// Flow
let flow = flow / 100.;
@ -110,33 +148,8 @@ fn brush_texture(diameter: f64, color: Color, hardness: f64, flow: f64) -> Image
// Color
let color = color.apply_opacity(flow as f32);
// Initial transparent image
let mut image = Image::new(dimension, dimension, Color::TRANSPARENT);
for y in 0..dimension {
for x in 0..dimension {
let summation = MULTISAMPLE_GRID.iter().fold(0., |acc, (offset_x, offset_y)| {
let position = DVec2::new(x as f64 + offset_x, y as f64 + offset_y);
let distance = (position - center).length();
if distance < radius {
acc + (1. - (distance / radius).powf(feather_exponent)).clamp(0., 1.)
} else {
acc
}
});
let pixel_fill = summation / MULTISAMPLE_GRID.len() as f64;
let pixel = image.get_pixel_mut(x, y).unwrap();
*pixel = color.apply_opacity(pixel_fill as f32);
}
}
ImageFrame {
image,
transform: DAffine2::from_scale_angle_translation(DVec2::splat(dimension as f64), 0., -DVec2::splat(radius)),
}
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(diameter), 0., -DVec2::splat(radius));
BrushStampGenerator { color, feather_exponent, transform }
}
#[derive(Clone, Debug, PartialEq)]
@ -183,19 +196,17 @@ mod test {
#[test]
fn test_brush_texture() {
let brush_texture_node = BrushTextureNode::new(ClonedNode::new(Color::BLACK), ClonedNode::new(100.), ClonedNode::new(100.));
let brush_texture_node = BrushStampGeneratorNode::new(ClonedNode::new(Color::BLACK), ClonedNode::new(100.), ClonedNode::new(100.));
let size = 20.;
let image = brush_texture_node.eval(size);
assert_eq!(image.image.width, size.ceil() as u32 + 4);
assert_eq!(image.image.height, size.ceil() as u32 + 4);
assert_eq!(image.transform, DAffine2::from_scale_angle_translation(DVec2::splat(size.ceil() + 4.), 0., -DVec2::splat(size / 2.)));
assert_eq!(image.transform(), DAffine2::from_scale_angle_translation(DVec2::splat(size.ceil()), 0., -DVec2::splat(size / 2.)));
// center pixel should be BLACK
assert_eq!(image.image.get_pixel(11, 11), Some(Color::BLACK));
assert_eq!(image.sample(DVec2::splat(0.), DVec2::ONE), Some(Color::BLACK));
}
#[test]
fn test_brush() {
let brush_texture_node = BrushTextureNode::new(ClonedNode::new(Color::BLACK), ClonedNode::new(1.0), ClonedNode::new(1.0));
let brush_texture_node = BrushStampGeneratorNode::new(ClonedNode::new(Color::BLACK), ClonedNode::new(1.0), ClonedNode::new(1.0));
let image = brush_texture_node.eval(20.);
let trace = vec![DVec2::new(0.0, 0.0), DVec2::new(10.0, 0.0)];
let trace = ClonedNode::new(trace.into_iter());
@ -203,7 +214,6 @@ mod test {
let frames = MapNode::new(ValueNode::new(translate_node));
let frames = trace.then(frames).eval(()).collect::<Vec<_>>();
assert_eq!(frames.len(), 2);
assert_eq!(frames[0].image.width, 24);
let background_bounds = ReduceNode::new(ClonedNode::new(None), ValueNode::new(MergeBoundingBoxNode::new()));
let background_bounds = background_bounds.eval(frames.clone().into_iter());
let background_bounds = ClonedNode::new(background_bounds.unwrap().to_transform());
@ -211,8 +221,8 @@ mod test {
let blend_node = graphene_core::raster::BlendNode::new(ClonedNode::new(BlendMode::Normal), ClonedNode::new(1.0));
let final_image = ReduceNode::new(background_image, ValueNode::new(BlendImageTupleNode::new(ValueNode::new(blend_node))));
let final_image = final_image.eval(frames.into_iter());
assert_eq!(final_image.image.height, 24);
assert_eq!(final_image.image.width, 34);
assert_eq!(final_image.image.height, 20);
assert_eq!(final_image.image.width, 30);
drop(final_image);
}
}

View File

@ -28,12 +28,12 @@ where
if let Some((_, cached_value, keep)) = self.cache.iter().find(|(h, _, _)| *h == hash) {
keep.store(true, std::sync::atomic::Ordering::Relaxed);
return cached_value;
cached_value
} else {
trace!("Cache miss");
let output = self.node.eval(input);
let index = self.cache.push((hash, output, AtomicBool::new(true)));
return &self.cache[index].1;
&self.cache[index].1
}
}
@ -70,7 +70,7 @@ where
fn serialize(&self) -> Option<String> {
let output = self.output.lock().unwrap();
(&*output).as_ref().map(|output| serde_json::to_string(output).ok()).flatten()
(*output).as_ref().and_then(|output| serde_json::to_string(output).ok())
}
}
@ -110,7 +110,7 @@ impl<'i, T: 'i + Hash> Node<'i, Option<T>> for LetNode<T> {
}
trace!("Cache miss");
let index = self.cache.push((hash, input));
return &self.cache[index].1;
&self.cache[index].1
}
None => &self.cache.iter().last().expect("Let node was not initialized").1,
}

View File

@ -2,7 +2,7 @@ use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2};
use graphene_core::raster::{Alpha, Channel, Image, ImageFrame, Luminance, Pixel, RasterMut, Sample};
use graphene_core::transform::Transform;
use graphene_core::value::{ClonedNode, ValueNode};
use graphene_core::Node;
use std::fmt::Debug;
@ -240,6 +240,7 @@ fn mask_image<
// Transforms a point from the background image to the forground image
let bg_to_fg = image.transform() * DAffine2::from_scale(1. / image_size);
let area = bg_to_fg.transform_point2(DVec2::new(1., 1.)) - bg_to_fg.transform_point2(DVec2::ZERO);
for y in 0..image.height() {
for x in 0..image.width() {
let image_point = DVec2::new(x as f64, y as f64);
@ -247,8 +248,8 @@ fn mask_image<
let local_mask_point = stencil.transform().inverse().transform_point2(mask_point);
mask_point = stencil.transform().transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE));
let image_pixel = image.get_pixel_mut(x as u32, y as u32).unwrap();
if let Some(mask_pixel) = stencil.sample(mask_point) {
let image_pixel = image.get_pixel_mut(x, y).unwrap();
if let Some(mask_pixel) = stencil.sample(mask_point, area) {
*image_pixel = image_pixel.multiplied_alpha(mask_pixel.l().to_channel());
}
}
@ -258,20 +259,20 @@ fn mask_image<
}
#[derive(Debug, Clone, Copy)]
pub struct BlendImageTupleNode<P, MapFn> {
pub struct BlendImageTupleNode<P, Fg, MapFn> {
map_fn: MapFn,
_p: PhantomData<P>,
_fg: PhantomData<Fg>,
}
#[node_macro::node_fn(BlendImageTupleNode<_P>)]
fn blend_image_tuple<_P: Pixel + Debug, MapFn>(images: (ImageFrame<_P>, ImageFrame<_P>), map_fn: &'any_input MapFn) -> ImageFrame<_P>
#[node_macro::node_fn(BlendImageTupleNode<_P, _Fg>)]
fn blend_image_tuple<_P: Pixel + Debug, MapFn, _Fg: Sample<Pixel = _P> + Transform>(images: (ImageFrame<_P>, _Fg), map_fn: &'any_input MapFn) -> ImageFrame<_P>
where
MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P> + 'input + Clone,
{
let (background, foreground) = images;
let node = BlendImageNode::new(ClonedNode::new(background), ValueNode::new(map_fn.clone()));
node.eval(foreground)
blend_image(foreground, background, map_fn)
}
#[derive(Debug, Clone, Copy)]
@ -283,13 +284,20 @@ pub struct BlendImageNode<P, Background, MapFn> {
// TODO: Implement proper blending
#[node_macro::node_fn(BlendImageNode<_P>)]
fn blend_image<_P: Clone, MapFn, Frame: Sample<Pixel = _P> + Transform, Background: RasterMut<Pixel = _P> + Transform>(
fn blend_image_node<_P: Clone, MapFn, Frame: Sample<Pixel = _P> + Transform, Background: RasterMut<Pixel = _P> + Transform>(
foreground: Frame,
mut background: Background,
background: Background,
map_fn: &'any_input MapFn,
) -> Background
where
MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P> + 'input,
{
blend_image(foreground, background, map_fn)
}
fn blend_image<_P: Clone, MapFn, Frame: Sample<Pixel = _P> + Transform, Background: RasterMut<Pixel = _P> + Transform>(foreground: Frame, mut background: Background, map_fn: &MapFn) -> Background
where
MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P>,
{
let background_size = DVec2::new(background.width() as f64, background.height() as f64);
@ -303,12 +311,13 @@ where
let start = (bg_aabb.start * background_size).max(DVec2::ZERO).as_uvec2();
let end = (bg_aabb.end * background_size).min(background_size).as_uvec2();
let area = bg_to_fg.transform_point2(DVec2::new(1., 1.)) - bg_to_fg.transform_point2(DVec2::ZERO);
for y in start.y..end.y {
for x in start.x..end.x {
let bg_point = DVec2::new(x as f64, y as f64);
let fg_point = bg_to_fg.transform_point2(bg_point);
if let Some(src_pixel) = foreground.sample(fg_point) {
if let Some(src_pixel) = foreground.sample(fg_point, area) {
if let Some(dst_pixel) = background.get_pixel_mut(x, y) {
*dst_pixel = map_fn.eval((src_pixel, dst_pixel.clone()));
}

View File

@ -176,8 +176,13 @@ impl BorrowTree {
}
pub fn push_node(&mut self, id: NodeId, proto_node: ProtoNode, typing_context: &TypingContext) -> Result<(), String> {
let ProtoNode { construction_args, identifier, .. } = proto_node;
self.source_map.insert(proto_node.document_node_path, id);
let ProtoNode {
construction_args,
identifier,
document_node_path,
..
} = proto_node;
self.source_map.insert(document_node_path, id);
match construction_args {
ConstructionArgs::Value(value) => {

View File

@ -8,7 +8,7 @@ use std::collections::HashMap;
use graphene_core::raster::color::Color;
use graphene_core::raster::*;
use graphene_core::structural::Then;
use graphene_core::value::{ClonedNode, ForgetNode, ValueNode};
use graphene_core::value::{ClonedNode, CopiedNode, ForgetNode, ValueNode};
use graphene_core::{Node, NodeIO, NodeIOTypes};
use graphene_std::brush::*;
use graphene_std::raster::*;
@ -175,6 +175,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
vec![(
NodeIdentifier::new("graphene_std::brush::BrushNode"),
|args| {
use graphene_core::value::*;
use graphene_std::brush::*;
let trace: DowncastBothNode<(), Vec<DVec2>> = DowncastBothNode::new(args[0]);
@ -183,24 +184,23 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
let flow: DowncastBothNode<(), f64> = DowncastBothNode::new(args[3]);
let color: DowncastBothNode<(), Color> = DowncastBothNode::new(args[4]);
let stamp = BrushTextureNode::new(color, ClonedNode::new(hardness.eval(())), ClonedNode::new(flow.eval(())));
let stamp = BrushStampGeneratorNode::new(color, CopiedNode::new(hardness.eval(())), CopiedNode::new(flow.eval(())));
let stamp = stamp.eval(diameter.eval(()));
let frames = TranslateNode::new(ClonedNode::new(stamp));
let frames = TranslateNode::new(CopiedNode::new(stamp));
let frames = MapNode::new(ValueNode::new(frames));
let frames = frames.eval(trace.eval(()).into_iter()).collect::<Vec<_>>();
let background_bounds = ReduceNode::new(ClonedNode::new(None), ValueNode::new(MergeBoundingBoxNode::new()));
let background_bounds = ReduceNode::new(DebugClonedNode::new(None), ValueNode::new(MergeBoundingBoxNode::new()));
let background_bounds = background_bounds.eval(frames.clone().into_iter());
let background_bounds = ClonedNode::new(background_bounds.unwrap().to_transform());
let background_bounds = DebugClonedNode::new(background_bounds.unwrap().to_transform());
let background_image = background_bounds.then(EmptyImageNode::new(ClonedNode::new(Color::TRANSPARENT)));
let background_image = background_bounds.then(EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)));
let blend_node = graphene_core::raster::BlendNode::new(ClonedNode::new(BlendMode::Normal), ClonedNode::new(100.));
let blend_node = graphene_core::raster::BlendNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.));
let final_image = ReduceNode::new(background_image, ValueNode::new(BlendImageTupleNode::new(ValueNode::new(blend_node))));
let final_image = final_image.eval(frames.into_iter());
let final_image = ClonedNode::new(final_image);
let final_image = DebugClonedNode::new(frames.into_iter()).then(final_image);
let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(ValueNode::new(final_image));
Box::pin(any)
@ -241,7 +241,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
let image: DowncastBothNode<(), ImageFrame<Color>> = DowncastBothNode::new(args[0]);
let blend_mode: DowncastBothNode<(), BlendMode> = DowncastBothNode::new(args[1]);
let opacity: DowncastBothNode<(), f64> = DowncastBothNode::new(args[2]);
let blend_node = graphene_core::raster::BlendNode::new(ClonedNode::new(blend_mode.eval(())), ClonedNode::new(opacity.eval(())));
let blend_node = graphene_core::raster::BlendNode::new(CopiedNode::new(blend_mode.eval(())), CopiedNode::new(opacity.eval(())));
let node = graphene_std::raster::BlendImageNode::new(image, ValueNode::new(blend_node));
let _ = &node as &dyn for<'i> Node<'i, ImageFrame<Color>, Output = ImageFrame<Color>>;
let any: DynAnyNode<ImageFrame<Color>, _, _> = graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(node));

View File

@ -207,13 +207,13 @@ impl WasmSubpath {
// Line between pivot and start point on curve
let original_dashed_line = format!(
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
self.0.iter().nth(0).unwrap().start().x,
self.0.iter().nth(0).unwrap().start().y
self.0.iter().next().unwrap().start().x,
self.0.iter().next().unwrap().start().y
);
let rotated_dashed_line = format!(
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
rotated_subpath.iter().nth(0).unwrap().start().x,
rotated_subpath.iter().nth(0).unwrap().start().y
rotated_subpath.iter().next().unwrap().start().x,
rotated_subpath.iter().next().unwrap().start().y
);
wrap_svg_tag(format!("{subpath_svg}{rotated_subpath_svg}{pivot}{original_dashed_line}{rotated_dashed_line}"))