Skip to content

Commit 595e1cc

Browse files
committed
Fix a label dragging bug
1 parent 2589f86 commit 595e1cc

5 files changed

Lines changed: 101 additions & 7 deletions

File tree

browser-tests/label-tool.spec.mjs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,6 +1443,48 @@ test('the justification follows the text across its anchor', async function({pag
14431443
expect(errors).toEqual([]);
14441444
});
14451445

1446+
test('an aligned label is dragged where it is drawn', async function({page}) {
1447+
// A label carrying a label-align is drawn with the anchor its alignment
1448+
// gives it, and the drag writes text-anchor=start against the correction the
1449+
// renderer applies. The preview used to write that stored value onto the
1450+
// rendered text, which drew a right-aligned label a full width right of the
1451+
// pointer and a centred one half a width -- and dropped it back into place
1452+
// on release, so the label appeared to jump left the moment it was let go.
1453+
var errors = collectPageErrors(page);
1454+
await loadFixture(page, FIXTURE);
1455+
1456+
await clickMap(page, 0.45, 0.45);
1457+
await writeLabel(page, 'Winnemucca');
1458+
await disarmTool(page);
1459+
await clickLabel(page, 0);
1460+
await setDragMode(page, 'draggable');
1461+
1462+
for (var align of ['right', 'center']) {
1463+
await page.locator('.text-style-panel .label-align-buttons [data-align="' +
1464+
align + '"]').click();
1465+
await page.waitForTimeout(200);
1466+
await hoverNothing(page);
1467+
var before = await getLabelBox(page, 0);
1468+
var from = await getGlyphPoint(page, 0, 0.5);
1469+
1470+
await page.mouse.move(from.x, from.y);
1471+
await page.waitForTimeout(80);
1472+
await page.mouse.down();
1473+
await page.mouse.move(from.x + 60, from.y - 30, {steps: 8});
1474+
// mid-drag: the text is under the pointer, not a width away from it
1475+
var dragging = await getLabelBox(page, 0);
1476+
expect(Math.abs(dragging.x - (before.x + 60))).toBeLessThan(2);
1477+
1478+
await page.mouse.up();
1479+
await page.waitForTimeout(250);
1480+
// and the command's redraw leaves it where the drag showed it
1481+
var after = await getLabelBox(page, 0);
1482+
expect(Math.abs(after.x - dragging.x)).toBeLessThan(2);
1483+
expect((await getLabelLayer(page)).records[0]['label-align']).toBe(align);
1484+
}
1485+
expect(errors).toEqual([]);
1486+
});
1487+
14461488
test('the drag mode is the tool\'s and not the label\'s', async function({page}) {
14471489
// Nothing in the record says which segment is lit: a label with a position
14481490
// can be dragged or not, and one that has been dragged stays dragged when

docs/development/label-tool-design.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2792,6 +2792,14 @@ the block's width to hold the text still while its lines re-justify — so
27922792
`start` is both the anchor whose `dx` is the left edge itself and the value that
27932793
would leave the label where it is if the alignment were later removed.
27942794

2795+
That is the value the drag *writes*, and not the one the label is *drawn* with
2796+
while the drag is in progress, which is still the alignment's own. So
2797+
`getOffsetDragValues()` returns both: `dx`/`text-anchor` for the command and
2798+
`x`/`anchor` for the preview. Previewing with the stored pair drew a
2799+
right-aligned label a full width to the right of the pointer, and a centred one
2800+
half a width, for as long as the drag lasted — the text then dropped back into
2801+
place on release, which read as the label jumping left the moment it was let go.
2802+
27952803
A drag works in the label's own coordinate space, inside its symbol group,
27962804
which is what `dx` and `dy` are measured in: pointer movement is divided by the
27972805
symbol scale on the way in, or a drag would overshoot at any scale but 1.

src/gui/gui-label-offset.mjs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,15 @@ var PRECISION = 10;
5050
// aligned: whether it carries a label-align (see below)
5151
// delta: {dx, dy} -- how far the pointer has moved, in the same space.
5252
//
53-
// Returns {dx, dy, text-anchor, x}, where x is where the text will be drawn
54-
// once the other three are written -- the same as dx except on an aligned
55-
// label. The three have to be written together: applying dx without its
56-
// text-anchor moves the text by half its own width or by all of it.
53+
// Returns two descriptions of the same placement:
54+
//
55+
// dx, dy, text-anchor: what the drag writes to the record. The three have to
56+
// be written together: applying dx without its text-anchor moves the text
57+
// by half its own width or by all of it.
58+
// x, anchor: where the text sits and how it is justified while it is drawn,
59+
// which is what the preview writes onto the rendered label. The same as
60+
// dx and text-anchor except on an aligned label, whose stored offset is
61+
// measured against a justification it is not drawn with.
5762
export function getOffsetDragValues(start, delta) {
5863
var width = start.width > 0 ? start.width : 0;
5964
var drawnAnchor = normalizeAnchor(start.anchor);
@@ -72,12 +77,18 @@ export function getOffsetDragValues(start, delta) {
7277
// if the alignment were later removed.
7378
var anchor = start.aligned ? 'start' :
7479
width > 0 ? getAnchorForCentre(left + width / 2, width) : drawnAnchor;
80+
// An aligned label goes on being drawn with the anchor its alignment gives
81+
// it, whatever the drag writes: the alignment is unchanged, so the renderer
82+
// will justify it the same way afterwards. Drawing it as 'start' instead
83+
// would slide the text right by half its width or by all of it for the
84+
// length of the drag, and drop it back on release.
85+
var drawn = start.aligned ? drawnAnchor : anchor;
7586
return {
7687
dx: round(left + anchorOffsets[anchor] * width),
7788
dy: round(start.dy + delta.dy),
7889
'text-anchor': anchor,
79-
x: round(start.aligned ? start.dx + delta.dx :
80-
left + anchorOffsets[anchor] * width)
90+
x: round(left + anchorOffsets[drawn] * width),
91+
anchor: drawn
8192
};
8293
}
8394

src/gui/gui-label-tool2.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -832,7 +832,10 @@ export function initLabelTool(gui, ext, hit) {
832832
// ended.
833833
setMultilineAttribute(nodes.text, 'x', o.values.x);
834834
nodes.text.setAttribute('y', o.values.dy);
835-
nodes.text.setAttribute('text-anchor', o.values['text-anchor']);
835+
// The anchor the label is drawn with, not the one the drag writes: on an
836+
// aligned label the two differ, and the stored one belongs with the stored
837+
// dx rather than with the x being previewed here.
838+
nodes.text.setAttribute('text-anchor', o.values.anchor);
836839
// The cue is drawn around the text rather than moved with it, so it has to
837840
// be rebuilt to follow -- one getBBox on one label per mouse move.
838841
selection.refresh(true);

test/gui-label-offset-test.mjs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ function leftEdge(values, width) {
1616
return getTextCentreOffset(values.dx, values['text-anchor'], width) - width / 2;
1717
}
1818

19+
// How far to the left of x a justification puts the text, as a fraction of its
20+
// width -- the same table the module works from.
21+
function anchorOffset(anchor) {
22+
return {start: 0, middle: 0.5, end: 1}[anchor];
23+
}
24+
1925
describe('gui label offset drags', function() {
2026

2127
describe('getOffsetDragValues()', function() {
@@ -90,6 +96,30 @@ describe('gui label offset drags', function() {
9096
var o = getOffsetDragValues(centred({dx: 20}), {dx: 5, dy: 0});
9197
assert.equal(o.x, o.dx);
9298
});
99+
100+
// The preview writes x and the anchor onto the rendered label, so the two
101+
// have to describe the same placement. An aligned label keeps the anchor
102+
// its alignment gives it; taking the stored 'start' instead drew the text
103+
// half a width or a whole width to the right until the drag was released.
104+
it('draws an aligned label with the justification it already had', function() {
105+
var o = getOffsetDragValues(centred({dx: 20, aligned: true}), {dx: 5, dy: 0});
106+
assert.equal(o.anchor, 'middle');
107+
assert.equal(o['text-anchor'], 'start');
108+
});
109+
110+
it('draws an aligned label where it was dragged to', function() {
111+
['start', 'middle', 'end'].forEach(function(anchor) {
112+
var o = getOffsetDragValues(centred({dx: 20, anchor: anchor, aligned: true}),
113+
{dx: 5, dy: 0});
114+
// the preview's own left edge, from the pair it writes
115+
assert.equal(o.x - anchorOffset(o.anchor) * 40, 20 + 5 - anchorOffset(anchor) * 40);
116+
});
117+
});
118+
119+
it('draws an unaligned label with the justification it writes', function() {
120+
var o = getOffsetDragValues(centred(), {dx: 30, dy: 0});
121+
assert.equal(o.anchor, o['text-anchor']);
122+
});
93123
});
94124

95125
describe('getAnchorForCentre()', function() {

0 commit comments

Comments
 (0)