math-tasks/tasks/cargo-filling/editor.html

561 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cargo Filling Editor</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a2e; color: #e0e0e0; font-family: 'Segoe UI', system-ui, sans-serif; }
#toolbar {
position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
background: #16213e; border-bottom: 1px solid #0f3460;
display: flex; align-items: center; gap: 12px; padding: 8px 16px; font-size: 14px;
}
#toolbar button {
padding: 6px 14px; border: 1px solid #0f3460; border-radius: 6px;
background: #1a1a2e; color: #e0e0e0; cursor: pointer; font-size: 13px;
}
#toolbar button:hover { background: #0f3460; }
#toolbar button.primary { background: #533483; border-color: #7b2d8e; }
#toolbar button.save { background: #1b8a5a; border-color: #239b6e; }
#page-indicator { color: #a0a0c0; font-weight: 600; min-width: 100px; text-align: center; }
.spacer { flex: 1; }
#statusbar {
position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000;
background: #16213e; border-top: 1px solid #0f3460;
padding: 6px 16px; font-size: 12px; color: #a0a0c0;
}
#worksheet-container {
margin-top: 52px; margin-bottom: 32px;
display: flex; flex-direction: column; align-items: center; gap: 20px; padding: 20px;
}
.page-label {
position: absolute; top: -24px; left: 50%; transform: translateX(-50%);
background: #533483; color: white; font-size: 11px; font-weight: 700;
padding: 2px 12px; border-radius: 4px;
}
.editor-selected { outline: 3px solid #7c3aed !important; outline-offset: 4px; }
.editor-changed { position: relative; }
.editor-changed::after {
content: ''; position: absolute; top: -2px; right: -2px;
width: 8px; height: 8px; background: #f97316; border-radius: 50%; z-index: 10;
}
#toast {
position: fixed; bottom: 50px; left: 50%; transform: translateX(-50%);
background: #533483; color: white; padding: 8px 20px; border-radius: 8px;
font-size: 13px; font-weight: 600; opacity: 0; transition: opacity 0.3s;
z-index: 2000; pointer-events: none;
}
#toast.show { opacity: 1; }
#coord-tooltip {
position: fixed; display: none; background: rgba(0,0,0,0.8); color: #e0e0e0;
padding: 3px 8px; border-radius: 4px; font-size: 11px; z-index: 2000;
pointer-events: none; white-space: nowrap;
}
.card-inner { overflow: visible !important; }
</style>
</head>
<body>
<div id="toolbar">
<button id="btn-prev">&larr; Prev</button>
<span id="page-indicator">Page 1 / ?</span>
<button id="btn-next">Next &rarr;</button>
<div class="spacer"></div>
<span id="selection-info" style="color: #c4b5fd; font-size: 12px;">Loading...</span>
<div class="spacer"></div>
<button id="btn-reset">Reset Page</button>
<button id="btn-copy" class="primary">Copy JSON</button>
<button id="btn-copy-changes" class="primary">Copy Changes</button>
<button id="btn-save" class="save">Save</button>
</div>
<div id="worksheet-container"></div>
<div id="statusbar">
<span id="status-text">Keys: +/- scale/resize &bull; arrows move &bull; 0 reset &bull; Esc deselect</span>
</div>
<div id="toast"></div>
<div id="coord-tooltip"></div>
<script src="../../src/editor/editor-core.js"></script>
<script>
(function() {
'use strict';
// Selection modes
var MODE_ASTEROID = 'asteroid';
var MODE_SHIP = 'ship';
var MODE_INNER = 'inner';
var MODE_BADGE = 'badge';
// Default values
var DEFAULTS = {
asteroidScale: 1.0,
ship: { right: -12.5, top: -5, width: 90, height: 42 },
innerAst: { left: 58, top: 73 },
badge: { left: 40, top: 39 }
};
var allCards = []; // [{el, spaceAsteroid, spaceAsteroidImg, cargoShip, badge, innerAst, remnantContainer, pageNum, cardIndex}]
var originals = new Map(); // "pageNum-cardIndex" → {asteroidScale, ship:{...}, innerAst:{...}}
var selected = null; // {card, mode}
var mmToPxRatio = 1;
function cardKey(card) {
return card.pageNum + '-' + card.cardIndex;
}
// --- Read current state from DOM ---
function getAsteroidScale(card) {
var t = card.spaceAsteroidImg.style.transform || '';
var m = t.match(/scale\(([\d.]+)\)/);
return m ? parseFloat(m[1]) : 1.0;
}
function setAsteroidScale(card, scale) {
// space-asteroid img
if (scale === 1.0) {
card.spaceAsteroidImg.style.transform = '';
} else {
card.spaceAsteroidImg.style.transform = 'scale(' + scale + ')';
}
// remnant shape container
if (card.remnantContainer) {
if (scale === 1.0) {
card.remnantContainer.style.transform = '';
} else {
card.remnantContainer.style.transform = 'scale(' + scale + ')';
}
}
// inner-ast: must compose with translate(-50%,-50%)
if (card.innerAst) {
if (scale === 1.0) {
card.innerAst.style.transform = 'translate(-50%, -50%)';
} else {
card.innerAst.style.transform = 'translate(-50%, -50%) scale(' + scale + ')';
}
}
}
function getShipValues(card) {
var cs = card.cargoShip.style;
return {
right: parseFloat(cs.right) || DEFAULTS.ship.right,
top: parseFloat(cs.top) || DEFAULTS.ship.top,
width: parseFloat(cs.width) || DEFAULTS.ship.width,
height: parseFloat(cs.height) || DEFAULTS.ship.height
};
}
function setShipValues(card, vals) {
card.cargoShip.style.right = vals.right + 'mm';
card.cargoShip.style.top = vals.top + 'mm';
card.cargoShip.style.width = vals.width + 'mm';
card.cargoShip.style.height = vals.height + 'mm';
}
function getInnerAstValues(card) {
var s = card.innerAst.style;
var left = parseFloat(s.left);
var top = parseFloat(s.top);
// If no inline style, read from computed or defaults
if (isNaN(left)) left = DEFAULTS.innerAst.left;
if (isNaN(top)) top = DEFAULTS.innerAst.top;
return { left: left, top: top };
}
function setInnerAstValues(card, vals) {
card.innerAst.style.left = vals.left + '%';
card.innerAst.style.top = vals.top + '%';
}
function getBadgeValues(card) {
if (!card.badge) return { left: DEFAULTS.badge.left, top: DEFAULTS.badge.top };
var s = card.badge.style;
var left = parseFloat(s.left);
var top = parseFloat(s.top);
if (isNaN(left)) left = DEFAULTS.badge.left;
if (isNaN(top)) top = DEFAULTS.badge.top;
return { left: left, top: top };
}
function setBadgeValues(card, vals) {
if (!card.badge) return;
card.badge.style.left = vals.left + '%';
card.badge.style.top = vals.top + '%';
}
// --- Get full card state ---
function getCardState(card) {
return {
asteroidScale: getAsteroidScale(card),
ship: getShipValues(card),
innerAst: card.innerAst ? getInnerAstValues(card) : { left: DEFAULTS.innerAst.left, top: DEFAULTS.innerAst.top },
badge: card.badge ? getBadgeValues(card) : { left: DEFAULTS.badge.left, top: DEFAULTS.badge.top }
};
}
function statesEqual(a, b) {
if (Math.abs(a.asteroidScale - b.asteroidScale) > 0.001) return false;
if (Math.abs(a.ship.right - b.ship.right) > 0.01) return false;
if (Math.abs(a.ship.top - b.ship.top) > 0.01) return false;
if (Math.abs(a.ship.width - b.ship.width) > 0.01) return false;
if (Math.abs(a.ship.height - b.ship.height) > 0.01) return false;
if (Math.abs(a.innerAst.left - b.innerAst.left) > 0.01) return false;
if (Math.abs(a.innerAst.top - b.innerAst.top) > 0.01) return false;
if (Math.abs(a.badge.left - b.badge.left) > 0.01) return false;
if (Math.abs(a.badge.top - b.badge.top) > 0.01) return false;
return true;
}
// --- Card discovery ---
function findCards(pages, mmToPx) {
mmToPxRatio = mmToPx;
allCards.length = 0;
pages.forEach(function(page, pi) {
var taskCards = page.querySelectorAll('.task-card');
taskCards.forEach(function(el, ci) {
var spaceAsteroid = el.querySelector('.space-asteroid');
var spaceAsteroidImg = spaceAsteroid ? spaceAsteroid.querySelector('img') : null;
var cargoShip = el.querySelector('.cargo-ship');
var badge = el.querySelector('.badge-10');
var innerAst = el.querySelector('.inner-ast');
var remnantContainer = el.querySelector('.remnant-shape .shape-container');
var card = {
el: el,
spaceAsteroid: spaceAsteroid,
spaceAsteroidImg: spaceAsteroidImg,
cargoShip: cargoShip,
badge: badge,
innerAst: innerAst,
remnantContainer: remnantContainer,
pageNum: pi + 1,
cardIndex: ci
};
allCards.push(card);
// Store initial state as original (will be overridden by applyData)
originals.set(cardKey(card), {
asteroidScale: DEFAULTS.asteroidScale,
ship: Object.assign({}, DEFAULTS.ship),
innerAst: Object.assign({}, DEFAULTS.innerAst),
badge: Object.assign({}, DEFAULTS.badge)
});
// Click handlers
if (spaceAsteroid) {
spaceAsteroid.style.cursor = 'pointer';
spaceAsteroid.addEventListener('click', function(e) {
e.stopPropagation();
selectCard(card, MODE_ASTEROID);
});
}
if (cargoShip) {
cargoShip.style.cursor = 'pointer';
cargoShip.addEventListener('click', function(e) {
if (e.target.closest('.inner-ast')) return;
if (e.target.closest('.badge-10')) return;
e.stopPropagation();
selectCard(card, MODE_SHIP);
});
}
if (badge) {
badge.style.cursor = 'pointer';
badge.style.zIndex = '20';
badge.addEventListener('click', function(e) {
e.stopPropagation();
selectCard(card, MODE_BADGE);
});
}
if (innerAst) {
innerAst.style.cursor = 'pointer';
innerAst.addEventListener('click', function(e) {
e.stopPropagation();
selectCard(card, MODE_INNER);
});
}
});
});
document.getElementById('status-text').textContent =
allCards.length + ' cards. Keys: +/- scale/resize \u2022 arrows move \u2022 0 reset \u2022 Esc deselect';
document.getElementById('selection-info').textContent = 'Click an element to select';
}
// --- Selection ---
function selectCard(card, mode) {
deselectAll();
selected = { card: card, mode: mode };
var highlight;
if (mode === MODE_ASTEROID) highlight = card.spaceAsteroid;
else if (mode === MODE_SHIP) highlight = card.cargoShip;
else if (mode === MODE_INNER) highlight = card.innerAst;
else if (mode === MODE_BADGE) highlight = card.badge;
if (highlight) highlight.classList.add('editor-selected');
updateInfo();
}
function deselectAll() {
if (selected) {
var card = selected.card;
if (card.spaceAsteroid) card.spaceAsteroid.classList.remove('editor-selected');
if (card.cargoShip) card.cargoShip.classList.remove('editor-selected');
if (card.innerAst) card.innerAst.classList.remove('editor-selected');
selected = null;
}
updateInfo();
}
function updateInfo() {
var el = document.getElementById('selection-info');
if (!selected) { el.textContent = 'Click an element to select'; return; }
var c = selected.card;
var prefix = 'P' + c.pageNum + ' C' + (c.cardIndex + 1) + ' | ';
if (selected.mode === MODE_ASTEROID) {
var s = getAsteroidScale(c);
el.textContent = prefix + 'Asteroid scale: ' + s.toFixed(2);
} else if (selected.mode === MODE_SHIP) {
var v = getShipValues(c);
el.textContent = prefix + 'Ship R:' + v.right.toFixed(1) + 'mm T:' + v.top.toFixed(1) + 'mm ' + v.width.toFixed(0) + '\u00D7' + v.height.toFixed(0) + 'mm';
} else if (selected.mode === MODE_INNER) {
var iv = getInnerAstValues(c);
el.textContent = prefix + 'Inner L:' + iv.left.toFixed(1) + '% T:' + iv.top.toFixed(1) + '%';
} else if (selected.mode === MODE_BADGE) {
var bv = getBadgeValues(c);
el.textContent = prefix + 'Badge L:' + bv.left.toFixed(1) + '% T:' + bv.top.toFixed(1) + '%';
}
}
// --- Change tracking ---
function markChanged(card) {
var orig = originals.get(cardKey(card));
var cur = getCardState(card);
var changed = !statesEqual(cur, orig);
card.el.classList.toggle('editor-changed', changed);
}
// --- Apply data.json to DOM ---
function applyData(data) {
if (!data || !data.pages) return;
var count = 0;
data.pages.forEach(function(p) {
(p.cards || []).forEach(function(cd) {
var card = allCards.find(function(c) {
return c.pageNum === p.page && c.cardIndex === cd.index;
});
if (!card) return;
var scale = cd.asteroidScale != null ? cd.asteroidScale : DEFAULTS.asteroidScale;
if (cd.asteroidScale != null) {
setAsteroidScale(card, scale);
}
if (cd.ship) {
var sv = {
right: cd.ship.right != null ? cd.ship.right : DEFAULTS.ship.right,
top: cd.ship.top != null ? cd.ship.top : DEFAULTS.ship.top,
width: cd.ship.width != null ? cd.ship.width : DEFAULTS.ship.width,
height: cd.ship.height != null ? cd.ship.height : DEFAULTS.ship.height
};
setShipValues(card, sv);
}
if (cd.innerAst && card.innerAst) {
var iv = {
left: cd.innerAst.left != null ? cd.innerAst.left : DEFAULTS.innerAst.left,
top: cd.innerAst.top != null ? cd.innerAst.top : DEFAULTS.innerAst.top
};
setInnerAstValues(card, iv);
}
// Update originals to saved state
originals.set(cardKey(card), getCardState(card));
card.el.classList.remove('editor-changed');
count++;
});
});
if (count > 0) core.showToast('Loaded ' + count + ' card edits');
}
// --- Serialization ---
function buildConfig(changesOnly) {
var pagesMap = {};
allCards.forEach(function(card) {
var cur = getCardState(card);
var orig = originals.get(cardKey(card));
var cardData = { index: card.cardIndex };
var hasChange = false;
// Asteroid scale
if (Math.abs(cur.asteroidScale - DEFAULTS.asteroidScale) > 0.001) {
if (!changesOnly || Math.abs(cur.asteroidScale - orig.asteroidScale) > 0.001) {
cardData.asteroidScale = +cur.asteroidScale.toFixed(3);
hasChange = true;
}
} else if (!changesOnly) {
// At default, skip
}
// Ship
var shipChanged = false;
var shipData = {};
if (Math.abs(cur.ship.right - DEFAULTS.ship.right) > 0.01) { shipData.right = +cur.ship.right.toFixed(1); shipChanged = true; }
if (Math.abs(cur.ship.top - DEFAULTS.ship.top) > 0.01) { shipData.top = +cur.ship.top.toFixed(1); shipChanged = true; }
if (Math.abs(cur.ship.width - DEFAULTS.ship.width) > 0.01) { shipData.width = +cur.ship.width.toFixed(1); shipChanged = true; }
if (Math.abs(cur.ship.height - DEFAULTS.ship.height) > 0.01) { shipData.height = +cur.ship.height.toFixed(1); shipChanged = true; }
if (shipChanged) {
if (!changesOnly || !statesEqual(cur, orig)) {
cardData.ship = shipData;
hasChange = true;
}
}
// Inner asteroid
if (card.innerAst) {
var innerChanged = false;
var innerData = {};
if (Math.abs(cur.innerAst.left - DEFAULTS.innerAst.left) > 0.01) { innerData.left = +cur.innerAst.left.toFixed(1); innerChanged = true; }
if (Math.abs(cur.innerAst.top - DEFAULTS.innerAst.top) > 0.01) { innerData.top = +cur.innerAst.top.toFixed(1); innerChanged = true; }
if (innerChanged) {
if (!changesOnly || Math.abs(cur.innerAst.left - orig.innerAst.left) > 0.01 || Math.abs(cur.innerAst.top - orig.innerAst.top) > 0.01) {
cardData.innerAst = innerData;
hasChange = true;
}
}
}
if (changesOnly && !hasChange) return;
if (!changesOnly && !hasChange) return;
if (!pagesMap[card.pageNum]) pagesMap[card.pageNum] = { page: card.pageNum, cards: [] };
pagesMap[card.pageNum].cards.push(cardData);
});
return { pages: Object.values(pagesMap) };
}
// --- Reset ---
function resetCurrentPage(pageNum) {
allCards.forEach(function(card) {
if (card.pageNum !== pageNum) return;
var orig = originals.get(cardKey(card));
setAsteroidScale(card, orig.asteroidScale);
setShipValues(card, orig.ship);
if (card.innerAst) setInnerAstValues(card, orig.innerAst);
card.el.classList.remove('editor-changed');
});
if (selected && selected.card.pageNum === pageNum) updateInfo();
}
// --- Keyboard ---
function setupKeyboard() {
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') { deselectAll(); return; }
if (!selected) return;
var card = selected.card;
var mode = selected.mode;
if (mode === MODE_ASTEROID) {
var scale = getAsteroidScale(card);
var step = e.shiftKey ? 0.01 : 0.05;
switch (e.key) {
case '+': case '=':
setAsteroidScale(card, Math.min(2, +(scale + step).toFixed(3)));
markChanged(card); updateInfo(); e.preventDefault(); break;
case '-': case '_':
setAsteroidScale(card, Math.max(0.3, +(scale - step).toFixed(3)));
markChanged(card); updateInfo(); e.preventDefault(); break;
case '0':
setAsteroidScale(card, DEFAULTS.asteroidScale);
markChanged(card); updateInfo(); e.preventDefault(); break;
}
} else if (mode === MODE_SHIP) {
var sv = getShipValues(card);
var moveStep = e.shiftKey ? 0.5 : 2;
var sizeStep = e.shiftKey ? 1 : 2;
switch (e.key) {
case 'ArrowLeft':
sv.right = +(sv.right + moveStep).toFixed(1); // left = increase right
setShipValues(card, sv); markChanged(card); updateInfo(); e.preventDefault(); break;
case 'ArrowRight':
sv.right = +(sv.right - moveStep).toFixed(1); // right = decrease right
setShipValues(card, sv); markChanged(card); updateInfo(); e.preventDefault(); break;
case 'ArrowUp':
sv.top = +(sv.top - moveStep).toFixed(1);
setShipValues(card, sv); markChanged(card); updateInfo(); e.preventDefault(); break;
case 'ArrowDown':
sv.top = +(sv.top + moveStep).toFixed(1);
setShipValues(card, sv); markChanged(card); updateInfo(); e.preventDefault(); break;
case '+': case '=':
sv.width = +(sv.width + sizeStep).toFixed(1);
sv.height = +(sv.height + sizeStep * (DEFAULTS.ship.height / DEFAULTS.ship.width)).toFixed(1);
setShipValues(card, sv); markChanged(card); updateInfo(); e.preventDefault(); break;
case '-': case '_':
sv.width = Math.max(20, +(sv.width - sizeStep).toFixed(1));
sv.height = Math.max(10, +(sv.height - sizeStep * (DEFAULTS.ship.height / DEFAULTS.ship.width)).toFixed(1));
setShipValues(card, sv); markChanged(card); updateInfo(); e.preventDefault(); break;
case '0':
setShipValues(card, Object.assign({}, DEFAULTS.ship));
markChanged(card); updateInfo(); e.preventDefault(); break;
}
} else if (mode === MODE_INNER) {
var iv = getInnerAstValues(card);
var pctStep = e.shiftKey ? 0.5 : 1;
switch (e.key) {
case 'ArrowLeft':
iv.left = +(iv.left - pctStep).toFixed(1);
setInnerAstValues(card, iv); markChanged(card); updateInfo(); e.preventDefault(); break;
case 'ArrowRight':
iv.left = +(iv.left + pctStep).toFixed(1);
setInnerAstValues(card, iv); markChanged(card); updateInfo(); e.preventDefault(); break;
case 'ArrowUp':
iv.top = +(iv.top - pctStep).toFixed(1);
setInnerAstValues(card, iv); markChanged(card); updateInfo(); e.preventDefault(); break;
case 'ArrowDown':
iv.top = +(iv.top + pctStep).toFixed(1);
setInnerAstValues(card, iv); markChanged(card); updateInfo(); e.preventDefault(); break;
case '0':
setInnerAstValues(card, Object.assign({}, DEFAULTS.innerAst));
// Also reset transform to just translate (remove scale if any)
card.innerAst.style.transform = 'translate(-50%, -50%)';
markChanged(card); updateInfo(); e.preventDefault(); break;
}
}
});
document.addEventListener('click', function(e) {
if (selected && !e.target.closest('.space-asteroid') && !e.target.closest('.cargo-ship') && !e.target.closest('.inner-ast')) {
deselectAll();
}
});
}
// --- Init via EditorCore ---
var core = EditorCore.init({
taskType: 'cargo-filling',
serialize: buildConfig,
onReset: resetCurrentPage,
onReady: function(pages, mmToPx) {
findCards(pages, mmToPx);
setupKeyboard();
},
onDataLoaded: function(data) {
if (data) applyData(data);
}
});
})();
</script>
</body>
</html>