mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-22 10:58:15 +00:00
fix(composer): eight editor bugs from review
All confirmed by reading the code rather than taken on trust.
Saved designs restored onto the wrong canvas. _buildPayload writes the
size as `preset`; _applyState read `state.currentPreset`, which is never
present, so changePreset(undefined) hit its `if (!preset) return` and did
nothing -- silently. A 256x64 design reopened at 128x32 with every
element misplaced. importDesign passed no size key at all, same result.
Both go through a new applyPresetLabel(), which also handles the custom
labels setCustomSize() writes ("200x50"): those are deliberately absent
from DISPLAY_PRESETS, so changePreset alone could never round-trip them.
Keyboard shortcuts hijacked text fields. The `inInput` guard sat below
the Ctrl/Cmd block, under a comment claiming combos "work everywhere".
In any input, Ctrl+C copied the selected *element* -- preventDefault
stopping the real copy -- Ctrl+V pasted an element, Ctrl+A could not
select the field contents, and Tab always moved the element selection,
so keyboard users could not reach the next input. Guard moved above both
blocks, and it now covers contenteditable too.
Resize handles were advertised on five shapes that ignored them. The
canvas drew handles for six element types; the editor gated resize and
hover on `type === 'rectangle'`. The list was also duplicated inside the
canvas. One exported RESIZABLE_TYPES now feeds all four sites.
Lines jumped on drag. addElement assigns x/y *before* spreading
ELEMENT_DEFAULTS, and the line defaults define only x0/y0 -- so a line
carries both, with x at canvas/4 and x0 at 0. Drag and nudge move x0/y0
only, so _getStoredPos preferring `x` handed the drag a base it never
updates.
Colour-picker edits were lost on reload. onColorChange mutated the
element but never set isDirty or called _snapshot, and _debouncedAutosave
only runs from _snapshot. applyPaletteColor did both; they match now.
Also: section elements drew nothing and reported a 0x0 box, so adding
"Section Label" from the palette looked broken and the element was
selectable only through the 3px hit-test padding -- they now draw their
label, with the bounding box using the same font fallback as the draw
call so the two agree. The gauge inset its arc radius by lw/2 where lw is
LED pixels and the radius is canvas pixels, then stroked at lw*s, so the
arc spilled outside its own bounding box at any scale above 1. And the
plugin id is encodeURIComponent'd before it becomes part of a request
path.
Verified: composer-app.js and composer-canvas.js parse cleanly under
tree-sitter (esprima cannot read this codebase -- it predates ??, and
fails identically on the unmodified files). Every symbol referenced
across module boundaries checked to exist. 156 Python composer tests
pass. The static-audit failure is the same 13 classes as before, all
defined on main and absent only because this branch is behind; nothing
here touches CSS or templates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
This commit is contained in:
co-authored by
Claude Opus 5
parent
986f74e38b
commit
f0bef7784c
@@ -22,6 +22,13 @@ window.ComposerCanvas = (() => {
|
||||
let _ctx = null;
|
||||
let _showGrid = true;
|
||||
|
||||
//: Element types the canvas draws resize handles for. Exported because the
|
||||
//: editor has to gate its resize and hover behaviour on exactly this list --
|
||||
//: the two had drifted, so handles appeared on five shapes that could not
|
||||
//: actually be resized.
|
||||
const RESIZABLE_TYPES = ['rectangle', 'rounded_rectangle', 'ellipse', 'arc',
|
||||
'gauge', 'sparkline'];
|
||||
|
||||
const DISPLAY_PRESETS = [
|
||||
{ label: '64×32', w: 64, h: 32 },
|
||||
{ label: '128×32', w: 128, h: 32 },
|
||||
@@ -225,8 +232,17 @@ window.ComposerCanvas = (() => {
|
||||
const pc = el.count ?? 5, ps = el.pipSize ?? 4, pg = el.pipSpacing ?? 2;
|
||||
return { x: ax, y: ay, w: pc * ps + (pc - 1) * pg, h: ps };
|
||||
}
|
||||
case 'section':
|
||||
return { x: ax, y: ay, w: 0, h: 0 };
|
||||
case 'section': {
|
||||
// Was 0x0, so the element was unselectable except through the 3px
|
||||
// hit-test padding and drew nothing at all -- a user adding one from
|
||||
// the palette saw an empty canvas.
|
||||
// Same font resolution as the draw case below, or the box will not
|
||||
// match the glyphs: getBoundingBox's shared `finfo` falls back to
|
||||
// press_start, and a section has no font of its own.
|
||||
const sinfo = FONT_MAP[el.font] || FONT_MAP.four_by_six;
|
||||
const label = el.label || 'Section';
|
||||
return { x: ax, y: ay, w: label.length * sinfo.charW, h: sinfo.sizePx };
|
||||
}
|
||||
default:
|
||||
return { x: ax, y: ay, w: 4, h: 4 };
|
||||
}
|
||||
@@ -252,7 +268,7 @@ window.ComposerCanvas = (() => {
|
||||
|
||||
// Returns the handle direction under LED-space point (lx, ly), or null
|
||||
function getResizeHandle(el, lx, ly, matrixW, matrixH) {
|
||||
if (!['rectangle', 'rounded_rectangle', 'ellipse', 'arc', 'gauge', 'sparkline'].includes(el.type)) return null;
|
||||
if (!RESIZABLE_TYPES.includes(el.type)) return null;
|
||||
const handles = _getRectHandles(el, matrixW, matrixH);
|
||||
const PAD = 4;
|
||||
for (const [dir, pt] of Object.entries(handles)) {
|
||||
@@ -307,6 +323,18 @@ window.ComposerCanvas = (() => {
|
||||
|
||||
try {
|
||||
switch (el.type) {
|
||||
case 'section': {
|
||||
// A design-time label: it marks a region for the author and is not
|
||||
// emitted into the generated plugin. There was no case here at all,
|
||||
// so adding "Section Label" from the palette drew nothing and left
|
||||
// the user with an apparently broken control.
|
||||
const sfinfo = FONT_MAP[el.font] || FONT_MAP.four_by_six;
|
||||
ctx.font = `${sfinfo.sizePx * s}px ${sfinfo.family}`;
|
||||
ctx.fillStyle = `rgba(${el.r ?? 120},${el.g ?? 120},${el.b ?? 120},0.85)`;
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(el.label || 'Section', ax * s, ay * s);
|
||||
break;
|
||||
}
|
||||
case 'text':
|
||||
case 'dynamic_text':
|
||||
case 'clock': {
|
||||
@@ -496,6 +524,12 @@ window.ComposerCanvas = (() => {
|
||||
const cx = (ax + gw / 2) * s, cy = (ay + gh / 2) * s;
|
||||
const rx = (gw / 2) * s, ry = (gh / 2) * s;
|
||||
const lw = Math.max(1, (el.lineWidth ?? 3));
|
||||
// rx/ry are canvas pixels ((gw/2)*s) but lw is LED pixels, so
|
||||
// insetting by lw/2 under-corrected by the scale factor while the
|
||||
// stroke was drawn at lw*s -- the arc spilled outside the element's
|
||||
// reported bounding box at any SCALE > 1, and the preview stopped
|
||||
// matching the generated PIL output.
|
||||
const lwPx = lw * s;
|
||||
const startDeg = el.startAngle ?? 135;
|
||||
const endDeg = el.endAngle ?? 45;
|
||||
// Arc sweep: from startDeg clockwise to endDeg (PIL convention)
|
||||
@@ -510,17 +544,17 @@ window.ComposerCanvas = (() => {
|
||||
// Track arc
|
||||
if (el.hasTrack !== false) {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, rx - lw / 2, ry - lw / 2, 0, toRad(startDeg), toRad(startDeg + totalSweep), false);
|
||||
ctx.ellipse(cx, cy, rx - lwPx / 2, ry - lwPx / 2, 0, toRad(startDeg), toRad(startDeg + totalSweep), false);
|
||||
ctx.strokeStyle = `rgb(${el.trackR ?? 40},${el.trackG ?? 40},${el.trackB ?? 40})`;
|
||||
ctx.lineWidth = lw * s;
|
||||
ctx.lineWidth = lwPx;
|
||||
ctx.stroke();
|
||||
}
|
||||
// Fill arc
|
||||
if (pct > 0) {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, rx - lw / 2, ry - lw / 2, 0, toRad(startDeg), toRad(startDeg + fillSweep), false);
|
||||
ctx.ellipse(cx, cy, rx - lwPx / 2, ry - lwPx / 2, 0, toRad(startDeg), toRad(startDeg + fillSweep), false);
|
||||
ctx.strokeStyle = `rgb(${el.r},${el.g},${el.b})`;
|
||||
ctx.lineWidth = lw * s;
|
||||
ctx.lineWidth = lwPx;
|
||||
ctx.stroke();
|
||||
}
|
||||
// Centre label
|
||||
@@ -624,7 +658,7 @@ window.ComposerCanvas = (() => {
|
||||
}
|
||||
|
||||
// Resize handles: on rect, rounded rect, ellipse
|
||||
if (['rectangle', 'rounded_rectangle', 'ellipse', 'arc', 'gauge', 'sparkline'].includes(el.type)) {
|
||||
if (RESIZABLE_TYPES.includes(el.type)) {
|
||||
const handles = _getRectHandles(el, matrixW, matrixH);
|
||||
const HS = 5;
|
||||
ctx.fillStyle = 'white';
|
||||
@@ -752,6 +786,6 @@ window.ComposerCanvas = (() => {
|
||||
init, render, setGrid, updateCanvasSize,
|
||||
hitTest, getBoundingBox, computeActualPos, resolveAnchor,
|
||||
getResizeHandle, getCursorForHandle,
|
||||
ELEMENT_DEFAULTS, FONT_MAP, DISPLAY_PRESETS,
|
||||
ELEMENT_DEFAULTS, FONT_MAP, DISPLAY_PRESETS, RESIZABLE_TYPES,
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user