From 2f42d179f68f2b4782b72bc8393a69fd0734a97d Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Sat, 22 Aug 2026 15:13:27 -0400 Subject: [PATCH] fix(composer): point the align toolbar at the anchor-clearing path Two alignment implementations existed and the toolbar used the wrong one. alignElement(dir) set el.x/el.y and stopped there. resolveAnchor turns anchor='right' into `dim - val`, so with xAnchor='right' an "align left" (el.x = 0) resolved to x = MATRIX_W and the element jumped to the far right edge -- the opposite of what was asked. It also never touched el.x0/el.y0, so a line's endpoints were left where they were. _alignElement already did both correctly: it clears the anchor so the stored value is absolute, and moves x0/y0 for lines. Its six wrappers -- alignLeft, alignHCenter, alignRight, alignTop, alignVCenter, alignBottom -- existed and had no callers at all. All six toolbar buttons now call the wrappers, and the legacy method is removed rather than left to drift back into use. Tests: the toolbar calls each wrapper and no longer calls alignElement, the legacy definition is gone, and _alignElement still clears the anchor and moves line endpoints. Two of them fail against the previous markup. Full suite 4062 passed, the one failure being test_install_lowmem (pre-existing, awaiting #492). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW --- test/test_composer_js_contracts.py | 40 +++++++++++++++++++ .../static/v3/js/composer/composer-app.js | 17 -------- web_interface/templates/v3/composer.html | 12 +++--- 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/test/test_composer_js_contracts.py b/test/test_composer_js_contracts.py index 202e11b1..eb08e0de 100644 --- a/test/test_composer_js_contracts.py +++ b/test/test_composer_js_contracts.py @@ -145,3 +145,43 @@ def test_restore_path_stays_snapshot_free(method): snap = body.index("_snapshot()") guard = body.index("!opts.silent") assert guard < snap, f"{method} snapshots outside the !opts.silent guard" + + +TEMPLATE_HTML = (Path(__file__).resolve().parent.parent + / "web_interface/templates/v3/composer.html") + +#: The six toolbar buttons and the wrapper each must call. +ALIGN_BUTTONS = ["alignLeft", "alignHCenter", "alignRight", + "alignTop", "alignVCenter", "alignBottom"] + + +def test_alignment_buttons_use_the_anchor_clearing_path(): + """Two alignment implementations existed and the toolbar used the wrong one. + + The legacy alignElement(dir) set el.x/el.y but left xAnchor/yAnchor in + place. resolveAnchor turns anchor='right' into `dim - val`, so "align left" + (el.x = 0) resolved to x = MATRIX_W -- the element jumped to the far right + edge instead. _alignElement clears the anchor first, so the stored value is + absolute, and it also updates el.x0/el.y0 so lines actually move. + """ + html = TEMPLATE_HTML.read_text() + for wrapper in ALIGN_BUTTONS: + assert f"{wrapper}()" in html, f"toolbar does not call {wrapper}()" + assert not re.search(r"[^_]alignElement\(", html), \ + "toolbar still calls the legacy alignElement()" + + +def test_the_legacy_alignelement_is_gone(): + """Leaving it in place invites the toolbar drifting back to it.""" + src = APP.read_text() + assert not re.search(r"^\s{4}alignElement\(dir\)", src, re.M), \ + "legacy alignElement(dir) still defined" + + +def test_align_clears_the_anchor_and_moves_line_endpoints(): + body = _method_source(APP, "_alignElement") + assert "xAnchor = null" in body and "yAnchor = null" in body, \ + "_alignElement no longer clears the anchor, so aligning an anchored " \ + "element resolves to the wrong edge" + assert "el.x0" in body and "el.y0" in body, \ + "_alignElement no longer moves line endpoints" diff --git a/web_interface/static/v3/js/composer/composer-app.js b/web_interface/static/v3/js/composer/composer-app.js index ff49ea41..01e6767f 100644 --- a/web_interface/static/v3/js/composer/composer-app.js +++ b/web_interface/static/v3/js/composer/composer-app.js @@ -1098,23 +1098,6 @@ function composerApp() { }, // ── Alignment ───────────────────────────────────────────────────── - alignElement(dir) { - const el = this.selectedElement; - if (!el) return; - const bb = window.ComposerCanvas.getBoundingBox(el, this.MATRIX_W, this.MATRIX_H); - switch (dir) { - case 'left': el.x = 0; break; - case 'center': el.x = Math.round((this.MATRIX_W - bb.w) / 2); break; - case 'right': el.x = this.MATRIX_W - bb.w; break; - case 'top': el.y = 0; break; - case 'middle': el.y = Math.round((this.MATRIX_H - bb.h) / 2); break; - case 'bottom': el.y = this.MATRIX_H - bb.h; break; - } - this.isDirty = true; - this._snapshot(); - this.render(); - }, - // ── Templates ───────────────────────────────────────────────────── loadTemplate(tmpl) { if (this.isDirty && this.elements.length > 0) { diff --git a/web_interface/templates/v3/composer.html b/web_interface/templates/v3/composer.html index b6b062a8..053c316b 100644 --- a/web_interface/templates/v3/composer.html +++ b/web_interface/templates/v3/composer.html @@ -667,13 +667,13 @@
Align to Canvas
- - - + + +
- - - + + +