w-flow-vue 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +9 -0
- package/.eslintignore +3 -0
- package/.eslintrc.js +55 -0
- package/.jsdoc +25 -0
- package/AGENT.md +223 -0
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/SECURITY.md +5 -0
- package/babel.config.js +16 -0
- package/dist/w-flow-vue.umd.js +15 -0
- package/dist/w-flow-vue.umd.js.map +1 -0
- package/docs/components_WFlowVue.vue.html +1214 -0
- package/docs/examples/app.html +62 -0
- package/docs/examples/app.umd.js +20 -0
- package/docs/examples/app.umd.js.map +1 -0
- package/docs/examples/ex-AppBasic.html +440 -0
- package/docs/examples/ex-AppConnectivity.html +131 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/global.html +1919 -0
- package/docs/index.html +84 -0
- package/docs/js_defaults.mjs.html +105 -0
- package/docs/js_edge-path.mjs.html +237 -0
- package/docs/js_geometry.mjs.html +298 -0
- package/docs/js_graph.mjs.html +103 -0
- package/docs/js_step-routing.mjs.html +346 -0
- package/docs/module-WFlowVue.html +2790 -0
- package/docs/scripts/collapse.js +39 -0
- package/docs/scripts/commonNav.js +28 -0
- package/docs/scripts/linenumber.js +25 -0
- package/docs/scripts/nav.js +12 -0
- package/docs/scripts/polyfill.js +4 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/scripts/prettify/lang-css.js +2 -0
- package/docs/scripts/prettify/prettify.js +28 -0
- package/docs/scripts/search.js +99 -0
- package/docs/styles/jsdoc.css +776 -0
- package/docs/styles/prettify.css +80 -0
- package/jest.config.js +20 -0
- package/package.json +80 -0
- package/public/index.html +38 -0
- package/script.txt +22 -0
- package/src/App.vue +326 -0
- package/src/AppBasic.vue +125 -0
- package/src/AppConnectivity.vue +186 -0
- package/src/components/WFlowVue.vue +1142 -0
- package/src/components/canvas/BackgroundLayer.vue +78 -0
- package/src/components/canvas/FlowCanvas.vue +64 -0
- package/src/components/canvas/SelectionBox.vue +36 -0
- package/src/components/canvas/ViewportTransform.vue +35 -0
- package/src/components/edges/ConnectionLine.vue +65 -0
- package/src/components/edges/EdgeMarkerDefs.vue +76 -0
- package/src/components/edges/EdgeRenderer.vue +120 -0
- package/src/components/edges/EdgeWrapper.vue +379 -0
- package/src/components/nodes/DefaultNode.vue +276 -0
- package/src/components/nodes/Handle.vue +101 -0
- package/src/components/nodes/InputNode.vue +47 -0
- package/src/components/nodes/NodeBody.vue +103 -0
- package/src/components/nodes/NodeFace.vue +128 -0
- package/src/components/nodes/NodeRenderer.vue +95 -0
- package/src/components/nodes/NodeWrapper.vue +475 -0
- package/src/components/nodes/OutputNode.vue +47 -0
- package/src/components/ui/ConnSettingsForm.vue +158 -0
- package/src/components/ui/Controls.vue +83 -0
- package/src/components/ui/NodeSettingsForm.vue +185 -0
- package/src/js/defaults.mjs +33 -0
- package/src/js/edge-path.mjs +165 -0
- package/src/js/geometry.mjs +226 -0
- package/src/js/graph.mjs +31 -0
- package/src/js/step-routing.mjs +274 -0
- package/src/main.js +22 -0
- package/test/WFlowVue-features.test.mjs +760 -0
- package/test/WFlowVue.test.mjs +421 -0
- package/test/components-canvas.test.mjs +102 -0
- package/test/components-edge.test.mjs +147 -0
- package/test/components-node.test.mjs +174 -0
- package/test/components-ui.test.mjs +69 -0
- package/test/defaults.test.mjs +86 -0
- package/test/edge-path.test.mjs +102 -0
- package/test/generate-routing-snapshots.mjs +77 -0
- package/test/generate-visual-baselines.mjs +206 -0
- package/test/geometry.test.mjs +236 -0
- package/test/graph.test.mjs +72 -0
- package/test/jsons/routing-snapshots.json +24994 -0
- package/test/pics/_check2.png +0 -0
- package/test/pics/_check3.png +0 -0
- package/test/pics/_check4.png +0 -0
- package/test/pics/_check5.png +0 -0
- package/test/pics/_v1.png +0 -0
- package/test/pics/_v2.png +0 -0
- package/test/pics/_v3.png +0 -0
- package/test/pics/_v4.png +0 -0
- package/test/pics/_v5.png +0 -0
- package/test/pics/_v6.png +0 -0
- package/test/pics/_v7.png +0 -0
- package/test/pics/vb-edge-hovered.png +0 -0
- package/test/pics/vb-edges-normal.png +0 -0
- package/test/pics/vb-locked-edge-hovered.png +0 -0
- package/test/pics/vb-locked-node-hovered.png +0 -0
- package/test/pics/vb-locked-node-selected.png +0 -0
- package/test/pics/vb-locked-overview.png +0 -0
- package/test/pics/vb-node-1.png +0 -0
- package/test/pics/vb-node-10.png +0 -0
- package/test/pics/vb-node-11.png +0 -0
- package/test/pics/vb-node-12.png +0 -0
- package/test/pics/vb-node-2.png +0 -0
- package/test/pics/vb-node-3.png +0 -0
- package/test/pics/vb-node-4.png +0 -0
- package/test/pics/vb-node-5.png +0 -0
- package/test/pics/vb-node-6.png +0 -0
- package/test/pics/vb-node-7.png +0 -0
- package/test/pics/vb-node-8.png +0 -0
- package/test/pics/vb-node-9.png +0 -0
- package/test/pics/vb-node-hovered.png +0 -0
- package/test/pics/vb-node-selected.png +0 -0
- package/test/pics/vb-overview.png +0 -0
- package/test/step-routing-connectivity.test.mjs +78 -0
- package/test/step-routing.test.mjs +88 -0
- package/test/visual-regression.test.mjs +274 -0
- package/toolg/addVersion.mjs +4 -0
- package/toolg/cleanFolder.mjs +4 -0
- package/toolg/gDistApp.mjs +34 -0
- package/toolg/gDistRollupComps.mjs +22 -0
- package/toolg/gDocExams.mjs +47 -0
- package/toolg/gExtractHtml.mjs +179 -0
- package/toolg/modifyReadme.mjs +4 -0
- package/vue.config.js +9 -0
- package/vue2/344/271/213foreignObject/345/205/247/346/270/262/346/237/223/345/225/217/351/241/214/350/210/207/344/277/256/346/255/243.md +151 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="['vue-flow__controls', 'vue-flow__panel', `vue-flow__panel--${position}`]">
|
|
3
|
+
<template v-if="showZoom">
|
|
4
|
+
<button
|
|
5
|
+
title="Zoom In"
|
|
6
|
+
@click="$emit('zoom-in')"
|
|
7
|
+
>
|
|
8
|
+
<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
|
9
|
+
</button>
|
|
10
|
+
<button
|
|
11
|
+
title="Zoom Out"
|
|
12
|
+
@click="$emit('zoom-out')"
|
|
13
|
+
>
|
|
14
|
+
<svg viewBox="0 0 24 24"><path d="M5 12h14" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
|
15
|
+
</button>
|
|
16
|
+
</template>
|
|
17
|
+
<button
|
|
18
|
+
v-if="showFitView"
|
|
19
|
+
title="Fit View"
|
|
20
|
+
@click="$emit('fit-view')"
|
|
21
|
+
>
|
|
22
|
+
<svg viewBox="0 0 24 24"><path d="M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>
|
|
23
|
+
</button>
|
|
24
|
+
<button
|
|
25
|
+
v-if="showInteractive"
|
|
26
|
+
:title="locked ? 'Unlock' : 'Lock'"
|
|
27
|
+
@click="$emit('toggle-interactive')"
|
|
28
|
+
>
|
|
29
|
+
<svg v-if="locked" viewBox="0 0 24 24"><path d="M17 11H7a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2zM12 17a1.5 1.5 0 110-3 1.5 1.5 0 010 3zM8 11V7a4 4 0 118 0v4" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>
|
|
30
|
+
<svg v-else viewBox="0 0 24 24"><path d="M17 11H7a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2zM12 17a1.5 1.5 0 110-3 1.5 1.5 0 010 3zM16 11V7a4 4 0 10-8 0" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>
|
|
31
|
+
</button>
|
|
32
|
+
<slot />
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script>
|
|
37
|
+
export default {
|
|
38
|
+
name: 'FlowControls',
|
|
39
|
+
props: {
|
|
40
|
+
showZoom: { type: Boolean, default: true },
|
|
41
|
+
showFitView: { type: Boolean, default: true },
|
|
42
|
+
showInteractive: { type: Boolean, default: true },
|
|
43
|
+
locked: { type: Boolean, default: false },
|
|
44
|
+
position: { type: String, default: 'top-left' },
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<style scoped>
|
|
50
|
+
.vue-flow__controls {
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
gap: 4px;
|
|
54
|
+
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.08);
|
|
55
|
+
background: #fefefe;
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
}
|
|
59
|
+
.vue-flow__controls button {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: center;
|
|
63
|
+
width: 26px;
|
|
64
|
+
height: 26px;
|
|
65
|
+
border: none;
|
|
66
|
+
background: #fefefe;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
padding: 4px;
|
|
69
|
+
font-size: 14px;
|
|
70
|
+
color: #333;
|
|
71
|
+
}
|
|
72
|
+
.vue-flow__controls button:hover {
|
|
73
|
+
background: #f0f0f0;
|
|
74
|
+
}
|
|
75
|
+
.vue-flow__panel {
|
|
76
|
+
position: absolute;
|
|
77
|
+
z-index: 5;
|
|
78
|
+
}
|
|
79
|
+
.vue-flow__panel--top-left { top: 10px; left: 10px; }
|
|
80
|
+
.vue-flow__panel--top-right { top: 10px; right: 10px; }
|
|
81
|
+
.vue-flow__panel--bottom-left { bottom: 10px; left: 10px; }
|
|
82
|
+
.vue-flow__panel--bottom-right { bottom: 10px; right: 10px; }
|
|
83
|
+
</style>
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="vue-flow__settings-form" :style="formStyle">
|
|
3
|
+
<label>Name
|
|
4
|
+
<input type="text" :value="node.name || ''" @input="$emit('update', 'name', $event.target.value)">
|
|
5
|
+
</label>
|
|
6
|
+
<label>Description
|
|
7
|
+
<input type="text" :value="node.description || ''" @input="$emit('update', 'description', $event.target.value)">
|
|
8
|
+
</label>
|
|
9
|
+
<label>Type
|
|
10
|
+
<select :value="node.type || defNode.type" @input="$emit('update', 'type', $event.target.value)">
|
|
11
|
+
<option value="input">Input</option>
|
|
12
|
+
<option value="basic">Basic</option>
|
|
13
|
+
<option value="output">Output</option>
|
|
14
|
+
</select>
|
|
15
|
+
</label>
|
|
16
|
+
<label>Shape
|
|
17
|
+
<select :value="node.shape || defNode.shape" @input="$emit('update', 'shape', $event.target.value)">
|
|
18
|
+
<option value="rectangle">Rectangle</option>
|
|
19
|
+
<option value="diamond">Diamond</option>
|
|
20
|
+
<option value="ellipse">Ellipse</option>
|
|
21
|
+
<option value="triangle">Triangle ▲</option>
|
|
22
|
+
<option value="triangle-right">Triangle ▶</option>
|
|
23
|
+
<option value="triangle-down">Triangle ▼</option>
|
|
24
|
+
<option value="triangle-left">Triangle ◀</option>
|
|
25
|
+
</select>
|
|
26
|
+
</label>
|
|
27
|
+
<label>Popup Direction
|
|
28
|
+
<select :value="node.popupDirection || defNode.popupDirection" @input="$emit('update', 'popupDirection', $event.target.value)">
|
|
29
|
+
<option value="top">Top</option>
|
|
30
|
+
<option value="right">Right</option>
|
|
31
|
+
<option value="bottom">Bottom</option>
|
|
32
|
+
<option value="left">Left</option>
|
|
33
|
+
</select>
|
|
34
|
+
</label>
|
|
35
|
+
<label>Font Size
|
|
36
|
+
<input type="number" :value="node.fontSize || defNode.fontSize" :min="defNode.fontSizeMin" :max="defNode.fontSizeMax" @input="onFontSizeInput($event.target.value)">
|
|
37
|
+
</label>
|
|
38
|
+
<label>Font Color
|
|
39
|
+
<WColorSelect :value="node.fontColor || defNode.fontColor" :size="160" :colorBlockSize="16" :showColorText="false" @input="$emit('update', 'fontColor', $event)" />
|
|
40
|
+
</label>
|
|
41
|
+
<label>Face Color
|
|
42
|
+
<WColorSelect :value="node.faceColor || defNode.faceColor" :size="160" :colorBlockSize="16" :showColorText="false" @input="$emit('update', 'faceColor', $event)" />
|
|
43
|
+
</label>
|
|
44
|
+
<label>Edge Color
|
|
45
|
+
<WColorSelect :value="node.edgeColor || defNode.edgeColor" :size="160" :colorBlockSize="16" :showColorText="false" @input="$emit('update', 'edgeColor', $event)" />
|
|
46
|
+
</label>
|
|
47
|
+
<label>Edge Width
|
|
48
|
+
<input type="number" :value="node.edgeWidth !== undefined ? node.edgeWidth : defNode.edgeWidth" min="1" max="24" @input="onEdgeWidthInput($event.target.value)">
|
|
49
|
+
</label>
|
|
50
|
+
<label v-if="node.type === 'output' || node.type === 'basic'">From Handle
|
|
51
|
+
<select :value="node.fromPosition || defNode.fromPosition" @input="$emit('update', 'fromPosition', $event.target.value)">
|
|
52
|
+
<option value="top">Top</option>
|
|
53
|
+
<option value="right">Right</option>
|
|
54
|
+
<option value="bottom">Bottom</option>
|
|
55
|
+
<option value="left">Left</option>
|
|
56
|
+
</select>
|
|
57
|
+
</label>
|
|
58
|
+
<label v-if="node.type === 'input' || node.type === 'basic'">To Handle
|
|
59
|
+
<select :value="node.toPosition || defNode.toPosition" @input="$emit('update', 'toPosition', $event.target.value)">
|
|
60
|
+
<option value="top">Top</option>
|
|
61
|
+
<option value="right">Right</option>
|
|
62
|
+
<option value="bottom">Bottom</option>
|
|
63
|
+
<option value="left">Left</option>
|
|
64
|
+
</select>
|
|
65
|
+
</label>
|
|
66
|
+
<div class="vue-flow__delete-area">
|
|
67
|
+
<button v-if="!confirmDelete" class="vue-flow__delete-btn" @click="confirmDelete = true">刪除節點</button>
|
|
68
|
+
<template v-else>
|
|
69
|
+
<span class="vue-flow__delete-warn">確定刪除?相關連線也會一併刪除</span>
|
|
70
|
+
<div class="vue-flow__delete-confirm-row">
|
|
71
|
+
<button class="vue-flow__delete-btn vue-flow__delete-btn--confirm" @click="$emit('delete')">確認刪除</button>
|
|
72
|
+
<button class="vue-flow__delete-btn vue-flow__delete-btn--cancel" @click="confirmDelete = false">取消</button>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<script>
|
|
80
|
+
import WColorSelect from 'w-component-vue/src/components/WColorSelect.vue'
|
|
81
|
+
|
|
82
|
+
export default {
|
|
83
|
+
components: { WColorSelect },
|
|
84
|
+
props: {
|
|
85
|
+
node: { type: Object, required: true },
|
|
86
|
+
defNode: { type: Object, required: true },
|
|
87
|
+
textFontSize: { type: String, default: '' },
|
|
88
|
+
},
|
|
89
|
+
data() {
|
|
90
|
+
return { confirmDelete: false }
|
|
91
|
+
},
|
|
92
|
+
computed: {
|
|
93
|
+
formStyle() {
|
|
94
|
+
let s = {}
|
|
95
|
+
if (this.textFontSize) s.fontSize = this.textFontSize
|
|
96
|
+
return s
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
methods: {
|
|
100
|
+
onFontSizeInput(val) {
|
|
101
|
+
let n = Number(val)
|
|
102
|
+
let d = this.defNode
|
|
103
|
+
if (!val || isNaN(n) || n < d.fontSizeMin) return
|
|
104
|
+
if (n > d.fontSizeMax) n = d.fontSizeMax
|
|
105
|
+
this.$emit('update', 'fontSize', n)
|
|
106
|
+
},
|
|
107
|
+
onEdgeWidthInput(val) {
|
|
108
|
+
let n = Number(val)
|
|
109
|
+
if (!val || isNaN(n) || n < 1) return
|
|
110
|
+
if (n > 24) n = 24
|
|
111
|
+
this.$emit('update', 'edgeWidth', n)
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<style>
|
|
118
|
+
.vue-flow__settings-form {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-direction: column;
|
|
121
|
+
gap: 8px;
|
|
122
|
+
min-width: 180px;
|
|
123
|
+
}
|
|
124
|
+
.vue-flow__settings-form label {
|
|
125
|
+
display: flex;
|
|
126
|
+
justify-content: space-between;
|
|
127
|
+
align-items: center;
|
|
128
|
+
gap: 8px;
|
|
129
|
+
font-size: 12px;
|
|
130
|
+
}
|
|
131
|
+
.vue-flow__settings-form select,
|
|
132
|
+
.vue-flow__settings-form input[type="number"],
|
|
133
|
+
.vue-flow__settings-form input[type="text"] {
|
|
134
|
+
width: 100px;
|
|
135
|
+
font-size: 12px;
|
|
136
|
+
padding: 1px 4px;
|
|
137
|
+
border: 1px solid #ccc;
|
|
138
|
+
border-radius: 3px;
|
|
139
|
+
}
|
|
140
|
+
.vue-flow__settings-form input[type="color"] {
|
|
141
|
+
width: 32px;
|
|
142
|
+
height: 24px;
|
|
143
|
+
padding: 0;
|
|
144
|
+
border: 1px solid #ccc;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
flex-shrink: 0;
|
|
147
|
+
}
|
|
148
|
+
.vue-flow__delete-area {
|
|
149
|
+
margin-top: 4px;
|
|
150
|
+
padding-top: 8px;
|
|
151
|
+
border-top: 1px solid #eee;
|
|
152
|
+
}
|
|
153
|
+
.vue-flow__delete-warn {
|
|
154
|
+
display: block;
|
|
155
|
+
font-size: 11px;
|
|
156
|
+
color: #c00;
|
|
157
|
+
margin-bottom: 4px;
|
|
158
|
+
}
|
|
159
|
+
.vue-flow__delete-confirm-row {
|
|
160
|
+
display: flex;
|
|
161
|
+
gap: 6px;
|
|
162
|
+
}
|
|
163
|
+
.vue-flow__delete-btn {
|
|
164
|
+
padding: 3px 10px;
|
|
165
|
+
font-size: 11px;
|
|
166
|
+
border: 1px solid #ccc;
|
|
167
|
+
border-radius: 3px;
|
|
168
|
+
background: #fff;
|
|
169
|
+
cursor: pointer;
|
|
170
|
+
}
|
|
171
|
+
.vue-flow__delete-btn:hover {
|
|
172
|
+
background: #f5f5f5;
|
|
173
|
+
}
|
|
174
|
+
.vue-flow__delete-btn--confirm {
|
|
175
|
+
color: #fff;
|
|
176
|
+
background: #dc2626;
|
|
177
|
+
border-color: #dc2626;
|
|
178
|
+
}
|
|
179
|
+
.vue-flow__delete-btn--confirm:hover {
|
|
180
|
+
background: #b91c1c;
|
|
181
|
+
}
|
|
182
|
+
.vue-flow__delete-btn--cancel {
|
|
183
|
+
color: #666;
|
|
184
|
+
}
|
|
185
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default values for node and connection properties.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const NODE_DEFAULTS = {
|
|
6
|
+
type: 'basic',
|
|
7
|
+
shape: 'rectangle',
|
|
8
|
+
width: 100,
|
|
9
|
+
height: 40,
|
|
10
|
+
fontSize: 12,
|
|
11
|
+
fontSizeMin: 1,
|
|
12
|
+
fontSizeMax: 72,
|
|
13
|
+
fontColor: '#333333',
|
|
14
|
+
faceColor: '#ffffff',
|
|
15
|
+
edgeColor: '#bbbbbb',
|
|
16
|
+
edgeWidth: 1,
|
|
17
|
+
toPosition: 'bottom',
|
|
18
|
+
fromPosition: 'top',
|
|
19
|
+
popupDirection: 'right',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const CONN_DEFAULTS = {
|
|
23
|
+
type: 'bezier',
|
|
24
|
+
fontSize: 10,
|
|
25
|
+
fontSizeMin: 1,
|
|
26
|
+
fontSizeMax: 72,
|
|
27
|
+
fontColor: '#333333',
|
|
28
|
+
edgeColor: '#b1b1b7',
|
|
29
|
+
edgeWidth: 1,
|
|
30
|
+
markerEnd: '',
|
|
31
|
+
animated: false,
|
|
32
|
+
defOffset: 24,
|
|
33
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge path generators for different edge types.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { calculateStepPoints, clearStepCache } from './step-routing.mjs'
|
|
6
|
+
|
|
7
|
+
export { clearStepCache }
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Calculate the control point offset for bezier curves based on handle position.
|
|
11
|
+
*/
|
|
12
|
+
function getControlOffset(distance, position) {
|
|
13
|
+
switch (position) {
|
|
14
|
+
case 'top': return { x: 0, y: -distance }
|
|
15
|
+
case 'bottom': return { x: 0, y: distance }
|
|
16
|
+
case 'left': return { x: -distance, y: 0 }
|
|
17
|
+
case 'right': return { x: distance, y: 0 }
|
|
18
|
+
default: return { x: 0, y: 0 }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get bezier curve path.
|
|
24
|
+
* @returns {{ path: string, labelX: number, labelY: number }}
|
|
25
|
+
*/
|
|
26
|
+
export function getBezierPath({
|
|
27
|
+
sourceX, sourceY, sourcePosition = 'bottom',
|
|
28
|
+
targetX, targetY, targetPosition = 'top',
|
|
29
|
+
curvature = 0.25,
|
|
30
|
+
}) {
|
|
31
|
+
const dist = Math.sqrt(Math.pow(targetX - sourceX, 2) + Math.pow(targetY - sourceY, 2))
|
|
32
|
+
const offset = Math.max(dist * curvature, 25)
|
|
33
|
+
|
|
34
|
+
const s = getControlOffset(offset, sourcePosition)
|
|
35
|
+
const t = getControlOffset(offset, targetPosition)
|
|
36
|
+
|
|
37
|
+
const controlX1 = sourceX + s.x
|
|
38
|
+
const controlY1 = sourceY + s.y
|
|
39
|
+
const controlX2 = targetX + t.x
|
|
40
|
+
const controlY2 = targetY + t.y
|
|
41
|
+
|
|
42
|
+
const path = `M ${sourceX},${sourceY} C ${controlX1},${controlY1} ${controlX2},${controlY2} ${targetX},${targetY}`
|
|
43
|
+
|
|
44
|
+
const labelX = (sourceX + controlX1 + controlX2 + targetX) / 4
|
|
45
|
+
const labelY = (sourceY + controlY1 + controlY2 + targetY) / 4
|
|
46
|
+
|
|
47
|
+
return { path, labelX, labelY }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get straight line path.
|
|
52
|
+
* @returns {{ path: string, labelX: number, labelY: number }}
|
|
53
|
+
*/
|
|
54
|
+
export function getStraightPath({ sourceX, sourceY, targetX, targetY }) {
|
|
55
|
+
const path = `M ${sourceX},${sourceY} L ${targetX},${targetY}`
|
|
56
|
+
const labelX = (sourceX + targetX) / 2
|
|
57
|
+
const labelY = (sourceY + targetY) / 2
|
|
58
|
+
return { path, labelX, labelY }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get step (right-angle) path.
|
|
63
|
+
* @returns {{ path: string, labelX: number, labelY: number }}
|
|
64
|
+
*/
|
|
65
|
+
export function getStepPath({
|
|
66
|
+
sourceX, sourceY, sourcePosition = 'bottom',
|
|
67
|
+
targetX, targetY, targetPosition = 'top',
|
|
68
|
+
offset = 20,
|
|
69
|
+
allNodes, nodeInternals, connFromId, connToId,
|
|
70
|
+
}) {
|
|
71
|
+
const points = calculateStepPoints(
|
|
72
|
+
sourceX, sourceY, sourcePosition,
|
|
73
|
+
targetX, targetY, targetPosition,
|
|
74
|
+
offset, allNodes, nodeInternals, connFromId, connToId
|
|
75
|
+
)
|
|
76
|
+
const path = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x},${p.y}`).join(' ')
|
|
77
|
+
const label = labelAtHalfLength(points)
|
|
78
|
+
return { path, labelX: label.x, labelY: label.y }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get smooth step (rounded right-angle) path.
|
|
83
|
+
* @returns {{ path: string, labelX: number, labelY: number }}
|
|
84
|
+
*/
|
|
85
|
+
export function getSmoothStepPath({
|
|
86
|
+
sourceX, sourceY, sourcePosition = 'bottom',
|
|
87
|
+
targetX, targetY, targetPosition = 'top',
|
|
88
|
+
borderRadius = 5,
|
|
89
|
+
offset = 20,
|
|
90
|
+
allNodes, nodeInternals, connFromId, connToId,
|
|
91
|
+
}) {
|
|
92
|
+
const points = calculateStepPoints(
|
|
93
|
+
sourceX, sourceY, sourcePosition,
|
|
94
|
+
targetX, targetY, targetPosition,
|
|
95
|
+
offset, allNodes, nodeInternals, connFromId, connToId
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if (points.length <= 2) {
|
|
99
|
+
const path = `M ${points[0].x},${points[0].y} L ${points[1].x},${points[1].y}`
|
|
100
|
+
const labelX = (points[0].x + points[1].x) / 2
|
|
101
|
+
const labelY = (points[0].y + points[1].y) / 2
|
|
102
|
+
return { path, labelX, labelY }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let path = `M ${points[0].x},${points[0].y}`
|
|
106
|
+
|
|
107
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
108
|
+
const prev = points[i - 1]
|
|
109
|
+
const curr = points[i]
|
|
110
|
+
const next = points[i + 1]
|
|
111
|
+
|
|
112
|
+
const dx1 = curr.x - prev.x
|
|
113
|
+
const dy1 = curr.y - prev.y
|
|
114
|
+
const dx2 = next.x - curr.x
|
|
115
|
+
const dy2 = next.y - curr.y
|
|
116
|
+
|
|
117
|
+
const len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1)
|
|
118
|
+
const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2)
|
|
119
|
+
|
|
120
|
+
if (len1 === 0 || len2 === 0) {
|
|
121
|
+
// Zero-length segment — skip rounding, just draw straight line
|
|
122
|
+
path += ` L ${curr.x},${curr.y}`
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const r = Math.min(borderRadius, len1 / 2, len2 / 2)
|
|
127
|
+
|
|
128
|
+
const beforeX = curr.x - (dx1 / len1) * r
|
|
129
|
+
const beforeY = curr.y - (dy1 / len1) * r
|
|
130
|
+
const afterX = curr.x + (dx2 / len2) * r
|
|
131
|
+
const afterY = curr.y + (dy2 / len2) * r
|
|
132
|
+
|
|
133
|
+
path += ` L ${beforeX},${beforeY} Q ${curr.x},${curr.y} ${afterX},${afterY}`
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
path += ` L ${points[points.length - 1].x},${points[points.length - 1].y}`
|
|
137
|
+
|
|
138
|
+
const label = labelAtHalfLength(points)
|
|
139
|
+
return { path, labelX: label.x, labelY: label.y }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Find the point at exactly half the total Manhattan path length.
|
|
144
|
+
*/
|
|
145
|
+
function labelAtHalfLength(pts) {
|
|
146
|
+
if (pts.length < 2) return { x: pts[0].x, y: pts[0].y }
|
|
147
|
+
let totalLen = 0
|
|
148
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
149
|
+
totalLen += Math.abs(pts[i + 1].x - pts[i].x) + Math.abs(pts[i + 1].y - pts[i].y)
|
|
150
|
+
}
|
|
151
|
+
let half = totalLen / 2
|
|
152
|
+
let acc = 0
|
|
153
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
154
|
+
let segLen = Math.abs(pts[i + 1].x - pts[i].x) + Math.abs(pts[i + 1].y - pts[i].y)
|
|
155
|
+
if (acc + segLen >= half) {
|
|
156
|
+
let ratio = segLen > 0 ? (half - acc) / segLen : 0
|
|
157
|
+
return {
|
|
158
|
+
x: pts[i].x + (pts[i + 1].x - pts[i].x) * ratio,
|
|
159
|
+
y: pts[i].y + (pts[i + 1].y - pts[i].y) * ratio,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
acc += segLen
|
|
163
|
+
}
|
|
164
|
+
return { x: pts[pts.length - 1].x, y: pts[pts.length - 1].y }
|
|
165
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculate the absolute position of a handle on the canvas.
|
|
3
|
+
*/
|
|
4
|
+
export function getHandlePosition(node, handlePosition, nodeInternals, handleType) {
|
|
5
|
+
const internals = nodeInternals || {}
|
|
6
|
+
const w = (internals.width) || node.width || 150
|
|
7
|
+
const h = (internals.height) || node.height || 40
|
|
8
|
+
const x = node.position.x
|
|
9
|
+
const y = node.position.y
|
|
10
|
+
const isDiamond = node.shape === 'diamond'
|
|
11
|
+
const isEllipse = node.shape === 'ellipse'
|
|
12
|
+
const ns = node.shape
|
|
13
|
+
const isTriangle = ns === 'triangle' || ns === 'triangle-right' || ns === 'triangle-down' || ns === 'triangle-left'
|
|
14
|
+
|
|
15
|
+
// Check if this is a default node with source and target on the same side
|
|
16
|
+
const sameSide = node.type === 'basic' &&
|
|
17
|
+
(node.toPosition || 'bottom') === (node.fromPosition || 'top')
|
|
18
|
+
let ratio = 0.5
|
|
19
|
+
if (sameSide && handleType === 'target') ratio = 0.33
|
|
20
|
+
if (sameSide && handleType === 'source') ratio = 0.67
|
|
21
|
+
|
|
22
|
+
// Diamond same-side: position along diamond edges
|
|
23
|
+
if (isDiamond && sameSide) {
|
|
24
|
+
return getDiamondEdgePoint(x, y, w, h, handlePosition, ratio)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Ellipse: position on the ellipse border
|
|
28
|
+
if (isEllipse) {
|
|
29
|
+
return getEllipseEdgePoint(x, y, w, h, handlePosition, ratio)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Triangle: position on the triangle edges
|
|
33
|
+
if (isTriangle) {
|
|
34
|
+
return getTriangleEdgePoint(x, y, w, h, handlePosition, ratio, ns)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
switch (handlePosition) {
|
|
38
|
+
case 'top': return { x: x + w * ratio, y }
|
|
39
|
+
case 'bottom': return { x: x + w * ratio, y: y + h }
|
|
40
|
+
case 'left': return { x, y: y + h * ratio }
|
|
41
|
+
case 'right': return { x: x + w, y: y + h * ratio }
|
|
42
|
+
default: return { x: x + w / 2, y: y + h }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get a point on the diamond edge for same-side handles.
|
|
48
|
+
* Each "side" of the diamond is split into two edges meeting at the vertex.
|
|
49
|
+
* ratio < 0.5: on the first edge, ratio >= 0.5: on the second edge.
|
|
50
|
+
*/
|
|
51
|
+
function getDiamondEdgePoint(x, y, w, h, side, ratio) {
|
|
52
|
+
let halfW = w / 2
|
|
53
|
+
let halfH = h / 2
|
|
54
|
+
|
|
55
|
+
switch (side) {
|
|
56
|
+
case 'top':
|
|
57
|
+
if (ratio <= 0.5) {
|
|
58
|
+
let t = ratio * 2
|
|
59
|
+
return { x: x + t * halfW, y: y + halfH - t * halfH }
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
let t = (ratio - 0.5) * 2
|
|
63
|
+
return { x: x + halfW + t * halfW, y: y + t * halfH }
|
|
64
|
+
}
|
|
65
|
+
case 'bottom':
|
|
66
|
+
if (ratio <= 0.5) {
|
|
67
|
+
let t = ratio * 2
|
|
68
|
+
return { x: x + t * halfW, y: y + halfH + t * halfH }
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
let t = (ratio - 0.5) * 2
|
|
72
|
+
return { x: x + halfW + t * halfW, y: y + h - t * halfH }
|
|
73
|
+
}
|
|
74
|
+
case 'left':
|
|
75
|
+
if (ratio <= 0.5) {
|
|
76
|
+
let t = ratio * 2
|
|
77
|
+
return { x: x + halfW - t * halfW, y: y + t * halfH }
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
let t = (ratio - 0.5) * 2
|
|
81
|
+
return { x: x + t * halfW, y: y + halfH + t * halfH }
|
|
82
|
+
}
|
|
83
|
+
case 'right':
|
|
84
|
+
if (ratio <= 0.5) {
|
|
85
|
+
let t = ratio * 2
|
|
86
|
+
return { x: x + halfW + t * halfW, y: y + t * halfH }
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
let t = (ratio - 0.5) * 2
|
|
90
|
+
return { x: x + w - t * halfW, y: y + halfH + t * halfH }
|
|
91
|
+
}
|
|
92
|
+
default:
|
|
93
|
+
return { x: x + halfW, y: y + h }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get a point on the ellipse edge for handle positioning.
|
|
99
|
+
* Maps ratio (0..1) to a parametric angle based on the side.
|
|
100
|
+
*/
|
|
101
|
+
function getEllipseEdgePoint(x, y, w, h, side, ratio) {
|
|
102
|
+
let cx = x + w / 2
|
|
103
|
+
let cy = y + h / 2
|
|
104
|
+
let rx = w / 2
|
|
105
|
+
let ry = h / 2
|
|
106
|
+
let angle
|
|
107
|
+
|
|
108
|
+
switch (side) {
|
|
109
|
+
case 'top':
|
|
110
|
+
angle = Math.PI * (1 - ratio); break
|
|
111
|
+
case 'bottom':
|
|
112
|
+
angle = Math.PI * (ratio - 1); break
|
|
113
|
+
case 'left':
|
|
114
|
+
angle = Math.PI * (0.5 + ratio); break
|
|
115
|
+
case 'right':
|
|
116
|
+
angle = Math.PI * (0.5 - ratio); break
|
|
117
|
+
default:
|
|
118
|
+
angle = 0
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
x: cx + rx * Math.cos(angle),
|
|
123
|
+
y: cy - ry * Math.sin(angle)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get a point on the triangle edge for handle positioning.
|
|
129
|
+
* Supports 4 directions: triangle (up), triangle-right, triangle-down, triangle-left.
|
|
130
|
+
*/
|
|
131
|
+
function getTriangleEdgePoint(x, y, w, h, side, ratio, shape) {
|
|
132
|
+
// Get absolute vertices based on direction
|
|
133
|
+
let apex, baseA, baseB, apexSide, baseSide, edgeA, edgeB
|
|
134
|
+
if (shape === 'triangle-right') {
|
|
135
|
+
apex = { x: x + w, y: y + h / 2 }; baseA = { x, y }; baseB = { x, y: y + h }
|
|
136
|
+
apexSide = 'right'; baseSide = 'left'; edgeA = 'top'; edgeB = 'bottom'
|
|
137
|
+
}
|
|
138
|
+
else if (shape === 'triangle-down') {
|
|
139
|
+
apex = { x: x + w / 2, y: y + h }; baseA = { x, y }; baseB = { x: x + w, y }
|
|
140
|
+
apexSide = 'bottom'; baseSide = 'top'; edgeA = 'left'; edgeB = 'right'
|
|
141
|
+
}
|
|
142
|
+
else if (shape === 'triangle-left') {
|
|
143
|
+
apex = { x, y: y + h / 2 }; baseA = { x: x + w, y }; baseB = { x: x + w, y: y + h }
|
|
144
|
+
apexSide = 'left'; baseSide = 'right'; edgeA = 'top'; edgeB = 'bottom'
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
// triangle (up)
|
|
148
|
+
apex = { x: x + w / 2, y }; baseA = { x, y: y + h }; baseB = { x: x + w, y: y + h }
|
|
149
|
+
apexSide = 'top'; baseSide = 'bottom'; edgeA = 'left'; edgeB = 'right'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (side === apexSide) {
|
|
153
|
+
if (ratio <= 0.5) {
|
|
154
|
+
let t = ratio * 2
|
|
155
|
+
return { x: baseA.x + (apex.x - baseA.x) * t, y: baseA.y + (apex.y - baseA.y) * t }
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
let t2 = (ratio - 0.5) * 2
|
|
159
|
+
return { x: apex.x + (baseB.x - apex.x) * t2, y: apex.y + (baseB.y - apex.y) * t2 }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (side === baseSide) {
|
|
163
|
+
return { x: baseA.x + (baseB.x - baseA.x) * ratio, y: baseA.y + (baseB.y - baseA.y) * ratio }
|
|
164
|
+
}
|
|
165
|
+
if (side === edgeA) {
|
|
166
|
+
return { x: apex.x + (baseA.x - apex.x) * ratio, y: apex.y + (baseA.y - apex.y) * ratio }
|
|
167
|
+
}
|
|
168
|
+
if (side === edgeB) {
|
|
169
|
+
return { x: apex.x + (baseB.x - apex.x) * ratio, y: apex.y + (baseB.y - apex.y) * ratio }
|
|
170
|
+
}
|
|
171
|
+
return { x: x + w / 2, y: y + h / 2 }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get all nodes that overlap with a given rectangle.
|
|
176
|
+
*/
|
|
177
|
+
export function getOverlappingNodes(rect, nodes, nodeInternals) {
|
|
178
|
+
return nodes.filter(node => {
|
|
179
|
+
const internals = (nodeInternals && nodeInternals[node.id]) || {}
|
|
180
|
+
const w = internals.width || node.width || 150
|
|
181
|
+
const h = internals.height || node.height || 40
|
|
182
|
+
const nodeRect = {
|
|
183
|
+
x: node.position.x,
|
|
184
|
+
y: node.position.y,
|
|
185
|
+
width: w,
|
|
186
|
+
height: h,
|
|
187
|
+
}
|
|
188
|
+
return rectsOverlap(rect, nodeRect)
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Check if two rectangles overlap.
|
|
194
|
+
*/
|
|
195
|
+
function rectsOverlap(a, b) {
|
|
196
|
+
return (
|
|
197
|
+
a.x < b.x + b.width &&
|
|
198
|
+
a.x + a.width > b.x &&
|
|
199
|
+
a.y < b.y + b.height &&
|
|
200
|
+
a.y + a.height > b.y
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Clamp a position within a coordinate extent.
|
|
206
|
+
*/
|
|
207
|
+
export function clampPosition(position, extent) {
|
|
208
|
+
if (!extent) return position
|
|
209
|
+
return {
|
|
210
|
+
x: Math.max(extent[0][0], Math.min(extent[1][0], position.x)),
|
|
211
|
+
y: Math.max(extent[0][1], Math.min(extent[1][1], position.y)),
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Snap a position to the nearest grid point.
|
|
217
|
+
* @param {{ x: number, y: number }} position
|
|
218
|
+
* @param {number|null} gridSize - Grid cell size (single number for both axes)
|
|
219
|
+
*/
|
|
220
|
+
export function snapPosition(position, gridSize) {
|
|
221
|
+
if (!gridSize) return position
|
|
222
|
+
return {
|
|
223
|
+
x: Math.round(position.x / gridSize) * gridSize,
|
|
224
|
+
y: Math.round(position.y / gridSize) * gridSize,
|
|
225
|
+
}
|
|
226
|
+
}
|