Mapping editor: snap to grid and to other zones' corners

'Aggancia' toggle + grid step (5%, 10%, 2.5%, 1/8, 1/16), persisted. While
dragging a corner or a whole zone the delta is shifted so the closest
moving point lands exactly on a grid line or on another zone's corner coordinate
(per axis, 9 css px capture radius) — adjacent LED panels butt together to
the pixel. Hold Alt to drag freely; keyboard nudge stays unsnapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVMQ2JhsJ77pGKqcvvFHqc
This commit is contained in:
lucianoandClaude Opus 5.5 committed 2026-10-02 09:15:56 +02:00
1 parent e754656585
commit 9af9d26b2b
2 files changed
+66 -10

No files matched your search

+11
View File
@@ -214,6 +214,14 @@
<div class="row">
<label class="chk"><input type="checkbox" id="map-on" /> Attiva</label>
<label class="chk"><input type="checkbox" id="map-edit" /> Mostra maniglie anche sull'output</label>
<label class="chk" title="Aggancia angoli e zone alla griglia e agli angoli delle altre zone (tieni premuto Alt per disattivarlo mentre trascini)"><input type="checkbox" id="map-snap" /> 🧲 Aggancia</label>
<select id="map-grid" title="Passo della griglia">
<option value="20">Griglia 5%</option>
<option value="10">Griglia 10%</option>
<option value="40">Griglia 2,5%</option>
<option value="8">Griglia 1/8</option>
<option value="16">Griglia 1/16</option>
</select>
</div>
<div class="map-ed-wrap">
<canvas id="map-editor" tabindex="0"></canvas>
@@ -247,6 +255,9 @@
vede subito sul ledwall/proiettore. Per la rifinitura usa la <b>tastiera</b>
(clicca prima sull'editor): <b>frecce</b> = 1 pixel, <b>Shift+frecce</b> = 10 pixel,
<b>Tab</b> = passa all'angolo successivo, <b>Esc</b> = torna a muovere la zona intera.
Con <b>🧲 Aggancia</b> angoli e zone si agganciano alla griglia (passo
a scelta) e agli angoli delle altre zone, così i pannelli adiacenti combaciano
al pixel; tieni premuto <b>Alt</b> mentre trascini per muoverti libero.
La riga sotto l'editor mostra la posizione in pixel. Se vuoi vedere i contorni anche
sullo schermo, spunta <b>Mostra maniglie anche sull'output</b>.</p>
<p><b>Opzioni per zona</b>:</p>
+55 -10
View File
@@ -1672,7 +1672,11 @@ $('#map-img-input').addEventListener('change', (e) => {
// normalized 0..1 (y-down) corners the output uses.
let mapSel = -1; // selected zone index
let mapSelC = -1; // selected corner (0 TL,1 TR,2 BR,3 BL) or -1 = whole zone
let mapEdDrag = null; // { ci, lx, ly }
let mapEdDrag = null; // { ci, x0, y0, start: corners snapshot }
// Snap: grid divisions per side + on/off, persisted.
let mapSnap = { on: true, div: 10 };
try { mapSnap = Object.assign(mapSnap, JSON.parse(localStorage.getItem('mapsnap') || '{}')); } catch (e) {}
const MAP_SNAP_PX = 9; // capture radius in editor css pixels
let outBounds = null; // main output display size (set by refreshDisplays)
const MAP_CORNER = ['alto-sx', 'alto-dx', 'basso-dx', 'basso-sx'];
@@ -1722,10 +1726,15 @@ function mapEdDraw() {
// output frame + 10% grid
ctx.fillStyle = '#000'; ctx.fillRect(ox, 0, W, H);
ctx.strokeStyle = 'rgba(255,255,255,0.06)'; ctx.lineWidth = 1;
for (let k = 1; k < 10; k++) {
ctx.beginPath(); ctx.moveTo(ox + W * k / 10, 0); ctx.lineTo(ox + W * k / 10, H); ctx.stroke();
ctx.beginPath(); ctx.moveTo(ox, H * k / 10); ctx.lineTo(ox + W, H * k / 10); ctx.stroke();
const dv = mapSnap.div;
ctx.strokeStyle = mapSnap.on ? 'rgba(127,168,255,0.13)' : 'rgba(255,255,255,0.06)';
for (let k = 1; k < dv; k++) {
// the centre lines are slightly stronger: handy for symmetric setups
ctx.globalAlpha = (k * 2 === dv) ? 1.8 : 1;
ctx.beginPath(); ctx.moveTo(ox + W * k / dv, 0); ctx.lineTo(ox + W * k / dv, H); ctx.stroke();
ctx.beginPath(); ctx.moveTo(ox, H * k / dv); ctx.lineTo(ox + W, H * k / dv); ctx.stroke();
}
ctx.globalAlpha = 1;
ctx.strokeStyle = 'rgba(255,255,255,0.25)'; ctx.strokeRect(ox + 0.5, 0.5, W - 1, H - 1);
const P = (c) => [ox + c[0] * W, c[1] * H];
mapCfg.zones.forEach((z, zi) => {
@@ -1765,7 +1774,21 @@ function mapEdDraw() {
: '● ' + z.name + ' · zona intera · angolo alto-sx ' + fmt(z.corners[0]);
}
// Snap targets on one axis: grid lines (screen edges included) and the
// corners of every OTHER zone, so adjacent panels butt together exactly.
function mapSnapTargets(axis, skipZone) {
const out = [];
for (let k = 0; k <= mapSnap.div; k++) out.push(k / mapSnap.div);
mapCfg.zones.forEach((z, zi) => { if (zi !== skipZone) z.corners.forEach(c => out.push(c[axis])); });
return out;
}
(function mapEdInit() {
$('#map-snap').checked = mapSnap.on;
$('#map-grid').value = String(mapSnap.div);
const snapSave = () => { localStorage.setItem('mapsnap', JSON.stringify(mapSnap)); mapEdDraw(); };
$('#map-snap').addEventListener('change', (e) => { mapSnap.on = e.target.checked; snapSave(); });
$('#map-grid').addEventListener('change', (e) => { mapSnap.div = parseInt(e.target.value, 10) || 10; snapSave(); });
const cv = $('#map-editor');
const toN = (e) => {
const r = cv.getBoundingClientRect(), g = cv._geo || { ox: 0, W: r.width, H: r.height };
@@ -1795,13 +1818,13 @@ function mapEdDraw() {
}
if (best) {
mapSelect(best.zi, best.ci);
mapEdDrag = { ci: best.ci, lx: x, ly: y };
mapEdDrag = { ci: best.ci, x0: x, y0: y, start: mapCfg.zones[best.zi].corners.map(c => c.slice()) };
return;
}
for (let zi = mapCfg.zones.length - 1; zi >= 0; zi--) {
if (inside(mapCfg.zones[zi].corners, x, y)) {
mapSelect(zi, -1);
mapEdDrag = { ci: -1, lx: x, ly: y };
mapEdDrag = { ci: -1, x0: x, y0: y, start: mapCfg.zones[zi].corners.map(c => c.slice()) };
return;
}
}
@@ -1812,12 +1835,34 @@ function mapEdDraw() {
const z = mapCfg.zones[mapSel];
if (!z) { mapEdDrag = null; return; }
const [x, y] = toN(e);
const g = cv._geo || { W: cv.clientWidth, H: cv.clientHeight };
let dx = x - mapEdDrag.x0, dy = y - mapEdDrag.y0;
const st = mapEdDrag.start;
// Moving points (one corner, or all four for a whole-zone drag) snap by
// shifting the WHOLE delta so the closest candidate lands exactly on a
// target — per axis, so an edge can align horizontally and vertically.
const moving = mapEdDrag.ci >= 0 ? [st[mapEdDrag.ci]] : st;
if (mapSnap.on && !e.altKey) {
const tx = mapSnapTargets(0, mapSel), ty = mapSnapTargets(1, mapSel);
const fix = (d, axis, targets, tol) => {
let best = null;
for (const p of moving) {
const v = p[axis] + d;
for (const t of targets) {
const off = t - v;
if (Math.abs(off) <= tol && (best === null || Math.abs(off) < Math.abs(best))) best = off;
}
}
return best === null ? d : d + best;
};
dx = fix(dx, 0, tx, MAP_SNAP_PX / g.W);
dy = fix(dy, 1, ty, MAP_SNAP_PX / g.H);
}
const z0 = mapCfg.zones[mapSel];
if (mapEdDrag.ci >= 0) {
z.corners[mapEdDrag.ci] = [clampC(x), clampC(y)];
z0.corners[mapEdDrag.ci] = [clampC(st[mapEdDrag.ci][0] + dx), clampC(st[mapEdDrag.ci][1] + dy)];
} else {
const dx = x - mapEdDrag.lx, dy = y - mapEdDrag.ly;
z.corners.forEach(c => { c[0] = clampC(c[0] + dx); c[1] = clampC(c[1] + dy); });
mapEdDrag.lx = x; mapEdDrag.ly = y;
z0.corners = st.map(c => [clampC(c[0] + dx), clampC(c[1] + dy)]);
}
mapEdDraw(); mapEdSync();
});