jellies-draw 0.2.0 → 0.2.1
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
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,43 @@ 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
|
+
// Reset cursor to default when over stage
|
|
206
|
+
if (target === this.stage) {
|
|
207
|
+
this.stage.container().style.cursor = 'default';
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Handle cursor for nodes
|
|
212
|
+
if (target.hasName('node')) {
|
|
213
|
+
if (this._shouldShowMoveCursor(target)) {
|
|
214
|
+
this.stage.container().style.cursor = 'move';
|
|
215
|
+
} else {
|
|
216
|
+
this.stage.container().style.cursor = 'default';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
_shouldShowMoveCursor(node) {
|
|
221
|
+
// Always show move cursor for non-shape nodes
|
|
222
|
+
if (!['Rect', 'Circle', 'Ellipse'].includes(node.className)) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Show move cursor for filled shapes
|
|
227
|
+
if (node.attrs.fill && node.attrs.fill !== 'transparent') {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// For unfilled shapes, only show move cursor near border
|
|
232
|
+
const mousePos = this.stage.getPointerPosition();
|
|
233
|
+
return Transformer._isNearBorder(node, mousePos);
|
|
196
234
|
}
|
|
197
235
|
}
|
|
198
236
|
|
|
@@ -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,38 @@ 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 'Circle':
|
|
111
|
+
case 'Ellipse':
|
|
112
|
+
return this._checkEllipseIntersection(node, point)
|
|
113
|
+
case 'Rect':
|
|
114
|
+
return this._checkRectIntersection(node, point)
|
|
115
|
+
default:
|
|
116
|
+
return this._checkDefaultIntersection(node)
|
|
117
|
+
}
|
|
96
118
|
},
|
|
97
119
|
|
|
98
120
|
_checkPenIntersection(node, point) {
|
|
99
121
|
const localPoint = this._transformPointToLocal(node, point)
|
|
100
|
-
const { points
|
|
122
|
+
const { points, widths } = node.attrs
|
|
123
|
+
const eraserRadius = Properties.strokeWidth
|
|
101
124
|
|
|
102
|
-
for (let i = 0; i <
|
|
125
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
103
126
|
const distance = this._getDistanceToLineSegment(
|
|
104
127
|
localPoint.x,
|
|
105
128
|
localPoint.y,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
points[i][0],
|
|
130
|
+
points[i][1],
|
|
131
|
+
points[i + 1][0],
|
|
132
|
+
points[i + 1][1]
|
|
110
133
|
)
|
|
111
134
|
|
|
112
|
-
|
|
135
|
+
const combinedWidth = (widths[i] + eraserRadius) / 2
|
|
136
|
+
if (distance < combinedWidth) {
|
|
113
137
|
return true
|
|
114
138
|
}
|
|
115
139
|
}
|
|
@@ -153,5 +177,102 @@ export default {
|
|
|
153
177
|
const dx = x1 - x2
|
|
154
178
|
const dy = y1 - y2
|
|
155
179
|
return Math.sqrt(dx * dx + dy * dy)
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
_checkLineIntersection(node, point) {
|
|
183
|
+
const points = node.points()
|
|
184
|
+
const transform = node.getTransform()
|
|
185
|
+
const localPoint = transform.copy().invert().point(point)
|
|
186
|
+
const eraserRadius = Properties.strokeWidth
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < points.length - 2; i += 2) {
|
|
189
|
+
const distance = this._getDistanceToLineSegment(
|
|
190
|
+
localPoint.x,
|
|
191
|
+
localPoint.y,
|
|
192
|
+
points[i],
|
|
193
|
+
points[i + 1],
|
|
194
|
+
points[i + 2],
|
|
195
|
+
points[i + 3]
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
const combinedWidth = (node.strokeWidth() + eraserRadius) / 2
|
|
199
|
+
if (distance < combinedWidth) {
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return false
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
_checkEllipseIntersection(node, point) {
|
|
207
|
+
const transform = node.getTransform()
|
|
208
|
+
const localPoint = transform.copy().invert().point(point)
|
|
209
|
+
|
|
210
|
+
const radiusX = node.radiusX()
|
|
211
|
+
const radiusY = node.radiusY()
|
|
212
|
+
|
|
213
|
+
// Normalize point to unit circle
|
|
214
|
+
const normalizedX = localPoint.x / radiusX
|
|
215
|
+
const normalizedY = localPoint.y / radiusY
|
|
216
|
+
|
|
217
|
+
// Check if point is within ellipse border
|
|
218
|
+
const distanceFromCenter = Math.sqrt(
|
|
219
|
+
normalizedX * normalizedX + normalizedY * normalizedY
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if (node.attrs.fill && node.attrs.fill !== 'transparent') {
|
|
223
|
+
// For filled ellipses, allow erasing anywhere inside
|
|
224
|
+
return distanceFromCenter <= 1
|
|
225
|
+
}
|
|
226
|
+
// For unfilled ellipses, only erase near the border
|
|
227
|
+
return Math.abs(distanceFromCenter - 1) < Properties.strokeWidth / Math.min(radiusX, radiusY)
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
_checkRectIntersection(node, point) {
|
|
231
|
+
const transform = node.getTransform()
|
|
232
|
+
const localPoint = transform.copy().invert().point(point)
|
|
233
|
+
|
|
234
|
+
const width = node.width()
|
|
235
|
+
const height = node.height()
|
|
236
|
+
const strokeWidth = Properties.strokeWidth
|
|
237
|
+
|
|
238
|
+
if (node.attrs.fill && node.attrs.fill !== 'transparent') {
|
|
239
|
+
// For filled rectangles, allow erasing anywhere inside
|
|
240
|
+
return localPoint.x >= -strokeWidth &&
|
|
241
|
+
localPoint.x <= width + strokeWidth &&
|
|
242
|
+
localPoint.y >= -strokeWidth &&
|
|
243
|
+
localPoint.y <= height + strokeWidth
|
|
244
|
+
}
|
|
245
|
+
// For unfilled rectangles, only erase near the border
|
|
246
|
+
return (Math.abs(localPoint.x) < strokeWidth ||
|
|
247
|
+
Math.abs(localPoint.x - width) < strokeWidth ||
|
|
248
|
+
Math.abs(localPoint.y) < strokeWidth ||
|
|
249
|
+
Math.abs(localPoint.y - height) < strokeWidth) &&
|
|
250
|
+
localPoint.x >= -strokeWidth &&
|
|
251
|
+
localPoint.x <= width + strokeWidth &&
|
|
252
|
+
localPoint.y >= -strokeWidth &&
|
|
253
|
+
localPoint.y <= height + strokeWidth
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
_interpolateAndCheck(x1, y1, x2, y2) {
|
|
257
|
+
const distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
|
|
258
|
+
const steps = Math.ceil(distance / (Properties.strokeWidth / 2))
|
|
259
|
+
|
|
260
|
+
if (steps > 1) {
|
|
261
|
+
for (let i = 1; i < steps; i++) {
|
|
262
|
+
const ratio = i / steps
|
|
263
|
+
const x = x1 + (x2 - x1) * ratio
|
|
264
|
+
const y = y1 + (y2 - y1) * ratio
|
|
265
|
+
this._checkAndMarkIntersections(x, y)
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
this._checkAndMarkIntersections(x2, y2)
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
_checkRectOverlap(rect1, rect2) {
|
|
273
|
+
return !(rect1.x > rect2.x + rect2.width ||
|
|
274
|
+
rect1.x + rect1.width < rect2.x ||
|
|
275
|
+
rect1.y > rect2.y + rect2.height ||
|
|
276
|
+
rect1.y + rect1.height < rect2.y)
|
|
156
277
|
}
|
|
157
278
|
}
|
|
@@ -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
|
}
|
|
@@ -39,6 +39,10 @@ export default {
|
|
|
39
39
|
});
|
|
40
40
|
},
|
|
41
41
|
selectNode(targetNode, isAppending = false) {
|
|
42
|
+
if (!this._shouldSelectNode(targetNode)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
42
46
|
const isSelected = this._nodes().indexOf(targetNode) >= 0 || this.linearTransformer.node === targetNode;
|
|
43
47
|
if (!isAppending && !isSelected) {
|
|
44
48
|
this.selectNodes([targetNode]);
|
|
@@ -211,5 +215,44 @@ export default {
|
|
|
211
215
|
return hasOnlyTextNodes;
|
|
212
216
|
}
|
|
213
217
|
return false;
|
|
218
|
+
},
|
|
219
|
+
_shouldSelectNode(node) {
|
|
220
|
+
if ((node.className === 'Rect' || node.className === 'Circle' || node.className === 'Ellipse') &&
|
|
221
|
+
(!node.attrs.fill || node.attrs.fill === 'transparent')) {
|
|
222
|
+
const stage = Canvas.stage;
|
|
223
|
+
const mousePos = stage.getPointerPosition();
|
|
224
|
+
return this._isNearBorder(node, mousePos);
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
},
|
|
228
|
+
_isNearBorder(node, point) {
|
|
229
|
+
const localPoint = node.getAbsoluteTransform().copy().invert().point(point);
|
|
230
|
+
const threshold = Properties.strokeWidth * 2;
|
|
231
|
+
|
|
232
|
+
if (node.className === 'Rect') {
|
|
233
|
+
const width = node.width();
|
|
234
|
+
const height = node.height();
|
|
235
|
+
|
|
236
|
+
return (Math.abs(localPoint.x) < threshold ||
|
|
237
|
+
Math.abs(localPoint.x - width) < threshold ||
|
|
238
|
+
Math.abs(localPoint.y) < threshold ||
|
|
239
|
+
Math.abs(localPoint.y - height) < threshold) &&
|
|
240
|
+
localPoint.x >= -threshold &&
|
|
241
|
+
localPoint.x <= width + threshold &&
|
|
242
|
+
localPoint.y >= -threshold &&
|
|
243
|
+
localPoint.y <= height + threshold;
|
|
244
|
+
} else if (node.className === 'Circle' || node.className === 'Ellipse') {
|
|
245
|
+
const radiusX = node.radiusX ? node.radiusX() : node.radius();
|
|
246
|
+
const radiusY = node.radiusY ? node.radiusY() : node.radius();
|
|
247
|
+
|
|
248
|
+
const normalizedX = localPoint.x / radiusX;
|
|
249
|
+
const normalizedY = localPoint.y / radiusY;
|
|
250
|
+
|
|
251
|
+
const distanceFromCenter = Math.sqrt(
|
|
252
|
+
normalizedX * normalizedX + normalizedY * normalizedY
|
|
253
|
+
);
|
|
254
|
+
return Math.abs(distanceFromCenter - 1) < threshold / Math.min(radiusX, radiusY);
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
214
257
|
}
|
|
215
258
|
}
|