Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e6c368afaa
|
|||
|
581daa1be7
|
|||
|
f652b712df
|
|||
|
68ae62527f
|
|||
|
49746c6079
|
|||
|
e5df19b6d8
|
|||
|
415be50ae0
|
|||
|
f0f4c00137
|
|||
|
3c5f897b26
|
|||
|
ed0c47068a
|
|||
|
a039bddba1
|
|||
|
5fa9d36b34
|
|||
|
7d788f7e19
|
Generated
+2
@@ -66,6 +66,7 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
|||||||
name = "leaf"
|
name = "leaf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"glam",
|
||||||
"nodarium_macros",
|
"nodarium_macros",
|
||||||
"nodarium_utils",
|
"nodarium_utils",
|
||||||
]
|
]
|
||||||
@@ -117,6 +118,7 @@ dependencies = [
|
|||||||
name = "noise"
|
name = "noise"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"glam",
|
||||||
"nodarium_macros",
|
"nodarium_macros",
|
||||||
"nodarium_utils",
|
"nodarium_utils",
|
||||||
"noise 0.9.0",
|
"noise 0.9.0",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { clone } from '$lib/helpers';
|
import { clone, debounce } from '$lib/helpers';
|
||||||
import throttle from '$lib/helpers/throttle';
|
import throttle from '$lib/helpers/throttle';
|
||||||
import { RemoteNodeRegistry } from '$lib/node-registry/index';
|
import { RemoteNodeRegistry } from '$lib/node-registry/index';
|
||||||
import type {
|
import type {
|
||||||
@@ -309,17 +309,18 @@ export class GraphManager extends EventEmitter<{
|
|||||||
this.nodes.set(n.id, n);
|
this.nodes.set(n.id, n);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.edges = graph.edges.map((edge) => {
|
this.edges = graph.edges.flatMap((edge) => {
|
||||||
const from = this.nodes.get(edge[0]);
|
const from = this.nodes.get(edge[0]);
|
||||||
const to = this.nodes.get(edge[2]);
|
const to = this.nodes.get(edge[2]);
|
||||||
if (!from || !to) {
|
if (!from || !to) {
|
||||||
throw new Error('Edge references non-existing node');
|
log.warn('Dropping orphaned edge', edge);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
from.state.children = from.state.children || [];
|
from.state.children = from.state.children || [];
|
||||||
from.state.children.push(to);
|
from.state.children.push(to);
|
||||||
to.state.parents = to.state.parents || [];
|
to.state.parents = to.state.parents || [];
|
||||||
to.state.parents.push(from);
|
to.state.parents.push(from);
|
||||||
return [from, edge[1], to, edge[3]] as Edge;
|
return [[from, edge[1], to, edge[3]] as Edge];
|
||||||
});
|
});
|
||||||
|
|
||||||
this.execute();
|
this.execute();
|
||||||
@@ -657,9 +658,9 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const inputs = Object.entries(to.state?.type?.inputs ?? {});
|
const inputs = Object.entries(to.state?.type?.inputs ?? {});
|
||||||
const outputs = from.state?.type?.outputs ?? [];
|
const outputs = from.state?.type?.outputs ?? [];
|
||||||
for (let i = 0; i < inputs.length; i++) {
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
const [inputName, input] = inputs[0];
|
const [inputName, input] = inputs[i];
|
||||||
for (let o = 0; o < outputs.length; o++) {
|
for (let o = 0; o < outputs.length; o++) {
|
||||||
const output = outputs[0];
|
const output = outputs[o];
|
||||||
if (input.type === output) {
|
if (input.type === output) {
|
||||||
return this.createEdge(from, o, to, inputName);
|
return this.createEdge(from, o, to, inputName);
|
||||||
}
|
}
|
||||||
@@ -1239,20 +1240,18 @@ export class GraphManager extends EventEmitter<{
|
|||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _emitSave = debounce(() => {
|
||||||
|
if (this.nodes.size === 0 && this.edges.length === 0) return;
|
||||||
|
const state = this.serialize();
|
||||||
|
this.emit('save', state);
|
||||||
|
log.log('saving graphs', state);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
if (this.currentUndoGroup) return;
|
if (this.currentUndoGroup) return;
|
||||||
const state = this.serialize();
|
// History snapshot is immediate; the IDB emit is debounced.
|
||||||
this.history.save(state);
|
this.history.save(this.serialize());
|
||||||
|
this._emitSave();
|
||||||
// This is some stupid race condition where the graph-manager emits a save event
|
|
||||||
// when the graph is not fully loaded
|
|
||||||
if (this.nodes.size === 0 && this.edges.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullState = this.serialize();
|
|
||||||
this.emit('save', fullState);
|
|
||||||
log.log('saving graphs', fullState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getParentsOfNode(node: NodeInstance) {
|
getParentsOfNode(node: NodeInstance) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { animate, lerp } from '$lib/helpers';
|
import { animate, debounce, lerp } from '$lib/helpers';
|
||||||
import type { NodeInstance, SerializedEdge, SerializedNode, Socket } from '@nodarium/types';
|
import type { NodeInstance, SerializedEdge, SerializedNode, Socket } from '@nodarium/types';
|
||||||
import { getContext, setContext } from 'svelte';
|
import { getContext, setContext } from 'svelte';
|
||||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||||
@@ -62,12 +62,20 @@ export class GraphState {
|
|||||||
colors = new ColorGenerator(predefinedColors);
|
colors = new ColorGenerator(predefinedColors);
|
||||||
|
|
||||||
constructor(private graph: GraphManager) {
|
constructor(private graph: GraphManager) {
|
||||||
|
const saveCameraPosition = debounce(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'cameraPosition',
|
||||||
|
`[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]`
|
||||||
|
);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
$effect.root(() => {
|
$effect.root(() => {
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
localStorage.setItem(
|
// Read values to subscribe to reactivity, then flush lazily.
|
||||||
'cameraPosition',
|
void this.cameraPosition[0];
|
||||||
`[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]`
|
void this.cameraPosition[1];
|
||||||
);
|
void this.cameraPosition[2];
|
||||||
|
saveCameraPosition();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const storedPosition = localStorage.getItem('cameraPosition');
|
const storedPosition = localStorage.getItem('cameraPosition');
|
||||||
@@ -157,6 +165,27 @@ export class GraphState {
|
|||||||
this.edges.delete(edgeId);
|
this.edges.delete(edgeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _dirtyPositions = new Set<NodeInstance>();
|
||||||
|
private _positionFlushPending = false;
|
||||||
|
|
||||||
|
private _flushPositions() {
|
||||||
|
for (const node of this._dirtyPositions) {
|
||||||
|
if (node.state['x'] !== undefined && node.state['y'] !== undefined) {
|
||||||
|
if (node.state.ref) {
|
||||||
|
node.state.ref.style.setProperty('--nx', `${node.state.x * 10}px`);
|
||||||
|
node.state.ref.style.setProperty('--ny', `${node.state.y * 10}px`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (node.state.ref) {
|
||||||
|
node.state.ref.style.setProperty('--nx', `${node.position[0] * 10}px`);
|
||||||
|
node.state.ref.style.setProperty('--ny', `${node.position[1] * 10}px`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._dirtyPositions.clear();
|
||||||
|
this._positionFlushPending = false;
|
||||||
|
}
|
||||||
|
|
||||||
updateNodePosition(node: NodeInstance) {
|
updateNodePosition(node: NodeInstance) {
|
||||||
if (
|
if (
|
||||||
node.state.x === node.position[0]
|
node.state.x === node.position[0]
|
||||||
@@ -166,16 +195,10 @@ export class GraphState {
|
|||||||
delete node.state.y;
|
delete node.state.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.state['x'] !== undefined && node.state['y'] !== undefined) {
|
this._dirtyPositions.add(node);
|
||||||
if (node.state.ref) {
|
if (!this._positionFlushPending) {
|
||||||
node.state.ref.style.setProperty('--nx', `${node.state.x * 10}px`);
|
this._positionFlushPending = true;
|
||||||
node.state.ref.style.setProperty('--ny', `${node.state.y * 10}px`);
|
requestAnimationFrame(() => this._flushPositions());
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (node.state.ref) {
|
|
||||||
node.state.ref.style.setProperty('--nx', `${node.position[0] * 10}px`);
|
|
||||||
node.state.ref.style.setProperty('--ny', `${node.position[1] * 10}px`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { GraphSchema, type NodeId } from '@nodarium/types';
|
import { GraphSchema, type NodeId } from '@nodarium/types';
|
||||||
|
import { toast } from '@nodarium/ui';
|
||||||
import type { GraphManager } from '../graph-manager.svelte';
|
import type { GraphManager } from '../graph-manager.svelte';
|
||||||
import type { GraphState } from '../graph-state.svelte';
|
import type { GraphState } from '../graph-state.svelte';
|
||||||
|
|
||||||
@@ -41,6 +42,9 @@ export class FileDropEventManager {
|
|||||||
props,
|
props,
|
||||||
position: pos
|
position: pos
|
||||||
});
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
toast(`Failed to load node: ${nodeId}`, 'error');
|
||||||
|
console.error(e);
|
||||||
});
|
});
|
||||||
} else if (event.dataTransfer.files.length) {
|
} else if (event.dataTransfer.files.length) {
|
||||||
const file = event.dataTransfer.files[0];
|
const file = event.dataTransfer.files[0];
|
||||||
@@ -65,8 +69,13 @@ export class FileDropEventManager {
|
|||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
const buffer = e.target?.result as ArrayBuffer;
|
const buffer = e.target?.result as ArrayBuffer;
|
||||||
if (buffer) {
|
if (buffer) {
|
||||||
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
|
try {
|
||||||
this.graph.load(state);
|
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
|
||||||
|
this.graph.load(state);
|
||||||
|
} catch (e) {
|
||||||
|
toast('Failed to load graph: invalid file', 'error');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { EdgeInteractionManager } from './edge.events';
|
|||||||
|
|
||||||
export class MouseEventManager {
|
export class MouseEventManager {
|
||||||
edgeInteractionManager: EdgeInteractionManager;
|
edgeInteractionManager: EdgeInteractionManager;
|
||||||
|
private pendingSelectionFrame = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private graph: GraphManager,
|
private graph: GraphManager,
|
||||||
@@ -282,24 +283,31 @@ export class MouseEventManager {
|
|||||||
if (this.state.boxSelection) {
|
if (this.state.boxSelection) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const mouseD = this.state.projectScreenToWorld(
|
if (!this.pendingSelectionFrame) {
|
||||||
this.state.mouseDown[0],
|
this.pendingSelectionFrame = true;
|
||||||
this.state.mouseDown[1]
|
requestAnimationFrame(() => {
|
||||||
);
|
this.pendingSelectionFrame = false;
|
||||||
const x1 = Math.min(mouseD[0], this.state.mousePosition[0]);
|
if (!this.state.mouseDown) return;
|
||||||
const x2 = Math.max(mouseD[0], this.state.mousePosition[0]);
|
const mouseD = this.state.projectScreenToWorld(
|
||||||
const y1 = Math.min(mouseD[1], this.state.mousePosition[1]);
|
this.state.mouseDown[0],
|
||||||
const y2 = Math.max(mouseD[1], this.state.mousePosition[1]);
|
this.state.mouseDown[1]
|
||||||
for (const node of this.graph.nodes.values()) {
|
);
|
||||||
if (!node?.state) continue;
|
const x1 = Math.min(mouseD[0], this.state.mousePosition[0]);
|
||||||
const x = node.position[0];
|
const x2 = Math.max(mouseD[0], this.state.mousePosition[0]);
|
||||||
const y = node.position[1];
|
const y1 = Math.min(mouseD[1], this.state.mousePosition[1]);
|
||||||
const height = getNodeHeight(node.state.type!);
|
const y2 = Math.max(mouseD[1], this.state.mousePosition[1]);
|
||||||
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
|
for (const node of this.graph.nodes.values()) {
|
||||||
this.state.selectedNodes?.add(node.id);
|
if (!node?.state) continue;
|
||||||
} else {
|
const x = node.position[0];
|
||||||
this.state.selectedNodes?.delete(node.id);
|
const y = node.position[1];
|
||||||
}
|
const height = getNodeHeight(node.state.type!);
|
||||||
|
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
|
||||||
|
this.state.selectedNodes?.add(node.id);
|
||||||
|
} else {
|
||||||
|
this.state.selectedNodes?.delete(node.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function serializeNode(node: SerializedNode | NodeInstance): SerializedNo
|
|||||||
id: node.id,
|
id: node.id,
|
||||||
position: [...node.position],
|
position: [...node.position],
|
||||||
type: node.type,
|
type: node.type,
|
||||||
props: node.props
|
props: node.props ? JSON.parse(JSON.stringify(node.props)) : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export class RemoteNodeRegistry implements NodeRegistry {
|
|||||||
wrapper = createWasmWrapper(wasmBuffer);
|
wrapper = createWasmWrapper(wasmBuffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to create node wrapper for node: ${id}`, error);
|
console.error(`Failed to create node wrapper for node: ${id}`, error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawDefinition = wrapper.get_definition();
|
const rawDefinition = wrapper.get_definition();
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export class ProjectManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveGraph(g: Graph) {
|
async saveGraph(g: Graph) {
|
||||||
db.saveGraph(g);
|
await db.saveGraph(g);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async init() {
|
private async init() {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function writePath(scene: Group, data: Int32Array): Vector3[] {
|
|||||||
|
|
||||||
// Instanced spheres at points
|
// Instanced spheres at points
|
||||||
if (positions.length > 0) {
|
if (positions.length > 0) {
|
||||||
const sphereGeometry = new SphereGeometry(0.05, 8, 8); // keep low-poly
|
const sphereGeometry = new SphereGeometry(0.02, 8, 8); // keep low-poly
|
||||||
const sphereMaterial = new MeshBasicMaterial({
|
const sphereMaterial = new MeshBasicMaterial({
|
||||||
color: 0xff0000,
|
color: 0xff0000,
|
||||||
depthTest: false
|
depthTest: false
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ export function createInstancedGeometryPool(
|
|||||||
existingInstance
|
existingInstance
|
||||||
&& instanceCount > existingInstance.geometry.userData.count
|
&& instanceCount > existingInstance.geometry.userData.count
|
||||||
) {
|
) {
|
||||||
|
existingInstance.geometry.dispose();
|
||||||
scene.remove(existingInstance);
|
scene.remove(existingInstance);
|
||||||
instances.splice(instances.indexOf(existingInstance), 1);
|
instances.splice(instances.indexOf(existingInstance), 1);
|
||||||
existingInstance = new InstancedMesh(geometry, material, instanceCount);
|
existingInstance = new InstancedMesh(geometry, material, instanceCount);
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { Graph, RuntimeExecutor } from '@nodarium/types';
|
|
||||||
|
|
||||||
export class RemoteRuntimeExecutor implements RuntimeExecutor {
|
|
||||||
constructor(private url: string) {}
|
|
||||||
|
|
||||||
async execute(graph: Graph, settings: Record<string, unknown>): Promise<Int32Array> {
|
|
||||||
const res = await fetch(this.url, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ graph, settings })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`Failed to execute graph`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Int32Array(await res.arrayBuffer());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -134,6 +134,14 @@ function getValue(input: NodeInput, value?: unknown) {
|
|||||||
return encodeFloat(value as number);
|
return encodeFloat(value as number);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.type === 'select' && typeof value !== 'number') {
|
||||||
|
const index = input.options?.indexOf(value as string);
|
||||||
|
if (index === undefined || index < 0) {
|
||||||
|
throw new Error(`Unknown value ${value} for select input ${input.label}`);
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
if (input.type === 'vec3' || input.type === 'shape') {
|
if (input.type === 'vec3' || input.type === 'shape') {
|
||||||
return [
|
return [
|
||||||
@@ -159,6 +167,8 @@ function getValue(input: NodeInput, value?: unknown) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log({ input, value });
|
||||||
|
|
||||||
throw new Error(`Unknown input type ${input.type}`);
|
throw new Error(`Unknown input type ${input.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,9 +183,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
|||||||
constructor(
|
constructor(
|
||||||
private registry: NodeRegistry,
|
private registry: NodeRegistry,
|
||||||
public cache?: SyncCache<Int32Array>
|
public cache?: SyncCache<Int32Array>
|
||||||
) {
|
) {}
|
||||||
this.cache = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getNodeDefinitions(graph: Graph) {
|
private async getNodeDefinitions(graph: Graph) {
|
||||||
if (this.registry.status !== 'ready') {
|
if (this.registry.status !== 'ready') {
|
||||||
@@ -399,7 +407,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
|||||||
log.groupEnd();
|
log.groupEnd();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.groupEnd();
|
log.groupEnd();
|
||||||
log.error(`Error executing node ${node_type.id || node.id}`, e);
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { debugNode } from '$lib/node-registry/debugNode';
|
|||||||
import { IndexDBCache, RemoteNodeRegistry } from '$lib/node-registry/index';
|
import { IndexDBCache, RemoteNodeRegistry } from '$lib/node-registry/index';
|
||||||
import type { Graph } from '@nodarium/types';
|
import type { Graph } from '@nodarium/types';
|
||||||
import { createPerformanceStore } from '@nodarium/utils';
|
import { createPerformanceStore } from '@nodarium/utils';
|
||||||
|
import * as Comlink from 'comlink';
|
||||||
import { MemoryRuntimeExecutor } from './runtime-executor';
|
import { MemoryRuntimeExecutor } from './runtime-executor';
|
||||||
import { MemoryRuntimeCache } from './runtime-executor-cache';
|
import { MemoryRuntimeCache } from './runtime-executor-cache';
|
||||||
|
|
||||||
@@ -38,6 +39,9 @@ export async function executeGraph(
|
|||||||
performanceStore.startRun();
|
performanceStore.startRun();
|
||||||
const res = await executor.execute(graph, settings);
|
const res = await executor.execute(graph, settings);
|
||||||
performanceStore.stopRun();
|
performanceStore.stopRun();
|
||||||
|
if (res?.buffer) {
|
||||||
|
return Comlink.transfer(res, [res.buffer]);
|
||||||
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export class WorkerRuntimeExecutor implements RuntimeExecutor {
|
|||||||
getPerformanceData() {
|
getPerformanceData() {
|
||||||
return this.worker.getPerformanceData();
|
return this.worker.getPerformanceData();
|
||||||
}
|
}
|
||||||
getDebugData() {
|
async getDebugData() {
|
||||||
return this.worker.getDebugData();
|
return await this.worker.getDebugData();
|
||||||
}
|
}
|
||||||
set useRuntimeCache(useCache: boolean) {
|
set useRuntimeCache(useCache: boolean) {
|
||||||
this.worker.setUseRuntimeCache(useCache);
|
this.worker.setUseRuntimeCache(useCache);
|
||||||
|
|||||||
+20
-13
@@ -7,7 +7,7 @@
|
|||||||
import { debugNode } from '$lib/node-registry/debugNode';
|
import { debugNode } from '$lib/node-registry/debugNode';
|
||||||
import { groupNode } from '$lib/node-registry/groupNode.js';
|
import { groupNode } from '$lib/node-registry/groupNode.js';
|
||||||
import { IndexDBCache, RemoteNodeRegistry } from '$lib/node-registry/index';
|
import { IndexDBCache, RemoteNodeRegistry } from '$lib/node-registry/index';
|
||||||
import NodeStore from '$lib/node-store/NodeStore.svelte';
|
|
||||||
import PerformanceViewer from '$lib/performance/PerformanceViewer.svelte';
|
import PerformanceViewer from '$lib/performance/PerformanceViewer.svelte';
|
||||||
import { ProjectManager } from '$lib/project-manager/project-manager.svelte';
|
import { ProjectManager } from '$lib/project-manager/project-manager.svelte';
|
||||||
import ProjectManagerEl from '$lib/project-manager/ProjectManager.svelte';
|
import ProjectManagerEl from '$lib/project-manager/ProjectManager.svelte';
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
|
|
||||||
let performanceStore = createPerformanceStore();
|
let performanceStore = createPerformanceStore();
|
||||||
let planty = $state<ReturnType<typeof Planty>>();
|
let planty = $state<ReturnType<typeof Planty>>();
|
||||||
|
let pendingSave = false;
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
|
|
||||||
@@ -52,8 +53,8 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
workerRuntime.useRegistryCache = appSettings.value.debug.cache.useRuntimeCache;
|
workerRuntime.useRegistryCache = appSettings.value.debug.cache.useRegistryCache;
|
||||||
workerRuntime.useRuntimeCache = appSettings.value.debug.cache.useRegistryCache;
|
workerRuntime.useRuntimeCache = appSettings.value.debug.cache.useRuntimeCache;
|
||||||
|
|
||||||
if (appSettings.value.debug.cache.useRegistryCache) {
|
if (appSettings.value.debug.cache.useRegistryCache) {
|
||||||
nodeRegistry.cache = registryCache;
|
nodeRegistry.cache = registryCache;
|
||||||
@@ -68,6 +69,16 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const handler = (e: BeforeUnloadEvent) => {
|
||||||
|
if (pendingSave) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('beforeunload', handler);
|
||||||
|
return () => window.removeEventListener('beforeunload', handler);
|
||||||
|
});
|
||||||
|
|
||||||
let activeNode = $state<NodeInstance | undefined>(undefined);
|
let activeNode = $state<NodeInstance | undefined>(undefined);
|
||||||
let scene = $state<Group>(null!);
|
let scene = $state<Group>(null!);
|
||||||
let isExecuting = $state(false);
|
let isExecuting = $state(false);
|
||||||
@@ -288,7 +299,11 @@
|
|||||||
bind:showHelp={appSettings.value.nodeInterface.showHelp}
|
bind:showHelp={appSettings.value.nodeInterface.showHelp}
|
||||||
bind:settings={graphSettings}
|
bind:settings={graphSettings}
|
||||||
bind:settingTypes={graphSettingTypes}
|
bind:settingTypes={graphSettingTypes}
|
||||||
onsave={(g) => pm.saveGraph(g)}
|
onsave={async (g) => {
|
||||||
|
pendingSave = true;
|
||||||
|
await pm.saveGraph(g);
|
||||||
|
pendingSave = false;
|
||||||
|
}}
|
||||||
onresult={(result) => handleUpdate(result as Graph)}
|
onresult={(result) => handleUpdate(result as Graph)}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
@@ -317,15 +332,7 @@
|
|||||||
<Panel id="exports" title="Exporter" icon="i-[tabler--package-export]">
|
<Panel id="exports" title="Exporter" icon="i-[tabler--package-export]">
|
||||||
<ExportSettings {scene} />
|
<ExportSettings {scene} />
|
||||||
</Panel>
|
</Panel>
|
||||||
{#if 0 > 1}
|
|
||||||
<Panel
|
|
||||||
id="node-store"
|
|
||||||
title="Node Store"
|
|
||||||
icon="i-[tabler--database] bg-green-400"
|
|
||||||
>
|
|
||||||
<NodeStore registry={nodeRegistry} />
|
|
||||||
</Panel>
|
|
||||||
{/if}
|
|
||||||
<Panel
|
<Panel
|
||||||
id="performance"
|
id="performance"
|
||||||
title="Performance"
|
title="Performance"
|
||||||
|
|||||||
@@ -83,6 +83,14 @@
|
|||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 360,
|
"max": 360,
|
||||||
"step": 0.01,
|
"step": 0.01,
|
||||||
|
"value": 137.5
|
||||||
|
},
|
||||||
|
"angle": {
|
||||||
|
"type": "float",
|
||||||
|
"description": "Upward tilt of branches. 0 = horizontal, positive = upward, negative = drooping.",
|
||||||
|
"min": -90,
|
||||||
|
"max": 90,
|
||||||
|
"step": 1,
|
||||||
"value": 0
|
"value": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,9 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let branch_direction = rotate_vector_by_angle(orthogonal, direction, rotation_angle);
|
let up_angle = evaluate_float(args[10]) * PI / 180.0;
|
||||||
|
let tilted = (orthogonal * up_angle.cos() + direction * up_angle.sin()).normalize();
|
||||||
|
let branch_direction = rotate_vector_by_angle(tilted, direction, rotation_angle);
|
||||||
|
|
||||||
log!(
|
log!(
|
||||||
"BRANCH depth: {}, branch_origin: {:?}, direction_at: {:?}, branch_direction: {:?}",
|
"BRANCH depth: {}, branch_origin: {:?}, direction_at: {:?}, branch_direction: {:?}",
|
||||||
|
|||||||
@@ -13,19 +13,28 @@
|
|||||||
"max": 1,
|
"max": 1,
|
||||||
"value": 1
|
"value": 1
|
||||||
},
|
},
|
||||||
"curviness": {
|
|
||||||
"type": "float",
|
|
||||||
"hidden": true,
|
|
||||||
"min": 0,
|
|
||||||
"max": 1,
|
|
||||||
"value": 0.5
|
|
||||||
},
|
|
||||||
"depth": {
|
"depth": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"min": 1,
|
"min": 1,
|
||||||
"max": 10,
|
"max": 10,
|
||||||
"hidden": true,
|
"hidden": true,
|
||||||
"value": 1
|
"value": 1
|
||||||
|
},
|
||||||
|
"elasticity": {
|
||||||
|
"type": "float",
|
||||||
|
"description": "How rigid the stem is. 0 = rope (uniform droop), 1 = stiff rod (only the tip bends).",
|
||||||
|
"min": 0,
|
||||||
|
"max": 1,
|
||||||
|
"step": 0.05,
|
||||||
|
"value": 0.3
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"type": "select",
|
||||||
|
"internal": true,
|
||||||
|
"label": "Mode",
|
||||||
|
"options": ["closed-form", "chain"],
|
||||||
|
"hidden": true,
|
||||||
|
"description": "closed-form lerps each segment toward gravity; chain is a forward-kinematic cantilever where each segment rotates by an angle that grows along the stem."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
|
|||||||
let args = split_args(input);
|
let args = split_args(input);
|
||||||
|
|
||||||
let plants = split_args(args[0]);
|
let plants = split_args(args[0]);
|
||||||
let depth = evaluate_int(args[3]);
|
let depth = evaluate_int(args[2]);
|
||||||
|
let elasticity = evaluate_float(args[3]).clamp(0.0, 1.0);
|
||||||
|
let mode = evaluate_int(args[4]); // 0 = closed-form, 1 = verlet
|
||||||
|
// 0 → sqrt (rope), 1 → ~4.5 (only the tip droops)
|
||||||
|
let bend_exponent = 0.5 + elasticity * 4.0;
|
||||||
|
|
||||||
let mut max_depth = 0;
|
let mut max_depth = 0;
|
||||||
for path_data in plants.iter() {
|
for path_data in plants.iter() {
|
||||||
@@ -42,50 +46,124 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
|
|||||||
let mut output_data = path_data.clone();
|
let mut output_data = path_data.clone();
|
||||||
let output = wrap_path_mut(&mut output_data);
|
let output = wrap_path_mut(&mut output_data);
|
||||||
|
|
||||||
let mut offset_vec = Vec3::ZERO;
|
if mode == 1 {
|
||||||
|
// Forward-kinematic cantilever chain. Each segment rotates around
|
||||||
|
// an axis perpendicular to (rest_dir, gravity) by an angle that
|
||||||
|
// grows with alpha along the stem. Positions are built from the
|
||||||
|
// anchored base outward, so segment lengths are preserved by
|
||||||
|
// construction (no iteration, no rescaling, no oscillation).
|
||||||
|
|
||||||
for i in 0..path.length - 1 {
|
let raw_strength = evaluate_float(args[1]);
|
||||||
let alpha = i as f32 / (path.length - 1) as f32;
|
let gravity_dir = Vec3::new(0.0, -1.0, 0.0);
|
||||||
let start_index = i * 4;
|
|
||||||
|
|
||||||
let start_point = Vec3::from_slice(&path.points[start_index..start_index + 3]);
|
// Tip bend angle in radians. PI/2 = horizontal tip at strength=1.
|
||||||
let end_point = Vec3::from_slice(&path.points[start_index + 4..start_index + 7]);
|
let max_angle = raw_strength * std::f32::consts::FRAC_PI_2;
|
||||||
|
|
||||||
let direction = end_point - start_point;
|
let original: Vec<Vec3> = (0..path.length)
|
||||||
|
.map(|i| {
|
||||||
|
let s = i * 4;
|
||||||
|
Vec3::from_slice(&path.points[s..s + 3])
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let length = direction.length();
|
let seg_lens: Vec<f32> = (0..path.length - 1)
|
||||||
|
.map(|i| (original[i + 1] - original[i]).length())
|
||||||
|
.collect();
|
||||||
|
let rest_dirs: Vec<Vec3> = (0..path.length - 1)
|
||||||
|
.map(|i| {
|
||||||
|
let d = original[i + 1] - original[i];
|
||||||
|
let l = d.length();
|
||||||
|
if l > 0.0001 { d / l } else { Vec3::Y }
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let curviness = evaluate_float(args[2]);
|
let mut cur = vec![Vec3::ZERO; path.length];
|
||||||
let strength =
|
cur[0] = original[0];
|
||||||
evaluate_float(args[1]) / curviness.max(0.0001) * evaluate_float(args[1]);
|
|
||||||
|
|
||||||
log!(
|
for i in 1..path.length {
|
||||||
"length: {}, curviness: {}, strength: {}",
|
let seg_idx = i - 1;
|
||||||
length,
|
let alpha = if path.length > 2 {
|
||||||
curviness,
|
seg_idx as f32 / (path.length - 2) as f32
|
||||||
strength
|
} else {
|
||||||
);
|
1.0
|
||||||
|
};
|
||||||
|
let bend_angle = max_angle * alpha.powf(bend_exponent);
|
||||||
|
|
||||||
let down_point = Vec3::new(0.0, -length * strength, 0.0);
|
let rest_dir = rest_dirs[seg_idx];
|
||||||
|
let mut bend_axis = rest_dir.cross(gravity_dir);
|
||||||
|
let axis_len = bend_axis.length();
|
||||||
|
bend_axis = if axis_len > 0.0001 {
|
||||||
|
bend_axis / axis_len
|
||||||
|
} else {
|
||||||
|
// rest_dir parallel to gravity — pick an arbitrary
|
||||||
|
// perpendicular axis to break symmetry.
|
||||||
|
Vec3::X
|
||||||
|
};
|
||||||
|
|
||||||
let mut mid_point = lerp_vec3(direction, down_point, curviness * alpha.sqrt());
|
// Rodrigues' rotation formula
|
||||||
|
let (sin_a, cos_a) = bend_angle.sin_cos();
|
||||||
|
let bent_dir = rest_dir * cos_a
|
||||||
|
+ bend_axis.cross(rest_dir) * sin_a
|
||||||
|
+ bend_axis * bend_axis.dot(rest_dir) * (1.0 - cos_a);
|
||||||
|
|
||||||
if mid_point[0] == 0.0 && mid_point[2] == 0.0 {
|
cur[i] = cur[i - 1] + bent_dir * seg_lens[seg_idx];
|
||||||
mid_point[0] += 0.0001;
|
|
||||||
mid_point[2] += 0.0001;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Correct midpoint length
|
for i in 0..path.length {
|
||||||
mid_point *= length / mid_point.length();
|
let s = i * 4;
|
||||||
|
output.points[s] = cur[i].x;
|
||||||
|
output.points[s + 1] = cur[i].y;
|
||||||
|
output.points[s + 2] = cur[i].z;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Closed-form: per-segment lerp toward a downward vector
|
||||||
|
let mut offset_vec = Vec3::ZERO;
|
||||||
|
|
||||||
let final_end_point = start_point + mid_point;
|
for i in 0..path.length - 1 {
|
||||||
let offset_end_point = end_point + offset_vec;
|
let alpha = i as f32 / (path.length - 1) as f32;
|
||||||
|
let start_index = i * 4;
|
||||||
|
|
||||||
output.points[start_index + 4] = offset_end_point[0];
|
let start_point = Vec3::from_slice(&path.points[start_index..start_index + 3]);
|
||||||
output.points[start_index + 5] = offset_end_point[1];
|
let end_point =
|
||||||
output.points[start_index + 6] = offset_end_point[2];
|
Vec3::from_slice(&path.points[start_index + 4..start_index + 7]);
|
||||||
|
|
||||||
offset_vec += final_end_point - end_point;
|
let direction = end_point - start_point;
|
||||||
|
|
||||||
|
let length = direction.length();
|
||||||
|
|
||||||
|
let curviness = elasticity.max(0.0001);
|
||||||
|
let strength_arg = evaluate_float(args[1]) * 10.0;
|
||||||
|
let strength = strength_arg / curviness * strength_arg;
|
||||||
|
|
||||||
|
log!(
|
||||||
|
"length: {}, curviness: {}, strength: {}",
|
||||||
|
length,
|
||||||
|
curviness,
|
||||||
|
strength
|
||||||
|
);
|
||||||
|
|
||||||
|
let down_point = Vec3::new(0.0, -length * strength, 0.0);
|
||||||
|
|
||||||
|
let mut mid_point =
|
||||||
|
lerp_vec3(direction, down_point, curviness * alpha.powf(bend_exponent));
|
||||||
|
|
||||||
|
if mid_point[0] == 0.0 && mid_point[2] == 0.0 {
|
||||||
|
mid_point[0] += 0.0001;
|
||||||
|
mid_point[2] += 0.0001;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correct midpoint length
|
||||||
|
mid_point *= length / mid_point.length();
|
||||||
|
|
||||||
|
let final_end_point = start_point + mid_point;
|
||||||
|
let offset_end_point = end_point + offset_vec;
|
||||||
|
|
||||||
|
output.points[start_index + 4] = offset_end_point[0];
|
||||||
|
output.points[start_index + 5] = offset_end_point[1];
|
||||||
|
output.points[start_index + 6] = offset_end_point[2];
|
||||||
|
|
||||||
|
offset_vec += final_end_point - end_point;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
output_data
|
output_data
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ edition = "2018"
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
glam = "0.30.10"
|
||||||
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
|
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
|
||||||
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
|
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
|
||||||
|
|||||||
@@ -19,6 +19,33 @@
|
|||||||
"max": 64,
|
"max": 64,
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"hidden": true
|
"hidden": true
|
||||||
|
},
|
||||||
|
"yCurve": {
|
||||||
|
"type": "float",
|
||||||
|
"description": "Curl the leaf upward along its length (radians). 0 = flat, ~1.57 = 90° tip curl.",
|
||||||
|
"min": -3.14,
|
||||||
|
"max": 3.14,
|
||||||
|
"step": 0.05,
|
||||||
|
"value": 0,
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
"yTwist": {
|
||||||
|
"type": "float",
|
||||||
|
"description": "Twist around the leaf's spine. Combined with yCurve, produces a 3D spiral.",
|
||||||
|
"min": -6.28,
|
||||||
|
"max": 6.28,
|
||||||
|
"step": 0.05,
|
||||||
|
"value": 0,
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
"xCurve": {
|
||||||
|
"type": "float",
|
||||||
|
"description": "Curl each cross-section into an arc, mirrored around the midrib. 0 = flat, ~1.57 = U-shape.",
|
||||||
|
"min": -3.14,
|
||||||
|
"max": 3.14,
|
||||||
|
"step": 0.05,
|
||||||
|
"value": 0,
|
||||||
|
"hidden": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
|
use glam::Vec3;
|
||||||
use nodarium_macros::nodarium_definition_file;
|
use nodarium_macros::nodarium_definition_file;
|
||||||
use nodarium_macros::nodarium_execute;
|
use nodarium_macros::nodarium_execute;
|
||||||
use nodarium_utils::encode_float;
|
use nodarium_utils::encode_float;
|
||||||
@@ -42,6 +43,9 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
|
|||||||
let input_path = split_args(args[0])[0];
|
let input_path = split_args(args[0])[0];
|
||||||
let size = evaluate_float(args[1]);
|
let size = evaluate_float(args[1]);
|
||||||
let width_resolution = evaluate_int(args[2]).max(3) as usize;
|
let width_resolution = evaluate_int(args[2]).max(3) as usize;
|
||||||
|
let y_curve = evaluate_float(args[3]);
|
||||||
|
let y_twist = evaluate_float(args[4]);
|
||||||
|
let x_curve = evaluate_float(args[5]);
|
||||||
let path_length = (input_path.len() - 4) / 2;
|
let path_length = (input_path.len() - 4) / 2;
|
||||||
|
|
||||||
let slice_count = path_length;
|
let slice_count = path_length;
|
||||||
@@ -93,27 +97,97 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
|
|||||||
|
|
||||||
// Writing Positions
|
// Writing Positions
|
||||||
let width = 50.0;
|
let width = 50.0;
|
||||||
|
let leaf_length: f32 = 100.0;
|
||||||
let mut positions = vec![[0.0f32; 3]; position_amount];
|
let mut positions = vec![[0.0f32; 3]; position_amount];
|
||||||
|
|
||||||
|
// Pre-compute a local frame (center, normal=local-Y, binormal=local-X) for
|
||||||
|
// each slice by walking the FK chain. At each step we bend around the
|
||||||
|
// current binormal (curls the leaf) and twist around the current tangent
|
||||||
|
// (rotates the bend plane → spiral).
|
||||||
|
let segs = (slice_count - 1).max(1) as f32;
|
||||||
|
let bend_per_step = y_curve / segs;
|
||||||
|
let twist_per_step = y_twist / segs;
|
||||||
|
|
||||||
|
let mut centers: Vec<Vec3> = Vec::with_capacity(slice_count);
|
||||||
|
let mut frame_n: Vec<Vec3> = Vec::with_capacity(slice_count);
|
||||||
|
let mut frame_b: Vec<Vec3> = Vec::with_capacity(slice_count);
|
||||||
|
|
||||||
|
let mut tangent = Vec3::new(0.0, 0.0, 1.0);
|
||||||
|
let mut normal = Vec3::new(0.0, 1.0, 0.0);
|
||||||
|
let mut binormal = Vec3::new(1.0, 0.0, 0.0);
|
||||||
|
|
||||||
|
let pz_first = decode_float(input_path[2 + 1]);
|
||||||
|
let mut center = Vec3::new(0.0, 0.0, pz_first - leaf_length);
|
||||||
|
|
||||||
for i in 0..slice_count {
|
for i in 0..slice_count {
|
||||||
let ax = i as f32 / (slice_count -1) as f32;
|
centers.push(center);
|
||||||
|
frame_n.push(normal);
|
||||||
|
frame_b.push(binormal);
|
||||||
|
|
||||||
|
if i + 1 < slice_count {
|
||||||
|
let pz_curr = decode_float(input_path[2 + i * 2 + 1]);
|
||||||
|
let pz_next = decode_float(input_path[2 + (i + 1) * 2 + 1]);
|
||||||
|
let seg_len = pz_next - pz_curr;
|
||||||
|
|
||||||
|
center = center + tangent * seg_len;
|
||||||
|
|
||||||
|
// Bend around binormal — tilts tangent toward normal
|
||||||
|
let (sin_b, cos_b) = bend_per_step.sin_cos();
|
||||||
|
let new_t = tangent * cos_b + normal * sin_b;
|
||||||
|
let new_n = -tangent * sin_b + normal * cos_b;
|
||||||
|
tangent = new_t;
|
||||||
|
normal = new_n;
|
||||||
|
|
||||||
|
// Twist around tangent — rotates normal/binormal so the next bend
|
||||||
|
// happens in a rotated plane
|
||||||
|
let (sin_tw, cos_tw) = twist_per_step.sin_cos();
|
||||||
|
let new_n2 = normal * cos_tw + binormal * sin_tw;
|
||||||
|
let new_b = -normal * sin_tw + binormal * cos_tw;
|
||||||
|
normal = new_n2;
|
||||||
|
binormal = new_b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..slice_count {
|
||||||
|
let ax = i as f32 / segs;
|
||||||
let px = decode_float(input_path[2 + i * 2 + 0]);
|
let px = decode_float(input_path[2 + i * 2 + 0]);
|
||||||
let pz = decode_float(input_path[2 + i * 2 + 1]);
|
let hw = width - px; // half-width at this slice
|
||||||
|
|
||||||
|
let c = centers[i];
|
||||||
|
let n = frame_n[i];
|
||||||
|
let b = frame_b[i];
|
||||||
|
|
||||||
for j in 0..width_resolution {
|
for j in 0..width_resolution {
|
||||||
let alpha = j as f32 / (width_resolution - 1) as f32;
|
let alpha = j as f32 / (width_resolution - 1) as f32;
|
||||||
let x = 2.0 * (-px * (alpha - 0.5) + alpha * width);
|
// Signed cross-section parameter, -1 (left edge) → +1 (right edge)
|
||||||
let py = calculate_y(alpha-0.5)*5.0*(ax*PI).sin();
|
let t = 2.0 * alpha - 1.0;
|
||||||
let pz_val = pz - 100.0;
|
let py_local = calculate_y(alpha - 0.5) * 5.0 * (ax * PI).sin();
|
||||||
|
|
||||||
|
// X-curl: each cross-section traces a circular arc with curvature
|
||||||
|
// x_curve / hw. Because theta = x_curve * t is signed around the
|
||||||
|
// midrib, sin/cos give a mirrored arc (left and right edges curl
|
||||||
|
// the same direction).
|
||||||
|
let theta = x_curve * t;
|
||||||
|
let (sin_t, cos_t) = theta.sin_cos();
|
||||||
|
let (b_arc, n_arc) = if x_curve.abs() < 0.0001 {
|
||||||
|
(t * hw, 0.0)
|
||||||
|
} else {
|
||||||
|
let r = hw / x_curve;
|
||||||
|
(r * sin_t, r * (1.0 - cos_t))
|
||||||
|
};
|
||||||
|
// Cross-section bulge follows the rotated local frame
|
||||||
|
let b_total = b_arc - py_local * sin_t;
|
||||||
|
let n_total = n_arc + py_local * cos_t;
|
||||||
|
|
||||||
|
let world = c + b * b_total + n * n_total;
|
||||||
|
|
||||||
let pos_idx = i * width_resolution + j;
|
let pos_idx = i * width_resolution + j;
|
||||||
positions[pos_idx] = [x - width, py, pz_val];
|
positions[pos_idx] = [world.x, world.y, world.z];
|
||||||
|
|
||||||
let flat_idx = offset + pos_idx * 3;
|
let flat_idx = offset + pos_idx * 3;
|
||||||
out[flat_idx + 0] = encode_float((x - width) * size);
|
out[flat_idx + 0] = encode_float(world.x * size);
|
||||||
out[flat_idx + 1] = encode_float(py * size);
|
out[flat_idx + 1] = encode_float(world.y * size);
|
||||||
out[flat_idx + 2] = encode_float(pz_val * size);
|
out[flat_idx + 2] = encode_float(world.z * size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ edition = "2018"
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
glam = "0.30.10"
|
||||||
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
|
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
|
||||||
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
|
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
|
||||||
noise = "0.9.0"
|
noise = "0.9.0"
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
},
|
},
|
||||||
"strength": {
|
"strength": {
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"min": 0.1,
|
"min": 0,
|
||||||
"max": 10,
|
"max": 1,
|
||||||
"value": 2
|
"value": 0.5
|
||||||
},
|
},
|
||||||
"fixBottom": {
|
"fixBottom": {
|
||||||
"type": "float",
|
"type": "float",
|
||||||
@@ -52,6 +52,12 @@
|
|||||||
"max": 5,
|
"max": 5,
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"hidden": true
|
"hidden": true
|
||||||
|
},
|
||||||
|
"preserveLength": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Preserve length",
|
||||||
|
"value": true,
|
||||||
|
"hidden": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use glam::Vec3;
|
||||||
use nodarium_macros::nodarium_definition_file;
|
use nodarium_macros::nodarium_definition_file;
|
||||||
use nodarium_macros::nodarium_execute;
|
use nodarium_macros::nodarium_execute;
|
||||||
use nodarium_utils::{
|
use nodarium_utils::{
|
||||||
@@ -30,6 +31,7 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
|
|||||||
let depth = evaluate_int(args[6]);
|
let depth = evaluate_int(args[6]);
|
||||||
|
|
||||||
let octaves = evaluate_int(args[7]);
|
let octaves = evaluate_int(args[7]);
|
||||||
|
let preserve_length = evaluate_int(args[8]) != 0;
|
||||||
|
|
||||||
let noise_x: HybridMulti<OpenSimplex> =
|
let noise_x: HybridMulti<OpenSimplex> =
|
||||||
HybridMulti::new(seed as u32 + 1).set_octaves(octaves as usize);
|
HybridMulti::new(seed as u32 + 1).set_octaves(octaves as usize);
|
||||||
@@ -65,24 +67,82 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
|
|||||||
|
|
||||||
let length = path.get_length() as f64;
|
let length = path.get_length() as f64;
|
||||||
|
|
||||||
for i in 0..path.length {
|
if preserve_length {
|
||||||
let a = i as f64 / (path.length - 1) as f64;
|
// Snapshot original positions so we can derive each segment's original
|
||||||
|
// direction even after we've modified earlier points.
|
||||||
|
let orig: Vec<f32> = path.points[..path.length * 4].to_vec();
|
||||||
|
|
||||||
let px = j as f64 + a * length * scale;
|
// Anchor the base (fix_bottom=1 → scale=0, no displacement at root)
|
||||||
let py = a * scale as f64;
|
let scale0 = lerp(1.0, 0.0, fix_bottom);
|
||||||
|
path.points[0] += noise_x.get([j as f64, 0.0]) as f32
|
||||||
path.points[i * 4] += noise_x.get([px, py]) as f32
|
|
||||||
* directional_strength[0]
|
* directional_strength[0]
|
||||||
* strength
|
* strength
|
||||||
* lerp(1.0, a as f32, fix_bottom);
|
* scale0;
|
||||||
path.points[i * 4 + 1] += noise_y.get([px, py]) as f32
|
path.points[1] += noise_y.get([j as f64, 0.0]) as f32
|
||||||
* directional_strength[1]
|
* directional_strength[1]
|
||||||
* strength
|
* strength
|
||||||
* lerp(1.0, a as f32, fix_bottom);
|
* scale0;
|
||||||
path.points[i * 4 + 2] += noise_z.get([px, py]) as f32
|
path.points[2] += noise_z.get([j as f64, 0.0]) as f32
|
||||||
* directional_strength[2]
|
* directional_strength[2]
|
||||||
* strength
|
* strength
|
||||||
* lerp(1.0, a as f32, fix_bottom);
|
* scale0;
|
||||||
|
let mut prev = Vec3::new(path.points[0], path.points[1], path.points[2]);
|
||||||
|
|
||||||
|
for i in 1..path.length {
|
||||||
|
let a = i as f64 / (path.length - 1) as f64;
|
||||||
|
let px = j as f64 + a * length * scale;
|
||||||
|
let py = a * scale as f64;
|
||||||
|
let sf = lerp(1.0, a as f32, fix_bottom);
|
||||||
|
|
||||||
|
let orig_dir = Vec3::new(
|
||||||
|
orig[i * 4] - orig[(i - 1) * 4],
|
||||||
|
orig[i * 4 + 1] - orig[(i - 1) * 4 + 1],
|
||||||
|
orig[i * 4 + 2] - orig[(i - 1) * 4 + 2],
|
||||||
|
);
|
||||||
|
let orig_len = orig_dir.length();
|
||||||
|
|
||||||
|
let perturb = Vec3::new(
|
||||||
|
noise_x.get([px, py]) as f32 * directional_strength[0] * strength * sf,
|
||||||
|
noise_y.get([px, py]) as f32 * directional_strength[1] * strength * sf,
|
||||||
|
noise_z.get([px, py]) as f32 * directional_strength[2] * strength * sf,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Perturb the original direction and rescale to original length.
|
||||||
|
// Biasing toward orig_dir prevents the segment from folding back.
|
||||||
|
let mut new_dir = orig_dir + perturb;
|
||||||
|
let nd_len = new_dir.length();
|
||||||
|
if nd_len > 0.0001 && orig_len > 0.0001 {
|
||||||
|
new_dir *= orig_len / nd_len;
|
||||||
|
} else {
|
||||||
|
new_dir = orig_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cur = prev + new_dir;
|
||||||
|
path.points[i * 4] = cur.x;
|
||||||
|
path.points[i * 4 + 1] = cur.y;
|
||||||
|
path.points[i * 4 + 2] = cur.z;
|
||||||
|
prev = cur;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i in 0..path.length {
|
||||||
|
let a = i as f64 / (path.length - 1) as f64;
|
||||||
|
let px = j as f64 + a * length * scale;
|
||||||
|
let py = a * scale as f64;
|
||||||
|
let sf = lerp(1.0, a as f32, fix_bottom);
|
||||||
|
|
||||||
|
path.points[i * 4] += noise_x.get([px, py]) as f32
|
||||||
|
* directional_strength[0]
|
||||||
|
* strength
|
||||||
|
* sf;
|
||||||
|
path.points[i * 4 + 1] += noise_y.get([px, py]) as f32
|
||||||
|
* directional_strength[1]
|
||||||
|
* strength
|
||||||
|
* sf;
|
||||||
|
path.points[i * 4 + 2] += noise_z.get([px, py]) as f32
|
||||||
|
* directional_strength[2]
|
||||||
|
* strength
|
||||||
|
* sf;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
path_data
|
path_data
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"internal": true,
|
"internal": true,
|
||||||
"hidden": true,
|
"hidden": true,
|
||||||
"value": true,
|
"value": false,
|
||||||
"description": "If multiple objects are connected, should we rotate them as one or spread them?"
|
"description": "If multiple objects are connected, should we rotate them as one or spread them?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ export const NodeInputSchema = z.union([
|
|||||||
NodeInputIntegerSchema,
|
NodeInputIntegerSchema,
|
||||||
NodeInputShapeSchema,
|
NodeInputShapeSchema,
|
||||||
NodeInputSelectSchema,
|
NodeInputSelectSchema,
|
||||||
NodeInputSeedSchema,
|
|
||||||
NodeInputVec3Schema,
|
NodeInputVec3Schema,
|
||||||
NodeInputGeometrySchema,
|
NodeInputGeometrySchema,
|
||||||
NodeInputPathSchema,
|
NodeInputPathSchema,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import JsonViewer from './JsonViewer.svelte';
|
import JsonViewer from './JsonViewer.svelte';
|
||||||
|
import { toast } from './toast.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
value,
|
value,
|
||||||
@@ -70,6 +71,11 @@
|
|||||||
let prevJson = '';
|
let prevJson = '';
|
||||||
let flashTimeout: ReturnType<typeof setTimeout> | null = null;
|
let flashTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
function copyValue() {
|
||||||
|
navigator.clipboard.writeText(JSON.stringify(key ? { [key]: value } : value, null, 2));
|
||||||
|
toast('Value copied to clipboard', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const json = JSON.stringify(value);
|
const json = JSON.stringify(value);
|
||||||
if (prevJson && json !== prevJson) {
|
if (prevJson && json !== prevJson) {
|
||||||
@@ -92,7 +98,7 @@
|
|||||||
<button
|
<button
|
||||||
class="text-text hover:bg-layer-3 cursor-pointer"
|
class="text-text hover:bg-layer-3 cursor-pointer"
|
||||||
title="Copy value"
|
title="Copy value"
|
||||||
onclick={() => navigator.clipboard.writeText(JSON.stringify({ [key]: value }, null, 2))}
|
onclick={() => copyValue()}
|
||||||
>
|
>
|
||||||
{key}
|
{key}
|
||||||
</button><span class="text-text/40">: </span>
|
</button><span class="text-text/40">: </span>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { fly, slide } from 'svelte/transition';
|
||||||
import { toasts } from './toast.svelte';
|
import { toasts } from './toast.svelte';
|
||||||
|
|
||||||
const typeClasses: Record<string, string> = {
|
const typeClasses: Record<string, string> = {
|
||||||
@@ -9,28 +10,22 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="fixed bottom-4 right-4 flex flex-col gap-2 z-[9999] pointer-events-none"
|
class="fixed bottom-4 right-4 flex flex-col items-end gap-2 z-9999 pointer-events-none"
|
||||||
role="status"
|
role="status"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
aria-atomic="false"
|
aria-atomic="false"
|
||||||
>
|
>
|
||||||
{#each toasts.value as item (item.id)}
|
{#each toasts.value as item (item.id)}
|
||||||
<div
|
<div
|
||||||
|
in:slide={{ duration: 250 }}
|
||||||
|
out:fly={{ x: 100, duration: 250 }}
|
||||||
class="
|
class="
|
||||||
bg-layer-2 text-text border border-outline rounded
|
bg-layer-2 text-text border border-outline rounded
|
||||||
px-3.5 py-2 text-sm min-w-[180px] max-w-xs
|
px-3.5 py-2 text-sm min-w-45 max-w-xs w-fit
|
||||||
border-l-3 {typeClasses[item.type] ?? 'border-l-outline'}
|
border-l-3 {typeClasses[item.type] ?? 'border-l-outline'}
|
||||||
animate-[slide-in_0.18s_ease]
|
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{item.message}
|
{item.message}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
@keyframes slide-in {
|
|
||||||
from { opacity: 0; transform: translateX(12px); }
|
|
||||||
to { opacity: 1; transform: translateX(0); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user