feat: add screenshots

This commit is contained in:
2026-04-21 20:27:16 +07:00
parent ba676c2924
commit 0b9c9638b1
20 changed files with 1472 additions and 2690 deletions
File diff suppressed because one or more lines are too long
+132 -372
View File
@@ -45,7 +45,6 @@
}
.obj-draggable { cursor: grab; user-select: none; z-index: 20 !important; position: absolute; }
/* In node mode, objects become click-through */
body.node-mode .obj-draggable { pointer-events: none !important; z-index: 4 !important; }
body.node-mode .node-draggable { z-index: 20 !important; cursor: grab; }
.obj-draggable:hover { outline: 2px solid rgba(99, 102, 241, 0.6); outline-offset: 2px; }
@@ -55,7 +54,6 @@
width: 8px; height: 8px; background: #f59e0b; border-radius: 50%;
border: 1px solid #1e1e2e; z-index: 100;
}
/* Node dragging (Shift+click) */
.node-draggable { cursor: grab; user-select: none; }
.node-draggable:hover { box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.4) !important; }
.node-selected { box-shadow: 0 0 0 3px #f59e0b !important; }
@@ -79,7 +77,6 @@
background: rgba(0,0,0,0.6); color: white; padding: 2px 8px;
border-radius: 4px; font-size: 12px; pointer-events: none;
}
/* Route highlight */
line.route-highlight {
stroke: #f87171 !important; stroke-width: 2.5 !important;
opacity: 0.8 !important; stroke-dasharray: none !important;
@@ -117,100 +114,61 @@
<div id="coord-tooltip"></div>
<div id="toast"></div>
<script src="/src/editor/editor-core.js"></script>
<script>
(function() {
const params = new URLSearchParams(location.search);
const fileParam = params.get('file') || 'space-route-1';
const file = 'docs/' + fileParam + '.template.html';
const docId = fileParam;
let pages = [];
let objects = []; // draggable objects (images with data-node-id)
let nodeEls = []; // all node divs
let originalState = new Map(); // obj originals
let nodeOrigState = new Map(); // node originals
let selectedObj = null; // selected object OR node
let selectedType = null; // 'obj' or 'node'
// === Space-route specific state ===
let objects = [];
let nodeEls = [];
let originalState = new Map();
let nodeOrigState = new Map();
let selectedObj = null;
let selectedType = null; // 'obj' or 'node'
let dragging = null;
let dragStart = null;
let mmToPx = 1;
let currentPage = 0;
let routeHighlight = false;
let editMode = 'objects'; // 'objects' or 'nodes'
let editMode = 'objects';
fetch(file)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const container = document.getElementById('worksheet-container');
// === Init via EditorCore ===
const core = EditorCore.init({
taskType: 'space-route',
serialize: buildConfig,
onReset: resetCurrentPage,
onReady: function(pages, mmToPx) {
setupObjects();
setupNodes();
setupGlobalEvents();
const tw = document.createElement('script');
tw.src = 'https://cdn.tailwindcss.com';
document.head.appendChild(tw);
document.getElementById('btn-flip').addEventListener('click', flipSelected);
document.getElementById('btn-mode').addEventListener('click', toggleMode);
document.getElementById('btn-route').addEventListener('click', toggleRouteHL);
doc.querySelectorAll('head script:not([src])').forEach(s => {
const ns = document.createElement('script');
ns.textContent = s.textContent;
document.head.appendChild(ns);
});
doc.querySelectorAll('head style').forEach(s => {
const ns = document.createElement('style');
ns.textContent = s.textContent;
document.head.appendChild(ns);
});
doc.querySelectorAll('head link').forEach(l => {
document.head.appendChild(l.cloneNode(true));
});
container.innerHTML = doc.body.innerHTML;
setTimeout(() => initEditor(), 400);
})
.catch(err => {
document.getElementById('worksheet-container').innerHTML =
`<p style="padding:80px 20px;color:#f38ba8;">Failed to load ${file}: ${err.message}</p>`;
});
function initEditor() {
pages = Array.from(document.querySelectorAll('.w-\\[210mm\\]'));
pages.forEach((p, i) => {
p.dataset.page = i + 1;
p.style.position = 'relative';
const label = document.createElement('div');
label.className = 'page-label';
label.textContent = `Page ${i + 1}`;
p.appendChild(label);
});
document.getElementById('page-indicator').textContent = `Page 1 / ${pages.length}`;
if (pages.length > 0) {
mmToPx = pages[0].getBoundingClientRect().width / 210;
window.getConfig = () => buildConfig(false);
window.getChanges = () => buildConfig(true);
}
});
// Find all space objects (images with data-node-id)
if (!core) return;
// === Setup objects ===
function setupObjects() {
document.querySelectorAll('img.space-obj').forEach((img, idx) => {
const page = img.closest('[data-page]');
const pageNum = page ? parseInt(page.dataset.page) : 0;
const nodeId = img.dataset.nodeId;
// Wrap in a container div for positioning if not already
// Objects are already absolutely positioned images
const left = parseFloat(img.style.left) || 0;
const top = parseFloat(img.style.top) || 0;
const w = parseFloat(img.style.width) || 24;
const h = parseFloat(img.style.height) || 24;
// Parse transform
let rotate = 0, scaleX = 1, scaleY = 1;
if (img.style.transform) {
const rotM = img.style.transform.match(/rotate\(([-\d.]+)deg\)/);
const scxM = img.style.transform.match(/scaleX\(([-\d.]+)\)/);
const scyM = img.style.transform.match(/scaleY\(([-\d.]+)\)/);
const scM = img.style.transform.match(/scale\(([-\d.]+)\)/);
if (rotM) rotate = parseFloat(rotM[1]);
if (scxM) scaleX = parseFloat(scxM[1]);
if (scyM) scaleY = parseFloat(scyM[1]);
if (scM) scaleX = scaleY = parseFloat(scM[1]);
}
@@ -220,27 +178,23 @@
img.style.position = 'absolute';
originalState.set(id, { left, top, w, h, rotate, scaleX, scaleY });
img.classList.add('obj-draggable');
img.draggable = false;
objects.push({
el: img, id, pageNum, nodeId,
type: img.dataset.type,
src: img.src.split('/').pop()
});
objects.push({ el: img, id, pageNum, nodeId, type: img.dataset.type, src: img.src.split('/').pop() });
img.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
e.preventDefault(); e.stopPropagation();
selectObj(img, id);
startDrag(img, e);
});
});
}
// Init nodes (Shift+click to select)
// === Setup nodes ===
function setupNodes() {
document.querySelectorAll('[data-node-id]').forEach(nodeDiv => {
if (nodeDiv.tagName === 'IMG') return; // skip objects, only div nodes
if (nodeDiv.tagName === 'IMG') return;
const page = nodeDiv.closest('[data-page]');
const pageNum = page ? parseInt(page.dataset.page) : 0;
const nodeId = nodeDiv.dataset.nodeId;
@@ -252,95 +206,54 @@
nodeOrigState.set(nid, { left, top });
nodeDiv.classList.add('node-draggable');
nodeEls.push({ el: nodeDiv, nid, pageNum, nodeId });
nodeDiv.addEventListener('mousedown', (e) => {
if (editMode !== 'nodes') return;
e.preventDefault();
e.stopPropagation();
e.preventDefault(); e.stopPropagation();
selectNode(nodeDiv, nid);
startDrag(nodeDiv, e);
});
});
}
// Page scroll tracking
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && entry.intersectionRatio > 0.3) {
currentPage = parseInt(entry.target.dataset.page) - 1;
document.getElementById('page-indicator').textContent =
`Page ${currentPage + 1} / ${pages.length}`;
}
});
}, { threshold: [0.3] });
pages.forEach(p => observer.observe(p));
// === Global events ===
function setupGlobalEvents() {
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('keydown', onKeyDown);
document.getElementById('worksheet-container').addEventListener('mousedown', (e) => {
if (!e.target.closest('.obj-draggable')) deselectAll();
});
document.addEventListener('keydown', onKeyDown);
document.getElementById('btn-prev').addEventListener('click', () => scrollToPage(currentPage - 1));
document.getElementById('btn-next').addEventListener('click', () => scrollToPage(currentPage + 1));
document.getElementById('btn-copy').addEventListener('click', () => copyConfig(false));
document.getElementById('btn-copy-changes').addEventListener('click', () => copyConfig(true));
document.getElementById('btn-reset').addEventListener('click', resetCurrentPage);
document.getElementById('btn-flip').addEventListener('click', flipSelected);
document.getElementById('btn-mode').addEventListener('click', toggleMode);
document.getElementById('btn-route').addEventListener('click', toggleRouteHL);
document.getElementById('btn-save').addEventListener('click', saveToServer);
window.getConfig = () => buildConfig(false);
window.getChanges = () => buildConfig(true);
window.addEventListener('resize', () => {
if (pages.length > 0) mmToPx = pages[0].getBoundingClientRect().width / 210;
});
}
// === Selection ===
function selectObj(el, id) {
deselectAll();
el.classList.add('obj-selected');
selectedObj = el;
selectedType = 'obj';
updateStatus(el, id);
selectedObj = el; selectedType = 'obj';
updateObjStatus(el, id);
}
function selectNode(el, nid) {
deselectAll();
el.classList.add('node-selected');
selectedObj = el;
selectedType = 'node';
const left = parseFloat(el.style.left).toFixed(1);
const top = parseFloat(el.style.top).toFixed(1);
selectedObj = el; selectedType = 'node';
const val = el.querySelector('span')?.textContent || '?';
document.getElementById('status-info').textContent =
`NODE | Page ${el.dataset.pageNum} | id:${el.dataset.nodeId} | value:${val}`;
document.getElementById('status-pos').textContent = `pos: ${left}, ${top}mm`;
document.getElementById('status-info').textContent = `NODE | Page ${el.dataset.pageNum} | id:${el.dataset.nodeId} | value:${val}`;
document.getElementById('status-pos').textContent = `pos: ${parseFloat(el.style.left).toFixed(1)}, ${parseFloat(el.style.top).toFixed(1)}mm`;
}
function deselectAll() {
document.querySelectorAll('.obj-selected').forEach(el => el.classList.remove('obj-selected'));
document.querySelectorAll('.node-selected').forEach(el => el.classList.remove('node-selected'));
selectedObj = null;
selectedType = null;
document.getElementById('status-info').textContent = 'Click object / Shift+click node';
document.querySelectorAll('.obj-selected, .node-selected').forEach(el => el.classList.remove('obj-selected', 'node-selected'));
selectedObj = null; selectedType = null;
document.getElementById('status-info').textContent = editMode === 'objects' ? 'Click an object to select' : 'Click a node to select';
document.getElementById('status-pos').textContent = '';
}
function updateStatus(el, id) {
const left = parseFloat(el.style.left).toFixed(1);
const top = parseFloat(el.style.top).toFixed(1);
const w = parseFloat(el.style.width).toFixed(0);
const h = parseFloat(el.style.height).toFixed(0);
function updateObjStatus(el, id) {
const obj = objects.find(o => o.id === id);
const page = el.dataset.pageNum || '?';
let rotStr = '0', flipStr = '';
if (el.style.transform) {
const rotM = el.style.transform.match(/rotate\(([-\d.]+)deg\)/);
@@ -348,93 +261,49 @@
if (rotM) rotStr = Math.round(parseFloat(rotM[1]));
if (scxM && parseFloat(scxM[1]) < 0) flipStr = ' FLIP';
}
document.getElementById('status-info').textContent =
`Page ${page} | node:${obj?.nodeId} | ${obj?.src || ''}`;
document.getElementById('status-pos').textContent =
`pos: ${left}, ${top}mm | size: ${w}×${h}mm | rot: ${rotStr}°${flipStr}`;
document.getElementById('status-info').textContent = `Page ${el.dataset.pageNum} | node:${obj?.nodeId} | ${obj?.src || ''}`;
document.getElementById('status-pos').textContent = `pos: ${parseFloat(el.style.left).toFixed(1)}, ${parseFloat(el.style.top).toFixed(1)}mm | size: ${parseFloat(el.style.width).toFixed(0)}×${parseFloat(el.style.height).toFixed(0)}mm | rot: ${rotStr}°${flipStr}`;
}
// Move the corresponding node and connected edges when an object moves
// === Graph helpers ===
function moveNodeAndEdges(el, newLeft, newTop) {
const nodeId = el.dataset.nodeId;
if (!nodeId) return;
const page = el.closest('[data-page]');
if (!page) return;
// Find the node div
const nodeDiv = page.querySelector(`[data-node-id="${nodeId}"]`);
if (nodeDiv) {
nodeDiv.style.left = newLeft.toFixed(1) + 'mm';
nodeDiv.style.top = newTop.toFixed(1) + 'mm';
}
// Update all edges connected to this node
const svg = page.querySelector('svg');
if (!svg) return;
svg.querySelectorAll(`line[data-edge]`).forEach(line => {
const [a, b] = line.dataset.edge.split('-');
if (a === nodeId) {
line.setAttribute('x1', newLeft.toFixed(1) + 'mm');
line.setAttribute('y1', newTop.toFixed(1) + 'mm');
}
if (b === nodeId) {
line.setAttribute('x2', newLeft.toFixed(1) + 'mm');
line.setAttribute('y2', newTop.toFixed(1) + 'mm');
}
});
if (nodeDiv) { nodeDiv.style.left = newLeft.toFixed(1) + 'mm'; nodeDiv.style.top = newTop.toFixed(1) + 'mm'; }
_updateEdges(page, nodeId, newLeft, newTop);
}
// Move only edges for a node (without moving the node itself — used when dragging node directly)
function moveEdgesForNode(nodeDiv, newLeft, newTop) {
const nodeId = nodeDiv.dataset.nodeId;
const page = nodeDiv.closest('[data-page]');
if (!page) return;
_updateEdges(page, nodeDiv.dataset.nodeId, newLeft, newTop);
}
function _updateEdges(page, nodeId, left, top) {
const svg = page.querySelector('svg');
if (!svg) return;
svg.querySelectorAll('line[data-edge]').forEach(line => {
const [a, b] = line.dataset.edge.split('-');
if (a === nodeId) {
line.setAttribute('x1', newLeft.toFixed(1) + 'mm');
line.setAttribute('y1', newTop.toFixed(1) + 'mm');
}
if (b === nodeId) {
line.setAttribute('x2', newLeft.toFixed(1) + 'mm');
line.setAttribute('y2', newTop.toFixed(1) + 'mm');
}
if (a === nodeId) { line.setAttribute('x1', left.toFixed(1) + 'mm'); line.setAttribute('y1', top.toFixed(1) + 'mm'); }
if (b === nodeId) { line.setAttribute('x2', left.toFixed(1) + 'mm'); line.setAttribute('y2', top.toFixed(1) + 'mm'); }
});
}
function markNodeChanged(nodeDiv) {
const nid = nodeDiv.dataset.nid;
const orig = nodeOrigState.get(nid);
if (!orig) return;
const curLeft = parseFloat(nodeDiv.style.left);
const curTop = parseFloat(nodeDiv.style.top);
const changed = Math.abs(orig.left - curLeft) > 0.3 || Math.abs(orig.top - curTop) > 0.3;
nodeDiv.classList.toggle('node-changed', changed);
}
// === Dragging ===
function startDrag(el, e) {
dragging = el;
dragStart = {
mouseX: e.clientX, mouseY: e.clientY,
left: parseFloat(el.style.left) || 0,
top: parseFloat(el.style.top) || 0
};
dragStart = { mouseX: e.clientX, mouseY: e.clientY, left: parseFloat(el.style.left) || 0, top: parseFloat(el.style.top) || 0 };
document.body.classList.add('dragging');
document.getElementById('coord-tooltip').style.display = 'block';
}
function onMouseMove(e) {
if (!dragging) return;
e.preventDefault();
const dx = (e.clientX - dragStart.mouseX) / mmToPx;
const dy = (e.clientY - dragStart.mouseY) / mmToPx;
const dx = (e.clientX - dragStart.mouseX) / core.mmToPx;
const dy = (e.clientY - dragStart.mouseY) / core.mmToPx;
const newLeft = Math.round((dragStart.left + dx) * 2) / 2;
const newTop = Math.round((dragStart.top + dy) * 2) / 2;
@@ -442,60 +311,48 @@
dragging.style.top = newTop.toFixed(1) + 'mm';
if (selectedType === 'obj') {
// Update margin to keep object centered
const w = parseFloat(dragging.style.width) || 24;
const h = parseFloat(dragging.style.height) || 24;
dragging.style.marginLeft = (-w / 2) + 'mm';
dragging.style.marginTop = (-h / 2) + 'mm';
updateStatus(dragging, dragging.dataset.objId);
markChanged(dragging);
updateObjStatus(dragging, dragging.dataset.objId);
markObjChanged(dragging);
} else if (selectedType === 'node') {
// Update margin for node centering
const size = parseFloat(dragging.style.width) || 10;
dragging.style.marginLeft = (-size / 2) + 'mm';
dragging.style.marginTop = (-size / 2) + 'mm';
// Move connected edges
moveEdgesForNode(dragging, newLeft, newTop);
markNodeChanged(dragging);
// Update status
const val = dragging.querySelector('span')?.textContent || '?';
document.getElementById('status-info').textContent =
`NODE | Page ${dragging.dataset.pageNum} | id:${dragging.dataset.nodeId} | value:${val}`;
document.getElementById('status-pos').textContent = `pos: ${newLeft.toFixed(1)}, ${newTop.toFixed(1)}mm`;
}
const tooltip = document.getElementById('coord-tooltip');
tooltip.textContent = `${newLeft.toFixed(1)}, ${newTop.toFixed(1)}mm`;
tooltip.style.left = (e.clientX + 15) + 'px';
tooltip.style.top = (e.clientY - 10) + 'px';
core.showTooltip(e, `${newLeft.toFixed(1)}, ${newTop.toFixed(1)}mm`);
}
function onMouseUp() {
if (!dragging) return;
document.body.classList.remove('dragging');
document.getElementById('coord-tooltip').style.display = 'none';
dragging = null;
dragStart = null;
core.hideTooltip();
dragging = null; dragStart = null;
}
// === Keyboard ===
function onKeyDown(e) {
if (e.key === 'm' || e.key === 'M') { toggleMode(); return; }
if (e.key === 'r' || e.key === 'R') { toggleRouteHL(); return; }
if (e.key === 'h' || e.key === 'H') { if (selectedType === 'obj') flipSelected(); return; }
if (e.key === 'Escape') { deselectAll(); return; }
if (!selectedObj) return;
// Node mode: only arrows
if (selectedType === 'node') {
const step = e.shiftKey ? 5 : 1;
let left = parseFloat(selectedObj.style.left) || 0;
let top = parseFloat(selectedObj.style.top) || 0;
switch (e.key) {
case 'ArrowLeft': left -= step; break;
case 'ArrowLeft': left -= step; break;
case 'ArrowRight': left += step; break;
case 'ArrowUp': top -= step; break;
case 'ArrowDown': top += step; break;
case 'ArrowUp': top -= step; break;
case 'ArrowDown': top += step; break;
default: return;
}
e.preventDefault();
@@ -506,12 +363,11 @@
selectedObj.style.marginTop = (-size / 2) + 'mm';
moveEdgesForNode(selectedObj, left, top);
markNodeChanged(selectedObj);
const val = selectedObj.querySelector('span')?.textContent || '?';
document.getElementById('status-pos').textContent = `pos: ${left.toFixed(1)}, ${top.toFixed(1)}mm`;
return;
}
// Object mode: arrows + rotate + resize
// Object mode
const step = e.shiftKey ? 5 : 1;
let left = parseFloat(selectedObj.style.left) || 0;
let top = parseFloat(selectedObj.style.top) || 0;
@@ -521,69 +377,53 @@
const rotStep = e.shiftKey ? 1 : 5;
switch (e.key) {
case 'ArrowLeft': left -= step; break;
case 'ArrowLeft': left -= step; break;
case 'ArrowRight': left += step; break;
case 'ArrowUp': top -= step; break;
case 'ArrowDown': top += step; break;
case 'ArrowUp': top -= step; break;
case 'ArrowDown': top += step; break;
case '[': applyRotate(selectedObj, -rotStep); break;
case ']': applyRotate(selectedObj, rotStep); break;
case '-': case '_':
w = Math.max(8, w - sizeStep);
h = Math.max(8, h - sizeStep * (h / w));
selectedObj.style.width = w + 'mm';
selectedObj.style.height = h + 'mm';
selectedObj.style.marginLeft = (-w / 2) + 'mm';
selectedObj.style.marginTop = (-h / 2) + 'mm';
w = Math.max(8, w - sizeStep); h = Math.max(8, h - sizeStep * (h / w));
selectedObj.style.width = w + 'mm'; selectedObj.style.height = h + 'mm';
selectedObj.style.marginLeft = (-w / 2) + 'mm'; selectedObj.style.marginTop = (-h / 2) + 'mm';
break;
case '=': case '+':
w = Math.min(60, w + sizeStep);
h = Math.min(60, h + sizeStep * (h / w));
selectedObj.style.width = w + 'mm';
selectedObj.style.height = h + 'mm';
selectedObj.style.marginLeft = (-w / 2) + 'mm';
selectedObj.style.marginTop = (-h / 2) + 'mm';
w = Math.min(60, w + sizeStep); h = Math.min(60, h + sizeStep * (h / w));
selectedObj.style.width = w + 'mm'; selectedObj.style.height = h + 'mm';
selectedObj.style.marginLeft = (-w / 2) + 'mm'; selectedObj.style.marginTop = (-h / 2) + 'mm';
break;
default: return;
}
e.preventDefault();
selectedObj.style.left = left.toFixed(1) + 'mm';
selectedObj.style.top = top.toFixed(1) + 'mm';
selectedObj.style.marginLeft = (-parseFloat(selectedObj.style.width) / 2) + 'mm';
selectedObj.style.marginTop = (-parseFloat(selectedObj.style.height) / 2) + 'mm';
updateStatus(selectedObj, selectedObj.dataset.objId);
markChanged(selectedObj);
updateObjStatus(selectedObj, selectedObj.dataset.objId);
markObjChanged(selectedObj);
}
// === Transform helpers ===
function applyRotate(el, delta) {
let transform = el.style.transform || '';
const rotM = transform.match(/rotate\(([-\d.]+)deg\)/);
let rot = rotM ? parseFloat(rotM[1]) : 0;
rot += delta;
if (rotM) {
transform = transform.replace(/rotate\([-\d.]+deg\)/, `rotate(${rot}deg)`);
} else {
transform += ` rotate(${rot}deg)`;
}
transform = rotM ? transform.replace(/rotate\([-\d.]+deg\)/, `rotate(${rot}deg)`) : transform + ` rotate(${rot}deg)`;
el.style.transform = transform.trim();
markChanged(el);
markObjChanged(el);
}
function flipSelected() {
if (!selectedObj) return;
let transform = selectedObj.style.transform || '';
const scxM = transform.match(/scaleX\(([-\d.]+)\)/);
let scx = scxM ? parseFloat(scxM[1]) : 1;
scx = scx < 0 ? 1 : -1;
if (scxM) {
transform = transform.replace(/scaleX\([-\d.]+\)/, `scaleX(${scx})`);
} else {
transform += ` scaleX(${scx})`;
}
let scx = scxM ? (parseFloat(scxM[1]) < 0 ? 1 : -1) : -1;
transform = scxM ? transform.replace(/scaleX\([-\d.]+\)/, `scaleX(${scx})`) : transform + ` scaleX(${scx})`;
selectedObj.style.transform = transform.trim();
updateStatus(selectedObj, selectedObj.dataset.objId);
markChanged(selectedObj);
updateObjStatus(selectedObj, selectedObj.dataset.objId);
markObjChanged(selectedObj);
}
function toggleMode() {
@@ -593,55 +433,30 @@
const btn = document.getElementById('btn-mode');
btn.textContent = editMode === 'objects' ? 'Mode: Objects' : 'Mode: Nodes';
btn.classList.toggle('active', editMode === 'nodes');
document.getElementById('status-info').textContent =
editMode === 'objects' ? 'Click an object to select' : 'Click a node to select';
}
function toggleRouteHL() {
routeHighlight = !routeHighlight;
const btn = document.getElementById('btn-route');
btn.classList.toggle('active', routeHighlight);
pages.forEach(pageEl => {
document.getElementById('btn-route').classList.toggle('active', routeHighlight);
core.pages.forEach(pageEl => {
const routeStr = pageEl.dataset.enemyRoute;
if (!routeStr) return;
const route = routeStr.split(',');
// Build set of edge keys on the route
const routeEdges = new Set();
for (let i = 0; i < route.length - 1; i++) {
const a = parseInt(route[i]), b = parseInt(route[i + 1]);
routeEdges.add(`${Math.min(a, b)}-${Math.max(a, b)}`);
}
pageEl.querySelectorAll('line[data-edge]').forEach(line => {
const [ea, eb] = line.dataset.edge.split('-').map(Number);
const key = `${Math.min(ea, eb)}-${Math.max(ea, eb)}`;
if (routeEdges.has(key)) {
if (routeEdges.has(`${Math.min(ea, eb)}-${Math.max(ea, eb)}`)) {
line.classList.toggle('route-highlight', routeHighlight);
}
});
});
}
function markChanged(el) {
const id = el.dataset.objId;
const orig = originalState.get(id);
if (!orig) return;
const curLeft = parseFloat(el.style.left);
const curTop = parseFloat(el.style.top);
const curW = parseFloat(el.style.width);
const curH = parseFloat(el.style.height);
const changed = (
Math.abs(orig.left - curLeft) > 0.3 ||
Math.abs(orig.top - curTop) > 0.3 ||
Math.abs(orig.w - curW) > 0.3 ||
Math.abs(orig.h - curH) > 0.3 ||
(el.style.transform || '') !== buildOrigTransform(orig)
);
el.classList.toggle('obj-changed', changed);
}
// === Change tracking ===
function buildOrigTransform(orig) {
let t = '';
if (orig.scaleX < 0) t += `scaleX(${orig.scaleX})`;
@@ -649,25 +464,37 @@
return t.trim();
}
function scrollToPage(index) {
if (index < 0 || index >= pages.length) return;
pages[index].scrollIntoView({ behavior: 'smooth', block: 'start' });
function markObjChanged(el) {
const id = el.dataset.objId;
const orig = originalState.get(id);
if (!orig) return;
const changed = Math.abs(orig.left - parseFloat(el.style.left)) > 0.3 ||
Math.abs(orig.top - parseFloat(el.style.top)) > 0.3 ||
Math.abs(orig.w - parseFloat(el.style.width)) > 0.3 ||
Math.abs(orig.h - parseFloat(el.style.height)) > 0.3 ||
(el.style.transform || '') !== buildOrigTransform(orig);
el.classList.toggle('obj-changed', changed);
}
function buildConfig(changesOnly) {
const result = { file, pages: [] };
function markNodeChanged(nodeDiv) {
const nid = nodeDiv.dataset.nid;
const orig = nodeOrigState.get(nid);
if (!orig) return;
const changed = Math.abs(orig.left - parseFloat(nodeDiv.style.left)) > 0.3 || Math.abs(orig.top - parseFloat(nodeDiv.style.top)) > 0.3;
nodeDiv.classList.toggle('node-changed', changed);
}
pages.forEach((pageEl, pageIndex) => {
// === Serialization ===
function buildConfig(changesOnly) {
const result = { file: core.docId, pages: [] };
core.pages.forEach((pageEl, pageIndex) => {
const pageObjs = objects.filter(o => o.pageNum === pageIndex + 1);
const items = [];
pageObjs.forEach(obj => {
const el = obj.el;
const left = parseFloat(el.style.left);
const top = parseFloat(el.style.top);
const w = parseFloat(el.style.width);
const h = parseFloat(el.style.height);
const left = parseFloat(el.style.left), top = parseFloat(el.style.top);
const w = parseFloat(el.style.width), h = parseFloat(el.style.height);
let rotate = 0, flipH = false;
if (el.style.transform) {
const rotM = el.style.transform.match(/rotate\(([-\d.]+)deg\)/);
@@ -675,44 +502,23 @@
if (rotM) rotate = Math.round(parseFloat(rotM[1]));
if (scxM && parseFloat(scxM[1]) < 0) flipH = true;
}
const orig = originalState.get(obj.id);
if (changesOnly && orig) {
const origTransform = buildOrigTransform(orig);
const same = Math.abs(orig.left - left) < 0.3 &&
Math.abs(orig.top - top) < 0.3 &&
Math.abs(orig.w - w) < 0.3 &&
Math.abs(orig.h - h) < 0.3 &&
(el.style.transform || '') === origTransform;
const same = Math.abs(orig.left - left) < 0.3 && Math.abs(orig.top - top) < 0.3 &&
Math.abs(orig.w - w) < 0.3 && Math.abs(orig.h - h) < 0.3 &&
(el.style.transform || '') === buildOrigTransform(orig);
if (same) return;
}
items.push({
nodeId: parseInt(obj.nodeId),
type: obj.type,
src: obj.src,
left: left.toFixed(1) + 'mm',
top: top.toFixed(1) + 'mm',
w: w.toFixed(0) + 'mm',
h: h.toFixed(0) + 'mm',
rotate, flipH
});
items.push({ nodeId: parseInt(obj.nodeId), type: obj.type, src: obj.src, left: left.toFixed(1) + 'mm', top: top.toFixed(1) + 'mm', w: w.toFixed(0) + 'mm', h: h.toFixed(0) + 'mm', rotate, flipH });
});
// Collect changed nodes for this page
const pageNodes = nodeEls.filter(n => n.pageNum === pageIndex + 1);
const movedNodes = [];
pageNodes.forEach(nd => {
const el = nd.el;
const left = parseFloat(el.style.left);
const top = parseFloat(el.style.top);
const left = parseFloat(nd.el.style.left), top = parseFloat(nd.el.style.top);
const orig = nodeOrigState.get(nd.nid);
if (changesOnly && orig && Math.abs(orig.left - left) < 0.3 && Math.abs(orig.top - top) < 0.3) return;
movedNodes.push({
nodeId: parseInt(nd.nodeId),
left: left.toFixed(1) + 'mm',
top: top.toFixed(1) + 'mm'
});
movedNodes.push({ nodeId: parseInt(nd.nodeId), left: left.toFixed(1) + 'mm', top: top.toFixed(1) + 'mm' });
});
if (items.length > 0 || movedNodes.length > 0) {
@@ -722,70 +528,24 @@
result.pages.push(pageData);
}
});
return result;
}
function copyConfig(changesOnly) {
const config = buildConfig(changesOnly);
const json = JSON.stringify(config, null, 2);
navigator.clipboard.writeText(json).then(() => {
showToast(changesOnly ? 'Changes copied!' : 'Full config copied!');
}).catch(() => {
console.log(json);
showToast('Copied to console (clipboard blocked)');
});
}
function resetCurrentPage() {
const pageNum = currentPage + 1;
// === Reset ===
function resetCurrentPage(pageNum) {
objects.filter(o => o.pageNum === pageNum).forEach(obj => {
const orig = originalState.get(obj.id);
if (!orig) return;
obj.el.style.left = orig.left + 'mm';
obj.el.style.top = orig.top + 'mm';
obj.el.style.width = orig.w + 'mm';
obj.el.style.height = orig.h + 'mm';
obj.el.style.marginLeft = (-orig.w / 2) + 'mm';
obj.el.style.marginTop = (-orig.h / 2) + 'mm';
obj.el.style.left = orig.left + 'mm'; obj.el.style.top = orig.top + 'mm';
obj.el.style.width = orig.w + 'mm'; obj.el.style.height = orig.h + 'mm';
obj.el.style.marginLeft = (-orig.w / 2) + 'mm'; obj.el.style.marginTop = (-orig.h / 2) + 'mm';
obj.el.style.transform = buildOrigTransform(orig);
obj.el.classList.remove('obj-changed');
// Reset node and edges too
moveNodeAndEdges(obj.el, orig.left, orig.top);
});
showToast(`Page ${pageNum} reset`);
core.showToast(`Page ${pageNum} reset`);
}
async function saveToServer() {
const config = buildConfig(false);
try {
const resp = await fetch('/api/save-edits', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
taskType: 'space-route',
docId: docId,
data: config
})
});
const result = await resp.json();
if (result.ok) {
showToast('Saved!');
} else {
showToast('Error: ' + result.error);
}
} catch (e) {
showToast('Save failed: ' + e.message);
}
}
function showToast(msg) {
const toast = document.getElementById('toast');
toast.textContent = msg;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 1500);
}
})();
</script>
</body>
+2
View File
@@ -23,6 +23,7 @@
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { postGenerate } from '../../../src/scripts/post-generate.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const docsDir = join(__dirname, '..', 'docs');
@@ -52,6 +53,7 @@ if (existsSync(dataPath)) {
writeFileSync(outputPath, html);
console.log(`Generated: ${outputPath}`);
await postGenerate(outputPath);
function applyData(html, data) {
if (!data.pages) return html;