|
31064 | 31064 | content: "", |
31065 | 31065 | showAddButtons: true, |
31066 | 31066 | resizeHotkey: null, |
| 31067 | + connectionType: "curve", |
| 31068 | + // line, curve, zigzag |
| 31069 | + connectionStyle: "solid", |
| 31070 | + // solid, dashed, dotted |
31067 | 31071 | onAddPoint: Metro2.noop, |
31068 | 31072 | onRemovePoint: Metro2.noop, |
31069 | 31073 | onStartConnection: Metro2.noop, |
|
31328 | 31332 | } |
31329 | 31333 | const connectionId = `connection-${Date.now()}`; |
31330 | 31334 | const connector = Metro2.connector.create(globalConnectionState.sourcePoint[0], targetPoint[0], { |
31331 | | - // type: connector.options.type || "curve", |
| 31335 | + type: o2.connectionType, |
| 31336 | + lineStyle: o2.connectionStyle, |
31332 | 31337 | container: globalConnectionState.sourceBlock.element.parent(), |
31333 | 31338 | id: connectionId, |
31334 | 31339 | autoUpdate: true |
|
31798 | 31803 | id: null, |
31799 | 31804 | // унікальний ID для з'єднання |
31800 | 31805 | deleteButton: false, |
| 31806 | + arrow: false, |
| 31807 | + lineStyle: "solid", |
| 31808 | + // solid, dashed, dotted |
31801 | 31809 | onConnectorCreate: Metro2.noop, |
31802 | 31810 | onConnectorUpdate: Metro2.noop, |
31803 | 31811 | onConnectorDestroy: Metro2.noop |
|
31850 | 31858 | this.svgElement = sharedSvg; |
31851 | 31859 | const shape = this._createShape(o2.id, o2.type, sharedSvg); |
31852 | 31860 | const deleteBtn = this._createDeleteButton(sharedSvg, o2.id); |
| 31861 | + if (o2.arrow) { |
| 31862 | + const markerId = this._ensureArrowMarker(sharedSvg); |
| 31863 | + shape.attr("marker-end", `url(#${markerId})`); |
| 31864 | + } |
| 31865 | + this._applyLineStyle(shape, o2.lineStyle); |
31853 | 31866 | this.connections.set(o2.id, { |
31854 | 31867 | pointA: o2.pointA, |
31855 | 31868 | pointB: o2.pointB, |
|
31935 | 31948 | } |
31936 | 31949 | return svg; |
31937 | 31950 | }, |
| 31951 | + _ensureArrowMarker: (svg) => { |
| 31952 | + const ns = "http://www.w3.org/2000/svg"; |
| 31953 | + const markerId = "connector-arrow"; |
| 31954 | + let defs = svg.find("defs"); |
| 31955 | + if (!defs.length) { |
| 31956 | + const d4 = document.createElementNS(ns, "defs"); |
| 31957 | + svg[0].appendChild(d4); |
| 31958 | + defs = $7(d4); |
| 31959 | + } |
| 31960 | + let marker = defs.find(`#${markerId}`); |
| 31961 | + if (!marker.length) { |
| 31962 | + const m5 = document.createElementNS(ns, "marker"); |
| 31963 | + m5.setAttribute("id", markerId); |
| 31964 | + m5.setAttribute("markerWidth", "6"); |
| 31965 | + m5.setAttribute("markerHeight", "6"); |
| 31966 | + m5.setAttribute("refX", "5"); |
| 31967 | + m5.setAttribute("refY", "3"); |
| 31968 | + m5.setAttribute("orient", "auto"); |
| 31969 | + m5.setAttribute("markerUnits", "strokeWidth"); |
| 31970 | + const line1 = document.createElementNS(ns, "line"); |
| 31971 | + line1.setAttribute("x1", "1"); |
| 31972 | + line1.setAttribute("y1", "1"); |
| 31973 | + line1.setAttribute("x2", "5"); |
| 31974 | + line1.setAttribute("y2", "3"); |
| 31975 | + line1.setAttribute("stroke", "context-stroke"); |
| 31976 | + line1.setAttribute("stroke-linecap", "round"); |
| 31977 | + const line2 = document.createElementNS(ns, "line"); |
| 31978 | + line2.setAttribute("x1", "1"); |
| 31979 | + line2.setAttribute("y1", "5"); |
| 31980 | + line2.setAttribute("x2", "5"); |
| 31981 | + line2.setAttribute("y2", "3"); |
| 31982 | + line2.setAttribute("stroke", "context-stroke"); |
| 31983 | + line2.setAttribute("stroke-linecap", "round"); |
| 31984 | + m5.appendChild(line1); |
| 31985 | + m5.appendChild(line2); |
| 31986 | + defs[0].appendChild(m5); |
| 31987 | + marker = $7(m5); |
| 31988 | + } |
| 31989 | + return markerId; |
| 31990 | + }, |
31938 | 31991 | _createShape: (id, type, svg) => { |
31939 | 31992 | const ns = "http://www.w3.org/2000/svg"; |
31940 | 31993 | let el; |
|
31990 | 32043 | }); |
31991 | 32044 | return $7(g5); |
31992 | 32045 | }, |
31993 | | - // Публічні методи |
31994 | | - update: function() { |
31995 | | - const o2 = this.options; |
31996 | | - const connection = this.connections.get(o2.id); |
31997 | | - if (!connection) return; |
31998 | | - switch (o2.type) { |
31999 | | - case "line": |
32000 | | - this._updateLine(connection.pointA, connection.pointB, connection.shape); |
32001 | | - break; |
32002 | | - case "curve": |
32003 | | - this._updateCurve(connection.pointA, connection.pointB, connection.shape); |
32004 | | - break; |
32005 | | - case "zigzag": |
32006 | | - this._updateZigzag(connection.pointA, connection.pointB, connection.shape); |
32007 | | - break; |
32008 | | - } |
32009 | | - this._positionDeleteButton(connection); |
32010 | | - this._fireEvent("connector-update", { |
32011 | | - connection, |
32012 | | - type: o2.type |
32013 | | - }); |
32014 | | - }, |
32015 | 32046 | _positionDeleteButton: (connection) => { |
32016 | 32047 | const { type, shape, deleteBtn } = connection; |
32017 | 32048 | if (!deleteBtn || !deleteBtn.length || !shape || !shape.length) return; |
|
32038 | 32069 | const offsetY = 10; |
32039 | 32070 | deleteBtn.attr("transform", `translate(${cx - offsetX}, ${cy - offsetY})`); |
32040 | 32071 | }, |
32041 | | - setType: function(type) { |
32042 | | - if (["line", "curve", "zigzag"].indexOf(type) === -1) { |
32043 | | - console.warn("Connector: \u043D\u0435\u0432\u0456\u0434\u043E\u043C\u0438\u0439 \u0442\u0438\u043F \u0437'\u0454\u0434\u043D\u0430\u043D\u043D\u044F:", type); |
32044 | | - return; |
32045 | | - } |
32046 | | - const o2 = this.options; |
32047 | | - const oldType = o2.type; |
32048 | | - o2.type = type; |
32049 | | - const connection = this.connections.get(o2.id); |
32050 | | - const oldShape = connection?.shape; |
32051 | | - const svg = connection?.svg || this.svgElement || this._getOrCreateSharedSVG(o2.container); |
32052 | | - const newShape = this._createShape(o2.id, type, svg); |
32053 | | - if (oldShape?.length) { |
32054 | | - oldShape.remove(); |
32055 | | - } |
32056 | | - this.connections.set(o2.id, { |
32057 | | - ...connection, |
32058 | | - type, |
32059 | | - old: oldType, |
32060 | | - svg, |
32061 | | - shape: newShape, |
32062 | | - deleteBtn: connection?.deleteBtn |
32063 | | - }); |
32064 | | - this.update(); |
32065 | | - }, |
32066 | | - setPoints: function(pointA, pointB) { |
32067 | | - const o2 = this.options; |
32068 | | - o2.pointA = pointA; |
32069 | | - o2.pointB = pointB; |
32070 | | - this.connections.set(o2.id, { |
32071 | | - ...this.connections.get(o2.id), |
32072 | | - pointA, |
32073 | | - pointB |
32074 | | - }); |
32075 | | - if (o2.autoUpdate) { |
32076 | | - this._cleanupAutoUpdate(); |
32077 | | - this._setupAutoUpdate(); |
32078 | | - } |
32079 | | - this.update(); |
32080 | | - }, |
32081 | 32072 | // Приватні методи оновлення |
32082 | 32073 | _updateLine: (pointA, pointB, shape) => { |
32083 | 32074 | const point1 = $7(pointA); |
@@ -32117,44 +32108,45 @@ |
32117 | 32108 | let cp1x, cp1y, cp2x, cp2y; |
32118 | 32109 | const side1 = parent1.attr("class").match(/(north|south|east|west)-side/)?.[1] || "north"; |
32119 | 32110 | const side2 = parent2.attr("class").match(/(north|south|east|west)-side/)?.[1] || "north"; |
| 32111 | + const magic = 20; |
32120 | 32112 | if (side1 === side2) { |
32121 | 32113 | const controlOffset = Math.max(60, distance * 0.3); |
32122 | 32114 | switch (side1) { |
32123 | 32115 | case "north": |
32124 | 32116 | cp1x = x1; |
32125 | | - cp1y = y1 - controlOffset; |
| 32117 | + cp1y = y1 - controlOffset - magic; |
32126 | 32118 | cp2x = x22; |
32127 | | - cp2y = y22 - controlOffset; |
| 32119 | + cp2y = y22 - controlOffset - magic; |
32128 | 32120 | break; |
32129 | 32121 | case "south": |
32130 | 32122 | cp1x = x1; |
32131 | | - cp1y = y1 + controlOffset; |
| 32123 | + cp1y = y1 + controlOffset + magic; |
32132 | 32124 | cp2x = x22; |
32133 | | - cp2y = y22 + controlOffset; |
| 32125 | + cp2y = y22 + controlOffset + magic; |
32134 | 32126 | break; |
32135 | 32127 | case "east": |
32136 | | - cp1x = x1 + controlOffset; |
| 32128 | + cp1x = x1 + controlOffset - magic; |
32137 | 32129 | cp1y = y1; |
32138 | | - cp2x = x22 + controlOffset; |
| 32130 | + cp2x = x22 + controlOffset - magic; |
32139 | 32131 | cp2y = y22; |
32140 | 32132 | break; |
32141 | 32133 | case "west": |
32142 | | - cp1x = x1 - controlOffset; |
| 32134 | + cp1x = x1 - controlOffset + magic; |
32143 | 32135 | cp1y = y1; |
32144 | | - cp2x = x22 - controlOffset; |
| 32136 | + cp2x = x22 - controlOffset + magic; |
32145 | 32137 | cp2y = y22; |
32146 | 32138 | break; |
32147 | 32139 | } |
32148 | 32140 | } else { |
32149 | 32141 | const direction = this._getDirection(parent1, parent2); |
32150 | 32142 | if (direction === "horizontal") { |
32151 | | - const controlDistance = Math.abs(dx) * 0.4; |
| 32143 | + const controlDistance = Math.abs(dx) * 0.4 + magic; |
32152 | 32144 | cp1x = x1 + (dx > 0 ? controlDistance : -controlDistance); |
32153 | 32145 | cp1y = y1; |
32154 | 32146 | cp2x = x22 - (dx > 0 ? controlDistance : -controlDistance); |
32155 | 32147 | cp2y = y22; |
32156 | 32148 | } else { |
32157 | | - const controlDistance = Math.abs(dy) * 0.4; |
| 32149 | + const controlDistance = Math.abs(dy) * 0.4 + magic; |
32158 | 32150 | cp1x = x1; |
32159 | 32151 | cp1y = y1 + (dy > 0 ? controlDistance : -controlDistance); |
32160 | 32152 | cp2x = x22; |
@@ -32327,10 +32319,119 @@ |
32327 | 32319 | } |
32328 | 32320 | $7(document).off(".connector." + o2.id); |
32329 | 32321 | }, |
| 32322 | + _getStrokeWidth: (shape) => { |
| 32323 | + const sw = parseFloat(getComputedStyle(shape[0]).strokeWidth || "1"); |
| 32324 | + return isFinite(sw) ? sw : 1; |
| 32325 | + }, |
| 32326 | + _getDashArray: function(style, shape) { |
| 32327 | + const sw = this._getStrokeWidth(shape); |
| 32328 | + switch ((style || "").toLowerCase()) { |
| 32329 | + case "dashed": |
| 32330 | + return `${Math.max(4, 6 * sw)} ${Math.max(3, 4 * sw)}`; |
| 32331 | + case "dotted": |
| 32332 | + return `${Math.max(1, 1.5 * sw)} ${Math.max(2, 3 * sw)}`; |
| 32333 | + case "solid": |
| 32334 | + default: |
| 32335 | + return null; |
| 32336 | + } |
| 32337 | + }, |
| 32338 | + _applyLineStyle: function(shape, style) { |
| 32339 | + const dash = this._getDashArray(style, shape); |
| 32340 | + if (dash) { |
| 32341 | + shape.attr("stroke-dasharray", dash); |
| 32342 | + shape.attr("data-line-style", style); |
| 32343 | + } else { |
| 32344 | + shape.removeAttr("stroke-dasharray"); |
| 32345 | + shape.attr("data-line-style", "solid"); |
| 32346 | + } |
| 32347 | + }, |
| 32348 | + // Публічні методи |
| 32349 | + setType: function(type) { |
| 32350 | + if (["line", "curve", "zigzag"].indexOf(type) === -1) { |
| 32351 | + console.warn("Connector: \u043D\u0435\u0432\u0456\u0434\u043E\u043C\u0438\u0439 \u0442\u0438\u043F \u0437'\u0454\u0434\u043D\u0430\u043D\u043D\u044F:", type); |
| 32352 | + return; |
| 32353 | + } |
| 32354 | + const o2 = this.options; |
| 32355 | + const oldType = o2.type; |
| 32356 | + o2.type = type; |
| 32357 | + const connection = this.connections.get(o2.id); |
| 32358 | + const oldShape = connection?.shape; |
| 32359 | + const svg = connection?.svg || this.svgElement || this._getOrCreateSharedSVG(o2.container); |
| 32360 | + const newShape = this._createShape(o2.id, type, svg); |
| 32361 | + if (o2.arrow) { |
| 32362 | + const markerId = this._ensureArrowMarker(svg); |
| 32363 | + newShape.attr("marker-end", `url(#${markerId})`); |
| 32364 | + } |
| 32365 | + this._applyLineStyle(newShape, o2.lineStyle); |
| 32366 | + if (oldShape?.length) { |
| 32367 | + oldShape.remove(); |
| 32368 | + } |
| 32369 | + this.connections.set(o2.id, { |
| 32370 | + ...connection, |
| 32371 | + type, |
| 32372 | + old: oldType, |
| 32373 | + svg, |
| 32374 | + shape: newShape, |
| 32375 | + deleteBtn: connection?.deleteBtn |
| 32376 | + }); |
| 32377 | + this.update(); |
| 32378 | + }, |
| 32379 | + setPoints: function(pointA, pointB) { |
| 32380 | + const o2 = this.options; |
| 32381 | + o2.pointA = pointA; |
| 32382 | + o2.pointB = pointB; |
| 32383 | + this.connections.set(o2.id, { |
| 32384 | + ...this.connections.get(o2.id), |
| 32385 | + pointA, |
| 32386 | + pointB |
| 32387 | + }); |
| 32388 | + if (o2.autoUpdate) { |
| 32389 | + this._cleanupAutoUpdate(); |
| 32390 | + this._setupAutoUpdate(); |
| 32391 | + } |
| 32392 | + this.update(); |
| 32393 | + }, |
| 32394 | + update: function() { |
| 32395 | + const o2 = this.options; |
| 32396 | + const connection = this.connections.get(o2.id); |
| 32397 | + if (!connection) return; |
| 32398 | + switch (o2.type) { |
| 32399 | + case "line": |
| 32400 | + this._updateLine(connection.pointA, connection.pointB, connection.shape); |
| 32401 | + break; |
| 32402 | + case "curve": |
| 32403 | + this._updateCurve(connection.pointA, connection.pointB, connection.shape); |
| 32404 | + break; |
| 32405 | + case "zigzag": |
| 32406 | + this._updateZigzag(connection.pointA, connection.pointB, connection.shape); |
| 32407 | + break; |
| 32408 | + } |
| 32409 | + this._positionDeleteButton(connection); |
| 32410 | + this._fireEvent("connector-update", { |
| 32411 | + connection, |
| 32412 | + type: o2.type |
| 32413 | + }); |
| 32414 | + }, |
| 32415 | + setLineStyle: function(style) { |
| 32416 | + const allowed = ["solid", "dashed", "dotted"]; |
| 32417 | + if (allowed.indexOf((style || "").toLowerCase()) === -1) { |
| 32418 | + console.warn("Connector: \u043D\u0435\u0432\u0456\u0434\u043E\u043C\u0438\u0439 \u0441\u0442\u0438\u043B\u044C \u043B\u0456\u043D\u0456\u0457:", style); |
| 32419 | + return; |
| 32420 | + } |
| 32421 | + const o2 = this.options; |
| 32422 | + o2.lineStyle = style.toLowerCase(); |
| 32423 | + const connection = this.connections.get(o2.id); |
| 32424 | + if (!connection?.shape) return; |
| 32425 | + this._applyLineStyle(connection.shape, o2.lineStyle); |
| 32426 | + this.update(); |
| 32427 | + }, |
32330 | 32428 | changeAttribute: function(attr, newValue) { |
32331 | 32429 | if (attr === "data-type") { |
32332 | 32430 | this.setType(newValue); |
32333 | 32431 | } |
| 32432 | + if (attr === "data-line-style") { |
| 32433 | + this.setLineStyle(newValue); |
| 32434 | + } |
32334 | 32435 | }, |
32335 | 32436 | destroy: function() { |
32336 | 32437 | const o2 = this.options; |
|
0 commit comments