Spaces:
Sleeping
Sleeping
Upload coordinatemaker.html
Browse files- coordinatemaker.html +128 -27
coordinatemaker.html
CHANGED
|
@@ -33,11 +33,13 @@
|
|
| 33 |
background: rgba(44,222,88,0.15);
|
| 34 |
box-sizing: border-box;
|
| 35 |
cursor: move;
|
|
|
|
| 36 |
}
|
| 37 |
.rect.selected {
|
| 38 |
border: 2px solid #f00;
|
| 39 |
background: rgba(255,100,100,0.15);
|
| 40 |
z-index: 10;
|
|
|
|
| 41 |
}
|
| 42 |
.handle {
|
| 43 |
position: absolute;
|
|
@@ -54,6 +56,24 @@
|
|
| 54 |
.rect.selected .handle {
|
| 55 |
display: block;
|
| 56 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
#coords {
|
| 58 |
font-family: monospace;
|
| 59 |
margin-top: 10px;
|
|
@@ -209,11 +229,14 @@
|
|
| 209 |
let drawing = false;
|
| 210 |
let moving = false;
|
| 211 |
let resizing = false;
|
|
|
|
| 212 |
let startX = 0, startY = 0;
|
| 213 |
let offsetX = 0, offsetY = 0;
|
| 214 |
let originX = 0, originY = 0;
|
| 215 |
let activeHandle = null;
|
| 216 |
let memory = [];
|
|
|
|
|
|
|
| 217 |
|
| 218 |
function updateFileNameLabel(name) {
|
| 219 |
fileNameLabel.textContent = name ? name : "";
|
|
@@ -314,6 +337,7 @@
|
|
| 314 |
memory.push({
|
| 315 |
filename: currentImageName || '',
|
| 316 |
coords: imgPoints,
|
|
|
|
| 317 |
id: Date.now() + Math.random()
|
| 318 |
});
|
| 319 |
updateMemoryList();
|
|
@@ -328,6 +352,7 @@
|
|
| 328 |
setSelectedRect(false);
|
| 329 |
}
|
| 330 |
|
|
|
|
| 331 |
container.addEventListener('mousedown', (e) => {
|
| 332 |
if (!imgLoaded) return;
|
| 333 |
if (!drawing) return;
|
|
@@ -341,7 +366,8 @@
|
|
| 341 |
container.appendChild(rectEl);
|
| 342 |
rectObj = {
|
| 343 |
left: startX, top: startY, width: 0, height: 0,
|
| 344 |
-
element: rectEl, handles: []
|
|
|
|
| 345 |
};
|
| 346 |
setSelectedRect(true);
|
| 347 |
updateRectUI(rectObj);
|
|
@@ -367,6 +393,7 @@
|
|
| 367 |
saveRectBtn.disabled = true;
|
| 368 |
} else {
|
| 369 |
createHandles(rectObj);
|
|
|
|
| 370 |
updateCoords(rectObj);
|
| 371 |
saveRectBtn.disabled = false;
|
| 372 |
}
|
|
@@ -397,6 +424,7 @@
|
|
| 397 |
rectObj.top = newTop;
|
| 398 |
updateRectUI(rectObj);
|
| 399 |
updateHandles(rectObj);
|
|
|
|
| 400 |
updateCoords(rectObj);
|
| 401 |
}
|
| 402 |
function onUp(ev) {
|
|
@@ -411,6 +439,7 @@
|
|
| 411 |
}
|
| 412 |
});
|
| 413 |
|
|
|
|
| 414 |
function createHandles(rectObj) {
|
| 415 |
if (rectObj.handles && rectObj.handles.length) {
|
| 416 |
rectObj.handles.forEach(h=>h.remove());
|
|
@@ -428,11 +457,13 @@
|
|
| 428 |
originX = e.clientX;
|
| 429 |
originY = e.clientY;
|
| 430 |
h._orig = {...rectObj};
|
|
|
|
| 431 |
document.body.style.cursor = h.style.cursor;
|
| 432 |
function onMove(ev) {
|
| 433 |
resizeRect(rectObj, ev, h._orig);
|
| 434 |
updateRectUI(rectObj);
|
| 435 |
updateHandles(rectObj);
|
|
|
|
| 436 |
updateCoords(rectObj);
|
| 437 |
}
|
| 438 |
function onUp(ev) {
|
|
@@ -462,16 +493,13 @@
|
|
| 462 |
function updateHandles(rectObj) {
|
| 463 |
const {width, height, handles} = rectObj;
|
| 464 |
if(!handles || handles.length!==4) return;
|
| 465 |
-
handles[0].style.left = '0px';
|
| 466 |
-
handles[
|
| 467 |
-
handles[
|
| 468 |
-
handles[
|
| 469 |
-
handles[2].style.left = width + 'px';
|
| 470 |
-
handles[2].style.top = height + 'px';
|
| 471 |
-
handles[3].style.left = '0px';
|
| 472 |
-
handles[3].style.top = height + 'px';
|
| 473 |
}
|
| 474 |
function resizeRect(rectObj, e, orig) {
|
|
|
|
| 475 |
let dx = e.clientX - originX;
|
| 476 |
let dy = e.clientY - originY;
|
| 477 |
let nd = {...orig};
|
|
@@ -507,12 +535,75 @@
|
|
| 507 |
nd.height = Math.max(10, Math.min(nd.height, container.clientHeight - nd.top));
|
| 508 |
Object.assign(rectObj, nd);
|
| 509 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
function updateRectUI(rectObj) {
|
| 511 |
rectObj.element.style.left = rectObj.left + 'px';
|
| 512 |
rectObj.element.style.top = rectObj.top + 'px';
|
| 513 |
rectObj.element.style.width = rectObj.width + 'px';
|
| 514 |
rectObj.element.style.height = rectObj.height + 'px';
|
| 515 |
rectObj.element.classList.add('selected');
|
|
|
|
|
|
|
|
|
|
| 516 |
}
|
| 517 |
|
| 518 |
function setSelectedRect(selected) {
|
|
@@ -528,18 +619,29 @@
|
|
| 528 |
|
| 529 |
function getRectImageCoords(rectObj) {
|
| 530 |
const {left:imgL, top:imgT, width:imgW, height:imgH} = dispImgInfo;
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
];
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
}
|
| 544 |
|
| 545 |
function updateCoords(rectObj) {
|
|
@@ -553,18 +655,20 @@
|
|
| 553 |
1. (${imgPoints[0][0]}, ${imgPoints[0][1]})<br>
|
| 554 |
2. (${imgPoints[1][0]}, ${imgPoints[1][1]})<br>
|
| 555 |
3. (${imgPoints[2][0]}, ${imgPoints[2][1]})<br>
|
| 556 |
-
4. (${imgPoints[3][0]}, ${imgPoints[3][1]})<br
|
|
|
|
| 557 |
}
|
| 558 |
|
| 559 |
function updateMemoryList() {
|
| 560 |
let html = '';
|
| 561 |
-
memory.forEach((mem
|
| 562 |
html += `<div class="memory-item" data-id="${mem.id}">
|
| 563 |
<span class="memory-label"><b>${mem.filename}</b> :
|
| 564 |
TL(${mem.coords[0][0]},${mem.coords[0][1]})
|
| 565 |
TR(${mem.coords[1][0]},${mem.coords[1][1]})
|
| 566 |
BR(${mem.coords[2][0]},${mem.coords[2][1]})
|
| 567 |
BL(${mem.coords[3][0]},${mem.coords[3][1]})
|
|
|
|
| 568 |
</span>
|
| 569 |
<button class="memory-delete-btn" data-id="${mem.id}" title="Delete this memory">×</button>
|
| 570 |
</div>`;
|
|
@@ -586,10 +690,10 @@
|
|
| 586 |
alert("No memory to save.");
|
| 587 |
return;
|
| 588 |
}
|
| 589 |
-
//
|
| 590 |
-
// { "filename": { "print_area": [[x,y], ...] }, ... }
|
| 591 |
let outObj = {};
|
| 592 |
memory.forEach(mem => {
|
|
|
|
| 593 |
outObj[mem.filename] = {
|
| 594 |
print_area: mem.coords
|
| 595 |
};
|
|
@@ -599,7 +703,6 @@
|
|
| 599 |
let filename = filenameInput.value.trim() || "templates.json";
|
| 600 |
filename = filename.replace(/[\\\/:*?"<>|]/g, "_");
|
| 601 |
|
| 602 |
-
// Prefer File System Access API if available
|
| 603 |
if (window.showSaveFilePicker) {
|
| 604 |
try {
|
| 605 |
const opts = {
|
|
@@ -623,8 +726,6 @@
|
|
| 623 |
}
|
| 624 |
}
|
| 625 |
}
|
| 626 |
-
|
| 627 |
-
// Fallback: Classic download
|
| 628 |
const blob = new Blob([jsonString], {type: "application/json"});
|
| 629 |
const url = URL.createObjectURL(blob);
|
| 630 |
const a = document.createElement('a');
|
|
|
|
| 33 |
background: rgba(44,222,88,0.15);
|
| 34 |
box-sizing: border-box;
|
| 35 |
cursor: move;
|
| 36 |
+
transition: box-shadow 0.2s;
|
| 37 |
}
|
| 38 |
.rect.selected {
|
| 39 |
border: 2px solid #f00;
|
| 40 |
background: rgba(255,100,100,0.15);
|
| 41 |
z-index: 10;
|
| 42 |
+
box-shadow: 0 0 10px #f00a;
|
| 43 |
}
|
| 44 |
.handle {
|
| 45 |
position: absolute;
|
|
|
|
| 56 |
.rect.selected .handle {
|
| 57 |
display: block;
|
| 58 |
}
|
| 59 |
+
.rotate-handle {
|
| 60 |
+
position: absolute;
|
| 61 |
+
left: 50%;
|
| 62 |
+
top: -32px;
|
| 63 |
+
width: 18px;
|
| 64 |
+
height: 18px;
|
| 65 |
+
margin-left: -9px;
|
| 66 |
+
background: #ff0;
|
| 67 |
+
border: 2px solid #f90;
|
| 68 |
+
border-radius: 50%;
|
| 69 |
+
cursor: pointer;
|
| 70 |
+
z-index: 30;
|
| 71 |
+
display: none;
|
| 72 |
+
box-shadow: 0 0 4px #0006;
|
| 73 |
+
}
|
| 74 |
+
.rect.selected .rotate-handle {
|
| 75 |
+
display: block;
|
| 76 |
+
}
|
| 77 |
#coords {
|
| 78 |
font-family: monospace;
|
| 79 |
margin-top: 10px;
|
|
|
|
| 229 |
let drawing = false;
|
| 230 |
let moving = false;
|
| 231 |
let resizing = false;
|
| 232 |
+
let rotating = false;
|
| 233 |
let startX = 0, startY = 0;
|
| 234 |
let offsetX = 0, offsetY = 0;
|
| 235 |
let originX = 0, originY = 0;
|
| 236 |
let activeHandle = null;
|
| 237 |
let memory = [];
|
| 238 |
+
let rotateStartAngle = 0;
|
| 239 |
+
let rotateCenter = {x:0, y:0};
|
| 240 |
|
| 241 |
function updateFileNameLabel(name) {
|
| 242 |
fileNameLabel.textContent = name ? name : "";
|
|
|
|
| 337 |
memory.push({
|
| 338 |
filename: currentImageName || '',
|
| 339 |
coords: imgPoints,
|
| 340 |
+
angle: rectObj.angle || 0,
|
| 341 |
id: Date.now() + Math.random()
|
| 342 |
});
|
| 343 |
updateMemoryList();
|
|
|
|
| 352 |
setSelectedRect(false);
|
| 353 |
}
|
| 354 |
|
| 355 |
+
// ----- Main Rectangle Creation -----
|
| 356 |
container.addEventListener('mousedown', (e) => {
|
| 357 |
if (!imgLoaded) return;
|
| 358 |
if (!drawing) return;
|
|
|
|
| 366 |
container.appendChild(rectEl);
|
| 367 |
rectObj = {
|
| 368 |
left: startX, top: startY, width: 0, height: 0,
|
| 369 |
+
element: rectEl, handles: [],
|
| 370 |
+
angle: 0
|
| 371 |
};
|
| 372 |
setSelectedRect(true);
|
| 373 |
updateRectUI(rectObj);
|
|
|
|
| 393 |
saveRectBtn.disabled = true;
|
| 394 |
} else {
|
| 395 |
createHandles(rectObj);
|
| 396 |
+
createRotateHandle(rectObj);
|
| 397 |
updateCoords(rectObj);
|
| 398 |
saveRectBtn.disabled = false;
|
| 399 |
}
|
|
|
|
| 424 |
rectObj.top = newTop;
|
| 425 |
updateRectUI(rectObj);
|
| 426 |
updateHandles(rectObj);
|
| 427 |
+
updateRotateHandle(rectObj);
|
| 428 |
updateCoords(rectObj);
|
| 429 |
}
|
| 430 |
function onUp(ev) {
|
|
|
|
| 439 |
}
|
| 440 |
});
|
| 441 |
|
| 442 |
+
// ---- Handles (resize) ----
|
| 443 |
function createHandles(rectObj) {
|
| 444 |
if (rectObj.handles && rectObj.handles.length) {
|
| 445 |
rectObj.handles.forEach(h=>h.remove());
|
|
|
|
| 457 |
originX = e.clientX;
|
| 458 |
originY = e.clientY;
|
| 459 |
h._orig = {...rectObj};
|
| 460 |
+
h._orig.angle = rectObj.angle;
|
| 461 |
document.body.style.cursor = h.style.cursor;
|
| 462 |
function onMove(ev) {
|
| 463 |
resizeRect(rectObj, ev, h._orig);
|
| 464 |
updateRectUI(rectObj);
|
| 465 |
updateHandles(rectObj);
|
| 466 |
+
updateRotateHandle(rectObj);
|
| 467 |
updateCoords(rectObj);
|
| 468 |
}
|
| 469 |
function onUp(ev) {
|
|
|
|
| 493 |
function updateHandles(rectObj) {
|
| 494 |
const {width, height, handles} = rectObj;
|
| 495 |
if(!handles || handles.length!==4) return;
|
| 496 |
+
handles[0].style.left = '0px'; handles[0].style.top = '0px';
|
| 497 |
+
handles[1].style.left = width + 'px'; handles[1].style.top = '0px';
|
| 498 |
+
handles[2].style.left = width + 'px'; handles[2].style.top = height + 'px';
|
| 499 |
+
handles[3].style.left = '0px'; handles[3].style.top = height + 'px';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
}
|
| 501 |
function resizeRect(rectObj, e, orig) {
|
| 502 |
+
// ignore angle for resizing, keep axis-aligned bounding box
|
| 503 |
let dx = e.clientX - originX;
|
| 504 |
let dy = e.clientY - originY;
|
| 505 |
let nd = {...orig};
|
|
|
|
| 535 |
nd.height = Math.max(10, Math.min(nd.height, container.clientHeight - nd.top));
|
| 536 |
Object.assign(rectObj, nd);
|
| 537 |
}
|
| 538 |
+
|
| 539 |
+
// ---- Rotation Handle ----
|
| 540 |
+
function createRotateHandle(rectObj) {
|
| 541 |
+
if(rectObj.rotateHandle) rectObj.rotateHandle.remove();
|
| 542 |
+
let h = document.createElement('div');
|
| 543 |
+
h.className = 'rotate-handle';
|
| 544 |
+
rectObj.element.appendChild(h);
|
| 545 |
+
rectObj.rotateHandle = h;
|
| 546 |
+
updateRotateHandle(rectObj);
|
| 547 |
+
|
| 548 |
+
h.addEventListener('mousedown', function(e) {
|
| 549 |
+
rotating = true;
|
| 550 |
+
const rectC = container.getBoundingClientRect();
|
| 551 |
+
// center of rectangle in container coords
|
| 552 |
+
let cx = rectObj.left + rectObj.width/2;
|
| 553 |
+
let cy = rectObj.top + rectObj.height/2;
|
| 554 |
+
rotateCenter = {x: cx, y: cy};
|
| 555 |
+
// 角度初期値
|
| 556 |
+
const mx = e.clientX - rectC.left, my = e.clientY - rectC.top;
|
| 557 |
+
rotateStartAngle = Math.atan2(my - cy, mx - cx) * 180/Math.PI - (rectObj.angle||0);
|
| 558 |
+
document.body.style.cursor = "crosshair";
|
| 559 |
+
function onMove(ev) {
|
| 560 |
+
const mx2 = ev.clientX - rectC.left, my2 = ev.clientY - rectC.top;
|
| 561 |
+
let angle = Math.atan2(my2 - cy, mx2 - cx) * 180/Math.PI - rotateStartAngle;
|
| 562 |
+
// angleを0-360に正規化
|
| 563 |
+
angle = ((angle % 360) + 360) % 360;
|
| 564 |
+
rectObj.angle = angle;
|
| 565 |
+
updateRectUI(rectObj);
|
| 566 |
+
updateHandles(rectObj);
|
| 567 |
+
updateRotateHandle(rectObj);
|
| 568 |
+
updateCoords(rectObj);
|
| 569 |
+
}
|
| 570 |
+
function onUp(ev) {
|
| 571 |
+
rotating = false;
|
| 572 |
+
document.body.style.cursor = "";
|
| 573 |
+
document.removeEventListener('mousemove', onMove);
|
| 574 |
+
document.removeEventListener('mouseup', onUp);
|
| 575 |
+
}
|
| 576 |
+
document.addEventListener('mousemove', onMove);
|
| 577 |
+
document.addEventListener('mouseup', onUp);
|
| 578 |
+
e.stopPropagation();
|
| 579 |
+
});
|
| 580 |
+
}
|
| 581 |
+
function updateRotateHandle(rectObj) {
|
| 582 |
+
if(!rectObj.rotateHandle) return;
|
| 583 |
+
// 回転ハンドルの位置は矩形の中心上方向に固定
|
| 584 |
+
let w = rectObj.width, h = rectObj.height;
|
| 585 |
+
let angle = rectObj.angle || 0;
|
| 586 |
+
// 中心
|
| 587 |
+
let cx = w/2, cy = h/2;
|
| 588 |
+
// ハンドルの相対座標(矩形の中心から上へ)
|
| 589 |
+
let r = Math.max(w, h)/2 + 24;
|
| 590 |
+
let rad = (-90 + angle) * Math.PI / 180.0;
|
| 591 |
+
let hx = cx + r * Math.cos(rad);
|
| 592 |
+
let hy = cy + r * Math.sin(rad);
|
| 593 |
+
rectObj.rotateHandle.style.left = `${hx}px`;
|
| 594 |
+
rectObj.rotateHandle.style.top = `${hy}px`;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
// --- 矩形のUI更新: 回転も反映
|
| 598 |
function updateRectUI(rectObj) {
|
| 599 |
rectObj.element.style.left = rectObj.left + 'px';
|
| 600 |
rectObj.element.style.top = rectObj.top + 'px';
|
| 601 |
rectObj.element.style.width = rectObj.width + 'px';
|
| 602 |
rectObj.element.style.height = rectObj.height + 'px';
|
| 603 |
rectObj.element.classList.add('selected');
|
| 604 |
+
rectObj.angle = typeof rectObj.angle === "number" ? rectObj.angle : 0;
|
| 605 |
+
rectObj.element.style.transform = `rotate(${rectObj.angle||0}deg)`;
|
| 606 |
+
rectObj.element.style.transformOrigin = "50% 50%";
|
| 607 |
}
|
| 608 |
|
| 609 |
function setSelectedRect(selected) {
|
|
|
|
| 619 |
|
| 620 |
function getRectImageCoords(rectObj) {
|
| 621 |
const {left:imgL, top:imgT, width:imgW, height:imgH} = dispImgInfo;
|
| 622 |
+
let x = rectObj.left, y = rectObj.top, w = rectObj.width, h = rectObj.height;
|
| 623 |
+
// 中心
|
| 624 |
+
let cx = x + w/2, cy = y + h/2;
|
| 625 |
+
// 角度
|
| 626 |
+
let angle = ((typeof rectObj.angle === "number" ? rectObj.angle : 0) * Math.PI) / 180;
|
| 627 |
+
// 各頂点
|
| 628 |
+
let box = [
|
| 629 |
+
[x, y ], // TL
|
| 630 |
+
[x+w, y ], // TR
|
| 631 |
+
[x+w, y+h ], // BR
|
| 632 |
+
[x, y+h ] // BL
|
| 633 |
];
|
| 634 |
+
// 回転適用
|
| 635 |
+
let rot = box.map(([px,py]) => {
|
| 636 |
+
let dx = px - cx, dy = py - cy;
|
| 637 |
+
let rx = dx * Math.cos(angle) - dy * Math.sin(angle) + cx;
|
| 638 |
+
let ry = dx * Math.sin(angle) + dy * Math.cos(angle) + cy;
|
| 639 |
+
// img領域→元画像座標
|
| 640 |
+
let ix = Math.round((rx - imgL) * imgNaturalWidth / imgW);
|
| 641 |
+
let iy = Math.round((ry - imgT) * imgNaturalHeight / imgH);
|
| 642 |
+
return [ix, iy];
|
| 643 |
+
});
|
| 644 |
+
return rot;
|
| 645 |
}
|
| 646 |
|
| 647 |
function updateCoords(rectObj) {
|
|
|
|
| 655 |
1. (${imgPoints[0][0]}, ${imgPoints[0][1]})<br>
|
| 656 |
2. (${imgPoints[1][0]}, ${imgPoints[1][1]})<br>
|
| 657 |
3. (${imgPoints[2][0]}, ${imgPoints[2][1]})<br>
|
| 658 |
+
4. (${imgPoints[3][0]}, ${imgPoints[3][1]})<br>
|
| 659 |
+
[angle: ${rectObj.angle ? rectObj.angle.toFixed(1) : 0}°]`;
|
| 660 |
}
|
| 661 |
|
| 662 |
function updateMemoryList() {
|
| 663 |
let html = '';
|
| 664 |
+
memory.forEach((mem) => {
|
| 665 |
html += `<div class="memory-item" data-id="${mem.id}">
|
| 666 |
<span class="memory-label"><b>${mem.filename}</b> :
|
| 667 |
TL(${mem.coords[0][0]},${mem.coords[0][1]})
|
| 668 |
TR(${mem.coords[1][0]},${mem.coords[1][1]})
|
| 669 |
BR(${mem.coords[2][0]},${mem.coords[2][1]})
|
| 670 |
BL(${mem.coords[3][0]},${mem.coords[3][1]})
|
| 671 |
+
[angle: ${mem.angle || 0}°]
|
| 672 |
</span>
|
| 673 |
<button class="memory-delete-btn" data-id="${mem.id}" title="Delete this memory">×</button>
|
| 674 |
</div>`;
|
|
|
|
| 690 |
alert("No memory to save.");
|
| 691 |
return;
|
| 692 |
}
|
| 693 |
+
// ★ 画像名に「bases/」を付けないように修正
|
|
|
|
| 694 |
let outObj = {};
|
| 695 |
memory.forEach(mem => {
|
| 696 |
+
// ファイル名そのままをキーに
|
| 697 |
outObj[mem.filename] = {
|
| 698 |
print_area: mem.coords
|
| 699 |
};
|
|
|
|
| 703 |
let filename = filenameInput.value.trim() || "templates.json";
|
| 704 |
filename = filename.replace(/[\\\/:*?"<>|]/g, "_");
|
| 705 |
|
|
|
|
| 706 |
if (window.showSaveFilePicker) {
|
| 707 |
try {
|
| 708 |
const opts = {
|
|
|
|
| 726 |
}
|
| 727 |
}
|
| 728 |
}
|
|
|
|
|
|
|
| 729 |
const blob = new Blob([jsonString], {type: "application/json"});
|
| 730 |
const url = URL.createObjectURL(blob);
|
| 731 |
const a = document.createElement('a');
|