Skip to content

Commit 6da41a3

Browse files
committed
Implement more intuitive line shapes movement and resizing [2038]
- Reason: until now line shapes were treated the same as rectangles and circles with the result that a line's start or end point couldn't be dragged freely but only in the quadrant where it was located. - Use of invisible helper elements to make grabbing a line and its ends easier. - Adapt shapes's Jasmine tests and add new ones. - Add an (editable) image test serving as an interactive testbed.
1 parent 776ddca commit 6da41a3

File tree

4 files changed

+484
-52
lines changed

4 files changed

+484
-52
lines changed

src/components/shapes/draw.js

Lines changed: 105 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -118,26 +118,34 @@ function drawOne(gd, index) {
118118
null
119119
);
120120

121-
if(gd._context.edits.shapePosition) setupDragElement(gd, path, options, index);
121+
if(gd._context.edits.shapePosition) setupDragElement(gd, path, options, index, shapeLayer);
122122
}
123123
}
124124

125-
function setupDragElement(gd, shapePath, shapeOptions, index) {
125+
function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) {
126126
var MINWIDTH = 10,
127127
MINHEIGHT = 10;
128128

129129
var xPixelSized = shapeOptions.xsizemode === 'pixel',
130-
yPixelSized = shapeOptions.ysizemode === 'pixel';
130+
yPixelSized = shapeOptions.ysizemode === 'pixel',
131+
isLine = shapeOptions.type === 'line';
131132

132133
var update;
133134
var x0, y0, x1, y1, xAnchor, yAnchor, astrX0, astrY0, astrX1, astrY1, astrXAnchor, astrYAnchor;
134135
var n0, s0, w0, e0, astrN, astrS, astrW, astrE, optN, optS, optW, optE;
135136
var pathIn, astrPath;
136137

137-
var xa, ya, x2p, y2p, p2x, p2y;
138+
// setup conversion functions
139+
var xa = Axes.getFromId(gd, shapeOptions.xref),
140+
ya = Axes.getFromId(gd, shapeOptions.yref),
141+
x2p = helpers.getDataToPixel(gd, xa),
142+
y2p = helpers.getDataToPixel(gd, ya, true),
143+
p2x = helpers.getPixelToData(gd, xa),
144+
p2y = helpers.getPixelToData(gd, ya, true);
138145

146+
var sensoryElement = obtainSensoryElement();
139147
var dragOptions = {
140-
element: shapePath.node(),
148+
element: sensoryElement.node(),
141149
gd: gd,
142150
prepFn: startDrag,
143151
doneFn: endDrag
@@ -146,39 +154,90 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
146154

147155
dragElement.init(dragOptions);
148156

149-
shapePath.node().onmousemove = updateDragMode;
157+
sensoryElement.node().onmousemove = updateDragMode;
150158

151-
function updateDragMode(evt) {
152-
// element might not be on screen at time of setup,
153-
// so obtain bounding box here
154-
var dragBBox = dragOptions.element.getBoundingClientRect();
155-
156-
// choose 'move' or 'resize'
157-
// based on initial position of cursor within the drag element
158-
var w = dragBBox.right - dragBBox.left,
159-
h = dragBBox.bottom - dragBBox.top,
160-
x = evt.clientX - dragBBox.left,
161-
y = evt.clientY - dragBBox.top,
162-
cursor = (w > MINWIDTH && h > MINHEIGHT && !evt.shiftKey) ?
163-
dragElement.getCursor(x / w, 1 - y / h) :
164-
'move';
165-
166-
setCursor(shapePath, cursor);
167-
168-
// possible values 'move', 'sw', 'w', 'se', 'e', 'ne', 'n', 'nw' and 'w'
169-
dragMode = cursor.split('-')[0];
159+
function obtainSensoryElement() {
160+
return isLine ? createLineDragHandles() : shapePath;
170161
}
171162

172-
function startDrag(evt) {
173-
// setup conversion functions
174-
xa = Axes.getFromId(gd, shapeOptions.xref);
175-
ya = Axes.getFromId(gd, shapeOptions.yref);
163+
function createLineDragHandles() {
164+
var minSensoryWidth = 10,
165+
sensoryWidth = Math.max(shapeOptions.line.width, minSensoryWidth);
166+
167+
// Helper shapes group
168+
// Note that by setting the `data-index` attr, it is ensured that
169+
// the helper group is purged in this modules `draw` function
170+
var g = shapeLayer.append('g')
171+
.attr('data-index', index);
172+
173+
// Helper path for moving
174+
g.append('path')
175+
.attr('d', shapePath.attr('d'))
176+
.style({
177+
'cursor': 'move',
178+
'stroke-width': sensoryWidth,
179+
'stroke-opacity': '0' // ensure not visible
180+
});
181+
182+
// Helper circles for resizing
183+
var circleStyle = {
184+
'cursor': 'default',
185+
'fill-opacity': '0' // ensure not visible
186+
};
187+
var circleRadius = sensoryWidth / 2 > minSensoryWidth ? sensoryWidth / 2 : minSensoryWidth;
188+
189+
g.append('circle')
190+
.attr({
191+
'data-line-point': 'start-point',
192+
'cx': xPixelSized ? x2p(shapeOptions.xanchor) + shapeOptions.x0 : x2p(shapeOptions.x0),
193+
'cy': yPixelSized ? y2p(shapeOptions.yanchor) - shapeOptions.y0 : y2p(shapeOptions.y0),
194+
'r': circleRadius
195+
})
196+
.style(circleStyle);
197+
198+
g.append('circle')
199+
.attr({
200+
'data-line-point': 'end-point',
201+
'cx': xPixelSized ? x2p(shapeOptions.xanchor) + shapeOptions.x1 : x2p(shapeOptions.x1),
202+
'cy': yPixelSized ? y2p(shapeOptions.yanchor) - shapeOptions.y1 : y2p(shapeOptions.y1),
203+
'r': circleRadius
204+
})
205+
.style(circleStyle);
206+
207+
return g;
208+
}
176209

177-
x2p = helpers.getDataToPixel(gd, xa);
178-
y2p = helpers.getDataToPixel(gd, ya, true);
179-
p2x = helpers.getPixelToData(gd, xa);
180-
p2y = helpers.getPixelToData(gd, ya, true);
210+
function updateDragMode(evt) {
211+
if(isLine) {
212+
if(evt.target.tagName === 'path') {
213+
dragMode = 'move';
214+
} else {
215+
dragMode = evt.target.attributes['data-line-point'].value === 'start-point' ?
216+
'resize-over-start-point' : 'resize-over-end-point';
217+
}
218+
} else {
219+
// element might not be on screen at time of setup,
220+
// so obtain bounding box here
221+
var dragBBox = dragOptions.element.getBoundingClientRect();
222+
223+
// choose 'move' or 'resize'
224+
// based on initial position of cursor within the drag element
225+
var w = dragBBox.right - dragBBox.left,
226+
h = dragBBox.bottom - dragBBox.top,
227+
x = evt.clientX - dragBBox.left,
228+
y = evt.clientY - dragBBox.top,
229+
cursor = (w > MINWIDTH && h > MINHEIGHT && !evt.shiftKey) ?
230+
dragElement.getCursor(x / w, 1 - y / h) :
231+
'move';
232+
233+
setCursor(shapePath, cursor);
234+
235+
// possible values 'move', 'sw', 'w', 'se', 'e', 'ne', 'n', 'nw' and 'w'
236+
dragMode = cursor.split('-')[0];
237+
}
238+
}
181239

240+
function startDrag(evt) {
182241
// setup update strings and initial values
183242
var astr = 'shapes[' + index + ']';
184243

@@ -305,6 +364,19 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
305364
shapeOptions.path = movePath(pathIn, moveX, moveY);
306365
update[astrPath] = shapeOptions.path;
307366
}
367+
else if(isLine) {
368+
if(dragMode === 'resize-over-start-point') {
369+
var newX0 = x0 + dx;
370+
var newY0 = yPixelSized ? y0 - dy : y0 + dy;
371+
update[astrX0] = shapeOptions.x0 = xPixelSized ? newX0 : p2x(newX0);
372+
update[astrY0] = shapeOptions.y0 = yPixelSized ? newY0 : p2y(newY0);
373+
} else if(dragMode === 'resize-over-end-point') {
374+
var newX1 = x1 + dx;
375+
var newY1 = yPixelSized ? y1 - dy : y1 + dy;
376+
update[astrX1] = shapeOptions.x1 = xPixelSized ? newX1 : p2x(newX1);
377+
update[astrY1] = shapeOptions.y1 = yPixelSized ? newY1 : p2y(newY1);
378+
}
379+
}
308380
else {
309381
var newN = (~dragMode.indexOf('n')) ? n0 + dy : n0,
310382
newS = (~dragMode.indexOf('s')) ? s0 + dy : s0,
101 KB
Loading

0 commit comments

Comments
 (0)