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 { SvelteMap, SvelteSet } from 'svelte/reactivity';
import type { OrthographicCamera, Vector3 } from 'three';
import type { GraphManager } from './graph-manager.svelte';
import { getNodeHeight, getSocketPosition } from './helpers/nodeHelpers';
const graphStateKey = Symbol('graph-state');
export function getGraphState() {
@@ -159,76 +160,6 @@ export class GraphState {
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() {
if (this.activeNodeId === -1 && !this.selectedNodes?.size) {
return;
@@ -286,7 +217,7 @@ export class GraphState {
if (edge[3] === index) {
node = edge[0];
index = edge[1];
position = this.getSocketPosition(node, index);
position = getSocketPosition(node, index);
this.graph.removeEdge(edge);
break;
}
@@ -306,7 +237,7 @@ export class GraphState {
return {
node,
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()) {
const x = node.position[0];
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) {
clickedNodeId = node.id;
break;
@@ -355,14 +286,12 @@ export class GraphState {
}
isNodeInView(node: NodeInstance) {
const height = this.getNodeHeight(node.type);
const height = getNodeHeight(node.state.type!);
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[1] > this.cameraBounds[2] - height
&& node.position[1] < this.cameraBounds[3];
console.log({ inView, height });
return inView;
}
openNodePalette() {

View File

@@ -11,6 +11,7 @@
import Debug from '../debug/Debug.svelte';
import EdgeEl from '../edges/Edge.svelte';
import { getGraphManager, getGraphState } from '../graph-state.svelte';
import { getSocketPosition } from '../helpers/nodeHelpers';
import NodeEl from '../node/Node.svelte';
import { maxZoom, minZoom } from './constants';
import { FileDropEventManager } from './drop.events';
@@ -38,8 +39,8 @@
return [0, 0, 0, 0];
}
const pos1 = graphState.getSocketPosition(fromNode, edge[1]);
const pos2 = graphState.getSocketPosition(toNode, edge[3]);
const pos1 = getSocketPosition(fromNode, edge[1]);
const pos2 = getSocketPosition(toNode, edge[3]);
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 GraphState } from '../graph-state.svelte';
import { snapToGrid as snapPointToGrid } from '../helpers';
import { getNodeHeight } from '../helpers/nodeHelpers';
import { maxZoom, minZoom, zoomSpeed } from './constants';
import { EdgeInteractionManager } from './edge.events';
@@ -289,7 +290,7 @@ export class MouseEventManager {
if (!node?.state) continue;
const x = node.position[0];
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) {
this.state.selectedNodes?.add(node.id);
} else {

View File

@@ -35,6 +35,9 @@ export function createNodePath({
rightBump = false,
aspectRatio = 1
} = {}) {
const leftBumpTopY = y + height / 2;
const leftBumpBottomY = y - height / 2;
return `M0,${cornerTop}
${
cornerTop
@@ -64,9 +67,7 @@ export function createNodePath({
}
${
leftBump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${
y - height / 2
}`
? ` V${leftBumpTopY} C${depth},${leftBumpTopY} ${depth},${leftBumpBottomY} 0,${leftBumpBottomY}`
: ` H0`
}
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;
uniform float uWidth;
uniform float uHeight;
uniform float uZoom;
uniform vec3 uColorDark;
uniform vec3 uColorBright;
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 sdCircle(vec2 p, float r) { return length(p) - r; }
vec4 roundedBoxSDF( in vec2 p, in vec2 b, in float r, in float s) {
vec2 q = abs(p) - b + 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 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 k4 = msign(p.x * p.y);
float k5 = r * k2 + max(-q.x, 0.0);
float ra = s * round(k1 / s);
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);
}
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 x = vUv.x * uWidth;
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 distance = roundedBoxSDF(uv * size, size, u_border_radius*2.0, 0.0);
vec4 boxData = roundedBoxSDF(uvCenter * size, size, borderRadius * 2.0, 0.0);
float sceneSDF = boxData.w;
if (distance.w > 0.0 ) {
// outside
vec2 headerDentPos = vec2(uWidth, uHeaderHeight * 0.5);
float headerDentDist = sdCircle(vec2(x, y) - headerDentPos, dentRadius);
sceneSDF = max(sceneSDF, -headerDentDist*2.0);
float currentYBoundary = uHeaderHeight;
float previousYBoundary = uHeaderHeight;
for (int i = 0; i < 16; i++) {
if (i >= uNumSections) break;
float sectionHeight = uSectionHeights[i];
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);
}else{
if (distance.w > -uStrokeWidth || mod(y+5.0, 10.0) < uStrokeWidth/2.0) {
// draw the outer stroke
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 if (y<5.0){
// draw the header
gl_FragColor = vec4(uColorBright, 1.0);
} else {
gl_FragColor = vec4(uColorDark, 1.0);
}
gl_FragColor = vec4(finalColor, 1.0);
}
}

View File

@@ -5,6 +5,7 @@
import { type Mesh } from 'three';
import { getGraphState } from '../graph-state.svelte';
import { colors } from '../graph/colors.svelte';
import { getNodeHeight, getParameterHeight } from '../helpers/nodeHelpers';
import NodeFrag from './Node.frag';
import NodeVert from './Node.vert';
import NodeHtml from './NodeHTML.svelte';
@@ -14,9 +15,10 @@
type Props = {
node: NodeInstance;
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 isSelected = $derived(graphState.selectedNodes.has(node.id));
@@ -29,9 +31,18 @@
: colors.outline)
);
const sectionHeights = $derived(
Object
.keys(nodeType.inputs || {})
.map(key => getParameterHeight(nodeType, key) / 10)
.filter(b => !!b)
);
let meshRef: Mesh | undefined = $state();
const height = graphState.getNodeHeight(node.type);
const height = getNodeHeight(node.state.type!);
const zoom = $derived(graphState.cameraPosition[2]);
$effect(() => {
if (meshRef && !node.state?.mesh) {
@@ -39,6 +50,10 @@
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>
<T.Mesh
@@ -47,7 +62,7 @@
position.y={0.8}
rotation.x={-Math.PI / 2}
bind:ref={meshRef}
visible={inView && z < 7}
visible={inView && zoom < 7}
>
<T.PlaneGeometry args={[20, height]} radius={1} />
<T.ShaderMaterial
@@ -57,14 +72,19 @@
uniforms={{
uColorBright: { value: colors['layer-2'] },
uColorDark: { value: colors['layer-1'] },
uStrokeColor: { value: colors['layer-2'].clone() },
uStrokeWidth: { value: 1.0 },
uStrokeColor: { value: colors.outline.clone() },
uSectionHeights: { value: [5, 10] },
uNumSections: { value: 2 },
uWidth: { value: 20 },
uHeight: { value: height }
uHeight: { value: 200 },
uZoom: { value: 1.0 }
}}
uniforms.uStrokeColor.value={strokeColor.clone()}
uniforms.uStrokeWidth.value={(7 - z) / 3}
uniforms.uZoom.value={zoomValue}
uniforms.uHeight.value={height}
uniforms.uSectionHeights.value={sectionHeights}
uniforms.uNumSections.value={sectionHeights.length}
uniforms.uStrokeColor.value={strokeColor}
/>
</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 { getGraphState } from '../graph-state.svelte';
import { createNodePath } from '../helpers/index.js';
import { getSocketPosition } from '../helpers/nodeHelpers';
const graphState = getGraphState();
@@ -15,7 +16,7 @@
graphState.setDownSocket?.({
node,
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 { getGraphManager, getGraphState } from '../graph-state.svelte';
import { createNodePath } from '../helpers';
import { getParameterHeight, getSocketPosition } from '../helpers/nodeHelpers';
import NodeInputEl from './NodeInput.svelte';
type Props = {
@@ -23,7 +24,7 @@
const inputType = $derived(nodeType.inputs?.[id]);
const socketId = $derived(`${node.id}-${id}`);
const height = $derived(graphState.getParameterHeight(nodeType, id));
const height = $derived(getParameterHeight(nodeType, id));
function handleMouseDown(ev: MouseEvent) {
ev.preventDefault();
@@ -31,7 +32,7 @@
graphState.setDownSocket({
node,
index: id,
position: graphState.getSocketPosition?.(node, id)
position: getSocketPosition(node, id)
});
}
@@ -42,7 +43,7 @@
const path = $derived(
createNodePath({
depth: 6,
height: 18,
height: 2000 / height,
y: 50.5,
cornerBottom,
leftBump,
@@ -52,7 +53,7 @@
const pathHover = $derived(
createNodePath({
depth: 7,
height: 20,
height: 2200 / height,
y: 50.5,
cornerBottom,
leftBump,

View File

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