Skip to content

Ability to interactively change length and rotate line shapes #2038

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
chriddyp opened this issue Sep 26, 2017 · 6 comments
Closed

Ability to interactively change length and rotate line shapes #2038

chriddyp opened this issue Sep 26, 2017 · 6 comments
Labels
bug something broken
Milestone

Comments

@chriddyp
Copy link
Member

Some dash users are interested in creating image analysis apps using background images and interactive line segments to measure distances on the image.

In order to do this, they need to be able to easily rotate and resize line segments.

Currently, it is possible to translate line segments but not reshape them.

move-line

cc @alexcjohnson

@chriddyp chriddyp added this to the Dash milestone Sep 26, 2017
@rreusser
Copy link
Contributor

Would be neat to have a dedicated 'ruler' feature with which you could click and measure, say slope or distance in data coords, though definitely a moderately tricky feature by itself.

@alexcjohnson
Copy link
Collaborator

More detail: horizontal and vertical lines can't be reshaped, only moved. Diagonal lines can be reshaped, but only within the quadrant they start in. Sounds like they're using the same algorithm as shapes with area, which isn't really appropriate there. They should use something closer to what annotations use - which also includes invisible handles which are wider than the line itself, particularly at the end, to make it easier to grab.

A question: should we snap to horizontal and vertical, and/or the original angle the line had? Most of the time this is useful, but on occasion you really do want slightly off-angle lines. Annotations currently have no snapping. I suppose one option would be no snapping at first, but some time later add snapping at the same time as we add the ability to select just one endpoint/corner and move it pixel-by-pixel with arrow keys.

@alexcjohnson alexcjohnson added the bug something broken label Sep 28, 2017
@rmoestl
Copy link
Contributor

rmoestl commented Apr 24, 2018

I'm currently working on that issue (without snapping, see above) and a major question arose:

Should the reshape line mode (as opposed to move line mode) be active "near" both ends only or would it be okay that the reshape mode is active for the first 1/3 and last 1/3 of the line (similar to

module.exports = function getCursor(x, y, xanchor, yanchor) {
)?

IMHO it'd be nicer to have the reshape line mode be active only "near" both ends of the line. However, in my attempt to implement this, I came across worrisome differences between what getClientBoundingRect() is returning for SVG elements in different browsers. For example Chrome does not include the stroke while FF does. And in addition FF is returning way too big bounding boxes for my test cases, which means that the sensitive area for reshape line would be off of the helper path's visible area. For more info about those browser inconsistencies see SVGElement.getBoundingClientRect(): which bounding box? · Issue #339 · w3c/svgwg.

That means that despite some browsers returning too big bounding boxes for the helper path, showing the reshape line mode for the first and last 1/3 of the line would (hopefully) work for each browser and situation. Of course one could decrease the areas for reshape line, e.g. first and last 1/8. But which divisor is the one that is as small as possible and yet works in all cases in all browsers?

The alternative, I already thought through but haven't tried yet, would be to have two helper circle elements at both ends, a "thick" helper line as a path and rely on the browser mousemove events fired for those distinct helper elements correctly.

First approach would be less accurate, hence less intuitive for the user, but less effort. Second approach would be more intuitive for the user, but a little bit more effort in implementation.

@alexcjohnson
Copy link
Collaborator

The helper element approach is what we did with annotations:

if(edits.annotationPosition && arrow.node().parentNode && !subplotId) {
var arrowDragHeadX = headX;
var arrowDragHeadY = headY;
if(options.standoff) {
var arrowLength = Math.sqrt(Math.pow(headX - tailX, 2) + Math.pow(headY - tailY, 2));
arrowDragHeadX += options.standoff * (tailX - headX) / arrowLength;
arrowDragHeadY += options.standoff * (tailY - headY) / arrowLength;
}
var arrowDrag = arrowGroup.append('path')
.classed('annotation-arrow', true)
.classed('anndrag', true)
.attr({
d: 'M3,3H-3V-3H3ZM0,0L' + (tailX - arrowDragHeadX) + ',' + (tailY - arrowDragHeadY),
transform: 'translate(' + arrowDragHeadX + ',' + arrowDragHeadY + ')'
})
.style('stroke-width', (strokewidth + 6) + 'px')
.call(Color.stroke, 'rgba(0,0,0,0)')
.call(Color.fill, 'rgba(0,0,0,0)');
var update,
annx0,
anny0;
// dragger for the arrow & head: translates the whole thing
// (head/tail/text) all together
dragElement.init({
element: arrowDrag.node(),
gd: gd,
prepFn: function() {
var pos = Drawing.getTranslate(annTextGroupInner);
annx0 = pos.x;
anny0 = pos.y;
update = {};
if(xa && xa.autorange) {
update[xa._name + '.autorange'] = true;
}
if(ya && ya.autorange) {
update[ya._name + '.autorange'] = true;
}
},
moveFn: function(dx, dy) {
var annxy0 = applyTransform(annx0, anny0),
xcenter = annxy0[0] + dx,
ycenter = annxy0[1] + dy;
annTextGroupInner.call(Drawing.setTranslate, xcenter, ycenter);
update[annbase + '.x'] = xa ?
xa.p2r(xa.r2p(options.x) + dx) :
(options.x + (dx / gs.w));
update[annbase + '.y'] = ya ?
ya.p2r(ya.r2p(options.y) + dy) :
(options.y - (dy / gs.h));
if(options.axref === options.xref) {
update[annbase + '.ax'] = xa.p2r(xa.r2p(options.ax) + dx);
}
if(options.ayref === options.yref) {
update[annbase + '.ay'] = ya.p2r(ya.r2p(options.ay) + dy);
}
arrowGroup.attr('transform', 'translate(' + dx + ',' + dy + ')');
annTextGroup.attr({
transform: 'rotate(' + textangle + ',' +
xcenter + ',' + ycenter + ')'
});
},
doneFn: function() {
Registry.call('relayout', gd, update);
var notesBox = document.querySelector('.js-notes-box-panel');
if(notesBox) notesBox.redraw(notesBox.selectedObj);
}
});
}

There we only needed one helper, since dragging the arrowhead and arrow line do the same operation. But the idea is the same, and we did exactly what you're describing above: a bigger active region right around the head (I made it a square but a circle would have been even better) plus a thicker version of the original line. I'd vote to do the same here; it's more DOM overhead but I can't really see people making hundreds of editable lines. Yes, more effort for us, but I agree that it should be a better experience.

I suppose it would be possible to do it with only one transparent helper element, drag is enabled whenever you're over it but the mode is calculated on the fly. I wouldn't pick a fixed fraction though, but a fixed size that perhaps grows with the line width. Like this, where the black is the shape and red is the transparent helper (not actual size 😅):
screen shot 2018-04-24 at 8 52 34 am
Anyway that's the desired behavior, you choose the implementation.

@rmoestl
Copy link
Contributor

rmoestl commented Apr 24, 2018

Thanks for clarifying / deciding about the behavior.

I was looking at annotations's implementation and I do somewhat the same (one helper element) in my current local spike test. Anyways, with the desired behavior I've to either wrap my head around how to calculate the mode on the fly without relying on getClientBoundingRect() or go the "three helper elements" route.

@rmoestl
Copy link
Contributor

rmoestl commented May 9, 2018

Fixed by #2594.

@rmoestl rmoestl closed this as completed May 9, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug something broken
Projects
None yet
Development

No branches or pull requests

4 participants