feat: add snapToGrid and showGrid settings

This commit is contained in:
max_richter 2024-04-22 16:52:52 +02:00
parent ad197db873
commit 1de0094c85
25 changed files with 290 additions and 99 deletions

28
Cargo.lock generated
View File

@ -159,6 +159,20 @@ dependencies = [
"web-sys",
]
[[package]]
name = "noise"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"macros",
"serde",
"serde-wasm-bindgen",
"utils",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
]
[[package]]
name = "once_cell"
version = "1.19.0"
@ -301,20 +315,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "template"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"macros",
"serde",
"serde-wasm-bindgen",
"utils",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
]
[[package]]
name = "types"
version = "0.1.0"

View File

@ -1,7 +1,12 @@
[workspace]
resolver = "2"
members = ["nodes/max/plantarium/*", "packages/macros", "packages/types", "packages/utils"]
members = [
"nodes/max/plantarium/*",
"packages/macros",
"packages/types",
"packages/utils",
]
exclude = ["nodes/max/plantarium/.template"]
[profile.release]
lto = true

View File

@ -67,27 +67,27 @@ void main(void) {
//extra small grid
float m1 = grid(ux, uy, divisions*4.0, thickness*4.0) * 0.1;
float m2 = grid(ux, uy, divisions*16.0, thickness*16.0) * 0.03;
float m1 = grid(ux, uy, divisions*4.0, thickness*4.0) * 0.9;
float m2 = grid(ux, uy, divisions*16.0, thickness*16.0) * 0.5;
float xsmall = max(m1, m2);
float s3 = circle_grid(ux, uy, cz/1.6, 1.0) * 0.2;
float s3 = circle_grid(ux, uy, cz/1.6, 1.0) * 0.5;
xsmall = max(xsmall, s3);
// small grid
float c1 = grid(ux, uy, divisions, thickness) * 0.2;
float c2 = grid(ux, uy, divisions*2.0, thickness) * 0.1;
float c1 = grid(ux, uy, divisions, thickness) * 0.6;
float c2 = grid(ux, uy, divisions*2.0, thickness) * 0.5;
float small = max(c1, c2);
float s1 = circle_grid(ux, uy, cz*10.0, 2.0) * 0.2;
float s1 = circle_grid(ux, uy, cz*10.0, 2.0) * 0.5;
small = max(small, s1);
// large grid
float c3 = grid(ux, uy, divisions/8.0, thickness/8.0) * 0.1;
float c4 = grid(ux, uy, divisions/2.0, thickness/4.0) * 0.05;
float c3 = grid(ux, uy, divisions/8.0, thickness/8.0) * 0.5;
float c4 = grid(ux, uy, divisions/2.0, thickness/4.0) * 0.4;
float large = max(c3, c4);
float s2 = circle_grid(ux, uy, cz*20.0, 1.0) * 0.2;
float s2 = circle_grid(ux, uy, cz*20.0, 1.0) * 0.4;
large = max(large, s2);
float c = mix(large, small, min(nz*2.0+0.05, 1.0));

View File

@ -53,7 +53,7 @@
}}
uniforms.camPos.value={cameraPosition}
uniforms.backgroundColor.value={$colors.layer0}
uniforms.lineColor.value={$colors.layer2}
uniforms.lineColor.value={$colors.outline}
uniforms.zoomLimits.value={[minZoom, maxZoom]}
uniforms.dimensions.value={[width, height]}
/>

View File

@ -25,6 +25,11 @@
export let graph: GraphManager;
export let settings = {
snapToGrid: true,
showGrid: true,
};
let keymap =
getContext<ReturnType<typeof createKeyMap>>("keymap") || createKeyMap([]);
@ -320,8 +325,10 @@
if (event.ctrlKey) {
const snapLevel = getSnapLevel();
newX = snapToGrid(newX, 5 / snapLevel);
newY = snapToGrid(newY, 5 / snapLevel);
if (settings.snapToGrid) {
newX = snapToGrid(newX, 5 / snapLevel);
newY = snapToGrid(newY, 5 / snapLevel);
}
}
if (!node.tmp.isMoving) {
@ -667,15 +674,20 @@
if (activeNode?.tmp?.isMoving) {
activeNode.tmp = activeNode.tmp || {};
activeNode.tmp.isMoving = false;
const snapLevel = getSnapLevel();
activeNode.position[0] = snapToGrid(
activeNode?.tmp?.x ?? activeNode.position[0],
5 / snapLevel,
);
activeNode.position[1] = snapToGrid(
activeNode?.tmp?.y ?? activeNode.position[1],
5 / snapLevel,
);
if (settings.snapToGrid) {
const snapLevel = getSnapLevel();
activeNode.position[0] = snapToGrid(
activeNode?.tmp?.x ?? activeNode.position[0],
5 / snapLevel,
);
activeNode.position[1] = snapToGrid(
activeNode?.tmp?.y ?? activeNode.position[1],
5 / snapLevel,
);
} else {
activeNode.position[0] = activeNode?.tmp?.x ?? activeNode.position[0];
activeNode.position[1] = activeNode?.tmp?.y ?? activeNode.position[1];
}
const nodes = [
...[...($selectedNodes?.values() || [])].map((id) => graph.getNode(id)),
] as NodeType[];
@ -796,11 +808,9 @@
const wrapper = createWasmWrapper(buffer);
const definition = wrapper.get_definition();
const res = NodeDefinitionSchema.parse(definition);
console.log(wrapper, res);
}
};
reader.readAsArrayBuffer(files[0]);
console.log({ files });
}
}
@ -858,7 +868,9 @@
<Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}>
<Camera bind:camera position={cameraPosition} />
<Background {cameraPosition} {maxZoom} {minZoom} {width} {height} />
{#if settings?.showGrid !== false}
<Background {cameraPosition} {maxZoom} {minZoom} {width} {height} />
{/if}
{#if boxSelection && mouseDown}
<BoxSelection

View File

@ -18,6 +18,9 @@
export const keymap = createKeyMap([]);
setContext("keymap", keymap);
export let showGrid = false;
export let snapToGrid = false;
const updateSettings = debounce((s) => {
manager.setSettings(s);
}, 200);
@ -43,4 +46,4 @@
const dispatch = createEventDispatcher();
</script>
<GraphEl graph={manager} />
<GraphEl graph={manager} settings={{ showGrid, snapToGrid }} />

View File

@ -1,5 +1,5 @@
import type { Graph, NodeRegistry, NodeDefinition, RuntimeExecutor } from "@nodes/types";
import { fastHash, concat_encoded, encodeFloat, encode } from "@nodes/utils"
import { fastHash, concatEncodedArrays, encodeFloat, encodeNestedArray, decodeNestedArray } from "@nodes/utils"
export class MemoryRuntimeExecutor implements RuntimeExecutor {
@ -109,7 +109,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
const sortedNodes = nodes.sort((a, b) => (b.tmp?.depth || 0) - (a.tmp?.depth || 0));
// here we store the intermediate results of the nodes
const results: Record<string, string | boolean | number> = {};
const results: Record<string, Int32Array> = {};
const runSeed = settings["randomSeed"] === true ? Math.floor(Math.random() * 100000000) : 5120983;
@ -192,7 +192,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
}
if (Array.isArray(value)) {
return encode(value);
return encodeNestedArray(value);
}
return value;
@ -204,10 +204,14 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
// console.log(`${a2 - a1}ms TRANSFORMED_INPUTS`);
const _inputs = concat_encoded(transformed_inputs);
const encoded_inputs = concatEncodedArrays(transformed_inputs);
const a3 = performance.now();
// console.log(`executing ${node_type.id || node.id}`, _inputs);
results[node.id] = node_type.execute(_inputs) as number;
console.groupCollapsed(`executing ${node_type.id || node.id}`);
console.log(`Inputs:`, transformed_inputs);
console.log(`Encoded Inputs:`, encoded_inputs);
results[node.id] = node_type.execute(encoded_inputs);
console.log("Result:", decodeNestedArray(results[node.id]));
console.groupEnd();
const duration = performance.now() - a3;
if (duration > 5) {
this.cache[cacheKey] = { eol: Date.now() + 10_000, value: results[node.id] };
@ -217,6 +221,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
const a4 = performance.now();
// console.log(`${a4 - a0}ms e2e duration`);
} catch (e) {
console.groupEnd();
console.error(`Error executing node ${node_type.id || node.id}`, e);
}

View File

@ -1,9 +1,10 @@
import localStore from "$lib/helpers/localStore";
import { label } from "three/examples/jsm/nodes/Nodes.js";
export const AppSettings = localStore("node-settings", {
theme: 0,
showGrid: true,
showNodeGrid: true,
snapToGrid: true,
wireframes: false,
showIndices: false,
});
@ -33,6 +34,16 @@ export const AppSettingTypes = {
label: "Show Grid",
value: true,
},
nodeInterface: {
showNodeGrid: {
type: "boolean",
value: true
},
snapToGrid: {
type: "boolean",
value: true
}
},
stressTest: {
amount: {
type: "integer",

View File

@ -14,10 +14,13 @@
import NodeStore from "$lib/node-store/NodeStore.svelte";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
import { setContext } from "svelte";
import { decodeNestedArray } from "@nodes/utils";
const nodeRegistry = new RemoteNodeRegistry("");
const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry);
globalThis.decode = decodeNestedArray;
let res: Int32Array;
let graph = localStorage.getItem("graph")
@ -120,6 +123,8 @@
registry={nodeRegistry}
{graph}
bind:keymap
showGrid={$AppSettings?.showNodeGrid}
snapToGrid={$AppSettings?.snapToGrid}
settings={settings?.graph?.settings}
on:settings={handleSettings}
on:result={handleResult}

View File

@ -1,5 +1,5 @@
use macros::include_definition_file;
use utils::{evaluate_arg, get_args};
use utils::{evaluate_int, get_args};
use wasm_bindgen::prelude::*;
use web_sys::console;
@ -11,8 +11,8 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
let args = get_args(input);
let value_encoded = evaluate_arg(args[0]);
let length = evaluate_arg(args[1]) as usize;
let value_encoded = evaluate_int(args[0]);
let length = evaluate_int(args[1]) as usize;
console::log_1(&format!("WASM(array): input: {:?} -> {:?}", args, value_encoded).into());

View File

@ -5,17 +5,13 @@ include_definition_file!("src/input.json");
#[wasm_bindgen]
pub fn execute(args: &[i32]) -> Vec<i32> {
let mut result = Vec::with_capacity(args.len() + 7);
result.push(0);
result.push(1);
let mut result = Vec::with_capacity(args.len() + 3);
result.push(0); // encoding the [ bracket
result.push(args[1] + 1);
result.push(0); // adding the node-type, math: 0
result.extend_from_slice(&args[2..]);
result.push(1);
result.push(1); // closing bracket
result.push(1);
result.push(1); // closing bracket
result

6
nodes/max/plantarium/noise/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/target
**/*.rs.bk
Cargo.lock
bin/
pkg/
wasm-pack.log

View File

@ -0,0 +1,28 @@
[package]
name = "noise"
version = "0.1.0"
authors = ["Max Richter <jim-x@web.de>"]
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
utils = { version = "0.1.0", path = "../../../../packages/utils" }
macros = { version = "0.1.0", path = "../../../../packages/macros" }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"
console_error_panic_hook = { version = "0.1.7", optional = true }
web-sys = { version = "0.3.69", features = ["console"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.34"

View File

@ -0,0 +1,6 @@
{
"scripts": {
"build": "wasm-pack build --release --out-name index --no-default-features",
"dev": "cargo watch -s 'wasm-pack build --dev --out-name index --no-default-features'"
}
}

View File

@ -0,0 +1,21 @@
{
"id": "max/plantarium/noise",
"outputs": [
"plant"
],
"inputs": {
"plant": {
"type": "plant"
},
"scale": {
"type": "float",
"min": 0.1,
"max": 100
},
"strength": {
"type": "float",
"min": 0.1,
"max": 100
}
}
}

View File

@ -0,0 +1,14 @@
use macros::include_definition_file;
use utils::{concat_args, get_args, log};
use wasm_bindgen::prelude::*;
include_definition_file!("src/input.json");
#[rustfmt::skip]
#[wasm_bindgen]
pub fn execute(input: &[i32]) -> Vec<i32> {
let args = get_args(input);
let plants = get_args(args[0]);
log!("noise plants: {:?}", plants);
concat_args(vec![plants[0].to_vec()])
}

View File

@ -0,0 +1,13 @@
//! Test suite for the Web and headless browsers.
#![cfg(target_arch = "wasm32")]
extern crate wasm_bindgen_test;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn pass() {
assert_eq!(1 + 1, 2);
}

View File

@ -1,7 +1,7 @@
use glam::{Mat4, Vec3};
use macros::include_definition_file;
use utils::{
concat_args, evaluate_arg,
concat_args, evaluate_int,
geometry::{extrude_path, transform_geometry},
get_args, log,
};
@ -10,13 +10,17 @@ use wasm_bindgen::prelude::*;
include_definition_file!("src/inputs.json");
#[wasm_bindgen]
pub fn execute(input: Vec<i32>) -> Vec<i32> {
pub fn execute(input: &[i32]) -> Vec<i32> {
utils::set_panic_hook();
let args = get_args(input.as_slice());
log!("output input: {:?}", input);
let args = get_args(input);
log!("output args: {:?}", args);
let inputs = get_args(args[0]);
let resolution = evaluate_arg(args[1]) as usize;
let resolution = evaluate_int(args[1]) as usize;
log!("inputs: {}, resolution: {}", inputs.len(), resolution);
@ -26,13 +30,19 @@ pub fn execute(input: Vec<i32>) -> Vec<i32> {
continue;
}
if arg[2] == 0 {
let arg_type = arg[0];
log!("arg: {:?}", arg);
if arg_type == 0 {
// this is stem
let _arg = &arg[3..];
let mut geometry = extrude_path(_arg, resolution);
let matrix = Mat4::from_translation(Vec3::new(0.0, 0.0, 0.0));
geometry = transform_geometry(geometry, matrix);
output.push(geometry);
} else if arg[2] == 1 {
} else if arg_type == 1 {
// this is geometry
output.push(arg.to_vec());
}
}

View File

@ -1,5 +1,5 @@
use macros::include_definition_file;
use utils::{evaluate_arg, evaluate_float, evaluate_vec3, get_args, set_panic_hook, wrap_arg};
use utils::{evaluate_float, evaluate_int, evaluate_vec3, get_args, log, set_panic_hook, wrap_arg};
use wasm_bindgen::prelude::*;
include_definition_file!("src/input.json");
@ -10,22 +10,30 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
let args = get_args(input);
log!("node-stem: {:?}", args);
let origin = evaluate_vec3(args[0]);
let length = evaluate_float(args[1]);
let thickness = evaluate_float(args[2]);
let res_curve = evaluate_arg(args[3]) as usize;
let res_curve = evaluate_int(args[3]) as usize;
let mut path: Vec<i32> = vec![0; res_curve * 4 + 1];
path.resize(res_curve * 4 + 1, 0);
log!("origin: {:?}", origin);
let slice = &mut path[1..];
let amount_points = res_curve * 4;
// +4 for opening and closing brackets and +1 node-type
let output_size = amount_points + 5;
let mut path: Vec<i32> = vec![0; output_size];
path[0] = 0; // encode opening bracket
path[1] = res_curve as i32 * 4 + 4; // encode opening bracket
path[2] = 0; // encode node-type, stem: 0
path[output_size - 2] = 1; // encode closing bracket
path[output_size - 1] = 1; // encode closing bracket
let slice = &mut path[3..output_size - 2];
// Unsafe code to transmute the i32 slice to an f32 slice
let path_p: &mut [f32] = unsafe {
// Ensure that the length of the slice is a multiple of 4
assert_eq!(slice.len() % 4, 0);
// Transmute the i32 slice to an f32 slice
std::slice::from_raw_parts_mut(slice.as_ptr() as *mut f32, slice.len())
};
@ -37,5 +45,7 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
path_p[i * 4 + 3] = thickness * (1.0 - a);
}
wrap_arg(&path)
log!("res: {:?}", path);
path
}

View File

@ -1,5 +1,5 @@
use macros::include_definition_file;
use utils::{decode_float, encode_float, evaluate_arg, get_args, wrap_arg};
use utils::{decode_float, encode_float, evaluate_int, get_args, wrap_arg};
use wasm_bindgen::prelude::*;
use web_sys::console;
@ -13,7 +13,7 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
let args = get_args(input);
let size = evaluate_arg(args[0]);
let size = evaluate_int(args[0]);
let decoded = decode_float(size);
let negative_size = encode_float(-decoded);

View File

@ -1,20 +1,9 @@
use macros::include_definition_file;
use utils::log;
use wasm_bindgen::prelude::*;
include_definition_file!("src/input.json");
#[wasm_bindgen]
pub fn execute(args: &[i32]) -> Vec<i32> {
let mut result = Vec::with_capacity(args.len() + 2);
result.push(0); // encoding the [ bracket
result.push(args[1]);
result.extend_from_slice(&args[2..]);
result.push(1);
result.push(1); // closing bracket
log!("WASM(vec3): res {:?}", result);
result
pub fn execute(input: &[i32]) -> Vec<i32> {
input.to_vec()
}

View File

@ -1,6 +1,6 @@
type SparseArray<T = number> = (T | T[] | SparseArray<T>)[];
export function concat_encoded(input: (number | number[])[]): number[] {
export function concatEncodedArrays(input: (number | number[])[]): number[] {
if (input.length === 1 && Array.isArray(input[0])) {
return input[0]
@ -26,11 +26,13 @@ export function concat_encoded(input: (number | number[])[]): number[] {
}
}
result.push(1, 1); // closing bracket
return result
}
// Encodes a nested array into a flat array with bracket and distance notation
export function encode(array: SparseArray): number[] {
export function encodeNestedArray(array: SparseArray): number[] {
const encoded = [0, 0]; // Initialize encoded array with root bracket notation
let missingBracketIndex = 1; // Track where to insert the distance to the next bracket
@ -44,7 +46,7 @@ export function encode(array: SparseArray): number[] {
encoded.push(0, 1, 1, 1);
} else {
// Recursively encode non-empty arrays
const child = encode(item);
const child = encodeNestedArray(item);
encoded.push(...child, 1, 0);
}
// Update missingBracketIndex to the position of the newly added bracket
@ -59,7 +61,12 @@ export function encode(array: SparseArray): number[] {
return encoded;
};
function decode_recursive(dense: number[], index = 0) {
function decode_recursive(dense: number[] | Int32Array, index = 0) {
if (dense instanceof Int32Array) {
dense = Array.from(dense)
}
const decoded: (number | number[])[] = [];
let nextBracketIndex = dense[index + 1] + index + 1; // Calculate the index of the next bracket
@ -83,6 +90,6 @@ function decode_recursive(dense: number[], index = 0) {
return [decoded, index, nextBracketIndex] as const;
}
export function decode(dense: number[]) {
export function decodeNestedArray(dense: number[] | Int32Array) {
return decode_recursive(dense, 0)[0];
}

View File

@ -67,7 +67,7 @@ pub fn extrude_path(input_path: &[i32], res_x: usize) -> Vec<i32> {
let i_index_offset = index_offset + j * 6;
let i_position_offset = position_offset + j;
log!("i: {}, j: {}, i_index_offset: {}, i_position_offset: {} res_x: {}", i, j, i_index_offset, i_position_offset,res_x);
//log!("i: {}, j: {}, i_index_offset: {}, i_position_offset: {} res_x: {}", i, j, i_index_offset, i_position_offset,res_x);
if j == res_x - 1 {
indices[i_index_offset ] = (i_position_offset + 1) as i32;

View File

@ -39,9 +39,13 @@ pub fn get_args(args: &[i32]) -> Vec<&[i32]> {
// skip over the bracket encoding
idx += 2;
} else {
if depth == 1 {
arg_start_index = idx + 2;
}
// skip to the next bracket if we are at depth > 0
idx = next_bracket_index;
}
continue;
}
@ -56,6 +60,12 @@ pub fn get_args(args: &[i32]) -> Vec<&[i32]> {
idx += 1;
}
println!("idx: {}, length: {}, asi: {}", idx, length, arg_start_index);
if arg_start_index < length {
out_args.push(&args[arg_start_index..]);
}
out_args
}
@ -101,6 +111,8 @@ pub fn wrap_arg(arg: &[i32]) -> Vec<i32> {
pub fn evaluate_node(input_args: &[i32]) -> i32 {
let node_type = input_args[0];
println!("node_type: {} -> {:?}", node_type, input_args);
match node_type {
0 => crate::nodes::math_node(&input_args[1..]),
1 => crate::nodes::random_node(&input_args[1..]),
@ -109,12 +121,21 @@ pub fn evaluate_node(input_args: &[i32]) -> i32 {
}
pub fn evaluate_vec3(input_args: &[i32]) -> Vec<f32> {
if input_args.len() == 3 {
return vec![
decode_float(input_args[0]),
decode_float(input_args[1]),
decode_float(input_args[2]),
];
}
let args = get_args(input_args);
assert!(
args.len() == 3,
"Failed to evaluate Vec3 - Expected 3 arguments, got {}",
args.len()
"Failed to evaluate Vec3 - Expected 3 arguments, got {} \n {:?}",
args.len(),
args
);
let x = evaluate_float(args[0]);
@ -125,16 +146,18 @@ pub fn evaluate_vec3(input_args: &[i32]) -> Vec<f32> {
}
pub fn evaluate_float(arg: &[i32]) -> f32 {
decode_float(evaluate_arg(arg))
decode_float(evaluate_int(arg))
}
pub fn evaluate_arg(input_args: &[i32]) -> i32 {
pub fn evaluate_int(input_args: &[i32]) -> i32 {
if input_args.len() == 1 {
return input_args.to_vec()[0];
return input_args[0];
}
let args = get_args(input_args);
println!("args: {:?}", args);
let mut resolved: Vec<i32> = Vec::new();
for arg in args {
@ -144,7 +167,7 @@ pub fn evaluate_arg(input_args: &[i32]) -> i32 {
resolved.push(arg[2]);
resolved.push(arg[3]);
} else {
resolved.push(evaluate_arg(arg));
resolved.push(evaluate_int(arg));
}
}
@ -160,6 +183,32 @@ mod tests {
use super::*;
#[rustfmt::skip]
#[test]
fn test_split_args() {
let input = [
0, 1, 0, 28, 0, 2, 1048576000, 0, 20, 0, 4, 0, 0, 1073741824, 0, 9, 0, 5, 0, 0,
1073741824, 1073741824, 1, 1, 1, 0, 1, 1, 1, 4, 1041865114, 1, 5, 1086324736,
1053609165, 54,
];
// this should be the output
/* [
[ 0, 2, 1048576000, 0, 20, 0, 4, 0, 0, 1073741824, 0, 9, 0, 5, 0, 0, 1073741824, 1073741824, 1, 1, 1, 0, 1, 1, 1, 4, 1041865114 ],
1086324736,
1053609165,
54
] */
let args = get_args(&input);
println!("{:?}", args[0]);
assert_eq!(args[0].len(), 27);
assert_eq!(args[1][0], 1086324736);
assert_eq!(args[2][0], 1053609165);
}
#[test]
fn test_recursive_evaluation() {
let input = vec![

View File

@ -2,3 +2,4 @@ packages:
- app
- packages/*
- nodes/**/**/**/*
- '!**/.template'