diff --git a/.coverage b/.coverage
deleted file mode 100644
index a53ebb9..0000000
Binary files a/.coverage and /dev/null differ
diff --git a/.gitignore b/.gitignore
index fcb291f..c4e5061 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
*.egg-info/
*.DS_Store
.pytest_cache/
+.coverage
pytest-cache-files-*/
desktop-build/
desktop-dist/
@@ -10,4 +11,4 @@ frontend/dist/
.venv/
sessions/
.*/
-frontend/coverage/tmp
\ No newline at end of file
+frontend/coverage/
\ No newline at end of file
diff --git a/README.md b/README.md
index 4495c07..d6cb8d3 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
-tono is a node-based image processing and analysis application.
+tono is a node-based SPM image processing and analysis tool. The main focus is on topographical measurements.
-It is heavily inspired by [Gwyddion](https://gwyddion.net/), one of the best scientific FOSS programs on the web.
+It is heavily inspired by [Gwyddion](https://gwyddion.net/), one of the my favorite scientific FOSS programs on the web.
## Project layout
diff --git a/frontend/coverage/lcov-report/angleMeasureGeometry.js.html b/frontend/coverage/lcov-report/angleMeasureGeometry.js.html
deleted file mode 100644
index b3d1cd4..0000000
--- a/frontend/coverage/lcov-report/angleMeasureGeometry.js.html
+++ /dev/null
@@ -1,316 +0,0 @@
-
-
-
-
-
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 | 18x -18x -18x -1x -1x -12x -12x -1x -1x -2x -2x -2x -2x -2x -2x - - -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -1x -1x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x - | function clamp01(value) {
- return Math.max(0, Math.min(1, Number(value) || 0));
-}
-
-export function round3(value) {
- return Number.parseFloat(Number(value).toFixed(3));
-}
-
-export function getAngleLabelBasePosition(x1, y1, xm, ym, x2, y2) {
- const va = { x: Number(x1) - Number(xm), y: Number(y1) - Number(ym) };
- const vb = { x: Number(x2) - Number(xm), y: Number(y2) - Number(ym) };
- const lenA = Math.hypot(va.x, va.y);
- const lenB = Math.hypot(vb.x, vb.y);
-
- if (lenA <= 1e-6 || lenB <= 1e-6) {
- return { x: clamp01(xm), y: clamp01(Number(ym) - 0.14) };
- }
-
- const unit = {
- x: (va.x / lenA) + (vb.x / lenB),
- y: (va.y / lenA) + (vb.y / lenB),
- };
- const unitLength = Math.hypot(unit.x, unit.y);
- const bisector = unitLength <= 1e-6
- ? { x: 0, y: -1 }
- : { x: unit.x / unitLength, y: unit.y / unitLength };
-
- return {
- x: clamp01(Number(xm) + bisector.x * 0.14),
- y: clamp01(Number(ym) + bisector.y * 0.14),
- };
-}
-
-export function getAngleLabelPosition(points, labelDx = 0, labelDy = 0) {
- const base = getAngleLabelBasePosition(points.x1, points.y1, points.xm, points.ym, points.x2, points.y2);
- return {
- x: clamp01(base.x + (Number(labelDx) || 0)),
- y: clamp01(base.y + (Number(labelDy) || 0)),
- };
-}
-
-export function moveAngleWidget(points, dx, dy) {
- const nextDx = Number(dx) || 0;
- const nextDy = Number(dy) || 0;
- const xs = [points.x1, points.xm, points.x2];
- const ys = [points.y1, points.ym, points.y2];
- const minX = Math.min(...xs);
- const maxX = Math.max(...xs);
- const minY = Math.min(...ys);
- const maxY = Math.max(...ys);
- const clampedDx = Math.max(-minX, Math.min(1 - maxX, nextDx));
- const clampedDy = Math.max(-minY, Math.min(1 - maxY, nextDy));
-
- return {
- x1: round3(clamp01(points.x1 + clampedDx)),
- y1: round3(clamp01(points.y1 + clampedDy)),
- xm: round3(clamp01(points.xm + clampedDx)),
- ym: round3(clamp01(points.ym + clampedDy)),
- x2: round3(clamp01(points.x2 + clampedDx)),
- y2: round3(clamp01(points.y2 + clampedDy)),
- };
-}
-
-export function measureAngleDegrees(x1, y1, xm, ym, x2, y2) {
- const ax = Number(x1) - Number(xm);
- const ay = Number(y1) - Number(ym);
- const bx = Number(x2) - Number(xm);
- const by = Number(y2) - Number(ym);
- const lenA = Math.hypot(ax, ay);
- const lenB = Math.hypot(bx, by);
-
- if (lenA <= 1e-12 || lenB <= 1e-12) return 0;
-
- const cosTheta = ((ax * bx) + (ay * by)) / (lenA * lenB);
- const clamped = Math.max(-1, Math.min(1, cosTheta));
- return Math.acos(clamped) * (180 / Math.PI);
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 | 1x -1x -1x -42x -42x -42x -42x - - - -42x -1x -17x -17x -17x -1x -25x -25x -25x -25x -1x -1x -9x -9x -6x -9x -1x -1x -3x -3x -3x - - -2x -3x -1x -1x -5x -5x -5x -2x -2x -2x -5x -1x -1x -3x -3x -3x - | const EXCLUDED_CANVAS_TARGETS = '.context-menu, .react-flow__node, .react-flow__edge, .react-flow__controls, .react-flow__minimap, .surface-view-container';
-const CANVAS_AREA_TARGETS = '.react-flow, .react-flow__renderer, .react-flow__viewport, .react-flow__pane, .react-flow__background, .react-flow__selectionpane';
-
-function getTargetElement(target) {
- if (!target) return null;
- if (typeof target.closest === 'function') return target;
- if (target.parentElement && typeof target.parentElement.closest === 'function') {
- return target.parentElement;
- }
- return null;
-}
-
-function supportsClosest(target) {
- return !!getTargetElement(target);
-}
-
-function matchesClosest(target, selector) {
- const element = getTargetElement(target);
- return !!element && element.closest(selector) !== null;
-}
-
-export function isEditableInteractionTarget(target) {
- if (!supportsClosest(target)) return false;
- if (matchesClosest(target, 'input, textarea, select')) return true;
- return matchesClosest(target, '[contenteditable="true"]');
-}
-
-export function canStartCanvasRightDragZoomTarget(target) {
- if (!supportsClosest(target)) return false;
- if (isEditableInteractionTarget(target)) return false;
- if (matchesClosest(target, EXCLUDED_CANVAS_TARGETS)) {
- return false;
- }
- return matchesClosest(target, CANVAS_AREA_TARGETS);
-}
-
-export function canOpenCanvasContextMenuTarget(target) {
- if (!supportsClosest(target)) return false;
- if (isEditableInteractionTarget(target)) return false;
- if (matchesClosest(target, EXCLUDED_CANVAS_TARGETS)) {
- return false;
- }
- return matchesClosest(target, CANVAS_AREA_TARGETS);
-}
-
-export function isSecondaryCanvasContextEvent(event) {
- if (!event || typeof event.button !== 'number') return false;
- return event.button === 2 || (event.button === 0 && !!event.ctrlKey);
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 | 1x -1x -1x -1x -1x -1x -1x -1x -20x -20x -1x -1x -12x -12x -1x -1x -5x -5x -1x -1x -11x -11x -1x -1x -12x -12x -12x -1x -1x -12x -1x -1x -33x -33x -9x -33x -7x -7x -7x -7x -7x -7x -33x -1x -1x -11x -11x -11x -1x -1x -10x -10x -10x -10x -10x -10x -10x -1x -1x -14x -14x -10x -10x -14x -14x -14x -1x -1x -1x -1x -17x -7x -7x -10x -17x -3x -3x -3x -8x -17x -17x -3x -3x -17x -17x -1x -1x -8x -3x -3x -8x -1x -1x -8x -1x -1x -8x -1x -1x -2x -8x -1x -1x -1x -1x -1x -1x -8x -8x -8x -8x -8x -3x -3x -8x -8x -8x -8x -8x -8x -8x -8x - | // ── Connection utility functions ───────────────────────────────────────
-// Pure functions extracted from App.jsx so they can be independently tested.
-
-import { socketSpecAcceptsType } from './constants.js';
-
-// ── Handle ID helpers ─────────────────────────────────────────────────
-
-export function getHandleType(handleId) {
- return handleId.split('::')[2];
-}
-
-export function getInputName(handleId) {
- return handleId.split('::')[1];
-}
-
-export function getOutputSlot(handleId) {
- return parseInt(handleId.split('::')[1], 10);
-}
-
-export function encodeProxyHandleRef(handleId) {
- return encodeURIComponent(String(handleId || ''));
-}
-
-export function decodeProxyHandleRef(encoded) {
- try {
- return decodeURIComponent(String(encoded || ''));
- } catch {
- return String(encoded || '');
- }
-}
-
-export function parseGroupProxyHandle(handleId) {
- const text = String(handleId || '');
- if (!text.startsWith('group-proxy::')) return null;
- const parts = text.split('::');
- if (parts.length < 5) return null;
- return {
- direction: parts[1],
- nodeId: parts[2],
- type: parts[3],
- realHandle: decodeProxyHandleRef(parts.slice(4).join('::')),
- };
-}
-
-export function getConnectionHandleType(handleId) {
- const proxy = parseGroupProxyHandle(handleId);
- return proxy?.type || getHandleType(handleId);
-}
-
-export function getResolvedHandleRef(nodeId, handleId) {
- const proxy = parseGroupProxyHandle(handleId);
- return {
- nodeId: proxy?.nodeId || nodeId,
- handleId: proxy?.realHandle || handleId,
- type: proxy?.type || getHandleType(handleId),
- };
-}
-
-export function getNodeInputSpecForHandle(node, handleId) {
- const definition = node?.data?.definition;
- if (!definition?.input) return null;
- const inputName = getInputName(handleId);
- return definition.input.required?.[inputName]
- || definition.input.optional?.[inputName]
- || null;
-}
-
-// ── Type compatibility ────────────────────────────────────────────────
-
-export function outputTypeCanConnectToTarget(outputType, targetSpecOrType, outputAcceptedTypes = []) {
- if (socketSpecAcceptsType(outputType, targetSpecOrType)) {
- return true;
- }
- // Polymorphic output: the output socket declares it can also produce the target type
- if (outputAcceptedTypes.length > 0) {
- const targetType = Array.isArray(targetSpecOrType) ? targetSpecOrType[0] : targetSpecOrType;
- if (outputAcceptedTypes.includes(targetType)) return true;
- }
- return outputType === 'ANNOTATION_SOURCE'
- && !socketSpecAcceptsType('ANNOTATION_SOURCE', targetSpecOrType)
- && (
- socketSpecAcceptsType('DATA_FIELD', targetSpecOrType)
- || socketSpecAcceptsType('IMAGE', targetSpecOrType)
- );
-}
-
-export function resolveOutputTypeForTarget(outputType, targetSpecOrType) {
- if (outputType !== 'ANNOTATION_SOURCE') {
- return outputType;
- }
- if (socketSpecAcceptsType('ANNOTATION_SOURCE', targetSpecOrType)) {
- return 'ANNOTATION_SOURCE';
- }
- if (socketSpecAcceptsType('DATA_FIELD', targetSpecOrType)) {
- return 'DATA_FIELD';
- }
- if (socketSpecAcceptsType('IMAGE', targetSpecOrType)) {
- return 'IMAGE';
- }
- return 'ANNOTATION_SOURCE';
-}
-
-// ── Pure connection validation ────────────────────────────────────────
-// Extracted from the isValidConnection useCallback so it can be unit-tested
-// without a ReactFlow context. Pass a `getNodeFn` that mirrors reactFlow.getNode.
-
-export function checkConnectionValid(connection, getNodeFn) {
- const srcType = getConnectionHandleType(connection.sourceHandle);
- const resolvedTarget = getResolvedHandleRef(connection.target, connection.targetHandle);
- const targetNode = getNodeFn(resolvedTarget.nodeId);
- const targetSpec = getNodeInputSpecForHandle(targetNode, resolvedTarget.handleId) || resolvedTarget.type;
- if (socketSpecAcceptsType(srcType, targetSpec)) return true;
- // Polymorphic output: check if the source output declares it can produce the target type
- const srcProxy = parseGroupProxyHandle(connection.sourceHandle);
- const srcNodeId = srcProxy ? srcProxy.nodeId : connection.source;
- const srcHandleId = srcProxy ? srcProxy.realHandle : connection.sourceHandle;
- const srcNode = getNodeFn(srcNodeId);
- const srcSlot = getOutputSlot(srcHandleId);
- const srcAcceptedTypes = srcNode?.data?.definition?.output_accepted_types?.[srcSlot] || [];
- const targetType = Array.isArray(targetSpec) ? targetSpec[0] : targetSpec;
- return Array.isArray(srcAcceptedTypes) && srcAcceptedTypes.includes(targetType);
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 | 5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -60x -60x -60x - -60x -5x -5x -23x -23x -5x -5x -23x -23x -23x -5x -5x -50x -50x -50x -50x - - -50x -50x -50x -50x -3x -3x -3x -3x -50x -50x -50x -17x -17x -17x -17x -50x -50x -50x -5x -5x -49x -49x -49x -5x -5x -5x -5x -5x -5x -5x - | // ── Shared type & color constants ─────────────────────────────────────
-
-export const DATA_TYPES = new Set([
- 'DATA_FIELD', 'IMAGE', 'LINE', 'RECORD_TABLE', 'DATA_TABLE',
- 'COORD', 'ANNOTATION_SOURCE', 'COLORMAP',
- 'MESH_MODEL', 'FONT', 'FILE_PATH', 'DIRECTORY', 'COORDPAIR',
-]);
-
-export const SOCKET_WIDGET_TYPES = new Set(['FLOAT', 'INT']);
-
-export const TYPE_COLORS = {
- DATA_FIELD: '#3a7abf',
- IMAGE: '#00ff08a0',
- LINE: '#ffbe5c',
- RECORD_TABLE: '#35e2fd',
- DATA_TABLE: '#ff7474',
- COORD: '#e91ed1',
- COORDPAIR: '#5cb861',
- FLOAT: '#ab3197',
- INT: '#ffffff',
- ANNOTATION_SOURCE: '#06b6d4',
- COLORMAP: '#f472b6',
- MESH_MODEL: '#14b8a6',
- FONT: '#fb7185',
- FILE_PATH: '#f59e0b',
- DIRECTORY: '#f97316',
-};
-
-export const CAT_COLORS = {
- Input: '#37474f',
- Display: '#212121',
- Overlay: '#0f766e',
- Geometry: '#0d9488',
- Filter: '#1a237e',
- Spectral: '#4c1d95',
- 'Level & Correct': '#1b5e20',
- Measure: '#4a148c',
- Mask: '#7c2d12',
- Grains: '#bf360c',
-};
-
-export const SOCKET_COMPATIBILITY = {
- FLOAT: new Set(['INT']),
- INT: new Set(['FLOAT']),
- LINE: new Set(['COORDPAIR']),
-};
-
-const EMPTY_SOCKET_TYPE_SET = new Set();
-
-export function getSpecTypeAndOptions(spec) {
- if (Array.isArray(spec)) {
- return [spec[0], spec[1] || {}];
- }
- return [spec, {}];
-}
-
-export function isDataSocketType(type) {
- return typeof type === 'string' && DATA_TYPES.has(type);
-}
-
-export function isDataSocketSpec(spec) {
- const [type] = getSpecTypeAndOptions(spec);
- return isDataSocketType(type);
-}
-
-export function getAcceptedSocketTypes(specOrType) {
- const [type, opts] = Array.isArray(specOrType)
- ? getSpecTypeAndOptions(specOrType)
- : [specOrType, {}];
- if (typeof type !== 'string') {
- return EMPTY_SOCKET_TYPE_SET;
- }
-
- const accepted = new Set([type]);
- const explicitAccepted = Array.isArray(opts?.accepted_types) ? opts.accepted_types : [];
- for (const acceptedType of explicitAccepted) {
- if (typeof acceptedType === 'string' && acceptedType) {
- accepted.add(acceptedType);
- }
- }
-
- const fallbackAccepted = SOCKET_COMPATIBILITY[type];
- if (fallbackAccepted) {
- for (const acceptedType of fallbackAccepted) {
- accepted.add(acceptedType);
- }
- }
-
- return accepted;
-}
-
-export function socketSpecAcceptsType(sourceType, targetSpecOrType) {
- if (typeof sourceType !== 'string' || !sourceType) return false;
- return getAcceptedSocketTypes(targetSpecOrType).has(sourceType);
-}
-
-// Colors used in Canvas 2D / toBlob contexts where CSS var() is unavailable.
-export const CANVAS_COLORS = {
- bgDeep: '#0f172a',
- maskStroke: '#ffffff',
- maskOverlay: 'rgba(255, 59, 59, 0.16)',
-};
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 | 1x -1x -1x -1x -1x -1x -1x -11x -11x -11x -11x -11x -2x -2x -9x -11x -11x -11x -11x -5x - - -4x -11x -2x -1x -1x -2x - - -2x -2x -11x -1x -11x - - -1x -11x -1x -1x -6x -6x -6x -6x -11x -11x -2x -2x -2x -2x -2x -2x -11x -4x -6x - | import { extractWorkflow } from './pngMetadata.js';
-
-const DEFAULT_WORKFLOW_CANDIDATES = [
- { path: '/default-workflow.json', type: 'json' },
- { path: '/default-workflow.png', type: 'png' },
-];
-
-async function loadCandidate(candidate, fetchImpl, extractWorkflowFn) {
- let response;
- try {
- response = await fetchImpl(candidate.path, { cache: 'no-store' });
- } catch {
- return null;
- }
-
- const contentType = response.headers?.get?.('content-type') || '';
- const isHtmlFallback = typeof contentType === 'string' && contentType.toLowerCase().includes('text/html');
-
- if (!response.ok) {
- if (response.status === 404 || response.status === 0) return null;
- throw new Error(`Failed to load ${candidate.path} (${response.status})`);
- }
-
- if (candidate.type === 'json') {
- if (isHtmlFallback) return null;
- try {
- return await response.json();
- } catch {
- throw new Error(`${candidate.path} is not valid JSON`);
- }
- }
-
- if (isHtmlFallback) return null;
- const workflow = await extractWorkflowFn(await response.blob());
- if (!workflow) {
- throw new Error(`${candidate.path} does not contain embedded workflow metadata`);
- }
- return workflow;
-}
-
-export async function loadDefaultWorkflowAsset({
- fetchImpl = fetch,
- extractWorkflowFn = extractWorkflow,
-} = {}) {
- for (const candidate of DEFAULT_WORKFLOW_CANDIDATES) {
- const workflow = await loadCandidate(candidate, fetchImpl, extractWorkflowFn);
- if (workflow) {
- return {
- source: candidate.path,
- format: candidate.type,
- workflow,
- };
- }
- }
- return null;
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 | 1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -9x -9x -9x -1x -5x -5x -5x -1x -22x -22x -22x -3x -3x -22x -22x -22x -22x -22x -22x -1x -1x -11x -11x -7x -7x -7x -7x -11x -11x -1x -11x -11x -11x -1x -9x -9x -5x -5x -9x -4x -4x - -9x -1x -11x -11x -11x -11x -11x -25x -25x -6x -6x -25x -11x -11x -11x -1x -1x -7x -7x -7x -7x -16x -13x -13x -16x -16x -16x -13x -13x -16x -16x -16x -16x -16x -16x -16x -16x -18x -12x -18x -18x -7x -7x -7x -18x -13x -13x -13x -13x -16x -5x -5x -5x -5x -13x -13x -13x -7x -7x -7x -1x -1x -4x -4x -4x -1x -1x -5x -5x -5x -5x -5x -7x -7x -7x -7x -7x -7x -1x -1x -1x -1x -7x -7x -7x -7x -6x -7x -2x -1x -1x -7x -4x -4x -3x -3x -3x -4x -7x -7x -3x -3x -5x - | import { getSpecTypeAndOptions, isDataSocketSpec } from './constants.js';
-
-const OMITTED_WIDGET_INPUTS_BY_CLASS = {
- View3D: new Set([
- 'camera_azimuth',
- 'camera_polar',
- 'camera_distance',
- 'camera_target_x',
- 'camera_target_y',
- 'camera_target_z',
- ]),
-};
-
-function getInputName(handleId) {
- return handleId.split('::')[1];
-}
-
-function getOutputSlot(handleId) {
- return parseInt(handleId.split('::')[1], 10);
-}
-
-function resolveExecutionEdge(edge) {
- const original = edge?.data?.groupProxyOriginal;
- if (!original) return edge;
- return {
- ...edge,
- source: original.source || edge.source,
- sourceHandle: original.sourceHandle || edge.sourceHandle,
- target: original.target || edge.target,
- targetHandle: original.targetHandle || edge.targetHandle,
- };
-}
-
-export function getConnectedNodeIds(edges) {
- const connectedNodeIds = new Set();
- for (const edge of edges) {
- const resolved = resolveExecutionEdge(edge);
- connectedNodeIds.add(resolved.source);
- connectedNodeIds.add(resolved.target);
- }
- return connectedNodeIds;
-}
-
-function isPreviewLoadNode(node) {
- return ['Image', 'ImageDemo'].includes(node?.data?.className);
-}
-
-function hasPreviewLoadSelection(node) {
- if (node?.data?.className === 'Image') {
- return !!String(node.data?.widgetValues?.filename || '').trim();
- }
- if (node?.data?.className === 'ImageDemo') {
- return !!String(node.data?.widgetValues?.name || '').trim();
- }
- return false;
-}
-
-function getRunnableNodeIds(nodes, edges) {
- const connectedNodeIds = getConnectedNodeIds(edges);
-
- const runnableNodeIds = new Set(connectedNodeIds);
- for (const node of nodes) {
- if (connectedNodeIds.has(node.id)) continue;
- if (isPreviewLoadNode(node) && hasPreviewLoadSelection(node)) {
- runnableNodeIds.add(node.id);
- }
- }
-
- return runnableNodeIds;
-}
-
-export function serializeExecutionGraph(nodes, edges, { excludeManualTrigger = false } = {}) {
- const runnableNodeIds = getRunnableNodeIds(nodes, edges);
- const prompt = {};
-
- for (const node of nodes) {
- if (!runnableNodeIds.has(node.id)) continue;
-
- const { className, definition, widgetValues, runtimeValues } = node.data;
- if (className === 'Group') continue;
- if (!definition) continue;
- if (excludeManualTrigger && definition.manual_trigger) continue;
-
- const inputs = {};
- const valueBag = { ...(widgetValues || {}), ...(runtimeValues || {}) };
- const omittedInputs = OMITTED_WIDGET_INPUTS_BY_CLASS[className] || null;
-
- const allWidgets = {
- ...(definition.input.required || {}),
- ...(definition.input.optional || {}),
- };
- for (const [name, spec] of Object.entries(allWidgets)) {
- if (omittedInputs?.has(name)) continue;
- const [type] = getSpecTypeAndOptions(spec);
- if (isDataSocketSpec(spec)) continue;
- if (type === 'BUTTON') continue;
- if (valueBag[name] !== undefined) {
- inputs[name] = valueBag[name];
- }
- }
-
- const incoming = edges
- .map(resolveExecutionEdge)
- .filter((edge) => edge.target === node.id);
- for (const edge of incoming) {
- const inputName = getInputName(edge.targetHandle);
- const outputSlot = getOutputSlot(edge.sourceHandle);
- inputs[inputName] = [edge.source, outputSlot];
- }
-
- prompt[node.id] = { class_type: className, inputs };
- }
-
- return prompt;
-}
-
-export function getAutoRunnableNodes(nodes, edges) {
- const runnableNodeIds = getRunnableNodeIds(nodes, edges);
- return nodes.filter((node) => runnableNodeIds.has(node.id));
-}
-
-export function hasBlockingAutoRunInput(node, edges) {
- const def = node.data?.definition;
- if (!def || def.manual_trigger) return false;
-
- const required = def.input.required || {};
- for (const [name, spec] of Object.entries(required)) {
- const [type, opts] = getSpecTypeAndOptions(spec);
- const hiddenByConnectedInput = (() => {
- const raw = opts?.hide_when_input_connected;
- if (!raw) return false;
- const inputs = Array.isArray(raw) ? raw : [raw];
- return inputs.some((inputName) => edges.some(
- (edge) => {
- const resolved = resolveExecutionEdge(edge);
- return resolved.target === node.id && getInputName(resolved.targetHandle) === String(inputName);
- }
- ));
- })();
-
- if (hiddenByConnectedInput) continue;
-
- if (type === 'FILE_PICKER' || type === 'FOLDER_PICKER') {
- if (!node.data.widgetValues?.[name]) return true;
- continue;
- }
- if (!isDataSocketSpec(spec)) continue;
- const hasEdge = edges.some(
- (edge) => {
- const resolved = resolveExecutionEdge(edge);
- return resolved.target === node.id && getInputName(resolved.targetHandle) === name;
- }
- );
- if (!hasEdge) return true;
- }
-
- return false;
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 | 1x -1x -1x -3x -3x -3x -3x -3x -3x -3x -3x -3x -3x -3x -1x -1x -2x -2x - | export const GROUP_DRAG_RELEASE_DISTANCE = 18;
-
-export function getPointDistanceOutsideRect(rect, point) {
- if (!rect || !point) return Infinity;
-
- const dx = point.x < rect.left
- ? rect.left - point.x
- : (point.x > rect.right ? point.x - rect.right : 0);
- const dy = point.y < rect.top
- ? rect.top - point.y
- : (point.y > rect.bottom ? point.y - rect.bottom : 0);
-
- return Math.hypot(dx, dy);
-}
-
-export function shouldReleaseFromGroup(rect, point, threshold = GROUP_DRAG_RELEASE_DISTANCE) {
- return getPointDistanceOutsideRect(rect, point) >= threshold;
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 | 1x -1x -1x -4x -4x -4x -4x -4x -4x -4x -4x - -4x -1x -1x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x - | const DEFAULT_CHILD_WIDTH = 200;
-const DEFAULT_CHILD_HEIGHT = 120;
-
-function getNodeSize(node, axis) {
- const fallback = axis === 'width' ? DEFAULT_CHILD_WIDTH : DEFAULT_CHILD_HEIGHT;
- const measured = Number(node?.measured?.[axis]);
- if (Number.isFinite(measured) && measured > 0) return measured;
- const direct = Number(node?.[axis]);
- if (Number.isFinite(direct) && direct > 0) return direct;
- const styled = Number(node?.style?.[axis]);
- if (Number.isFinite(styled) && styled > 0) return styled;
- return fallback;
-}
-
-export function getGroupMinimumSize(memberNodes, {
- minWidth = 260,
- minHeight = 180,
- paddingX = 24,
- paddingY = 24,
-} = {}) {
- let maxRight = 0;
- let maxBottom = 0;
-
- for (const node of memberNodes || []) {
- const x = Number(node?.position?.x) || 0;
- const y = Number(node?.position?.y) || 0;
- maxRight = Math.max(maxRight, x + getNodeSize(node, 'width'));
- maxBottom = Math.max(maxBottom, y + getNodeSize(node, 'height'));
- }
-
- return {
- width: Math.max(minWidth, Math.ceil(maxRight + paddingX)),
- height: Math.max(minHeight, Math.ceil(maxBottom + paddingY)),
- };
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| angleMeasureGeometry.js | -
-
- |
- 97.4% | -75/77 | -53.33% | -8/15 | -100% | -6/6 | -97.4% | -75/77 | -
| canvasInteractionTargets.js | -
-
- |
- 89.79% | -44/49 | -68% | -17/25 | -100% | -7/7 | -89.79% | -44/49 | -
| connectionUtils.js | -
-
- |
- 100% | -122/122 | -93.93% | -62/66 | -100% | -12/12 | -100% | -122/122 | -
| constants.js | -
-
- |
- 97.05% | -99/102 | -76.47% | -13/17 | -100% | -5/5 | -97.05% | -99/102 | -
| defaultWorkflow.js | -
-
- |
- 89.28% | -50/56 | -86.36% | -19/22 | -100% | -2/2 | -89.28% | -50/56 | -
| executionGraph.js | -
-
- |
- 99.36% | -157/158 | -79.48% | -62/78 | -100% | -10/10 | -99.36% | -157/158 | -
| groupDrag.js | -
-
- |
- 100% | -18/18 | -55.55% | -5/9 | -100% | -2/2 | -100% | -18/18 | -
| groupSizing.js | -
-
- |
- 97.14% | -34/35 | -35.71% | -5/14 | -100% | -2/2 | -97.14% | -34/35 | -
| loadNodeOutputs.js | -
-
- |
- 90% | -27/30 | -66.66% | -10/15 | -100% | -3/3 | -90% | -27/30 | -
| markupShapeGeometry.js | -
-
- |
- 76.53% | -75/98 | -44.44% | -8/18 | -83.33% | -5/6 | -76.53% | -75/98 | -
| nodeClipboard.js | -
-
- |
- 94.37% | -302/320 | -59.67% | -74/124 | -100% | -17/17 | -94.37% | -302/320 | -
| nodeHierarchy.js | -
-
- |
- 100% | -28/28 | -86.66% | -13/15 | -100% | -2/2 | -100% | -28/28 | -
| nodeWidgetDefaults.js | -
-
- |
- 92.3% | -24/26 | -69.23% | -9/13 | -100% | -2/2 | -92.3% | -24/26 | -
| nodeWidgetLayout.js | -
-
- |
- 100% | -49/49 | -73.07% | -19/26 | -100% | -4/4 | -100% | -49/49 | -
| pngMetadata.js | -
-
- |
- 98.78% | -162/164 | -61.11% | -22/36 | -100% | -8/8 | -98.78% | -162/164 | -
| runtimeValuePersistence.js | -
-
- |
- 90% | -9/10 | -87.5% | -7/8 | -100% | -1/1 | -90% | -9/10 | -
| valueFormatting.js | -
-
- |
- 80.35% | -180/224 | -65.57% | -40/61 | -83.33% | -10/12 | -80.35% | -180/224 | -
| workflowCapture.js | -
-
- |
- 40.93% | -79/193 | -36.84% | -7/19 | -16.66% | -2/12 | -40.93% | -79/193 | -
| workflowHydration.js | -
-
- |
- 100% | -82/82 | -73.52% | -25/34 | -100% | -5/5 | -100% | -82/82 | -
| workflowSerialization.js | -
-
- |
- 100% | -65/65 | -64.7% | -22/34 | -100% | -4/4 | -100% | -65/65 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 | 1x -3x -3x -3x -3x -3x -3x -1x -1x -3x - - -3x -1x -1x -1x -1x -1x - -3x -1x -1x -2x -2x -2x -2x -1x -1x -2x -2x - | export function resolveLoadNodeChannelPath({
- explicitPath = null,
- resolvedPathInput = null,
- className = '',
- widgetValues = {},
-} = {}) {
- if (typeof explicitPath === 'string' && explicitPath) {
- return explicitPath;
- }
- if (typeof resolvedPathInput === 'string' && resolvedPathInput) {
- return resolvedPathInput;
- }
- if (className === 'Image') {
- return String(widgetValues?.filename || '');
- }
- if (className === 'ImageDemo') {
- return String(widgetValues?.name || '');
- }
- return '';
-}
-
-export function beginTrackedNodeRequest(requestVersions, nodeId) {
- const nextVersion = (requestVersions.get(nodeId) || 0) + 1;
- requestVersions.set(nodeId, nextVersion);
- return nextVersion;
-}
-
-export function isTrackedNodeRequestCurrent(requestVersions, nodeId, version) {
- return requestVersions.get(nodeId) === version;
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 | 1x -1x -1x -1x -4x -4x -4x -4x -4x -1x -1x -2x -1x -2x -2x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x - - - - - - - - - - - - - - - - - - - - - - - -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -2x -2x -2x -2x -2x - | export const MARKUP_DEFAULT_SHAPE = 'arrow';
-export const MARKUP_DEFAULT_COLOR = '#ff0000';
-export const MARKUP_PREVIEW_REFERENCE_DIM = 512;
-
-function clampFraction(value) {
- const numeric = Number(value);
- if (!Number.isFinite(numeric)) return 0;
- return Math.max(0, Math.min(1, numeric));
-}
-
-export function sanitizeMarkupColor(color, fallback = MARKUP_DEFAULT_COLOR) {
- if (typeof color !== 'string') return fallback;
- const value = color.trim();
- return /^#[0-9a-fA-F]{6}$/.test(value) ? value.toLowerCase() : fallback;
-}
-
-export function sanitizeMarkupShape(
- shape,
- fallbackShape = MARKUP_DEFAULT_SHAPE,
- fallbackColor = MARKUP_DEFAULT_COLOR,
- fallbackWidth = 3,
-) {
- if (!shape || typeof shape !== 'object') return null;
- const kind = ['line', 'rectangle', 'circle', 'arrow'].includes(shape.kind) ? shape.kind : fallbackShape;
- const x1 = clampFraction(shape.x1);
- const y1 = clampFraction(shape.y1);
- const x2 = clampFraction(shape.x2);
- const y2 = clampFraction(shape.y2);
- const width = Math.max(1, Math.min(64, Math.round(Number(shape.width) || fallbackWidth || 1)));
- return {
- kind,
- x1: Number(x1.toFixed(4)),
- y1: Number(y1.toFixed(4)),
- x2: Number(x2.toFixed(4)),
- y2: Number(y2.toFixed(4)),
- width,
- color: sanitizeMarkupColor(shape.color, fallbackColor),
- };
-}
-
-export function parseMarkupShapes(
- markupShapes,
- fallbackShape = MARKUP_DEFAULT_SHAPE,
- fallbackColor = MARKUP_DEFAULT_COLOR,
- fallbackWidth = 3,
-) {
- if (Array.isArray(markupShapes)) {
- return markupShapes
- .map((shape) => sanitizeMarkupShape(shape, fallbackShape, fallbackColor, fallbackWidth))
- .filter(Boolean);
- }
-
- if (typeof markupShapes !== 'string' || !markupShapes.trim()) return [];
-
- try {
- const parsed = JSON.parse(markupShapes);
- if (!Array.isArray(parsed)) return [];
- return parsed
- .map((shape) => sanitizeMarkupShape(shape, fallbackShape, fallbackColor, fallbackWidth))
- .filter(Boolean);
- } catch {
- return [];
- }
-}
-
-export function getArrowGeometry(shape, imageWidth, imageHeight) {
- const x1 = shape.x1 * imageWidth;
- const y1 = shape.y1 * imageHeight;
- const x2 = shape.x2 * imageWidth;
- const y2 = shape.y2 * imageHeight;
- const dx = x2 - x1;
- const dy = y2 - y1;
- const length = Math.hypot(dx, dy) || 1;
- const ux = dx / length;
- const uy = dy / length;
- const strokeWidth = Math.max(1, shape.width);
- const headLength = Math.max(10, strokeWidth * 4);
- const headWidth = Math.max(8, strokeWidth * 3);
- const shaftX = x2 - ux * headLength;
- const shaftY = y2 - uy * headLength;
- const px = -uy;
- const py = ux;
- const leftX = shaftX + px * headWidth * 0.5;
- const leftY = shaftY + py * headWidth * 0.5;
- const rightX = shaftX - px * headWidth * 0.5;
- const rightY = shaftY - py * headWidth * 0.5;
- return {
- line: `${x1},${y1} ${shaftX},${shaftY}`,
- head: `${x2},${y2} ${leftX},${leftY} ${rightX},${rightY}`,
- };
-}
-
-export function getMarkupPreviewStrokeWidth(width, imageWidth, imageHeight) {
- const normalizedWidth = Math.max(1, Math.round(Number(width) || 1));
- const longestDim = Math.max(1, Number(imageWidth) || 0, Number(imageHeight) || 0);
- const scale = Math.max(1, longestDim / MARKUP_PREVIEW_REFERENCE_DIM);
- return Math.max(1, Math.round(normalizedWidth * scale));
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 | 2x -2x -2x -2x -2x -26x -26x -26x -26x -26x -26x - - -26x - -26x -2x -39x -39x -39x -39x -2x -2x -2x -2x -2x -2x -2x -2x -2x - - -2x -2x -9x -9x -9x -2x -9x -2x -2x -2x -2x -2x -2x -9x -2x -6x -6x -6x -2x -5x -5x -5x -5x -2x -9x -9x -9x -2x -9x -2x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -2x -8x -8x -8x -1x -1x -8x - - -8x -8x -2x -1x -1x -1x -1x -1x - - -1x -1x -1x -1x -1x -1x -1x -1x -1x - - -1x -1x -1x -1x -1x -1x -1x -2x -4x -4x -4x -4x -4x -4x -4x -4x -8x -8x -8x - - - -8x -4x -4x -4x -2x -5x -5x -5x -5x -15x -15x -15x -15x -15x -15x -15x -15x -15x -15x -15x -15x -5x -5x -5x -2x -2x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -2x -2x -2x -4x -4x -4x -4x -4x -4x -4x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -5x -4x -4x -2x -2x -2x -2x -2x -2x -2x -4x -4x -4x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -1x -1x -1x -1x -1x -1x -1x -1x - - -1x -2x -2x -5x -5x -5x -5x -5x -5x -5x - - -5x -5x -5x -5x -5x -8x -5x -5x -5x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -8x -5x -5x -5x -5x -3x -3x -5x -5x -3x -3x -3x -3x -3x -3x -3x -3x -3x -3x -3x -3x -3x -5x -5x -5x -5x -5x -5x -5x -5x - | import { sortNodesForParentOrder } from './nodeHierarchy.js';
-
-export const NODE_CLIPBOARD_KIND = 'tono/node-selection';
-export const NODE_CLIPBOARD_MIME = 'application/x-tono-node-selection';
-
-function cloneValue(value) {
- if (value == null) return value;
- if (typeof structuredClone === 'function') {
- try {
- return structuredClone(value);
- } catch {
- // Fall through to JSON clone for simple plain data.
- }
- }
- return JSON.parse(JSON.stringify(value));
-}
-
-function clonePlainObject(value) {
- if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
- return cloneValue(value) || {};
-}
-
-function encodeProxyHandleRef(handleId) {
- return encodeURIComponent(String(handleId || ''));
-}
-
-function decodeProxyHandleRef(encoded) {
- try {
- return decodeURIComponent(String(encoded || ''));
- } catch {
- return String(encoded || '');
- }
-}
-
-function parseGroupProxyHandle(handleId) {
- const text = String(handleId || '');
- if (!text.startsWith('group-proxy::')) return null;
- const parts = text.split('::');
- if (parts.length < 5) return null;
- return {
- direction: parts[1],
- nodeId: parts[2],
- type: parts[3],
- realHandle: decodeProxyHandleRef(parts.slice(4).join('::')),
- };
-}
-
-function hasOwn(obj, key) {
- return Object.prototype.hasOwnProperty.call(obj, key);
-}
-
-function remapNodeId(value, idMap) {
- if (value == null) return value;
- return idMap.get(String(value)) || String(value);
-}
-
-function remapGroupProxyHandle(handleId, idMap) {
- const proxy = parseGroupProxyHandle(handleId);
- if (!proxy) return handleId;
- return `group-proxy::${proxy.direction}::${remapNodeId(proxy.nodeId, idMap)}::${proxy.type}::${encodeProxyHandleRef(proxy.realHandle)}`;
-}
-
-function remapGroupProxyDescriptors(items, idMap) {
- if (!Array.isArray(items)) return items;
- return items.map((item) => {
- if (!item || typeof item !== 'object') return item;
- const nextItem = { ...item };
- if (typeof nextItem.key === 'string') {
- const separator = nextItem.key.indexOf('::');
- if (separator !== -1) {
- const handleId = nextItem.key.slice(separator + 2);
- nextItem.key = `${remapNodeId(nextItem.key.slice(0, separator), idMap)}::${remapGroupProxyHandle(handleId, idMap)}`;
- }
- }
- if (typeof nextItem.handleId === 'string') {
- nextItem.handleId = remapGroupProxyHandle(nextItem.handleId, idMap);
- }
- return nextItem;
- });
-}
-
-function remapClipboardExtraData(extraData, idMap) {
- const nextExtraData = clonePlainObject(extraData);
- if (Array.isArray(nextExtraData.proxyInputs)) {
- nextExtraData.proxyInputs = remapGroupProxyDescriptors(nextExtraData.proxyInputs, idMap);
- }
- if (Array.isArray(nextExtraData.proxyOutputs)) {
- nextExtraData.proxyOutputs = remapGroupProxyDescriptors(nextExtraData.proxyOutputs, idMap);
- }
- return nextExtraData;
-}
-
-function remapClipboardEdgeData(data, idMap) {
- if (!data || typeof data !== 'object' || Array.isArray(data)) return cloneValue(data);
-
- const nextData = cloneValue(data);
- if (hasOwn(nextData, 'groupInternalHiddenBy')) {
- nextData.groupInternalHiddenBy = remapNodeId(nextData.groupInternalHiddenBy, idMap);
- }
- if (hasOwn(nextData, 'groupProxyOwner')) {
- nextData.groupProxyOwner = remapNodeId(nextData.groupProxyOwner, idMap);
- }
-
- const original = nextData.groupProxyOriginal;
- if (original && typeof original === 'object' && !Array.isArray(original)) {
- if (hasOwn(original, 'source')) original.source = remapNodeId(original.source, idMap);
- if (hasOwn(original, 'target')) original.target = remapNodeId(original.target, idMap);
- if (hasOwn(original, 'sourceHandle')) {
- original.sourceHandle = remapGroupProxyHandle(original.sourceHandle, idMap);
- }
- if (hasOwn(original, 'targetHandle')) {
- original.targetHandle = remapGroupProxyHandle(original.targetHandle, idMap);
- }
- }
-
- return nextData;
-}
-
-function collectSelectedNodeIds(nodes, nodeIds) {
- const selectedIdSet = new Set((Array.isArray(nodeIds) ? nodeIds : []).map((id) => String(id)));
- if (selectedIdSet.size === 0) return selectedIdSet;
-
- let changed = true;
- while (changed) {
- changed = false;
- for (const node of Array.isArray(nodes) ? nodes : []) {
- const parentId = node?.parentId ? String(node.parentId) : null;
- const nodeId = String(node?.id);
- if (parentId && selectedIdSet.has(parentId) && !selectedIdSet.has(nodeId)) {
- selectedIdSet.add(nodeId);
- changed = true;
- }
- }
- }
- return selectedIdSet;
-}
-
-function extractExtraData(data) {
- const source = data || {};
- return Object.fromEntries(
- Object.entries(source).filter(([key]) => ![
- 'label',
- 'className',
- 'widgetValues',
- 'runtimeValues',
- 'definition',
- 'previewImage',
- 'tableRows',
- 'meshData',
- 'overlay',
- 'scalarValue',
- 'processingTimeMs',
- 'warning',
- ].includes(key)),
- );
-}
-
-export function buildNodeClipboardPayloadForIds(
- nodes,
- edges,
- nodeIds,
- { includeIncomingExternalEdges = false } = {},
-) {
- const selectedIdSet = collectSelectedNodeIds(nodes, nodeIds);
- const selectedNodes = Array.isArray(nodes)
- ? nodes.filter((node) => selectedIdSet.has(String(node.id)))
- : [];
- if (selectedNodes.length === 0) return null;
-
- const capturedEdges = Array.isArray(edges)
- ? edges.filter((edge) => (
- selectedIdSet.has(String(edge.target))
- && (
- selectedIdSet.has(String(edge.source))
- || (includeIncomingExternalEdges && !selectedIdSet.has(String(edge.source)))
- )
- ))
- : [];
-
- return {
- kind: NODE_CLIPBOARD_KIND,
- version: 1,
- nodes: selectedNodes.map((node) => ({
- id: String(node.id),
- type: node.type || 'custom',
- position: {
- x: Number(node.position?.x) || 0,
- y: Number(node.position?.y) || 0,
- },
- ...(node.className ? { className: node.className } : {}),
- ...(node.parentId ? { parentId: String(node.parentId) } : {}),
- ...(node.extent ? { extent: node.extent } : {}),
- ...(node.hidden ? { hidden: true } : {}),
- ...(node.style ? { style: cloneValue(node.style) } : {}),
- dragHandle: node.dragHandle || '.drag-handle',
- data: {
- label: node.data?.label || node.data?.className || 'Node',
- className: node.data?.className || '',
- widgetValues: clonePlainObject(node.data?.widgetValues),
- runtimeValues: clonePlainObject(node.data?.runtimeValues),
- extraData: clonePlainObject(extractExtraData(node.data)),
- },
- })),
- edges: capturedEdges.map((edge) => ({
- source: String(edge.source),
- sourceHandle: edge.sourceHandle,
- target: String(edge.target),
- targetHandle: edge.targetHandle,
- ...(edge.style ? { style: { ...edge.style } } : {}),
- ...(edge.hidden ? { hidden: true } : {}),
- ...(edge.data ? { data: cloneValue(edge.data) } : {}),
- })),
- };
-}
-
-export function buildNodeClipboardPayload(nodes, edges) {
- const selectedNodes = Array.isArray(nodes)
- ? nodes.filter((node) => node?.selected)
- : [];
- const selectedIds = selectedNodes.map((node) => String(node.id));
- const includeIncomingExternalEdges = selectedNodes.some((node) => node?.data?.className === 'Group');
- return buildNodeClipboardPayloadForIds(nodes, edges, selectedIds, { includeIncomingExternalEdges });
-}
-
-export function parseNodeClipboardPayload(text) {
- if (typeof text !== 'string' || !text.trim()) return null;
-
- try {
- const parsed = JSON.parse(text);
- if (parsed?.kind !== NODE_CLIPBOARD_KIND) return null;
- if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.edges)) return null;
- return parsed;
- } catch {
- return null;
- }
-}
-
-export function instantiateNodeClipboardPayload(
- payload,
- defs = {},
- nextNodeId = 1,
- offset = { x: 40, y: 40 },
- { keepExternalSources = false } = {},
-) {
- if (!payload || !Array.isArray(payload.nodes) || payload.nodes.length === 0) {
- return { nodes: [], edges: [], nextNodeId };
- }
-
- const idMap = new Map();
- let currentId = Number(nextNodeId) || 1;
-
- payload.nodes.forEach((node) => {
- idMap.set(String(node.id), String(currentId++));
- });
-
- const nodes = sortNodesForParentOrder(payload.nodes.map((node) => {
- const newId = idMap.get(String(node.id));
- const className = node.data?.className || '';
- const definition = className ? defs[className] || null : null;
- const extraData = remapClipboardExtraData(node.data?.extraData, idMap);
-
- return {
- id: newId,
- type: node.type || 'custom',
- className: node.className,
- position: {
- x: (Number(node.position?.x) || 0) + (Number(offset?.x) || 0),
- y: (Number(node.position?.y) || 0) + (Number(offset?.y) || 0),
- },
- ...(node.parentId ? { parentId: idMap.get(String(node.parentId)) || String(node.parentId) } : {}),
- ...(node.extent ? { extent: node.extent } : {}),
- ...(node.hidden ? { hidden: true } : {}),
- ...(node.style ? { style: cloneValue(node.style) } : {}),
- dragHandle: node.dragHandle || '.drag-handle',
- selected: true,
- data: {
- label: node.data?.label || className || 'Node',
- className,
- widgetValues: clonePlainObject(node.data?.widgetValues),
- runtimeValues: clonePlainObject(node.data?.runtimeValues),
- ...extraData,
- definition,
- previewImage: null,
- tableRows: null,
- meshData: null,
- overlay: null,
- scalarValue: null,
- processingTimeMs: null,
- warning: null,
- },
- };
- }));
-
- const edges = payload.edges
- .filter((edge) => (
- idMap.has(String(edge.target))
- && (idMap.has(String(edge.source)) || keepExternalSources)
- ))
- .map((edge, index) => {
- const source = idMap.get(String(edge.source)) || String(edge.source);
- const target = idMap.get(String(edge.target));
- return {
- id: `e${source}-${target}-${index}`,
- source,
- sourceHandle: remapGroupProxyHandle(edge.sourceHandle, idMap),
- target,
- targetHandle: remapGroupProxyHandle(edge.targetHandle, idMap),
- selected: false,
- ...(edge.style ? { style: { ...edge.style } } : {}),
- ...(edge.hidden ? { hidden: true } : {}),
- ...(edge.data ? { data: remapClipboardEdgeData(edge.data, idMap) } : {}),
- };
- });
-
- return {
- nodes,
- edges,
- nextNodeId: currentId,
- };
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 | 3x -12x -12x -12x -12x -12x -12x -12x -12x -24x -24x -24x -19x -19x -19x -24x -24x -5x -5x -19x -19x -19x -19x -24x -12x -12x -12x -12x - | export function sortNodesForParentOrder(nodes) {
- const list = Array.isArray(nodes) ? nodes.filter(Boolean) : [];
- const entries = list.map((node) => ({ id: String(node.id), node }));
- const byId = new Map(entries.map((entry) => [entry.id, entry]));
- const visiting = new Set();
- const visited = new Set();
- const ordered = [];
-
- function visit(entry) {
- if (!entry) return;
- const { id, node } = entry;
- if (visited.has(id) || visiting.has(id)) return;
-
- visiting.add(id);
-
- const parentId = node?.parentId ? String(node.parentId) : null;
- if (parentId) {
- visit(byId.get(parentId));
- }
-
- visiting.delete(id);
- visited.add(id);
- ordered.push(node);
- }
-
- entries.forEach((entry) => visit(entry));
- return ordered;
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 | 1x -1x -1x -6x -6x -6x -6x -2x -2x -2x - - -6x -6x -1x -1x -1x -1x -1x -5x -5x -3x -3x -5x -1x -1x - | import { getSpecTypeAndOptions, isDataSocketSpec } from './constants.js';
-
-export function getDefaultWidgetValue(spec) {
- const [type, opts] = getSpecTypeAndOptions(spec);
- if (isDataSocketSpec(spec)) return undefined;
- if (type === 'BUTTON') return undefined;
- if (Array.isArray(type)) {
- if (typeof opts?.default === 'string' && type.includes(opts.default)) {
- return opts.default;
- }
- return type[0];
- }
- return opts?.default ?? '';
-}
-
-export function buildDefaultWidgetValues(definition) {
- const widgetValues = {};
- const required = definition?.input?.required || {};
- for (const [name, spec] of Object.entries(required)) {
- const value = getDefaultWidgetValue(spec);
- if (value !== undefined) {
- widgetValues[name] = value;
- }
- }
- return widgetValues;
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 | 1x -4x -4x -4x -4x -4x -4x -1x -5x -5x -5x -5x -5x -5x -1x -1x -3x -3x -1x -1x -2x -3x -3x -2x -3x -2x -2x -2x -2x -1x -1x -2x -1x -1x -3x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x -1x - | export function formatUiLabel(text) {
- return String(text ?? '')
- .replace(/_/g, ' ')
- .replace(/\s+/g, ' ')
- .trim()
- .toLowerCase();
-}
-
-function normalizeInputNames(raw) {
- if (!raw) return [];
- return (Array.isArray(raw) ? raw : [raw])
- .map((value) => String(value))
- .filter((value) => value.length > 0);
-}
-
-export function getWidgetCombinedInputName(widget, dataInputByName) {
- const explicitInputName = normalizeInputNames(widget?.opts?.top_socket_input)[0];
- if (explicitInputName && dataInputByName?.has(explicitInputName)) {
- return explicitInputName;
- }
-
- const widgetLabel = formatUiLabel(widget?.opts?.label || widget?.name);
- if (!widgetLabel) return null;
-
- for (const inputName of normalizeInputNames(widget?.opts?.hide_when_input_connected)) {
- const input = dataInputByName?.get(inputName);
- if (!input) continue;
- const inputLabel = formatUiLabel(input.label || input.name);
- if (inputLabel === widgetLabel) {
- return input.name;
- }
- }
-
- return null;
-}
-
-export function buildCombinedInputNameByWidgetName(widgets, dataInputs) {
- const dataInputByName = new Map((dataInputs || []).map((input) => [input.name, input]));
- const combinedInputNameByWidgetName = new Map();
-
- for (const widget of widgets || []) {
- const combinedInputName = getWidgetCombinedInputName(widget, dataInputByName);
- if (combinedInputName) {
- combinedInputNameByWidgetName.set(widget.name, combinedInputName);
- }
- }
-
- return combinedInputNameByWidgetName;
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 | 2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -2x -512x -512x -4096x -4096x -512x -512x -2x -4x -4x -4x -382x -382x -4x -4x -2x -2x -2x -2x -2x -7x -7x -7x -56x -56x -7x -7x -2x -26x -26x -26x -26x -26x -2x -26x -26x -26x -2x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -2x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x - - -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -2x -2x -2x -2x -2x -2x -2x -2x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -4x -13x -13x -13x -13x -9x -9x -4x -4x -4x -4x -4x -4x -4x -4x -4x -2x -2x -2x -2x -2x -2x -3x -3x -3x -3x -3x -3x -3x -13x -13x -13x -13x -13x -4x -4x -4x -4x -13x -13x -10x -10x -3x -3x -3x - | /**
- * PNG text chunk utilities for embedding/extracting workflow metadata.
- *
- * PNG files are composed of chunks: [4-byte length][4-byte type][data][4-byte CRC].
- * We add an iTXt chunk with key "workflow" containing the JSON-serialised graph,
- * inserted just before the IEND chunk.
- */
-
-// ── CRC32 (PNG uses CRC-32/ISO 3309) ────────────────────────────────
-
-const crcTable = new Uint32Array(256);
-for (let i = 0; i < 256; i++) {
- let c = i;
- for (let j = 0; j < 8; j++) {
- c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
- }
- crcTable[i] = c;
-}
-
-function crc32(bytes) {
- let crc = 0xFFFFFFFF;
- for (let i = 0; i < bytes.length; i++) {
- crc = crcTable[(crc ^ bytes[i]) & 0xFF] ^ (crc >>> 8);
- }
- return (crc ^ 0xFFFFFFFF) >>> 0;
-}
-
-// ── Helpers ──────────────────────────────────────────────────────────
-
-const PNG_SIG = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
-
-function isPng(data) {
- if (data.length < 8) return false;
- for (let i = 0; i < 8; i++) {
- if (data[i] !== PNG_SIG[i]) return false;
- }
- return true;
-}
-
-function chunkType(data, offset) {
- return String.fromCharCode(
- data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
- );
-}
-
-function readUint32(data, offset) {
- return new DataView(data.buffer, data.byteOffset + offset, 4).getUint32(0);
-}
-
-function buildChunk(type, payload) {
- const encoder = new TextEncoder();
- const typeBytes = encoder.encode(type);
- const forCrc = new Uint8Array(4 + payload.length);
- forCrc.set(typeBytes, 0);
- forCrc.set(payload, 4);
-
- const chunk = new Uint8Array(12 + payload.length);
- const view = new DataView(chunk.buffer);
- view.setUint32(0, payload.length);
- chunk.set(typeBytes, 4);
- chunk.set(payload, 8);
- view.setUint32(8 + payload.length, crc32(forCrc));
- return chunk;
-}
-
-function parseTextChunk(type, chunkData) {
- const decoder = new TextDecoder();
- const keywordEnd = chunkData.indexOf(0);
- if (keywordEnd === -1) return null;
-
- const keyword = decoder.decode(chunkData.subarray(0, keywordEnd));
- if (keyword !== 'workflow') return null;
-
- if (type !== 'iTXt') return null;
-
- const compressionFlagIdx = keywordEnd + 1;
- const compressionMethodIdx = keywordEnd + 2;
- if (compressionMethodIdx >= chunkData.length) return null;
-
- const compressionFlag = chunkData[compressionFlagIdx];
- if (compressionFlag !== 0) {
- throw new Error('Compressed PNG workflow metadata is not supported');
- }
-
- let offset = compressionMethodIdx + 1;
- const languageEnd = chunkData.indexOf(0, offset);
- if (languageEnd === -1) return null;
-
- offset = languageEnd + 1;
- const translatedEnd = chunkData.indexOf(0, offset);
- if (translatedEnd === -1) return null;
-
- return JSON.parse(decoder.decode(chunkData.subarray(translatedEnd + 1)));
-}
-
-// ── Public API ───────────────────────────────────────────────────────
-
-/**
- * Embed a workflow object into a PNG blob as an iTXt chunk.
- * Returns a new Blob with the metadata inserted before IEND.
- */
-export async function embedWorkflow(pngBlob, workflow) {
- const data = new Uint8Array(await pngBlob.arrayBuffer());
- if (!isPng(data)) throw new Error('Not a valid PNG file');
-
- const encoder = new TextEncoder();
-
- // Build iTXt payload:
- // keyword \0 compression-flag compression-method language-tag \0 translated-keyword \0 text
- const key = encoder.encode('workflow');
- const val = encoder.encode(JSON.stringify(workflow));
- const payload = new Uint8Array(key.length + 5 + val.length);
- payload.set(key, 0);
- payload.set(val, key.length + 5);
- const chunk = buildChunk('iTXt', payload);
-
- // Locate IEND
- let pos = 8;
- let iendPos = data.length;
- while (pos < data.length) {
- if (pos + 8 > data.length) break;
- const len = readUint32(data, pos);
- if (pos + 12 + len > data.length) break;
- if (chunkType(data, pos) === 'IEND') { iendPos = pos; break; }
- pos += 12 + len;
- }
-
- // Splice: [before IEND] + [tEXt chunk] + [IEND]
- const result = new Uint8Array(data.length + chunk.length);
- result.set(data.subarray(0, iendPos), 0);
- result.set(chunk, iendPos);
- result.set(data.subarray(iendPos), iendPos + chunk.length);
-
- return new Blob([result], { type: 'image/png' });
-}
-
-/**
- * Extract the workflow object from a PNG blob's iTXt chunks.
- * Returns the parsed object, or null if no "workflow" key is found.
- */
-export async function extractWorkflow(pngBlob) {
- const data = new Uint8Array(await pngBlob.arrayBuffer());
- if (!isPng(data)) return null;
-
- let pos = 8;
- let found = null;
-
- while (pos + 8 <= data.length) {
- const len = readUint32(data, pos);
- if (pos + 12 + len > data.length) break;
- const type = chunkType(data, pos);
-
- if (type === 'iTXt') {
- const chunkData = data.subarray(pos + 8, pos + 8 + len);
- const parsed = parseTextChunk(type, chunkData);
- if (parsed) found = parsed;
- }
-
- if (type === 'IEND') break;
- pos += 12 + len;
- }
-
- return found;
-}
- |