mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
* Rework Gradient into a newtype of List<Color> with optional position and midpoint attributes * Fix Vello stopless-gradient fallback coverage, empty legacy gradient tables, the node docs gradient swatch, NaN position elision, and wired setter input overwrites
257 lines
8.0 KiB
Rust
257 lines
8.0 KiB
Rust
use crate::utility::*;
|
|
use convert_case::{Case, Casing};
|
|
use graph_craft::concrete;
|
|
use graph_craft::document::value;
|
|
use graph_craft::proto::{NodeMetadata, RegistryValueSource};
|
|
use graphene_std::{ContextDependencies, core_types};
|
|
use indoc::formatdoc;
|
|
use std::collections::HashSet;
|
|
use std::io::Write;
|
|
|
|
pub fn write_node_page(index: usize, id: &core_types::ProtoNodeIdentifier, metadata: &NodeMetadata, category_path: &String) {
|
|
let node_registry = core_types::registry::NODE_REGISTRY.lock().unwrap();
|
|
let Some(implementations) = node_registry.get(id) else { return };
|
|
|
|
// Path to page
|
|
let name_url_part = sanitize_path(&metadata.display_name.to_case(Case::Kebab));
|
|
let page_path = format!("{category_path}/{name_url_part}.md");
|
|
let mut page = std::fs::File::create(&page_path).expect("Failed to create node page file");
|
|
|
|
// Context features
|
|
let context_features = &metadata.context_features;
|
|
let context_dependencies: ContextDependencies = context_features.as_slice().into();
|
|
|
|
// Input types
|
|
let mut valid_input_types = vec![Vec::new(); metadata.fields.len()];
|
|
for (_, node_io) in implementations.iter() {
|
|
for (i, ty) in node_io.inputs.iter().enumerate() {
|
|
valid_input_types[i].push(ty.nested_type().clone());
|
|
}
|
|
}
|
|
for item in valid_input_types.iter_mut() {
|
|
// Dedupe while preserving order
|
|
let mut found = HashSet::new();
|
|
*item = item.clone().into_iter().filter(|s| found.insert(s.clone())).collect::<Vec<_>>()
|
|
}
|
|
|
|
// Primary output types
|
|
let valid_primary_outputs = implementations.iter().map(|(_, node_io)| node_io.return_value.nested_type().clone()).collect::<Vec<_>>();
|
|
|
|
// Write sections to the file
|
|
write_frontmatter(&mut page, metadata, index + 1);
|
|
write_description(&mut page, metadata);
|
|
write_interface_header(&mut page);
|
|
write_context(&mut page, context_dependencies);
|
|
write_inputs(&mut page, &valid_input_types, metadata);
|
|
write_outputs(&mut page, &valid_primary_outputs);
|
|
}
|
|
|
|
fn write_frontmatter(page: &mut std::fs::File, metadata: &NodeMetadata, order: usize) {
|
|
let name = metadata.display_name;
|
|
|
|
let content = formatdoc!(
|
|
"
|
|
+++
|
|
title = \"{name}\"
|
|
|
|
[extra]
|
|
order = {order}
|
|
css = [\"/page/user-manual/node.css\"]
|
|
+++
|
|
"
|
|
);
|
|
page.write_all(content.as_bytes()).expect("Failed to write to node page file");
|
|
}
|
|
|
|
fn write_description(page: &mut std::fs::File, metadata: &NodeMetadata) {
|
|
let description = node_description(metadata);
|
|
|
|
let content = formatdoc!(
|
|
"
|
|
|
|
{description}
|
|
"
|
|
);
|
|
page.write_all(content.as_bytes()).expect("Failed to write to node page file");
|
|
}
|
|
|
|
fn write_interface_header(page: &mut std::fs::File) {
|
|
let content = formatdoc!(
|
|
"
|
|
|
|
## Interface
|
|
"
|
|
);
|
|
page.write_all(content.as_bytes()).expect("Failed to write to node page file");
|
|
}
|
|
|
|
fn write_context(page: &mut std::fs::File, context_dependencies: ContextDependencies) {
|
|
let extract = context_dependencies.extract;
|
|
let inject = context_dependencies.inject;
|
|
if !extract.is_empty() || !inject.is_empty() {
|
|
let mut context_features = "| | |\n|:-|:-|".to_string();
|
|
if !extract.is_empty() {
|
|
let names = extract.iter().map(|ty| format!("`{}`", ty.name())).collect::<Vec<_>>().join("<br />");
|
|
context_features.push_str(&format!("\n| **Reads** | {names} |"));
|
|
}
|
|
if !inject.is_empty() {
|
|
let names = inject.iter().map(|ty| format!("`{}`", ty.name())).collect::<Vec<_>>().join("<br />");
|
|
context_features.push_str(&format!("\n| **Sets** | {names} |"));
|
|
}
|
|
|
|
let content = formatdoc!(
|
|
"
|
|
|
|
### Context
|
|
|
|
{context_features}
|
|
"
|
|
);
|
|
page.write_all(content.as_bytes()).expect("Failed to write to node page file");
|
|
};
|
|
}
|
|
|
|
fn write_inputs(page: &mut std::fs::File, valid_input_types: &[Vec<core_types::Type>], metadata: &NodeMetadata) {
|
|
let rows = metadata
|
|
.fields
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|&(index, field)| !field.hidden || index == 0)
|
|
.map(|(index, field)| {
|
|
// Parameter
|
|
let parameter = field.name;
|
|
|
|
// Possible types
|
|
let possible_types_list = valid_input_types.get(index).cloned().unwrap_or_default();
|
|
if index == 0 && possible_types_list.as_slice() == [concrete!(())] {
|
|
return "| - | *No Primary Input* | - |".to_string();
|
|
}
|
|
let mut possible_types = possible_types_list.iter().map(|ty| format!("`{ty}`")).collect::<Vec<_>>();
|
|
possible_types.sort();
|
|
possible_types.dedup();
|
|
let mut possible_types = possible_types.join("<br />");
|
|
if possible_types.is_empty() {
|
|
possible_types = "*Any Type*".to_string();
|
|
}
|
|
|
|
// Details: description
|
|
let mut details = field
|
|
.description
|
|
.trim()
|
|
.split('\n')
|
|
.filter(|line| !line.is_empty())
|
|
.map(|line| format!("<p>{}</p>", line.trim()))
|
|
.collect::<Vec<_>>();
|
|
|
|
// Details: primary input
|
|
if index == 0 {
|
|
details.push("<p>*Primary Input*</p>".to_string());
|
|
}
|
|
|
|
// Details: exposed by default
|
|
if field.exposed {
|
|
details.push("<p>*Exposed to the Graph by Default*</p>".to_string());
|
|
}
|
|
|
|
// Details: sourced from scope
|
|
if let RegistryValueSource::Scope(scope_name) = &field.value_source {
|
|
details.push(format!("<p>*Sourced From Scope: `{scope_name}`*</p>"));
|
|
}
|
|
|
|
// Details: default value
|
|
let default_value = match &field.value_source {
|
|
RegistryValueSource::Default(default_value) => Some(default_value.to_string().replace(" :: ", "::")),
|
|
_ => field
|
|
.default_type
|
|
.as_ref()
|
|
.or(match possible_types_list.as_slice() {
|
|
[single] => Some(single),
|
|
_ => None,
|
|
})
|
|
.and_then(|ty| value::TaggedValue::from_type(ty.nested_type()))
|
|
.map(|ty| ty.to_debug_string()),
|
|
};
|
|
if index > 0
|
|
&& !field.exposed
|
|
&& let Some(default_value) = default_value
|
|
{
|
|
let default_value = default_value.trim_end_matches('.').trim_end_matches(".0"); // Display whole-number floats as integers
|
|
|
|
let default_value = match field.default_colors {
|
|
Some(colors) => color_swatch(colors),
|
|
None => format!("`{default_value}{}`", field.unit.unwrap_or_default()),
|
|
};
|
|
|
|
details.push(format!("<p>*Default:* {default_value}</p>"));
|
|
}
|
|
|
|
// Construct the table row
|
|
let details = details.join("");
|
|
format!("| {parameter} | {details} | {possible_types} |")
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
if !rows.is_empty() {
|
|
let content = formatdoc!(
|
|
"
|
|
|
|
### Inputs
|
|
|
|
| Parameter | Details | Possible Types |
|
|
|:-|:-|:-|
|
|
{rows}
|
|
"
|
|
);
|
|
page.write_all(content.as_bytes()).expect("Failed to write to node page file");
|
|
}
|
|
}
|
|
|
|
/// A bordered swatch painted with one color, or with several as the stops of a left-to-right gradient.
|
|
fn color_swatch(colors: &[core_types::Color]) -> String {
|
|
let hex: Vec<String> = colors.iter().map(|&color| format!("#{}", core_types::color::SRGBA8::from(color).to_rgba_hex())).collect();
|
|
let background = match hex.as_slice() {
|
|
[single] => single.clone(),
|
|
multiple => format!("linear-gradient(to right, {})", multiple.join(", ")),
|
|
};
|
|
|
|
format!(r#"<span style="padding-right: 100px; border: 2px solid var(--color-fog); background: {background}"></span>"#)
|
|
}
|
|
|
|
fn write_outputs(page: &mut std::fs::File, valid_primary_outputs: &[core_types::Type]) {
|
|
// Product
|
|
let product = "Result";
|
|
|
|
// Details: description
|
|
let details = "The value produced by the node operation.";
|
|
let mut details = format!("<p>{details}</p>");
|
|
|
|
// Details: primary output
|
|
details.push_str("<p>*Primary Output*</p>");
|
|
|
|
// Possible types
|
|
let valid_primary_outputs = valid_primary_outputs.iter().map(|ty| format!("`{ty}`")).collect::<Vec<_>>();
|
|
let valid_primary_outputs = {
|
|
// Dedupe while preserving order
|
|
let mut found = HashSet::new();
|
|
valid_primary_outputs.into_iter().filter(|s| found.insert(s.clone())).collect::<Vec<_>>()
|
|
};
|
|
let valid_primary_outputs = {
|
|
// Dedupe while preserving order
|
|
let mut found = HashSet::new();
|
|
valid_primary_outputs.into_iter().filter(|s| found.insert(s.clone())).collect::<Vec<_>>()
|
|
};
|
|
let valid_primary_outputs = valid_primary_outputs.join("<br />");
|
|
|
|
let content = formatdoc!(
|
|
"
|
|
|
|
### Outputs
|
|
|
|
| Product | Details | Possible Types |
|
|
|:-|:-|:-|
|
|
| {product} | {details} | {valid_primary_outputs} |
|
|
"
|
|
);
|
|
page.write_all(content.as_bytes()).expect("Failed to write to node page file");
|
|
}
|