3 Commits

Author SHA1 Message Date
release-bot
3f440728fc feat: implement variable height for node shader
Some checks failed
🚀 Lint & Test & Deploy / release (pull_request) Failing after 1m3s
2026-02-12 18:11:14 +01:00
release-bot
da09f8ba1e refactor: move debug node into runtime 2026-02-12 16:18:29 +01:00
release-bot
ddc3b4ce35 feat: allow variable height node parameters 2026-02-12 16:18:12 +01:00
10 changed files with 184 additions and 127 deletions

View File

@@ -1,8 +1,9 @@
import type { NodeDefinition, NodeInstance, Socket } from '@nodarium/types'; import type { NodeInstance, 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';
import type { OrthographicCamera, Vector3 } from 'three'; import type { OrthographicCamera, Vector3 } from 'three';
import type { GraphManager } from './graph-manager.svelte'; import type { GraphManager } from './graph-manager.svelte';
import { getNodeHeight, getSocketPosition } from './helpers/nodeHelpers';
const graphStateKey = Symbol('graph-state'); const graphStateKey = Symbol('graph-state');
export function getGraphState() { export function getGraphState() {
@@ -159,76 +160,6 @@ export class GraphState {
return 1; return 1;
} }
getParameterHeight(node: NodeDefinition, inputKey: string) {
const input = node.inputs?.[inputKey];
if (!input) {
return 100;
}
if (input.type === 'shape' && input.external !== true) {
return 200;
}
if (
input?.label !== '' && !input.external && input.type !== 'path'
&& input.type !== 'geometry'
) {
return 100;
}
return 50;
}
getSocketPosition(
node: NodeInstance,
index: string | number
): [number, number] {
if (typeof index === 'number') {
return [
(node?.state?.x ?? node.position[0]) + 20,
(node?.state?.y ?? node.position[1]) + 2.5 + 10 * index
];
} else {
let height = 5;
let nodeType = node.state.type!;
const inputs = nodeType.inputs || {};
for (const inputKey in inputs) {
const h = this.getParameterHeight(nodeType, inputKey) / 10;
console.log({ inputKey, h });
if (inputKey === index) {
height += h / 2;
break;
}
height += h;
}
return [
node?.state?.x ?? node.position[0],
(node?.state?.y ?? node.position[1]) + height
];
}
}
private nodeHeightCache: Record<string, number> = {};
getNodeHeight(nodeTypeId: string) {
if (nodeTypeId in this.nodeHeightCache) {
return this.nodeHeightCache[nodeTypeId];
}
const node = this.graph.getNodeType(nodeTypeId);
if (!node?.inputs) {
return 5;
}
let height = 5;
console.log('Get Node Height', nodeTypeId);
for (const key in node.inputs) {
const h = this.getParameterHeight(node, key);
console.log({ key, h });
height += h;
}
this.nodeHeightCache[nodeTypeId] = height;
console.log(this.nodeHeightCache);
return height;
}
copyNodes() { copyNodes() {
if (this.activeNodeId === -1 && !this.selectedNodes?.size) { if (this.activeNodeId === -1 && !this.selectedNodes?.size) {
return; return;
@@ -286,7 +217,7 @@ export class GraphState {
if (edge[3] === index) { if (edge[3] === index) {
node = edge[0]; node = edge[0];
index = edge[1]; index = edge[1];
position = this.getSocketPosition(node, index); position = getSocketPosition(node, index);
this.graph.removeEdge(edge); this.graph.removeEdge(edge);
break; break;
} }
@@ -306,7 +237,7 @@ export class GraphState {
return { return {
node, node,
index, index,
position: this.getSocketPosition(node, index) position: getSocketPosition(node, index)
}; };
}); });
} }
@@ -343,7 +274,7 @@ export class GraphState {
for (const node of this.graph.nodes.values()) { for (const node of this.graph.nodes.values()) {
const x = node.position[0]; const x = node.position[0];
const y = node.position[1]; const y = node.position[1];
const height = this.getNodeHeight(node.type); const height = getNodeHeight(node.state.type!);
if (downX > x && downX < x + 20 && downY > y && downY < y + height) { if (downX > x && downX < x + 20 && downY > y && downY < y + height) {
clickedNodeId = node.id; clickedNodeId = node.id;
break; break;
@@ -355,14 +286,12 @@ export class GraphState {
} }
isNodeInView(node: NodeInstance) { isNodeInView(node: NodeInstance) {
const height = this.getNodeHeight(node.type); const height = getNodeHeight(node.state.type!);
const width = 20; const width = 20;
const inView = node.position[0] > this.cameraBounds[0] - width return node.position[0] > this.cameraBounds[0] - width
&& node.position[0] < this.cameraBounds[1] && node.position[0] < this.cameraBounds[1]
&& node.position[1] > this.cameraBounds[2] - height && node.position[1] > this.cameraBounds[2] - height
&& node.position[1] < this.cameraBounds[3]; && node.position[1] < this.cameraBounds[3];
console.log({ inView, height });
return inView;
} }
openNodePalette() { openNodePalette() {

View File

@@ -11,6 +11,7 @@
import Debug from '../debug/Debug.svelte'; import Debug from '../debug/Debug.svelte';
import EdgeEl from '../edges/Edge.svelte'; import EdgeEl from '../edges/Edge.svelte';
import { getGraphManager, getGraphState } from '../graph-state.svelte'; import { getGraphManager, getGraphState } from '../graph-state.svelte';
import { getSocketPosition } from '../helpers/nodeHelpers';
import NodeEl from '../node/Node.svelte'; import NodeEl from '../node/Node.svelte';
import { maxZoom, minZoom } from './constants'; import { maxZoom, minZoom } from './constants';
import { FileDropEventManager } from './drop.events'; import { FileDropEventManager } from './drop.events';
@@ -38,8 +39,8 @@
return [0, 0, 0, 0]; return [0, 0, 0, 0];
} }
const pos1 = graphState.getSocketPosition(fromNode, edge[1]); const pos1 = getSocketPosition(fromNode, edge[1]);
const pos2 = graphState.getSocketPosition(toNode, edge[3]); const pos2 = getSocketPosition(toNode, edge[3]);
return [pos1[0], pos1[1], pos2[0], pos2[1]]; return [pos1[0], pos1[1], pos2[0], pos2[1]];
} }

View File

@@ -3,6 +3,7 @@ import { type NodeInstance } from '@nodarium/types';
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';
import { snapToGrid as snapPointToGrid } from '../helpers'; import { snapToGrid as snapPointToGrid } from '../helpers';
import { getNodeHeight } from '../helpers/nodeHelpers';
import { maxZoom, minZoom, zoomSpeed } from './constants'; import { maxZoom, minZoom, zoomSpeed } from './constants';
import { EdgeInteractionManager } from './edge.events'; import { EdgeInteractionManager } from './edge.events';
@@ -289,7 +290,7 @@ export class MouseEventManager {
if (!node?.state) continue; if (!node?.state) continue;
const x = node.position[0]; const x = node.position[0];
const y = node.position[1]; const y = node.position[1];
const height = this.state.getNodeHeight(node.type); const height = getNodeHeight(node.state.type!);
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) { if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
this.state.selectedNodes?.add(node.id); this.state.selectedNodes?.add(node.id);
} else { } else {

View File

@@ -35,6 +35,9 @@ export function createNodePath({
rightBump = false, rightBump = false,
aspectRatio = 1 aspectRatio = 1
} = {}) { } = {}) {
const leftBumpTopY = y + height / 2;
const leftBumpBottomY = y - height / 2;
return `M0,${cornerTop} return `M0,${cornerTop}
${ ${
cornerTop cornerTop
@@ -64,9 +67,7 @@ export function createNodePath({
} }
${ ${
leftBump leftBump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${ ? ` V${leftBumpTopY} C${depth},${leftBumpTopY} ${depth},${leftBumpBottomY} 0,${leftBumpBottomY}`
y - height / 2
}`
: ` H0` : ` H0`
} }
Z`.replace(/\s+/g, ' '); Z`.replace(/\s+/g, ' ');

View File

@@ -0,0 +1,71 @@
import type { NodeDefinition, NodeInstance } from '@nodarium/types';
export function getParameterHeight(node: NodeDefinition, inputKey: string) {
const input = node.inputs?.[inputKey];
if (!input) {
return 0;
}
if (inputKey === 'seed') return 0;
if (!node.inputs) return 0;
if ('setting' in input) return 0;
if (input.hidden) return 0;
if (input.type === 'shape' && input.external !== true) {
return 200;
}
if (
input?.label !== '' && !input.external && input.type !== 'path'
&& input.type !== 'geometry'
) {
return 100;
}
return 50;
}
export function getSocketPosition(
node: NodeInstance,
index: string | number
): [number, number] {
if (typeof index === 'number') {
return [
(node?.state?.x ?? node.position[0]) + 20,
(node?.state?.y ?? node.position[1]) + 2.5 + 10 * index
];
} else {
let height = 5;
let nodeType = node.state.type!;
const inputs = nodeType.inputs || {};
for (const inputKey in inputs) {
const h = getParameterHeight(nodeType, inputKey) / 10;
if (inputKey === index) {
height += h / 2;
break;
}
height += h;
}
return [
node?.state?.x ?? node.position[0],
(node?.state?.y ?? node.position[1]) + height
];
}
}
const nodeHeightCache: Record<string, number> = {};
export function getNodeHeight(node: NodeDefinition) {
if (node.id in nodeHeightCache) {
return nodeHeightCache[node.id];
}
if (!node?.inputs) {
return 5;
}
let height = 5;
for (const key in node.inputs) {
const h = getParameterHeight(node, key) / 10;
height += h;
}
nodeHeightCache[node.id] = height;
return height;
}

View File

@@ -1,56 +1,88 @@
varying vec2 vUv; varying vec2 vUv;
uniform float uWidth; uniform float uWidth;
uniform float uHeight; uniform float uHeight;
uniform float uZoom;
uniform vec3 uColorDark; uniform vec3 uColorDark;
uniform vec3 uColorBright; uniform vec3 uColorBright;
uniform vec3 uStrokeColor; uniform vec3 uStrokeColor;
uniform float uStrokeWidth;
const float uHeaderHeight = 5.0;
uniform float uSectionHeights[16];
uniform int uNumSections;
float msign(in float x) { return (x < 0.0) ? -1.0 : 1.0; } float msign(in float x) { return (x < 0.0) ? -1.0 : 1.0; }
float sdCircle(vec2 p, float r) { return length(p) - r; }
vec4 roundedBoxSDF( in vec2 p, in vec2 b, in float r, in float s) { vec4 roundedBoxSDF( in vec2 p, in vec2 b, in float r, in float s) {
vec2 q = abs(p) - b + r; vec2 q = abs(p) - b + r;
float l = b.x + b.y + 1.570796 * r; float l = b.x + b.y + 1.570796 * r;
float k1 = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r; float k1 = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
float k2 = ((q.x > 0.0) ? atan(q.y, q.x) : 1.570796); float k2 = ((q.x > 0.0) ? atan(q.y, q.x) : 1.570796);
float k3 = 3.0 + 2.0 * msign(min(p.x, -p.y)) - msign(p.x); float k3 = 3.0 + 2.0 * msign(min(p.x, -p.y)) - msign(p.x);
float k4 = msign(p.x * p.y); float k4 = msign(p.x * p.y);
float k5 = r * k2 + max(-q.x, 0.0); float k5 = r * k2 + max(-q.x, 0.0);
float ra = s * round(k1 / s); float ra = s * round(k1 / s);
float l2 = l + 1.570796 * ra; float l2 = l + 1.570796 * ra;
return vec4(k1 - ra, k3 * l2 + k4 * (b.y + ((q.y > 0.0) ? k5 + k2 * ra : q.y)), 4.0 * l2, k1); return vec4(k1 - ra, k3 * l2 + k4 * (b.y + ((q.y > 0.0) ? k5 + k2 * ra : q.y)), 4.0 * l2, k1);
} }
void main(){ void main(){
float strokeWidth = mix(2.0, 0.5, uZoom);
float borderRadius = 0.5;
float dentRadius = 0.8;
float y = (1.0-vUv.y) * uHeight; float y = (1.0 - vUv.y) * uHeight;
float x = vUv.x * uWidth; float x = vUv.x * uWidth;
vec2 size = vec2(uWidth, uHeight); vec2 size = vec2(uWidth, uHeight);
vec2 uv = (vUv - 0.5) * 2.0; vec2 uvCenter = (vUv - 0.5) * 2.0;
float u_border_radius = 0.4; vec4 boxData = roundedBoxSDF(uvCenter * size, size, borderRadius * 2.0, 0.0);
vec4 distance = roundedBoxSDF(uv * size, size, u_border_radius*2.0, 0.0); float sceneSDF = boxData.w;
if (distance.w > 0.0 ) { vec2 headerDentPos = vec2(uWidth, uHeaderHeight * 0.5);
// outside float headerDentDist = sdCircle(vec2(x, y) - headerDentPos, dentRadius);
gl_FragColor = vec4(0.0,0.0,0.0, 0.0); sceneSDF = max(sceneSDF, -headerDentDist*2.0);
}else{
if (distance.w > -uStrokeWidth || mod(y+5.0, 10.0) < uStrokeWidth/2.0) { float currentYBoundary = uHeaderHeight;
// draw the outer stroke float previousYBoundary = uHeaderHeight;
gl_FragColor = vec4(uStrokeColor, 1.0);
}else if (y<5.0){ for (int i = 0; i < 16; i++) {
// draw the header if (i >= uNumSections) break;
gl_FragColor = vec4(uColorBright, 1.0);
}else{ float sectionHeight = uSectionHeights[i];
gl_FragColor = vec4(uColorDark, 1.0); currentYBoundary += sectionHeight;
}
float centerY = previousYBoundary + (sectionHeight * 0.5);
vec2 circlePos = vec2(0.0, centerY);
float circleDist = sdCircle(vec2(x, y) - circlePos, dentRadius);
sceneSDF = max(sceneSDF, -circleDist*2.0);
previousYBoundary = currentYBoundary;
}
if (sceneSDF > 0.05) {
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
return;
}
vec3 finalColor = (y < uHeaderHeight) ? uColorBright : uColorDark;
bool isDivider = false;
float dividerY = uHeaderHeight;
if (abs(y - dividerY) < strokeWidth * 0.25) isDivider = true;
for (int i = 0; i < 16; i++) {
if (i >= uNumSections - 1) break;
dividerY += uSectionHeights[i];
if (abs(y - dividerY) < strokeWidth * 0.25) isDivider = true;
}
if (sceneSDF > -strokeWidth || isDivider) {
gl_FragColor = vec4(uStrokeColor, 1.0);
} else {
gl_FragColor = vec4(finalColor, 1.0);
} }
} }

View File

@@ -5,6 +5,7 @@
import { type Mesh } from 'three'; import { type Mesh } from 'three';
import { getGraphState } from '../graph-state.svelte'; import { getGraphState } from '../graph-state.svelte';
import { colors } from '../graph/colors.svelte'; import { colors } from '../graph/colors.svelte';
import { getNodeHeight, getParameterHeight } from '../helpers/nodeHelpers';
import NodeFrag from './Node.frag'; import NodeFrag from './Node.frag';
import NodeVert from './Node.vert'; import NodeVert from './Node.vert';
import NodeHtml from './NodeHTML.svelte'; import NodeHtml from './NodeHTML.svelte';
@@ -14,9 +15,10 @@
type Props = { type Props = {
node: NodeInstance; node: NodeInstance;
inView: boolean; inView: boolean;
z: number;
}; };
let { node = $bindable(), inView, z }: Props = $props(); let { node = $bindable(), inView }: Props = $props();
const nodeType = $derived(node.state.type!);
const isActive = $derived(graphState.activeNodeId === node.id); const isActive = $derived(graphState.activeNodeId === node.id);
const isSelected = $derived(graphState.selectedNodes.has(node.id)); const isSelected = $derived(graphState.selectedNodes.has(node.id));
@@ -29,9 +31,18 @@
: colors.outline) : colors.outline)
); );
const sectionHeights = $derived(
Object
.keys(nodeType.inputs || {})
.map(key => getParameterHeight(nodeType, key) / 10)
.filter(b => !!b)
);
let meshRef: Mesh | undefined = $state(); let meshRef: Mesh | undefined = $state();
const height = graphState.getNodeHeight(node.type); const height = getNodeHeight(node.state.type!);
const zoom = $derived(graphState.cameraPosition[2]);
$effect(() => { $effect(() => {
if (meshRef && !node.state?.mesh) { if (meshRef && !node.state?.mesh) {
@@ -39,6 +50,10 @@
graphState.updateNodePosition(node); graphState.updateNodePosition(node);
} }
}); });
const zoomValue = $derived(
(Math.log(graphState.cameraPosition[2]) - Math.log(1)) / (Math.log(40) - Math.log(1))
);
// const zoomValue = (graphState.cameraPosition[2] - 1) / 39;
</script> </script>
<T.Mesh <T.Mesh
@@ -47,7 +62,7 @@
position.y={0.8} position.y={0.8}
rotation.x={-Math.PI / 2} rotation.x={-Math.PI / 2}
bind:ref={meshRef} bind:ref={meshRef}
visible={inView && z < 7} visible={inView && zoom < 7}
> >
<T.PlaneGeometry args={[20, height]} radius={1} /> <T.PlaneGeometry args={[20, height]} radius={1} />
<T.ShaderMaterial <T.ShaderMaterial
@@ -57,14 +72,19 @@
uniforms={{ uniforms={{
uColorBright: { value: colors['layer-2'] }, uColorBright: { value: colors['layer-2'] },
uColorDark: { value: colors['layer-1'] }, uColorDark: { value: colors['layer-1'] },
uStrokeColor: { value: colors['layer-2'].clone() }, uStrokeColor: { value: colors.outline.clone() },
uStrokeWidth: { value: 1.0 }, uSectionHeights: { value: [5, 10] },
uNumSections: { value: 2 },
uWidth: { value: 20 }, uWidth: { value: 20 },
uHeight: { value: height } uHeight: { value: 200 },
uZoom: { value: 1.0 }
}} }}
uniforms.uStrokeColor.value={strokeColor.clone()} uniforms.uZoom.value={zoomValue}
uniforms.uStrokeWidth.value={(7 - z) / 3} uniforms.uHeight.value={height}
uniforms.uSectionHeights.value={sectionHeights}
uniforms.uNumSections.value={sectionHeights.length}
uniforms.uStrokeColor.value={strokeColor}
/> />
</T.Mesh> </T.Mesh>
<NodeHtml bind:node {inView} {isActive} {isSelected} {z} /> <NodeHtml bind:node {inView} {isActive} {isSelected} z={zoom} />

View File

@@ -3,6 +3,7 @@
import type { NodeInstance } from '@nodarium/types'; import type { NodeInstance } from '@nodarium/types';
import { getGraphState } from '../graph-state.svelte'; import { getGraphState } from '../graph-state.svelte';
import { createNodePath } from '../helpers/index.js'; import { createNodePath } from '../helpers/index.js';
import { getSocketPosition } from '../helpers/nodeHelpers';
const graphState = getGraphState(); const graphState = getGraphState();
@@ -15,7 +16,7 @@
graphState.setDownSocket?.({ graphState.setDownSocket?.({
node, node,
index: 0, index: 0,
position: graphState.getSocketPosition?.(node, 0) position: getSocketPosition?.(node, 0)
}); });
} }
} }

View File

@@ -2,6 +2,7 @@
import type { NodeInput, NodeInstance } from '@nodarium/types'; import type { NodeInput, NodeInstance } from '@nodarium/types';
import { getGraphManager, getGraphState } from '../graph-state.svelte'; import { getGraphManager, getGraphState } from '../graph-state.svelte';
import { createNodePath } from '../helpers'; import { createNodePath } from '../helpers';
import { getParameterHeight, getSocketPosition } from '../helpers/nodeHelpers';
import NodeInputEl from './NodeInput.svelte'; import NodeInputEl from './NodeInput.svelte';
type Props = { type Props = {
@@ -23,7 +24,7 @@
const inputType = $derived(nodeType.inputs?.[id]); const inputType = $derived(nodeType.inputs?.[id]);
const socketId = $derived(`${node.id}-${id}`); const socketId = $derived(`${node.id}-${id}`);
const height = $derived(graphState.getParameterHeight(nodeType, id)); const height = $derived(getParameterHeight(nodeType, id));
function handleMouseDown(ev: MouseEvent) { function handleMouseDown(ev: MouseEvent) {
ev.preventDefault(); ev.preventDefault();
@@ -31,7 +32,7 @@
graphState.setDownSocket({ graphState.setDownSocket({
node, node,
index: id, index: id,
position: graphState.getSocketPosition?.(node, id) position: getSocketPosition(node, id)
}); });
} }
@@ -42,7 +43,7 @@
const path = $derived( const path = $derived(
createNodePath({ createNodePath({
depth: 6, depth: 6,
height: 18, height: 2000 / height,
y: 50.5, y: 50.5,
cornerBottom, cornerBottom,
leftBump, leftBump,
@@ -52,7 +53,7 @@
const pathHover = $derived( const pathHover = $derived(
createNodePath({ createNodePath({
depth: 7, depth: 7,
height: 20, height: 2200 / height,
y: 50.5, y: 50.5,
cornerBottom, cornerBottom,
leftBump, leftBump,

View File

@@ -3,10 +3,10 @@
prefix: "i"; prefix: "i";
} }
@source inline("{hover:,}{bg-,outline-,text-,}layer-0"); @source inline("{hover:,}{bg-,outline-,text-,}layer-0{/30,/50,}");
@source inline("{hover:,}{bg-,outline-,text-,}layer-1"); @source inline("{hover:,}{bg-,outline-,text-,}layer-1{/30,/50,}");
@source inline("{hover:,}{bg-,outline-,text-,}layer-2"); @source inline("{hover:,}{bg-,outline-,text-,}layer-2{/30,/50,}");
@source inline("{hover:,}{bg-,outline-,text-,}layer-3"); @source inline("{hover:,}{bg-,outline-,text-,}layer-3{/30,/50,}");
@source inline("{hover:,}{bg-,outline-,text-,}active"); @source inline("{hover:,}{bg-,outline-,text-,}active");
@source inline("{hover:,}{bg-,outline-,text-,}selected"); @source inline("{hover:,}{bg-,outline-,text-,}selected");
@source inline("{hover:,}{bg-,outline-,text-,}outline{!,}"); @source inline("{hover:,}{bg-,outline-,text-,}outline{!,}");