diff --git a/DEVSMap_to_Cadmium_Parser/generate_coupled_model_hpp.py b/DEVSMap_to_Cadmium_Parser/generate_coupled_model_hpp.py index a152c86..40f95f8 100644 --- a/DEVSMap_to_Cadmium_Parser/generate_coupled_model_hpp.py +++ b/DEVSMap_to_Cadmium_Parser/generate_coupled_model_hpp.py @@ -86,11 +86,44 @@ def include_component_models(coupled_model): components = get_components(coupled_model) include_statements = "" for component in components: - include_statements += '#include "' + component["model"].lower() + '.hpp"\n' + model_name = str(component.get("model", "")).strip() + is_library = bool(component.get("is_library")) or model_name.startswith("lib::") + header = component.get("header") + + if is_library and header: + include_statements += f'#include <{header}>\n' + elif not is_library and not model_name.startswith("lib::"): + include_statements += '#include "' + model_name.lower() + '.hpp"\n' include_statements += '\n' return include_statements +def serialize_constructor_args(component): + ''' + Normalizes constructor arguments from the DEVSMap element into a list of C++-ready expressions. + ''' + raw_args = component.get("args", []) + + if isinstance(raw_args, str): + raw_args = [raw_args] + + if not isinstance(raw_args, list): + return [] + + serialized = [] + for arg in raw_args: + if arg is None: + continue + value = str(arg).strip() + if value == "": + continue + if value.startswith(('"', "'", "{", "[", "(", "std::", "true", "false")): + serialized.append(value) + else: + serialized.append('"' + value + '"') + return serialized + + def normalize_cpp_type(data_type): ''' Returns the appropriate C++ datatype for Cadmium port declarations. @@ -251,7 +284,12 @@ def generate_coupled_model_struct(model_name, model): for component in components: component_model_name = component["model"] model_id = component["id"] - component_statements += ('\t\tauto ' + model_id +' = addComponent<' + component_model_name +'>("' + model_id + '");\n') + extra_args = serialize_constructor_args(component) + if extra_args: + args_str = ", " + ", ".join(extra_args) + else: + args_str = "" + component_statements += ('\t\tauto ' + model_id +' = addComponent<' + component_model_name +'>("' + model_id + '"' + args_str + ');\n') constructor += component_statements + '\n' # addCoupling statements diff --git a/GUI/js/conversions.js b/GUI/js/conversions.js index 7184de7..fbb968e 100644 --- a/GUI/js/conversions.js +++ b/GUI/js/conversions.js @@ -432,10 +432,26 @@ export class ConversionManager { // convert internal component_id -> exported id if (Array.isArray(modelCopy.components)) { - modelCopy.components = modelCopy.components.map(c => ({ - model: c.model, - id: c.component_id ?? c.id - })); + modelCopy.components = modelCopy.components.map(c => { + const component = { + model: c.model, + id: c.component_id ?? c.id + }; + + if (Array.isArray(c.args) && c.args.length > 0) { + component.args = c.args; + } + + if (c.is_library) { + component.is_library = true; + } + + if (c.header) { + component.header = c.header; + } + + return component; + }); } return { diff --git a/GUI/js/main.js b/GUI/js/main.js index bfabe41..5eb4b5f 100644 --- a/GUI/js/main.js +++ b/GUI/js/main.js @@ -83,6 +83,111 @@ function getAvailableTypes() { ...window.customDataTypes.map(t => t.name) ]; } + +function createPortMarkerSvg(color = '#3b82f6', width = 8, height = 14) { + const svg = ` + + + + `; + return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg.trim()); +} + +function refreshPortMarkers(graph) { + if (!graph || !graph.getModel || !graph.addCellOverlay || !graph.removeCellOverlay) return; + + const model = graph.getModel(); + const collectCells = (node, out = []) => { + if (!node) return out; + const childCount = model.getChildCount(node); + for (let i = 0; i < childCount; i++) { + const child = model.getChildAt(node, i); + if (child) { + out.push(child); + collectCells(child, out); + } + } + return out; + }; + + const cells = collectCells(model.getRoot()); + + const addPortHoverLabel = (cell, overlay, portName, side) => { + const overlayShape = graph.getView().getState(cell)?.overlays?.get(overlay); + const overlayNode = overlayShape?.node; + if (!overlayNode) return; + + mxEvent.addListener(overlayNode, 'mouseenter', () => { + const label = document.createElement('div'); + label.textContent = portName; + label.style.position = 'fixed'; + label.style.zIndex = '1000'; + label.style.padding = '2px 6px'; + label.style.background = '#ffffff'; + label.style.border = '1px solid #9ca3af'; + label.style.borderRadius = '3px'; + label.style.color = '#111827'; + label.style.font = '12px sans-serif'; + label.style.whiteSpace = 'nowrap'; + label.style.pointerEvents = 'none'; + document.body.appendChild(label); + + const bounds = overlayNode.getBoundingClientRect(); + label.style.top = `${bounds.top + (bounds.height - label.offsetHeight) / 2}px`; + label.style.left = side === 'left' + ? `${bounds.left - label.offsetWidth - 4}px` + : `${bounds.right + 4}px`; + overlay.__devsHoverLabel = label; + }); + + mxEvent.addListener(overlayNode, 'mouseleave', () => { + overlay.__devsHoverLabel?.remove(); + overlay.__devsHoverLabel = null; + }); + }; + + cells.forEach(cell => { + if (!cell || typeof cell.isVertex !== 'function' || !cell.isVertex() || !(cell.isAtomicModel?.() || cell.isCoupledModel?.())) { + return; + } + + const overlays = cell.__devsPortOverlays || []; + overlays.forEach(overlay => { + overlay.__devsHoverLabel?.remove(); + graph.removeCellOverlay(cell, overlay); + }); + cell.__devsPortOverlays = []; + + const portModel = cell.userObject?.json?.model || {}; + const inputs = Object.entries(portModel.x || {}); + const outputs = Object.entries(portModel.y || {}); + + const inputMarkerImage = new mxImage(createPortMarkerSvg('#3b82f6', 8, 14), 8, 14); + const outputMarkerImage = new mxImage(createPortMarkerSvg('#ef4444', 8, 14), 8, 14); + const portSpacing = 18; + + inputs.forEach(([name], index) => { + const overlay = new mxCellOverlay(inputMarkerImage, ''); + overlay.align = mxConstants.ALIGN_LEFT; + overlay.verticalAlign = mxConstants.ALIGN_MIDDLE; + overlay.offset = new mxPoint(-8, (index - (inputs.length - 1) / 2) * portSpacing); + graph.addCellOverlay(cell, overlay); + cell.__devsPortOverlays.push(overlay); + addPortHoverLabel(cell, overlay, name, 'left'); + }); + + outputs.forEach(([name], index) => { + const overlay = new mxCellOverlay(outputMarkerImage, ''); + overlay.align = mxConstants.ALIGN_RIGHT; + overlay.verticalAlign = mxConstants.ALIGN_MIDDLE; + overlay.offset = new mxPoint(8, (index - (outputs.length - 1) / 2) * portSpacing); + graph.addCellOverlay(cell, overlay); + cell.__devsPortOverlays.push(overlay); + addPortHoverLabel(cell, overlay, name, 'right'); + }); + }); +} + const markDirty = () => { window.autosaveGraphNow?.(); }; @@ -138,16 +243,19 @@ function main(container) { // Create the graph inside the container const graph = new mxGraph(container); + refreshPortMarkers(graph); + graph.getModel().addListener(mxEvent.CHANGE, () => refreshPortMarkers(graph)); // These for sure graph.setPanning(true); - graph.setConnectable(true); + graph.setConnectable(false); graph.setCellsMovable(true); graph.setCellsSelectable(true); + graph.isCellSelectable = cell => !cell?.isEdge?.(); // May want to change these later graph.setGridEnabled(false); // snapping to grid - graph.setAllowDanglingEdges(true); // whether edges must be connected on both ends + graph.setAllowDanglingEdges(false); // coupling arrows are created from two ports // Graph selection via mouse dragging @@ -356,13 +464,10 @@ function main(container) { // Populate components selectedCells.forEach(child => { - const childName = child.userObject?.model_name || 'unnamed'; - const childId = child.userObject?.unique_id || child.getId(); - - group.userObject.json.model.components.push({ - model: childName, - component_id: childId //changed to component_id instead of id to allow for retainment after a refresh - }); + const component = buildComponentMetadataFromChild(child); + if (component) { + group.userObject.json.model.components.push(component); + } }); // Move the group on top of children @@ -480,13 +585,10 @@ function main(container) { // Populate components as { model, id } selectedCells.forEach(child => { - const childName = child.userObject?.model_name || 'unnamed'; - const childId = child.userObject?.unique_id || child.getId(); - - group.userObject.json.model.components.push({ - model: childName, - id: childId - }); + const component = buildComponentMetadataFromChild(child); + if (component) { + group.userObject.json.model.components.push(component); + } }); // Move the group on top of children @@ -773,6 +875,43 @@ function main(container) { } } + function parseConstructorArgs(value) { + return (value || "") + .split(",") + .map(part => part.trim()) + .filter(Boolean); + } + + function buildComponentMetadataFromChild(child) { + if (!child?.userObject) return null; + + const childObj = child.userObject; + const component = { + model: childObj.model_name || "", + component_id: childObj.unique_id || "" + }; + + const args = Array.isArray(childObj.args) + ? childObj.args.filter(Boolean) + : (typeof childObj.constructor_args === "string" && childObj.constructor_args.trim() + ? [childObj.constructor_args.trim()] + : []); + + if (args.length > 0) { + component.args = args; + } + + if (childObj.is_library) { + component.is_library = true; + } + + if (childObj.header) { + component.header = childObj.header; + } + + return component; + } + function rebuildCoupledComponentsFromChildren(coupledCell) { if (!coupledCell || !coupledCell.userObject || !coupledCell.userObject.json?.model) return; @@ -792,16 +931,8 @@ function main(container) { c.userObject && (c.userObject.elementType === "atomicModel" || c.userObject.elementType === "coupledModel") ) - .map(c => { - const childObj = c.userObject; - - console.log("Rebuilding component from child:", childObj); - - return { - model: childObj.model_name || "", - component_id: childObj.unique_id || "" - }; - }); + .map(c => buildComponentMetadataFromChild(c)) + .filter(Boolean); coupledCell.userObject.json.model.components = components; } @@ -853,10 +984,38 @@ function main(container) { modelInput.value = userObj.model_name; modelInput.classList.add("property-input"); + const argsLabel = document.createElement("label"); + argsLabel.textContent = "Constructor Args:"; + const argsInput = document.createElement("input"); + argsInput.type = "text"; + argsInput.value = Array.isArray(userObj.args) + ? userObj.args.join(", ") + : (userObj.constructor_args || ""); + argsInput.classList.add("property-input"); + + const libraryLabel = document.createElement("label"); + libraryLabel.textContent = "Library component:"; + const libraryInput = document.createElement("input"); + libraryInput.type = "checkbox"; + libraryInput.checked = Boolean(userObj.is_library); + + const headerLabel = document.createElement("label"); + headerLabel.textContent = "Header:"; + const headerInput = document.createElement("input"); + headerInput.type = "text"; + headerInput.value = userObj.header || ""; + headerInput.classList.add("property-input"); + wrapper.appendChild(idLabel); wrapper.appendChild(idInput); wrapper.appendChild(modelLabel); wrapper.appendChild(modelInput); + wrapper.appendChild(argsLabel); + wrapper.appendChild(argsInput); + wrapper.appendChild(libraryLabel); + wrapper.appendChild(libraryInput); + wrapper.appendChild(headerLabel); + wrapper.appendChild(headerInput); container.appendChild(wrapper); // Update events @@ -874,6 +1033,24 @@ function main(container) { markDirty(); }); + argsInput.addEventListener("input", () => { + userObj.args = parseConstructorArgs(argsInput.value); + syncParentCoupledComponentMetadata(cell); + markDirty(); + }); + + libraryInput.addEventListener("change", () => { + userObj.is_library = libraryInput.checked; + syncParentCoupledComponentMetadata(cell); + markDirty(); + }); + + headerInput.addEventListener("input", () => { + userObj.header = headerInput.value.trim(); + syncParentCoupledComponentMetadata(cell); + markDirty(); + }); + function updateLabel() { const id = userObj.unique_id || ""; const model = userObj.model_name || ""; @@ -937,6 +1114,7 @@ function main(container) { delete ports[name]; renderPorts(cell); renderAddCouplingUI(cell); + refreshPortMarkers(graph); markDirty(); }); @@ -1010,6 +1188,7 @@ function main(container) { portNameInput.value = ''; renderPorts(cell); renderAddCouplingUI(cell); + refreshPortMarkers(graph); markDirty(); }); @@ -1638,6 +1817,7 @@ function main(container) { removeBtn.textContent = '-'; removeBtn.style.marginLeft = '8px'; removeBtn.addEventListener('click', () => { + removeCouplingEdge(parentCell, c); couplings.splice(idx, 1); // remove coupling from the model renderCouplings(parentCell); markDirty(); @@ -1659,6 +1839,12 @@ function main(container) { renderCouplingSection(model.ic, 'internalCouplingsHeader', 'internalCouplingsContent'); } + function removeCouplingEdge(parentCell, coupling) { + const edges = graph.getModel().getChildEdges(parentCell) || []; + const edge = edges.find(candidate => candidate.__devsCoupling === coupling); + if (edge) graph.removeCells([edge]); + } + function renderAddCouplingUI(cell) { // TODO alert box when the data types don't match const addCouplingSection = document.getElementById("addCouplingSection"); @@ -1731,6 +1917,45 @@ function main(container) { compFrom.onchange = populatePorts; compTo.onchange = populatePorts; + function getPortConstraint(portCell, portName, isOutput) { + const ports = isOutput ? portCell.getOutputPorts() : portCell.getInputPorts(); + const portIndex = ports.findIndex(port => port.name === portName); + const geometry = portCell.geometry; + if (portIndex < 0 || !geometry || geometry.height === 0) return null; + + const offset = (portIndex - (ports.length - 1) / 2) * 18; + return new mxConnectionConstraint( + new mxPoint(isOutput ? 1 : 0, 0.5 + offset / geometry.height), + false, + null, + isOutput ? 8 : -8 + ); + } + + function addCouplingEdge(coupling, storedCoupling) { + const sourceCell = getCellById(coupling.componentFrom); + const targetCell = getCellById(coupling.componentTo); + if (!sourceCell || !targetCell) return; + + const sourceIsOutput = coupling.type !== "EIC"; + const targetIsOutput = coupling.type === "EOC"; + const sourceConstraint = getPortConstraint(sourceCell, coupling.portFrom, sourceIsOutput); + const targetConstraint = getPortConstraint(targetCell, coupling.portTo, targetIsOutput); + if (!sourceConstraint || !targetConstraint) return; + + const edge = graph.insertEdge( + cell, + null, + '', + sourceCell, + targetCell, + 'noEdgeStyle=1;rounded=0;endArrow=classic;' + ); + graph.setConnectionConstraint(edge, sourceCell, true, sourceConstraint); + graph.setConnectionConstraint(edge, targetCell, false, targetConstraint); + edge.__devsCoupling = storedCoupling; + } + populateAll(); document.getElementById("addCouplingBtn").onclick = () => { @@ -1743,7 +1968,8 @@ function main(container) { portTo: portTo.value.split('<')[0] }; //console.log("Adding coupling:", coupling); - addCouplingToModel(cell, coupling); // Save to model + const storedCoupling = addCouplingToModel(cell, coupling); // Save to model + addCouplingEdge(coupling, storedCoupling); renderCouplings(cell); }; } @@ -1777,38 +2003,43 @@ function main(container) { function addCouplingToModel(parentCell, coupling) { - if (!parentCell || !coupling) return; + if (!parentCell || !coupling) return null; const model = parentCell.userObject?.json?.model; - if (!model) return; + if (!model) return null; + + let storedCoupling = null; switch (coupling.type) { case 'EIC': if (!model.eic) model.eic = []; - model.eic.push({ + storedCoupling = { port_from: coupling.portFrom, port_to: coupling.portTo, component_to: coupling.componentTo - }); + }; + model.eic.push(storedCoupling); break; case 'EOC': if (!model.eoc) model.eoc = []; - model.eoc.push({ + storedCoupling = { port_from: coupling.portFrom, port_to: coupling.portTo, component_from: coupling.componentFrom, - }); + }; + model.eoc.push(storedCoupling); break; case 'IC': if (!model.ic) model.ic = []; - model.ic.push({ + storedCoupling = { port_from: coupling.portFrom, port_to: coupling.portTo, component_from: coupling.componentFrom, component_to: coupling.componentTo, - }); + }; + model.ic.push(storedCoupling); break; default: @@ -1816,6 +2047,7 @@ function main(container) { } markDirty(); + return storedCoupling; } @@ -2273,7 +2505,6 @@ function main(container) { }); - ///////////////////////////////////////////////////////////////////////////// ///////// Keyboard Shortcuts setup ///////////////////////////////////////////////////////////////////////////// diff --git a/GUI/models/test_models.js b/GUI/models/test_models.js index 90cca82..7e4bf06 100644 --- a/GUI/models/test_models.js +++ b/GUI/models/test_models.js @@ -414,6 +414,42 @@ const generalItems = [ ]; const genericItems = [ + { + label: 'iestream : lib::IEStream', + userObject: { + elementType: 'atomicModel', + model_name: 'lib::IEStream', + unique_id: 'iestream', + json: { + model: { + x: {}, + y: { + out: 'int' + }, + s: {}, + delta_int: { + "otherwise": {} + }, + delta_ext: { + "otherwise": {} + }, + delta_con: {}, + lambda: { + "otherwise": {} + }, + ta: { + "otherwise": "sigma" + } + }, + include_sets: ["default_sets.json"], + parameters: {} + }, + }, + width: 120, + height: 60, + style: DEFAULT_STYLES.atomicModel + }, + { label: 'Generic Atomic', userObject: {