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: {