feat: add screenshots
This commit is contained in:
File diff suppressed because one or more lines are too long
+132
-372
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user