jellies-draw 0.2.0 → 0.2.2
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/package.json +1 -1
- package/src/components/functions/canvas.js +39 -0
- package/src/components/functions/tools/arrow.js +11 -2
- package/src/components/functions/tools/ellipse.js +12 -3
- package/src/components/functions/tools/eraser.js +131 -11
- package/src/components/functions/tools/helper/angleSnap.js +29 -0
- package/src/components/functions/tools/helper/shapeConstraint.js +19 -0
- package/src/components/functions/tools/line.js +11 -2
- package/src/components/functions/tools/rectangle.js +12 -3
- package/src/components/functions/tools/selector.js +37 -2
- package/src/components/functions/transformer.js +90 -5
package/package.json
CHANGED
|
@@ -42,6 +42,7 @@ export default {
|
|
|
42
42
|
window.addEventListener('pointerup', this.handleGlobalPointerUp, { once: true });
|
|
43
43
|
}
|
|
44
44
|
});
|
|
45
|
+
this.stage.on('mousemove', this._handleMouseMove.bind(this));
|
|
45
46
|
},
|
|
46
47
|
generateEventHandlers() {
|
|
47
48
|
this.resizeHandler = this._handleResize.bind(this);
|
|
@@ -193,6 +194,44 @@ export default {
|
|
|
193
194
|
return;
|
|
194
195
|
}
|
|
195
196
|
Transformer.selectNode(targetNode, metaPressed);
|
|
197
|
+
},
|
|
198
|
+
_handleMouseMove(e) {
|
|
199
|
+
if (Properties.isUsingDrawingTool || Properties.isUsingText) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const target = e.target;
|
|
204
|
+
|
|
205
|
+
if (target === this.stage) {
|
|
206
|
+
this.stage.container().style.cursor = 'default';
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (target.hasName('node')) {
|
|
211
|
+
const mousePos = this.stage.getPointerPosition();
|
|
212
|
+
const canInteract = this._canInteractWithNode(target, mousePos);
|
|
213
|
+
|
|
214
|
+
this.stage.container().style.cursor = canInteract ? 'move' : 'default';
|
|
215
|
+
|
|
216
|
+
if (!canInteract) {
|
|
217
|
+
target.draggable(false);
|
|
218
|
+
} else if (!Properties.isUsingDrawingTool) {
|
|
219
|
+
target.draggable(true);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
_canInteractWithNode(node, mousePos) {
|
|
224
|
+
const selectedNodes = Transformer._nodes();
|
|
225
|
+
if (selectedNodes.includes(node)) {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!['Rect', 'Ellipse'].includes(node.className) ||
|
|
230
|
+
(node.attrs.fill && node.attrs.fill !== 'transparent')) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return Transformer._isNearBorder(node, mousePos);
|
|
196
235
|
}
|
|
197
236
|
}
|
|
198
237
|
|
|
@@ -2,6 +2,8 @@ import Konva from 'konva'
|
|
|
2
2
|
import Properties from '../properties'
|
|
3
3
|
import Canvas from '../canvas'
|
|
4
4
|
import Histories from '../histories'
|
|
5
|
+
import AngleSnap from './helper/angleSnap'
|
|
6
|
+
|
|
5
7
|
export default {
|
|
6
8
|
temporalShape: null,
|
|
7
9
|
startX: 0,
|
|
@@ -24,9 +26,16 @@ export default {
|
|
|
24
26
|
this.temporalShape = newArrow
|
|
25
27
|
Canvas.layer.add(this.temporalShape)
|
|
26
28
|
},
|
|
27
|
-
change({ offsetX, offsetY }) {
|
|
29
|
+
change({ offsetX, offsetY, shiftKey }) {
|
|
28
30
|
if (Canvas.isDrawing) {
|
|
29
|
-
|
|
31
|
+
const endPoint = AngleSnap.getSnappedEndPoint(
|
|
32
|
+
this.startX,
|
|
33
|
+
this.startY,
|
|
34
|
+
offsetX,
|
|
35
|
+
offsetY,
|
|
36
|
+
shiftKey
|
|
37
|
+
);
|
|
38
|
+
this.temporalShape.points([this.startX, this.startY, endPoint.x, endPoint.y]);
|
|
30
39
|
}
|
|
31
40
|
},
|
|
32
41
|
finish() {
|
|
@@ -2,6 +2,8 @@ import Konva from 'konva'
|
|
|
2
2
|
import Properties from '../properties'
|
|
3
3
|
import Canvas from '../canvas'
|
|
4
4
|
import Histories from '../histories'
|
|
5
|
+
import ShapeConstraint from './helper/shapeConstraint'
|
|
6
|
+
|
|
5
7
|
export default {
|
|
6
8
|
temporalShape: null,
|
|
7
9
|
startX: 0,
|
|
@@ -26,12 +28,19 @@ export default {
|
|
|
26
28
|
this.temporalShape = newEllipse
|
|
27
29
|
Canvas.layer.add(this.temporalShape)
|
|
28
30
|
},
|
|
29
|
-
change({ offsetX, offsetY }) {
|
|
31
|
+
change({ offsetX, offsetY, shiftKey }) {
|
|
30
32
|
if (Canvas.isDrawing) {
|
|
33
|
+
const { width, height } = ShapeConstraint.getConstrainedSize(
|
|
34
|
+
this.startX,
|
|
35
|
+
this.startY,
|
|
36
|
+
offsetX,
|
|
37
|
+
offsetY,
|
|
38
|
+
shiftKey
|
|
39
|
+
);
|
|
31
40
|
this.temporalShape.x((offsetX + this.startX) / 2);
|
|
32
41
|
this.temporalShape.y((offsetY + this.startY) / 2);
|
|
33
|
-
this.temporalShape.radiusX(Math.abs(
|
|
34
|
-
this.temporalShape.radiusY(Math.abs(
|
|
42
|
+
this.temporalShape.radiusX(Math.abs(width) / 2);
|
|
43
|
+
this.temporalShape.radiusY(Math.abs(height) / 2);
|
|
35
44
|
}
|
|
36
45
|
},
|
|
37
46
|
finish() {
|
|
@@ -26,8 +26,8 @@ export default {
|
|
|
26
26
|
change({ offsetX, offsetY }) {
|
|
27
27
|
if (!Canvas.isDrawing) return
|
|
28
28
|
|
|
29
|
+
this._interpolateAndCheck(this.lastX, this.lastY, offsetX, offsetY)
|
|
29
30
|
this._updateEraserPath(offsetX, offsetY)
|
|
30
|
-
this._checkAndMarkIntersections(offsetX, offsetY)
|
|
31
31
|
|
|
32
32
|
this.lastX = offsetX
|
|
33
33
|
this.lastY = offsetY
|
|
@@ -60,11 +60,21 @@ export default {
|
|
|
60
60
|
},
|
|
61
61
|
|
|
62
62
|
_checkAndMarkIntersections(x, y) {
|
|
63
|
+
const eraserRadius = Properties.strokeWidth
|
|
64
|
+
|
|
63
65
|
Canvas.stage.find('.node').forEach(node => {
|
|
64
66
|
if (node === this.temporalShape) return
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
const nodeRect = node.getClientRect()
|
|
69
|
+
const eraserRect = {
|
|
70
|
+
x: x - eraserRadius,
|
|
71
|
+
y: y - eraserRadius,
|
|
72
|
+
width: eraserRadius * 2,
|
|
73
|
+
height: eraserRadius * 2
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this._checkRectOverlap(nodeRect, eraserRect)) {
|
|
77
|
+
if (this._checkIntersection(node, { x, y }) && !this.erasedNodes.has(node)) {
|
|
68
78
|
this._markNodeForErasure(node)
|
|
69
79
|
}
|
|
70
80
|
}
|
|
@@ -92,24 +102,37 @@ export default {
|
|
|
92
102
|
if (node.attrs.nodeType === 'pen') {
|
|
93
103
|
return this._checkPenIntersection(node, point)
|
|
94
104
|
}
|
|
95
|
-
|
|
105
|
+
|
|
106
|
+
switch (node.className) {
|
|
107
|
+
case 'Line':
|
|
108
|
+
case 'Arrow':
|
|
109
|
+
return this._checkLineIntersection(node, point)
|
|
110
|
+
case 'Ellipse':
|
|
111
|
+
return this._checkEllipseIntersection(node, point)
|
|
112
|
+
case 'Rect':
|
|
113
|
+
return this._checkRectIntersection(node, point)
|
|
114
|
+
default:
|
|
115
|
+
return this._checkDefaultIntersection(node)
|
|
116
|
+
}
|
|
96
117
|
},
|
|
97
118
|
|
|
98
119
|
_checkPenIntersection(node, point) {
|
|
99
120
|
const localPoint = this._transformPointToLocal(node, point)
|
|
100
|
-
const { points
|
|
121
|
+
const { points, widths } = node.attrs
|
|
122
|
+
const eraserRadius = Properties.strokeWidth
|
|
101
123
|
|
|
102
|
-
for (let i = 0; i <
|
|
124
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
103
125
|
const distance = this._getDistanceToLineSegment(
|
|
104
126
|
localPoint.x,
|
|
105
127
|
localPoint.y,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
128
|
+
points[i][0],
|
|
129
|
+
points[i][1],
|
|
130
|
+
points[i + 1][0],
|
|
131
|
+
points[i + 1][1]
|
|
110
132
|
)
|
|
111
133
|
|
|
112
|
-
|
|
134
|
+
const combinedWidth = (widths[i] + eraserRadius) / 2
|
|
135
|
+
if (distance < combinedWidth) {
|
|
113
136
|
return true
|
|
114
137
|
}
|
|
115
138
|
}
|
|
@@ -153,5 +176,102 @@ export default {
|
|
|
153
176
|
const dx = x1 - x2
|
|
154
177
|
const dy = y1 - y2
|
|
155
178
|
return Math.sqrt(dx * dx + dy * dy)
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
_checkLineIntersection(node, point) {
|
|
182
|
+
const points = node.points()
|
|
183
|
+
const transform = node.getTransform()
|
|
184
|
+
const localPoint = transform.copy().invert().point(point)
|
|
185
|
+
const eraserRadius = Properties.strokeWidth
|
|
186
|
+
|
|
187
|
+
for (let i = 0; i < points.length - 2; i += 2) {
|
|
188
|
+
const distance = this._getDistanceToLineSegment(
|
|
189
|
+
localPoint.x,
|
|
190
|
+
localPoint.y,
|
|
191
|
+
points[i],
|
|
192
|
+
points[i + 1],
|
|
193
|
+
points[i + 2],
|
|
194
|
+
points[i + 3]
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
const combinedWidth = (node.strokeWidth() + eraserRadius) / 2
|
|
198
|
+
if (distance < combinedWidth) {
|
|
199
|
+
return true
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return false
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
_checkEllipseIntersection(node, point) {
|
|
206
|
+
const transform = node.getTransform()
|
|
207
|
+
const localPoint = transform.copy().invert().point(point)
|
|
208
|
+
|
|
209
|
+
const radiusX = node.radiusX()
|
|
210
|
+
const radiusY = node.radiusY()
|
|
211
|
+
|
|
212
|
+
// Normalize point to unit circle
|
|
213
|
+
const normalizedX = localPoint.x / radiusX
|
|
214
|
+
const normalizedY = localPoint.y / radiusY
|
|
215
|
+
|
|
216
|
+
// Check if point is within ellipse border
|
|
217
|
+
const distanceFromCenter = Math.sqrt(
|
|
218
|
+
normalizedX * normalizedX + normalizedY * normalizedY
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if (node.attrs.fill && node.attrs.fill !== 'transparent') {
|
|
222
|
+
// For filled ellipses, allow erasing anywhere inside
|
|
223
|
+
return distanceFromCenter <= 1
|
|
224
|
+
}
|
|
225
|
+
// For unfilled ellipses, only erase near the border
|
|
226
|
+
return Math.abs(distanceFromCenter - 1) < Properties.strokeWidth / Math.min(radiusX, radiusY)
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
_checkRectIntersection(node, point) {
|
|
230
|
+
const transform = node.getTransform()
|
|
231
|
+
const localPoint = transform.copy().invert().point(point)
|
|
232
|
+
|
|
233
|
+
const width = node.width()
|
|
234
|
+
const height = node.height()
|
|
235
|
+
const strokeWidth = Properties.strokeWidth
|
|
236
|
+
|
|
237
|
+
if (node.attrs.fill && node.attrs.fill !== 'transparent') {
|
|
238
|
+
// For filled rectangles, allow erasing anywhere inside
|
|
239
|
+
return localPoint.x >= -strokeWidth &&
|
|
240
|
+
localPoint.x <= width + strokeWidth &&
|
|
241
|
+
localPoint.y >= -strokeWidth &&
|
|
242
|
+
localPoint.y <= height + strokeWidth
|
|
243
|
+
}
|
|
244
|
+
// For unfilled rectangles, only erase near the border
|
|
245
|
+
return (Math.abs(localPoint.x) < strokeWidth ||
|
|
246
|
+
Math.abs(localPoint.x - width) < strokeWidth ||
|
|
247
|
+
Math.abs(localPoint.y) < strokeWidth ||
|
|
248
|
+
Math.abs(localPoint.y - height) < strokeWidth) &&
|
|
249
|
+
localPoint.x >= -strokeWidth &&
|
|
250
|
+
localPoint.x <= width + strokeWidth &&
|
|
251
|
+
localPoint.y >= -strokeWidth &&
|
|
252
|
+
localPoint.y <= height + strokeWidth
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
_interpolateAndCheck(x1, y1, x2, y2) {
|
|
256
|
+
const distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
|
|
257
|
+
const steps = Math.ceil(distance / (Properties.strokeWidth / 2))
|
|
258
|
+
|
|
259
|
+
if (steps > 1) {
|
|
260
|
+
for (let i = 1; i < steps; i++) {
|
|
261
|
+
const ratio = i / steps
|
|
262
|
+
const x = x1 + (x2 - x1) * ratio
|
|
263
|
+
const y = y1 + (y2 - y1) * ratio
|
|
264
|
+
this._checkAndMarkIntersections(x, y)
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
this._checkAndMarkIntersections(x2, y2)
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
_checkRectOverlap(rect1, rect2) {
|
|
272
|
+
return !(rect1.x > rect2.x + rect2.width ||
|
|
273
|
+
rect1.x + rect1.width < rect2.x ||
|
|
274
|
+
rect1.y > rect2.y + rect2.height ||
|
|
275
|
+
rect1.y + rect1.height < rect2.y)
|
|
156
276
|
}
|
|
157
277
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
getAngle(startX, startY, endX, endY) {
|
|
3
|
+
return Math.atan2(endY - startY, endX - startX);
|
|
4
|
+
},
|
|
5
|
+
|
|
6
|
+
snapAngle(angle) {
|
|
7
|
+
let degrees = (angle * 180) / Math.PI;
|
|
8
|
+
degrees = (degrees + 360) % 360;
|
|
9
|
+
const snappedDegrees = Math.round(degrees / 15) * 15;
|
|
10
|
+
return (snappedDegrees * Math.PI) / 180;
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
getSnappedEndPoint(startX, startY, endX, endY, isShiftPressed) {
|
|
14
|
+
if (!isShiftPressed) {
|
|
15
|
+
return { x: endX, y: endY };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const angle = this.getAngle(startX, startY, endX, endY);
|
|
19
|
+
const snappedAngle = this.snapAngle(angle);
|
|
20
|
+
const distance = Math.sqrt(
|
|
21
|
+
Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
x: startX + distance * Math.cos(snappedAngle),
|
|
26
|
+
y: startY + distance * Math.sin(snappedAngle)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
getConstrainedSize(startX, startY, currentX, currentY, isShiftPressed) {
|
|
3
|
+
if (!isShiftPressed) {
|
|
4
|
+
return {
|
|
5
|
+
width: currentX - startX,
|
|
6
|
+
height: currentY - startY
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const width = currentX - startX;
|
|
11
|
+
const height = currentY - startY;
|
|
12
|
+
const size = Math.max(Math.abs(width), Math.abs(height));
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
width: size * Math.sign(width),
|
|
16
|
+
height: size * Math.sign(height)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -2,6 +2,8 @@ import Konva from 'konva'
|
|
|
2
2
|
import Properties from '../properties'
|
|
3
3
|
import Canvas from '../canvas'
|
|
4
4
|
import Histories from '../histories'
|
|
5
|
+
import AngleSnap from './helper/angleSnap'
|
|
6
|
+
|
|
5
7
|
export default {
|
|
6
8
|
temporalShape: null,
|
|
7
9
|
startX: 0,
|
|
@@ -20,9 +22,16 @@ export default {
|
|
|
20
22
|
this.temporalShape = newLine
|
|
21
23
|
Canvas.layer.add(this.temporalShape)
|
|
22
24
|
},
|
|
23
|
-
change({ offsetX, offsetY }) {
|
|
25
|
+
change({ offsetX, offsetY, shiftKey }) {
|
|
24
26
|
if (Canvas.isDrawing) {
|
|
25
|
-
|
|
27
|
+
const endPoint = AngleSnap.getSnappedEndPoint(
|
|
28
|
+
this.startX,
|
|
29
|
+
this.startY,
|
|
30
|
+
offsetX,
|
|
31
|
+
offsetY,
|
|
32
|
+
shiftKey
|
|
33
|
+
);
|
|
34
|
+
this.temporalShape.points([this.startX, this.startY, endPoint.x, endPoint.y]);
|
|
26
35
|
}
|
|
27
36
|
},
|
|
28
37
|
finish() {
|
|
@@ -2,6 +2,8 @@ import Konva from 'konva'
|
|
|
2
2
|
import Properties from '../properties'
|
|
3
3
|
import Canvas from '../canvas'
|
|
4
4
|
import Histories from '../histories'
|
|
5
|
+
import ShapeConstraint from './helper/shapeConstraint'
|
|
6
|
+
|
|
5
7
|
export default {
|
|
6
8
|
temporalShape: null,
|
|
7
9
|
startX: 0,
|
|
@@ -26,10 +28,17 @@ export default {
|
|
|
26
28
|
this.temporalShape = newRectangle
|
|
27
29
|
Canvas.layer.add(this.temporalShape)
|
|
28
30
|
},
|
|
29
|
-
change({ offsetX, offsetY }) {
|
|
31
|
+
change({ offsetX, offsetY, shiftKey }) {
|
|
30
32
|
if (Canvas.isDrawing) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
const { width, height } = ShapeConstraint.getConstrainedSize(
|
|
34
|
+
this.startX,
|
|
35
|
+
this.startY,
|
|
36
|
+
offsetX,
|
|
37
|
+
offsetY,
|
|
38
|
+
shiftKey
|
|
39
|
+
);
|
|
40
|
+
this.temporalShape.width(width);
|
|
41
|
+
this.temporalShape.height(height);
|
|
33
42
|
}
|
|
34
43
|
},
|
|
35
44
|
finish() {
|
|
@@ -39,8 +39,43 @@ export default {
|
|
|
39
39
|
return this.selector !== null
|
|
40
40
|
},
|
|
41
41
|
_isInSelector(node) {
|
|
42
|
-
const nodeRect = node.getClientRect()
|
|
43
42
|
const selectorRect = this.selector.getClientRect()
|
|
44
|
-
|
|
43
|
+
|
|
44
|
+
if (node.className === 'Line' || node.className === 'Arrow') {
|
|
45
|
+
return this._isLineFullyContained(node, selectorRect)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return this._isShapeFullyContained(node, selectorRect)
|
|
49
|
+
},
|
|
50
|
+
_isLineFullyContained(node, selectorRect) {
|
|
51
|
+
const points = node.points()
|
|
52
|
+
for (let i = 0; i < points.length; i += 2) {
|
|
53
|
+
const point = node.getAbsoluteTransform().point({
|
|
54
|
+
x: points[i],
|
|
55
|
+
y: points[i + 1]
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (!this._isPointInRect(point, selectorRect)) {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return true
|
|
63
|
+
},
|
|
64
|
+
_isShapeFullyContained(node, selectorRect) {
|
|
65
|
+
const nodeRect = node.getClientRect()
|
|
66
|
+
const corners = [
|
|
67
|
+
{ x: nodeRect.x, y: nodeRect.y }, // top-left
|
|
68
|
+
{ x: nodeRect.x + nodeRect.width, y: nodeRect.y }, // top-right
|
|
69
|
+
{ x: nodeRect.x, y: nodeRect.y + nodeRect.height }, // bottom-left
|
|
70
|
+
{ x: nodeRect.x + nodeRect.width, y: nodeRect.y + nodeRect.height } // bottom-right
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
return corners.every(point => this._isPointInRect(point, selectorRect))
|
|
74
|
+
},
|
|
75
|
+
_isPointInRect(point, rect) {
|
|
76
|
+
return point.x >= rect.x &&
|
|
77
|
+
point.x <= rect.x + rect.width &&
|
|
78
|
+
point.y >= rect.y &&
|
|
79
|
+
point.y <= rect.y + rect.height
|
|
45
80
|
}
|
|
46
81
|
}
|
|
@@ -11,6 +11,15 @@ export default {
|
|
|
11
11
|
},
|
|
12
12
|
initialize() {
|
|
13
13
|
this.transformer = new Konva.Transformer();
|
|
14
|
+
|
|
15
|
+
// 创建一个透明的拖动区域
|
|
16
|
+
this.dragArea = new Konva.Rect({
|
|
17
|
+
fill: 'transparent',
|
|
18
|
+
draggable: true,
|
|
19
|
+
listening: true
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
Canvas.layer.add(this.dragArea);
|
|
14
23
|
Canvas.layer.add(this.transformer);
|
|
15
24
|
},
|
|
16
25
|
copySelectedNodes() {
|
|
@@ -39,6 +48,10 @@ export default {
|
|
|
39
48
|
});
|
|
40
49
|
},
|
|
41
50
|
selectNode(targetNode, isAppending = false) {
|
|
51
|
+
if (!this._shouldSelectNode(targetNode)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
const isSelected = this._nodes().indexOf(targetNode) >= 0 || this.linearTransformer.node === targetNode;
|
|
43
56
|
if (!isAppending && !isSelected) {
|
|
44
57
|
this.selectNodes([targetNode]);
|
|
@@ -64,20 +77,22 @@ export default {
|
|
|
64
77
|
this._selectLinearNode(targetNodes[0]);
|
|
65
78
|
} else {
|
|
66
79
|
this._nodes(targetNodes);
|
|
67
|
-
if (targetNodes.length
|
|
68
|
-
this.
|
|
80
|
+
if (targetNodes.length > 0) {
|
|
81
|
+
this._updateDragArea(targetNodes);
|
|
82
|
+
this._updateTransformerAnchors(targetNodes);
|
|
69
83
|
} else {
|
|
70
|
-
this.
|
|
84
|
+
this.dragArea.visible(false);
|
|
71
85
|
}
|
|
72
|
-
Properties.isTextNodesSelected = this._hasSelectedOnlyTextNodes()
|
|
86
|
+
Properties.isTextNodesSelected = this._hasSelectedOnlyTextNodes();
|
|
73
87
|
}
|
|
74
88
|
Properties.setProperties(this._getCommonPropertiesOfSelectedNodes());
|
|
75
89
|
},
|
|
76
90
|
deselectAllNodes() {
|
|
91
|
+
this.dragArea.visible(false);
|
|
77
92
|
this.selectNodes([]);
|
|
78
93
|
if (this.linearTransformer.node) {
|
|
79
94
|
this.linearTransformer.node.destroy();
|
|
80
|
-
this._removeLinearTransformer()
|
|
95
|
+
this._removeLinearTransformer();
|
|
81
96
|
}
|
|
82
97
|
},
|
|
83
98
|
selectAllNodes() {
|
|
@@ -211,5 +226,75 @@ export default {
|
|
|
211
226
|
return hasOnlyTextNodes;
|
|
212
227
|
}
|
|
213
228
|
return false;
|
|
229
|
+
},
|
|
230
|
+
_shouldSelectNode(node) {
|
|
231
|
+
if ((node.className === 'Rect' || node.className === 'Ellipse') &&
|
|
232
|
+
(!node.attrs.fill || node.attrs.fill === 'transparent')) {
|
|
233
|
+
const stage = Canvas.stage;
|
|
234
|
+
const mousePos = stage.getPointerPosition();
|
|
235
|
+
return this._isNearBorder(node, mousePos);
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
},
|
|
239
|
+
_isNearBorder(node, point) {
|
|
240
|
+
const transform = node.getAbsoluteTransform().copy();
|
|
241
|
+
const localPoint = transform.invert().point(point);
|
|
242
|
+
|
|
243
|
+
const threshold = node.attrs.strokeWidth * 2;
|
|
244
|
+
|
|
245
|
+
if (node.className === 'Rect') {
|
|
246
|
+
const width = node.width();
|
|
247
|
+
const height = node.height();
|
|
248
|
+
|
|
249
|
+
const nearHorizontalEdge = (localPoint.y >= -threshold && localPoint.y <= threshold) ||
|
|
250
|
+
(localPoint.y >= height - threshold && localPoint.y <= height + threshold);
|
|
251
|
+
const nearVerticalEdge = (localPoint.x >= -threshold && localPoint.x <= threshold) ||
|
|
252
|
+
(localPoint.x >= width - threshold && localPoint.x <= width + threshold);
|
|
253
|
+
|
|
254
|
+
return (nearHorizontalEdge && localPoint.x >= -threshold && localPoint.x <= width + threshold) ||
|
|
255
|
+
(nearVerticalEdge && localPoint.y >= -threshold && localPoint.y <= height + threshold);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (node.className === 'Ellipse') {
|
|
259
|
+
const radiusX = node.radiusX();
|
|
260
|
+
const radiusY = node.radiusY();
|
|
261
|
+
|
|
262
|
+
const normalizedX = localPoint.x / radiusX;
|
|
263
|
+
const normalizedY = localPoint.y / radiusY;
|
|
264
|
+
const distanceFromCenter = Math.sqrt(normalizedX * normalizedX + normalizedY * normalizedY);
|
|
265
|
+
|
|
266
|
+
return Math.abs(distanceFromCenter - 1) < threshold / Math.min(radiusX, radiusY);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return true;
|
|
270
|
+
},
|
|
271
|
+
_updateDragArea(targetNodes) {
|
|
272
|
+
const box = this.transformer.getClientRect();
|
|
273
|
+
this.dragArea.setAttrs({
|
|
274
|
+
x: box.x,
|
|
275
|
+
y: box.y,
|
|
276
|
+
width: box.width,
|
|
277
|
+
height: box.height,
|
|
278
|
+
visible: true
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
this.dragArea.off('dragmove');
|
|
282
|
+
this.dragArea.on('dragmove', () => {
|
|
283
|
+
const dx = this.dragArea.x() - box.x;
|
|
284
|
+
const dy = this.dragArea.y() - box.y;
|
|
285
|
+
targetNodes.forEach(node => {
|
|
286
|
+
node.x(node.x() + dx);
|
|
287
|
+
node.y(node.y() + dy);
|
|
288
|
+
});
|
|
289
|
+
box.x = this.dragArea.x();
|
|
290
|
+
box.y = this.dragArea.y();
|
|
291
|
+
});
|
|
292
|
+
},
|
|
293
|
+
_updateTransformerAnchors(targetNodes) {
|
|
294
|
+
if (targetNodes.length === 1) {
|
|
295
|
+
this.transformer.enabledAnchors(['top-left', 'top-center', 'top-right', 'middle-right', 'middle-left', 'bottom-left', 'bottom-center', 'bottom-right']);
|
|
296
|
+
} else {
|
|
297
|
+
this.transformer.enabledAnchors(['top-left', 'top-right', 'bottom-left', 'bottom-right']);
|
|
298
|
+
}
|
|
214
299
|
}
|
|
215
300
|
}
|