New node: Assign Colors (#1938)

* New node: Assign Colors

* Simplify the Procedural String Lights demo artwork

* Add "Group" node to the node catalog

* Better comment styling in profiling action
This commit is contained in:
Keavon Chambers 2024-08-15 05:52:46 -07:00 committed by GitHub
parent 12ebc6f972
commit fa981a0897
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 573 additions and 231 deletions

View file

@ -2,7 +2,7 @@ name: Profiling
on:
pull_request:
branches: [ master ]
branches: [master]
env:
CARGO_TERM_COLOR: always
@ -11,90 +11,90 @@ jobs:
profile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- name: Install Valgrind
run: |
sudo apt-get update
sudo apt-get install -y valgrind
- name: Install Valgrind
run: |
sudo apt-get update
sudo apt-get install -y valgrind
- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install iai-callgrind
run: |
cargo install iai-callgrind-runner@0.12.3
- name: Install iai-callgrind
run: |
cargo install iai-callgrind-runner@0.12.3
- name: Checkout master branch
run: |
git fetch origin master:master
git checkout master
- name: Checkout master branch
run: |
git fetch origin master:master
git checkout master
- name: Run baseline benchmarks
run: |
cargo bench --bench compile_demo_art --features=iai -- --save-baseline=master
- name: Run baseline benchmarks
run: |
cargo bench --bench compile_demo_art --features=iai -- --save-baseline=master
- name: Checkout PR branch
run: |
git checkout ${{ github.event.pull_request.head.sha }}
- name: Checkout PR branch
run: |
git checkout ${{ github.event.pull_request.head.sha }}
- name: Run PR benchmarks
id: benchmark
run: |
BENCH_OUTPUT=$(cargo bench --bench compile_demo_art --features=iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
echo "BENCHMARK_OUTPUT<<EOF" >> $GITHUB_OUTPUT
echo "$BENCH_OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Run PR benchmarks
id: benchmark
run: |
BENCH_OUTPUT=$(cargo bench --bench compile_demo_art --features=iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
echo "BENCHMARK_OUTPUT<<EOF" >> $GITHUB_OUTPUT
echo "$BENCH_OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Comment PR
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const benchmarkOutput = JSON.parse(`${{ steps.benchmark.outputs.BENCHMARK_OUTPUT }}`);
let significantChanges = false;
let commentBody = "#### Performance Benchmark Results\n\n";
for (const benchmark of benchmarkOutput) {
if (benchmark.callgrind_summary && benchmark.callgrind_summary.summaries) {
for (const summary of benchmark.callgrind_summary.summaries) {
for (const [eventKind, costsDiff] of Object.entries(summary.events)) {
if (costsDiff.diff_pct !== null && Math.abs(costsDiff.diff_pct) > 5) {
significantChanges = true;
const changeDirection = costsDiff.diff_pct > 0 ? "increase" : "decrease";
const color = costsDiff.diff_pct > 0 ? "red" : "green";
commentBody += `\`${benchmark.module_path}\` - ${eventKind}:\n`;
commentBody += `\\color{${color}}${changeDirection} of ${Math.abs(costsDiff.diff_pct).toFixed(2)}%\n`;
commentBody += `Old: ${costsDiff.old}, New: ${costsDiff.new}\n\n`;
- name: Comment PR
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const benchmarkOutput = JSON.parse(`${{ steps.benchmark.outputs.BENCHMARK_OUTPUT }}`);
let significantChanges = false;
let commentBody = "#### Performance Benchmark Results\n\n";
for (const benchmark of benchmarkOutput) {
if (benchmark.callgrind_summary && benchmark.callgrind_summary.summaries) {
for (const summary of benchmark.callgrind_summary.summaries) {
for (const [eventKind, costsDiff] of Object.entries(summary.events)) {
if (costsDiff.diff_pct !== null && Math.abs(costsDiff.diff_pct) > 5) {
significantChanges = true;
const changeDirection = costsDiff.diff_pct > 0 ? "Increase" : "Decrease";
const color = costsDiff.diff_pct > 0 ? "red" : "lime";
commentBody += `\`${benchmark.module_path}\` - ${eventKind}:\n`;
commentBody += `${changeDirection} of $$\\color{${color}}${Math.abs(costsDiff.diff_pct).toFixed(2)}\\\\%$$\n`;
commentBody += `Old: ${costsDiff.old}, New: ${costsDiff.new}\n\n`;
}
}
}
}
}
}
if (significantChanges) {
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
});
} else {
console.log("No significant performance changes detected. Skipping comment.");
console.log(commentBody);
}
if (significantChanges) {
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
});
} else {
console.log("No significant performance changes detected. Skipping comment.");
console.log(commentBody);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -172,6 +172,23 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
category: "Structural",
properties: |_document_node, _node_id, _context| node_properties::string_properties("The Monitor node stores the value of its last evaluation"),
},
DocumentNodeDefinition {
identifier: "Group",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::proto("graphene_core::ToGraphicGroupNode"),
inputs: vec![NodeInput::value(TaggedValue::VectorData(VectorData::empty()), true)],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_names: vec!["Element".to_string()],
output_names: vec!["Graphic Group".to_string()],
..Default::default()
},
},
category: "General",
properties: node_properties::node_no_properties,
},
DocumentNodeDefinition {
identifier: "Merge",
node_template: NodeTemplate {
@ -235,7 +252,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "To Graphic Group".to_string(),
display_name: "Group".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -3)),
..Default::default()
},
@ -3499,6 +3516,39 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
category: "Transform",
properties: node_properties::node_no_properties,
},
DocumentNodeDefinition {
identifier: "Assign Colors",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::proto("graphene_core::vector::AssignColorsNode<_, _, _, _, _, _>"),
inputs: vec![
NodeInput::value(TaggedValue::GraphicGroup(graphene_core::GraphicGroup::default()), true),
NodeInput::value(TaggedValue::Bool(true), false),
NodeInput::value(TaggedValue::Bool(false), false),
NodeInput::value(TaggedValue::GradientStops(vector::style::GradientStops::default()), false),
NodeInput::value(TaggedValue::Bool(false), false),
NodeInput::value(TaggedValue::Bool(false), false),
NodeInput::value(TaggedValue::U32(0), false),
],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_names: vec![
"Vector Group".to_string(),
"Fill".to_string(),
"Stroke".to_string(),
"Gradient".to_string(),
"Reverse".to_string(),
"Randomize".to_string(),
"Repeat Every".to_string(),
],
output_names: vec!["Vector Group".to_string()],
..Default::default()
},
},
category: "Vector",
properties: node_properties::assign_colors_properties,
},
DocumentNodeDefinition {
identifier: "Fill",
node_template: NodeTemplate {

View file

@ -87,7 +87,11 @@ fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, na
fn text_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec<WidgetHolder> {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(TaggedValue::String(x)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(TaggedValue::String(x)) = &input.as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextInput::new(x.clone())
@ -102,7 +106,11 @@ fn text_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name
fn text_area_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec<WidgetHolder> {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(TaggedValue::String(x)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(TaggedValue::String(x)) = &input.as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextAreaInput::new(x.clone())
@ -117,7 +125,11 @@ fn text_area_widget(document_node: &DocumentNode, node_id: NodeId, index: usize,
fn bool_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec<WidgetHolder> {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(&TaggedValue::Bool(x)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(&TaggedValue::Bool(x)) = &input.as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
CheckboxInput::new(x)
@ -141,7 +153,11 @@ fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize)
add_blank_assist(&mut resolution_widgets);
resolution_widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
if let Some(&TaggedValue::Footprint(footprint)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(&TaggedValue::Footprint(footprint)) = &input.as_non_exposed_value() {
let top_left = footprint.transform.transform_point2(DVec2::ZERO);
let bounds = footprint.scale();
let oversample = footprint.resolution.as_dvec2() / bounds;
@ -271,77 +287,86 @@ fn vec2_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name
assist(&mut widgets);
if let Some(&TaggedValue::DVec2(dvec2)) = document_node.inputs[index].as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
NumberInput::new(Some(dvec2.x))
.label(x)
.unit(unit)
.min(min.unwrap_or(-((1_u64 << f64::MANTISSA_DIGITS) as f64)))
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), dvec2.y)), node_id, index))
.on_commit(commit_value)
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(dvec2.y))
.label(y)
.unit(unit)
.min(min.unwrap_or(-((1_u64 << f64::MANTISSA_DIGITS) as f64)))
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(dvec2.x, input.value.unwrap())), node_id, index))
.on_commit(commit_value)
.widget_holder(),
]);
} else if let Some(&TaggedValue::IVec2(ivec2)) = document_node.inputs[index].as_non_exposed_value() {
let update_x = move |input: &NumberInput| TaggedValue::IVec2(IVec2::new(input.value.unwrap() as i32, ivec2.y));
let update_y = move |input: &NumberInput| TaggedValue::IVec2(IVec2::new(ivec2.x, input.value.unwrap() as i32));
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
NumberInput::new(Some(ivec2.x as f64))
.int()
.label(x)
.unit(unit)
.min(min.unwrap_or(-((1_u64 << f64::MANTISSA_DIGITS) as f64)))
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(update_value(update_x, node_id, index))
.on_commit(commit_value)
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(ivec2.y as f64))
.int()
.label(y)
.unit(unit)
.min(min.unwrap_or(-((1_u64 << f64::MANTISSA_DIGITS) as f64)))
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(update_value(update_y, node_id, index))
.on_commit(commit_value)
.widget_holder(),
]);
} else if let Some(&TaggedValue::UVec2(uvec2)) = document_node.inputs[index].as_non_exposed_value() {
let update_x = move |input: &NumberInput| TaggedValue::UVec2(UVec2::new(input.value.unwrap() as u32, uvec2.y));
let update_y = move |input: &NumberInput| TaggedValue::UVec2(UVec2::new(uvec2.x, input.value.unwrap() as u32));
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
NumberInput::new(Some(uvec2.x as f64))
.int()
.label(x)
.unit(unit)
.min(min.unwrap_or(0.))
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(update_value(update_x, node_id, index))
.on_commit(commit_value)
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(uvec2.y as f64))
.int()
.label(y)
.unit(unit)
.min(min.unwrap_or(0.))
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(update_value(update_y, node_id, index))
.on_commit(commit_value)
.widget_holder(),
]);
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
match input.as_non_exposed_value() {
Some(&TaggedValue::DVec2(dvec2)) => {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
NumberInput::new(Some(dvec2.x))
.label(x)
.unit(unit)
.min(min.unwrap_or(-((1_u64 << f64::MANTISSA_DIGITS) as f64)))
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), dvec2.y)), node_id, index))
.on_commit(commit_value)
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(dvec2.y))
.label(y)
.unit(unit)
.min(min.unwrap_or(-((1_u64 << f64::MANTISSA_DIGITS) as f64)))
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(dvec2.x, input.value.unwrap())), node_id, index))
.on_commit(commit_value)
.widget_holder(),
]);
}
Some(&TaggedValue::IVec2(ivec2)) => {
let update_x = move |input: &NumberInput| TaggedValue::IVec2(IVec2::new(input.value.unwrap() as i32, ivec2.y));
let update_y = move |input: &NumberInput| TaggedValue::IVec2(IVec2::new(ivec2.x, input.value.unwrap() as i32));
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
NumberInput::new(Some(ivec2.x as f64))
.int()
.label(x)
.unit(unit)
.min(min.unwrap_or(-((1_u64 << f64::MANTISSA_DIGITS) as f64)))
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(update_value(update_x, node_id, index))
.on_commit(commit_value)
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(ivec2.y as f64))
.int()
.label(y)
.unit(unit)
.min(min.unwrap_or(-((1_u64 << f64::MANTISSA_DIGITS) as f64)))
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(update_value(update_y, node_id, index))
.on_commit(commit_value)
.widget_holder(),
]);
}
Some(&TaggedValue::UVec2(uvec2)) => {
let update_x = move |input: &NumberInput| TaggedValue::UVec2(UVec2::new(input.value.unwrap() as u32, uvec2.y));
let update_y = move |input: &NumberInput| TaggedValue::UVec2(UVec2::new(uvec2.x, input.value.unwrap() as u32));
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
NumberInput::new(Some(uvec2.x as f64))
.int()
.label(x)
.unit(unit)
.min(min.unwrap_or(0.))
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(update_value(update_x, node_id, index))
.on_commit(commit_value)
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(uvec2.y as f64))
.int()
.label(y)
.unit(unit)
.min(min.unwrap_or(0.))
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(update_value(update_y, node_id, index))
.on_commit(commit_value)
.widget_holder(),
]);
}
_ => {}
}
LayoutGroup::Row { widgets }
@ -360,7 +385,11 @@ fn vec_f64_input(document_node: &DocumentNode, node_id: NodeId, index: usize, na
.map(TaggedValue::VecF64)
};
if let Some(TaggedValue::VecF64(x)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(TaggedValue::VecF64(x)) = &input.as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
text_props
@ -385,7 +414,11 @@ fn vec_dvec2_input(document_node: &DocumentNode, node_id: NodeId, index: usize,
.map(TaggedValue::VecDVec2)
};
if let Some(TaggedValue::VecDVec2(x)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(TaggedValue::VecDVec2(x)) = &input.as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
text_props
@ -403,7 +436,11 @@ fn font_inputs(document_node: &DocumentNode, node_id: NodeId, index: usize, name
let from_font_input = |font: &FontInput| TaggedValue::Font(Font::new(font.font_family.clone(), font.font_style.clone()));
if let Some(TaggedValue::Font(font)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return (vec![], None);
};
if let Some(TaggedValue::Font(font)) = &input.as_non_exposed_value() {
first_widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
FontInput::new(font.font_family.clone(), font.font_style.clone())
@ -439,32 +476,41 @@ fn vector_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, na
fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, number_props: NumberInput, blank_assist: bool) -> Vec<WidgetHolder> {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Number, blank_assist);
if let Some(&TaggedValue::F64(x)) = document_node.inputs[index].as_non_exposed_value() {
widgets.extend_from_slice(&[
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
match input.as_non_exposed_value() {
Some(&TaggedValue::F64(x)) => widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
number_props
.value(Some(x))
.on_update(update_value(move |x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, index))
.on_commit(commit_value)
.widget_holder(),
])
} else if let Some(&TaggedValue::U32(x)) = document_node.inputs[index].as_non_exposed_value() {
widgets.extend_from_slice(&[
]),
Some(&TaggedValue::U32(x)) => widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
number_props
.value(Some(x as f64))
.on_update(update_value(move |x: &NumberInput| TaggedValue::U32((x.value.unwrap()) as u32), node_id, index))
.on_commit(commit_value)
.widget_holder(),
])
]),
_ => {}
}
widgets
}
// TODO: Generalize this instead of using a separate function per dropdown menu enum
fn color_channel(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(&TaggedValue::RedGreenBlue(mode)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(&TaggedValue::RedGreenBlue(mode)) = &input.as_non_exposed_value() {
let calculation_modes = [RedGreenBlue::Red, RedGreenBlue::Green, RedGreenBlue::Blue];
let mut entries = Vec::with_capacity(calculation_modes.len());
for method in calculation_modes {
@ -487,7 +533,11 @@ fn color_channel(document_node: &DocumentNode, node_id: NodeId, index: usize, na
fn rgba_channel(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(&TaggedValue::RedGreenBlueAlpha(mode)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(&TaggedValue::RedGreenBlueAlpha(mode)) = &input.as_non_exposed_value() {
let calculation_modes = [RedGreenBlueAlpha::Red, RedGreenBlueAlpha::Green, RedGreenBlueAlpha::Blue, RedGreenBlueAlpha::Alpha];
let mut entries = Vec::with_capacity(calculation_modes.len());
for method in calculation_modes {
@ -511,7 +561,11 @@ fn rgba_channel(document_node: &DocumentNode, node_id: NodeId, index: usize, nam
// TODO: Generalize this instead of using a separate function per dropdown menu enum
fn noise_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(&TaggedValue::NoiseType(noise_type)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(&TaggedValue::NoiseType(noise_type)) = &input.as_non_exposed_value() {
let entries = NoiseType::list()
.iter()
.map(|noise_type| {
@ -533,7 +587,11 @@ fn noise_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name:
// TODO: Generalize this instead of using a separate function per dropdown menu enum
fn fractal_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(&TaggedValue::FractalType(fractal_type)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(&TaggedValue::FractalType(fractal_type)) = &input.as_non_exposed_value() {
let entries = FractalType::list()
.iter()
.map(|fractal_type| {
@ -555,7 +613,11 @@ fn fractal_type(document_node: &DocumentNode, node_id: NodeId, index: usize, nam
// TODO: Generalize this instead of using a separate function per dropdown menu enum
fn cellular_distance_function(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(&TaggedValue::CellularDistanceFunction(cellular_distance_function)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(&TaggedValue::CellularDistanceFunction(cellular_distance_function)) = &input.as_non_exposed_value() {
let entries = CellularDistanceFunction::list()
.iter()
.map(|cellular_distance_function| {
@ -580,7 +642,11 @@ fn cellular_distance_function(document_node: &DocumentNode, node_id: NodeId, ind
// TODO: Generalize this instead of using a separate function per dropdown menu enum
fn cellular_return_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(&TaggedValue::CellularReturnType(cellular_return_type)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(&TaggedValue::CellularReturnType(cellular_return_type)) = &input.as_non_exposed_value() {
let entries = CellularReturnType::list()
.iter()
.map(|cellular_return_type| {
@ -602,7 +668,11 @@ fn cellular_return_type(document_node: &DocumentNode, node_id: NodeId, index: us
// TODO: Generalize this instead of using a separate function per dropdown menu enum
fn domain_warp_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(&TaggedValue::DomainWarpType(domain_warp_type)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(&TaggedValue::DomainWarpType(domain_warp_type)) = &input.as_non_exposed_value() {
let entries = DomainWarpType::list()
.iter()
.map(|domain_warp_type| {
@ -624,7 +694,11 @@ fn domain_warp_type(document_node: &DocumentNode, node_id: NodeId, index: usize,
// TODO: Generalize this instead of using a separate function per dropdown menu enum
fn blend_mode(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(&TaggedValue::BlendMode(blend_mode)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(&TaggedValue::BlendMode(blend_mode)) = &input.as_non_exposed_value() {
let entries = BlendMode::list_svg_subset()
.iter()
.map(|category| {
@ -653,7 +727,11 @@ fn blend_mode(document_node: &DocumentNode, node_id: NodeId, index: usize, name:
// TODO: Generalize this for all dropdowns (also see blend_mode and channel_extration)
fn luminance_calculation(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(&TaggedValue::LuminanceCalculation(calculation)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(&TaggedValue::LuminanceCalculation(calculation)) = &input.as_non_exposed_value() {
let calculation_modes = LuminanceCalculation::list();
let mut entries = Vec::with_capacity(calculation_modes.len());
for method in calculation_modes {
@ -677,7 +755,11 @@ fn luminance_calculation(document_node: &DocumentNode, node_id: NodeId, index: u
fn boolean_operation_radio_buttons(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(&TaggedValue::BooleanOperation(calculation)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(&TaggedValue::BooleanOperation(calculation)) = &input.as_non_exposed_value() {
let operations = BooleanOperation::list();
let icons = BooleanOperation::icons();
let mut entries = Vec::with_capacity(operations.len());
@ -702,7 +784,11 @@ fn boolean_operation_radio_buttons(document_node: &DocumentNode, node_id: NodeId
fn line_cap_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(&TaggedValue::LineCap(line_cap)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(&TaggedValue::LineCap(line_cap)) = &input.as_non_exposed_value() {
let entries = [("Butt", LineCap::Butt), ("Round", LineCap::Round), ("Square", LineCap::Square)]
.into_iter()
.map(|(name, val)| {
@ -723,7 +809,11 @@ fn line_cap_widget(document_node: &DocumentNode, node_id: NodeId, index: usize,
fn line_join_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(&TaggedValue::LineJoin(line_join)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(&TaggedValue::LineJoin(line_join)) = &input.as_non_exposed_value() {
let entries = [("Miter", LineJoin::Miter), ("Bevel", LineJoin::Bevel), ("Round", LineJoin::Round)]
.into_iter()
.map(|(name, val)| {
@ -742,7 +832,7 @@ fn line_join_widget(document_node: &DocumentNode, node_id: NodeId, index: usize,
LayoutGroup::Row { widgets }
}
fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, color_props: ColorButton, blank_assist: bool) -> LayoutGroup {
fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, color_button: ColorButton, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
// Return early with just the label if the input is exposed to the graph, meaning we don't want to show the color picker widget in the Properties panel
@ -754,14 +844,14 @@ fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, nam
match &**tagged_value {
TaggedValue::Color(color) => widgets.push(
color_props
color_button
.value(FillChoice::Solid(*color))
.on_update(update_value(|x: &ColorButton| TaggedValue::Color(x.value.as_solid().unwrap_or_default()), node_id, index))
.on_commit(commit_value)
.widget_holder(),
),
TaggedValue::OptionalColor(color) => widgets.push(
color_props
color_button
.value(match color {
Some(color) => FillChoice::Solid(*color),
None => FillChoice::None,
@ -771,7 +861,7 @@ fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, nam
.widget_holder(),
),
TaggedValue::GradientStops(ref x) => widgets.push(
color_props
color_button
.value(FillChoice::Gradient(x.clone()))
.on_update(update_value(
|x: &ColorButton| TaggedValue::GradientStops(x.value.as_gradient().cloned().unwrap_or_default()),
@ -790,7 +880,11 @@ fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, nam
fn curves_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let Some(TaggedValue::Curve(curve)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(TaggedValue::Curve(curve)) = &input.as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
CurveInput::new(curve.clone())
@ -804,7 +898,11 @@ fn curves_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, na
fn centroid_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, "Centroid Type", FrontendGraphDataType::General, true);
if let Some(&TaggedValue::CentroidType(centroid_type)) = &document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(&TaggedValue::CentroidType(centroid_type)) = &input.as_non_exposed_value() {
let entries = vec![
RadioEntryData::new("area")
.label("Area")
@ -902,7 +1000,7 @@ pub fn color_properties(document_node: &DocumentNode, node_id: NodeId, _context:
}
pub fn load_image_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let url = text_widget(document_node, node_id, 1, "Url", true);
let url = text_widget(document_node, node_id, 1, "URL", true);
vec![LayoutGroup::Row { widgets: url }]
}
@ -1137,40 +1235,63 @@ pub fn threshold_properties(document_node: &DocumentNode, node_id: NodeId, _cont
}
pub fn gradient_map_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let gradient_input = 1;
let reverse_input = 2;
let gradient_index = 1;
let reverse_index = 2;
let gradient = if let Some(TaggedValue::GradientStops(gradient)) = &document_node.inputs[gradient_input].as_value() {
gradient.clone()
let gradient_row = color_widget(document_node, node_id, gradient_index, "Gradient", ColorButton::default().allow_none(false), true);
let reverse_row = bool_widget(document_node, node_id, reverse_index, "Reverse", true);
vec![gradient_row, LayoutGroup::Row { widgets: reverse_row }]
}
pub fn assign_colors_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let fill_index = 1;
let stroke_index = 2;
let gradient_index = 3;
let reverse_index = 4;
let randomize_index = 5;
let repeat_every_index = 6;
let fill_row = bool_widget(document_node, node_id, fill_index, "Fill", true);
let stroke_row = bool_widget(document_node, node_id, stroke_index, "Stroke", true);
let gradient_row = color_widget(document_node, node_id, gradient_index, "Gradient", ColorButton::default().allow_none(false), true);
let reverse_row = bool_widget(document_node, node_id, reverse_index, "Reverse", true);
let randomize_row = bool_widget(document_node, node_id, randomize_index, "Randomize", true);
let randomize_enabled = if let Some(&TaggedValue::Bool(randomize_enabled)) = &document_node.inputs[randomize_index].as_value() {
randomize_enabled
} else {
return vec![LayoutGroup::Row { widgets: vec![] }];
false
};
let mut gradient_row = vec![TextLabel::new("Gradient").widget_holder()];
add_blank_assist(&mut gradient_row);
gradient_row.extend([
Separator::new(SeparatorType::Unrelated).widget_holder(),
ColorButton::default()
.allow_none(false)
.value(FillChoice::Gradient(gradient))
.on_update(move |x: &ColorButton| {
NodeGraphMessage::SetInputValue {
node_id,
input_index: gradient_input,
value: TaggedValue::GradientStops(x.value.as_gradient().unwrap().clone()),
}
.into()
})
.on_commit(commit_value)
.widget_holder(),
]);
let repeat_every_row = number_widget(
document_node,
node_id,
repeat_every_index,
"Repeat Every",
NumberInput::default().min(0.).int().disabled(randomize_enabled),
true,
);
let reverse_row = bool_widget(document_node, node_id, reverse_input, "Reverse", true);
vec![LayoutGroup::Row { widgets: gradient_row }, LayoutGroup::Row { widgets: reverse_row }]
vec![
LayoutGroup::Row { widgets: fill_row },
LayoutGroup::Row { widgets: stroke_row },
gradient_row,
LayoutGroup::Row { widgets: reverse_row },
LayoutGroup::Row { widgets: randomize_row },
LayoutGroup::Row { widgets: repeat_every_row },
]
}
pub fn vibrance_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let vibrance = number_widget(document_node, node_id, 1, "Vibrance", NumberInput::default().mode_range().min(-100.).max(100.).unit("%"), true);
let vibrance_index = 1;
let vibrance = number_widget(
document_node,
node_id,
vibrance_index,
"Vibrance",
NumberInput::default().mode_range().min(-100.).max(100.).unit("%"),
true,
);
vec![LayoutGroup::Row { widgets: vibrance }]
}
@ -1189,7 +1310,12 @@ pub fn channel_mixer_properties(document_node: &DocumentNode, node_id: NodeId, _
let output_channel_index = 18;
let mut output_channel = vec![TextLabel::new("Output Channel").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()];
add_blank_assist(&mut output_channel);
if let Some(&TaggedValue::RedGreenBlue(choice)) = &document_node.inputs[output_channel_index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(output_channel_index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(&TaggedValue::RedGreenBlue(choice)) = input.as_non_exposed_value() {
let entries = vec![
RadioEntryData::new(format!("{:?}", RedGreenBlue::Red))
.label(RedGreenBlue::Red.to_string())
@ -1206,6 +1332,7 @@ pub fn channel_mixer_properties(document_node: &DocumentNode, node_id: NodeId, _
];
output_channel.extend([RadioInput::new(entries).selected_index(Some(choice as u32)).widget_holder()]);
};
let is_output_channel = if let Some(&TaggedValue::RedGreenBlue(choice)) = &document_node.inputs[output_channel_index].as_value() {
choice
} else {
@ -1274,7 +1401,12 @@ pub fn selective_color_properties(document_node: &DocumentNode, node_id: NodeId,
let colors_index = 38;
let mut colors = vec![TextLabel::new("Colors").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()];
add_blank_assist(&mut colors);
if let Some(&TaggedValue::SelectiveColorChoice(choice)) = &document_node.inputs[colors_index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(colors_index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(&TaggedValue::SelectiveColorChoice(choice)) = input.as_non_exposed_value() {
use SelectiveColorChoice::*;
let entries = [[Reds, Yellows, Greens, Cyans, Blues, Magentas].as_slice(), [Whites, Neutrals, Blacks].as_slice()]
.into_iter()
@ -1291,7 +1423,8 @@ pub fn selective_color_properties(document_node: &DocumentNode, node_id: NodeId,
})
.collect();
colors.extend([DropdownInput::new(entries).selected_index(Some(choice as u32)).widget_holder()]);
};
}
let colors_choice_index = if let Some(&TaggedValue::SelectiveColorChoice(choice)) = &document_node.inputs[colors_index].as_value() {
choice
} else {
@ -1320,7 +1453,12 @@ pub fn selective_color_properties(document_node: &DocumentNode, node_id: NodeId,
let mode_index = 1;
let mut mode = start_widgets(document_node, node_id, mode_index, "Mode", FrontendGraphDataType::General, true);
mode.push(Separator::new(SeparatorType::Unrelated).widget_holder());
if let Some(&TaggedValue::RelativeAbsolute(relative_or_absolute)) = &document_node.inputs[mode_index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(mode_index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(&TaggedValue::RelativeAbsolute(relative_or_absolute)) = &input.as_non_exposed_value() {
let entries = vec![
RadioEntryData::new("relative")
.label("Relative")
@ -1490,14 +1628,22 @@ pub fn rectangle_properties(document_node: &DocumentNode, node_id: NodeId, _cont
corner_radius_row_2.push(TextLabel::new("").widget_holder());
add_blank_assist(&mut corner_radius_row_2);
if let Some(&TaggedValue::Bool(is_individual)) = &document_node.inputs[corner_rounding_type_index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(corner_rounding_type_index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(&TaggedValue::Bool(is_individual)) = &input.as_non_exposed_value() {
// Values
let uniform_val = match document_node.inputs[corner_radius_index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(corner_radius_index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
let uniform_val = match input.as_non_exposed_value() {
Some(TaggedValue::F64(x)) => *x,
Some(TaggedValue::F64Array4(x)) => x[0],
_ => 0.,
};
let individual_val = match document_node.inputs[corner_radius_index].as_non_exposed_value() {
let individual_val = match input.as_non_exposed_value() {
Some(&TaggedValue::F64Array4(x)) => x,
Some(&TaggedValue::F64(x)) => [x; 4],
_ => [0.; 4],
@ -1624,7 +1770,11 @@ pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _cont
let mut widgets = start_widgets(document_node, node_id, index, "Rotation", FrontendGraphDataType::Number, true);
if let Some(&TaggedValue::F64(val)) = document_node.inputs[index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(&TaggedValue::F64(val)) = input.as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
NumberInput::new(Some(val.to_degrees()))
@ -1839,7 +1989,11 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte
let seed = {
let mut widgets = start_widgets(document_node, node_id, seed_index, "Seed", FrontendGraphDataType::Number, false);
if let Some(&TaggedValue::F64(seed)) = &document_node.inputs[seed_index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(seed_index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(&TaggedValue::F64(seed)) = &input.as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
IconButton::new("Regenerate", 24)
@ -1906,7 +2060,11 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte
DVec2::new(x as f64, y as f64)
};
if let Some(&TaggedValue::OptionalDVec2(vec2)) = &document_node.inputs[resolution_index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(resolution_index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(&TaggedValue::OptionalDVec2(vec2)) = &input.as_non_exposed_value() {
let dimensions_is_auto = vec2.is_none();
let vec2 = vec2.unwrap_or_else(|| round((image_size.0 as f64, image_size.1 as f64).into()));
@ -1985,7 +2143,11 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte
let sampling_method = {
let mut widgets = start_widgets(document_node, node_id, sampling_method_index, "Sampling Method", FrontendGraphDataType::General, true);
if let Some(&TaggedValue::ImaginateSamplingMethod(sampling_method)) = &document_node.inputs[sampling_method_index].as_non_exposed_value() {
let Some(input) = document_node.inputs.get(sampling_method_index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(&TaggedValue::ImaginateSamplingMethod(sampling_method)) = &input.as_non_exposed_value() {
let sampling_methods = ImaginateSamplingMethod::list();
let mut entries = Vec::with_capacity(sampling_methods.len());
for method in sampling_methods {

View file

@ -55,6 +55,22 @@ impl core::hash::Hash for GraphicGroup {
}
}
impl GraphicGroup {
pub const EMPTY: Self = Self {
elements: Vec::new(),
transform: DAffine2::IDENTITY,
alpha_blending: AlphaBlending::new(),
};
pub fn new(elements: Vec<GraphicElement>) -> Self {
Self {
elements,
transform: DAffine2::IDENTITY,
alpha_blending: AlphaBlending::new(),
}
}
}
/// The possible forms of graphical content held in a Vec by the `elements` field of [`GraphicElement`].
/// Can be another recursively nested [`GraphicGroup`], a [`VectorData`] shape, an [`ImageFrame`], or an [`Artboard`].
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
@ -74,6 +90,50 @@ impl Default for GraphicElement {
}
}
impl GraphicElement {
pub fn as_group(&self) -> Option<&GraphicGroup> {
match self {
GraphicElement::GraphicGroup(group) => Some(group),
_ => None,
}
}
pub fn as_group_mut(&mut self) -> Option<&mut GraphicGroup> {
match self {
GraphicElement::GraphicGroup(group) => Some(group),
_ => None,
}
}
pub fn as_vector_data(&self) -> Option<&VectorData> {
match self {
GraphicElement::VectorData(data) => Some(data),
_ => None,
}
}
pub fn as_vector_data_mut(&mut self) -> Option<&mut VectorData> {
match self {
GraphicElement::VectorData(data) => Some(data),
_ => None,
}
}
pub fn as_raster(&self) -> Option<&Raster> {
match self {
GraphicElement::Raster(raster) => Some(raster),
_ => None,
}
}
pub fn as_raster_mut(&mut self) -> Option<&mut Raster> {
match self {
GraphicElement::Raster(raster) => Some(raster),
_ => None,
}
}
}
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
pub enum Raster {
/// A bitmap image with a finite position and extent, equivalent to the SVG <image> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image
@ -301,11 +361,3 @@ where
}
}
}
impl GraphicGroup {
pub const EMPTY: Self = Self {
elements: Vec::new(),
transform: DAffine2::IDENTITY,
alpha_blending: AlphaBlending::new(),
};
}

View file

@ -60,6 +60,10 @@ impl GradientStops {
Color::BLACK
}
pub fn reversed(&self) -> Self {
Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect())
}
}
/// A gradient fill.

View file

@ -1,5 +1,5 @@
use super::misc::CentroidType;
use super::style::{Fill, Stroke};
use super::style::{Fill, GradientStops, Stroke};
use super::{PointId, SegmentId, StrokeId, VectorData};
use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, Transform, TransformMut};
@ -9,6 +9,78 @@ use bezier_rs::{Cap, Join, Subpath, SubpathTValue, TValue};
use glam::{DAffine2, DVec2};
use rand::{Rng, SeedableRng};
#[derive(Debug, Clone, Copy)]
pub struct AssignColorsNode<Fill, Stroke, Gradient, Reverse, Randomize, RepeatEvery> {
fill: Fill,
stroke: Stroke,
gradient: Gradient,
reverse: Reverse,
randomize: Randomize,
repeat_every: RepeatEvery,
}
#[node_macro::node_fn(AssignColorsNode)]
fn assign_colors_node(group: GraphicGroup, fill: bool, stroke: bool, gradient: GradientStops, reverse: bool, randomize: bool, repeat_every: u32) -> GraphicGroup {
let mut group = group;
let vector_data_list: Vec<_> = group.iter_mut().filter_map(|element| element.as_vector_data_mut()).collect();
let list = (vector_data_list.len(), vector_data_list.into_iter());
assign_colors(list, fill, stroke, gradient, reverse, randomize, repeat_every);
group
}
#[node_macro::node_impl(AssignColorsNode)]
fn assign_colors_node(vector_data: VectorData, fill: bool, stroke: bool, gradient: GradientStops, reverse: bool, randomize: bool, repeat_every: u32) -> GraphicGroup {
let mut vector_data_list: Vec<_> = vector_data
.region_bezier_paths()
.map(|(_, subpath)| {
let mut vector = VectorData::from_subpath(subpath);
vector.style = vector_data.style.clone();
crate::GraphicElement::VectorData(Box::new(vector))
})
.collect();
let list = (vector_data_list.len(), vector_data_list.iter_mut().map(|element| element.as_vector_data_mut().unwrap()));
assign_colors(list, fill, stroke, gradient, reverse, randomize, repeat_every);
let mut group = GraphicGroup::new(vector_data_list);
group.transform = vector_data.transform;
group.alpha_blending = vector_data.alpha_blending;
group
}
fn assign_colors<'a>((length, vector_data): (usize, impl Iterator<Item = &'a mut VectorData>), fill: bool, stroke: bool, gradient: GradientStops, reverse: bool, randomize: bool, repeat_every: u32) {
let gradient = if reverse { gradient.reversed() } else { gradient };
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
for (i, vector_data) in vector_data.enumerate() {
let factor = match randomize {
true => rng.gen::<f64>(),
false => match repeat_every {
0 => i as f64 / (length - 1) as f64,
1 => 0.,
_ => i as f64 % repeat_every as f64 / (repeat_every - 1) as f64,
},
};
let color = gradient.evalute(factor);
if fill {
vector_data.style.set_fill(Fill::Solid(color));
}
if stroke {
if let Some(stroke) = vector_data.style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) {
vector_data.style.set_stroke(stroke);
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SetFillNode<Fill> {
fill: Fill,

View file

@ -679,6 +679,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
register_node!(graphene_core::vector::SetFillNode<_>, input: VectorData, params: [Color]),
register_node!(graphene_core::vector::SetFillNode<_>, input: VectorData, params: [Option<Color>]),
register_node!(graphene_core::vector::SetFillNode<_>, input: VectorData, params: [graphene_std::vector::style::Gradient]),
register_node!(graphene_core::vector::AssignColorsNode<_, _, _, _, _, _>, input: GraphicGroup, params: [bool, bool, graphene_std::vector::style::GradientStops, bool, bool, u32]),
register_node!(graphene_core::vector::AssignColorsNode<_, _, _, _, _, _>, input: VectorData, params: [bool, bool, graphene_std::vector::style::GradientStops, bool, bool, u32]),
register_node!(graphene_core::vector::SetStrokeNode<_, _, _, _, _, _, _>, input: VectorData, params: [Option<graphene_core::Color>, f64, Vec<f64>, f64, graphene_core::vector::style::LineCap, graphene_core::vector::style::LineJoin, f64]),
register_node!(graphene_core::vector::RepeatNode<_, _, _>, input: VectorData, params: [DVec2, f64, u32]),
register_node!(graphene_core::vector::BoundingBoxNode, input: VectorData, params: []),