Broaden canvas workflow import compatibility
This commit is contained in:
@@ -223,16 +223,42 @@ function normalizeCanvasProjectState(value: unknown): CanvasProjectState {
|
||||
};
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
function firstString(...values: unknown[]) {
|
||||
return values.find((value): value is string => typeof value === 'string' && value.trim().length > 0)?.trim() || '';
|
||||
}
|
||||
|
||||
function firstNumber(...values: unknown[]) {
|
||||
return values.find((value): value is number => typeof value === 'number' && Number.isFinite(value));
|
||||
}
|
||||
|
||||
function firstArray(...values: unknown[]) {
|
||||
return values.find((value): value is unknown[] => Array.isArray(value)) || [];
|
||||
}
|
||||
|
||||
function getExternalNodes(value: unknown): unknown[] {
|
||||
const input = asRecord(value);
|
||||
return firstArray(input.nodes, input.elements, input.items, input.cells, input.blocks);
|
||||
}
|
||||
|
||||
function getExternalConnections(value: unknown): unknown[] {
|
||||
const input = asRecord(value);
|
||||
return firstArray(input.connections, input.edges, input.links, input.lines);
|
||||
}
|
||||
|
||||
function isExternalCanvasProject(value: unknown) {
|
||||
if (!value || typeof value !== 'object') return false;
|
||||
const input = value as { nodes?: unknown; view?: unknown; projectName?: unknown };
|
||||
if (!Array.isArray(input.nodes)) return false;
|
||||
return input.nodes.some(node => (
|
||||
const input = asRecord(value);
|
||||
const nodes = getExternalNodes(value);
|
||||
if (nodes.length === 0) return false;
|
||||
return nodes.some(node => (
|
||||
!!node
|
||||
&& typeof node === 'object'
|
||||
&& typeof (node as { type?: unknown }).type === 'string'
|
||||
&& ['text-node', 'gen-image', 'image-node', 'image'].includes((node as { type: string }).type)
|
||||
)) || !!input.view || typeof input.projectName === 'string';
|
||||
)) || !!input.view || typeof input.projectName === 'string' || typeof input.name === 'string';
|
||||
}
|
||||
|
||||
function getExternalAssetKey(value?: unknown) {
|
||||
@@ -241,6 +267,22 @@ function getExternalAssetKey(value?: unknown) {
|
||||
return '';
|
||||
}
|
||||
|
||||
function getAssetLookupKeys(pathOrName: string) {
|
||||
const normalized = pathOrName.replace(/\\/g, '/').replace(/^\.?\//, '');
|
||||
const name = normalized.split('/').pop() || normalized;
|
||||
const withoutExt = normalized.replace(/\.[^.]+$/, '');
|
||||
const nameWithoutExt = name.replace(/\.[^.]+$/, '');
|
||||
const afterAssets = normalized.includes('/assets/') ? normalized.split('/assets/').pop() || normalized : normalized.startsWith('assets/') ? normalized.slice('assets/'.length) : '';
|
||||
return Array.from(new Set([
|
||||
normalized,
|
||||
withoutExt,
|
||||
name,
|
||||
nameWithoutExt,
|
||||
afterAssets,
|
||||
afterAssets ? afterAssets.replace(/\.[^.]+$/, '') : '',
|
||||
].filter(Boolean)));
|
||||
}
|
||||
|
||||
function resolveExternalAssetUrl(value: unknown, assetMap?: ExternalAssetMap) {
|
||||
if (typeof value !== 'string' || !value) return '';
|
||||
const assetKey = getExternalAssetKey(value);
|
||||
@@ -280,6 +322,17 @@ function getImportCanvasCandidate(payload: unknown) {
|
||||
|| parsed;
|
||||
}
|
||||
|
||||
function normalizeExternalNodeType(type: string, node: Record<string, unknown>, settings: Record<string, unknown>): CanvasNodeType {
|
||||
const normalized = type.toLowerCase().replace(/[_\s-]+/g, '');
|
||||
if (normalized.includes('text')) return 'text';
|
||||
if (normalized.includes('img2img') || normalized.includes('imagetoimage')) return 'img2img';
|
||||
if (normalized.includes('text2img') || normalized.includes('txt2img') || normalized.includes('texttoimage')) return 'text2img';
|
||||
if (normalized.includes('gen') || normalized.includes('generate')) return 'image';
|
||||
if (normalized.includes('image') || normalized.includes('asset')) return 'image';
|
||||
if (firstString(settings.text, node.text, node.content) && !firstString(node.content, node.imageUrl, node.url, node.src)) return 'text';
|
||||
return Array.isArray(settings.generatedImages) || firstString(node.prompt, settings.prompt) ? 'image' : 'text';
|
||||
}
|
||||
|
||||
function normalizeExternalRatio(value: unknown) {
|
||||
if (typeof value !== 'string' || !value) return undefined;
|
||||
const match = value.match(/^(\d+)\s*x\s*(\d+)$/i);
|
||||
@@ -307,32 +360,26 @@ function normalizeExternalResolution(value: unknown) {
|
||||
}
|
||||
|
||||
function convertExternalCanvasProject(value: unknown, assetMap?: ExternalAssetMap): CanvasProjectState {
|
||||
const input = value as {
|
||||
nodes?: unknown[];
|
||||
connections?: unknown[];
|
||||
groups?: unknown[];
|
||||
view?: { x?: unknown; y?: unknown; zoom?: unknown };
|
||||
};
|
||||
const input = asRecord(value);
|
||||
const view = asRecord(input.view || input.viewport);
|
||||
const createdAt = nowIso();
|
||||
const nodes = (Array.isArray(input.nodes) ? input.nodes : []).map((rawNode, index): CanvasNode | null => {
|
||||
const nodes = getExternalNodes(value).map((rawNode, index): CanvasNode | null => {
|
||||
if (!rawNode || typeof rawNode !== 'object') return null;
|
||||
const node = rawNode as Record<string, unknown>;
|
||||
const type = typeof node.type === 'string' ? node.type : '';
|
||||
const settings = node.settings && typeof node.settings === 'object' ? node.settings as Record<string, unknown> : {};
|
||||
const id = typeof node.id === 'string' ? node.id : createId('external-node');
|
||||
const title = typeof node.nodeName === 'string' ? node.nodeName : typeof node.title === 'string' ? node.title : '外部节点';
|
||||
const x = typeof node.x === 'number' ? node.x : 120 + index * 420;
|
||||
const y = typeof node.y === 'number' ? node.y : 120;
|
||||
const width = typeof node.width === 'number' ? node.width : 360;
|
||||
const height = typeof node.height === 'number' ? node.height : 420;
|
||||
if (type === 'text-node') {
|
||||
const text = typeof settings.text === 'string'
|
||||
? settings.text
|
||||
: typeof node.prompt === 'string'
|
||||
? node.prompt
|
||||
: typeof node.content === 'string'
|
||||
? node.content
|
||||
: '';
|
||||
const position = asRecord(node.position);
|
||||
const size = asRecord(node.size);
|
||||
const data = asRecord(node.data);
|
||||
const settings = asRecord(node.settings || data.settings || data);
|
||||
const rawType = firstString(node.type, data.type, settings.type);
|
||||
const type = normalizeExternalNodeType(rawType, node, settings);
|
||||
const id = firstString(node.id, data.id) || createId('external-node');
|
||||
const title = firstString(node.nodeName, node.title, node.name, data.label, data.title, settings.title) || '外部节点';
|
||||
const x = firstNumber(node.x, position.x, data.x) ?? 120 + index * 420;
|
||||
const y = firstNumber(node.y, position.y, data.y) ?? 120;
|
||||
const width = firstNumber(node.width, size.width, data.width) ?? 360;
|
||||
const height = firstNumber(node.height, size.height, data.height) ?? 420;
|
||||
if (type === 'text') {
|
||||
const text = firstString(settings.text, settings.prompt, node.text, node.prompt, node.content, data.text, data.prompt);
|
||||
return {
|
||||
id,
|
||||
type: 'text',
|
||||
@@ -347,8 +394,8 @@ function convertExternalCanvasProject(value: unknown, assetMap?: ExternalAssetMa
|
||||
updatedAt: createdAt,
|
||||
};
|
||||
}
|
||||
const contentUrl = resolveExternalAssetUrl(node.content, assetMap);
|
||||
const thumbnailUrl = resolveExternalAssetUrl(node.thumbnailUrl, assetMap);
|
||||
const contentUrl = resolveExternalAssetUrl(node.content || node.imageUrl || node.url || node.src || data.imageUrl || data.url, assetMap);
|
||||
const thumbnailUrl = resolveExternalAssetUrl(node.thumbnailUrl || node.thumbnail || data.thumbnailUrl, assetMap);
|
||||
const generatedImages = Array.isArray(settings.generatedImages) ? settings.generatedImages : [];
|
||||
const outputImages = generatedImages
|
||||
.map((item) => item && typeof item === 'object' ? item as Record<string, unknown> : null)
|
||||
@@ -361,12 +408,12 @@ function convertExternalCanvasProject(value: unknown, assetMap?: ExternalAssetMa
|
||||
})
|
||||
.filter(Boolean);
|
||||
const imageUrl = outputImages[0] || contentUrl || thumbnailUrl;
|
||||
const prompt = typeof node.prompt === 'string' ? node.prompt : '';
|
||||
const prompt = firstString(node.prompt, settings.prompt, data.prompt);
|
||||
const ratio = normalizeExternalRatio(settings.ratio);
|
||||
const resolution = normalizeExternalResolution(settings.ratio || settings.previewSize || settings.previewActualSize);
|
||||
return {
|
||||
id,
|
||||
type: type === 'gen-image' ? 'image' : 'image',
|
||||
type,
|
||||
x,
|
||||
y,
|
||||
width: Math.max(320, width),
|
||||
@@ -391,17 +438,19 @@ function convertExternalCanvasProject(value: unknown, assetMap?: ExternalAssetMa
|
||||
};
|
||||
}).filter((node): node is CanvasNode => !!node);
|
||||
|
||||
const connections = (Array.isArray(input.connections) ? input.connections : []).map((rawConnection): CanvasConnection | null => {
|
||||
const connections = getExternalConnections(value).map((rawConnection): CanvasConnection | null => {
|
||||
if (!rawConnection || typeof rawConnection !== 'object') return null;
|
||||
const connection = rawConnection as Record<string, unknown>;
|
||||
const sourceNodeId = typeof connection.sourceNodeId === 'string' ? connection.sourceNodeId : typeof connection.from === 'string' ? connection.from : '';
|
||||
const targetNodeId = typeof connection.targetNodeId === 'string' ? connection.targetNodeId : typeof connection.to === 'string' ? connection.to : '';
|
||||
const source = asRecord(connection.source);
|
||||
const target = asRecord(connection.target);
|
||||
const sourceNodeId = firstString(connection.sourceNodeId, connection.from, connection.source, source.nodeId, source.cell, source.id);
|
||||
const targetNodeId = firstString(connection.targetNodeId, connection.to, connection.target, target.nodeId, target.cell, target.id);
|
||||
if (!sourceNodeId || !targetNodeId) return null;
|
||||
return {
|
||||
id: typeof connection.id === 'string' ? connection.id : createId('connection'),
|
||||
id: firstString(connection.id) || createId('connection'),
|
||||
sourceNodeId,
|
||||
targetNodeId,
|
||||
label: typeof connection.label === 'string' ? connection.label : undefined,
|
||||
label: firstString(connection.label) || undefined,
|
||||
createdAt,
|
||||
};
|
||||
}).filter((connection): connection is CanvasConnection => !!connection);
|
||||
@@ -418,9 +467,9 @@ function convertExternalCanvasProject(value: unknown, assetMap?: ExternalAssetMa
|
||||
groups: [],
|
||||
assets: imageAssets,
|
||||
viewport: {
|
||||
x: typeof input.view?.x === 'number' ? input.view.x : 0,
|
||||
y: typeof input.view?.y === 'number' ? input.view.y : 0,
|
||||
zoom: typeof input.view?.zoom === 'number' ? input.view.zoom : 1,
|
||||
x: typeof view.x === 'number' ? view.x : 0,
|
||||
y: typeof view.y === 'number' ? view.y : 0,
|
||||
zoom: typeof view.zoom === 'number' ? view.zoom : 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1034,18 +1083,17 @@ export function InfiniteCanvasWorkspace() {
|
||||
const relativePath = file.webkitRelativePath || file.name;
|
||||
if (!relativePath.includes('/assets/') && !relativePath.startsWith('assets/')) return false;
|
||||
if (file.type.startsWith('video/')) return false;
|
||||
const name = relativePath.split('/').pop() || file.name;
|
||||
const key = name.replace(/\.[^.]+$/, '');
|
||||
return requiredAssetKeys.has(key);
|
||||
return getAssetLookupKeys(relativePath).some(key => requiredAssetKeys.has(key));
|
||||
});
|
||||
const assetMap: ExternalAssetMap = new Map();
|
||||
await Promise.all(assetFiles.map(async (file) => {
|
||||
const relativePath = file.webkitRelativePath || file.name;
|
||||
const name = relativePath.split('/').pop() || file.name;
|
||||
const key = name.replace(/\.[^.]+$/, '');
|
||||
const url = await fileToDataUrl(file);
|
||||
const type = file.type.startsWith('video/') ? 'video' : 'image';
|
||||
assetMap.set(key, { url, name, type });
|
||||
getAssetLookupKeys(relativePath).forEach(key => {
|
||||
assetMap.set(key, { url, name, type });
|
||||
});
|
||||
}));
|
||||
const importedState = getImportCanvasState(parsed, assetMap);
|
||||
if (importedState.nodes.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user