mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-26 02:39:41 +02:00
feat(arch): XY edges now have a 90deg bend
This commit is contained in:
@@ -120,7 +120,6 @@ const getGroups = (): ArchitectureGroup[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getDataStructures = () => {
|
const getDataStructures = () => {
|
||||||
console.log('===== createSpatialMap =====');
|
|
||||||
if (datastructures === undefined) {
|
if (datastructures === undefined) {
|
||||||
// Create an adjacency list of the diagram to perform BFS on
|
// Create an adjacency list of the diagram to perform BFS on
|
||||||
// Outer reduce applied on all services
|
// Outer reduce applied on all services
|
||||||
@@ -169,7 +168,6 @@ const getDataStructures = () => {
|
|||||||
const [posX, posY] = spatialMap[id];
|
const [posX, posY] = spatialMap[id];
|
||||||
Object.entries(adj).forEach(([dir, rhsId]) => {
|
Object.entries(adj).forEach(([dir, rhsId]) => {
|
||||||
if (!visited[rhsId]) {
|
if (!visited[rhsId]) {
|
||||||
console.log(`${id} -- ${rhsId}`);
|
|
||||||
spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair(
|
spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair(
|
||||||
[posX, posY],
|
[posX, posY],
|
||||||
dir as ArchitectureDirectionPair
|
dir as ArchitectureDirectionPair
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import cytoscape from 'cytoscape';
|
import cytoscape, { Position } from 'cytoscape';
|
||||||
import type { Diagram } from '../../Diagram.js';
|
import type { Diagram } from '../../Diagram.js';
|
||||||
import fcose, { FcoseLayoutOptions } from 'cytoscape-fcose';
|
import fcose, { FcoseLayoutOptions } from 'cytoscape-fcose';
|
||||||
import type { MermaidConfig } from '../../config.type.js';
|
import type { MermaidConfig } from '../../config.type.js';
|
||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
ArchitectureDataStructures,
|
ArchitectureDataStructures,
|
||||||
ArchitectureDirectionName,
|
ArchitectureDirectionName,
|
||||||
getOppositeArchitectureDirection,
|
getOppositeArchitectureDirection,
|
||||||
|
isArchitectureDirectionXY,
|
||||||
|
isArchitectureDirectionY,
|
||||||
} from './architectureTypes.js';
|
} from './architectureTypes.js';
|
||||||
import { select } from 'd3';
|
import { select } from 'd3';
|
||||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||||
@@ -51,6 +53,19 @@ function drawServices(
|
|||||||
services.forEach((service) => drawService(db, svg, service, conf));
|
services.forEach((service) => drawService(db, svg, service, conf));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function positionServices(db: ArchitectureDB, cy: cytoscape.Core) {
|
||||||
|
cy.nodes().map((node, id) => {
|
||||||
|
const data = node.data();
|
||||||
|
if (data.type === 'group') return;
|
||||||
|
data.x = node.position().x;
|
||||||
|
data.y = node.position().y;
|
||||||
|
|
||||||
|
const nodeElem = db.getElementById(data.id);
|
||||||
|
nodeElem.attr('transform', 'translate(' + (data.x || 0) + ',' + (data.y || 0) + ')');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) {
|
function addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) {
|
||||||
groups.forEach((group) => {
|
groups.forEach((group) => {
|
||||||
cy.add({
|
cy.add({
|
||||||
@@ -67,32 +82,41 @@ function addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function positionServices(db: ArchitectureDB, cy: cytoscape.Core) {
|
|
||||||
cy.nodes().map((node, id) => {
|
|
||||||
const data = node.data();
|
|
||||||
if (data.type === 'group') return;
|
|
||||||
data.x = node.position().x;
|
|
||||||
data.y = node.position().y;
|
|
||||||
console.log(`Position service (${data.id}): (${data.x}, ${data.y})`);
|
|
||||||
|
|
||||||
const nodeElem = db.getElementById(data.id);
|
|
||||||
nodeElem.attr('transform', 'translate(' + (data.x || 0) + ',' + (data.y || 0) + ')');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addEdges(lines: ArchitectureLine[], cy: cytoscape.Core) {
|
function addEdges(lines: ArchitectureLine[], cy: cytoscape.Core) {
|
||||||
lines.forEach((line) => {
|
lines.forEach((line) => {
|
||||||
|
const { lhs_id, rhs_id, lhs_into, rhs_into, lhs_dir, rhs_dir } = line;
|
||||||
|
const edgeType = isArchitectureDirectionXY(line.lhs_dir, line.rhs_dir)
|
||||||
|
? 'segments'
|
||||||
|
: 'straight';
|
||||||
|
const edge: cytoscape._EdgeSingularData = {
|
||||||
|
id: `${lhs_id}-${rhs_id}`,
|
||||||
|
source: lhs_id,
|
||||||
|
sourceDir: lhs_dir,
|
||||||
|
sourceArrow: lhs_into,
|
||||||
|
sourceEndpoint:
|
||||||
|
lhs_dir === 'L'
|
||||||
|
? '0 50%'
|
||||||
|
: lhs_dir === 'R'
|
||||||
|
? '100% 50%'
|
||||||
|
: lhs_dir === 'T'
|
||||||
|
? '50% 0'
|
||||||
|
: '50% 100%',
|
||||||
|
target: rhs_id,
|
||||||
|
targetDir: rhs_dir,
|
||||||
|
targetArrow: rhs_into,
|
||||||
|
targetEndpoint:
|
||||||
|
rhs_dir === 'L'
|
||||||
|
? '0 50%'
|
||||||
|
: rhs_dir === 'R'
|
||||||
|
? '100% 50%'
|
||||||
|
: rhs_dir === 'T'
|
||||||
|
? '50% 0'
|
||||||
|
: '50% 100%',
|
||||||
|
};
|
||||||
cy.add({
|
cy.add({
|
||||||
group: 'edges',
|
group: 'edges',
|
||||||
data: {
|
data: edge,
|
||||||
id: `${line.lhs_id}-${line.rhs_id}`,
|
classes: edgeType,
|
||||||
source: line.lhs_id,
|
|
||||||
sourceDir: line.lhs_dir,
|
|
||||||
sourceArrow: line.lhs_into,
|
|
||||||
target: line.rhs_id,
|
|
||||||
targetDir: line.rhs_dir,
|
|
||||||
targetArrow: line.rhs_into,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -112,8 +136,20 @@ function layoutArchitecture(
|
|||||||
selector: 'edge',
|
selector: 'edge',
|
||||||
style: {
|
style: {
|
||||||
'curve-style': 'straight',
|
'curve-style': 'straight',
|
||||||
'source-endpoint': '50% 50%',
|
'source-endpoint': 'data(sourceEndpoint)',
|
||||||
'target-endpoint': '50% 50%',
|
'target-endpoint': 'data(targetEndpoint)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'edge.segments',
|
||||||
|
style: {
|
||||||
|
'curve-style': 'segments',
|
||||||
|
'segment-weights': '0',
|
||||||
|
'segment-distances': [0.5],
|
||||||
|
//@ts-ignore
|
||||||
|
'edge-distances': 'endpoints',
|
||||||
|
'source-endpoint': 'data(sourceEndpoint)',
|
||||||
|
'target-endpoint': 'data(targetEndpoint)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -196,8 +232,6 @@ function layoutArchitecture(
|
|||||||
const invSpatialMap = Object.fromEntries(
|
const invSpatialMap = Object.fromEntries(
|
||||||
Object.entries(spatialMap).map(([id, pos]) => [posToStr(pos), id])
|
Object.entries(spatialMap).map(([id, pos]) => [posToStr(pos), id])
|
||||||
);
|
);
|
||||||
console.log('===== invSpatialMap =====');
|
|
||||||
console.log(invSpatialMap);
|
|
||||||
|
|
||||||
// perform BFS
|
// perform BFS
|
||||||
const queue = [posToStr([0, 0])];
|
const queue = [posToStr([0, 0])];
|
||||||
@@ -227,7 +261,7 @@ function layoutArchitecture(
|
|||||||
[ArchitectureDirectionName[
|
[ArchitectureDirectionName[
|
||||||
getOppositeArchitectureDirection(dir as ArchitectureDirection)
|
getOppositeArchitectureDirection(dir as ArchitectureDirection)
|
||||||
]]: currId,
|
]]: currId,
|
||||||
gap: 100,
|
gap: 1.5 * getConfigField('iconSize'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -244,12 +278,12 @@ function layoutArchitecture(
|
|||||||
console.log(`Relative Alignments:`);
|
console.log(`Relative Alignments:`);
|
||||||
console.log(relativeConstraints);
|
console.log(relativeConstraints);
|
||||||
|
|
||||||
cy.layout({
|
const layout = cy.layout({
|
||||||
name: 'fcose',
|
name: 'fcose',
|
||||||
quality: 'proof',
|
quality: 'proof',
|
||||||
styleEnabled: false,
|
styleEnabled: false,
|
||||||
animate: false,
|
animate: false,
|
||||||
nodeDimensionsIncludeLabels: true,
|
nodeDimensionsIncludeLabels: false,
|
||||||
// Adjust the edge parameters if it passes through the border of a group
|
// Adjust the edge parameters if it passes through the border of a group
|
||||||
// Hacky fix for: https://github.com/iVis-at-Bilkent/cytoscape.js-fcose/issues/67
|
// Hacky fix for: https://github.com/iVis-at-Bilkent/cytoscape.js-fcose/issues/67
|
||||||
idealEdgeLength(edge) {
|
idealEdgeLength(edge) {
|
||||||
@@ -257,14 +291,11 @@ function layoutArchitecture(
|
|||||||
const { parent: parentA } = nodeA.data();
|
const { parent: parentA } = nodeA.data();
|
||||||
const { parent: parentB } = nodeB.data();
|
const { parent: parentB } = nodeB.data();
|
||||||
const elasticity =
|
const elasticity =
|
||||||
parentA === parentB
|
parentA === parentB ? 1.5 * getConfigField('iconSize') : 0.5 * getConfigField('iconSize');
|
||||||
? 1.25 * getConfigField('iconSize')
|
|
||||||
: 0.5 * getConfigField('iconSize');
|
|
||||||
return elasticity;
|
return elasticity;
|
||||||
},
|
},
|
||||||
edgeElasticity(edge) {
|
edgeElasticity(edge) {
|
||||||
const [nodeA, nodeB] = edge.connectedNodes();
|
const [nodeA, nodeB] = edge.connectedNodes();
|
||||||
console.log(nodeA.data());
|
|
||||||
const { parent: parentA } = nodeA.data();
|
const { parent: parentA } = nodeA.data();
|
||||||
const { parent: parentB } = nodeB.data();
|
const { parent: parentB } = nodeB.data();
|
||||||
const elasticity = parentA === parentB ? 0.45 : 0.001;
|
const elasticity = parentA === parentB ? 0.45 : 0.001;
|
||||||
@@ -275,7 +306,79 @@ function layoutArchitecture(
|
|||||||
vertical: verticalAlignments,
|
vertical: verticalAlignments,
|
||||||
},
|
},
|
||||||
relativePlacementConstraint: relativeConstraints,
|
relativePlacementConstraint: relativeConstraints,
|
||||||
} as FcoseLayoutOptions).run();
|
} as FcoseLayoutOptions);
|
||||||
|
|
||||||
|
layout.one('layoutstop', (_event) => {
|
||||||
|
function getSegmentWeights(
|
||||||
|
source: Position,
|
||||||
|
target: Position,
|
||||||
|
pointX: number,
|
||||||
|
pointY: number
|
||||||
|
) {
|
||||||
|
let W, D;
|
||||||
|
const { x: sX, y: sY } = source;
|
||||||
|
const { x: tX, y: tY } = target;
|
||||||
|
|
||||||
|
D =
|
||||||
|
(pointY - sY + ((sX - pointX) * (sY - tY)) / (sX - tX)) /
|
||||||
|
Math.sqrt(1 + Math.pow((sY - tY) / (sX - tX), 2));
|
||||||
|
W = Math.sqrt(Math.pow(pointY - sY, 2) + Math.pow(pointX - sX, 2) - Math.pow(D, 2));
|
||||||
|
|
||||||
|
const distAB = Math.sqrt(Math.pow(tX - sX, 2) + Math.pow(tY - sY, 2));
|
||||||
|
W = W / distAB;
|
||||||
|
|
||||||
|
//check whether the point (pointX, pointY) is on right or left of the line src to tgt. for instance : a point C(X, Y) and line (AB). d=(xB-xA)(yC-yA)-(yB-yA)(xC-xA). if d>0, then C is on left of the line. if d<0, it is on right. if d=0, it is on the line.
|
||||||
|
let delta1 = (tX - sX) * (pointY - sY) - (tY - sY) * (pointX - sX);
|
||||||
|
switch (true) {
|
||||||
|
case delta1 >= 0:
|
||||||
|
delta1 = 1;
|
||||||
|
break;
|
||||||
|
case delta1 < 0:
|
||||||
|
delta1 = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
//check whether the point (pointX, pointY) is "behind" the line src to tgt
|
||||||
|
let delta2 = (tX - sX) * (pointX - sX) + (tY - sY) * (pointY - sY);
|
||||||
|
switch (true) {
|
||||||
|
case delta2 >= 0:
|
||||||
|
delta2 = 1;
|
||||||
|
break;
|
||||||
|
case delta2 < 0:
|
||||||
|
delta2 = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
D = Math.abs(D) * delta1; //ensure that sign of D is same as sign of delta1. Hence we need to take absolute value of D and multiply by delta1
|
||||||
|
W = W * delta2;
|
||||||
|
|
||||||
|
return {
|
||||||
|
distances: D,
|
||||||
|
weights: W,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
cy.startBatch();
|
||||||
|
for (let edge of Object.values(cy.edges())) {
|
||||||
|
if (edge.data) {
|
||||||
|
let { x: s_x, y: s_y } = edge.source().position();
|
||||||
|
let { x: t_x, y: t_y } = edge.target().position();
|
||||||
|
if (s_x !== t_x && s_y !== t_y) {
|
||||||
|
let sEP = edge.sourceEndpoint();
|
||||||
|
let tEP = edge.targetEndpoint();
|
||||||
|
const { sourceDir } = edge.data();
|
||||||
|
const [pointX, pointY] = isArchitectureDirectionY(sourceDir)
|
||||||
|
? [sEP.x, tEP.y]
|
||||||
|
: [tEP.x, sEP.y];
|
||||||
|
const { weights, distances } = getSegmentWeights(sEP, tEP, pointX, pointY);
|
||||||
|
edge.style('segment-distances', distances);
|
||||||
|
edge.style('segment-weights', weights);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cy.endBatch();
|
||||||
|
layout.run();
|
||||||
|
});
|
||||||
|
layout.run();
|
||||||
|
|
||||||
cy.ready((e) => {
|
cy.ready((e) => {
|
||||||
log.info('Ready', e);
|
log.info('Ready', e);
|
||||||
resolve(cy);
|
resolve(cy);
|
||||||
@@ -309,7 +412,7 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram)
|
|||||||
drawServices(db, servicesElem, services, conf);
|
drawServices(db, servicesElem, services, conf);
|
||||||
|
|
||||||
const cy = await layoutArchitecture(services, groups, lines, ds);
|
const cy = await layoutArchitecture(services, groups, lines, ds);
|
||||||
console.log(cy.nodes().map((node) => ({ a: node.data() })));
|
// console.log(cy.nodes().map((node) => ({ a: node.data() })));
|
||||||
|
|
||||||
drawEdges(edgesElem, cy);
|
drawEdges(edgesElem, cy);
|
||||||
drawGroups(groupElem, cy);
|
drawGroups(groupElem, cy);
|
||||||
|
@@ -21,10 +21,10 @@ export const ArchitectureDirectionArrow = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ArchitectureDirectionArrowShift = {
|
export const ArchitectureDirectionArrowShift = {
|
||||||
L: (orig: number, iconSize: number, arrowSize: number) => orig - iconSize / 2 - arrowSize + 2,
|
L: (orig: number, arrowSize: number) => orig - arrowSize + 2,
|
||||||
R: (orig: number, iconSize: number, arrowSize: number) => orig + iconSize / 2 - 2,
|
R: (orig: number, arrowSize: number) => orig - 2,
|
||||||
T: (orig: number, iconSize: number, arrowSize: number) => orig - iconSize / 2 - arrowSize + 2,
|
T: (orig: number, arrowSize: number) => orig - arrowSize + 2,
|
||||||
B: (orig: number, iconSize: number, arrowSize: number) => orig + iconSize / 2 - 2,
|
B: (orig: number, arrowSize: number) => orig - 2,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const getOppositeArchitectureDirection = function (
|
export const getOppositeArchitectureDirection = function (
|
||||||
@@ -104,7 +104,6 @@ export const shiftPositionByArchitectureDirectionPair = function (
|
|||||||
): number[] {
|
): number[] {
|
||||||
const lhs = pair[0] as ArchitectureDirection;
|
const lhs = pair[0] as ArchitectureDirection;
|
||||||
const rhs = pair[1] as ArchitectureDirection;
|
const rhs = pair[1] as ArchitectureDirection;
|
||||||
console.log(`${pair}: (${x},${y})`);
|
|
||||||
if (isArchitectureDirectionX(lhs)) {
|
if (isArchitectureDirectionX(lhs)) {
|
||||||
if (isArchitectureDirectionY(rhs)) {
|
if (isArchitectureDirectionY(rhs)) {
|
||||||
return [x + (lhs === 'L' ? -1 : 1), y + (rhs === 'T' ? 1 : -1)];
|
return [x + (lhs === 'L' ? -1 : 1), y + (rhs === 'T' ? 1 : -1)];
|
||||||
|
@@ -77,30 +77,29 @@ declare module 'cytoscape' {
|
|||||||
|
|
||||||
export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) {
|
export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) {
|
||||||
const iconSize = getConfigField('iconSize');
|
const iconSize = getConfigField('iconSize');
|
||||||
const halfIconSize = iconSize / 2;
|
|
||||||
const arrowSize = iconSize / 6;
|
const arrowSize = iconSize / 6;
|
||||||
const halfArrowSize = arrowSize / 2;
|
const halfArrowSize = arrowSize / 2;
|
||||||
|
|
||||||
cy.edges().map((edge, id) => {
|
cy.edges().map((edge, id) => {
|
||||||
const { source, sourceDir, sourceArrow, target, targetDir, targetArrow } = edge.data();
|
const { sourceDir, sourceArrow, targetDir, targetArrow } = edge.data();
|
||||||
if (edge[0]._private.bodyBounds) {
|
const { x: startX, y: startY } = edge[0].sourceEndpoint();
|
||||||
|
const { x: midX, y: midY } = edge[0].midpoint();
|
||||||
|
const { x: endX, y: endY } = edge[0].targetEndpoint();
|
||||||
|
if (edge[0]._private.rscratch) {
|
||||||
const bounds = edge[0]._private.rscratch;
|
const bounds = edge[0]._private.rscratch;
|
||||||
|
|
||||||
const g = edgesEl.insert('g');
|
const g = edgesEl.insert('g');
|
||||||
|
|
||||||
g.insert('path')
|
g.insert('path')
|
||||||
.attr(
|
.attr('d', `M ${startX},${startY} L ${midX},${midY} L${endX},${endY} `)
|
||||||
'd',
|
|
||||||
`M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} `
|
|
||||||
)
|
|
||||||
.attr('class', 'edge');
|
.attr('class', 'edge');
|
||||||
|
|
||||||
if (sourceArrow) {
|
if (sourceArrow) {
|
||||||
console.log(`New source arrow: ${sourceDir} for ${source}`);
|
|
||||||
const xShift = isArchitectureDirectionX(sourceDir)
|
const xShift = isArchitectureDirectionX(sourceDir)
|
||||||
? ArchitectureDirectionArrowShift[sourceDir](bounds.startX, iconSize, arrowSize)
|
? ArchitectureDirectionArrowShift[sourceDir](bounds.startX, arrowSize)
|
||||||
: bounds.startX - halfArrowSize;
|
: bounds.startX - halfArrowSize;
|
||||||
const yShift = isArchitectureDirectionY(sourceDir)
|
const yShift = isArchitectureDirectionY(sourceDir)
|
||||||
? ArchitectureDirectionArrowShift[sourceDir](bounds.startY, iconSize, arrowSize)
|
? ArchitectureDirectionArrowShift[sourceDir](bounds.startY, arrowSize)
|
||||||
: bounds.startY - halfArrowSize;
|
: bounds.startY - halfArrowSize;
|
||||||
|
|
||||||
g.insert('polygon')
|
g.insert('polygon')
|
||||||
@@ -109,13 +108,13 @@ export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) {
|
|||||||
.attr('class', 'arrow');
|
.attr('class', 'arrow');
|
||||||
}
|
}
|
||||||
if (targetArrow) {
|
if (targetArrow) {
|
||||||
console.log(`New target arrow: ${targetDir} for ${target}`);
|
|
||||||
const xShift = isArchitectureDirectionX(targetDir)
|
const xShift = isArchitectureDirectionX(targetDir)
|
||||||
? ArchitectureDirectionArrowShift[targetDir](bounds.endX, iconSize, arrowSize)
|
? ArchitectureDirectionArrowShift[targetDir](bounds.endX, arrowSize)
|
||||||
: bounds.endX - halfArrowSize;
|
: bounds.endX - halfArrowSize;
|
||||||
const yShift = isArchitectureDirectionY(targetDir)
|
const yShift = isArchitectureDirectionY(targetDir)
|
||||||
? ArchitectureDirectionArrowShift[targetDir](bounds.endY, iconSize, arrowSize)
|
? ArchitectureDirectionArrowShift[targetDir](bounds.endY, arrowSize)
|
||||||
: bounds.endY - halfArrowSize;
|
: bounds.endY - halfArrowSize;
|
||||||
|
|
||||||
g.insert('polygon')
|
g.insert('polygon')
|
||||||
.attr('points', ArchitectureDirectionArrow[targetDir](arrowSize))
|
.attr('points', ArchitectureDirectionArrow[targetDir](arrowSize))
|
||||||
.attr('transform', `translate(${xShift},${yShift})`)
|
.attr('transform', `translate(${xShift},${yShift})`)
|
||||||
@@ -207,7 +206,6 @@ export const drawService = function (
|
|||||||
const { width, height } = serviceElem._groups[0][0].getBBox();
|
const { width, height } = serviceElem._groups[0][0].getBBox();
|
||||||
service.width = width;
|
service.width = width;
|
||||||
service.height = height;
|
service.height = height;
|
||||||
console.log(`Draw service (${service.id})`);
|
|
||||||
db.setElementForId(service.id, serviceElem);
|
db.setElementForId(service.id, serviceElem);
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user