coordinate-maker / index.html
junjiro1129's picture
Upgrade file
2a05f0c verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Coordinate Maker</title>
<meta name="viewport" content="width=800,user-scalable=no">
<style>
/* ...(CSSは変更不要なので省略。前回そのまま)... */
#img-container {
position: relative;
width: 800px;
height: 600px;
border: 2px dashed #aaa;
background: #f9f9f9;
user-select: none;
display: inline-block;
overflow: hidden;
transition: border-color 0.2s, background 0.2s;
}
#img-container.dragover {
border-color: #2a7;
background: #e2ffe9;
}
#the-img {
width: 100%;
height: 100%;
object-fit: contain;
user-select: none;
pointer-events: none;
display: none;
}
.rect {
position: absolute;
border: 2px solid #2a7;
background: rgba(44,222,88,0.15);
box-sizing: border-box;
cursor: move;
pointer-events: auto;
transition: box-shadow 0.2s;
}
.rect.selected {
border: 2px solid #f00;
background: rgba(255,100,100,0.15);
z-index: 10;
box-shadow: 0 0 10px #f00a;
}
.handle {
position: absolute;
width: 14px; height: 14px;
margin: -7px 0 0 -7px;
background: #2a7;
border-radius: 50%;
cursor: pointer;
z-index: 20;
border: 2px solid #fff;
box-shadow: 0 0 2px #0004;
display: block;
}
.rect.selected .handle {
background: #f00;
}
.rotate-handle, .quad-rotate-handle {
position: absolute;
width: 18px;
height: 18px;
background: #ff0;
border: 2px solid #f90;
border-radius: 50%;
cursor: grab;
z-index: 30;
display: block;
box-shadow: 0 0 4px #0006;
}
.rotate-handle {
left: 50%;
top: -32px;
transform: translate(-50%,0);
}
.quad-svg {
position: absolute;
left: 0; top: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 2;
}
.quad-svg polygon {
fill: rgba(44,222,88,0.15);
stroke: #f00;
stroke-width: 2;
pointer-events: stroke;
}
#coords {
font-family: monospace;
margin-top: 10px;
background: #f8f8f8;
padding: 10px;
border: 1px solid #bbb;
width: 480px;
min-height: 120px;
}
#drop-message {
position: absolute;
top: 50%;
left: 50%;
width: 90%;
transform: translate(-50%, -50%);
color: #2a7;
font-size: 1.6em;
text-align: center;
pointer-events: none;
display: none;
background: rgba(255,255,255,0.75);
border-radius: 10px;
padding: 0.5em 0;
z-index: 1000;
}
#toolbar {
margin-bottom: 0.5em;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
}
#toolbar button, #toolbar input[type="file"] {
margin-right: 1em;
padding: 0.5em 1em;
font-size: 1em;
cursor: pointer;
}
#filename-input {
padding: 0.5em 1em;
font-size: 1em;
width: 220px;
margin-right: 1em;
}
#file-name-label {
font-size: 0.98em;
margin-right: 1em;
color: #333;
min-width: 120px;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
vertical-align: middle;
}
#memory-list {
margin-top: 1em;
font-family: monospace;
font-size: 1em;
background: #fff;
border: 1px solid #bbb;
padding: 8px 12px;
width: 480px;
min-height: 40px;
}
.memory-item {
margin-bottom: 0.5em;
border-bottom: 1px dotted #ccc;
padding-bottom: 0.3em;
display: flex;
align-items: center;
justify-content: space-between;
}
.memory-item:last-child {
border-bottom: none;
}
.memory-item .memory-label {
display: inline-block;
max-width: 360px;
word-break: break-all;
}
.memory-item .memory-delete-btn {
color: #c00;
background: transparent;
border: none;
cursor: pointer;
margin-left: 1em;
font-size: 1.1em;
}
#add-rect-btn, #add-quad-btn, #save-rect-btn, #memory-save-btn {
margin-right: 1em;
padding: 0.5em 1em;
font-size: 1em;
cursor: pointer;
}
#save-rect-btn:disabled {
color: #ccc;
border-color: #ccc;
cursor: default;
}
input[type="file"]::-webkit-file-upload-button { visibility: visible; }
input[type="file"]::file-selector-button { visibility: visible; }
input[type="file"]::-ms-value { display: none; }
input[type="file"]::-webkit-file-upload-button,
input[type="file"]::before {
content: none !important;
}
input[type="file"] {
color: transparent;
}
</style>
</head>
<body>
<h2>Coordinate Maker</h2>
<div id="toolbar">
<label style="display:inline-block; position:relative;">
<input type="file" id="img-input" accept="image/*" style="width:140px;">
<span style="position:absolute;left:12px;top:8px;pointer-events:none;color:#555;font-size:1em;" id="file-input-label">Choose File</span>
</label>
<span id="file-name-label"></span>
<button id="add-rect-btn">Rectangle Mode</button>
<button id="add-quad-btn">Free Transform Mode</button>
<button id="save-rect-btn" disabled>Save Rectangle</button>
<input type="text" id="filename-input" placeholder="templates.json">
<button id="memory-save-btn">Save Memory to File</button>
</div>
<div id="img-container">
<span id="drop-message">Drop image here</span>
<img id="the-img">
</div>
<div id="coords">Coordinates:<br></div>
<div id="memory-list"></div>
<script>
// === DOM取得 ===
const imgInput = document.getElementById('img-input');
const img = document.getElementById('the-img');
const container = document.getElementById('img-container');
const coords = document.getElementById('coords');
const dropMessage = document.getElementById('drop-message');
const addRectBtn = document.getElementById('add-rect-btn');
const addQuadBtn = document.getElementById('add-quad-btn');
const saveRectBtn = document.getElementById('save-rect-btn');
const memorySaveBtn = document.getElementById('memory-save-btn');
const memoryList = document.getElementById('memory-list');
const filenameInput = document.getElementById('filename-input');
const fileNameLabel = document.getElementById('file-name-label');
const fileInputLabel = document.getElementById('file-input-label');
// --- State ---
let imgLoaded = false, imgNaturalWidth = 0, imgNaturalHeight = 0;
let dispImgInfo = {left:0, top:0, width:0, height:0};
let currentImageName = '';
let currentMode = "rect"; // "rect" or "quad"
let rectObj = null;
let drawing = false, moving = false, resizing = false, rotating = false;
let startX = 0, startY = 0, offsetX = 0, offsetY = 0, originX = 0, originY = 0, rotateStartAngle = 0;
let activeHandle = null, memory = [];
let quadPoints = null; // only for quad mode
let quadSVG = null;
let rotateHandle = null;
let quadRotateHandle = null;
// --- File input label (hide when file is selected) ---
imgInput.addEventListener('change', (e) => {
if(imgInput.files && imgInput.files.length) fileInputLabel.style.display = 'none';
else fileInputLabel.style.display = '';
});
imgInput.addEventListener('input', (e) => {
if(imgInput.files && imgInput.files.length) fileInputLabel.style.display = 'none';
else fileInputLabel.style.display = '';
});
// --- Mode Buttons ---
addRectBtn.onclick = () => {
currentMode = "rect";
addRectBtn.style.background = "#baffba";
addQuadBtn.style.background = "";
clearRect(); saveRectBtn.disabled = true;
};
addQuadBtn.onclick = () => {
currentMode = "quad";
addQuadBtn.style.background = "#baffba";
addRectBtn.style.background = "";
clearRect(); saveRectBtn.disabled = true;
};
addRectBtn.style.background = "#baffba";
function updateFileNameLabel(name) { fileNameLabel.textContent = name ? name : ""; }
function loadImageFromFile(file) {
if (!file || !file.type.match(/^image\//)) return;
const reader = new FileReader();
reader.onload = ev => { img.src = ev.target.result; currentImageName = file.name || ''; updateFileNameLabel(currentImageName); };
reader.readAsDataURL(file);
}
imgInput.addEventListener('change', e => {
const file = e.target.files[0];
imgInput.value = "";
updateFileNameLabel("");
if (file) loadImageFromFile(file);
});
container.addEventListener('dragover', e => { e.preventDefault(); e.stopPropagation(); container.classList.add('dragover'); dropMessage.textContent = "Drop image here"; dropMessage.style.display = 'block'; });
container.addEventListener('dragleave', e => { e.preventDefault(); e.stopPropagation(); container.classList.remove('dragover'); dropMessage.style.display = 'none'; });
container.addEventListener('drop', e => {
e.preventDefault(); e.stopPropagation();
container.classList.remove('dragover'); dropMessage.style.display = 'none';
if (e.dataTransfer.files && e.dataTransfer.files[0]) { const file = e.dataTransfer.files[0]; updateFileNameLabel(""); loadImageFromFile(file); }
});
img.addEventListener('load', () => {
imgLoaded = true; img.style.display = "block";
imgNaturalWidth = img.naturalWidth; imgNaturalHeight = img.naturalHeight;
img.style.width = '100%'; img.style.height = '100%'; img.style.objectFit = 'contain';
clearRect(); updateDispImgInfo(); updateCoords();
});
function updateDispImgInfo() {
const cW = container.clientWidth, cH = container.clientHeight, iW = imgNaturalWidth, iH = imgNaturalHeight;
let dispW = cW, dispH = cH, dispL = 0, dispT = 0, imgAspect = iW / iH, contAspect = cW / cH;
if (imgAspect > contAspect) { dispW = cW; dispH = cW / imgAspect; dispT = (cH - dispH) / 2; dispL = 0; }
else { dispH = cH; dispW = cH * imgAspect; dispL = (cW - dispW) / 2; dispT = 0; }
dispImgInfo = {left:dispL, top:dispT, width:dispW, height:dispH};
}
window.addEventListener('resize', updateDispImgInfo);
// --- Drawing ---
container.addEventListener('mousedown', e => {
if (!imgLoaded || drawing || rectObj) return;
if (e.target !== container && e.target !== img) return;
const rectC = container.getBoundingClientRect();
startX = e.clientX - rectC.left; startY = e.clientY - rectC.top;
let el = document.createElement('div');
el.className = 'rect selected';
container.appendChild(el);
rectObj = {
type: currentMode,
el: el,
x: startX, y: startY, w: 0, h: 0,
angle: 0, handles: []
};
setSelectedRect(true); updateRectUI(rectObj);
drawing = true; saveRectBtn.disabled = true;
function onMove(ev) {
const currX = ev.clientX - rectC.left, currY = ev.clientY - rectC.top;
rectObj.x = Math.min(startX, currX); rectObj.y = Math.min(startY, currY);
rectObj.w = Math.abs(currX - startX); rectObj.h = Math.abs(currY - startY);
updateRectUI(rectObj); updateCoords(rectObj);
}
function onUp(ev) {
drawing = false; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp);
if (rectObj.w < 10 || rectObj.h < 10) { clearRect(); saveRectBtn.disabled = true; }
else {
createHandles(rectObj);
if(currentMode==="rect") createRotateHandle(rectObj);
else { quadPoints = null; createHandles(rectObj); }
updateCoords(rectObj); saveRectBtn.disabled = false;
}
}
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
e.preventDefault();
});
// --- Rect/Quad move ---
container.addEventListener('mousedown', e => {
if (drawing || !imgLoaded) return;
if (!rectObj || !rectObj.el) return;
if (e.target === rectObj.el && !rotating) {
setSelectedRect(true); saveRectBtn.disabled = false; moving = true;
const rectC = container.getBoundingClientRect();
offsetX = e.clientX - rectC.left - rectObj.x;
offsetY = e.clientY - rectC.top - rectObj.y;
document.body.style.cursor = "move";
function onMove(ev) {
let newX = ev.clientX - rectC.left - offsetX, newY = ev.clientY - rectC.top - offsetY;
if(currentMode==="quad" && quadPoints) {
let dx = newX - rectObj.x, dy = newY - rectObj.y;
for(let i=0;i<4;i++) {
quadPoints[i][0] += dx;
quadPoints[i][1] += dy;
}
rectObj.x = newX; rectObj.y = newY;
updateQuadSVG();
updateHandlesQuad();
updateCoordsQuad();
updateQuadRotateHandle();
} else {
rectObj.x = newX; rectObj.y = newY; updateRectUI(rectObj); updateHandles(rectObj); updateRotateHandle(rectObj); updateCoords(rectObj);
}
}
function onUp(ev) {
moving = false; document.body.style.cursor = ""; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); e.stopPropagation();
}
});
// --- Handles (resize/drag) ---
function createHandles(rectObj) {
if (rectObj.handles && rectObj.handles.length) rectObj.handles.forEach(h=>h.remove());
rectObj.handles = [];
const positions = [
{name:"tl", left:0, top:0},
{name:"tr", left:1, top:0},
{name:"br", left:1, top:1},
{name:"bl", left:0, top:1},
];
for(let i=0; i<4; i++) {
let h = document.createElement('div');
h.className = 'handle';
h.dataset.handle = positions[i].name;
h.style.left = (positions[i].left*100) + '%';
h.style.top = (positions[i].top*100) + '%';
h.style.cursor = ["nwse-resize","nesw-resize","nwse-resize","nesw-resize"][i];
h.addEventListener('mousedown', evt => {
resizing = true; activeHandle = i;
originX = evt.clientX; originY = evt.clientY;
let orig = {...rectObj};
let origAngle = rectObj.angle;
let origPoints = getRectCornerPoints(rectObj);
document.body.style.cursor = h.style.cursor;
function onMove(ev) {
if(rectObj.type==="rect") {
// 対角固定のリサイズ
const idx = activeHandle, oppIdx = (idx+2)%4;
const [fx,fy] = origPoints[oppIdx];
let mx = ev.clientX, my = ev.clientY;
let [cx,cy] = getRectCenter(orig);
let rad = -origAngle * Math.PI/180;
let tx = mx - container.getBoundingClientRect().left, ty = my - container.getBoundingClientRect().top;
let dx = (tx-cx)*Math.cos(rad)+(ty-cy)*Math.sin(rad);
let dy =-(tx-cx)*Math.sin(rad)+(ty-cy)*Math.cos(rad);
let ofx = fx - cx, ofy = fy - cy;
let newW = Math.abs(dx-ofx), newH = Math.abs(dy-ofy);
let newCx = (dx+ofx)/2+cx, newCy = (dy+ofy)/2+cy;
rectObj.w = Math.max(10,newW); rectObj.h = Math.max(10,newH);
rectObj.x = newCx - rectObj.w/2; rectObj.y = newCy - rectObj.h/2;
updateRectUI(rectObj); updateHandles(rectObj); updateRotateHandle(rectObj); updateCoords(rectObj);
} else {
// --- quad: 他の3点は絶対固定 ---
if(!quadPoints) {
quadPoints = getRectCornerPoints(rectObj).map(p=>[...p]);
showQuadSVG();
createQuadRotateHandle();
}
const idx = activeHandle;
const tx = ev.clientX - container.getBoundingClientRect().left;
const ty = ev.clientY - container.getBoundingClientRect().top;
quadPoints[idx][0] = tx;
quadPoints[idx][1] = ty;
updateQuadSVG();
updateHandlesQuad();
updateCoordsQuad();
updateQuadRotateHandle();
}
}
function onUp(ev) {
resizing = false; document.body.style.cursor = ""; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
evt.stopPropagation();
});
rectObj.el.appendChild(h); rectObj.handles.push(h);
}
if(currentMode==="rect" || !quadPoints) updateHandles(rectObj);
else updateHandlesQuad();
}
function updateHandles(rectObj) {
let pts = getRectCornerPoints(rectObj);
for(let i=0;i<4;i++) {
rectObj.handles[i].style.left = pts[i][0]-rectObj.x+'px';
rectObj.handles[i].style.top = pts[i][1]-rectObj.y+'px';
}
}
function updateHandlesQuad() {
if(!rectObj || !rectObj.handles || !quadPoints) return;
for(let i=0;i<4;i++) {
rectObj.handles[i].style.left = quadPoints[i][0] - rectObj.x + 'px';
rectObj.handles[i].style.top = quadPoints[i][1] - rectObj.y + 'px';
}
}
function getRectCenter(r) {
return [r.x + r.w/2, r.y + r.h/2];
}
function getRectCornerPoints(r) {
let cx = r.x + r.w/2, cy = r.y + r.h/2;
let rad = (r.angle||0)*Math.PI/180;
let dx = r.w/2, dy = r.h/2;
let corners = [
[-dx,-dy],
[ dx,-dy],
[ dx, dy],
[-dx, dy]
];
return corners.map(([ox,oy])=>{
let x = ox*Math.cos(rad)-oy*Math.sin(rad)+cx;
let y = ox*Math.sin(rad)+oy*Math.cos(rad)+cy;
return [x,y];
});
}
// --- 回転ハンドルと回転処理 ---
function createRotateHandle(rectObj) {
if(rotateHandle) rotateHandle.remove();
rotateHandle = document.createElement('div');
rotateHandle.className = 'rotate-handle';
rectObj.el.appendChild(rotateHandle);
updateRotateHandle(rectObj);
rotateHandle.addEventListener('mousedown', function(e) {
rotating = true;
let [cx,cy] = getRectCenter(rectObj);
const rectC = container.getBoundingClientRect();
let mx = e.clientX - rectC.left, my = e.clientY - rectC.top;
let startAngle = rectObj.angle || 0;
let baseAngle = Math.atan2(my - cx, mx - cy);
document.body.style.cursor = "crosshair";
function onMove(ev) {
let mx2 = ev.clientX - rectC.left, my2 = ev.clientY - rectC.top;
let currAngle = Math.atan2(my2 - cx, mx2 - cy);
let angle = startAngle + (currAngle - baseAngle) * 180 / Math.PI;
rectObj.angle = angle;
updateRectUI(rectObj); updateHandles(rectObj); updateRotateHandle(rectObj); updateCoords(rectObj);
}
function onUp(ev) {
rotating = false; document.body.style.cursor = ""; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
e.stopPropagation();
});
}
function updateRotateHandle(rectObj) {
rotateHandle.style.left = '50%';
rotateHandle.style.top = '-32px';
rotateHandle.style.transform = `translate(-50%,0)`;
rotateHandle.style.display = currentMode === "rect" ? "block" : "none";
}
// Quad専用の回転ハンドル
function createQuadRotateHandle() {
if(quadRotateHandle) quadRotateHandle.remove();
quadRotateHandle = document.createElement('div');
quadRotateHandle.className = 'quad-rotate-handle rotate-handle';
quadRotateHandle.style.position = 'absolute';
updateQuadRotateHandle();
container.appendChild(quadRotateHandle);
quadRotateHandle.addEventListener('mousedown', function(e) {
rotating = true;
let center = getQuadCenter(quadPoints);
let rectC = container.getBoundingClientRect();
let mx = e.clientX - rectC.left, my = e.clientY - rectC.top;
let baseAngle = Math.atan2(my - center[1], mx - center[0]);
document.body.style.cursor = "crosshair";
function onMove(ev) {
let mx2 = ev.clientX - rectC.left, my2 = ev.clientY - rectC.top;
let currAngle = Math.atan2(my2 - center[1], mx2 - center[0]);
let diff = currAngle - baseAngle;
let cos = Math.cos(diff), sin = Math.sin(diff);
for(let i=0;i<4;i++) {
let x = quadPoints[i][0] - center[0], y = quadPoints[i][1] - center[1];
let rx = x * cos - y * sin;
let ry = x * sin + y * cos;
quadPoints[i][0] = rx + center[0];
quadPoints[i][1] = ry + center[1];
}
baseAngle = currAngle;
updateQuadSVG(); updateHandlesQuad(); updateCoordsQuad(); updateQuadRotateHandle();
}
function onUp(ev) {
rotating = false; document.body.style.cursor = ""; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
e.stopPropagation();
});
}
function updateQuadRotateHandle() {
if(!quadPoints || !quadRotateHandle) return;
let center = getQuadCenter(quadPoints);
let p0 = quadPoints[0], p1 = quadPoints[1];
let dx = p1[0] - p0[0], dy = p1[1] - p0[1];
let mx = (p0[0]+p1[0])/2, my = (p0[1]+p1[1])/2;
let len = Math.sqrt(dx*dx+dy*dy);
let nx = -dy/len, ny = dx/len; // normal
let hx = mx + nx*40, hy = my + ny*40;
quadRotateHandle.style.left = (hx) + 'px';
quadRotateHandle.style.top = (hy) + 'px';
quadRotateHandle.style.transform = 'translate(-50%,-50%)';
quadRotateHandle.style.display = currentMode === "quad" && quadPoints ? "block" : "none";
}
function getQuadCenter(pts) {
let x=0, y=0;
for(let i=0;i<4;i++) { x+=pts[i][0]; y+=pts[i][1]; }
return [x/4, y/4];
}
function updateRectUI(rectObj) {
rectObj.el.style.left = rectObj.x + 'px';
rectObj.el.style.top = rectObj.y + 'px';
rectObj.el.style.width = rectObj.w + 'px';
rectObj.el.style.height = rectObj.h + 'px';
rectObj.el.style.transform = `rotate(${rectObj.angle||0}deg)`;
rectObj.el.style.transformOrigin = "50% 50%";
if(currentMode==="rect") {
if(rotateHandle) rotateHandle.style.display = "block";
if(quadRotateHandle) quadRotateHandle.style.display = "none";
}
if(currentMode==="quad" && quadPoints) {
updateQuadSVG();
updateHandlesQuad();
updateQuadRotateHandle();
if(rotateHandle) rotateHandle.style.display = "none";
if(quadRotateHandle) quadRotateHandle.style.display = "block";
}
}
function setSelectedRect(selected) {
if (!rectObj) return;
if (selected) rectObj.el.classList.add('selected');
else rectObj.el.classList.remove('selected');
saveRectBtn.disabled = !rectObj;
}
function clearRect() {
if(rectObj && rectObj.el) rectObj.el.remove();
if(document.getElementById('quad-svg')) document.getElementById('quad-svg').remove();
if(rotateHandle) { rotateHandle.remove(); rotateHandle = null; }
if(quadRotateHandle) { quadRotateHandle.remove(); quadRotateHandle = null; }
rectObj = null; quadPoints = null; setSelectedRect(false);
}
// --- SVG(自由四角形) ---
function showQuadSVG() {
let svg = document.getElementById('quad-svg');
if(svg) svg.remove();
svg = document.createElementNS("http://www.w3.org/2000/svg","svg");
svg.setAttribute("id","quad-svg");
svg.classList.add("quad-svg");
svg.setAttribute("width",container.clientWidth);
svg.setAttribute("height",container.clientHeight);
let poly = document.createElementNS("http://www.w3.org/2000/svg","polygon");
svg.appendChild(poly);
container.appendChild(svg);
updateQuadSVG();
}
function updateQuadSVG() {
let svg = document.getElementById('quad-svg');
if(!svg) showQuadSVG();
let poly = svg.querySelector('polygon');
if(quadPoints && poly) {
let pts = quadPoints.map(p=>p.join(',')).join(' ');
poly.setAttribute('points',pts);
}
}
// --- 座標計算 ---
function getRectImageCoords(rectObj) {
let pts;
if(currentMode==="quad" && quadPoints) {
pts = quadPoints;
} else {
pts = getRectCornerPoints(rectObj);
}
const {left:imgL, top:imgT, width:imgW, height:imgH} = dispImgInfo;
return pts.map(([rx, ry]) => [
Math.round((rx - imgL) * imgNaturalWidth / imgW),
Math.round((ry - imgT) * imgNaturalHeight / imgH)
]);
}
function updateCoords(rectObj) {
if (!rectObj) { coords.innerHTML = "Coordinates:<br>"; return; }
const imgPoints = getRectImageCoords(rectObj);
coords.innerHTML =
`Original image pixel coordinates (top-left origin):<br>
1. (${imgPoints[0][0]}, ${imgPoints[0][1]})<br>
2. (${imgPoints[1][0]}, ${imgPoints[1][1]})<br>
3. (${imgPoints[2][0]}, ${imgPoints[2][1]})<br>
4. (${imgPoints[3][0]}, ${imgPoints[3][1]})<br>`;
}
function updateCoordsQuad() {
updateCoords(rectObj);
}
// --- Memory ---
saveRectBtn.addEventListener('click', () => {
if (!rectObj) return;
const imgPoints = getRectImageCoords(rectObj);
memory.push({
filename: currentImageName || '',
coords: imgPoints,
id: Date.now() + Math.random()
});
updateMemoryList();
clearRect();
coords.innerHTML = "Coordinates:<br>";
saveRectBtn.disabled = true;
});
function updateMemoryList() {
let html = '';
memory.forEach((mem) => {
html += `<div class="memory-item" data-id="${mem.id}">
<span class="memory-label"><b>${mem.filename}</b> :
TL(${mem.coords[0][0]},${mem.coords[0][1]})
TR(${mem.coords[1][0]},${mem.coords[1][1]})
BR(${mem.coords[2][0]},${mem.coords[2][1]})
BL(${mem.coords[3][0]},${mem.coords[3][1]})
</span>
<button class="memory-delete-btn" data-id="${mem.id}" title="Delete this memory">&times;</button>
</div>`;
});
memoryList.innerHTML = html || "<span style='color:#999;'>No memory saved.</span>";
}
memoryList.addEventListener('click', (e) => {
if (e.target.classList.contains('memory-delete-btn')) {
const id = e.target.dataset.id;
memory = memory.filter(m => m.id != id);
updateMemoryList();
}
});
memorySaveBtn.addEventListener('click', async () => {
if (!memory.length) { alert("No memory to save."); return; }
let outObj = {};
memory.forEach(mem => { outObj[mem.filename] = { print_area: mem.coords }; });
let jsonString = JSON.stringify(outObj, null, 2);
let filename = filenameInput.value.trim() || "templates.json";
filename = filename.replace(/[\\\/:*?"<>|]/g, "_");
if (window.showSaveFilePicker) {
try {
const opts = {
suggestedName: filename,
types: [{ description: 'JSON Files', accept: {'application/json': ['.json', '.txt']} }]
};
const handle = await window.showSaveFilePicker(opts);
const writable = await handle.createWritable();
await writable.write(jsonString); await writable.close();
alert("Memory saved!"); return;
} catch (err) { if (err.name !== "AbortError") alert("Failed to save file: " + err.message); }
}
const blob = new Blob([jsonString], {type: "application/json"});
const url = URL.createObjectURL(blob); const a = document.createElement('a');
a.href = url; a.download = filename; document.body.appendChild(a); a.click();
setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
});
document.addEventListener('keydown', e => {
if ((e.key === "Delete" || e.key === "Backspace") && rectObj) {
clearRect();
setSelectedRect(false);
coords.innerHTML = "Coordinates:<br>";
saveRectBtn.disabled = true;
}
});
coords.innerHTML = "Coordinates:<br>";
updateMemoryList();
updateFileNameLabel("");
</script>
</body>
</html>