junjiro1129 commited on
Commit
2a05f0c
·
verified ·
1 Parent(s): f79a535

Upgrade file

Browse files

"Free transformation mode" that allows you to freely change the shape of the rectangle

Files changed (1) hide show
  1. index.html +395 -408
index.html CHANGED
@@ -3,7 +3,9 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <title>Coordinate Maker</title>
 
6
  <style>
 
7
  #img-container {
8
  position: relative;
9
  width: 800px;
@@ -33,6 +35,7 @@
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 {
@@ -43,36 +46,48 @@
43
  }
44
  .handle {
45
  position: absolute;
46
- width: 12px; height: 12px;
 
47
  background: #2a7;
48
  border-radius: 50%;
49
  cursor: pointer;
50
- margin: -6px 0 0 -6px;
51
  z-index: 20;
52
- border: 1px solid #fff;
53
  box-shadow: 0 0 2px #0004;
54
- display: none;
55
  }
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;
@@ -164,7 +179,7 @@
164
  margin-left: 1em;
165
  font-size: 1.1em;
166
  }
167
- #add-rect-btn, #save-rect-btn, #memory-save-btn {
168
  margin-right: 1em;
169
  padding: 0.5em 1em;
170
  font-size: 1em;
@@ -175,9 +190,6 @@
175
  border-color: #ccc;
176
  cursor: default;
177
  }
178
- #rect-list {
179
- display: none;
180
- }
181
  input[type="file"]::-webkit-file-upload-button { visibility: visible; }
182
  input[type="file"]::file-selector-button { visibility: visible; }
183
  input[type="file"]::-ms-value { display: none; }
@@ -195,9 +207,11 @@
195
  <div id="toolbar">
196
  <label style="display:inline-block; position:relative;">
197
  <input type="file" id="img-input" accept="image/*" style="width:140px;">
 
198
  </label>
199
  <span id="file-name-label"></span>
200
- <button id="add-rect-btn">Add Rectangle</button>
 
201
  <button id="save-rect-btn" disabled>Save Rectangle</button>
202
  <input type="text" id="filename-input" placeholder="templates.json">
203
  <button id="memory-save-btn">Save Memory to File</button>
@@ -209,456 +223,461 @@
209
  <div id="coords">Coordinates:<br></div>
210
  <div id="memory-list"></div>
211
  <script>
 
212
  const imgInput = document.getElementById('img-input');
213
  const img = document.getElementById('the-img');
214
  const container = document.getElementById('img-container');
215
  const coords = document.getElementById('coords');
216
  const dropMessage = document.getElementById('drop-message');
217
  const addRectBtn = document.getElementById('add-rect-btn');
 
218
  const saveRectBtn = document.getElementById('save-rect-btn');
219
  const memorySaveBtn = document.getElementById('memory-save-btn');
220
  const memoryList = document.getElementById('memory-list');
221
  const filenameInput = document.getElementById('filename-input');
222
  const fileNameLabel = document.getElementById('file-name-label');
 
223
 
224
- let imgLoaded = false;
225
- let imgNaturalWidth = 0, imgNaturalHeight = 0;
226
  let dispImgInfo = {left:0, top:0, width:0, height:0};
227
  let currentImageName = '';
 
228
  let rectObj = null;
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 : "";
243
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
 
245
  function loadImageFromFile(file) {
246
  if (!file || !file.type.match(/^image\//)) return;
247
  const reader = new FileReader();
248
- reader.onload = function(ev) {
249
- img.src = ev.target.result;
250
- currentImageName = file.name || '';
251
- updateFileNameLabel(currentImageName);
252
- };
253
  reader.readAsDataURL(file);
254
  }
255
-
256
- imgInput.addEventListener('change', e => {
257
- const file = e.target.files[0];
258
- imgInput.value = "";
259
- updateFileNameLabel(""); // Clear previous file name immediately
260
- if (file) {
261
- loadImageFromFile(file);
262
- }
263
- });
264
-
265
- container.addEventListener('dragover', (e) => {
266
- e.preventDefault();
267
- e.stopPropagation();
268
- container.classList.add('dragover');
269
- dropMessage.style.display = 'block';
270
- });
271
- container.addEventListener('dragleave', (e) => {
272
- e.preventDefault();
273
- e.stopPropagation();
274
- container.classList.remove('dragover');
275
- dropMessage.style.display = 'none';
276
  });
277
- container.addEventListener('drop', (e) => {
278
- e.preventDefault();
279
- e.stopPropagation();
280
- container.classList.remove('dragover');
281
- dropMessage.style.display = 'none';
282
- if (e.dataTransfer.files && e.dataTransfer.files[0]) {
283
- const file = e.dataTransfer.files[0];
284
- updateFileNameLabel(""); // Clear previous name immediately
285
- loadImageFromFile(file);
286
- }
287
  });
288
-
289
  img.addEventListener('load', () => {
290
- imgLoaded = true;
291
- img.style.display = "block";
292
- imgNaturalWidth = img.naturalWidth;
293
- imgNaturalHeight = img.naturalHeight;
294
- img.style.width = '100%';
295
- img.style.height = '100%';
296
- img.style.objectFit = 'contain';
297
- clearRect();
298
- updateDispImgInfo();
299
- updateCoords();
300
  });
301
-
302
  function updateDispImgInfo() {
303
- const cW = container.clientWidth;
304
- const cH = container.clientHeight;
305
- const iW = imgNaturalWidth;
306
- const iH = imgNaturalHeight;
307
- let dispW = cW, dispH = cH, dispL = 0, dispT = 0;
308
- const imgAspect = iW / iH;
309
- const contAspect = cW / cH;
310
- if (imgAspect > contAspect) {
311
- dispW = cW;
312
- dispH = cW / imgAspect;
313
- dispT = (cH - dispH) / 2;
314
- dispL = 0;
315
- } else {
316
- dispH = cH;
317
- dispW = cH * imgAspect;
318
- dispL = (cW - dispW) / 2;
319
- dispT = 0;
320
- }
321
  dispImgInfo = {left:dispL, top:dispT, width:dispW, height:dispH};
322
  }
323
  window.addEventListener('resize', updateDispImgInfo);
324
 
325
- addRectBtn.addEventListener('click', () => {
326
- if (!imgLoaded) return;
327
- if (rectObj) return;
328
- drawing = true;
329
- setSelectedRect(true);
330
- coords.innerHTML = "Draw a rectangle on the image.<br>";
331
- saveRectBtn.disabled = true;
332
- });
333
-
334
- saveRectBtn.addEventListener('click', () => {
335
- if (!rectObj) return;
336
- const imgPoints = getRectImageCoords(rectObj);
337
- memory.push({
338
- filename: currentImageName || '',
339
- coords: imgPoints,
340
- angle: rectObj.angle || 0,
341
- id: Date.now() + Math.random()
342
- });
343
- updateMemoryList();
344
- clearRect();
345
- coords.innerHTML = "Coordinates:<br>";
346
- saveRectBtn.disabled = true;
347
- });
348
-
349
- function clearRect() {
350
- if (rectObj && rectObj.element) rectObj.element.remove();
351
- rectObj = null;
352
- setSelectedRect(false);
353
- }
354
-
355
- // ----- Main Rectangle Creation -----
356
- container.addEventListener('mousedown', (e) => {
357
- if (!imgLoaded) return;
358
- if (!drawing) return;
359
- if (rectObj) return;
360
  if (e.target !== container && e.target !== img) return;
361
  const rectC = container.getBoundingClientRect();
362
- startX = e.clientX - rectC.left;
363
- startY = e.clientY - rectC.top;
364
- let rectEl = document.createElement('div');
365
- rectEl.className = 'rect selected';
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);
374
- document.body.style.cursor = "crosshair";
375
- drawing = true;
376
- function onMouseMove(ev) {
377
- const currX = ev.clientX - rectC.left;
378
- const currY = ev.clientY - rectC.top;
379
- rectObj.left = Math.min(startX, currX);
380
- rectObj.top = Math.min(startY, currY);
381
- rectObj.width = Math.abs(currX - startX);
382
- rectObj.height = Math.abs(currY - startY);
383
- updateRectUI(rectObj);
384
- updateCoords(rectObj);
385
  }
386
- function onMouseUp(ev) {
387
- drawing = false;
388
- document.body.style.cursor = "";
389
- document.removeEventListener('mousemove', onMouseMove);
390
- document.removeEventListener('mouseup', onMouseUp);
391
- if (rectObj.width < 10 || rectObj.height < 10) {
392
- clearRect();
393
- saveRectBtn.disabled = true;
394
- } else {
395
- createHandles(rectObj);
396
- createRotateHandle(rectObj);
397
- updateCoords(rectObj);
398
- saveRectBtn.disabled = false;
399
  }
400
  }
401
- document.addEventListener('mousemove', onMouseMove);
402
- document.addEventListener('mouseup', onMouseUp);
403
  e.preventDefault();
404
  });
405
 
406
- container.addEventListener('mousedown', (e) => {
407
- if (drawing) return;
408
- if (!imgLoaded) return;
409
- if (!rectObj || !rectObj.element) return;
410
- if (e.target === rectObj.element) {
411
- setSelectedRect(true);
412
- saveRectBtn.disabled = false;
413
- moving = true;
414
  const rectC = container.getBoundingClientRect();
415
- offsetX = e.clientX - rectC.left - rectObj.left;
416
- offsetY = e.clientY - rectC.top - rectObj.top;
417
  document.body.style.cursor = "move";
418
  function onMove(ev) {
419
- let newLeft = ev.clientX - rectC.left - offsetX;
420
- let newTop = ev.clientY - rectC.top - offsetY;
421
- newLeft = Math.max(0, Math.min(newLeft, container.clientWidth - rectObj.width));
422
- newTop = Math.max(0, Math.min(newTop, container.clientHeight - rectObj.height));
423
- rectObj.left = newLeft;
424
- rectObj.top = newTop;
425
- updateRectUI(rectObj);
426
- updateHandles(rectObj);
427
- updateRotateHandle(rectObj);
428
- updateCoords(rectObj);
 
 
 
 
 
429
  }
430
  function onUp(ev) {
431
- moving = false;
432
- document.body.style.cursor = "";
433
- document.removeEventListener('mousemove', onMove);
434
- document.removeEventListener('mouseup', onUp);
435
  }
436
- document.addEventListener('mousemove', onMove);
437
- document.addEventListener('mouseup', onUp);
438
- e.stopPropagation();
439
  }
440
  });
441
 
442
- // ---- Handles (resize) ----
443
  function createHandles(rectObj) {
444
- if (rectObj.handles && rectObj.handles.length) {
445
- rectObj.handles.forEach(h=>h.remove());
446
- }
447
- const positions = ['tl','tr','br','bl'];
448
  rectObj.handles = [];
 
 
 
 
 
 
449
  for(let i=0; i<4; i++) {
450
  let h = document.createElement('div');
451
  h.className = 'handle';
452
- h.dataset.handle = positions[i];
453
- h.style.cursor = handleCursor(positions[i]);
454
- h.addEventListener('mousedown', (e) => {
455
- resizing = true;
456
- activeHandle = positions[i];
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) {
470
- resizing = false;
471
- document.body.style.cursor = "";
472
- document.removeEventListener('mousemove', onMove);
473
- document.removeEventListener('mouseup', onUp);
474
  }
475
- document.addEventListener('mousemove', onMove);
476
- document.addEventListener('mouseup', onUp);
477
- e.stopPropagation();
478
  });
479
- rectObj.element.appendChild(h);
480
- rectObj.handles.push(h);
481
- }
482
- updateHandles(rectObj);
483
- }
484
- function handleCursor(pos) {
485
- switch(pos) {
486
- case 'tl': return 'nwse-resize';
487
- case 'tr': return 'nesw-resize';
488
- case 'br': return 'nwse-resize';
489
- case 'bl': return 'nesw-resize';
490
- default: return 'pointer';
491
  }
 
 
492
  }
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};
506
- switch(activeHandle) {
507
- case 'tl':
508
- nd.left = Math.min(orig.left + dx, orig.left + orig.width - 10);
509
- nd.top = Math.min(orig.top + dy, orig.top + orig.height - 10);
510
- nd.width = orig.width - (nd.left - orig.left);
511
- nd.height = orig.height - (nd.top - orig.top);
512
- break;
513
- case 'tr':
514
- nd.top = Math.min(orig.top + dy, orig.top + orig.height - 10);
515
- nd.width = Math.max(10, orig.width + dx);
516
- nd.height = orig.height - (nd.top - orig.top);
517
- nd.left = orig.left;
518
- break;
519
- case 'br':
520
- nd.width = Math.max(10, orig.width + dx);
521
- nd.height = Math.max(10, orig.height + dy);
522
- nd.left = orig.left;
523
- nd.top = orig.top;
524
- break;
525
- case 'bl':
526
- nd.left = Math.min(orig.left + dx, orig.left + orig.width - 10);
527
- nd.width = orig.width - (nd.left - orig.left);
528
- nd.height = Math.max(10, orig.height + dy);
529
- nd.top = orig.top;
530
- break;
531
  }
532
- nd.left = Math.max(0, Math.min(nd.left, container.clientWidth - nd.width));
533
- nd.top = Math.max(0, Math.min(nd.top, container.clientHeight - nd.height));
534
- nd.width = Math.max(10, Math.min(nd.width, container.clientWidth - nd.left));
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) {
610
- if (rectObj && rectObj.element) {
611
- if (selected) {
612
- rectObj.element.classList.add('selected');
613
- } else {
614
- rectObj.element.classList.remove('selected');
615
- }
616
- }
617
  saveRectBtn.disabled = !rectObj;
618
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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) {
648
- if (!rectObj) {
649
- coords.innerHTML = "Coordinates:<br>";
650
- return;
651
- }
652
  const imgPoints = getRectImageCoords(rectObj);
653
  coords.innerHTML =
654
  `Original image pixel coordinates (top-left origin):<br>
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) => {
@@ -668,14 +687,12 @@
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">&times;</button>
674
  </div>`;
675
  });
676
  memoryList.innerHTML = html || "<span style='color:#999;'>No memory saved.</span>";
677
  }
678
-
679
  memoryList.addEventListener('click', (e) => {
680
  if (e.target.classList.contains('memory-delete-btn')) {
681
  const id = e.target.dataset.id;
@@ -684,59 +701,29 @@
684
  }
685
  });
686
 
687
- // Save memory to JSON file using the File System Access API if available (showSaveFilePicker)
688
  memorySaveBtn.addEventListener('click', async () => {
689
- if (!memory.length) {
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
- };
700
- });
701
  let jsonString = JSON.stringify(outObj, null, 2);
702
-
703
  let filename = filenameInput.value.trim() || "templates.json";
704
  filename = filename.replace(/[\\\/:*?"<>|]/g, "_");
705
-
706
  if (window.showSaveFilePicker) {
707
  try {
708
  const opts = {
709
  suggestedName: filename,
710
- types: [
711
- {
712
- description: 'JSON Files',
713
- accept: {'application/json': ['.json', '.txt']}
714
- }
715
- ]
716
  };
717
  const handle = await window.showSaveFilePicker(opts);
718
  const writable = await handle.createWritable();
719
- await writable.write(jsonString);
720
- await writable.close();
721
- alert("Memory saved!");
722
- return;
723
- } catch (err) {
724
- if (err.name !== "AbortError") {
725
- alert("Failed to save file: " + err.message);
726
- }
727
- }
728
  }
729
  const blob = new Blob([jsonString], {type: "application/json"});
730
- const url = URL.createObjectURL(blob);
731
- const a = document.createElement('a');
732
- a.href = url;
733
- a.download = filename;
734
- document.body.appendChild(a);
735
- a.click();
736
- setTimeout(() => {
737
- document.body.removeChild(a);
738
- URL.revokeObjectURL(url);
739
- }, 100);
740
  });
741
 
742
  document.addEventListener('keydown', e => {
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <title>Coordinate Maker</title>
6
+ <meta name="viewport" content="width=800,user-scalable=no">
7
  <style>
8
+ /* ...(CSSは変更不要なので省略。前回そのまま)... */
9
  #img-container {
10
  position: relative;
11
  width: 800px;
 
35
  background: rgba(44,222,88,0.15);
36
  box-sizing: border-box;
37
  cursor: move;
38
+ pointer-events: auto;
39
  transition: box-shadow 0.2s;
40
  }
41
  .rect.selected {
 
46
  }
47
  .handle {
48
  position: absolute;
49
+ width: 14px; height: 14px;
50
+ margin: -7px 0 0 -7px;
51
  background: #2a7;
52
  border-radius: 50%;
53
  cursor: pointer;
 
54
  z-index: 20;
55
+ border: 2px solid #fff;
56
  box-shadow: 0 0 2px #0004;
57
+ display: block;
58
  }
59
  .rect.selected .handle {
60
+ background: #f00;
61
  }
62
+ .rotate-handle, .quad-rotate-handle {
63
  position: absolute;
 
 
64
  width: 18px;
65
  height: 18px;
 
66
  background: #ff0;
67
  border: 2px solid #f90;
68
  border-radius: 50%;
69
+ cursor: grab;
70
  z-index: 30;
71
+ display: block;
72
  box-shadow: 0 0 4px #0006;
73
  }
74
+ .rotate-handle {
75
+ left: 50%;
76
+ top: -32px;
77
+ transform: translate(-50%,0);
78
+ }
79
+ .quad-svg {
80
+ position: absolute;
81
+ left: 0; top: 0;
82
+ width: 100%; height: 100%;
83
+ pointer-events: none;
84
+ z-index: 2;
85
+ }
86
+ .quad-svg polygon {
87
+ fill: rgba(44,222,88,0.15);
88
+ stroke: #f00;
89
+ stroke-width: 2;
90
+ pointer-events: stroke;
91
  }
92
  #coords {
93
  font-family: monospace;
 
179
  margin-left: 1em;
180
  font-size: 1.1em;
181
  }
182
+ #add-rect-btn, #add-quad-btn, #save-rect-btn, #memory-save-btn {
183
  margin-right: 1em;
184
  padding: 0.5em 1em;
185
  font-size: 1em;
 
190
  border-color: #ccc;
191
  cursor: default;
192
  }
 
 
 
193
  input[type="file"]::-webkit-file-upload-button { visibility: visible; }
194
  input[type="file"]::file-selector-button { visibility: visible; }
195
  input[type="file"]::-ms-value { display: none; }
 
207
  <div id="toolbar">
208
  <label style="display:inline-block; position:relative;">
209
  <input type="file" id="img-input" accept="image/*" style="width:140px;">
210
+ <span style="position:absolute;left:12px;top:8px;pointer-events:none;color:#555;font-size:1em;" id="file-input-label">Choose File</span>
211
  </label>
212
  <span id="file-name-label"></span>
213
+ <button id="add-rect-btn">Rectangle Mode</button>
214
+ <button id="add-quad-btn">Free Transform Mode</button>
215
  <button id="save-rect-btn" disabled>Save Rectangle</button>
216
  <input type="text" id="filename-input" placeholder="templates.json">
217
  <button id="memory-save-btn">Save Memory to File</button>
 
223
  <div id="coords">Coordinates:<br></div>
224
  <div id="memory-list"></div>
225
  <script>
226
+ // === DOM取得 ===
227
  const imgInput = document.getElementById('img-input');
228
  const img = document.getElementById('the-img');
229
  const container = document.getElementById('img-container');
230
  const coords = document.getElementById('coords');
231
  const dropMessage = document.getElementById('drop-message');
232
  const addRectBtn = document.getElementById('add-rect-btn');
233
+ const addQuadBtn = document.getElementById('add-quad-btn');
234
  const saveRectBtn = document.getElementById('save-rect-btn');
235
  const memorySaveBtn = document.getElementById('memory-save-btn');
236
  const memoryList = document.getElementById('memory-list');
237
  const filenameInput = document.getElementById('filename-input');
238
  const fileNameLabel = document.getElementById('file-name-label');
239
+ const fileInputLabel = document.getElementById('file-input-label');
240
 
241
+ // --- State ---
242
+ let imgLoaded = false, imgNaturalWidth = 0, imgNaturalHeight = 0;
243
  let dispImgInfo = {left:0, top:0, width:0, height:0};
244
  let currentImageName = '';
245
+ let currentMode = "rect"; // "rect" or "quad"
246
  let rectObj = null;
247
+ let drawing = false, moving = false, resizing = false, rotating = false;
248
+ let startX = 0, startY = 0, offsetX = 0, offsetY = 0, originX = 0, originY = 0, rotateStartAngle = 0;
249
+ let activeHandle = null, memory = [];
250
+ let quadPoints = null; // only for quad mode
251
+ let quadSVG = null;
252
+ let rotateHandle = null;
253
+ let quadRotateHandle = null;
 
 
 
 
254
 
255
+ // --- File input label (hide when file is selected) ---
256
+ imgInput.addEventListener('change', (e) => {
257
+ if(imgInput.files && imgInput.files.length) fileInputLabel.style.display = 'none';
258
+ else fileInputLabel.style.display = '';
259
+ });
260
+ imgInput.addEventListener('input', (e) => {
261
+ if(imgInput.files && imgInput.files.length) fileInputLabel.style.display = 'none';
262
+ else fileInputLabel.style.display = '';
263
+ });
264
+
265
+ // --- Mode Buttons ---
266
+ addRectBtn.onclick = () => {
267
+ currentMode = "rect";
268
+ addRectBtn.style.background = "#baffba";
269
+ addQuadBtn.style.background = "";
270
+ clearRect(); saveRectBtn.disabled = true;
271
+ };
272
+ addQuadBtn.onclick = () => {
273
+ currentMode = "quad";
274
+ addQuadBtn.style.background = "#baffba";
275
+ addRectBtn.style.background = "";
276
+ clearRect(); saveRectBtn.disabled = true;
277
+ };
278
+ addRectBtn.style.background = "#baffba";
279
 
280
+ function updateFileNameLabel(name) { fileNameLabel.textContent = name ? name : ""; }
281
  function loadImageFromFile(file) {
282
  if (!file || !file.type.match(/^image\//)) return;
283
  const reader = new FileReader();
284
+ reader.onload = ev => { img.src = ev.target.result; currentImageName = file.name || ''; updateFileNameLabel(currentImageName); };
 
 
 
 
285
  reader.readAsDataURL(file);
286
  }
287
+ imgInput.addEventListener('change', e => {
288
+ const file = e.target.files[0];
289
+ imgInput.value = "";
290
+ updateFileNameLabel("");
291
+ if (file) loadImageFromFile(file);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  });
293
+ container.addEventListener('dragover', e => { e.preventDefault(); e.stopPropagation(); container.classList.add('dragover'); dropMessage.textContent = "Drop image here"; dropMessage.style.display = 'block'; });
294
+ container.addEventListener('dragleave', e => { e.preventDefault(); e.stopPropagation(); container.classList.remove('dragover'); dropMessage.style.display = 'none'; });
295
+ container.addEventListener('drop', e => {
296
+ e.preventDefault(); e.stopPropagation();
297
+ container.classList.remove('dragover'); dropMessage.style.display = 'none';
298
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) { const file = e.dataTransfer.files[0]; updateFileNameLabel(""); loadImageFromFile(file); }
 
 
 
 
299
  });
 
300
  img.addEventListener('load', () => {
301
+ imgLoaded = true; img.style.display = "block";
302
+ imgNaturalWidth = img.naturalWidth; imgNaturalHeight = img.naturalHeight;
303
+ img.style.width = '100%'; img.style.height = '100%'; img.style.objectFit = 'contain';
304
+ clearRect(); updateDispImgInfo(); updateCoords();
 
 
 
 
 
 
305
  });
 
306
  function updateDispImgInfo() {
307
+ const cW = container.clientWidth, cH = container.clientHeight, iW = imgNaturalWidth, iH = imgNaturalHeight;
308
+ let dispW = cW, dispH = cH, dispL = 0, dispT = 0, imgAspect = iW / iH, contAspect = cW / cH;
309
+ if (imgAspect > contAspect) { dispW = cW; dispH = cW / imgAspect; dispT = (cH - dispH) / 2; dispL = 0; }
310
+ else { dispH = cH; dispW = cH * imgAspect; dispL = (cW - dispW) / 2; dispT = 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  dispImgInfo = {left:dispL, top:dispT, width:dispW, height:dispH};
312
  }
313
  window.addEventListener('resize', updateDispImgInfo);
314
 
315
+ // --- Drawing ---
316
+ container.addEventListener('mousedown', e => {
317
+ if (!imgLoaded || drawing || rectObj) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  if (e.target !== container && e.target !== img) return;
319
  const rectC = container.getBoundingClientRect();
320
+ startX = e.clientX - rectC.left; startY = e.clientY - rectC.top;
321
+ let el = document.createElement('div');
322
+ el.className = 'rect selected';
323
+ container.appendChild(el);
 
324
  rectObj = {
325
+ type: currentMode,
326
+ el: el,
327
+ x: startX, y: startY, w: 0, h: 0,
328
+ angle: 0, handles: []
329
  };
330
+ setSelectedRect(true); updateRectUI(rectObj);
331
+ drawing = true; saveRectBtn.disabled = true;
332
+ function onMove(ev) {
333
+ const currX = ev.clientX - rectC.left, currY = ev.clientY - rectC.top;
334
+ rectObj.x = Math.min(startX, currX); rectObj.y = Math.min(startY, currY);
335
+ rectObj.w = Math.abs(currX - startX); rectObj.h = Math.abs(currY - startY);
336
+ updateRectUI(rectObj); updateCoords(rectObj);
 
 
 
 
 
 
337
  }
338
+ function onUp(ev) {
339
+ drawing = false; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp);
340
+ if (rectObj.w < 10 || rectObj.h < 10) { clearRect(); saveRectBtn.disabled = true; }
341
+ else {
342
+ createHandles(rectObj);
343
+ if(currentMode==="rect") createRotateHandle(rectObj);
344
+ else { quadPoints = null; createHandles(rectObj); }
345
+ updateCoords(rectObj); saveRectBtn.disabled = false;
 
 
 
 
 
346
  }
347
  }
348
+ document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
 
349
  e.preventDefault();
350
  });
351
 
352
+ // --- Rect/Quad move ---
353
+ container.addEventListener('mousedown', e => {
354
+ if (drawing || !imgLoaded) return;
355
+ if (!rectObj || !rectObj.el) return;
356
+ if (e.target === rectObj.el && !rotating) {
357
+ setSelectedRect(true); saveRectBtn.disabled = false; moving = true;
 
 
358
  const rectC = container.getBoundingClientRect();
359
+ offsetX = e.clientX - rectC.left - rectObj.x;
360
+ offsetY = e.clientY - rectC.top - rectObj.y;
361
  document.body.style.cursor = "move";
362
  function onMove(ev) {
363
+ let newX = ev.clientX - rectC.left - offsetX, newY = ev.clientY - rectC.top - offsetY;
364
+ if(currentMode==="quad" && quadPoints) {
365
+ let dx = newX - rectObj.x, dy = newY - rectObj.y;
366
+ for(let i=0;i<4;i++) {
367
+ quadPoints[i][0] += dx;
368
+ quadPoints[i][1] += dy;
369
+ }
370
+ rectObj.x = newX; rectObj.y = newY;
371
+ updateQuadSVG();
372
+ updateHandlesQuad();
373
+ updateCoordsQuad();
374
+ updateQuadRotateHandle();
375
+ } else {
376
+ rectObj.x = newX; rectObj.y = newY; updateRectUI(rectObj); updateHandles(rectObj); updateRotateHandle(rectObj); updateCoords(rectObj);
377
+ }
378
  }
379
  function onUp(ev) {
380
+ moving = false; document.body.style.cursor = ""; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp);
 
 
 
381
  }
382
+ document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); e.stopPropagation();
 
 
383
  }
384
  });
385
 
386
+ // --- Handles (resize/drag) ---
387
  function createHandles(rectObj) {
388
+ if (rectObj.handles && rectObj.handles.length) rectObj.handles.forEach(h=>h.remove());
 
 
 
389
  rectObj.handles = [];
390
+ const positions = [
391
+ {name:"tl", left:0, top:0},
392
+ {name:"tr", left:1, top:0},
393
+ {name:"br", left:1, top:1},
394
+ {name:"bl", left:0, top:1},
395
+ ];
396
  for(let i=0; i<4; i++) {
397
  let h = document.createElement('div');
398
  h.className = 'handle';
399
+ h.dataset.handle = positions[i].name;
400
+ h.style.left = (positions[i].left*100) + '%';
401
+ h.style.top = (positions[i].top*100) + '%';
402
+ h.style.cursor = ["nwse-resize","nesw-resize","nwse-resize","nesw-resize"][i];
403
+ h.addEventListener('mousedown', evt => {
404
+ resizing = true; activeHandle = i;
405
+ originX = evt.clientX; originY = evt.clientY;
406
+ let orig = {...rectObj};
407
+ let origAngle = rectObj.angle;
408
+ let origPoints = getRectCornerPoints(rectObj);
409
  document.body.style.cursor = h.style.cursor;
410
  function onMove(ev) {
411
+ if(rectObj.type==="rect") {
412
+ // 対角固定のリサイズ
413
+ const idx = activeHandle, oppIdx = (idx+2)%4;
414
+ const [fx,fy] = origPoints[oppIdx];
415
+ let mx = ev.clientX, my = ev.clientY;
416
+ let [cx,cy] = getRectCenter(orig);
417
+ let rad = -origAngle * Math.PI/180;
418
+ let tx = mx - container.getBoundingClientRect().left, ty = my - container.getBoundingClientRect().top;
419
+ let dx = (tx-cx)*Math.cos(rad)+(ty-cy)*Math.sin(rad);
420
+ let dy =-(tx-cx)*Math.sin(rad)+(ty-cy)*Math.cos(rad);
421
+ let ofx = fx - cx, ofy = fy - cy;
422
+ let newW = Math.abs(dx-ofx), newH = Math.abs(dy-ofy);
423
+ let newCx = (dx+ofx)/2+cx, newCy = (dy+ofy)/2+cy;
424
+ rectObj.w = Math.max(10,newW); rectObj.h = Math.max(10,newH);
425
+ rectObj.x = newCx - rectObj.w/2; rectObj.y = newCy - rectObj.h/2;
426
+ updateRectUI(rectObj); updateHandles(rectObj); updateRotateHandle(rectObj); updateCoords(rectObj);
427
+ } else {
428
+ // --- quad: 他の3点は絶対固定 ---
429
+ if(!quadPoints) {
430
+ quadPoints = getRectCornerPoints(rectObj).map(p=>[...p]);
431
+ showQuadSVG();
432
+ createQuadRotateHandle();
433
+ }
434
+ const idx = activeHandle;
435
+ const tx = ev.clientX - container.getBoundingClientRect().left;
436
+ const ty = ev.clientY - container.getBoundingClientRect().top;
437
+ quadPoints[idx][0] = tx;
438
+ quadPoints[idx][1] = ty;
439
+ updateQuadSVG();
440
+ updateHandlesQuad();
441
+ updateCoordsQuad();
442
+ updateQuadRotateHandle();
443
+ }
444
  }
445
  function onUp(ev) {
446
+ resizing = false; document.body.style.cursor = ""; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp);
 
 
 
447
  }
448
+ document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
449
+ evt.stopPropagation();
 
450
  });
451
+ rectObj.el.appendChild(h); rectObj.handles.push(h);
 
 
 
 
 
 
 
 
 
 
 
452
  }
453
+ if(currentMode==="rect" || !quadPoints) updateHandles(rectObj);
454
+ else updateHandlesQuad();
455
  }
456
  function updateHandles(rectObj) {
457
+ let pts = getRectCornerPoints(rectObj);
458
+ for(let i=0;i<4;i++) {
459
+ rectObj.handles[i].style.left = pts[i][0]-rectObj.x+'px';
460
+ rectObj.handles[i].style.top = pts[i][1]-rectObj.y+'px';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  }
462
+ }
463
+ function updateHandlesQuad() {
464
+ if(!rectObj || !rectObj.handles || !quadPoints) return;
465
+ for(let i=0;i<4;i++) {
466
+ rectObj.handles[i].style.left = quadPoints[i][0] - rectObj.x + 'px';
467
+ rectObj.handles[i].style.top = quadPoints[i][1] - rectObj.y + 'px';
468
+ }
469
+ }
470
+ function getRectCenter(r) {
471
+ return [r.x + r.w/2, r.y + r.h/2];
472
+ }
473
+ function getRectCornerPoints(r) {
474
+ let cx = r.x + r.w/2, cy = r.y + r.h/2;
475
+ let rad = (r.angle||0)*Math.PI/180;
476
+ let dx = r.w/2, dy = r.h/2;
477
+ let corners = [
478
+ [-dx,-dy],
479
+ [ dx,-dy],
480
+ [ dx, dy],
481
+ [-dx, dy]
482
+ ];
483
+ return corners.map(([ox,oy])=>{
484
+ let x = ox*Math.cos(rad)-oy*Math.sin(rad)+cx;
485
+ let y = ox*Math.sin(rad)+oy*Math.cos(rad)+cy;
486
+ return [x,y];
487
+ });
488
  }
489
 
490
+ // --- 回転ハンドルと回転処理 ---
491
  function createRotateHandle(rectObj) {
492
+ if(rotateHandle) rotateHandle.remove();
493
+ rotateHandle = document.createElement('div');
494
+ rotateHandle.className = 'rotate-handle';
495
+ rectObj.el.appendChild(rotateHandle);
 
496
  updateRotateHandle(rectObj);
497
+ rotateHandle.addEventListener('mousedown', function(e) {
 
498
  rotating = true;
499
+ let [cx,cy] = getRectCenter(rectObj);
500
  const rectC = container.getBoundingClientRect();
501
+ let mx = e.clientX - rectC.left, my = e.clientY - rectC.top;
502
+ let startAngle = rectObj.angle || 0;
503
+ let baseAngle = Math.atan2(my - cx, mx - cy);
 
 
 
 
504
  document.body.style.cursor = "crosshair";
505
  function onMove(ev) {
506
+ let mx2 = ev.clientX - rectC.left, my2 = ev.clientY - rectC.top;
507
+ let currAngle = Math.atan2(my2 - cx, mx2 - cy);
508
+ let angle = startAngle + (currAngle - baseAngle) * 180 / Math.PI;
 
509
  rectObj.angle = angle;
510
+ updateRectUI(rectObj); updateHandles(rectObj); updateRotateHandle(rectObj); updateCoords(rectObj);
 
 
 
511
  }
512
  function onUp(ev) {
513
+ rotating = false; document.body.style.cursor = ""; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp);
 
 
 
514
  }
515
+ document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
 
516
  e.stopPropagation();
517
  });
518
  }
519
  function updateRotateHandle(rectObj) {
520
+ rotateHandle.style.left = '50%';
521
+ rotateHandle.style.top = '-32px';
522
+ rotateHandle.style.transform = `translate(-50%,0)`;
523
+ rotateHandle.style.display = currentMode === "rect" ? "block" : "none";
524
+ }
525
+ // Quad専用の回転ハンドル
526
+ function createQuadRotateHandle() {
527
+ if(quadRotateHandle) quadRotateHandle.remove();
528
+ quadRotateHandle = document.createElement('div');
529
+ quadRotateHandle.className = 'quad-rotate-handle rotate-handle';
530
+ quadRotateHandle.style.position = 'absolute';
531
+ updateQuadRotateHandle();
532
+ container.appendChild(quadRotateHandle);
533
+ quadRotateHandle.addEventListener('mousedown', function(e) {
534
+ rotating = true;
535
+ let center = getQuadCenter(quadPoints);
536
+ let rectC = container.getBoundingClientRect();
537
+ let mx = e.clientX - rectC.left, my = e.clientY - rectC.top;
538
+ let baseAngle = Math.atan2(my - center[1], mx - center[0]);
539
+ document.body.style.cursor = "crosshair";
540
+ function onMove(ev) {
541
+ let mx2 = ev.clientX - rectC.left, my2 = ev.clientY - rectC.top;
542
+ let currAngle = Math.atan2(my2 - center[1], mx2 - center[0]);
543
+ let diff = currAngle - baseAngle;
544
+ let cos = Math.cos(diff), sin = Math.sin(diff);
545
+ for(let i=0;i<4;i++) {
546
+ let x = quadPoints[i][0] - center[0], y = quadPoints[i][1] - center[1];
547
+ let rx = x * cos - y * sin;
548
+ let ry = x * sin + y * cos;
549
+ quadPoints[i][0] = rx + center[0];
550
+ quadPoints[i][1] = ry + center[1];
551
+ }
552
+ baseAngle = currAngle;
553
+ updateQuadSVG(); updateHandlesQuad(); updateCoordsQuad(); updateQuadRotateHandle();
554
+ }
555
+ function onUp(ev) {
556
+ rotating = false; document.body.style.cursor = ""; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp);
557
+ }
558
+ document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
559
+ e.stopPropagation();
560
+ });
561
+ }
562
+ function updateQuadRotateHandle() {
563
+ if(!quadPoints || !quadRotateHandle) return;
564
+ let center = getQuadCenter(quadPoints);
565
+ let p0 = quadPoints[0], p1 = quadPoints[1];
566
+ let dx = p1[0] - p0[0], dy = p1[1] - p0[1];
567
+ let mx = (p0[0]+p1[0])/2, my = (p0[1]+p1[1])/2;
568
+ let len = Math.sqrt(dx*dx+dy*dy);
569
+ let nx = -dy/len, ny = dx/len; // normal
570
+ let hx = mx + nx*40, hy = my + ny*40;
571
+ quadRotateHandle.style.left = (hx) + 'px';
572
+ quadRotateHandle.style.top = (hy) + 'px';
573
+ quadRotateHandle.style.transform = 'translate(-50%,-50%)';
574
+ quadRotateHandle.style.display = currentMode === "quad" && quadPoints ? "block" : "none";
575
+ }
576
+ function getQuadCenter(pts) {
577
+ let x=0, y=0;
578
+ for(let i=0;i<4;i++) { x+=pts[i][0]; y+=pts[i][1]; }
579
+ return [x/4, y/4];
580
  }
581
 
 
582
  function updateRectUI(rectObj) {
583
+ rectObj.el.style.left = rectObj.x + 'px';
584
+ rectObj.el.style.top = rectObj.y + 'px';
585
+ rectObj.el.style.width = rectObj.w + 'px';
586
+ rectObj.el.style.height = rectObj.h + 'px';
587
+ rectObj.el.style.transform = `rotate(${rectObj.angle||0}deg)`;
588
+ rectObj.el.style.transformOrigin = "50% 50%";
589
+ if(currentMode==="rect") {
590
+ if(rotateHandle) rotateHandle.style.display = "block";
591
+ if(quadRotateHandle) quadRotateHandle.style.display = "none";
592
+ }
593
+ if(currentMode==="quad" && quadPoints) {
594
+ updateQuadSVG();
595
+ updateHandlesQuad();
596
+ updateQuadRotateHandle();
597
+ if(rotateHandle) rotateHandle.style.display = "none";
598
+ if(quadRotateHandle) quadRotateHandle.style.display = "block";
599
+ }
600
  }
 
601
  function setSelectedRect(selected) {
602
+ if (!rectObj) return;
603
+ if (selected) rectObj.el.classList.add('selected');
604
+ else rectObj.el.classList.remove('selected');
 
 
 
 
605
  saveRectBtn.disabled = !rectObj;
606
  }
607
+ function clearRect() {
608
+ if(rectObj && rectObj.el) rectObj.el.remove();
609
+ if(document.getElementById('quad-svg')) document.getElementById('quad-svg').remove();
610
+ if(rotateHandle) { rotateHandle.remove(); rotateHandle = null; }
611
+ if(quadRotateHandle) { quadRotateHandle.remove(); quadRotateHandle = null; }
612
+ rectObj = null; quadPoints = null; setSelectedRect(false);
613
+ }
614
+
615
+ // --- SVG(自由四角形) ---
616
+ function showQuadSVG() {
617
+ let svg = document.getElementById('quad-svg');
618
+ if(svg) svg.remove();
619
+ svg = document.createElementNS("http://www.w3.org/2000/svg","svg");
620
+ svg.setAttribute("id","quad-svg");
621
+ svg.classList.add("quad-svg");
622
+ svg.setAttribute("width",container.clientWidth);
623
+ svg.setAttribute("height",container.clientHeight);
624
+ let poly = document.createElementNS("http://www.w3.org/2000/svg","polygon");
625
+ svg.appendChild(poly);
626
+ container.appendChild(svg);
627
+ updateQuadSVG();
628
+ }
629
+ function updateQuadSVG() {
630
+ let svg = document.getElementById('quad-svg');
631
+ if(!svg) showQuadSVG();
632
+ let poly = svg.querySelector('polygon');
633
+ if(quadPoints && poly) {
634
+ let pts = quadPoints.map(p=>p.join(',')).join(' ');
635
+ poly.setAttribute('points',pts);
636
+ }
637
+ }
638
 
639
+ // --- 座標計算 ---
640
  function getRectImageCoords(rectObj) {
641
+ let pts;
642
+ if(currentMode==="quad" && quadPoints) {
643
+ pts = quadPoints;
644
+ } else {
645
+ pts = getRectCornerPoints(rectObj);
646
+ }
647
  const {left:imgL, top:imgT, width:imgW, height:imgH} = dispImgInfo;
648
+ return pts.map(([rx, ry]) => [
649
+ Math.round((rx - imgL) * imgNaturalWidth / imgW),
650
+ Math.round((ry - imgT) * imgNaturalHeight / imgH)
651
+ ]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
  }
 
653
  function updateCoords(rectObj) {
654
+ if (!rectObj) { coords.innerHTML = "Coordinates:<br>"; return; }
 
 
 
655
  const imgPoints = getRectImageCoords(rectObj);
656
  coords.innerHTML =
657
  `Original image pixel coordinates (top-left origin):<br>
658
  1. (${imgPoints[0][0]}, ${imgPoints[0][1]})<br>
659
  2. (${imgPoints[1][0]}, ${imgPoints[1][1]})<br>
660
  3. (${imgPoints[2][0]}, ${imgPoints[2][1]})<br>
661
+ 4. (${imgPoints[3][0]}, ${imgPoints[3][1]})<br>`;
662
+ }
663
+ function updateCoordsQuad() {
664
+ updateCoords(rectObj);
665
  }
666
 
667
+ // --- Memory ---
668
+ saveRectBtn.addEventListener('click', () => {
669
+ if (!rectObj) return;
670
+ const imgPoints = getRectImageCoords(rectObj);
671
+ memory.push({
672
+ filename: currentImageName || '',
673
+ coords: imgPoints,
674
+ id: Date.now() + Math.random()
675
+ });
676
+ updateMemoryList();
677
+ clearRect();
678
+ coords.innerHTML = "Coordinates:<br>";
679
+ saveRectBtn.disabled = true;
680
+ });
681
  function updateMemoryList() {
682
  let html = '';
683
  memory.forEach((mem) => {
 
687
  TR(${mem.coords[1][0]},${mem.coords[1][1]})
688
  BR(${mem.coords[2][0]},${mem.coords[2][1]})
689
  BL(${mem.coords[3][0]},${mem.coords[3][1]})
 
690
  </span>
691
  <button class="memory-delete-btn" data-id="${mem.id}" title="Delete this memory">&times;</button>
692
  </div>`;
693
  });
694
  memoryList.innerHTML = html || "<span style='color:#999;'>No memory saved.</span>";
695
  }
 
696
  memoryList.addEventListener('click', (e) => {
697
  if (e.target.classList.contains('memory-delete-btn')) {
698
  const id = e.target.dataset.id;
 
701
  }
702
  });
703
 
 
704
  memorySaveBtn.addEventListener('click', async () => {
705
+ if (!memory.length) { alert("No memory to save."); return; }
 
 
 
 
706
  let outObj = {};
707
+ memory.forEach(mem => { outObj[mem.filename] = { print_area: mem.coords }; });
 
 
 
 
 
708
  let jsonString = JSON.stringify(outObj, null, 2);
 
709
  let filename = filenameInput.value.trim() || "templates.json";
710
  filename = filename.replace(/[\\\/:*?"<>|]/g, "_");
 
711
  if (window.showSaveFilePicker) {
712
  try {
713
  const opts = {
714
  suggestedName: filename,
715
+ types: [{ description: 'JSON Files', accept: {'application/json': ['.json', '.txt']} }]
 
 
 
 
 
716
  };
717
  const handle = await window.showSaveFilePicker(opts);
718
  const writable = await handle.createWritable();
719
+ await writable.write(jsonString); await writable.close();
720
+ alert("Memory saved!"); return;
721
+ } catch (err) { if (err.name !== "AbortError") alert("Failed to save file: " + err.message); }
 
 
 
 
 
 
722
  }
723
  const blob = new Blob([jsonString], {type: "application/json"});
724
+ const url = URL.createObjectURL(blob); const a = document.createElement('a');
725
+ a.href = url; a.download = filename; document.body.appendChild(a); a.click();
726
+ setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
 
 
 
 
 
 
 
727
  });
728
 
729
  document.addEventListener('keydown', e => {