| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840 |
- Draw.loadPlugin(function(ui) {
- const model = ui.editor.graph.model;
- const defaultConfig = {
- // When making an activity wider, the x-position of ports will be scaled proportionally to the new width, and vice-versa with height and y-position. This may be annoying when simply resizing an activity to add an extra port.
- movePortsOnResizeActivity: false,
- // The new algorithm probably gives better results under all circumstances.
- newPortSnappingAlgorithm: true,
- // Edge styles.
- edgeStyleCtrlFlow: "edgeStyle=orthogonalEdgeStyle;endArrow=classic;rounded=0;html=1;strokeWidth=2;fontSize=14;strokeColor=#004C99;jumpStyle=gap;",
- edgeStyleDataFlow: "edgeStyle=orthogonalEdgeStyle;endArrow=open;rounded=0;html=1;strokeWidth=1;fontSize=14;fontColor=#000000;fillColor=#d5e8d4;strokeColor=#6D9656;endFill=0;jumpStyle=gap;",
- edgeStyleTypedBy: "edgeStyle=0;endArrow=blockThin;html=1;strokeWidth=1;fontSize=14;fontColor=#000000;dashed=1;endFill=1;endSize=6;strokeColor=#999999;",
- edgeStyleParentVersion: "edgeStyle=orthogonalEdgeStyle;endArrow=classic;rounded=0;html=1;fontSize=11;fontColor=#000000;fillColor=#fff2cc;strokeColor=#d6b656;rounded=0;jumpStyle=gap;",
- edgeStyleSSLink: "edgeStyle=0;endArrow=blockThin;html=1;fontSize=11;strokeColor=#694B2E;rounded=0;dashed=1;dashPattern=1 4;endFill=1;",
- edgeStyleComment: "edgeStyle=0;endArrow=none;dashed=1;html=1;",
- portLabelOffsetEdgeDirection: 22,
- portLabelOffsetPerpendicularToEdgeDirection: 24,
- };
- let currentConfig;
- const existingConfig = window.localStorage.getItem('ftgpmConfig');
- if (existingConfig === null) { // unlike Object and Map, localStorage returns 'null' when item doesn't exist...
- currentConfig = {};
- } else {
- console.log("Have existing ftgpm config...", existingConfig)
- currentConfig = JSON.parse(existingConfig);
- }
- // Configuration dialog window:
- function createFtgpmConfigWindow() { // only called once - this is only a function in order not to pollute namespace
- // Drawio has no decent abstraction for UI elements or even displaying a dialog window.
- // The following is based on code from src/main/webapp/js/diagramly/Dialogs.js
- const wndDiv = document.createElement('div');
- wndDiv.style.userSelect = 'none';
- wndDiv.style.overflow = 'hidden';
- wndDiv.style.padding = '10px';
- wndDiv.style.height = '100%';
- const wnd = new mxWindow("FTG+PM Plugin Configuration",
- wndDiv, 100, 100, 640, 570, true, true);
- wnd.destroyOnClose = false;
- wnd.setMaximizable(false);
- wnd.setResizable(false);
- wnd.setClosable(true);
- wnd.addListener('show', mxUtils.bind(this, function() {
- statusText.innerHTML = "";
- }));
- function createConfig(labelText, json, readonly) {
- const columnDiv = document.createElement('div');
- columnDiv.style.display = 'inline-block';
- const label = document.createElement('label');
- const textarea = document.createElement('textarea');
- label.setAttribute('for', textarea);
- label.innerHTML = labelText;
- textarea.setAttribute('wrap', 'off');
- textarea.setAttribute('spellcheck', 'false');
- textarea.setAttribute('autocorrect', 'off');
- textarea.setAttribute('autocomplete', 'off');
- textarea.setAttribute('autocapitalize', 'off');
- textarea.setAttribute('wrap', 'hard');
- if (readonly) {
- textarea.setAttribute('readonly', 'true');
- }
- textarea.value = JSON.stringify(json, null, 2);
- textarea.style.overflow = 'auto';
- textarea.style.resize = 'none';
- textarea.style.width = '300px';
- textarea.style.height = '360px';
- textarea.style.marginBottom = '16px';
- columnDiv.appendChild(label);
- mxUtils.br(columnDiv);
- columnDiv.appendChild(textarea);
- return [columnDiv, textarea];
- }
- const [defaultColumn] = createConfig("Default config (hardcoded & read-only):", defaultConfig, true);
- const [customColumn, customTextArea] = createConfig("Custom config (acts as an 'overlay' on top of default config):", currentConfig, false);
- wndDiv.appendChild(defaultColumn);
- wndDiv.appendChild(customColumn);
- mxUtils.br(wndDiv);
- const buttonsDiv = document.createElement('div');
- buttonsDiv.style.textAlign = 'right';
- const statusText = document.createElement('div');
- statusText.style.display = 'inline-block';
- // attempts to save user configuration and displays status text as a side effect
- // returns true if all went well
- function saveConfigurationAndDisplayStatus() {
- let parsed;
- try {
- parsed = JSON.parse(customTextArea.value); // may throw
- } catch (parseErr) {
- statusText.innerHTML = "Parse error: " + parseErr.toString();
- statusText.style.color = 'red';
- return false;
- }
- if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) {
- statusText.innerHTML = "JSON value is not an object";
- statusText.style.color = 'red';
- return false;
- }
- currentConfig = parsed;
- window.localStorage.setItem('ftgpmConfig', JSON.stringify(parsed));
- customTextArea.value = JSON.stringify(parsed, null, 2); // prettify
- statusText.innerHTML = "Configuration successfully set and saved (in window.localStorage)";
- statusText.style.color = 'green';
- return true;
- }
- const applyButton = mxUtils.button("Apply", saveConfigurationAndDisplayStatus);
- applyButton.className = 'geBtn gePrimaryBtn';
- applyButton.style.float = 'none'; // override geBtn style
- const resetButton = mxUtils.button("Reset", function() {
- customTextArea.value = JSON.stringify(currentConfig, null, 2);
- });
- resetButton.className = 'geBtn';
- resetButton.style.float = 'none'; // override geBtn style
- buttonsDiv.appendChild(statusText);
- buttonsDiv.appendChild(applyButton);
- buttonsDiv.appendChild(resetButton);
- // hacks buttons:
- const forceEdgeStyleButton = mxUtils.button("Apply style rules to all edges", function() {
- if (saveConfigurationAndDisplayStatus()) {
- model.beginUpdate();
- for (const [cellId, cell] of Object.entries(model.cells)) {
- applyCellStyleFromConfig(cell);
- }
- model.endUpdate();
- }
- });
- const forcePortSnapButton = mxUtils.button("Re-snap ports to their activities and re-position port labels", function() {
- if (saveConfigurationAndDisplayStatus()) {
- model.beginUpdate();
- for (const [cellId, cell] of Object.entries(model.cells)) {
- if (cell.geometry) {
- // we'll force a geometry update:
- const newGeometry = cell.geometry.clone();
- snapToBorderIfCellIsPortAndParentIsActivity(cell, newGeometry);
- model.setGeometry(cell, newGeometry);
- }
- }
- model.endUpdate();
- }
- })
- forceEdgeStyleButton.className = 'geBtn';
- forceEdgeStyleButton.style.float = 'none'; // override geBtn style
- forcePortSnapButton.className = 'geBtn';
- forcePortSnapButton.style.float = 'none'; // override geBtn style
- hacksDiv = document.createElement('div');
- hacksDiv.appendChild(document.createTextNode("Hacks: (use undo if you fuck things up)"));
- mxUtils.br(hacksDiv);
- hacksDiv.appendChild(forceEdgeStyleButton);
- mxUtils.br(hacksDiv);
- hacksDiv.appendChild(forcePortSnapButton);
- wndDiv.appendChild(buttonsDiv);
- mxUtils.br(wndDiv);
- wndDiv.appendChild(hacksDiv);
- return wnd;
- }
- const ftgpmConfigWindow = createFtgpmConfigWindow();
- function getFromConfig(parameter) {
- if (currentConfig.hasOwnProperty(parameter)) {
- return currentConfig[parameter];
- } else {
- return defaultConfig[parameter];
- }
- }
- // // Force style window:
- // const forceStyleDiv = document.createElement('div');
- // const forceStyleWindow = new mxWindow("FTG+PM: Force Style Rules",
- // forceStyleDiv, 200, 200, 400, 90, true, true);
- // forceStyleWindow.destroyOnClose = false;
- // forceStyleWindow.setMaximizable(false);
- // forceStyleWindow.setResizable(false);
- // forceStyleWindow.setClosable(true);
- // const forceEdgeStyleButton = mxUtils.button("Apply style rules to all edges", function() {
- // for (const [cellId, cell] of Object.entries(model.cells)) {
- // applyCellStyleFromConfig(cell);
- // }
- // });
- // const forcePortSnapButton = mxUtils.button("Re-snap ports to their activities and re-position port labels", function() {
- // for (const [cellId, cell] of Object.entries(model.cells)) {
- // if (cell.geometry) {
- // // we'll force a geometry update:
- // const newGeometry = cell.geometry.clone();
- // snapToBorderIfCellIsPortAndParentIsActivity(cell, newGeometry);
- // model.setGeometry(cell, newGeometry);
- // }
- // }
- // })
- // forceEdgeStyleButton.className = 'geBtn';
- // forcePortSnapButton.className = 'geBtn';
- // forceStyleDiv.appendChild(forceEdgeStyleButton);
- // mxUtils.br(forceStyleDiv);
- // forceStyleDiv.appendChild(forcePortSnapButton);
- window.onkeyup = function(e) {
- // Shortcut to show FTG+PM config:
- if (e.ctrlKey && e.key === '.') {
- ftgpmConfigWindow.show();
- }
- // // Shortcut to apply style from config to all edges:
- // if (e.ctrlKey && e.key === 'Enter') {
- // forceStyleWindow.show();
- // }
- }
- // For auto-updating port orientation
- // Note: this is only the 'shape', not the full style
- const encodedPortShapes = {
- in: {
- l: `stencil(pZNdD4IgFIZ/DbcNYa3rZvU/yE7JRHBAX/++Y0c3zXC2btzO+4wHfFEm81CqBpjgJZM7JoRc4wPHO42C06hCA0Wk7Ka8VkcDREL0roK7PsXOoG0JXseWyj3jW9Y6OJN54axFiXY2jMiAo0xpi2v5g2Td9s9uWnWna3CHGiJ4yjNKmTgsFmf/iGWOSep1ZH5URXXx7mpP01VJdnYevoAeN6rtdwJ6XLsbJFuba6cXGG0Hgg9Dtun7+UUydshFhsK4ADNXiXmiCWxQG0NfY+rKJh2/U/oD3sEL)`,
- r: `stencil(pZNdD4IgFIZ/DbcNYa3rZvU/yE7JQnBAVv++o0eXH9ls3bid9xkP8qJMpiFXJTDBcyZ3TAi5xgeOdxoFp1GFErJIWaW8VkcDREL07gp3fYqtQdscvI41lXvGt6x2cCbTzFmLEu1sGJAeR5nSFtfyB8na7Z/ttGrfrsQdCojgKU8oZeKwWJz8I5YpJnPHkelRZdeLdzd7mq6aZWfn4QPocKnqfiegw4Wr4H24cW3f6ukMRtueYShINl0/vzhGErlIkRkX4MtdYj5TBVaojaHPce7OJiU3Kf0CTfAC)`,
- t: `stencil(nZPRDoIgGIWfhtuGsNZ1s3oP0r9kIjggrbfv1183S23VDds5Z3zAAZhMQ6FqYIIXTB6YEILjgLIlKbckVaghi+Q1ymt1NkBJiN6V0Oo8DgRtC/A6dqk8Mr5nHZIzmWbOWoRoZ8NLMskRprTFufxOML4ZNvAYNKkaV6gggic3IZeJ09/g5BewTNFZO45Mzyorr97dbD6ftZpdnIeFYIxr1fU7C8a4cg1MDrfU2XI7I8BoOwHIxdZ/ISS7t4K/g2TGBfhwleivNIENamPoNa5d2azj3qUf0BtP)`,
- b: `stencil(nZNRD4IgFIV/Da8NYa3nZvU/EG/JQnBAWv++q1c3m9pWL27nnPHJPQCTeaxUA0zwiskTE0Jw/KDsSMo9SRUb0Im8VgWjCguUxBT8HTpTppFgXAXBpD6VZ8aPrEdyJnPtnUOI8S5+JLMcYco4XMufBOO7cQOvUZNq8A81JAjkZuQycfkbnP0Cljk6W+PIvFD6fgv+4crlqs3s6gOsBFPcqL7fRTDFtW9hNtzHaJNcr2ciWONmhOywXvwvDLn/Yxva+ghfzhL9jSqwQmMtXcetM1uUPLj0BAbjDQ==)`,
- }
- };
- encodedPortShapes.out = {
- r: encodedPortShapes.in.l,
- l: encodedPortShapes.in.r,
- t: encodedPortShapes.in.b,
- b: encodedPortShapes.in.t,
- }
- // Styles for our different edge types
- // Mapping from 'pmRole' attribute to key in config.
- const edgeStyles = {
- ctrl_flow: 'edgeStyleCtrlFlow',
- data_flow: 'edgeStyleDataFlow',
- typed_by: 'edgeStyleTypedBy',
- parent_version: 'edgeStyleParentVersion',
- comment_edge: 'edgeStyleComment',
- };
- function applyCellStyleFromConfig(cell) {
- const ftgpmType = cell.getAttribute(TYPE_ATTR);
- if (ftgpmType) {
- if (edgeStyles.hasOwnProperty(ftgpmType)) {
- setEdgeStyle(cell, ftgpmType);
- }
- // TODO: do the same on nodes.
- }
- }
- const TYPE_ATTR = "pmRole"; // cell attribute holding the ftgpm type
- function parseStyle(str) {
- const pairs = str.split(';').filter(s => s !== '');
- const map = new Map();
- pairs.map(pair => {
- const [key,value] = pair.split('=');
- map.set(key, value);
- });
- return map;
- }
- function unparseStyle(map) {
- let str = "";
- for (const [key, value] of map.entries()) {
- if (value === undefined) {
- str += key + ';';
- } else {
- str += key + '=' + value + ';';
- }
- }
- return str;
- }
- // copy selected keys (=array) from fromMap to toMap
- function mapAssign(toMap, fromMap, keys) {
- for (const key of keys) {
- const value = fromMap.get(key);
- if (value !== undefined) {
- toMap.set(key, value);
- }
- }
- }
- function reverseEdge(edge, sourceCell, targetCell) {
- // Reverse source and target
- ui.editor.graph.model.setTerminals(edge, targetCell, sourceCell);
- // Reverse 'waypoints'
- if (edge.geometry.points) {
- const newGeometry = edge.geometry.clone();
- newGeometry.points.reverse();
- ui.editor.graph.model.setGeometry(edge, newGeometry);
- }
- // Reverse entry and exit points
- const oldStyle = parseStyle(model.getStyle(edge));
- const newStyle = new Map(oldStyle);
- // entry becomes exit
- newStyle.set('entryX', oldStyle.get('exitX'));
- newStyle.set('entryY', oldStyle.get('exitY'));
- // exit becomes entry
- newStyle.set('exitX', oldStyle.get('entryX'));
- newStyle.set('exitY', oldStyle.get('entryY'));
- // entryD becomes exitD
- newStyle.set('entryDx', oldStyle.get('exitDx'));
- newStyle.set('entryDy', oldStyle.get('exitDy'));
- // exitD becomes entryD
- newStyle.set('exitDx', oldStyle.get('entryDx'));
- newStyle.set('exitDy', oldStyle.get('entryDy'));
- model.setStyle(edge, unparseStyle(newStyle));
- }
- // Simply sets the data attribute TYPE_ATTR to ftgpm_type.
- function setFtgpmType(cell, ftgpm_type) {
- // Set type attribute
- let value = model.getValue(cell);
- if (!value) {
- // Workaround: 'value' must be an XML element
- value = mxUtils.createXmlDocument().createElement('object');
- value.setAttribute('label', '');
- }
- value.setAttribute(TYPE_ATTR, ftgpm_type);
- model.setValue(cell, value);
- }
- function setEdgeStyle(edge, style_type) {
- // Update style
-
- // Workaround: don't overwrite connection points
- const oldstyle = parseStyle(model.getStyle(edge));
- const newstyle = new Map(parseStyle(getFromConfig(edgeStyles[style_type])));
- mapAssign(newstyle, oldstyle, [
- // retain these properties from oldstyle:
- "entryX", "entryY", "entryDx", "entryDy",
- "exitX", "exitY", "exitDx", "exitDy"
- ]);
- model.setStyle(edge, unparseStyle(newstyle));
- }
- function isArtifact(type) {
- return type === "artifact";
- }
- function isFormalism(type) {
- return type === "formalism";
- }
- function isTransformation(type) {
- return type === "auto_transformation" || type === "transformation" || type === "comp_transformation";
- }
- function isActivityNode(type) {
- return type === "autom_activity" || type === "activity" || type === "comp_activity";
- }
- function isControlFlowPort(type) {
- return type === "ctrl_in" || type === "ctrl_out";
- }
- function isDataPort(type) {
- return type === "data_in" || type === "data_out";
- }
- function isInport(type) {
- return type === "data_in" || type === "ctrl_in";
- }
- function isOutport(type) {
- return type === "data_out" || type === "ctrl_out";
- }
- function isPort(type) {
- return ["data_in", "data_out", "ctrl_in", "ctrl_out"].includes(type);
- }
- function isControlFlowNode(type) {
- return isControlFlowPort(type) || type === "initial" || type === "final" || type === "fork_join";
- }
- function isDataFlowNode(type) {
- return isDataPort(type) || isArtifact(type) || isFormalism(type);
- }
- function isTraceEvent(type) {
- return type === "traceevent_begin" || type === "traceevent_end";
- }
- function isArtifactVersion(type) {
- return type === "artifact_version";
- }
- function getFlowType(type) {
- if (isDataFlowNode(type)) {
- return "data";
- }
- if (isControlFlowNode(type)) {
- return "ctrl";
- }
- // throw new Error("unknown flow type");
- console.log("unknown flow type");
- }
- // [ condition, flow-type ]
- const edgeTypeRules = [
- // PM control flow
- [ (src, tgt) => isControlFlowNode(src) && isControlFlowNode(tgt), "ctrl_flow" ],
- // PM data flow
- [ (src, tgt) => isDataFlowNode(src) && isDataFlowNode(tgt), "data_flow" ],
- // FTG data flow
- [ (src, tgt) => isTransformation(src) && isFormalism(tgt), "data_flow" ],
- [ (src, tgt) => isFormalism(src) && isTransformation(tgt), "data_flow" ],
- [ (src,tgt) => isActivityNode(src) && isTransformation(tgt), "typed_by" ],
- [ (src,tgt) => isArtifact(src) && isFormalism(tgt), "typed_by" ],
- [ (src,tgt) => isTraceEvent(src) && isArtifactVersion(tgt), "data_flow"],
- [ (src,tgt) => isArtifactVersion(src) && isTraceEvent(tgt), "data_flow"],
- [ (src,tgt) => isTraceEvent(src) && isTraceEvent(tgt), "ctrl_flow"],
- [ (src,tgt) => src === "traceevent_begin" && tgt === "ctrl_in", "typed_by"],
- [ (src,tgt) => src === "traceevent_end" && tgt === "ctrl_out", "typed_by"],
- [ (src,tgt) => isArtifactVersion(src) && isArtifact(tgt), "typed_by"],
- [ (src,tgt) => isArtifactVersion(src) && isArtifactVersion(tgt), "parent_version"],
- [ (src,tgt) => isArtifactVersion(src) && tgt === "storage", "ss_link"],
- [ (src,tgt) => isArtifactVersion(src) && tgt === "real_object", "ss_link"],
- [ (src,tgt) => src === "traceevent_begin" && tgt === "service", "ss_link"],
- [ (src,tgt) => src === "comment" || tgt === "comment", "comment_edge"],
- ];
- // Very specific: This function is used to determine the correct direction of an arrow automatically
- // An activity or artifact at the highest level (= a direct child of a layer) has level 0
- // Every time something is nested in an activity, the level increases by 1.
- // Ports are special: they can only be a child of an activity, but they have the same level as the activity.
- function getNestedLevel(cell) {
- function getLvlRecursive(cell) {
- if (!cell.parent) {
- throw Error("getNestedLevel called with parentless cell (i.e. cell is root, or is deleted)");
- }
- if (model.isLayer(cell)) {
- return 0;
- // throw Error("getNestedLevel called with layer cell");
- }
- if (cell.parent === model.isLayer(cell.parent)) {
- return 0;
- }
- else if (isActivityNode(cell.parent.getAttribute(TYPE_ATTR))) {
- return getLvlRecursive(cell.parent) + 1;
- }
- else {
- return getLvlRecursive(cell.parent);
- }
- }
- const baseLevel = getLvlRecursive(cell);
- if (isPort(cell.getAttribute(TYPE_ATTR))) {
- return baseLevel - 1;
- } else {
- return baseLevel;
- }
- }
- function isDirectionless(type) {
- return isArtifact(type) || type === "fork_join";
- }
- ui.editor.graph.addListener(mxEvent.CELL_CONNECTED, (_, eventObj) => {
- // Happens whenever an edge is (dis)connected.
- const edge = eventObj.properties.edge;
- const sourceCell = edge.source;
- const targetCell = edge.getTerminal();
- // This will change the edge style WITHIN the transaction of the edit operation.
- // The terminal-change and style-change will be one edit operation from point of view of undo manager.
- const sourceType = sourceCell ? sourceCell.getAttribute(TYPE_ATTR) : null;
- const targetType = targetCell ? targetCell.getAttribute(TYPE_ATTR) : null;
- // Update style if necessary
- for (const [cond, linkType] of edgeTypeRules) {
- if (cond(sourceType, targetType)) {
- setFtgpmType(edge, linkType);
- setEdgeStyle(edge, linkType);
- }
- }
- if (sourceCell && targetCell) {
- // Auto-correct edge direction in certain cases.
- // We call 'reverseEdge' if we have an illegal situation, but reversing the arrow gives a legal situation.
- const sourceLvl = getNestedLevel(sourceCell);
- const targetLvl = getNestedLevel(targetCell);
- // We have 2 flow types: ctrl and data. They remain segregated.
- if (getFlowType(sourceType) === getFlowType(targetType)) {
- if (sourceLvl === targetLvl) {
- // Equally nested ...
- if (isInport(sourceType) && isOutport(targetType)
- || isInport(sourceType) && isDirectionless(targetType)
- || isDirectionless(sourceType) && isOutport(targetType)) {
- reverseEdge(edge, sourceCell, targetCell);
- }
- }
- else if (sourceLvl === targetLvl + 1) {
- // Source of arrow is nested deeper ...
- if (isInport(sourceType) && isInport(targetType)
- || isDirectionless(sourceType) && isInport(targetType)) {
- // Should flow from shallowly nested input port to deeply nested input port:
- reverseEdge(edge, sourceCell, targetCell);
- }
- }
- else if (sourceLvl + 1 === targetLvl) {
- // Target of arrow is nested deeper ...
- if (isOutport(sourceType) && isOutport(targetType)
- || isOutport(sourceType) && isDirectionless(targetType)) {
- // Should flow from deeply nested output port to shallowly nested output port
- reverseEdge(edge, sourceCell, targetCell);
- }
- }
- }
- }
- });
- function outsideOffset(wOrH) {
- return 0;
- }
- function centerOffset(wOrH) {
- return -wOrH / 2;
- }
- function insideOffset(wOrH) {
- return -wOrH;
- }
- // Change this to position ports outside, inside or centered at the edge.
- const borderOffset = centerOffset;
- function rightOrBottomBorder(wOrH, parentWorH) {
- return parentWorH + borderOffset(wOrH);
- }
- function leftOrTopBorder(wOrH) {
- return -wOrH - borderOffset(wOrH);
- }
- function cellCenter(cellGeometry) {
- // Coordinates are relative to topleft corner of parent shape
- const {x,y, width, height} = cellGeometry;
- // Center of cell
- return {
- x: x + width/2,
- y: y + height/2,
- };
- }
- // Deprecated snapping algorithm
- function closestBorder(cellGeometry, parentGeometry) {
- // Cell center is relative to parent: (0,0) is topleft
- const c = cellCenter(cellGeometry);
- // We draw two imaginary diagonals through the parent shape, to determine whether the child shape is more close to the top, right, bottom or left side.
- //
- // 2
- // /
- // +----+
- // |\ t/|
- // |l\/ |
- // | /\r|
- // |/b \|
- // +----+
- // \
- // 1
- const slope = parentGeometry.height / parentGeometry.width;
- const above1 = c.y < slope * c.x;
- const above2 = c.y < parentGeometry.height - slope * c.x;
- if (above1)
- if (above2)
- return 't';
- else
- return 'r';
- else
- if (above2)
- return 'l';
- else
- return 'b';
- }
- // Improved snapping algorithm
- function closestBorder2(cellGeometry, parentGeometry) {
- // Cell center is relative to parent: (0,0) is topleft
- const c = cellCenter(cellGeometry);
- const slope = 1; // fixed slope, 45 degrees
- const above1 = c.y < slope * c.x;
- const above3 = c.y < (-slope) * c.x + parentGeometry.height ;
- const above4 = c.y < slope * (c.x - parentGeometry.width) + parentGeometry.height;
- const above2 = c.y < (-slope) * (c.x - parentGeometry.width);
- if (parentGeometry.width >= parentGeometry.height) {
- // Parent's width > height
- //
- // 1 2
- // \ /
- // +------+ --> x
- // |\ t /|
- // |l\__/ |__ 5
- // | / \r|
- // |/ b \|
- // +------+
- // / \
- // 3 4
- // |
- // V
- // y
- const above5 = c.y < parentGeometry.height / 2;
- if (!above1 && above3) {
- return 'l';
- }
- if (!above2 && above4) {
- return 'r';
- }
- if (above1 && above2 && above5) {
- return 't';
- }
- if (!above3 && !above4 && !above5) {
- return 'b';
- }
- }
- else {
- // Parent's height > width
- // 1 2
- // \ /
- // +----+
- // |\ t/|
- // | \/ |
- // |l | |
- // | | |
- // | /\r|
- // |/b \|
- // +----+
- // / | \
- // 3 5 4
- const leftOf5 = c.x < parentGeometry.width / 2;
- if (above1 && above2) {
- return 't';
- }
- if (!above3 && !above4) {
- return 'b';
- }
- if (above3 && !above1 && leftOf5) {
- return 'l';
- }
- if (above4 && !above2 && !leftOf5) {
- return 'r';
- }
- }
- }
- // Update port shape (so it points in the right direction), move port (and port label) to border of activity.
- function snapPortToBorder(cell, cellGeometry, parentGeometry, border) {
- const style = parseStyle(model.getStyle(cell));
- const type = cell.getAttribute(TYPE_ATTR);
- const inOrOut = isInport(type) ? "in" : "out";
- style.set('shape', encodedPortShapes[inOrOut][border]);
- switch (border) {
- case 't':
- cellGeometry.width = 35;
- cellGeometry.height = 20;
- cellGeometry.y = leftOrTopBorder(cellGeometry.height);
- style.set('align', 'right');
- style.set('verticalAlign', 'bottom');
- cellGeometry.offset = new mxPoint(
- -getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'),
- -getFromConfig('portLabelOffsetEdgeDirection'));
- break;
- case 'r':
- cellGeometry.width = 20;
- cellGeometry.height = 35;
- cellGeometry.x = rightOrBottomBorder(cellGeometry.width, parentGeometry.width);
- style.set('align', 'left');
- style.set('verticalAlign', 'bottom');
- cellGeometry.offset = new mxPoint(
- getFromConfig('portLabelOffsetEdgeDirection'),
- -getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'));
- break;
- case 'l':
- cellGeometry.width = 20;
- cellGeometry.height = 35;
- cellGeometry.x = leftOrTopBorder(cellGeometry.width);
- style.set('align', 'right');
- style.set('verticalAlign', 'top');
- cellGeometry.offset = new mxPoint(
- -getFromConfig('portLabelOffsetEdgeDirection'),
- getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'));
- break;
- case 'b':
- cellGeometry.width = 35;
- cellGeometry.height = 20;
- cellGeometry.y = rightOrBottomBorder(cellGeometry.height, parentGeometry.height);
- style.set('align', 'left');
- style.set('verticalAlign', 'top');
- cellGeometry.offset = new mxPoint(
- getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'),
- getFromConfig('portLabelOffsetEdgeDirection'));
- break;
- }
- model.setStyle(cell, unparseStyle(style));
- }
- function snapToBorderIfCellIsPortAndParentIsActivity(cell, geometry) {
- const type = cell.getAttribute(TYPE_ATTR);
- if (isPort(type)) {
- // Port was moved
- if (cell.parent) {
- const parentType = cell.parent.getAttribute(TYPE_ATTR);
- if (isActivityNode(parentType) || isTransformation(parentType)) {
- // Snap port to activity border
- const portSnappingAlgorithm = getFromConfig('newPortSnappingAlgorithm') ? closestBorder2 : closestBorder;
- // border will be one of: 'l', 'r', 't', 'b'
- const border = portSnappingAlgorithm(geometry, cell.parent.geometry);
- snapPortToBorder(cell, geometry, cell.parent.geometry, border);
- }
- }
- }
- }
- ui.editor.graph.addListener(mxEvent.MOVE_CELLS, (_, eventObj) => {
- // High-level event: Happens when the user releases dragged shape(s)
- for (const cell of eventObj.properties.cells) {
- snapToBorderIfCellIsPortAndParentIsActivity(cell, cell.geometry);
- }
- });
- ui.editor.graph.addListener(mxEvent.RESIZE_CELLS, (_, eventObj) => {
- // High-level event: Happens when the user resized cell(s)
- for (let i=0; i<eventObj.properties.cells.length; i++) {
- const cell = eventObj.properties.cells[i];
- const type = cell.getAttribute(TYPE_ATTR);
- if (isActivityNode(type) || isTransformation(type)) {
- // Activity was resized...
- // In drawio, 'arcSize' of a rounded rectangle is relative to size of shape.
- // Retain apparent arc size of activity:
- const style = parseStyle(model.getStyle(cell));
- const newArcSize = 1000 / Math.sqrt(cell.geometry.width * cell.geometry.height);
- style.set("arcSize", newArcSize);
- model.setStyle(cell, unparseStyle(style));
- if (cell.children) {
- // Need this for moving contained ports:
- const prevGeometry = eventObj.properties.previous[i];
- const scaleW = cell.geometry.width / prevGeometry.width;
- const scaleH = cell.geometry.height / prevGeometry.height;
- for (const child of cell.children) {
- const childType = child.getAttribute(TYPE_ATTR);
- // Keep activity icon in place
- if (childType === "activityIcon") {
- newIconGeometry = child.geometry.clone();
- newIconGeometry.x = cell.geometry.width - 28;
- newIconGeometry.y = 4;
- model.setGeometry(child, newIconGeometry);
- }
- else if (isPort(childType)) {
- // Move contained ports
- const border = closestBorder2(child.geometry, prevGeometry); // < what was the border in the old geometry?
- const newGeometry = child.geometry.clone();
- snapPortToBorder(child, newGeometry, cell.geometry, border);
- if (getFromConfig('movePortsOnResizeActivity')) {
- // Scale position
- switch (border) {
- case 't':
- newGeometry.x = child.geometry.x * scaleW;
- break;
- case 'l':
- newGeometry.y = child.geometry.y * scaleH;
- break;
- case 'b':
- newGeometry.x = child.geometry.x * scaleW;
- break;
- case 'r':
- newGeometry.y = child.geometry.y * scaleH;
- break;
- }
- }
- model.setGeometry(child, newGeometry);
- }
- }
- }
- }
- }
- });
- // Hardcoded primitive shape libraries.
- // To alter, make changes to the library in the drawio webapp, then 'save' (this downloads the library as XML), and overwrite the file(s) in drawiolibs.
- Promise.all([
- fetch("myPlugins/shape_libs/common.xml"),
- fetch("myPlugins/shape_libs/ftg.xml"),
- fetch("myPlugins/shape_libs/pm.xml"),
- fetch("myPlugins/shape_libs/pt.xml"),
- fetch("myPlugins/shape_libs/ss.xml"),
- ])
- .then(all => Promise.all(all.map(response => response.text())))
- .then(([common, ftg, pm, pt, ss]) => {
- ui.loadLibrary(new LocalLibrary(ui, ss, "FTG+PM - S/S"));
- ui.loadLibrary(new LocalLibrary(ui, pt, "FTG+PM - PT"));
- ui.loadLibrary(new LocalLibrary(ui, pm, "FTG+PM - PM"));
- ui.loadLibrary(new LocalLibrary(ui, ftg, "FTG+PM - FTG"));
- ui.loadLibrary(new LocalLibrary(ui, common, "FTG+PM - Common"));
- });
- // For debugging only
- window.ui = ui;
- });
|