UNPKG

@selenite/graph-editor

Version:

A graph editor for visual programming, based on rete and svelte.

380 lines (379 loc) 16.1 kB
import { Connection, Node } from '../../nodes/Node.svelte'; import { XmlNode } from '../../nodes/XML/XmlNode.svelte'; import { ErrorWNotif } from '../../../global'; import { animationFrame, getElementFromParsedXml, getXmlAttributes, newLocalId, parseXml, XmlSchema } from '@selenite/commons'; import { FormatGroupNameNode } from '../../nodes/io/FormatNode'; import { BreakArrayNode } from '../../nodes/data/array'; import { lowerFirst } from 'lodash-es'; export function xmlToGraph(params) { const parsedXml = parseXml(params.xml); console.debug('parsed xml', parsedXml); const res = parsedXmlToGraph({ ...params, xml: parsedXml }); return res; } /** * Recursively converts the parsed xml to editor nodes */ export function parsedXmlToGraph({ xml, schema, factory }) { const nodes = []; const connections = []; const rootTypes = schema.roots; const nameToXmlNode = new Map(); const groupNameLinks = []; const cellBlockMap = new Map(); function rec({ xml, parent }) { for (const xmlNode of xml) { const xmlTag = getElementFromParsedXml(xmlNode); if (xmlTag === null) { continue; } if (xmlTag.startsWith('?')) continue; const complex = schema.complexTypes.get(xmlTag ?? ''); if (!complex) { console.warn('Complex type not found', xmlTag); continue; } if (!xmlTag) throw new ErrorWNotif('Missing xml tag in parsed xml node'); const hasNameAttribute = complex.attributes.has('name'); const xmlAttributes = getXmlAttributes(xmlNode); const arrayAttrs = new Map(); for (const [k, v] of Object.entries(xmlAttributes)) { const attrDef = complex.attributes.get(k); if (!attrDef) { console.warn('Attribute not found :', `${xmlTag}.${k}`); continue; } if (attrDef.type === 'R1Tensor') { const a = v .slice(1, -1) .split(',') .map((t) => t.trim()); // console.log('R1Tensor', a, { x: a[0], y: a[1], z: a[2] }); xmlAttributes[k] = { x: a[0], y: a[1], z: a[2] }; continue; } if (attrDef.type.endsWith('Tensor')) { continue; } const isArray = /^\s*?\{\s*?/.test(v); if (!isArray) continue; const candidate = JSON.parse(v .replaceAll('{', '[') .replaceAll('}', ']') .replaceAll(/[a-zA-Z0-9.\-_/]+/g, (t) => { // if (t === '') return ''; // if (t === ',') return ','; return `"${t}"`; })); if (candidate === undefined) continue; if (attrDef.type.startsWith('real') && attrDef.type.endsWith('array2d')) { // console.log('candidate', candidate); xmlAttributes[k] = candidate.map((t) => { if (typeof t === 'string') throw new ErrorWNotif('Expected array for type real array2d'); return { x: t[0], y: t[1], z: t[2] }; }); arrayAttrs.set(attrDef.name, xmlAttributes[k]); continue; } // put nested arrays back to xml notation // maybe later all kinds of nested arrays will be supported // and this will become obsolete function arraysToXmlNotation(a) { if (typeof a === 'string') return a; // removes " " return ('{ ' + a .map((t) => { if (Array.isArray(t)) return arraysToXmlNotation(t); return t; // removes " " }) .join(', ') + ' }'); } const array1d = candidate.map(arraysToXmlNotation); xmlAttributes[k] = array1d; arrayAttrs.set(attrDef.name, array1d); } const node = new XmlNode({ factory, initialValues: xmlAttributes, schema, xmlConfig: { complex } }); for (const k of Object.keys(xmlAttributes)) { if (node.optionalXmlAttributes.has(k)) node.addOptionalAttribute(k); } // Register all cellblocks defined by the internal mesh if (complex.name === 'InternalMesh') { const cellblockNames = xmlAttributes['cellBlockNames']; if (cellblockNames) { if (Array.isArray(cellblockNames) && cellblockNames.length > 0 && typeof cellblockNames[0] === 'string') { for (const cb of cellblockNames) { cellBlockMap.set(cb, node); } } else { console.error('Wrong types for cellblocks names of internal mesh.', cellblockNames); } } } nodes.push(node); // Automatically select output of root types like Solvers, Mesh... if (rootTypes.includes(complex.name.trim())) { node.selectOutput('value'); } if (hasNameAttribute) { const name = xmlAttributes['name']; if (!nameToXmlNode.has(name)) { nameToXmlNode.set(name, node); } else { const previousNode = nameToXmlNode.get(name); console.warn('Duplicate name:', name, { previous: previousNode.label, new: node.label }); factory?.notifications.warn({ message: `Duplicate name in XML : '${name}'.\nThe first one that is not an event will be kept.`, autoClose: 5000 }); if (previousNode.label.includes('Event')) { console.warn('Previous node is an event node, replacing it with the new one'); nameToXmlNode.set(name, node); } else if (node.label.includes('Event')) { console.warn('New node is an event node, skipping it'); } else { console.warn('Both nodes are not event nodes, skipping the new one'); } } } for (const [k, a] of arrayAttrs.entries()) { const attrDef = complex.attributes.get(k); if (!attrDef) throw new ErrorWNotif("Couldn't find simple type for array attribute"); const initialValues = {}; for (const [i, t] of a.entries()) { initialValues[`data-${i}`] = t; } // const makeArrayNode = new MakeArrayNode({ // factory, // initialValues, // numPins: a.length // }); // nodes.push(makeArrayNode as Node); // Gather array group name links if (attrDef.type === 'groupNameRef_array') { for (const [inputKey, t] of Object.entries(initialValues)) { if (typeof t !== 'string') { console.error('Value of type groupNameRef should be of string type, type :', typeof t); continue; } groupNameLinks.push({ source: t, target: { node: node, key: k } }); } } // connections.push(new Connection(makeArrayNode, 'array', node, k) as Connection); } // Gather group name links for (const [k, v] of Object.entries(xmlAttributes)) { const attrDef = complex.attributes.get(k); if (!attrDef) { console.warn('Attribute type not found', k, v); continue; } if (attrDef.type.startsWith('groupNameRef')) { const isArray = attrDef.type.endsWith('array'); if (!isArray) { if (typeof v !== 'string') { console.error('Value of type groupNameRef should be of string type, type :', typeof v); continue; } groupNameLinks.push({ source: v, target: { node, key: k } }); } } } if (parent) { const target = parent.childrenSockets.get(complex.name); if (target) { connections.push(new Connection(node, 'value', parent, target)); } else { console.warn('Parent has no socket for child', { parent: $state.snapshot(parent.inputs), node, type: complex.name }); } } rec({ xml: xmlNode[xmlTag], parent: node }); } } rec({ xml }); const objectPathFormatMap = new Map(); const cellBlockBreakNodes = new Map(); function getCellBlockBreakNode(sourceNode) { if (!cellBlockBreakNodes.has(sourceNode)) { const breakNode = new BreakArrayNode({ type: 'groupNameRef', name: 'cellBlocks', factory, initialValues: { array: [...(sourceNode.getData('cellBlockNames') ?? [])] } }); nodes.push(breakNode); cellBlockBreakNodes.set(sourceNode, breakNode); connections.push(new Connection(sourceNode, 'cellBlockNames', breakNode, 'array')); } return cellBlockBreakNodes.get(sourceNode); } // Process group name links for (const { source, target } of groupNameLinks) { if (cellBlockMap.has(source)) { const sourceNode = cellBlockMap.get(source); if (sourceNode === target.node) continue; const breakNode = getCellBlockBreakNode(sourceNode); connections.push(new Connection(breakNode, source, target.node, target.key)); continue; } // Object Paths links if (target.key === 'objectPath') { if (objectPathFormatMap.has(source)) { const sourceFormat = objectPathFormatMap.get(source); connections.push(new Connection(sourceFormat, 'result', target.node, target.key)); continue; } let formatString = []; let numVars = 0; let numCellblocks = 0; const vars = []; const nodeVarCount = new Map(); for (const name of source.split('/')) { const node = nameToXmlNode.get(name); if (cellBlockMap.has(name)) { const key = numCellblocks === 0 ? 'cellBlock' : `cellBlock-${numCellblocks}`; const breakNode = getCellBlockBreakNode(cellBlockMap.get(name)); formatString.push(`{${key}}`); vars.push({ node: breakNode, outKey: name, inKey: key }); numCellblocks++; continue; } if (node) { let key = lowerFirst(node.outLabel); if (nodeVarCount.has(node.outLabel)) { key += `-${nodeVarCount.get(name)}`; } formatString.push(`{${key}}`); vars.push({ node: node, outKey: 'name', inKey: key }); numVars++; nodeVarCount.set(node.outLabel, (nodeVarCount.get(node.outLabel) ?? 0) + 1); continue; } formatString.push(name); } if (vars.length === 0) continue; const formatNode = new FormatGroupNameNode({ name: 'objectPath', factory, initialValues: { format: formatString.join('/') } }); // console.log(Object.keys(formatNode.inputs)); nodes.push(formatNode); objectPathFormatMap.set(source, formatNode); for (const [i, { node, outKey, inKey }] of vars.entries()) { connections.push(new Connection(node, outKey, formatNode, `data-${inKey}`)); } connections.push(new Connection(formatNode, 'result', target.node, target.key)); continue; } const candidates = source .split('/') .map((s) => nameToXmlNode.get(s)) .filter(Boolean); if (candidates.length === 0) { console.warn('Source node not found', source); continue; } const sourceNode = candidates[0]; connections.push(new Connection(sourceNode, target.key === 'target' || target.key === 'sources' ? 'value' : 'name', target.node, target.key)); } return { nodes, connections }; } export async function addGraphToEditor({ factory, nodes, connections, t0 }) { if (nodes.length === 0 && connections.length === 0) return; let addedNodes = $state([]); let addedConns = $state([]); const notifId = newLocalId('code-integration-progress'); factory.notifications.show({ id: notifId, title: 'Code Integration', autoClose: false, get message() { return `Progress: ${(((addedNodes.length + addedConns.length) / (nodes.length + connections.length)) * 100).toFixed(2)}%`; } }); for (const [i, node] of nodes.entries()) { let n; if (node instanceof Node) { n = node; n.factory = factory; } else { n = await Node.fromJSON(node, { factory }); } n.visible = false; if (n) { await factory.editor.addNode(n); } addedNodes.push(n); if (i % 10 === 9) await animationFrame(); } for (const [i, conn] of connections.entries()) { const addedConn = await factory.editor.addNewConnection(conn.source, conn.sourceOutput, conn.target, conn.targetInput); if (addedConn) { addedConns.push(addedConn); } else { console.warn('Failed to add connection', conn); } if (i % 10 === 9) await animationFrame(); } await animationFrame(2); await factory.arrange?.layout(); for (const node of addedNodes) { node.visible = true; } factory.focusNodes(addedNodes); setTimeout(() => { factory.notifications.hide(notifId); }, 1000); if (true || import.meta.env.MODE === 'development') factory.notifications.info({ title: 'Code Integration', message: 'It took ' + ((performance.now() - t0) / 1000).toFixed(2) + ' s to parse the xml!' }); }