feat/remove-wasm-bindgen #19

Merged
max merged 9 commits from feat/remove-wasm-bindgen into main 2026-01-20 18:57:42 +01:00
31 changed files with 1450 additions and 2128 deletions
Showing only changes of commit bd0c2eaacd - Show all commits

59
.dprint.jsonc Normal file
View File

@@ -0,0 +1,59 @@
{
"$schema": "https://dprint.dev/schemas/v0.json",
"indentWidth": 2,
"typescript": {
// https://dprint.dev/plugins/typescript/config/
"quoteStyle": "preferSingle",
"trailingCommas": "never"
},
"json": {
},
"markdown": {
},
"toml": {
},
"dockerfile": {
},
"ruff": {
},
"jupyter": {
},
"malva": {
},
"markup": {
},
"yaml": {
},
"graphql": {
},
"exec": {
"cwd": "${configDir}",
"commands": [{
"command": "rustfmt --edition 2024",
"exts": ["rs"],
// add the config files for automatic cache invalidation when the rust version or rustfmt config changes
"cacheKeyFiles": [
"rustfmt.toml",
"rust-toolchain.toml"
]
}]
},
"excludes": [
"**/node_modules",
"**/*-lock.yaml"
],
"plugins": [
"https://plugins.dprint.dev/typescript-0.95.13.wasm",
"https://plugins.dprint.dev/json-0.21.1.wasm",
"https://plugins.dprint.dev/markdown-0.20.0.wasm",
"https://plugins.dprint.dev/toml-0.7.0.wasm",
"https://plugins.dprint.dev/dockerfile-0.3.3.wasm",
"https://plugins.dprint.dev/ruff-0.6.11.wasm",
"https://plugins.dprint.dev/jupyter-0.2.1.wasm",
"https://plugins.dprint.dev/g-plane/malva-v0.15.1.wasm",
"https://plugins.dprint.dev/g-plane/markup_fmt-v0.25.3.wasm",
"https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm",
"https://plugins.dprint.dev/g-plane/pretty_graphql-v0.2.3.wasm",
"https://plugins.dprint.dev/exec-0.6.0.json@a054130d458f124f9b5c91484833828950723a5af3f8ff2bd1523bd47b83b364"
]
}

1576
app/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,44 @@
<script lang="ts"> <script lang="ts">
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras'; import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
import { points, lines } from './store.js'; import { points, lines, rects } from "./store.js";
import { T } from '@threlte/core'; import { T } from "@threlte/core";
import { Color } from "three";
</script> </script>
{#each $points as point} {#each $points as point}
<T.Mesh position.x={point.x} position.y={point.y} position.z={point.z} rotation.x={-Math.PI / 2}> <T.Mesh
<T.CircleGeometry args={[0.2, 32]} /> position.x={point.x}
<T.MeshBasicMaterial color="red" /> position.y={point.y}
</T.Mesh> position.z={point.z}
rotation.x={-Math.PI / 2}
>
<T.CircleGeometry args={[0.2, 32]} />
<T.MeshBasicMaterial color="red" />
</T.Mesh>
{/each}
{#each $rects as rect, i}
<T.Mesh
position.x={(rect.minX + rect.maxX) / 2}
position.y={0}
position.z={(rect.minY + rect.maxY) / 2}
rotation.x={-Math.PI / 2}
>
<T.PlaneGeometry args={[rect.maxX - rect.minX, rect.maxY - rect.minY]} />
<T.MeshBasicMaterial
color={new Color().setHSL((i * 1.77) % 1, 1, 0.5)}
opacity={0.9}
/>
</T.Mesh>
{/each} {/each}
{#each $lines as line} {#each $lines as line}
<T.Mesh> <T.Mesh position.y={1}>
<MeshLineGeometry points={line} /> <MeshLineGeometry points={line.points} />
<MeshLineMaterial color="red" linewidth={1} attenuate={false} /> <MeshLineMaterial
</T.Mesh> color={line.color || "red"}
linewidth={1}
attenuate={false}
/>
</T.Mesh>
{/each} {/each}

View File

@@ -1,5 +1,7 @@
import { Vector3 } from "three/src/math/Vector3.js"; import type { Box } from '@nodarium/types';
import { lines, points } from "./store"; import { Vector3 } from 'three/src/math/Vector3.js';
import Component from './Debug.svelte';
import { lines, points, rects } from './store';
export function debugPosition(x: number, y: number) { export function debugPosition(x: number, y: number) {
points.update((p) => { points.update((p) => {
@@ -8,18 +10,28 @@ export function debugPosition(x: number, y: number) {
}); });
} }
export function debugRect(rect: Box) {
console.log(rect);
rects.update((r) => {
r.push(rect);
return r;
});
}
export function clear() { export function clear() {
points.set([]); points.set([]);
lines.set([]); lines.set([]);
rects.set([]);
} }
export function debugLine(line: Vector3[]) { export function debugLine(points: Vector3[], color?: Color) {
lines.update((l) => { lines.update((l) => {
l.push(line); l.push({ points, color });
return l; return l;
}); });
} }
import Component from "./Debug.svelte"; export default Component;
export function clearLines() {
export default Component lines.set([]);
}

View File

@@ -1,6 +1,8 @@
import { writable } from "svelte/store"; import type { Box } from '@nodarium/types';
import { Vector3 } from "three/src/math/Vector3.js"; import { writable } from 'svelte/store';
import type { Color } from 'three';
import { Vector3 } from 'three/src/math/Vector3.js';
export const points = writable<Vector3[]>([]); export const points = writable<Vector3[]>([]);
export const rects = writable<Box[]>([]);
export const lines = writable<Vector3[][]>([]); export const lines = writable<{ points: Vector3[]; color?: Color }[]>([]);

View File

@@ -31,6 +31,10 @@
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js"; import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
import { Vector2 } from "three/src/math/Vector2.js"; import { Vector2 } from "three/src/math/Vector2.js";
import { appSettings } from "$lib/settings/app-settings.svelte"; import { appSettings } from "$lib/settings/app-settings.svelte";
import { getGraphState } from "../graph-state.svelte";
import { onDestroy } from "svelte";
const graphState = getGraphState();
type Props = { type Props = {
x1: number; x1: number;
@@ -38,20 +42,21 @@
x2: number; x2: number;
y2: number; y2: number;
z: number; z: number;
id?: string;
}; };
const { x1, y1, x2, y2, z }: Props = $props(); const { x1, y1, x2, y2, z, id }: Props = $props();
const thickness = $derived(Math.max(0.001, 0.00082 * Math.exp(0.055 * z))); const thickness = $derived(Math.max(0.001, 0.00082 * Math.exp(0.055 * z)));
let points = $state<Vector3[]>([]); let points = $state<Vector3[]>([]);
let lastId: string | null = null; let lastId: string | null = null;
const curveId = $derived(`${x1}-${y1}-${x2}-${y2}`);
function update() { function update() {
const new_x = x2 - x1; const new_x = x2 - x1;
const new_y = y2 - y1; const new_y = y2 - y1;
const curveId = `${x1}-${y1}-${x2}-${y2}`;
if (lastId === curveId) { if (lastId === curveId) {
return; return;
} }
@@ -72,6 +77,15 @@
.getPoints(samples) .getPoints(samples)
.map((p) => new Vector3(p.x, 0, p.y)) .map((p) => new Vector3(p.x, 0, p.y))
.flat(); .flat();
if (id) {
graphState.setEdgeGeometry(
id,
x1,
y1,
$state.snapshot(points) as unknown as Vector3[],
);
}
} }
$effect(() => { $effect(() => {
@@ -79,6 +93,10 @@
update(); update();
} }
}); });
onDestroy(() => {
if (id) graphState.removeEdgeGeometry(id);
});
</script> </script>
<T.Mesh <T.Mesh
@@ -101,6 +119,18 @@
<T.CircleGeometry args={[0.5, 16]} /> <T.CircleGeometry args={[0.5, 16]} />
</T.Mesh> </T.Mesh>
{#if graphState.hoveredEdgeId === id}
<T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
<MeshLineGeometry {points} />
<MeshLineMaterial
width={thickness * 5}
color={lineColor}
opacity={0.5}
transparent
/>
</T.Mesh>
{/if}
<T.Mesh position.x={x1} position.z={y1} position.y={0.1}> <T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
<MeshLineGeometry {points} /> <MeshLineGeometry {points} />
<MeshLineMaterial width={thickness} color={lineColor} /> <MeshLineMaterial width={thickness} color={lineColor} />

View File

@@ -1,31 +1,30 @@
import throttle from '$lib/helpers/throttle';
import type { import type {
Edge, Edge,
Graph, Graph,
NodeInstance,
NodeDefinition, NodeDefinition,
NodeInput,
NodeRegistry,
NodeId, NodeId,
Socket, NodeInput,
} from "@nodarium/types"; NodeInstance,
import { fastHashString } from "@nodarium/utils"; NodeRegistry,
import { SvelteMap } from "svelte/reactivity"; Socket
import EventEmitter from "./helpers/EventEmitter"; } from '@nodarium/types';
import { createLogger } from "@nodarium/utils"; import { fastHashString } from '@nodarium/utils';
import throttle from "$lib/helpers/throttle"; import { createLogger } from '@nodarium/utils';
import { HistoryManager } from "./history-manager"; import { SvelteMap } from 'svelte/reactivity';
import EventEmitter from './helpers/EventEmitter';
import { HistoryManager } from './history-manager';
const logger = createLogger("graph-manager"); const logger = createLogger('graph-manager');
logger.mute(); logger.mute();
const clone = const clone = 'structuredClone' in self
"structuredClone" in self ? self.structuredClone
? self.structuredClone : (args: any) => JSON.parse(JSON.stringify(args));
: (args: any) => JSON.parse(JSON.stringify(args));
function areSocketsCompatible( function areSocketsCompatible(
output: string | undefined, output: string | undefined,
inputs: string | (string | undefined)[] | undefined, inputs: string | (string | undefined)[] | undefined
) { ) {
if (Array.isArray(inputs) && output) { if (Array.isArray(inputs) && output) {
return inputs.includes(output); return inputs.includes(output);
@@ -34,24 +33,23 @@ function areSocketsCompatible(
} }
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) { function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
if (firstEdge[0].id !== secondEdge[0].id) { if (firstEdge[0].id !== secondEdge[0].id) {
return false; return false;
} }
if (firstEdge[1] !== secondEdge[1]) { if (firstEdge[1] !== secondEdge[1]) {
return false return false;
} }
if (firstEdge[2].id !== secondEdge[2].id) { if (firstEdge[2].id !== secondEdge[2].id) {
return false return false;
} }
if (firstEdge[3] !== secondEdge[3]) { if (firstEdge[3] !== secondEdge[3]) {
return false return false;
} }
return true return true;
} }
export class GraphManager extends EventEmitter<{ export class GraphManager extends EventEmitter<{
@@ -62,7 +60,7 @@ export class GraphManager extends EventEmitter<{
values: Record<string, unknown>; values: Record<string, unknown>;
}; };
}> { }> {
status = $state<"loading" | "idle" | "error">(); status = $state<'loading' | 'idle' | 'error'>();
loaded = false; loaded = false;
graph: Graph = { id: 0, nodes: [], edges: [] }; graph: Graph = { id: 0, nodes: [], edges: [] };
@@ -88,7 +86,7 @@ export class GraphManager extends EventEmitter<{
history: HistoryManager = new HistoryManager(); history: HistoryManager = new HistoryManager();
execute = throttle(() => { execute = throttle(() => {
if (this.loaded === false) return; if (this.loaded === false) return;
this.emit("result", this.serialize()); this.emit('result', this.serialize());
}, 10); }, 10);
constructor(public registry: NodeRegistry) { constructor(public registry: NodeRegistry) {
@@ -100,21 +98,21 @@ export class GraphManager extends EventEmitter<{
id: node.id, id: node.id,
position: [...node.position], position: [...node.position],
type: node.type, type: node.type,
props: node.props, props: node.props
})) as NodeInstance[]; })) as NodeInstance[];
const edges = this.edges.map((edge) => [ const edges = this.edges.map((edge) => [
edge[0].id, edge[0].id,
edge[1], edge[1],
edge[2].id, edge[2].id,
edge[3], edge[3]
]) as Graph["edges"]; ]) as Graph['edges'];
const serialized = { const serialized = {
id: this.graph.id, id: this.graph.id,
settings: $state.snapshot(this.settings), settings: $state.snapshot(this.settings),
nodes, nodes,
edges, edges
}; };
logger.log("serializing graph", serialized); logger.log('serializing graph', serialized);
return clone($state.snapshot(serialized)); return clone($state.snapshot(serialized));
} }
@@ -148,6 +146,95 @@ export class GraphManager extends EventEmitter<{
return [...nodes.values()]; return [...nodes.values()];
} }
getEdgeId(e: Edge) {
return `${e[0].id}-${e[1]}-${e[2].id}-${e[3]}`;
}
getEdgeById(id: string): Edge | undefined {
return this.edges.find((e) => this.getEdgeId(e) === id);
}
dropNodeOnEdge(nodeId: number, edge: Edge) {
const draggedNode = this.getNode(nodeId);
if (!draggedNode || !draggedNode.state?.type) return;
const [fromNode, fromSocketIdx, toNode, toSocketKey] = edge;
const draggedInputs = Object.entries(draggedNode.state.type.inputs ?? {});
const draggedOutputs = draggedNode.state.type.outputs ?? [];
const edgeOutputSocketType = fromNode.state?.type?.outputs?.[fromSocketIdx];
const targetInput = toNode.state?.type?.inputs?.[toSocketKey];
const targetAcceptedTypes = [targetInput?.type, ...(targetInput?.accepts || [])];
const bestInputEntry = draggedInputs.find(([_, input]) => {
const accepted = [input.type, ...(input.accepts || [])];
return areSocketsCompatible(edgeOutputSocketType, accepted);
});
const bestOutputIdx = draggedOutputs.findIndex(outputType => areSocketsCompatible(outputType, targetAcceptedTypes));
if (!bestInputEntry || bestOutputIdx === -1) {
logger.error('Could not find compatible sockets for drop');
return;
}
this.startUndoGroup();
this.removeEdge(edge, { applyDeletion: false });
this.createEdge(fromNode, fromSocketIdx, draggedNode, bestInputEntry[0], {
applyUpdate: false
});
this.createEdge(draggedNode, bestOutputIdx, toNode, toSocketKey, {
applyUpdate: false
});
this.saveUndoGroup();
this.execute();
}
getPossibleDropOnEdges(nodeId: number): Edge[] {
const draggedNode = this.getNode(nodeId);
if (!draggedNode || !draggedNode.state?.type) return [];
const draggedInputs = Object.values(draggedNode.state.type.inputs ?? {});
const draggedOutputs = draggedNode.state.type.outputs ?? [];
// Optimization: Pre-calculate parents to avoid cycles
const parentIds = new Set(this.getParentsOfNode(draggedNode).map(n => n.id));
return this.edges.filter((edge) => {
const [fromNode, fromSocketIdx, toNode, toSocketKey] = edge;
// 1. Prevent cycles: If the target node is already a parent, we can't drop here
if (parentIds.has(toNode.id)) return false;
// 2. Prevent self-dropping: Don't drop on edges already connected to this node
if (fromNode.id === nodeId || toNode.id === nodeId) return false;
// 3. Check if edge.source can plug into ANY draggedNode.input
const edgeOutputSocketType = fromNode.state?.type?.outputs?.[fromSocketIdx];
const canPlugIntoDragged = draggedInputs.some(input => {
const acceptedTypes = [input.type, ...(input.accepts || [])];
return areSocketsCompatible(edgeOutputSocketType, acceptedTypes);
});
if (!canPlugIntoDragged) return false;
// 4. Check if ANY draggedNode.output can plug into edge.target
const targetInput = toNode.state?.type?.inputs?.[toSocketKey];
const targetAcceptedTypes = [targetInput?.type, ...(targetInput?.accepts || [])];
const draggedCanPlugIntoTarget = draggedOutputs.some(outputType =>
areSocketsCompatible(outputType, targetAcceptedTypes)
);
return draggedCanPlugIntoTarget;
});
}
getEdgesBetweenNodes(nodes: NodeInstance[]): [number, number, number, string][] { getEdgesBetweenNodes(nodes: NodeInstance[]): [number, number, number, string][] {
const edges = []; const edges = [];
for (const node of nodes) { for (const node of nodes) {
@@ -155,14 +242,14 @@ export class GraphManager extends EventEmitter<{
for (const child of children) { for (const child of children) {
if (nodes.includes(child)) { if (nodes.includes(child)) {
const edge = this.edges.find( const edge = this.edges.find(
(e) => e[0].id === node.id && e[2].id === child.id, (e) => e[0].id === node.id && e[2].id === child.id
); );
if (edge) { if (edge) {
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [ edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [
number, number,
number, number,
number, number,
string, string
]); ]);
} }
} }
@@ -179,18 +266,18 @@ export class GraphManager extends EventEmitter<{
const n = node as NodeInstance; const n = node as NodeInstance;
if (nodeType) { if (nodeType) {
n.state = { n.state = {
type: nodeType, type: nodeType
}; };
} }
return [node.id, n]; return [node.id, n];
}), })
); );
const edges = graph.edges.map((edge) => { const edges = graph.edges.map((edge) => {
const from = nodes.get(edge[0]); const from = nodes.get(edge[0]);
const to = nodes.get(edge[2]); const to = nodes.get(edge[2]);
if (!from || !to) { if (!from || !to) {
throw new Error("Edge references non-existing node"); throw new Error('Edge references non-existing node');
} }
from.state.children = from.state.children || []; from.state.children = from.state.children || [];
from.state.children.push(to); from.state.children.push(to);
@@ -214,21 +301,21 @@ export class GraphManager extends EventEmitter<{
this.loaded = false; this.loaded = false;
this.graph = graph; this.graph = graph;
this.status = "loading"; this.status = 'loading';
this.id = graph.id; this.id = graph.id;
logger.info("loading graph", $state.snapshot(graph)); logger.info('loading graph', $state.snapshot(graph));
const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)])); const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)]));
await this.registry.load(nodeIds); await this.registry.load(nodeIds);
logger.info("loaded node types", this.registry.getAllNodes()); logger.info('loaded node types', this.registry.getAllNodes());
for (const node of this.graph.nodes) { for (const node of this.graph.nodes) {
const nodeType = this.registry.getNode(node.type); const nodeType = this.registry.getNode(node.type);
if (!nodeType) { if (!nodeType) {
logger.error(`Node type not found: ${node.type}`); logger.error(`Node type not found: ${node.type}`);
this.status = "error"; this.status = 'error';
return; return;
} }
// Turn into runtime node // Turn into runtime node
@@ -253,11 +340,11 @@ export class GraphManager extends EventEmitter<{
settingTypes[settingId] = { settingTypes[settingId] = {
__node_type: type.id, __node_type: type.id,
__node_input: key, __node_input: key,
...type.inputs[key], ...type.inputs[key]
}; };
if ( if (
settingValues[settingId] === undefined && settingValues[settingId] === undefined
"value" in type.inputs[key] && 'value' in type.inputs[key]
) { ) {
settingValues[settingId] = type.inputs[key].value; settingValues[settingId] = type.inputs[key].value;
} }
@@ -267,14 +354,14 @@ export class GraphManager extends EventEmitter<{
} }
this.settings = settingValues; this.settings = settingValues;
this.emit("settings", { types: settingTypes, values: settingValues }); this.emit('settings', { types: settingTypes, values: settingValues });
this.history.reset(); this.history.reset();
this._init(this.graph); this._init(this.graph);
this.save(); this.save();
this.status = "idle"; this.status = 'idle';
this.loaded = true; this.loaded = true;
logger.log(`Graph loaded in ${performance.now() - a}ms`); logger.log(`Graph loaded in ${performance.now() - a}ms`);
@@ -307,9 +394,9 @@ export class GraphManager extends EventEmitter<{
if (settingId) { if (settingId) {
settingTypes[settingId] = nodeType.inputs[key]; settingTypes[settingId] = nodeType.inputs[key];
if ( if (
settingValues && settingValues
settingValues?.[settingId] === undefined && && settingValues?.[settingId] === undefined
"value" in nodeType.inputs[key] && 'value' in nodeType.inputs[key]
) { ) {
settingValues[settingId] = nodeType.inputs[key].value; settingValues[settingId] = nodeType.inputs[key].value;
} }
@@ -319,7 +406,7 @@ export class GraphManager extends EventEmitter<{
this.settings = settingValues; this.settings = settingValues;
this.settingTypes = settingTypes; this.settingTypes = settingTypes;
this.emit("settings", { types: settingTypes, values: settingValues }); this.emit('settings', { types: settingTypes, values: settingValues });
} }
getChildren(node: NodeInstance) { getChildren(node: NodeInstance) {
@@ -368,7 +455,7 @@ export class GraphManager extends EventEmitter<{
const inputType = to?.state?.type?.inputs?.[toSocket]?.type; const inputType = to?.state?.type?.inputs?.[toSocket]?.type;
if (outputType === inputType) { if (outputType === inputType) {
this.createEdge(from, fromSocket, to, toSocket, { this.createEdge(from, fromSocket, to, toSocket, {
applyUpdate: false, applyUpdate: false
}); });
continue; continue;
} }
@@ -403,7 +490,7 @@ export class GraphManager extends EventEmitter<{
// map old ids to new ids // map old ids to new ids
const idMap = new Map<number, number>(); const idMap = new Map<number, number>();
let startId = this.createNodeId() let startId = this.createNodeId();
nodes = nodes.map((node) => { nodes = nodes.map((node) => {
const id = startId++; const id = startId++;
@@ -420,7 +507,7 @@ export class GraphManager extends EventEmitter<{
const to = nodes.find((n) => n.id === idMap.get(edge[2])); const to = nodes.find((n) => n.id === idMap.get(edge[2]));
if (!from || !to) { if (!from || !to) {
throw new Error("Edge references non-existing node"); throw new Error('Edge references non-existing node');
} }
to.state.parents = to.state.parents || []; to.state.parents = to.state.parents || [];
@@ -445,11 +532,11 @@ export class GraphManager extends EventEmitter<{
createNode({ createNode({
type, type,
position, position,
props = {}, props = {}
}: { }: {
type: NodeInstance["type"]; type: NodeInstance['type'];
position: NodeInstance["position"]; position: NodeInstance['position'];
props: NodeInstance["props"]; props: NodeInstance['props'];
}) { }) {
const nodeType = this.registry.getNode(type); const nodeType = this.registry.getNode(type);
if (!nodeType) { if (!nodeType) {
@@ -462,14 +549,14 @@ export class GraphManager extends EventEmitter<{
type, type,
position, position,
state: { type: nodeType }, state: { type: nodeType },
props, props
}); });
this.nodes.set(node.id, node); this.nodes.set(node.id, node);
this.save(); this.save();
return node return node;
} }
createEdge( createEdge(
@@ -477,17 +564,16 @@ export class GraphManager extends EventEmitter<{
fromSocket: number, fromSocket: number,
to: NodeInstance, to: NodeInstance,
toSocket: string, toSocket: string,
{ applyUpdate = true } = {}, { applyUpdate = true } = {}
): Edge | undefined { ): Edge | undefined {
const existingEdges = this.getEdgesToNode(to); const existingEdges = this.getEdgesToNode(to);
// check if this exact edge already exists // check if this exact edge already exists
const existingEdge = existingEdges.find( const existingEdge = existingEdges.find(
(e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket, (e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket
); );
if (existingEdge) { if (existingEdge) {
logger.error("Edge already exists", existingEdge); logger.error('Edge already exists', existingEdge);
return; return;
} }
@@ -500,13 +586,13 @@ export class GraphManager extends EventEmitter<{
if (!areSocketsCompatible(fromSocketType, toSocketType)) { if (!areSocketsCompatible(fromSocketType, toSocketType)) {
logger.error( logger.error(
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`, `Socket types do not match: ${fromSocketType} !== ${toSocketType}`
); );
return; return;
} }
const edgeToBeReplaced = this.edges.find( const edgeToBeReplaced = this.edges.find(
(e) => e[2].id === to.id && e[3] === toSocket, (e) => e[2].id === to.id && e[3] === toSocket
); );
if (edgeToBeReplaced) { if (edgeToBeReplaced) {
this.removeEdge(edgeToBeReplaced, { applyDeletion: false }); this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
@@ -533,7 +619,7 @@ export class GraphManager extends EventEmitter<{
const nextState = this.history.undo(); const nextState = this.history.undo();
if (nextState) { if (nextState) {
this._init(nextState); this._init(nextState);
this.emit("save", this.serialize()); this.emit('save', this.serialize());
} }
} }
@@ -541,7 +627,7 @@ export class GraphManager extends EventEmitter<{
const nextState = this.history.redo(); const nextState = this.history.redo();
if (nextState) { if (nextState) {
this._init(nextState); this._init(nextState);
this.emit("save", this.serialize()); this.emit('save', this.serialize());
} }
} }
@@ -558,8 +644,8 @@ export class GraphManager extends EventEmitter<{
if (this.currentUndoGroup) return; if (this.currentUndoGroup) return;
const state = this.serialize(); const state = this.serialize();
this.history.save(state); this.history.save(state);
this.emit("save", state); this.emit('save', state);
logger.log("saving graphs", state); logger.log('saving graphs', state);
} }
getParentsOfNode(node: NodeInstance) { getParentsOfNode(node: NodeInstance) {
@@ -567,7 +653,7 @@ export class GraphManager extends EventEmitter<{
const stack = node.state?.parents?.slice(0); const stack = node.state?.parents?.slice(0);
while (stack?.length) { while (stack?.length) {
if (parents.length > 1000000) { if (parents.length > 1000000) {
logger.warn("Infinite loop detected"); logger.warn('Infinite loop detected');
break; break;
} }
const parent = stack.pop(); const parent = stack.pop();
@@ -586,26 +672,28 @@ export class GraphManager extends EventEmitter<{
return []; return [];
} }
const definitions = typeof socket.index === "string" const definitions = typeof socket.index === 'string'
? allDefinitions.filter(s => { ? allDefinitions.filter(s => {
return s.outputs?.find(_s => Object return s.outputs?.find(_s =>
.values(nodeType?.inputs || {}) Object
.map(s => s.type) .values(nodeType?.inputs || {})
.includes(_s as NodeInput["type"]) .map(s => s.type)
) .includes(_s as NodeInput['type'])
);
}) })
: allDefinitions.filter(s => Object : allDefinitions.filter(s =>
.values(s.inputs ?? {}) Object
.find(s => { .values(s.inputs ?? {})
if (s.hidden) return false; .find(s => {
if (nodeType.outputs?.includes(s.type)) { if (s.hidden) return false;
return true if (nodeType.outputs?.includes(s.type)) {
} return true;
return s.accepts?.find(a => nodeType.outputs?.includes(a)) }
})) return s.accepts?.find(a => nodeType.outputs?.includes(a));
})
return definitions );
return definitions;
} }
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] { getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
@@ -615,11 +703,11 @@ export class GraphManager extends EventEmitter<{
const sockets: [NodeInstance, string | number][] = []; const sockets: [NodeInstance, string | number][] = [];
// if index is a string, we are an input looking for outputs // if index is a string, we are an input looking for outputs
if (typeof index === "string") { if (typeof index === 'string') {
// filter out self and child nodes // filter out self and child nodes
const children = new Set(this.getChildren(node).map((n) => n.id)); const children = new Set(this.getChildren(node).map((n) => n.id));
const nodes = this.getAllNodes().filter( const nodes = this.getAllNodes().filter(
(n) => n.id !== node.id && !children.has(n.id), (n) => n.id !== node.id && !children.has(n.id)
); );
const ownType = nodeType?.inputs?.[index].type; const ownType = nodeType?.inputs?.[index].type;
@@ -634,20 +722,20 @@ export class GraphManager extends EventEmitter<{
} }
} }
} }
} else if (typeof index === "number") { } else if (typeof index === 'number') {
// if index is a number, we are an output looking for inputs // if index is a number, we are an output looking for inputs
// filter out self and parent nodes // filter out self and parent nodes
const parents = new Set(this.getParentsOfNode(node).map((n) => n.id)); const parents = new Set(this.getParentsOfNode(node).map((n) => n.id));
const nodes = this.getAllNodes().filter( const nodes = this.getAllNodes().filter(
(n) => n.id !== node.id && !parents.has(n.id), (n) => n.id !== node.id && !parents.has(n.id)
); );
// get edges from this socket // get edges from this socket
const edges = new Map( const edges = new Map(
this.getEdgesFromNode(node) this.getEdgesFromNode(node)
.filter((e) => e[1] === index) .filter((e) => e[1] === index)
.map((e) => [e[2].id, e[3]]), .map((e) => [e[2].id, e[3]])
); );
const ownType = nodeType.outputs?.[index]; const ownType = nodeType.outputs?.[index];
@@ -660,8 +748,8 @@ export class GraphManager extends EventEmitter<{
otherType.push(...(inputs[key].accepts || [])); otherType.push(...(inputs[key].accepts || []));
if ( if (
areSocketsCompatible(ownType, otherType) && areSocketsCompatible(ownType, otherType)
edges.get(node.id) !== key && edges.get(node.id) !== key
) { ) {
sockets.push([node, key]); sockets.push([node, key]);
} }
@@ -674,7 +762,7 @@ export class GraphManager extends EventEmitter<{
removeEdge( removeEdge(
edge: Edge, edge: Edge,
{ applyDeletion = true }: { applyDeletion?: boolean } = {}, { applyDeletion = true }: { applyDeletion?: boolean } = {}
) { ) {
const id0 = edge[0].id; const id0 = edge[0].id;
const sid0 = edge[1]; const sid0 = edge[1];
@@ -682,21 +770,20 @@ export class GraphManager extends EventEmitter<{
const sid2 = edge[3]; const sid2 = edge[3];
const _edge = this.edges.find( const _edge = this.edges.find(
(e) => (e) => e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2
e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2,
); );
if (!_edge) return; if (!_edge) return;
if (edge[0].state.children) { if (edge[0].state.children) {
edge[0].state.children = edge[0].state.children.filter( edge[0].state.children = edge[0].state.children.filter(
(n: NodeInstance) => n.id !== id2, (n: NodeInstance) => n.id !== id2
); );
} }
if (edge[2].state.parents) { if (edge[2].state.parents) {
edge[2].state.parents = edge[2].state.parents.filter( edge[2].state.parents = edge[2].state.parents.filter(
(n: NodeInstance) => n.id !== id0, (n: NodeInstance) => n.id !== id0
); );
} }
@@ -705,7 +792,6 @@ export class GraphManager extends EventEmitter<{
this.execute(); this.execute();
this.save(); this.save();
} }
} }
getEdgesToNode(node: NodeInstance) { getEdgesToNode(node: NodeInstance) {

View File

@@ -1,36 +1,43 @@
import type { NodeInstance, Socket } from "@nodarium/types"; import type { NodeInstance, Socket } from '@nodarium/types';
import { getContext, setContext } from "svelte"; import { getContext, setContext } from 'svelte';
import { SvelteSet } from "svelte/reactivity"; import { SvelteSet } from 'svelte/reactivity';
import type { GraphManager } from "./graph-manager.svelte"; import type { OrthographicCamera, Vector3 } from 'three';
import type { OrthographicCamera } from "three"; import type { GraphManager } from './graph-manager.svelte';
const graphStateKey = Symbol('graph-state');
const graphStateKey = Symbol("graph-state");
export function getGraphState() { export function getGraphState() {
return getContext<GraphState>(graphStateKey); return getContext<GraphState>(graphStateKey);
} }
export function setGraphState(graphState: GraphState) { export function setGraphState(graphState: GraphState) {
return setContext(graphStateKey, graphState) return setContext(graphStateKey, graphState);
} }
const graphManagerKey = Symbol("graph-manager"); const graphManagerKey = Symbol('graph-manager');
export function getGraphManager() { export function getGraphManager() {
return getContext<GraphManager>(graphManagerKey) return getContext<GraphManager>(graphManagerKey);
} }
export function setGraphManager(manager: GraphManager) { export function setGraphManager(manager: GraphManager) {
return setContext(graphManagerKey, manager); return setContext(graphManagerKey, manager);
} }
export class GraphState { type EdgeData = {
x1: number;
y1: number;
points: Vector3[];
};
export class GraphState {
constructor(private graph: GraphManager) { constructor(private graph: GraphManager) {
$effect.root(() => { $effect.root(() => {
$effect(() => { $effect(() => {
localStorage.setItem("cameraPosition", `[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]`) localStorage.setItem(
}) 'cameraPosition',
}) `[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]`
const storedPosition = localStorage.getItem("cameraPosition") );
});
});
const storedPosition = localStorage.getItem('cameraPosition');
if (storedPosition) { if (storedPosition) {
try { try {
const d = JSON.parse(storedPosition); const d = JSON.parse(storedPosition);
@@ -38,7 +45,7 @@ export class GraphState {
this.cameraPosition[1] = d[1]; this.cameraPosition[1] = d[1];
this.cameraPosition[2] = d[2]; this.cameraPosition[2] = d[2];
} catch (e) { } catch (e) {
console.log("Failed to parsed stored camera position", e); console.log('Failed to parsed stored camera position', e);
} }
} }
} }
@@ -46,13 +53,16 @@ export class GraphState {
width = $state(100); width = $state(100);
height = $state(100); height = $state(100);
hoveredEdgeId = $state<string | null>(null);
edges = new Map<string, EdgeData>();
wrapper = $state<HTMLDivElement>(null!); wrapper = $state<HTMLDivElement>(null!);
rect: DOMRect = $derived( rect: DOMRect = $derived(
(this.wrapper && this.width && this.height) ? this.wrapper.getBoundingClientRect() : new DOMRect(0, 0, 0, 0), (this.wrapper && this.width && this.height) ? this.wrapper.getBoundingClientRect() : new DOMRect(0, 0, 0, 0)
); );
camera = $state<OrthographicCamera>(null!); camera = $state<OrthographicCamera>(null!);
cameraPosition: [number, number, number] = $state([0, 0, 4]); cameraPosition: [number, number, number] = $state([0, 0, 100]);
clipboard: null | { clipboard: null | {
nodes: NodeInstance[]; nodes: NodeInstance[];
@@ -63,7 +73,7 @@ export class GraphState {
this.cameraPosition[0] - this.width / this.cameraPosition[2] / 2, this.cameraPosition[0] - this.width / this.cameraPosition[2] / 2,
this.cameraPosition[0] + this.width / this.cameraPosition[2] / 2, this.cameraPosition[0] + this.width / this.cameraPosition[2] / 2,
this.cameraPosition[1] - this.height / this.cameraPosition[2] / 2, this.cameraPosition[1] - this.height / this.cameraPosition[2] / 2,
this.cameraPosition[1] + this.height / this.cameraPosition[2] / 2, this.cameraPosition[1] + this.height / this.cameraPosition[2] / 2
]); ]);
boxSelection = $state(false); boxSelection = $state(false);
@@ -71,8 +81,8 @@ export class GraphState {
addMenuPosition = $state<[number, number] | null>(null); addMenuPosition = $state<[number, number] | null>(null);
snapToGrid = $state(false); snapToGrid = $state(false);
showGrid = $state(true) showGrid = $state(true);
showHelp = $state(false) showHelp = $state(false);
cameraDown = [0, 0]; cameraDown = [0, 0];
mouseDownNodeId = -1; mouseDownNodeId = -1;
@@ -88,33 +98,49 @@ export class GraphState {
hoveredSocket = $state<Socket | null>(null); hoveredSocket = $state<Socket | null>(null);
possibleSockets = $state<Socket[]>([]); possibleSockets = $state<Socket[]>([]);
possibleSocketIds = $derived( possibleSocketIds = $derived(
new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`)), new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`))
); );
getEdges() {
return $state.snapshot(this.edges);
}
clearSelection() { clearSelection() {
this.selectedNodes.clear(); this.selectedNodes.clear();
} }
isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT"; isBodyFocused = () => document?.activeElement?.nodeName !== 'INPUT';
setEdgeGeometry(edgeId: string, x1: number, y1: number, points: Vector3[]) {
this.edges.set(edgeId, { x1, y1, points });
}
removeEdgeGeometry(edgeId: string) {
this.edges.delete(edgeId);
}
getEdgeData() {
return this.edges;
}
updateNodePosition(node: NodeInstance) { updateNodePosition(node: NodeInstance) {
if ( if (
node.state.x === node.position[0] && node.state.x === node.position[0]
node.state.y === node.position[1] && node.state.y === node.position[1]
) { ) {
delete node.state.x; delete node.state.x;
delete node.state.y; delete node.state.y;
} }
if (node.state["x"] !== undefined && node.state["y"] !== undefined) { if (node.state['x'] !== undefined && node.state['y'] !== undefined) {
if (node.state.ref) { if (node.state.ref) {
node.state.ref.style.setProperty("--nx", `${node.state.x * 10}px`); node.state.ref.style.setProperty('--nx', `${node.state.x * 10}px`);
node.state.ref.style.setProperty("--ny", `${node.state.y * 10}px`); node.state.ref.style.setProperty('--ny', `${node.state.y * 10}px`);
} }
} else { } else {
if (node.state.ref) { if (node.state.ref) {
node.state.ref.style.setProperty("--nx", `${node.position[0] * 10}px`); node.state.ref.style.setProperty('--nx', `${node.position[0] * 10}px`);
node.state.ref.style.setProperty("--ny", `${node.position[1] * 10}px`); node.state.ref.style.setProperty('--ny', `${node.position[1] * 10}px`);
} }
} }
} }
@@ -134,18 +160,18 @@ export class GraphState {
getSocketPosition( getSocketPosition(
node: NodeInstance, node: NodeInstance,
index: string | number, index: string | number
): [number, number] { ): [number, number] {
if (typeof index === "number") { if (typeof index === 'number') {
return [ return [
(node?.state?.x ?? node.position[0]) + 20, (node?.state?.x ?? node.position[0]) + 20,
(node?.state?.y ?? node.position[1]) + 2.5 + 10 * index, (node?.state?.y ?? node.position[1]) + 2.5 + 10 * index
]; ];
} else { } else {
const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index); const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index);
return [ return [
node?.state?.x ?? node.position[0], node?.state?.x ?? node.position[0],
(node?.state?.y ?? node.position[1]) + 10 + 10 * _index, (node?.state?.y ?? node.position[1]) + 10 + 10 * _index
]; ];
} }
} }
@@ -159,26 +185,26 @@ export class GraphState {
if (!node?.inputs) { if (!node?.inputs) {
return 5; return 5;
} }
const height = const height = 5
5 + + 10
10 * * Object.keys(node.inputs).filter(
Object.keys(node.inputs).filter(
(p) => (p) =>
p !== "seed" && p !== 'seed'
node?.inputs && && node?.inputs
!("setting" in node?.inputs?.[p]) && && !('setting' in node?.inputs?.[p])
node.inputs[p].hidden !== true, && node.inputs[p].hidden !== true
).length; ).length;
this.nodeHeightCache[nodeTypeId] = height; this.nodeHeightCache[nodeTypeId] = height;
return height; return height;
} }
copyNodes() { copyNodes() {
if (this.activeNodeId === -1 && !this.selectedNodes?.size) if (this.activeNodeId === -1 && !this.selectedNodes?.size) {
return; return;
}
let nodes = [ let nodes = [
this.activeNodeId, this.activeNodeId,
...(this.selectedNodes?.values() || []), ...(this.selectedNodes?.values() || [])
] ]
.map((id) => this.graph.getNode(id)) .map((id) => this.graph.getNode(id))
.filter(b => !!b); .filter(b => !!b);
@@ -188,14 +214,14 @@ export class GraphState {
...node, ...node,
position: [ position: [
this.mousePosition[0] - node.position[0], this.mousePosition[0] - node.position[0],
this.mousePosition[1] - node.position[1], this.mousePosition[1] - node.position[1]
], ],
tmp: undefined, tmp: undefined
})); }));
this.clipboard = { this.clipboard = {
nodes: nodes, nodes: nodes,
edges: edges, edges: edges
}; };
} }
@@ -217,14 +243,13 @@ export class GraphState {
} }
} }
setDownSocket(socket: Socket) { setDownSocket(socket: Socket) {
this.activeSocket = socket; this.activeSocket = socket;
let { node, index, position } = socket; let { node, index, position } = socket;
// remove existing edge // remove existing edge
if (typeof index === "string") { if (typeof index === 'string') {
const edges = this.graph.getEdgesToNode(node); const edges = this.graph.getEdgesToNode(node);
for (const edge of edges) { for (const edge of edges) {
if (edge[3] === index) { if (edge[3] === index) {
@@ -241,7 +266,7 @@ export class GraphState {
this.activeSocket = { this.activeSocket = {
node, node,
index, index,
position, position
}; };
this.possibleSockets = this.graph this.possibleSockets = this.graph
@@ -250,18 +275,17 @@ export class GraphState {
return { return {
node, node,
index, index,
position: this.getSocketPosition(node, index), position: this.getSocketPosition(node, index)
}; };
}); });
}; }
projectScreenToWorld(x: number, y: number): [number, number] { projectScreenToWorld(x: number, y: number): [number, number] {
return [ return [
this.cameraPosition[0] + this.cameraPosition[0]
(x - this.width / 2) / this.cameraPosition[2], + (x - this.width / 2) / this.cameraPosition[2],
this.cameraPosition[1] + this.cameraPosition[1]
(y - this.height / 2) / this.cameraPosition[2], + (y - this.height / 2) / this.cameraPosition[2]
]; ];
} }
@@ -274,8 +298,8 @@ export class GraphState {
if (event.button === 0) { if (event.button === 0) {
// check if the clicked element is a node // check if the clicked element is a node
if (event.target instanceof HTMLElement) { if (event.target instanceof HTMLElement) {
const nodeElement = event.target.closest(".node"); const nodeElement = event.target.closest('.node');
const nodeId = nodeElement?.getAttribute?.("data-node-id"); const nodeId = nodeElement?.getAttribute?.('data-node-id');
if (nodeId) { if (nodeId) {
clickedNodeId = parseInt(nodeId, 10); clickedNodeId = parseInt(nodeId, 10);
} }
@@ -303,10 +327,10 @@ export class GraphState {
const height = this.getNodeHeight(node.type); const height = this.getNodeHeight(node.type);
const width = 20; const width = 20;
return ( return (
node.position[0] > this.cameraBounds[0] - width && 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]
); );
}; }
} }

View File

@@ -11,8 +11,10 @@
import HelpView from "../components/HelpView.svelte"; import HelpView from "../components/HelpView.svelte";
import { getGraphManager, getGraphState } from "../graph-state.svelte"; import { getGraphManager, getGraphState } from "../graph-state.svelte";
import { HTML } from "@threlte/extras"; import { HTML } from "@threlte/extras";
import { FileDropEventManager, MouseEventManager } from "./events";
import { maxZoom, minZoom } from "./constants"; import { maxZoom, minZoom } from "./constants";
import Debug from "../debug/Debug.svelte";
import { FileDropEventManager } from "./drop.events";
import { MouseEventManager } from "./mouse.events";
const { const {
keymap, keymap,
@@ -103,6 +105,7 @@
onwheel={(ev) => mouseEvents.handleMouseScroll(ev)} onwheel={(ev) => mouseEvents.handleMouseScroll(ev)}
bind:this={graphState.wrapper} bind:this={graphState.wrapper}
class="graph-wrapper" class="graph-wrapper"
style="height: 100vh;"
class:is-panning={graphState.isPanning} class:is-panning={graphState.isPanning}
class:is-hovering={graphState.hoveredNodeId !== -1} class:is-hovering={graphState.hoveredNodeId !== -1}
aria-label="Graph" aria-label="Graph"
@@ -174,9 +177,18 @@
{#each graph.edges as edge} {#each graph.edges as edge}
{@const [x1, y1, x2, y2] = getEdgePosition(edge)} {@const [x1, y1, x2, y2] = getEdgePosition(edge)}
<EdgeEl z={graphState.cameraPosition[2]} {x1} {y1} {x2} {y2} /> <EdgeEl
id={graph.getEdgeId(edge)}
z={graphState.cameraPosition[2]}
{x1}
{y1}
{x2}
{y2}
/>
{/each} {/each}
<Debug />
<HTML transform={false}> <HTML transform={false}>
<div <div
role="tree" role="tree"

View File

@@ -0,0 +1,107 @@
import { GraphSchema, type NodeId } from '@nodarium/types';
import type { GraphManager } from '../graph-manager.svelte';
import type { GraphState } from '../graph-state.svelte';
export class FileDropEventManager {
constructor(
private graph: GraphManager,
private state: GraphState
) { }
handleFileDrop(event: DragEvent) {
event.preventDefault();
this.state.isDragging = false;
if (!event.dataTransfer) return;
const nodeId = event.dataTransfer.getData('data/node-id') as NodeId;
let mx = event.clientX - this.state.rect.x;
let my = event.clientY - this.state.rect.y;
if (nodeId) {
let nodeOffsetX = event.dataTransfer.getData('data/node-offset-x');
let nodeOffsetY = event.dataTransfer.getData('data/node-offset-y');
if (nodeOffsetX && nodeOffsetY) {
mx += parseInt(nodeOffsetX);
my += parseInt(nodeOffsetY);
}
let props = {};
let rawNodeProps = event.dataTransfer.getData('data/node-props');
if (rawNodeProps) {
try {
props = JSON.parse(rawNodeProps);
} catch (e) { }
}
const pos = this.state.projectScreenToWorld(mx, my);
this.graph.registry.load([nodeId]).then(() => {
this.graph.createNode({
type: nodeId,
props,
position: pos
});
});
} else if (event.dataTransfer.files.length) {
const file = event.dataTransfer.files[0];
if (file.type === 'application/wasm') {
const reader = new FileReader();
reader.onload = async (e) => {
const buffer = e.target?.result;
if (buffer?.constructor === ArrayBuffer) {
const nodeType = await this.graph.registry.register(buffer);
this.graph.createNode({
type: nodeType.id,
props: {},
position: this.state.projectScreenToWorld(mx, my)
});
}
};
reader.readAsArrayBuffer(file);
} else if (file.type === 'application/json') {
const reader = new FileReader();
reader.onload = (e) => {
const buffer = e.target?.result as ArrayBuffer;
if (buffer) {
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
this.graph.load(state);
}
};
reader.readAsText(file);
}
}
}
handleMouseLeave() {
this.state.isDragging = false;
this.state.isPanning = false;
}
handleDragEnter(e: DragEvent) {
e.preventDefault();
this.state.isDragging = true;
this.state.isPanning = false;
}
handleDragOver(e: DragEvent) {
e.preventDefault();
this.state.isDragging = true;
this.state.isPanning = false;
}
handleDragEnd(e: DragEvent) {
e.preventDefault();
this.state.isDragging = true;
this.state.isPanning = false;
}
getEventListenerProps() {
return {
ondragenter: (ev: DragEvent) => this.handleDragEnter(ev),
ondragover: (ev: DragEvent) => this.handleDragOver(ev),
ondragexit: (ev: DragEvent) => this.handleDragEnd(ev),
ondrop: (ev: DragEvent) => this.handleFileDrop(ev),
onmouseleave: () => this.handleMouseLeave()
};
}
}

View File

@@ -0,0 +1,110 @@
import type { Box } from '@nodarium/types';
import type { GraphManager } from '../graph-manager.svelte';
import type { GraphState } from '../graph-state.svelte';
import { distanceFromPointToSegment } from '../helpers';
export class EdgeInteractionManager {
constructor(
private graph: GraphManager,
private state: GraphState
) { }
private MIN_DISTANCE = 3;
private _boundingBoxes = new Map<string, Box>();
handleMouseDown() {
this._boundingBoxes.clear();
const possibleEdges = this.graph
.getPossibleDropOnEdges(this.state.activeNodeId)
.map(e => this.graph.getEdgeId(e));
const edges = this.state.getEdges();
for (const edge of edges) {
const edgeId = edge[0];
if (!possibleEdges.includes(edgeId)) {
edges.delete(edgeId);
}
}
for (const [edgeId, data] of edges) {
const points = data.points;
let minX = points[0].x + data.x1;
let maxX = points[0].x + data.x1;
let minY = points[0].z + data.y1;
let maxY = points[0].z + data.y1;
// Iterate through all points to find the true bounds
for (let i = 0; i < points.length; i++) {
const x = data.x1 + points[i].x;
const y = data.y1 + points[i].z;
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
}
const boundingBox = {
minX: minX - this.MIN_DISTANCE,
maxX: maxX + this.MIN_DISTANCE,
minY: minY - this.MIN_DISTANCE,
maxY: maxY + this.MIN_DISTANCE
};
this._boundingBoxes.set(edgeId, boundingBox);
}
}
handleMouseMove() {
const [mouseX, mouseY] = this.state.mousePosition;
const hoveredEdgeIds: string[] = [];
const edges = this.state.getEdges();
// Check if mouse is inside any bounding box
for (const [edgeId, box] of this._boundingBoxes) {
const isInside = mouseX >= box.minX
&& mouseX <= box.maxX
&& mouseY >= box.minY
&& mouseY <= box.maxY;
if (isInside) {
hoveredEdgeIds.push(edgeId);
}
}
let hoveredEdgeId: string | null = null;
let hoveredEdgeDistance = Infinity;
const DENSITY = 10; // higher DENSITY = less points checked (yes density might not be the best name :-)
for (const edgeId of hoveredEdgeIds) {
const edge = edges.get(edgeId)!;
for (let i = 0; i < edge.points.length - DENSITY; i += DENSITY) {
const pointAx = edge.points[i].x + edge.x1;
const pointAy = edge.points[i].z + edge.y1;
const pointBx = edge.points[i + DENSITY].x + edge.x1;
const pointBy = edge.points[i + DENSITY].z + edge.y1;
const distance = distanceFromPointToSegment(pointAx, pointAy, pointBx, pointBy, mouseX, mouseY);
if (distance < this.MIN_DISTANCE) {
if (distance < hoveredEdgeDistance) {
hoveredEdgeDistance = distance;
hoveredEdgeId = edgeId;
}
}
}
}
this.state.hoveredEdgeId = hoveredEdgeId;
}
handleMouseUp() {
if (this.state.hoveredEdgeId) {
const edge = this.graph.getEdgeById(this.state.hoveredEdgeId);
if (edge) {
this.graph.dropNodeOnEdge(this.state.activeNodeId, edge);
}
this.state.hoveredEdgeId = null;
}
}
}

View File

@@ -1,127 +1,23 @@
import { GraphSchema, type NodeId, type NodeInstance } from "@nodarium/types"; import { animate, lerp } from '$lib/helpers';
import type { GraphManager } from "../graph-manager.svelte"; import { type NodeInstance } from '@nodarium/types';
import type { GraphState } from "../graph-state.svelte"; import type { GraphManager } from '../graph-manager.svelte';
import { animate, lerp } from "$lib/helpers"; import type { GraphState } from '../graph-state.svelte';
import { snapToGrid as snapPointToGrid } from "../helpers"; import { snapToGrid as snapPointToGrid } from '../helpers';
import { maxZoom, minZoom, zoomSpeed } from "./constants"; import { maxZoom, minZoom, zoomSpeed } from './constants';
import { EdgeInteractionManager } from './edge.events';
export class FileDropEventManager {
constructor(
private graph: GraphManager,
private state: GraphState
) { }
handleFileDrop(event: DragEvent) {
event.preventDefault();
this.state.isDragging = false;
if (!event.dataTransfer) return;
const nodeId = event.dataTransfer.getData("data/node-id") as NodeId;
let mx = event.clientX - this.state.rect.x;
let my = event.clientY - this.state.rect.y;
if (nodeId) {
let nodeOffsetX = event.dataTransfer.getData("data/node-offset-x");
let nodeOffsetY = event.dataTransfer.getData("data/node-offset-y");
if (nodeOffsetX && nodeOffsetY) {
mx += parseInt(nodeOffsetX);
my += parseInt(nodeOffsetY);
}
let props = {};
let rawNodeProps = event.dataTransfer.getData("data/node-props");
if (rawNodeProps) {
try {
props = JSON.parse(rawNodeProps);
} catch (e) { }
}
const pos = this.state.projectScreenToWorld(mx, my);
this.graph.registry.load([nodeId]).then(() => {
this.graph.createNode({
type: nodeId,
props,
position: pos,
});
});
} else if (event.dataTransfer.files.length) {
const file = event.dataTransfer.files[0];
if (file.type === "application/wasm") {
const reader = new FileReader();
reader.onload = async (e) => {
const buffer = e.target?.result;
if (buffer?.constructor === ArrayBuffer) {
const nodeType = await this.graph.registry.register(buffer);
this.graph.createNode({
type: nodeType.id,
props: {},
position: this.state.projectScreenToWorld(mx, my),
});
}
};
reader.readAsArrayBuffer(file);
} else if (file.type === "application/json") {
const reader = new FileReader();
reader.onload = (e) => {
const buffer = e.target?.result as ArrayBuffer;
if (buffer) {
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
this.graph.load(state);
}
};
reader.readAsText(file);
}
}
}
handleMouseLeave() {
this.state.isDragging = false;
this.state.isPanning = false;
}
handleDragEnter(e: DragEvent) {
e.preventDefault();
this.state.isDragging = true;
this.state.isPanning = false;
}
handleDragOver(e: DragEvent) {
e.preventDefault();
this.state.isDragging = true;
this.state.isPanning = false;
}
handleDragEnd(e: DragEvent) {
e.preventDefault();
this.state.isDragging = true;
this.state.isPanning = false;
}
getEventListenerProps() {
return {
ondragenter: (ev: DragEvent) => this.handleDragEnter(ev),
ondragover: (ev: DragEvent) => this.handleDragOver(ev),
ondragexit: (ev: DragEvent) => this.handleDragEnd(ev),
ondrop: (ev: DragEvent) => this.handleFileDrop(ev),
onmouseleave: () => this.handleMouseLeave(),
}
}
}
export class MouseEventManager { export class MouseEventManager {
edgeInteractionManager: EdgeInteractionManager;
constructor( constructor(
private graph: GraphManager, private graph: GraphManager,
private state: GraphState private state: GraphState
) { } ) {
this.edgeInteractionManager = new EdgeInteractionManager(graph, state);
}
handleMouseUp(event: MouseEvent) { handleMouseUp(event: MouseEvent) {
this.edgeInteractionManager.handleMouseUp();
this.state.isPanning = false; this.state.isPanning = false;
if (!this.state.mouseDown) return; if (!this.state.mouseDown) return;
@@ -145,25 +41,23 @@ export class MouseEventManager {
const snapLevel = this.state.getSnapLevel(); const snapLevel = this.state.getSnapLevel();
activeNode.position[0] = snapPointToGrid( activeNode.position[0] = snapPointToGrid(
activeNode?.state?.x ?? activeNode.position[0], activeNode?.state?.x ?? activeNode.position[0],
5 / snapLevel, 5 / snapLevel
); );
activeNode.position[1] = snapPointToGrid( activeNode.position[1] = snapPointToGrid(
activeNode?.state?.y ?? activeNode.position[1], activeNode?.state?.y ?? activeNode.position[1],
5 / snapLevel, 5 / snapLevel
); );
} else { } else {
activeNode.position[0] = activeNode?.state?.x ?? activeNode.position[0]; activeNode.position[0] = activeNode?.state?.x ?? activeNode.position[0];
activeNode.position[1] = activeNode?.state?.y ?? activeNode.position[1]; activeNode.position[1] = activeNode?.state?.y ?? activeNode.position[1];
} }
const nodes = [ const nodes = [
...[...(this.state.selectedNodes?.values() || [])].map((id) => ...[...(this.state.selectedNodes?.values() || [])].map((id) => this.graph.getNode(id))
this.graph.getNode(id),
),
] as NodeInstance[]; ] as NodeInstance[];
const vec = [ const vec = [
activeNode.position[0] - (activeNode?.state.x || 0), activeNode.position[0] - (activeNode?.state.x || 0),
activeNode.position[1] - (activeNode?.state.y || 0), activeNode.position[1] - (activeNode?.state.y || 0)
]; ];
for (const node of nodes) { for (const node of nodes) {
@@ -179,9 +73,9 @@ export class MouseEventManager {
animate(500, (a: number) => { animate(500, (a: number) => {
for (const node of nodes) { for (const node of nodes) {
if ( if (
node?.state && node?.state
node.state["x"] !== undefined && && node.state['x'] !== undefined
node.state["y"] !== undefined && node.state['y'] !== undefined
) { ) {
node.state.x = lerp(node.state.x, node.position[0], a); node.state.x = lerp(node.state.x, node.position[0], a);
node.state.y = lerp(node.state.y, node.position[1], a); node.state.y = lerp(node.state.y, node.position[1], a);
@@ -195,24 +89,24 @@ export class MouseEventManager {
this.graph.save(); this.graph.save();
} else if (this.state.hoveredSocket && this.state.activeSocket) { } else if (this.state.hoveredSocket && this.state.activeSocket) {
if ( if (
typeof this.state.hoveredSocket.index === "number" && typeof this.state.hoveredSocket.index === 'number'
typeof this.state.activeSocket.index === "string" && typeof this.state.activeSocket.index === 'string'
) { ) {
this.graph.createEdge( this.graph.createEdge(
this.state.hoveredSocket.node, this.state.hoveredSocket.node,
this.state.hoveredSocket.index || 0, this.state.hoveredSocket.index || 0,
this.state.activeSocket.node, this.state.activeSocket.node,
this.state.activeSocket.index, this.state.activeSocket.index
); );
} else if ( } else if (
typeof this.state.activeSocket.index == "number" && typeof this.state.activeSocket.index == 'number'
typeof this.state.hoveredSocket.index === "string" && typeof this.state.hoveredSocket.index === 'string'
) { ) {
this.graph.createEdge( this.graph.createEdge(
this.state.activeSocket.node, this.state.activeSocket.node,
this.state.activeSocket.index || 0, this.state.activeSocket.index || 0,
this.state.hoveredSocket.node, this.state.hoveredSocket.node,
this.state.hoveredSocket.index, this.state.hoveredSocket.index
); );
} }
this.graph.save(); this.graph.save();
@@ -220,18 +114,18 @@ export class MouseEventManager {
// Handle automatic adding of nodes on ctrl+mouseUp // Handle automatic adding of nodes on ctrl+mouseUp
this.state.edgeEndPosition = [ this.state.edgeEndPosition = [
this.state.mousePosition[0], this.state.mousePosition[0],
this.state.mousePosition[1], this.state.mousePosition[1]
]; ];
if (typeof this.state.activeSocket.index === "number") { if (typeof this.state.activeSocket.index === 'number') {
this.state.addMenuPosition = [ this.state.addMenuPosition = [
this.state.mousePosition[0], this.state.mousePosition[0],
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2], this.state.mousePosition[1] - 25 / this.state.cameraPosition[2]
]; ];
} else { } else {
this.state.addMenuPosition = [ this.state.addMenuPosition = [
this.state.mousePosition[0] - 155 / this.state.cameraPosition[2], this.state.mousePosition[0] - 155 / this.state.cameraPosition[2],
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2], this.state.mousePosition[1] - 25 / this.state.cameraPosition[2]
]; ];
} }
return; return;
@@ -239,11 +133,11 @@ export class MouseEventManager {
// check if camera moved // check if camera moved
if ( if (
clickedNodeId === -1 && clickedNodeId === -1
!this.state.boxSelection && && !this.state.boxSelection
this.state.cameraDown[0] === this.state.cameraPosition[0] && && this.state.cameraDown[0] === this.state.cameraPosition[0]
this.state.cameraDown[1] === this.state.cameraPosition[1] && && this.state.cameraDown[1] === this.state.cameraPosition[1]
this.state.isBodyFocused() && this.state.isBodyFocused()
) { ) {
this.state.activeNodeId = -1; this.state.activeNodeId = -1;
this.state.clearSelection(); this.state.clearSelection();
@@ -257,16 +151,15 @@ export class MouseEventManager {
this.state.addMenuPosition = null; this.state.addMenuPosition = null;
} }
handleMouseDown(event: MouseEvent) { handleMouseDown(event: MouseEvent) {
if (this.state.mouseDown) return; if (this.state.mouseDown) return;
this.state.edgeEndPosition = null; this.state.edgeEndPosition = null;
if (event.target instanceof HTMLElement) { if (event.target instanceof HTMLElement) {
if ( if (
event.target.nodeName !== "CANVAS" && event.target.nodeName !== 'CANVAS'
!event.target.classList.contains("node") && && !event.target.classList.contains('node')
!event.target.classList.contains("content") && !event.target.classList.contains('content')
) { ) {
return; return;
} }
@@ -288,7 +181,7 @@ export class MouseEventManager {
this.state.activeNodeId = clickedNodeId; this.state.activeNodeId = clickedNodeId;
// if the selected node is the same as the clicked node // if the selected node is the same as the clicked node
} else if (this.state.activeNodeId === clickedNodeId) { } else if (this.state.activeNodeId === clickedNodeId) {
//$activeNodeId = -1; // $activeNodeId = -1;
// if the clicked node is different from the selected node and secondary // if the clicked node is different from the selected node and secondary
} else if (event.ctrlKey) { } else if (event.ctrlKey) {
this.state.selectedNodes.add(this.state.activeNodeId); this.state.selectedNodes.add(this.state.activeNodeId);
@@ -312,6 +205,7 @@ export class MouseEventManager {
this.state.activeNodeId = clickedNodeId; this.state.activeNodeId = clickedNodeId;
this.state.clearSelection(); this.state.clearSelection();
} }
this.edgeInteractionManager.handleMouseDown();
} else if (event.ctrlKey) { } else if (event.ctrlKey) {
this.state.boxSelection = true; this.state.boxSelection = true;
} }
@@ -335,7 +229,6 @@ export class MouseEventManager {
this.state.edgeEndPosition = null; this.state.edgeEndPosition = null;
} }
handleMouseMove(event: MouseEvent) { handleMouseMove(event: MouseEvent) {
let mx = event.clientX - this.state.rect.x; let mx = event.clientX - this.state.rect.x;
let my = event.clientY - this.state.rect.y; let my = event.clientY - this.state.rect.y;
@@ -351,8 +244,8 @@ export class MouseEventManager {
let _socket; let _socket;
for (const socket of this.state.possibleSockets) { for (const socket of this.state.possibleSockets) {
const dist = Math.sqrt( const dist = Math.sqrt(
(socket.position[0] - this.state.mousePosition[0]) ** 2 + (socket.position[0] - this.state.mousePosition[0]) ** 2
(socket.position[1] - this.state.mousePosition[1]) ** 2, + (socket.position[1] - this.state.mousePosition[1]) ** 2
); );
if (dist < smallestDist) { if (dist < smallestDist) {
smallestDist = dist; smallestDist = dist;
@@ -375,7 +268,7 @@ export class MouseEventManager {
event.stopPropagation(); event.stopPropagation();
const mouseD = this.state.projectScreenToWorld( const mouseD = this.state.projectScreenToWorld(
this.state.mouseDown[0], this.state.mouseDown[0],
this.state.mouseDown[1], this.state.mouseDown[1]
); );
const x1 = Math.min(mouseD[0], this.state.mousePosition[0]); const x1 = Math.min(mouseD[0], this.state.mousePosition[0]);
const x2 = Math.max(mouseD[0], this.state.mousePosition[0]); const x2 = Math.max(mouseD[0], this.state.mousePosition[0]);
@@ -397,6 +290,7 @@ export class MouseEventManager {
// here we are handling dragging of nodes // here we are handling dragging of nodes
if (this.state.activeNodeId !== -1 && this.state.mouseDownNodeId !== -1) { if (this.state.activeNodeId !== -1 && this.state.mouseDownNodeId !== -1) {
this.edgeInteractionManager.handleMouseMove();
const node = this.graph.getNode(this.state.activeNodeId); const node = this.graph.getNode(this.state.activeNodeId);
if (!node || event.buttons !== 1) return; if (!node || event.buttons !== 1) return;
@@ -405,10 +299,8 @@ export class MouseEventManager {
const oldX = node.state.downX || 0; const oldX = node.state.downX || 0;
const oldY = node.state.downY || 0; const oldY = node.state.downY || 0;
let newX = let newX = oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2]; let newY = oldY + (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
let newY =
oldY + (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
if (event.ctrlKey) { if (event.ctrlKey) {
const snapLevel = this.state.getSnapLevel(); const snapLevel = this.state.getSnapLevel();
@@ -448,23 +340,19 @@ export class MouseEventManager {
// here we are handling panning of camera // here we are handling panning of camera
this.state.isPanning = true; this.state.isPanning = true;
let newX = let newX = this.state.cameraDown[0]
this.state.cameraDown[0] - - (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
(mx - this.state.mouseDown[0]) / this.state.cameraPosition[2]; let newY = this.state.cameraDown[1]
let newY = - (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
this.state.cameraDown[1] -
(my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
this.state.cameraPosition[0] = newX; this.state.cameraPosition[0] = newX;
this.state.cameraPosition[1] = newY; this.state.cameraPosition[1] = newY;
} }
handleMouseScroll(event: WheelEvent) { handleMouseScroll(event: WheelEvent) {
const bodyIsFocused = const bodyIsFocused = document.activeElement === document.body
document.activeElement === document.body || || document.activeElement === this.state.wrapper
document.activeElement === this.state.wrapper || || document?.activeElement?.id === 'graph';
document?.activeElement?.id === "graph";
if (!bodyIsFocused) return; if (!bodyIsFocused) return;
// Define zoom speed and clamp it between -1 and 1 // Define zoom speed and clamp it between -1 and 1
@@ -479,21 +367,19 @@ export class MouseEventManager {
maxZoom, maxZoom,
isNegative isNegative
? this.state.cameraPosition[2] / delta ? this.state.cameraPosition[2] / delta
: this.state.cameraPosition[2] * delta, : this.state.cameraPosition[2] * delta
), )
); );
// Calculate the ratio of the new zoom to the original zoom // Calculate the ratio of the new zoom to the original zoom
const zoomRatio = newZoom / this.state.cameraPosition[2]; const zoomRatio = newZoom / this.state.cameraPosition[2];
// Update camera position and zoom level // Update camera position and zoom level
this.state.cameraPosition[0] = this.state.mousePosition[0] - this.state.cameraPosition[0] = this.state.mousePosition[0]
(this.state.mousePosition[0] - this.state.cameraPosition[0]) / - (this.state.mousePosition[0] - this.state.cameraPosition[0])
zoomRatio; / zoomRatio;
this.state.cameraPosition[1] = this.state.mousePosition[1] - this.state.cameraPosition[1] = this.state.mousePosition[1]
(this.state.mousePosition[1] - this.state.cameraPosition[1]) / - (this.state.mousePosition[1] - this.state.cameraPosition[1])
zoomRatio, / zoomRatio, this.state.cameraPosition[2] = newZoom;
this.state.cameraPosition[2] = newZoom;
} }
} }

View File

@@ -8,7 +8,7 @@ export function lerp(a: number, b: number, t: number) {
export function animate( export function animate(
duration: number, duration: number,
callback: (progress: number) => void | false, callback: (progress: number) => void | false
) { ) {
const start = performance.now(); const start = performance.now();
const loop = (time: number) => { const loop = (time: number) => {
@@ -33,41 +33,37 @@ export function createNodePath({
cornerBottom = 0, cornerBottom = 0,
leftBump = false, leftBump = false,
rightBump = false, rightBump = false,
aspectRatio = 1, aspectRatio = 1
} = {}) { } = {}) {
return `M0,${cornerTop} return `M0,${cornerTop}
${ ${cornerTop
cornerTop ? ` V${cornerTop}
? ` V${cornerTop}
Q0,0 ${cornerTop * aspectRatio},0 Q0,0 ${cornerTop * aspectRatio},0
H${100 - cornerTop * aspectRatio} H${100 - cornerTop * aspectRatio}
Q100,0 100,${cornerTop} Q100,0 100,${cornerTop}
` `
: ` V0 : ` V0
H100 H100
` `
} }
V${y - height / 2} V${y - height / 2}
${ ${rightBump
rightBump ? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}` : ` H100`
: ` H100` }
} ${cornerBottom
${ ? ` V${100 - cornerBottom}
cornerBottom
? ` V${100 - cornerBottom}
Q100,100 ${100 - cornerBottom * aspectRatio},100 Q100,100 ${100 - cornerBottom * aspectRatio},100
H${cornerBottom * aspectRatio} H${cornerBottom * aspectRatio}
Q0,100 0,${100 - cornerBottom} Q0,100 0,${100 - cornerBottom}
` `
: `${leftBump ? `V100 H0` : `V100`}` : `${leftBump ? `V100 H0` : `V100`}`
} }
${ ${leftBump
leftBump ? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}` : ` H0`
: ` H0` }
} Z`.replace(/\s+/g, ' ');
Z`.replace(/\s+/g, " ");
} }
export const debounce = (fn: Function, ms = 300) => { export const debounce = (fn: Function, ms = 300) => {
@@ -78,14 +74,13 @@ export const debounce = (fn: Function, ms = 300) => {
}; };
}; };
export const clone: <T>(v: T) => T = export const clone: <T>(v: T) => T = 'structedClone' in globalThis
"structedClone" in globalThis ? globalThis.structuredClone
? globalThis.structuredClone : (obj) => JSON.parse(JSON.stringify(obj));
: (obj) => JSON.parse(JSON.stringify(obj));
export function withSubComponents<A, B extends Record<string, any>>( export function withSubComponents<A, B extends Record<string, any>>(
component: A, component: A,
subcomponents: B, subcomponents: B
): A & B { ): A & B {
Object.keys(subcomponents).forEach((key) => { Object.keys(subcomponents).forEach((key) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -93,3 +88,27 @@ export function withSubComponents<A, B extends Record<string, any>>(
}); });
return component as A & B; return component as A & B;
} }
export function distanceFromPointToSegment(
x1: number,
y1: number,
x2: number,
y2: number,
x0: number,
y0: number
): number {
const dx = x2 - x1;
const dy = y2 - y1;
if (dx === 0 && dy === 0) {
return Math.hypot(x0 - x1, y0 - y1);
}
const t = ((x0 - x1) * dx + (y0 - y1) * dy) / (dx * dx + dy * dy);
const clampedT = Math.max(0, Math.min(1, t));
const px = x1 + clampedT * dx;
const py = y1 + clampedT * dy;
return Math.hypot(x0 - px, y0 - py);
}

View File

@@ -1,16 +1,15 @@
import { animate, lerp } from "$lib/helpers"; import { animate, lerp } from '$lib/helpers';
import type { createKeyMap } from "$lib/helpers/createKeyMap"; import type { createKeyMap } from '$lib/helpers/createKeyMap';
import FileSaver from "file-saver"; import { panelState } from '$lib/sidebar/PanelState.svelte';
import type { GraphManager } from "./graph-manager.svelte"; import FileSaver from 'file-saver';
import type { GraphState } from "./graph-state.svelte"; import type { GraphManager } from './graph-manager.svelte';
import type { GraphState } from './graph-state.svelte';
type Keymap = ReturnType<typeof createKeyMap>; type Keymap = ReturnType<typeof createKeyMap>;
export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: GraphState) { export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: GraphState) {
keymap.addShortcut({ keymap.addShortcut({
key: "l", key: 'l',
description: "Select linked nodes", description: 'Select linked nodes',
callback: () => { callback: () => {
const activeNode = graph.getNode(graphState.activeNodeId); const activeNode = graph.getNode(graphState.activeNodeId);
if (activeNode) { if (activeNode) {
@@ -20,56 +19,54 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
graphState.selectedNodes.add(node.id); graphState.selectedNodes.add(node.id);
} }
} }
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "?", key: '?',
description: "Toggle Help", description: 'Toggle Help',
callback: () => { callback: () => {
// TODO: fix this panelState.setActivePanel('shortcuts');
// showHelp = !showHelp; }
},
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "c", key: 'c',
ctrl: true, ctrl: true,
description: "Copy active nodes", description: 'Copy active nodes',
callback: () => graphState.copyNodes(), callback: () => graphState.copyNodes()
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "v", key: 'v',
ctrl: true, ctrl: true,
description: "Paste nodes", description: 'Paste nodes',
callback: () => graphState.pasteNodes(), callback: () => graphState.pasteNodes()
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "Escape", key: 'Escape',
description: "Deselect nodes", description: 'Deselect nodes',
callback: () => { callback: () => {
graphState.activeNodeId = -1; graphState.activeNodeId = -1;
graphState.clearSelection(); graphState.clearSelection();
graphState.edgeEndPosition = null; graphState.edgeEndPosition = null;
(document.activeElement as HTMLElement)?.blur(); (document.activeElement as HTMLElement)?.blur();
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "A", key: 'A',
shift: true, shift: true,
description: "Add new Node", description: 'Add new Node',
callback: () => { callback: () => {
graphState.addMenuPosition = [graphState.mousePosition[0], graphState.mousePosition[1]]; graphState.addMenuPosition = [graphState.mousePosition[0], graphState.mousePosition[1]];
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: ".", key: '.',
description: "Center camera", description: 'Center camera',
callback: () => { callback: () => {
if (!graphState.isBodyFocused()) return; if (!graphState.isBodyFocused()) return;
@@ -90,67 +87,67 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
animate(500, (a: number) => { animate(500, (a: number) => {
graphState.cameraPosition[0] = lerp(camX, average[0], ease(a)); graphState.cameraPosition[0] = lerp(camX, average[0], ease(a));
graphState.cameraPosition[1] = lerp(camY, average[1], ease(a)); graphState.cameraPosition[1] = lerp(camY, average[1], ease(a));
graphState.cameraPosition[2] = lerp(camZ, 2, ease(a)) graphState.cameraPosition[2] = lerp(camZ, 2, ease(a));
if (graphState.mouseDown) return false; if (graphState.mouseDown) return false;
}); });
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "a", key: 'a',
ctrl: true, ctrl: true,
preventDefault: true, preventDefault: true,
description: "Select all nodes", description: 'Select all nodes',
callback: () => { callback: () => {
if (!graphState.isBodyFocused()) return; if (!graphState.isBodyFocused()) return;
for (const node of graph.nodes.keys()) { for (const node of graph.nodes.keys()) {
graphState.selectedNodes.add(node); graphState.selectedNodes.add(node);
} }
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "z", key: 'z',
ctrl: true, ctrl: true,
description: "Undo", description: 'Undo',
callback: () => { callback: () => {
if (!graphState.isBodyFocused()) return; if (!graphState.isBodyFocused()) return;
graph.undo(); graph.undo();
for (const node of graph.nodes.values()) { for (const node of graph.nodes.values()) {
graphState.updateNodePosition(node); graphState.updateNodePosition(node);
} }
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "y", key: 'y',
ctrl: true, ctrl: true,
description: "Redo", description: 'Redo',
callback: () => { callback: () => {
graph.redo(); graph.redo();
for (const node of graph.nodes.values()) { for (const node of graph.nodes.values()) {
graphState.updateNodePosition(node); graphState.updateNodePosition(node);
} }
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "s", key: 's',
ctrl: true, ctrl: true,
description: "Save", description: 'Save',
preventDefault: true, preventDefault: true,
callback: () => { callback: () => {
const state = graph.serialize(); const state = graph.serialize();
const blob = new Blob([JSON.stringify(state)], { const blob = new Blob([JSON.stringify(state)], {
type: "application/json;charset=utf-8", type: 'application/json;charset=utf-8'
}); });
FileSaver.saveAs(blob, "nodarium-graph.json"); FileSaver.saveAs(blob, 'nodarium-graph.json');
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: ["Delete", "Backspace", "x"], key: ['Delete', 'Backspace', 'x'],
description: "Delete selected nodes", description: 'Delete selected nodes',
callback: (event) => { callback: (event) => {
if (!graphState.isBodyFocused()) return; if (!graphState.isBodyFocused()) return;
graph.startUndoGroup(); graph.startUndoGroup();
@@ -171,20 +168,18 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
graphState.clearSelection(); graphState.clearSelection();
} }
graph.saveUndoGroup(); graph.saveUndoGroup();
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "f", key: 'f',
description: "Smart Connect Nodes", description: 'Smart Connect Nodes',
callback: () => { callback: () => {
const nodes = [...graphState.selectedNodes.values()] const nodes = [...graphState.selectedNodes.values()]
.map((g) => graph.getNode(g)) .map((g) => graph.getNode(g))
.filter((n) => !!n); .filter((n) => !!n);
const edge = graph.smartConnect(nodes[0], nodes[1]); const edge = graph.smartConnect(nodes[0], nodes[1]);
if (!edge) graph.smartConnect(nodes[1], nodes[0]); if (!edge) graph.smartConnect(nodes[1], nodes[0]);
}, }
}); });
} }

View File

@@ -73,8 +73,14 @@
{#key id && graphId} {#key id && graphId}
<div class="content" class:disabled={graph?.inputSockets?.has(socketId)}> <div class="content" class:disabled={graph?.inputSockets?.has(socketId)}>
{#if inputType.label !== ""} {#if inputType.label !== ""}
<label for={elementId}>{input.label || id}</label> <label for={elementId} title={input.description}
>{input.label || id}</label
>
{/if} {/if}
<span
class="absolute i-[tabler--help-circle] size-4 block top-2 right-2 opacity-30"
title={JSON.stringify(input, null, 2)}
></span>
{#if inputType.external !== true} {#if inputType.external !== true}
<NodeInputEl {graph} {elementId} bind:node {input} {id} /> <NodeInputEl {graph} {elementId} bind:node {input} {id} />
{/if} {/if}
@@ -181,9 +187,6 @@
.content.disabled { .content.disabled {
opacity: 0.2; opacity: 0.2;
} }
.content.disabled > * {
pointer-events: none;
}
.disabled svg path { .disabled svg path {
d: var(--hover-path-disabled) !important; d: var(--hover-path-disabled) !important;

View File

@@ -95,12 +95,14 @@
<SmallPerformanceViewer {fps} store={perf} /> <SmallPerformanceViewer {fps} store={perf} />
{/if} {/if}
<Canvas> <div style="height: 100vh">
<Scene <Canvas>
bind:this={sceneComponent} <Scene
{lines} bind:this={sceneComponent}
{centerCamera} {lines}
bind:scene {centerCamera}
bind:fps bind:scene
/> bind:fps
</Canvas> />
</Canvas>
</div>

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getContext, type Snippet } from "svelte"; import { type Snippet } from "svelte";
import type { PanelState } from "./PanelState.svelte"; import { panelState } from "./PanelState.svelte";
const { const {
id, id,
@@ -18,8 +18,6 @@
children?: Snippet; children?: Snippet;
}>(); }>();
const panelState = getContext<PanelState>("panel-state");
const panel = panelState.registerPanel(id, icon, classes, hidden); const panel = panelState.registerPanel(id, icon, classes, hidden);
$effect(() => { $effect(() => {
panel.hidden = hidden; panel.hidden = hidden;

View File

@@ -1,15 +1,14 @@
import { localState } from "$lib/helpers/localState.svelte"; import { localState } from '$lib/helpers/localState.svelte';
type Panel = { type Panel = {
icon: string; icon: string;
classes: string; classes: string;
hidden?: boolean; hidden?: boolean;
} };
export class PanelState {
class PanelState {
panels = $state<Record<string, Panel>>({}); panels = $state<Record<string, Panel>>({});
activePanel = localState<string | boolean>("node.activePanel", "") activePanel = localState<string | boolean>('node.activePanel', '');
get keys() { get keys() {
return Object.keys(this.panels); return Object.keys(this.panels);
@@ -19,7 +18,7 @@ export class PanelState {
const state = $state({ const state = $state({
icon: icon, icon: icon,
classes: classes, classes: classes,
hidden: hidden, hidden: hidden
}); });
this.panels[id] = state; this.panels[id] = state;
return state; return state;
@@ -29,7 +28,13 @@ export class PanelState {
if (this.activePanel.value) { if (this.activePanel.value) {
this.activePanel.value = false; this.activePanel.value = false;
} else { } else {
this.activePanel.value = this.keys[0] this.activePanel.value = this.keys[0];
} }
} }
public setActivePanel(panelId: string) {
this.activePanel.value = panelId;
}
} }
export const panelState = new PanelState();

View File

@@ -1,9 +1,6 @@
<script lang="ts"> <script lang="ts">
import { setContext, type Snippet } from "svelte"; import { type Snippet } from "svelte";
import { PanelState } from "./PanelState.svelte"; import { panelState as state } from "./PanelState.svelte";
const state = new PanelState();
setContext("panel-state", state);
const { children } = $props<{ children?: Snippet }>(); const { children } = $props<{ children?: Snippet }>();
</script> </script>

View File

@@ -4,7 +4,7 @@ This guide will help you developing your first Nodarium Node written in Rust. As
## Prerequesites ## Prerequesites
You need to have [Rust](https://www.rust-lang.org/tools/install) and [wasm-pack](https://rustwasm.github.io/wasm-pack/book/) installed. Rust is the language we are going to develop our node in and wasm-pack helps us compile our rust code into a webassembly file. You need to have [Rust](https://www.rust-lang.org/tools/install) and [wasm-pack](https://rustwasm.github.io/docs/wasm-pack/) installed. Rust is the language we are going to develop our node in and wasm-pack helps us compile our rust code into a webassembly file.
```bash ```bash
# install rust # install rust
@@ -22,11 +22,12 @@ cd my-new-node
## Setup Definition ## Setup Definition
Now we create the definition file of the node. Now we create the definition file of the node.
Here we define what kind of inputs our node will expect and what kind of output it produces. If you want to dive deeper into this topic, have a look at [NODE_DEFINITION.md](./NODE_DEFINITION.md). Here we define what kind of inputs our node will expect and what kind of output it produces. If you want to dive deeper into this topic, have a look at [NODE_DEFINITION.md](./NODE_DEFINITION.md).
`src/definition.json` `src/definition.json`
```json
```json
{ {
"id": "my-name/my-namespace/zylinder-node", "id": "my-name/my-namespace/zylinder-node",
"outputs": [ "outputs": [
@@ -35,7 +36,7 @@ Here we define what kind of inputs our node will expect and what kind of output
"inputs": { "inputs": {
"height": { "height": {
"type": "float", "type": "float",
"value": 2, "value": 2
}, },
"radius": { "radius": {
"type": "float", "type": "float",
@@ -44,6 +45,7 @@ Here we define what kind of inputs our node will expect and what kind of output
} }
} }
``` ```
If we take a look at the `src/lib.rs` file we see that `src/definition.json` is included with the following line: If we take a look at the `src/lib.rs` file we see that `src/definition.json` is included with the following line:
```rust ```rust

View File

@@ -33,6 +33,7 @@
pkgs.typescript-language-server pkgs.typescript-language-server
pkgs.prettier pkgs.prettier
pkgs.tailwindcss-language-server pkgs.tailwindcss-language-server
pkgs.svelte-language-server
]; ];
}; };
}); });

View File

@@ -4,9 +4,10 @@
"build:story": "pnpm -r --filter 'ui' story:build", "build:story": "pnpm -r --filter 'ui' story:build",
"build:app": "BASE_PATH=/ui pnpm -r --filter 'ui' build && pnpm -r --filter 'app' build", "build:app": "BASE_PATH=/ui pnpm -r --filter 'ui' build && pnpm -r --filter 'app' build",
"build:nodes": "pnpm -r --filter './nodes/**' build", "build:nodes": "pnpm -r --filter './nodes/**' build",
"dev:nodes": "pnpm -r --parallel --filter './nodes/**' dev",
"build:deploy": "pnpm build", "build:deploy": "pnpm build",
"dev": "pnpm -r --filter 'app' --filter './packages/node-registry' dev" "dev:all": "pnpm -r dev",
"dev:nodes": "pnpm -r --parallel --filter './nodes/**' dev",
"dev": "pnpm -r --filter 'app' --filter './packages/ui' dev"
}, },
"packageManager": "pnpm@10.24.0" "packageManager": "pnpm@10.24.0"
} }

View File

@@ -1,18 +1,5 @@
export type { NodeInput } from "./inputs"; export type { AsyncCache, NodeRegistry, RuntimeExecutor, SyncCache } from './components';
export type { export type { NodeInput } from './inputs';
NodeRegistry, export type { Box, Edge, Graph, NodeDefinition, NodeId, NodeInstance, SerializedNode, Socket } from './types';
RuntimeExecutor, export { GraphSchema, NodeSchema } from './types';
SyncCache, export { NodeDefinitionSchema } from './types';
AsyncCache,
} from "./components";
export type {
SerializedNode,
NodeInstance,
NodeDefinition,
Socket,
NodeId,
Edge,
Graph,
} from "./types";
export { NodeSchema, GraphSchema } from "./types";
export { NodeDefinitionSchema } from "./types";

View File

@@ -1,9 +1,16 @@
import { z } from "zod"; import { z } from 'zod';
import { NodeInputSchema } from "./inputs"; import { NodeInputSchema } from './inputs';
export type Box = {
minX: number;
maxX: number;
minY: number;
maxY: number;
};
export const NodeIdSchema = z export const NodeIdSchema = z
.string() .string()
.regex(/^[^/]+\/[^/]+\/[^/]+$/, "Invalid NodeId format") .regex(/^[^/]+\/[^/]+\/[^/]+$/, 'Invalid NodeId format')
.transform((value) => value as `${string}/${string}/${string}`); .transform((value) => value as `${string}/${string}/${string}`);
export type NodeId = z.infer<typeof NodeIdSchema>; export type NodeId = z.infer<typeof NodeIdSchema>;
@@ -35,9 +42,9 @@ export const NodeDefinitionSchema = z.object({
meta: z meta: z
.object({ .object({
description: z.string().optional(), description: z.string().optional(),
title: z.string().optional(), title: z.string().optional()
}) })
.optional(), .optional()
}); });
export const NodeSchema = z.object({ export const NodeSchema = z.object({
@@ -49,13 +56,12 @@ export const NodeSchema = z.object({
meta: z meta: z
.object({ .object({
title: z.string().optional(), title: z.string().optional(),
lastModified: z.string().optional(), lastModified: z.string().optional()
}) })
.optional(), .optional(),
position: z.tuple([z.number(), z.number()]), position: z.tuple([z.number(), z.number()])
}); });
export type SerializedNode = z.infer<typeof NodeSchema>; export type SerializedNode = z.infer<typeof NodeSchema>;
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & { export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & {
@@ -75,12 +81,12 @@ export const GraphSchema = z.object({
meta: z meta: z
.object({ .object({
title: z.string().optional(), title: z.string().optional(),
lastModified: z.string().optional(), lastModified: z.string().optional()
}) })
.optional(), .optional(),
settings: z.record(z.string(), z.any()).optional(), settings: z.record(z.string(), z.any()).optional(),
nodes: z.array(NodeSchema), nodes: z.array(NodeSchema),
edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()])), edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()]))
}); });
export type Graph = z.infer<typeof GraphSchema>; export type Graph = z.infer<typeof GraphSchema>;

View File

@@ -2,7 +2,7 @@
"name": "@nodarium/ui", "name": "@nodarium/ui",
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "chokidar './src/**' --initial -c 'pnpm build'",
"build": "vite build && npm run package", "build": "vite build && npm run package",
"preview": "vite preview", "preview": "vite preview",
"package": "svelte-kit sync && svelte-package && publint", "package": "svelte-kit sync && svelte-package && publint",
@@ -37,6 +37,7 @@
"@types/three": "^0.182.0", "@types/three": "^0.182.0",
"@typescript-eslint/eslint-plugin": "^8.53.0", "@typescript-eslint/eslint-plugin": "^8.53.0",
"@typescript-eslint/parser": "^8.53.0", "@typescript-eslint/parser": "^8.53.0",
"chokidar-cli": "^3.0.0",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-plugin-svelte": "^3.14.0", "eslint-plugin-svelte": "^3.14.0",
"publint": "^0.3.16", "publint": "^0.3.16",

View File

@@ -1,28 +1,28 @@
<script lang="ts"> <script lang="ts">
import Checkbox from './inputs/Checkbox.svelte';
import Float from './inputs/Float.svelte';
import Integer from './inputs/Integer.svelte';
import Select from './inputs/Select.svelte';
import type { NodeInput } from '@nodarium/types'; import type { NodeInput } from '@nodarium/types';
import Checkbox from './inputs/Checkbox.svelte';
import Number from './inputs/Number.svelte';
import Select from './inputs/Select.svelte';
import Vec3 from './inputs/Vec3.svelte'; import Vec3 from './inputs/Vec3.svelte';
interface Props { interface Props {
input: NodeInput; input: NodeInput;
value: any; value: any;
id?: string;
} }
let { input, value = $bindable() }: Props = $props(); let { input, value = $bindable(), id }: Props = $props();
</script> </script>
{#if input.type === 'float'} {#if input.type === 'float'}
<Float bind:value min={input?.min} max={input?.max} /> <Number bind:value min={input?.min} max={input?.max} step={0.01} />
{:else if input.type === 'integer'} {:else if input.type === 'integer'}
<Integer bind:value min={input?.min} max={input?.max} /> <Number bind:value min={input?.min} max={input?.max} />
{:else if input.type === 'boolean'} {:else if input.type === 'boolean'}
<Checkbox bind:value /> <Checkbox bind:value {id} />
{:else if input.type === 'select'} {:else if input.type === 'select'}
<Select bind:value options={input.options} /> <Select bind:value options={input.options} {id} />
{:else if input.type === 'vec3'} {:else if input.type === 'vec3'}
<Vec3 bind:value /> <Vec3 bind:value {id} />
{/if} {/if}

View File

@@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
value: boolean; value: boolean;
id?: string;
} }
let { value = $bindable(false) }: Props = $props(); let { value = $bindable(false), id }: Props = $props();
$effect(() => { $effect(() => {
if (typeof value === 'string') { if (typeof value === 'string') {
@@ -23,6 +24,7 @@
type="checkbox" type="checkbox"
bind:checked={value} bind:checked={value}
class="peer absolute h-px w-px overflow-hidden whitespace-nowrap border-0 p-0 [clip:rect(0,0,0,0)]" class="peer absolute h-px w-px overflow-hidden whitespace-nowrap border-0 p-0 [clip:rect(0,0,0,0)]"
{id}
/> />
<span <span
class="absolute opacity-0 peer-checked:opacity-100 transition-opacity duration-100 flex w-full h-full items-center justify-center" class="absolute opacity-0 peer-checked:opacity-100 transition-opacity duration-100 flex w-full h-full items-center justify-center"

View File

@@ -4,13 +4,15 @@
step?: number; step?: number;
min?: number; min?: number;
max?: number; max?: number;
id?: string;
} }
let { let {
value = $bindable(0.5), value = $bindable(0.5),
step = 0.01, step = 0.01,
min = $bindable(0), min = $bindable(0),
max = $bindable(1) max = $bindable(1),
id
}: Props = $props(); }: Props = $props();
if (min > max) { if (min > max) {
@@ -110,6 +112,7 @@
<input <input
bind:value bind:value
bind:this={inputEl} bind:this={inputEl}
{id}
{step} {step}
{max} {max}
{min} {min}

View File

@@ -1,41 +1,48 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
interface Props { interface Props {
value?: number;
step?: number;
min?: number | undefined; min?: number | undefined;
max?: number | undefined; max?: number | undefined;
step?: number;
value?: number;
id?: string; id?: string;
change?: (arg: number) => void;
} }
let { let {
min = undefined,
max = undefined,
step = 1,
value = $bindable(0), value = $bindable(0),
id = '' step = 1,
min = $bindable(0),
max = $bindable(1),
id,
change
}: Props = $props(); }: Props = $props();
if (!value) { if (min > max) {
value = 0; [min, max] = [max, min];
}
if (value > max) {
max = value;
} }
let inputEl: HTMLInputElement | undefined = $state(); function strip(input: number) {
let wrapper: HTMLDivElement | undefined = $state(); return +parseFloat(input + '').toPrecision(2);
}
let inputEl = $state() as HTMLInputElement;
let wrapper = $state() as HTMLDivElement;
let prev = -1; let prev = -1;
function update() { function update() {
if (prev === value) return; if (prev === value) return;
prev = value; prev = value;
dispatch('change', parseFloat(value + '')); change?.(value);
} }
function handleChange(change: number) { function handleChange(change: number) {
value = Math.max(min ?? -Infinity, Math.min(+value + change, max ?? Infinity)); value = Math.max(min ?? -Infinity, Math.min(+value + change, max ?? Infinity));
} }
let isMouseDown = $state(false);
let downX = 0; let downX = 0;
let downV = 0; let downV = 0;
let rect: DOMRect; let rect: DOMRect;
@@ -65,6 +72,13 @@
window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mousemove', handleMouseMove);
} }
function handleKeyDown(ev: KeyboardEvent) {
if (ev.key === 'Escape' || ev.key === 'Enter') {
handleMouseUp();
inputEl?.blur();
}
}
function handleMouseMove(ev: MouseEvent) { function handleMouseMove(ev: MouseEvent) {
if (!ev.ctrlKey && typeof min === 'number' && typeof max === 'number') { if (!ev.ctrlKey && typeof min === 'number' && typeof max === 'number') {
const vx = (ev.clientX - rect.left) / rect.width; const vx = (ev.clientX - rect.left) / rect.width;
@@ -76,8 +90,12 @@
} }
$effect(() => { $effect(() => {
if ((value || 0).toString().length > 5) {
value = strip(value || 0);
}
value !== undefined && update(); value !== undefined && update();
}); });
let width = $derived( let width = $derived(
Number.isFinite(value) ? Math.max((value?.toString().length ?? 1) * 8, 30) + 'px' : '20px' Number.isFinite(value) ? Math.max((value?.toString().length ?? 1) * 8, 30) + 'px' : '20px'
); );
@@ -86,29 +104,33 @@
<div <div
class="component-wrapper" class="component-wrapper"
bind:this={wrapper} bind:this={wrapper}
class:is-down={isMouseDown}
role="slider" role="slider"
tabindex="0" tabindex="0"
aria-valuenow={value} aria-valuenow={value}
onkeydown={handleKeyDown}
onmousedown={handleMouseDown} onmousedown={handleMouseDown}
onmouseup={handleMouseUp} onmouseup={handleMouseUp}
> >
<div class="">
<button onclick={() => handleChange(-step)}>-</button>
<input
bind:value
bind:this={inputEl}
{id}
{step}
{max}
{min}
type="number"
style={`width:${width};`}
/>
<button onclick={() => handleChange(+step)}>+</button>
</div>
{#if typeof min !== 'undefined' && typeof max !== 'undefined'} {#if typeof min !== 'undefined' && typeof max !== 'undefined'}
<span class="overlay" style={`width: ${Math.min((value - min) / (max - min), 1) * 100}%`} <span class="overlay" style={`width: ${Math.min((value - min) / (max - min), 1) * 100}%`}
></span> ></span>
{/if} {/if}
<button onclick={() => handleChange(-step)}>-</button>
<input
bind:value
bind:this={inputEl}
{id}
{step}
{max}
{min}
type="number"
style={`width:${width};`}
/>
<button onclick={() => handleChange(+step)}>+</button>
</div> </div>
<style> <style>
@@ -124,21 +146,32 @@
border-radius: var(--border-radius, 2px); border-radius: var(--border-radius, 2px);
} }
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
}
input[type='number'] { input[type='number'] {
-webkit-appearance: textfield; -webkit-appearance: textfield;
-moz-appearance: textfield; -moz-appearance: textfield;
appearance: textfield; appearance: textfield;
cursor: pointer; cursor: pointer;
font-size: 1em;
font-family: var(--font-family); font-family: var(--font-family);
padding-top: 8px; font-variant-numeric: tabular-nums;
color: var(--text-color);
background-color: transparent;
padding: var(--padding, 6px);
font-size: 1em;
padding-inline: 10px;
text-align: center;
border: none;
border-style: none;
flex: 1; flex: 1;
width: 72%; width: 72%;
} }
input[type='number']::-webkit-inner-spin-button, .is-down > input {
input[type='number']::-webkit-outer-spin-button { cursor: ew-resize !important;
-webkit-appearance: none;
} }
.overlay { .overlay {
@@ -151,6 +184,10 @@
pointer-events: none; pointer-events: none;
} }
.is-down > .overlay {
transition: none !important;
}
button { button {
background-color: transparent; background-color: transparent;
border: none; border: none;

View File

@@ -0,0 +1,177 @@
<script lang="ts">
interface Props {
value?: number;
step?: number;
min?: number;
max?: number;
id?: string;
}
let {
value = $bindable(1),
step = 1,
min = $bindable(0),
max = $bindable(1),
id: _id
}: Props = $props();
const uid = $props.id();
const id = $derived(_id || uid);
if (min > max) {
[min, max] = [max, min];
}
if (value > max) {
max = value;
}
function strip(input: number) {
return +parseFloat(input + '').toPrecision(2);
}
let inputEl = $state() as HTMLInputElement;
let prev = -1;
function update() {
if (prev === value) return;
if (value.toString().length > 5) {
value = strip(value);
}
prev = value;
}
function handleChange(change: number) {
value = Math.max(min ?? -Infinity, Math.min(+value + change, max ?? Infinity));
}
function handleKeyDown(ev: KeyboardEvent) {
if (ev.key === 'Escape' || ev.key === 'Enter') {
inputEl?.blur();
}
}
$effect(() => {
update();
});
let ratio = $derived(((value - min) / (max - min)) * 100);
</script>
<div>
<div class="component-wrapper">
<button onclick={() => handleChange(-step)}>-</button>
<input
bind:value
bind:this={inputEl}
{id}
{step}
{max}
{min}
type="number"
onkeydown={handleKeyDown}
/>
<button onclick={() => handleChange(+step)}>+</button>
</div>
<div class="slider">
<input
type="range"
bind:value
{min}
{max}
{step}
style={`background: linear-gradient(90deg, var(--text-color) ${ratio}%, var(--layer-2, #4b4b4b) ${ratio}%)`}
/>
</div>
</div>
<style>
:root {
--slider-height: 4px;
}
.component-wrapper {
display: flex;
background-color: var(--layer-2, #4b4b4b);
user-select: none;
transition: box-shadow 0.3s ease;
border: solid 1px var(--outline);
overflow: hidden;
border-radius: 0 var(--border-radius, 2px); /* only top */
}
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
}
input[type='number'] {
-webkit-appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
cursor: pointer;
font-family: var(--font-family);
font-variant-numeric: tabular-nums;
color: var(--text-color);
background-color: transparent;
padding: var(--padding, 6px);
font-size: 1em;
padding-inline: 10px;
text-align: center;
border: none;
border-style: none;
flex: 1;
width: 72%;
}
button {
background-color: transparent;
border: none;
cursor: pointer;
line-height: 0px;
margin: 0;
color: var(--text-color);
margin-inline: 6px;
}
div input[type='number'] {
color: var(--text-color);
background-color: transparent;
padding: var(--padding, 6px);
padding-inline: 0px;
text-align: center;
border: none;
border-style: none;
}
.slider {
position: relative;
margin-top: -1px; /* hide edge */
}
input[type='range'] {
position: absolute;
appearance: none;
width: 100%;
height: var(--slider-height);
background: var(--layer-2, #4b4b4b);
cursor: pointer;
}
/* Thumb: for Chrome, Safari, Edge */
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: var(--slider-height);
background: var(--text-color);
box-shadow: none;
}
/* Thumb: for Firefox */
input[type='range']::-moz-range-thumb {
border: none;
width: 12px;
height: var(--slider-height);
background: var(--text-color);
box-shadow: none;
}
</style>

309
pnpm-lock.yaml generated
View File

@@ -197,6 +197,9 @@ importers:
'@typescript-eslint/parser': '@typescript-eslint/parser':
specifier: ^8.53.0 specifier: ^8.53.0
version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
chokidar-cli:
specifier: ^3.0.0
version: 3.0.0
eslint: eslint:
specifier: ^9.39.2 specifier: ^9.39.2
version: 9.39.2(jiti@2.6.1) version: 9.39.2(jiti@2.6.1)
@@ -1248,10 +1251,22 @@ packages:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'} engines: {node: '>=6'}
ansi-regex@4.1.1:
resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==}
engines: {node: '>=6'}
ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
ansi-styles@4.3.0: ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'} engines: {node: '>=8'}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
arg@4.1.3: arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
@@ -1279,6 +1294,10 @@ packages:
bidi-js@1.0.3: bidi-js@1.0.3:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
boolbase@1.0.0: boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
@@ -1288,6 +1307,10 @@ packages:
brace-expansion@2.0.2: brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
buffer-from@1.1.2: buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -1311,6 +1334,10 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
camera-controls@3.1.2: camera-controls@3.1.2:
resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==} resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==}
engines: {node: '>=22.0.0', npm: '>=10.5.1'} engines: {node: '>=22.0.0', npm: '>=10.5.1'}
@@ -1325,6 +1352,15 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
chokidar-cli@3.0.0:
resolution: {integrity: sha512-xVW+Qeh7z15uZRxHOkP93Ux8A0xbPzwK4GaqD8dQOYc34TlkqUhVSS59fK36DOp5WdJlrRzlYSy02Ht99FjZqQ==}
engines: {node: '>= 8.10.0'}
hasBin: true
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
chokidar@4.0.3: chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'} engines: {node: '>= 14.16.0'}
@@ -1336,14 +1372,23 @@ packages:
citty@0.1.6: citty@0.1.6:
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
cliui@5.0.0:
resolution: {integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==}
clsx@2.1.1: clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
@@ -1437,6 +1482,10 @@ packages:
supports-color: supports-color:
optional: true optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
decimal.js@10.6.0: decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
@@ -1510,6 +1559,9 @@ packages:
earcut@2.2.4: earcut@2.2.4:
resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
emoji-regex@7.0.3:
resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==}
enhanced-resolve@5.18.4: enhanced-resolve@5.18.4:
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@@ -1658,6 +1710,14 @@ packages:
file-saver@2.0.5: file-saver@2.0.5:
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
find-up@3.0.0:
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
engines: {node: '>=6'}
find-up@5.0.0: find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1681,6 +1741,10 @@ packages:
function-bind@1.1.2: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1696,6 +1760,10 @@ packages:
resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==}
hasBin: true hasBin: true
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
glob-parent@6.0.2: glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@@ -1774,6 +1842,10 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'} engines: {node: '>=0.8.19'}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
is-docker@3.0.0: is-docker@3.0.0:
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -1783,6 +1855,10 @@ packages:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
is-fullwidth-code-point@2.0.0:
resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==}
engines: {node: '>=4'}
is-glob@4.0.3: is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1796,6 +1872,10 @@ packages:
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
hasBin: true hasBin: true
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-potential-custom-element-name@1.0.1: is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
@@ -1944,13 +2024,23 @@ packages:
locate-character@3.0.0: locate-character@3.0.0:
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
locate-path@3.0.0:
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
engines: {node: '>=6'}
locate-path@6.0.0: locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.merge@4.6.2: lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash.throttle@4.1.1:
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
lodash@4.17.21: lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
@@ -2049,6 +2139,10 @@ packages:
node-fetch-native@1.6.7: node-fetch-native@1.6.7:
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
nth-check@2.1.1: nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
@@ -2074,14 +2168,26 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
p-limit@3.1.0: p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
p-locate@3.0.0:
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
engines: {node: '>=6'}
p-locate@5.0.0: p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'} engines: {node: '>=10'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
package-manager-detector@1.6.0: package-manager-detector@1.6.0:
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
@@ -2096,6 +2202,10 @@ packages:
parse5@7.3.0: parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
path-exists@3.0.0:
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
engines: {node: '>=4'}
path-exists@4.0.0: path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -2113,6 +2223,10 @@ packages:
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
picomatch@4.0.3: picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -2182,6 +2296,10 @@ packages:
rc9@2.1.2: rc9@2.1.2:
resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
readdirp@4.1.2: readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'} engines: {node: '>= 14.18.0'}
@@ -2190,10 +2308,17 @@ packages:
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-from-string@2.0.2: require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
resolve-from@4.0.0: resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -2248,6 +2373,9 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
set-cookie-parser@2.7.2: set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
@@ -2287,6 +2415,14 @@ packages:
std-env@3.10.0: std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
string-width@3.1.0:
resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==}
engines: {node: '>=6'}
strip-ansi@5.2.0:
resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==}
engines: {node: '>=6'}
strip-json-comments@3.1.1: strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -2387,6 +2523,10 @@ packages:
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
hasBin: true hasBin: true
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
totalist@3.0.1: totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -2601,6 +2741,9 @@ packages:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'} engines: {node: '>=18'}
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
which@2.0.2: which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -2615,6 +2758,10 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
wrap-ansi@5.1.0:
resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==}
engines: {node: '>=6'}
ws@8.19.0: ws@8.19.0:
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@@ -2638,10 +2785,19 @@ packages:
xmlchars@2.2.0: xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
yaml@1.10.2: yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
yargs-parser@13.1.2:
resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==}
yargs@13.3.2:
resolution: {integrity: sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==}
yn@3.1.1: yn@3.1.1:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -3564,10 +3720,21 @@ snapshots:
ansi-colors@4.1.3: {} ansi-colors@4.1.3: {}
ansi-regex@4.1.1: {}
ansi-styles@3.2.1:
dependencies:
color-convert: 1.9.3
ansi-styles@4.3.0: ansi-styles@4.3.0:
dependencies: dependencies:
color-convert: 2.0.1 color-convert: 2.0.1
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
arg@4.1.3: arg@4.1.3:
optional: true optional: true
@@ -3588,6 +3755,8 @@ snapshots:
dependencies: dependencies:
require-from-string: 2.0.2 require-from-string: 2.0.2
binary-extensions@2.3.0: {}
boolbase@1.0.0: {} boolbase@1.0.0: {}
brace-expansion@1.1.12: brace-expansion@1.1.12:
@@ -3599,6 +3768,10 @@ snapshots:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
braces@3.0.3:
dependencies:
fill-range: 7.1.1
buffer-from@1.1.2: buffer-from@1.1.2:
optional: true optional: true
@@ -3629,6 +3802,8 @@ snapshots:
callsites@3.1.0: {} callsites@3.1.0: {}
camelcase@5.3.1: {}
camera-controls@3.1.2(three@0.182.0): camera-controls@3.1.2(three@0.182.0):
dependencies: dependencies:
three: 0.182.0 three: 0.182.0
@@ -3640,6 +3815,25 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
chokidar-cli@3.0.0:
dependencies:
chokidar: 3.6.0
lodash.debounce: 4.0.8
lodash.throttle: 4.1.1
yargs: 13.3.2
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
braces: 3.0.3
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
chokidar@4.0.3: chokidar@4.0.3:
dependencies: dependencies:
readdirp: 4.1.2 readdirp: 4.1.2
@@ -3652,12 +3846,24 @@ snapshots:
dependencies: dependencies:
consola: 3.4.2 consola: 3.4.2
cliui@5.0.0:
dependencies:
string-width: 3.1.0
strip-ansi: 5.2.0
wrap-ansi: 5.1.0
clsx@2.1.1: {} clsx@2.1.1: {}
color-convert@1.9.3:
dependencies:
color-name: 1.1.3
color-convert@2.0.1: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
color-name@1.1.3: {}
color-name@1.1.4: {} color-name@1.1.4: {}
color-support@1.1.3: {} color-support@1.1.3: {}
@@ -3742,6 +3948,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decamelize@1.2.0: {}
decimal.js@10.6.0: decimal.js@10.6.0:
optional: true optional: true
@@ -3805,6 +4013,8 @@ snapshots:
earcut@2.2.4: {} earcut@2.2.4: {}
emoji-regex@7.0.3: {}
enhanced-resolve@5.18.4: enhanced-resolve@5.18.4:
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
@@ -4021,6 +4231,14 @@ snapshots:
file-saver@2.0.5: {} file-saver@2.0.5: {}
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
find-up@3.0.0:
dependencies:
locate-path: 3.0.0
find-up@5.0.0: find-up@5.0.0:
dependencies: dependencies:
locate-path: 6.0.0 locate-path: 6.0.0
@@ -4048,6 +4266,8 @@ snapshots:
function-bind@1.1.2: function-bind@1.1.2:
optional: true optional: true
get-caller-file@2.0.5: {}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@@ -4082,6 +4302,10 @@ snapshots:
nypm: 0.6.2 nypm: 0.6.2
pathe: 2.0.3 pathe: 2.0.3
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
glob-parent@6.0.2: glob-parent@6.0.2:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
@@ -4155,10 +4379,16 @@ snapshots:
imurmurhash@0.1.4: {} imurmurhash@0.1.4: {}
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
is-docker@3.0.0: {} is-docker@3.0.0: {}
is-extglob@2.1.1: {} is-extglob@2.1.1: {}
is-fullwidth-code-point@2.0.0: {}
is-glob@4.0.3: is-glob@4.0.3:
dependencies: dependencies:
is-extglob: 2.1.1 is-extglob: 2.1.1
@@ -4169,6 +4399,8 @@ snapshots:
dependencies: dependencies:
is-docker: 3.0.0 is-docker: 3.0.0
is-number@7.0.0: {}
is-potential-custom-element-name@1.0.1: is-potential-custom-element-name@1.0.1:
optional: true optional: true
@@ -4313,12 +4545,21 @@ snapshots:
locate-character@3.0.0: {} locate-character@3.0.0: {}
locate-path@3.0.0:
dependencies:
p-locate: 3.0.0
path-exists: 3.0.0
locate-path@6.0.0: locate-path@6.0.0:
dependencies: dependencies:
p-locate: 5.0.0 p-locate: 5.0.0
lodash.debounce@4.0.8: {}
lodash.merge@4.6.2: {} lodash.merge@4.6.2: {}
lodash.throttle@4.1.1: {}
lodash@4.17.21: {} lodash@4.17.21: {}
lru-cache@10.4.3: lru-cache@10.4.3:
@@ -4406,6 +4647,8 @@ snapshots:
node-fetch-native@1.6.7: {} node-fetch-native@1.6.7: {}
normalize-path@3.0.0: {}
nth-check@2.1.1: nth-check@2.1.1:
dependencies: dependencies:
boolbase: 1.0.0 boolbase: 1.0.0
@@ -4443,14 +4686,24 @@ snapshots:
type-check: 0.4.0 type-check: 0.4.0
word-wrap: 1.2.5 word-wrap: 1.2.5
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
p-limit@3.1.0: p-limit@3.1.0:
dependencies: dependencies:
yocto-queue: 0.1.0 yocto-queue: 0.1.0
p-locate@3.0.0:
dependencies:
p-limit: 2.3.0
p-locate@5.0.0: p-locate@5.0.0:
dependencies: dependencies:
p-limit: 3.1.0 p-limit: 3.1.0
p-try@2.2.0: {}
package-manager-detector@1.6.0: {} package-manager-detector@1.6.0: {}
parent-module@1.0.1: parent-module@1.0.1:
@@ -4465,6 +4718,8 @@ snapshots:
entities: 6.0.1 entities: 6.0.1
optional: true optional: true
path-exists@3.0.0: {}
path-exists@4.0.0: {} path-exists@4.0.0: {}
path-key@3.1.1: {} path-key@3.1.1: {}
@@ -4475,6 +4730,8 @@ snapshots:
picocolors@1.1.1: {} picocolors@1.1.1: {}
picomatch@2.3.1: {}
picomatch@4.0.3: {} picomatch@4.0.3: {}
pify@4.0.1: pify@4.0.1:
@@ -4540,12 +4797,20 @@ snapshots:
defu: 6.1.4 defu: 6.1.4
destr: 2.0.5 destr: 2.0.5
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
readdirp@4.1.2: {} readdirp@4.1.2: {}
readdirp@5.0.0: {} readdirp@5.0.0: {}
require-directory@2.1.1: {}
require-from-string@2.0.2: {} require-from-string@2.0.2: {}
require-main-filename@2.0.0: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: resolve-pkg-maps@1.0.0:
@@ -4620,6 +4885,8 @@ snapshots:
semver@7.7.3: {} semver@7.7.3: {}
set-blocking@2.0.0: {}
set-cookie-parser@2.7.2: {} set-cookie-parser@2.7.2: {}
shebang-command@2.0.0: shebang-command@2.0.0:
@@ -4653,6 +4920,16 @@ snapshots:
std-env@3.10.0: {} std-env@3.10.0: {}
string-width@3.1.0:
dependencies:
emoji-regex: 7.0.3
is-fullwidth-code-point: 2.0.0
strip-ansi: 5.2.0
strip-ansi@5.2.0:
dependencies:
ansi-regex: 4.1.1
strip-json-comments@3.1.1: {} strip-json-comments@3.1.1: {}
supports-color@7.2.0: supports-color@7.2.0:
@@ -4772,6 +5049,10 @@ snapshots:
tldts-core: 6.1.86 tldts-core: 6.1.86
optional: true optional: true
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
totalist@3.0.1: {} totalist@3.0.1: {}
tough-cookie@5.1.2: tough-cookie@5.1.2:
@@ -4958,6 +5239,8 @@ snapshots:
webidl-conversions: 7.0.0 webidl-conversions: 7.0.0
optional: true optional: true
which-module@2.0.1: {}
which@2.0.2: which@2.0.2:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0
@@ -4969,6 +5252,12 @@ snapshots:
word-wrap@1.2.5: {} word-wrap@1.2.5: {}
wrap-ansi@5.1.0:
dependencies:
ansi-styles: 3.2.1
string-width: 3.1.0
strip-ansi: 5.2.0
ws@8.19.0: ws@8.19.0:
optional: true optional: true
@@ -4983,8 +5272,28 @@ snapshots:
xmlchars@2.2.0: xmlchars@2.2.0:
optional: true optional: true
y18n@4.0.3: {}
yaml@1.10.2: {} yaml@1.10.2: {}
yargs-parser@13.1.2:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs@13.3.2:
dependencies:
cliui: 5.0.0
find-up: 3.0.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 3.1.0
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 13.1.2
yn@3.1.1: yn@3.1.1:
optional: true optional: true