From b606767599b32137f51e9a8d687a942e1bfe2324 Mon Sep 17 00:00:00 2001 From: Roman Bruckner Date: Mon, 28 Apr 2025 19:03:31 +0800 Subject: [PATCH 1/8] feat(dia.Element): add getPortBBox(), getPortCenter(); port metrics caching; --- examples/connection-points-ts/.gitignore | 3 + examples/connection-points-ts/README.md | 24 ++ examples/connection-points-ts/index.html | 13 ++ examples/connection-points-ts/package.json | 36 +++ examples/connection-points-ts/src/index.ts | 217 ++++++++++++++++++ examples/connection-points-ts/tsconfig.json | 10 + .../connection-points-ts/webpack.config.js | 30 +++ packages/joint-core/src/anchors/index.mjs | 6 +- .../joint-core/src/connectionPoints/index.mjs | 42 +++- packages/joint-core/src/dia/Element.mjs | 7 +- packages/joint-core/src/dia/ports.mjs | 152 ++++++++---- .../joint-core/test/jointjs/elementPorts.js | 98 ++++++++ packages/joint-core/types/joint.d.ts | 8 +- yarn.lock | 15 ++ 14 files changed, 596 insertions(+), 65 deletions(-) create mode 100644 examples/connection-points-ts/.gitignore create mode 100644 examples/connection-points-ts/README.md create mode 100644 examples/connection-points-ts/index.html create mode 100644 examples/connection-points-ts/package.json create mode 100644 examples/connection-points-ts/src/index.ts create mode 100644 examples/connection-points-ts/tsconfig.json create mode 100644 examples/connection-points-ts/webpack.config.js diff --git a/examples/connection-points-ts/.gitignore b/examples/connection-points-ts/.gitignore new file mode 100644 index 0000000000..69c575d17f --- /dev/null +++ b/examples/connection-points-ts/.gitignore @@ -0,0 +1,3 @@ +build/ +dist/ +node_modules/ diff --git a/examples/connection-points-ts/README.md b/examples/connection-points-ts/README.md new file mode 100644 index 0000000000..6e537d3c1d --- /dev/null +++ b/examples/connection-points-ts/README.md @@ -0,0 +1,24 @@ +# JointJS Link Label Tools + +The example shows different tools for editing labels. + +## Setup + +Use Yarn to run this demo. + +You need to build *JointJS* first. Navigate to the root folder and run: +```bash +yarn install +yarn run build +``` + +Navigate to this directory, then run: +```bash +yarn start +``` + +## License + +The *JointJS* library is licensed under the [Mozilla Public License 2.0](https://github.com/clientIO/joint/blob/master/LICENSE). + +Copyright © 2013-2024 client IO diff --git a/examples/connection-points-ts/index.html b/examples/connection-points-ts/index.html new file mode 100644 index 0000000000..3950f87abb --- /dev/null +++ b/examples/connection-points-ts/index.html @@ -0,0 +1,13 @@ + + + + + + + Anchors Typescript | JointJS + + +
+ + + diff --git a/examples/connection-points-ts/package.json b/examples/connection-points-ts/package.json new file mode 100644 index 0000000000..b5842e2e97 --- /dev/null +++ b/examples/connection-points-ts/package.json @@ -0,0 +1,36 @@ +{ + "name": "@joint/demo-connection-points-ts", + "version": "4.1.3", + "main": "src/index.ts", + "homepage": "https://jointjs.com", + "author": { + "name": "client IO", + "url": "https://client.io" + }, + "license": "MPL-2.0", + "private": true, + "installConfig": { + "hoistingLimits": "workspaces" + }, + "scripts": { + "start": "webpack-dev-server", + "tsc": "tsc" + }, + "dependencies": { + "@joint/core": "workspace:^" + }, + "devDependencies": { + "css-loader": "3.5.3", + "style-loader": "1.2.1", + "ts-loader": "^9.2.5", + "typescript": "^5.7.3", + "webpack": "^5.61.0", + "webpack-cli": "^4.8.0", + "webpack-dev-server": "^4.2.1" + }, + "volta": { + "node": "22.14.0", + "npm": "11.2.0", + "yarn": "4.7.0" + } +} diff --git a/examples/connection-points-ts/src/index.ts b/examples/connection-points-ts/src/index.ts new file mode 100644 index 0000000000..226d50aa3c --- /dev/null +++ b/examples/connection-points-ts/src/index.ts @@ -0,0 +1,217 @@ +import { dia, shapes, util } from '@joint/core'; + +const cellNamespace = { + ...shapes, +} + +const graph = new dia.Graph({}, { + cellNamespace: cellNamespace +}); + +const paper = new dia.Paper({ + el: document.getElementById('paper'), + width: 1000, + height: 800, + overflow: true, + model: graph, + cellViewNamespace: cellNamespace, + gridSize: 1, + // async: true, + defaultAnchor: { + name: 'center', + args: { + useModelGeometry: true, + } + } +}); + +paper.el.style.display = 'none' + +let y = 100; + +function createPair(graph,{ + sourceConnectionPoint = null, + targetConnectionPoint = null, + sourceLabel = '', + targetLabel = '', + sourceAttributes = {}, + targetAttributes = {}, + sourcePort = null, + targetPort = null +} = {}) { + const portMarkup = util.svg``; + + const sourceEl = new shapes.standard.Rectangle({ + ...sourceAttributes, + position: { + x: 100, + y + }, + size: { + width: 140, + height: 100 + }, + attrs: { + label: { + fontFamily: 'sans-serif', + text: sourceLabel, + } + }, + ports: { + groups: { + portGroup1: { + position: 'top', + size: { width: 20, height: 20 }, + } + } + }, + portMarkup, + }); + const targetEl = new shapes.standard.Rectangle({ + ...targetAttributes, + position: { + x: 400, + y + }, + size: { + width: 150, + height: 100 + }, + attrs: { + label: { + fontFamily: 'sans-serif', + text: targetLabel, + } + }, + ports: { + groups: { + portGroup1: { + position: 'top', + size: { width: 20, height: 20 }, + } + } + }, + portMarkup, + }); + if (sourcePort) { + sourceEl.addPort({ + id: sourcePort, + group: 'portGroup1', + }); + } + if (targetPort) { + targetEl.addPort({ + id: targetPort, + group: 'portGroup1', + }); + } + const link = new shapes.standard.Link({ + source: { + id: sourceEl.id, + port: sourcePort, + connectionPoint: sourceConnectionPoint, + }, + target: { + id: targetEl.id, + port: targetPort, + connectionPoint: targetConnectionPoint, + }, + attrs: { + line: { + stroke: 'red', + strokeWidth: 3 + } + } + }); + graph.addCells([sourceEl, targetEl, link]); + y += 200; + return [sourceEl, targetEl, link]; +} + +createPair(graph, { + sourceConnectionPoint: { + name: 'bbox', + args: { + useModelGeometry: true, + } + }, + sourceAttributes: { + angle: 45 + }, + sourceLabel: 'bbox', + targetConnectionPoint: { + name: 'bbox', + args: { + useModelGeometry: true, + } + }, + targetLabel: 'bbox', +}); + +createPair(graph, { + sourceAttributes: { + angle: 45 + }, + sourceConnectionPoint: { + name: 'rectangle', + args: { + useModelGeometry: true, + } + }, + sourceLabel: 'rectangle', + targetConnectionPoint: { + name: 'rectangle', + args: { + useModelGeometry: true, + } + }, + targetLabel: 'rectangle', +}); + +createPair(graph, { + sourceConnectionPoint: { + name: 'bbox', + args: { + useModelGeometry: true, + } + }, + sourcePort: 'port1', + sourceLabel: 'bbox', + targetAttributes: { + angle: 45 + }, + targetConnectionPoint: { + name: 'bbox', + args: { + useModelGeometry: true, + } + }, + targetPort: 'port1', + targetLabel: 'bbox', +}); + +createPair(graph, { + sourceConnectionPoint: { + name: 'rectangle', + args: { + useModelGeometry: true, + } + }, + sourcePort: 'port1', + sourceLabel: 'rectangle', + targetAttributes: { + angle: 45 + }, + targetConnectionPoint: { + name: 'rectangle', + args: { + useModelGeometry: true, + } + }, + targetLabel: 'rectangle', + targetPort: 'port1', +}); + +paper.el.style.display = 'block'; + +paper.fitToContent({ useModelGeometry: true, padding: 20 }); diff --git a/examples/connection-points-ts/tsconfig.json b/examples/connection-points-ts/tsconfig.json new file mode 100644 index 0000000000..25244fca36 --- /dev/null +++ b/examples/connection-points-ts/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "moduleResolution": "nodenext", + "module": "NodeNext", + "target": "es6", + "noImplicitAny": false, + "sourceMap": false, + "outDir": "./build" + } +} diff --git a/examples/connection-points-ts/webpack.config.js b/examples/connection-points-ts/webpack.config.js new file mode 100644 index 0000000000..0d287f7177 --- /dev/null +++ b/examples/connection-points-ts/webpack.config.js @@ -0,0 +1,30 @@ +const path = require('path'); + +module.exports = { + resolve: { + extensions: ['.ts', '.tsx', '.js'] + }, + entry: './src/index.ts', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + publicPath: '/dist/' + }, + mode: 'development', + module: { + rules: [ + { test: /\.ts$/, loader: 'ts-loader' }, + { + test: /\.css$/, + sideEffects: true, + use: ['style-loader', 'css-loader'], + } + ] + }, + devServer: { + static: { + directory: __dirname, + }, + compress: true + }, +}; diff --git a/packages/joint-core/src/anchors/index.mjs b/packages/joint-core/src/anchors/index.mjs index b982b13fd4..1ab3c68f26 100644 --- a/packages/joint-core/src/anchors/index.mjs +++ b/packages/joint-core/src/anchors/index.mjs @@ -26,11 +26,7 @@ function getModelBBoxFromConnectedLink(element, link, endType, rotate) { const portId = link.get(endType).port; if (element.hasPort(portId)) { - const port = element.getPort(portId); - // Note: the `angle` property of the `port` is ignore here for now - bbox = new Rect(element.getPortsRects(port.group)[portId]); - bbox.offset(elementBBox.x, elementBBox.y); - bbox.moveAroundPoint(elementBBox.center(), -angle); + bbox = element.getPortBBox(portId); } else { bbox = elementBBox; } diff --git a/packages/joint-core/src/connectionPoints/index.mjs b/packages/joint-core/src/connectionPoints/index.mjs index 5998c488a3..9870197ffa 100644 --- a/packages/joint-core/src/connectionPoints/index.mjs +++ b/packages/joint-core/src/connectionPoints/index.mjs @@ -78,10 +78,12 @@ function anchorConnectionPoint(line, _view, _magnet, opt) { function bboxIntersection(line, view, magnet, opt) { - var bbox = view.getNodeBBox(magnet); + const bbox = (opt.useModelGeometry) + ? getNodeModelGeometry(view, magnet, true) + : view.getNodeBBox(magnet); if (opt.stroke) bbox.inflate(stroke(magnet) / 2); - var intersections = line.intersect(bbox); - var cp = (intersections) + const intersections = line.intersect(bbox); + const cp = (intersections) ? line.start.chooseClosest(intersections) : line.end; return offsetPoint(cp, line.start, opt.offset); @@ -89,22 +91,44 @@ function bboxIntersection(line, view, magnet, opt) { function rectangleIntersection(line, view, magnet, opt) { - var angle = view.model.angle(); + const angle = view.model.angle(); if (angle === 0) { return bboxIntersection(line, view, magnet, opt); } - var bboxWORotation = view.getNodeUnrotatedBBox(magnet); + const bboxWORotation = (opt.useModelGeometry) + ? getNodeModelGeometry(view, magnet, false) + : view.getNodeUnrotatedBBox(magnet); if (opt.stroke) bboxWORotation.inflate(stroke(magnet) / 2); - var center = bboxWORotation.center(); - var lineWORotation = line.clone().rotate(center, angle); - var intersections = lineWORotation.setLength(1e6).intersect(bboxWORotation); - var cp = (intersections) + const center = bboxWORotation.center(); + const lineWORotation = line.clone().rotate(center, angle); + const intersections = lineWORotation.setLength(1e6).intersect(bboxWORotation); + const cp = (intersections) ? lineWORotation.start.chooseClosest(intersections).rotate(center, -angle) : line.end; return offsetPoint(cp, line.start, opt.offset); } + +function getNodeModelGeometry(elementView, magnet, rotate) { + + const element = elementView.model; + const portId = elementView.findAttribute('port', magnet); + + let bbox; + if (element.hasPort(portId)) { + bbox = element.getPortBBox(portId); + } else { + bbox = element.getBBox(); + } + + if (rotate) { + const angle = element.angle(); + bbox.rotateAroundCenter(angle); + } + return bbox; +} + function findShapeNode(magnet) { if (!magnet) return null; var node = magnet; diff --git a/packages/joint-core/src/dia/Element.mjs b/packages/joint-core/src/dia/Element.mjs index b3239c9687..487cf51d9d 100644 --- a/packages/joint-core/src/dia/Element.mjs +++ b/packages/joint-core/src/dia/Element.mjs @@ -550,12 +550,7 @@ export const Element = Cell.extend({ if (!endDef) return center; var portId = endDef.port; if (!portId || !this.hasPort(portId)) return center; - var portGroup = this.portProp(portId, ['group']); - var portsPositions = this.getPortsPositions(portGroup); - var portCenter = new Point(portsPositions[portId]).offset(bbox.origin()); - var angle = this.angle(); - if (angle) portCenter.rotate(center, -angle); - return portCenter; + return this.getPortCenter(portId); } }); diff --git a/packages/joint-core/src/dia/ports.mjs b/packages/joint-core/src/dia/ports.mjs index 54a4a60986..1a9d3dab08 100644 --- a/packages/joint-core/src/dia/ports.mjs +++ b/packages/joint-core/src/dia/ports.mjs @@ -8,15 +8,24 @@ var PortData = function(data) { var clonedData = util.cloneDeep(data) || {}; this.ports = []; + this.portsMap = {}; this.groups = {}; this.portLayoutNamespace = Port; this.portLabelLayoutNamespace = PortLabel; + this.metrics = {}; + this.metricsKey = null; this._init(clonedData); }; PortData.prototype = { + getPort: function(id) { + const port = this.portsMap[id]; + if (port) return port; + throw new Error('Element: unable to find port with id ' + id); + }, + getPorts: function() { return this.ports; }, @@ -32,7 +41,26 @@ PortData.prototype = { }); }, - getGroupPortsMetrics: function(groupName, elBBox) { + getGroupPortsMetrics: function(groupName, rect) { + const { x = 0, y = 0, width = 0, height = 0 } = rect; + const metricsKey = `${x}:${y}:${width}:${height}`; + if (this.metricsKey !== metricsKey) { + // Clear the cache (the element size has changed) + this.metrics = {}; + this.metricsKey = metricsKey; + } + let groupPortsMetrics = this.metrics[groupName]; + if (groupPortsMetrics) { + // Return cached metrics + return groupPortsMetrics; + } + // Calculate the metrics + groupPortsMetrics = this.resolveGroupPortsMetrics(groupName, new Rect(x, y, width, height)); + this.metrics[groupName] = groupPortsMetrics; + return groupPortsMetrics; + }, + + resolveGroupPortsMetrics: function(groupName, elBBox) { var group = this.getGroup(groupName); var ports = this.getPortsByGroup(groupName); @@ -52,21 +80,23 @@ PortData.prototype = { var accumulator = { ports: ports, - result: [] + result: {} }; - util.toArray(groupPortTransformations).reduce(function(res, portTransformation, index) { - var port = res.ports[index]; - res.result.push({ - portId: port.id, + util.toArray(groupPortTransformations).reduce((res, portTransformation, index) => { + const port = res.ports[index]; + const portId = port.id; + res.result[portId] = { + index, + portId, portTransformation: portTransformation, labelTransformation: this._getPortLabelLayout(port, Point(portTransformation), elBBox), portAttrs: port.attrs, portSize: port.size, labelSize: port.label.size - }); + }; return res; - }.bind(this), accumulator); + }, accumulator); return accumulator.result; }, @@ -97,7 +127,9 @@ PortData.prototype = { // prepare ports var ports = util.toArray(data.items); for (var j = 0, m = ports.length; j < m; j++) { - this.ports.push(this._evaluatePort(ports[j])); + const resolvedPort = this._evaluatePort(ports[j]); + this.ports.push(resolvedPort); + this.portsMap[resolvedPort.id] = resolvedPort; } }, @@ -296,38 +328,70 @@ export const elementPortPrototype = { */ getPortsPositions: function(groupName) { - var portsMetrics = this._portSettingsData.getGroupPortsMetrics(groupName, Rect(this.size())); - - return portsMetrics.reduce(function(positions, metrics) { - var transformation = metrics.portTransformation; - positions[metrics.portId] = { - x: transformation.x, - y: transformation.y, - angle: transformation.angle + const portsMetrics = this.getGroupPortsMetrics(groupName); + const portsPosition = {}; + for (const portId in portsMetrics) { + const { + portTransformation: { x, y, angle }, + } = portsMetrics[portId]; + portsPosition[portId] = { + x: x, + y: y, + angle }; - return positions; - }, {}); + } + return portsPosition; }, - getPortsRects: function(groupName) { + getPortMetrics: function(portId) { + const port = this._portSettingsData.getPort(portId); + return this.getGroupPortsMetrics(port.group)[portId]; + }, - var portsMetrics = this._portSettingsData.getGroupPortsMetrics(groupName, Rect(this.size())); + getGroupPortsMetrics: function(groupName) { + return this._portSettingsData.getGroupPortsMetrics(groupName, this.size()); + }, - return portsMetrics.reduce(function(rects, metrics) { - const { - portId, - portTransformation: { x, y, angle }, - portSize: { width, height } - } = metrics; - rects[portId] = { - x: x - width / 2, - y: y - height / 2, - width, - height, - angle - }; - return rects; - }, {}); + getPortRelativePosition: function(portId) { + const { portTransformation: { x, y, angle }} = this.getPortMetrics(portId); + return { x, y, angle }; + }, + + getPortRelativeRect(portId) { + const { + portTransformation: { x, y, angle }, + portSize: { width, height } + } = this.getPortMetrics(portId); + const portRect = { + x: x - width / 2, + y: y - height / 2, + width, + height, + angle + }; + return portRect; + }, + + getPortCenter(portId) { + const elementBBox = this.getBBox(); + const portPosition = this.getPortRelativePosition(portId); + const portCenter = new Point(portPosition).offset(elementBBox.x, elementBBox.y); + const angle = this.angle(); + if (angle) portCenter.rotate(elementBBox.center(), -angle); + return portCenter; + }, + + getPortBBox: function(portId) { + const portRect = this.getPortRelativeRect(portId); + const elementBBox = this.getBBox(); + // Note: the `angle` property of the `port` is ignore here for now + const portBBox = new Rect(portRect); + portBBox.offset(elementBBox.x, elementBBox.y); + const angle = this.angle(); + if (angle) { + portBBox.moveAroundPoint(elementBBox.center(), -angle); + } + return portBBox; }, /** @@ -849,15 +913,15 @@ export const elementViewPortPrototype = { */ _updatePortGroup: function(groupName) { - var elementBBox = Rect(this.model.size()); - var portsMetrics = this.model._portSettingsData.getGroupPortsMetrics(groupName, elementBBox); + const portsMetrics = this.model.getGroupPortsMetrics(groupName); + const portsIds = Object.keys(portsMetrics); - for (var i = 0, n = portsMetrics.length; i < n; i++) { - var metrics = portsMetrics[i]; - var portId = metrics.portId; - var cached = this._portElementsCache[portId] || {}; - var portTransformation = metrics.portTransformation; - var labelTransformation = metrics.labelTransformation; + for (let i = 0, n = portsIds.length; i < n; i++) { + const portId = portsIds[i]; + const metrics = portsMetrics[portId]; + const cached = this._portElementsCache[portId] || {}; + const portTransformation = metrics.portTransformation; + const labelTransformation = metrics.labelTransformation; if (labelTransformation && cached.portLabelElement) { this.updateDOMSubtreeAttributes(cached.portLabelElement.node, labelTransformation.attrs, { rootBBox: new Rect(metrics.labelSize), diff --git a/packages/joint-core/test/jointjs/elementPorts.js b/packages/joint-core/test/jointjs/elementPorts.js index 77de6a6ea6..07c4bcbb7f 100644 --- a/packages/joint-core/test/jointjs/elementPorts.js +++ b/packages/joint-core/test/jointjs/elementPorts.js @@ -1992,6 +1992,104 @@ QUnit.module('element ports', function() { }); }); + QUnit.module('getPortCenter', function() { + + QUnit.test('ports center can be retrieved', function(assert) { + + const shape = create({ + groups: { + 'a': { position: 'left' } + }, + items: [ + { id: 'one', group: 'a' }, + { id: 'two', group: 'a' }, + { id: 'three', group: 'a' } + ] + }).set('size', { width: 5, height: 10 }); + + const layoutSpy = sinon.spy(joint.layout.Port, 'left'); + + let portPositionOne, portPositionTwo, portPositionThree; + + portPositionOne = shape.getPortCenter('one'); + portPositionTwo = shape.getPortCenter('two'); + portPositionThree = shape.getPortCenter('three'); + + assert.ok(portPositionOne.y > 0); + assert.ok(portPositionOne.y < portPositionTwo.y); + assert.ok(portPositionTwo.y < portPositionThree.y); + + assert.ok(layoutSpy.calledOnce, 'layout function called once'); + + shape.resize(13, 17); + + portPositionOne = shape.getPortCenter('one'); + portPositionTwo = shape.getPortCenter('two'); + portPositionThree = shape.getPortCenter('three'); + + assert.ok(portPositionOne.y > 0); + assert.ok(portPositionOne.y < portPositionTwo.y); + assert.ok(portPositionTwo.y < portPositionThree.y); + + assert.ok(layoutSpy.calledTwice, 'layout function called once'); + + layoutSpy.restore(); + }); + }); + + QUnit.module('getPortBBox', function() { + + QUnit.test('port bounding box can be retrieved', function(assert) { + + const width = 17; + const height = 13; + + var shape = create({ + groups: { + 'a': { + position: 'left', + size: { width, height } + } + }, + items: [ + { id: 'one', group: 'a' }, + { id: 'two', group: 'a' }, + { id: 'three', group: 'a' } + ] + }).set('size', { width: 50, height: 50 }); + + const layoutSpy = sinon.spy(joint.layout.Port, 'left'); + + let portBBoxOne, portBBoxTwo, portBBoxThree; + + portBBoxOne = shape.getPortBBox('one'); + portBBoxTwo = shape.getPortBBox('two'); + portBBoxThree = shape.getPortBBox('three'); + + assert.ok(portBBoxOne.y > 0); + assert.ok(portBBoxOne.y < portBBoxTwo.y); + assert.ok(portBBoxTwo.y < portBBoxThree.y); + assert.equal(portBBoxOne.width, width); + assert.equal(portBBoxOne.height, height); + + assert.ok(layoutSpy.calledOnce, 'layout function called once'); + + shape.resize(100, 100); + + portBBoxOne = shape.getPortBBox('one'); + portBBoxTwo = shape.getPortBBox('two'); + portBBoxThree = shape.getPortBBox('three'); + + assert.ok(portBBoxOne.y > 0); + assert.ok(portBBoxOne.y < portBBoxTwo.y); + assert.ok(portBBoxTwo.y < portBBoxThree.y); + + assert.ok(layoutSpy.calledTwice, 'layout function called once'); + + layoutSpy.restore(); + }); + }); + QUnit.module('getGroupPorts', function() { QUnit.test('return ports with given group', function(assert) { diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index 78879be07b..3ea655a090 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -687,7 +687,13 @@ export namespace dia { getPortsPositions(groupName: string): { [id: string]: Element.PortPosition }; - getPortsRects(groupName: string): { [id: string]: Element.PortRect }; + getPortRelativePosition(portId: string): Element.PortPosition; + + getPortRelativeRect(portId: string): Element.PortRect; + + getPortCenter(portId: string): g.Point; + + getPortBBox(portId: string): g.Rect; getPortIndex(port: string | Element.Port): number; diff --git a/yarn.lock b/yarn.lock index ee8f214b22..a54d90f8a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2187,6 +2187,21 @@ __metadata: languageName: unknown linkType: soft +"@joint/demo-connection-points-ts@workspace:examples/connection-points-ts": + version: 0.0.0-use.local + resolution: "@joint/demo-connection-points-ts@workspace:examples/connection-points-ts" + dependencies: + "@joint/core": "workspace:^" + css-loader: "npm:3.5.3" + style-loader: "npm:1.2.1" + ts-loader: "npm:^9.2.5" + typescript: "npm:^5.7.3" + webpack: "npm:^5.61.0" + webpack-cli: "npm:^4.8.0" + webpack-dev-server: "npm:^4.2.1" + languageName: unknown + linkType: soft + "@joint/demo-decorators@workspace:examples/decorators": version: 0.0.0-use.local resolution: "@joint/demo-decorators@workspace:examples/decorators" From 7f41e48ad3df9369a635ced37288b3848c5d2ae1 Mon Sep 17 00:00:00 2001 From: Roman Bruckner Date: Mon, 28 Apr 2025 19:14:35 +0800 Subject: [PATCH 2/8] udpate --- packages/joint-core/src/dia/ports.mjs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/joint-core/src/dia/ports.mjs b/packages/joint-core/src/dia/ports.mjs index 1a9d3dab08..08f7a5a61f 100644 --- a/packages/joint-core/src/dia/ports.mjs +++ b/packages/joint-core/src/dia/ports.mjs @@ -20,6 +20,10 @@ var PortData = function(data) { PortData.prototype = { + hasPort: function(id) { + return id in this.portsMap; + }, + getPort: function(id) { const port = this.portsMap[id]; if (port) return port; @@ -280,8 +284,7 @@ export const elementPortPrototype = { */ hasPorts: function() { - var ports = this.prop('ports/items'); - return Array.isArray(ports) && ports.length > 0; + return this._portSettingsData.getPorts().length > 0; }, /** @@ -290,7 +293,7 @@ export const elementPortPrototype = { */ hasPort: function(id) { - return this.getPortIndex(id) !== -1; + return this._portSettingsData.hasPort(id); }, /** From 682d9e2189a8bbb5c93ab15a42f25d6b26a7054a Mon Sep 17 00:00:00 2001 From: Roman Bruckner Date: Mon, 28 Apr 2025 19:38:10 +0800 Subject: [PATCH 3/8] update --- packages/joint-core/src/anchors/index.mjs | 21 +++++-------------- .../joint-core/src/connectionPoints/index.mjs | 21 ++++++------------- packages/joint-core/src/dia/ports.mjs | 5 ++++- 3 files changed, 15 insertions(+), 32 deletions(-) diff --git a/packages/joint-core/src/anchors/index.mjs b/packages/joint-core/src/anchors/index.mjs index 1ab3c68f26..447332a6e6 100644 --- a/packages/joint-core/src/anchors/index.mjs +++ b/packages/joint-core/src/anchors/index.mjs @@ -19,23 +19,12 @@ const SideMode = { function getModelBBoxFromConnectedLink(element, link, endType, rotate) { - let bbox; - - const elementBBox = element.getBBox(); - const angle = element.angle(); const portId = link.get(endType).port; - if (element.hasPort(portId)) { - bbox = element.getPortBBox(portId); - } else { - bbox = elementBBox; - } - - if (!rotate) { - bbox.rotateAroundCenter(-angle); + return element.getPortBBox(portId, { rotate }); } - return bbox; + return element.getBBox({ rotate }); } function getMiddleSide(rect, point, opt) { @@ -98,7 +87,7 @@ function bboxWrapper(method) { let bbox, center; if (opt.useModelGeometry) { - bbox = getModelBBoxFromConnectedLink(element, link, endType, rotate); + bbox = getModelBBoxFromConnectedLink(element, link, endType, !rotate); center = bbox.center(); } else { center = element.getBBox().center(); @@ -156,7 +145,7 @@ function _perpendicular(elementView, magnet, refPoint, opt, endType, linkView) { let bbox; if (opt.useModelGeometry) { - bbox = getModelBBoxFromConnectedLink(element, linkView.model, endType, false); + bbox = getModelBBoxFromConnectedLink(element, linkView.model, endType, true); } else { bbox = elementView.getNodeBBox(magnet); } @@ -188,7 +177,7 @@ function _midSide(view, magnet, refPoint, opt, endType, linkView) { var bbox; if (opt.useModelGeometry) { - bbox = getModelBBoxFromConnectedLink(view.model, linkView.model, endType, rotate); + bbox = getModelBBoxFromConnectedLink(view.model, linkView.model, endType, !rotate); center = bbox.center(); } else { bbox = rotate ? view.getNodeUnrotatedBBox(magnet) : view.getNodeBBox(magnet); diff --git a/packages/joint-core/src/connectionPoints/index.mjs b/packages/joint-core/src/connectionPoints/index.mjs index 9870197ffa..909866f677 100644 --- a/packages/joint-core/src/connectionPoints/index.mjs +++ b/packages/joint-core/src/connectionPoints/index.mjs @@ -79,7 +79,7 @@ function anchorConnectionPoint(line, _view, _magnet, opt) { function bboxIntersection(line, view, magnet, opt) { const bbox = (opt.useModelGeometry) - ? getNodeModelGeometry(view, magnet, true) + ? getNodeModelBBox(view, magnet, true) : view.getNodeBBox(magnet); if (opt.stroke) bbox.inflate(stroke(magnet) / 2); const intersections = line.intersect(bbox); @@ -97,7 +97,7 @@ function rectangleIntersection(line, view, magnet, opt) { } const bboxWORotation = (opt.useModelGeometry) - ? getNodeModelGeometry(view, magnet, false) + ? getNodeModelBBox(view, magnet, false) : view.getNodeUnrotatedBBox(magnet); if (opt.stroke) bboxWORotation.inflate(stroke(magnet) / 2); const center = bboxWORotation.center(); @@ -109,24 +109,15 @@ function rectangleIntersection(line, view, magnet, opt) { return offsetPoint(cp, line.start, opt.offset); } - -function getNodeModelGeometry(elementView, magnet, rotate) { - +function getNodeModelBBox(elementView, magnet, rotate) { const element = elementView.model; - const portId = elementView.findAttribute('port', magnet); - let bbox; + const portId = elementView.findAttribute('port', magnet); if (element.hasPort(portId)) { - bbox = element.getPortBBox(portId); - } else { - bbox = element.getBBox(); + return element.getPortBBox(portId, { rotate }); } - if (rotate) { - const angle = element.angle(); - bbox.rotateAroundCenter(angle); - } - return bbox; + return element.getBBox({ rotate }); } function findShapeNode(magnet) { diff --git a/packages/joint-core/src/dia/ports.mjs b/packages/joint-core/src/dia/ports.mjs index 08f7a5a61f..f1b8606c32 100644 --- a/packages/joint-core/src/dia/ports.mjs +++ b/packages/joint-core/src/dia/ports.mjs @@ -384,7 +384,7 @@ export const elementPortPrototype = { return portCenter; }, - getPortBBox: function(portId) { + getPortBBox: function(portId, opt) { const portRect = this.getPortRelativeRect(portId); const elementBBox = this.getBBox(); // Note: the `angle` property of the `port` is ignore here for now @@ -394,6 +394,9 @@ export const elementPortPrototype = { if (angle) { portBBox.moveAroundPoint(elementBBox.center(), -angle); } + if (opt && opt.rotate) { + portBBox.rotateAroundCenter(angle); + } return portBBox; }, From 0e74a1144805eb28af8c29e1ef03e1ff37808563 Mon Sep 17 00:00:00 2001 From: Roman Bruckner Date: Mon, 28 Apr 2025 19:53:58 +0800 Subject: [PATCH 4/8] update --- .../joint-core/test/jointjs/elementPorts.js | 53 ++++++++++++++++++- packages/joint-core/types/joint.d.ts | 8 ++- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/packages/joint-core/test/jointjs/elementPorts.js b/packages/joint-core/test/jointjs/elementPorts.js index 07c4bcbb7f..3fd54f681f 100644 --- a/packages/joint-core/test/jointjs/elementPorts.js +++ b/packages/joint-core/test/jointjs/elementPorts.js @@ -2044,7 +2044,7 @@ QUnit.module('element ports', function() { const width = 17; const height = 13; - var shape = create({ + const shape = create({ groups: { 'a': { position: 'left', @@ -2088,6 +2088,57 @@ QUnit.module('element ports', function() { layoutSpy.restore(); }); + + QUnit.test('option: rotate', function(assert) { + + const width = 17; + const height = 13; + + const elX = 47; + const elY = 53; + const elWidth = 100; + const elHeight = 100; + + const shape = create({ + groups: { + 'a': { + position: 'left', + size: { width, height } + } + }, + items: [ + { id: 'one', group: 'a' }, + ] + }); + + shape.set({ + position: { x: elX, y: elY }, + size: { width: elWidth, height: elHeight }, + angle: 90, + }) + + const layoutSpy = sinon.spy(joint.layout.Port, 'left'); + + const portUnrotatedBBox = shape.getPortBBox('one'); + const portRotatedBBox = shape.getPortBBox('one', { rotate: true }); + + assert.ok(portUnrotatedBBox instanceof g.Rect); + assert.ok(portRotatedBBox instanceof g.Rect); + + assert.equal(portUnrotatedBBox.x, elX + elWidth / 2 - width / 2); + assert.equal(portUnrotatedBBox.y, elY - height / 2); + assert.equal(Math.round(portUnrotatedBBox.width), width); + assert.equal(Math.round(portUnrotatedBBox.height), height); + + assert.equal(portRotatedBBox.x, elX + elWidth / 2 - height / 2); + assert.equal(portRotatedBBox.y, elY - width / 2); + assert.equal(Math.round(portRotatedBBox.width), height); + assert.equal(Math.round(portRotatedBBox.height), width); + + assert.ok(layoutSpy.calledOnce, 'layout function called once'); + + layoutSpy.restore(); + }); }); QUnit.module('getGroupPorts', function() { diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index 3ea655a090..a8244b5cc2 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -633,9 +633,13 @@ export namespace dia { terminator?: Cell | Cell.ID; } - interface BBoxOptions extends Cell.EmbeddableOptions { + interface RotateOptions { rotate?: boolean; } + + interface BBoxOptions extends Cell.EmbeddableOptions, RotateOptions { + + } } class Element extends Cell { @@ -693,7 +697,7 @@ export namespace dia { getPortCenter(portId: string): g.Point; - getPortBBox(portId: string): g.Rect; + getPortBBox(portId: string, opt?: Element.RotateOptions): g.Rect; getPortIndex(port: string | Element.Port): number; From 586cee5dfd75e5cdb0d4f85018181552a9618648 Mon Sep 17 00:00:00 2001 From: Roman Bruckner Date: Tue, 29 Apr 2025 17:43:26 +0800 Subject: [PATCH 5/8] udpate --- examples/connection-points-ts/src/index.ts | 13 +- .../test/jointjs/connectionPoints.js | 162 +++++++++++++++++- 2 files changed, 170 insertions(+), 5 deletions(-) diff --git a/examples/connection-points-ts/src/index.ts b/examples/connection-points-ts/src/index.ts index 226d50aa3c..b6cd498cf1 100644 --- a/examples/connection-points-ts/src/index.ts +++ b/examples/connection-points-ts/src/index.ts @@ -39,7 +39,7 @@ function createPair(graph,{ sourcePort = null, targetPort = null } = {}) { - const portMarkup = util.svg``; + const portMarkup = util.svg``; const sourceEl = new shapes.standard.Rectangle({ ...sourceAttributes, @@ -61,7 +61,7 @@ function createPair(graph,{ groups: { portGroup1: { position: 'top', - size: { width: 20, height: 20 }, + size: { width: 40, height: 20 }, } } }, @@ -87,7 +87,14 @@ function createPair(graph,{ groups: { portGroup1: { position: 'top', - size: { width: 20, height: 20 }, + size: { width: 40, height: 20 }, + attrs: { + portBody: { + width: 'calc(w)', + height: 'calc(h)', + + } + } } } }, diff --git a/packages/joint-core/test/jointjs/connectionPoints.js b/packages/joint-core/test/jointjs/connectionPoints.js index dacaadef85..d77aa9f05e 100644 --- a/packages/joint-core/test/jointjs/connectionPoints.js +++ b/packages/joint-core/test/jointjs/connectionPoints.js @@ -1,6 +1,6 @@ QUnit.module('connectionPoints', function(hooks) { - var paper, graph, r1, rv1, l1, lv1, sp, tp, fullNode; + var paper, graph, r1, rv1, l1, lv1, sp, tp, fullNode, quarterNode, textNode; hooks.beforeEach(function() { @@ -28,6 +28,7 @@ QUnit.module('connectionPoints', function(hooks) { }, { markup: [{ tagName: 'g', + selector: 'text', textContent: 'test-test-content' }, { tagName: 'rect', @@ -52,7 +53,9 @@ QUnit.module('connectionPoints', function(hooks) { graph.addCells([r1, l1]); rv1 = r1.findView(paper); lv1 = l1.findView(paper); - fullNode = rv1.el.querySelector('[joint-selector="full"]'); + fullNode = rv1.findNode('full'); + quarterNode = rv1.findNode('quarter'); + textNode = rv1.findNode('text'); }); hooks.afterEach(function() { @@ -164,6 +167,84 @@ QUnit.module('connectionPoints', function(hooks) { cp = connectionPointFn.call(lv1, line, rv1, fullNode, { stroke: true }); assert.ok(cp.round().equals(r1.getBBox().rightMiddle().move(tp, -strokeWidth / 2).round())); }); + + QUnit.module('useModelGeometry', function() { + + QUnit.test('uses model metrics when connected to an element', function(assert) { + const connectionPointFn = joint.connectionPoints.bbox; + let cp, line; + + r1.position(0, 0); + r1.resize(52, 74); + + line = new g.Line(new g.Point(100, 37), new g.Point(26, 37)); + cp = connectionPointFn.call(lv1, line, rv1, fullNode, { useModelGeometry: true }); + assert.ok(cp.round().equals(r1.getBBox().rightMiddle().round())); + cp = connectionPointFn.call(lv1, line, rv1, quarterNode, { useModelGeometry: true }); + assert.ok(cp.round().equals(r1.getBBox().rightMiddle().round())); + cp = connectionPointFn.call(lv1, line, rv1, textNode, { useModelGeometry: true }); + assert.ok(cp.round().equals(r1.getBBox().rightMiddle().round())); + + r1.rotate(90); + + cp = connectionPointFn.call(lv1, line, rv1, fullNode, { useModelGeometry: true }); + assert.ok(cp.round().equals(r1.getBBox({ rotate: true }).rightMiddle().round())); + cp = connectionPointFn.call(lv1, line, rv1, quarterNode, { useModelGeometry: true }); + assert.ok(cp.round().equals(r1.getBBox({ rotate: true }).rightMiddle().round())); + cp = connectionPointFn.call(lv1, line, rv1, textNode, { useModelGeometry: true }); + assert.ok(cp.round().equals(r1.getBBox({ rotate: true }).rightMiddle().round())); + }); + + QUnit.test('uses port model metrics when connected to a port', function(assert) { + const connectionPointFn = joint.connectionPoints.bbox; + let cp, line; + + const width = 52; + const height = 74; + const portWidth = 11; + const portHeight = 17; + + r1.position(0, 0); + r1.resize(width, height); + r1.set('ports', { + groups: { + 'g1': { + position: { + name: 'right' + } + }, + }, + items: [{ + id: 'p1', + group: 'g1', + size: { width: portWidth, height: portHeight }, + }] + }); + + const portNode = rv1.findPortNode('p1'); + + line = new g.Line(new g.Point(2 * width, height / 2), new g.Point(width, height / 2)); + cp = connectionPointFn.call(lv1, line, rv1, portNode, { useModelGeometry: true }); + assert.ok(cp.equals(r1.getPortBBox('p1', { rotate: true }).rightMiddle())); + + line = new g.Line(new g.Point(width, 2 * height), new g.Point(width, height / 2)); + cp = connectionPointFn.call(lv1, line, rv1, portNode, { useModelGeometry: true }); + assert.ok(cp.equals(r1.getPortBBox('p1', { rotate: true }).bottomMiddle())); + + r1.rotate(45); + + const r1BBoxWR = r1.getBBox({ rotate: true }); + const p1BBoxWR = r1.getPortBBox('p1', { rotate: true }); + + line = new g.Line(p1BBoxWR.center().offset(0, 1000), p1BBoxWR.center()); + cp = connectionPointFn.call(lv1, line, rv1, portNode, { useModelGeometry: true }); + assert.ok(cp.equals(r1.getPortBBox('p1', { rotate: true }).bottomMiddle())); + + line = new g.Line(p1BBoxWR.center().offset(1000, 0), p1BBoxWR.center()); + cp = connectionPointFn.call(lv1, line, rv1, portNode, { useModelGeometry: true }); + assert.ok(cp.equals(r1.getPortBBox('p1', { rotate: true }).rightMiddle())); + }); + }); }); }); @@ -212,6 +293,83 @@ QUnit.module('connectionPoints', function(hooks) { cp = connectionPointFn.call(lv1, line, rv1, fullNode, { stroke: true }); assert.ok(cp.round().equals(r1.getBBox().rightMiddle().move(tp, -strokeWidth / 2).round())); }); + + QUnit.module('useModelGeometry', function() { + + QUnit.test('uses model metrics when connected to an element', function(assert) { + const connectionPointFn = joint.connectionPoints.rectangle; + let cp, line; + + r1.position(0, 0); + r1.resize(52, 74); + + line = new g.Line(new g.Point(100, 37), new g.Point(26, 37)); + cp = connectionPointFn.call(lv1, line, rv1, fullNode, { useModelGeometry: true }); + assert.ok(cp.round().equals(r1.getBBox().rightMiddle().round())); + cp = connectionPointFn.call(lv1, line, rv1, quarterNode, { useModelGeometry: true }); + assert.ok(cp.round().equals(r1.getBBox().rightMiddle().round())); + cp = connectionPointFn.call(lv1, line, rv1, textNode, { useModelGeometry: true }); + assert.ok(cp.round().equals(r1.getBBox().rightMiddle().round())); + + r1.rotate(90); + + cp = connectionPointFn.call(lv1, line, rv1, fullNode, { useModelGeometry: true }); + assert.ok(cp.round().equals(r1.getBBox({ rotate: true }).rightMiddle().round())); + cp = connectionPointFn.call(lv1, line, rv1, quarterNode, { useModelGeometry: true }); + assert.ok(cp.round().equals(r1.getBBox({ rotate: true }).rightMiddle().round())); + cp = connectionPointFn.call(lv1, line, rv1, textNode, { useModelGeometry: true }); + assert.ok(cp.round().equals(r1.getBBox({ rotate: true }).rightMiddle().round())); + }); + + QUnit.test('uses port model metrics when connected to a port', function(assert) { + const connectionPointFn = joint.connectionPoints.rectangle; + let cp, line; + + const width = 52; + const height = 74; + const portWidth = 11; + const portHeight = 17; + + r1.position(0, 0); + r1.resize(width, height); + r1.set('ports', { + groups: { + 'g1': { + position: { + name: 'right' + } + }, + }, + items: [{ + id: 'p1', + group: 'g1', + size: { width: portWidth, height: portHeight }, + }] + }); + + const portNode = rv1.findPortNode('p1'); + + line = new g.Line(new g.Point(2 * width, height / 2), new g.Point(width, height / 2)); + cp = connectionPointFn.call(lv1, line, rv1, portNode, { useModelGeometry: true }); + assert.ok(cp.round().equals(r1.getBBox({ rotate: true }).rightMiddle().offset(portWidth / 2, 0).round())); + + line = new g.Line(new g.Point(width, 2 * height), new g.Point(width, height / 2)); + cp = connectionPointFn.call(lv1, line, rv1, portNode, { useModelGeometry: true }); + assert.ok(cp.round().equals(r1.getBBox({ rotate: true }).rightMiddle().offset(0, portHeight / 2).round())); + + r1.rotate(90); + + const r1BBoxWR = r1.getBBox({ rotate: true }); + + line = new g.Line(r1BBoxWR.bottomMiddle().offset(0, 1000), r1BBoxWR.bottomMiddle()); + cp = connectionPointFn.call(lv1, line, rv1, portNode, { useModelGeometry: true }); + assert.ok(cp.equals(r1.getPortBBox('p1', { rotate: true }).bottomMiddle())); + + line = new g.Line(r1BBoxWR.bottomMiddle().offset(1000, 0), r1BBoxWR.bottomMiddle()); + cp = connectionPointFn.call(lv1, line, rv1, portNode, { useModelGeometry: true }); + assert.ok(cp.equals(r1.getPortBBox('p1', { rotate: true }).rightMiddle())); + }); + }); }); }); From f7d316b739d1385077c026dac5574c175b5233c4 Mon Sep 17 00:00:00 2001 From: Roman Bruckner Date: Tue, 29 Apr 2025 17:52:12 +0800 Subject: [PATCH 6/8] update --- packages/joint-core/src/dia/ports.mjs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/joint-core/src/dia/ports.mjs b/packages/joint-core/src/dia/ports.mjs index f1b8606c32..fb51565787 100644 --- a/packages/joint-core/src/dia/ports.mjs +++ b/packages/joint-core/src/dia/ports.mjs @@ -375,6 +375,13 @@ export const elementPortPrototype = { return portRect; }, + /** + * @param {string} portId + * @returns {Point} + * @description Returns the port center in the graph coordinate system. + * The port center is in the graph coordinate system, and the position + * already takes into account the element rotation. + **/ getPortCenter(portId) { const elementBBox = this.getBBox(); const portPosition = this.getPortRelativePosition(portId); @@ -384,6 +391,16 @@ export const elementPortPrototype = { return portCenter; }, + /** + * @param {string} portId + * @param {object} [opt] + * @param {boolean} [opt.rotate] - If true, the port bounding box is rotated + * around the port center. + * @returns {Rect} + * @description Returns the bounding box of the port in the graph coordinate system. + * The port center is rotated around the element center, but the port bounding box + * is not rotated (unless `opt.rotate` is set to true). + */ getPortBBox: function(portId, opt) { const portRect = this.getPortRelativeRect(portId); const elementBBox = this.getBBox(); From 88fff47556e5a3b68df5c71c24460ae8b9c52563 Mon Sep 17 00:00:00 2001 From: Roman Bruckner Date: Tue, 29 Apr 2025 18:05:49 +0800 Subject: [PATCH 7/8] update --- packages/joint-core/src/anchors/index.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/joint-core/src/anchors/index.mjs b/packages/joint-core/src/anchors/index.mjs index 447332a6e6..799c1a9eb5 100644 --- a/packages/joint-core/src/anchors/index.mjs +++ b/packages/joint-core/src/anchors/index.mjs @@ -1,5 +1,5 @@ import * as util from '../util/index.mjs'; -import { toRad, Rect } from '../g/index.mjs'; +import { toRad } from '../g/index.mjs'; import { resolveRef } from '../linkAnchors/index.mjs'; const Side = { From 4b4a906e4910321403caa6f04f2815e3d9a3e124 Mon Sep 17 00:00:00 2001 From: Roman Bruckner Date: Tue, 29 Apr 2025 18:08:27 +0800 Subject: [PATCH 8/8] Update packages/joint-core/test/jointjs/elementPorts.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/joint-core/test/jointjs/elementPorts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/joint-core/test/jointjs/elementPorts.js b/packages/joint-core/test/jointjs/elementPorts.js index 3fd54f681f..55667989e2 100644 --- a/packages/joint-core/test/jointjs/elementPorts.js +++ b/packages/joint-core/test/jointjs/elementPorts.js @@ -2031,7 +2031,7 @@ QUnit.module('element ports', function() { assert.ok(portPositionOne.y < portPositionTwo.y); assert.ok(portPositionTwo.y < portPositionThree.y); - assert.ok(layoutSpy.calledTwice, 'layout function called once'); + assert.ok(layoutSpy.calledTwice, 'layout function called twice'); layoutSpy.restore(); });