diff --git a/README.md b/README.md
index 5cd66f2..24574fa 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,7 @@
-# imagemarkpengent README
+# imagemarkpengent
+
+
+
This is the README for your extension "imagemarkpengent". After writing up a brief description, we recommend including the following sections.
diff --git a/readme/images/top.png b/readme/images/top.png
new file mode 100644
index 0000000..07a8792
Binary files /dev/null and b/readme/images/top.png differ
diff --git a/src/webviewContent.ts b/src/webviewContent.ts
index 60be6cf..44c2c9f 100644
--- a/src/webviewContent.ts
+++ b/src/webviewContent.ts
@@ -28,6 +28,7 @@ export function getWebviewContent(imageSrc: string): string {
移動モード
+ 選択モード
マーク追加(Ctr+左クリック)
太さ:
@@ -49,12 +50,13 @@ export function getWebviewContent(imageSrc: string): string {
// UI要素
const moveBtn = document.getElementById('moveBtn');
+ const selectBtn = document.getElementById('selectBtn');
const markBtn = document.getElementById('markBtn');
const lineWidthSelect = document.getElementById('lineWidth');
const colorPicker = document.getElementById('colorPicker');
// 状態
- let mode = 'move'; // 'move' or 'mark'
+ let mode = 'move'; // 'move' or 'mark' or 'select'
let scale = 1;
let minScale = 0.1;
let maxScale = 10;
@@ -66,6 +68,14 @@ export function getWebviewContent(imageSrc: string): string {
let lastOffsetX = 0;
let lastOffsetY = 0;
let ctrlPressed = false;
+ // 図形移動用
+ let isMarkMoving = false;
+ let markMoveStart = null;
+ let markMoveOrigin = null;
+ // 図形リサイズ用
+ let isMarkResizing = false;
+ let resizeTarget = null; // {mark, handleIndex}
+ let resizeStart = null; // {x, y, orig}
// マーク(楕円)の配列
let marks = [];
@@ -79,24 +89,54 @@ export function getWebviewContent(imageSrc: string): string {
// UIイベント
moveBtn.onclick = () => setMode('move');
+ selectBtn.onclick = () => setMode('select');
markBtn.onclick = () => setMode('mark');
- lineWidthSelect.onchange = () => { markLineWidth = parseInt(lineWidthSelect.value, 10); draw(); };
- colorPicker.oninput = () => { markColor = colorPicker.value; draw(); };
+ lineWidthSelect.onchange = () => {
+ markLineWidth = parseInt(lineWidthSelect.value, 10);
+ let changed = false;
+ for (const mark of marks) {
+ if (mark.isSelected) {
+ mark.lineWidth = markLineWidth;
+ changed = true;
+ }
+ }
+ if (changed) pushUndo();
+ draw();
+ };
+ colorPicker.oninput = () => {
+ markColor = colorPicker.value;
+ let changed = false;
+ for (const mark of marks) {
+ if (mark.isSelected) {
+ mark.color = markColor;
+ changed = true;
+ }
+ }
+ if (changed) pushUndo();
+ draw();
+ };
function setMode(newMode) {
mode = newMode;
+ moveBtn.classList.remove('active-btn');
+ selectBtn.classList.remove('active-btn');
+ markBtn.classList.remove('active-btn');
if (mode === 'move') {
moveBtn.classList.add('active-btn');
- markBtn.classList.remove('active-btn');
canvas.style.cursor = 'grab';
+ } else if (mode === 'select') {
+ selectBtn.classList.add('active-btn');
+ canvas.style.cursor = 'pointer';
} else {
- moveBtn.classList.remove('active-btn');
markBtn.classList.add('active-btn');
canvas.style.cursor = 'crosshair';
}
- // プレビュー消去
tempMark = null;
markStart = null;
+ // 選択解除
+ if (mode !== 'select') {
+ for (const mark of marks) mark.isSelected = false;
+ }
draw();
}
@@ -112,6 +152,19 @@ export function getWebviewContent(imageSrc: string): string {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') {
undo();
}
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'y') {
+ redo();
+ }
+ // 選択モードでDeleteキー
+ if (mode === 'select' && (e.key === 'Delete' || e.key === 'Backspace')) {
+ const before = marks.length;
+ const newMarks = marks.filter(m => !m.isSelected);
+ if (newMarks.length !== marks.length) {
+ pushUndo();
+ marks = newMarks;
+ draw();
+ }
+ }
});
window.addEventListener('keyup', (e) => {
if (e.key === 'Control') {
@@ -147,6 +200,10 @@ export function getWebviewContent(imageSrc: string): string {
// マークをすべて描画
for (const mark of marks) {
drawEllipse(mark.x1, mark.y1, mark.x2, mark.y2, mark.color, mark.lineWidth);
+ if (mark.isSelected) {
+ drawEllipse(mark.x1, mark.y1, mark.x2, mark.y2, '#1976d2', (mark.lineWidth + 4), true);
+ drawResizeHandles(mark);
+ }
}
// プレビュー中のマーク
if (tempMark) {
@@ -171,6 +228,42 @@ export function getWebviewContent(imageSrc: string): string {
ctx.restore();
}
+ // リサイズハンドル描画
+ function drawResizeHandles(mark) {
+ const handles = getHandlePositions(mark);
+ for (const h of handles) {
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(h.x - 5, h.y - 5, 10, 10);
+ ctx.fillStyle = '#fff';
+ ctx.strokeStyle = '#1976d2';
+ ctx.lineWidth = 2 / scale;
+ ctx.fill();
+ ctx.stroke();
+ ctx.restore();
+ }
+ }
+ // 四隅のハンドル座標取得
+ function getHandlePositions(mark) {
+ return [
+ { x: mark.x1, y: mark.y1 }, // 左上
+ { x: mark.x2, y: mark.y1 }, // 右上
+ { x: mark.x2, y: mark.y2 }, // 右下
+ { x: mark.x1, y: mark.y2 }, // 左下
+ ];
+ }
+ // ハンドル上か判定
+ function getHandleAt(x, y, mark) {
+ const handles = getHandlePositions(mark);
+ for (let i = 0; i < handles.length; i++) {
+ const h = handles[i];
+ if (Math.abs(x - h.x) < 10 / scale && Math.abs(y - h.y) < 10 / scale) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
// ホイールでズーム
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
@@ -188,7 +281,7 @@ export function getWebviewContent(imageSrc: string): string {
draw();
}, { passive: false });
- // --- マーク追加モード or Ctrl+ドラッグで楕円追加 ---
+ // --- マーク追加モード or Ctrl+ドラッグで楕円追加 or 選択モードで選択 ---
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left - offsetX) / scale;
@@ -198,13 +291,66 @@ export function getWebviewContent(imageSrc: string): string {
markStart = { x1: x, y1: y };
tempMark = { x1: x, y1: y, x2: x, y2: y, color: markColor, lineWidth: markLineWidth };
}
+ } else if (mode === 'select') {
+ // 先にリサイズハンドル判定
+ let resized = false;
+ for (const mark of marks) {
+ if (mark.isSelected) {
+ const handleIdx = getHandleAt(x, y, mark);
+ if (handleIdx !== -1) {
+ isMarkResizing = true;
+ resizeTarget = { mark, handleIdx };
+ resizeStart = { x, y, orig: { x1: mark.x1, y1: mark.y1, x2: mark.x2, y2: mark.y2 } };
+ resized = true;
+ break;
+ }
+ }
+ }
+ if (resized) return;
+ // 既存マークの選択判定
+ let found = false;
+ for (let i = marks.length - 1; i >= 0; i--) { // 上に描画されているもの優先
+ const mark = marks[i];
+ if (isPointInEllipse(x, y, mark)) {
+ for (const m of marks) m.isSelected = false;
+ mark.isSelected = true;
+ found = true;
+ // 選択中マークの移動開始
+ isMarkMoving = true;
+ markMoveStart = { x, y };
+ // 複数選択対応のため配列で保存
+ markMoveOrigin = marks.filter(m => m.isSelected).map(m => ({ x1: m.x1, y1: m.y1, x2: m.x2, y2: m.y2 }));
+ break;
+ }
+ }
+ if (!found) {
+ for (const m of marks) m.isSelected = false;
+ isMarkMoving = false;
+ markMoveStart = null;
+ markMoveOrigin = null;
+ }
+ draw();
} else if (mode === 'move') {
- isDragging = true;
- dragStartX = e.clientX;
- dragStartY = e.clientY;
- lastOffsetX = offsetX;
- lastOffsetY = offsetY;
- canvas.style.cursor = 'grabbing';
+ // 追加: マーク上なら選択モードに切り替え
+ let found = false;
+ for (let i = marks.length - 1; i >= 0; i--) {
+ const mark = marks[i];
+ if (isPointInEllipse(x, y, mark)) {
+ for (const m of marks) m.isSelected = false;
+ mark.isSelected = true;
+ setMode('select');
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ isDragging = true;
+ dragStartX = e.clientX;
+ dragStartY = e.clientY;
+ lastOffsetX = offsetX;
+ lastOffsetY = offsetY;
+ canvas.style.cursor = 'grabbing';
+ }
}
});
window.addEventListener('mousemove', (e) => {
@@ -214,6 +360,42 @@ export function getWebviewContent(imageSrc: string): string {
if ((mode === 'mark' || ctrlPressed) && markStart) {
tempMark = { x1: markStart.x1, y1: markStart.y1, x2: x, y2: y, color: markColor, lineWidth: markLineWidth };
draw();
+ } else if (mode === 'select' && isMarkResizing && resizeTarget && resizeStart) {
+ // リサイズ処理
+ const { mark, handleIdx } = resizeTarget;
+ const dx = x - resizeStart.x;
+ const dy = y - resizeStart.y;
+ // ハンドルごとに座標を調整
+ let { x1, y1, x2, y2 } = resizeStart.orig;
+ if (handleIdx === 0) { // 左上
+ mark.x1 = x1 + dx;
+ mark.y1 = y1 + dy;
+ } else if (handleIdx === 1) { // 右上
+ mark.x2 = x2 + dx;
+ mark.y1 = y1 + dy;
+ } else if (handleIdx === 2) { // 右下
+ mark.x2 = x2 + dx;
+ mark.y2 = y2 + dy;
+ } else if (handleIdx === 3) { // 左下
+ mark.x1 = x1 + dx;
+ mark.y2 = y2 + dy;
+ }
+ draw();
+ } else if (mode === 'select' && isMarkMoving && markMoveStart && markMoveOrigin) {
+ // 選択中図形の移動
+ const dx = x - markMoveStart.x;
+ const dy = y - markMoveStart.y;
+ let idx = 0;
+ for (let i = 0; i < marks.length; i++) {
+ if (marks[i].isSelected) {
+ const orig = markMoveOrigin[idx++];
+ marks[i].x1 = orig.x1 + dx;
+ marks[i].y1 = orig.y1 + dy;
+ marks[i].x2 = orig.x2 + dx;
+ marks[i].y2 = orig.y2 + dy;
+ }
+ }
+ draw();
} else if (mode === 'move' && isDragging) {
offsetX = lastOffsetX + (e.clientX - dragStartX);
offsetY = lastOffsetY + (e.clientY - dragStartY);
@@ -224,12 +406,24 @@ export function getWebviewContent(imageSrc: string): string {
if ((mode === 'mark' || ctrlPressed) && markStart && tempMark) {
if (Math.abs(tempMark.x2 - tempMark.x1) >= 2 / scale && Math.abs(tempMark.y2 - tempMark.y1) >= 2 / scale) {
pushUndo();
- marks.push({ ...tempMark });
+ marks.push({ ...tempMark, isSelected: false });
}
markStart = null;
tempMark = null;
if (mode === 'mark') setMode('move');
draw();
+ } else if (mode === 'select' && isMarkResizing) {
+ isMarkResizing = false;
+ resizeTarget = null;
+ resizeStart = null;
+ pushUndo();
+ draw();
+ } else if (mode === 'select' && isMarkMoving) {
+ isMarkMoving = false;
+ markMoveStart = null;
+ markMoveOrigin = null;
+ pushUndo();
+ draw();
} else if (mode === 'move' && isDragging) {
isDragging = false;
canvas.style.cursor = 'grab';
@@ -237,19 +431,41 @@ export function getWebviewContent(imageSrc: string): string {
});
let undoStack = [];
+ let redoStack = [];
function pushUndo() {
undoStack.push(JSON.stringify(marks));
if (undoStack.length > 100) undoStack.shift();
+ redoStack = []; // Undoした後に新しい操作があればRedo履歴は消す
}
function undo() {
- if (undoStack.length > 0) {
+ if (undoStack.length > 1) {
+ redoStack.push(JSON.stringify(marks));
marks = JSON.parse(undoStack.pop());
draw();
}
}
+ function redo() {
+ if (redoStack.length > 0) {
+ undoStack.push(JSON.stringify(marks));
+ marks = JSON.parse(redoStack.pop());
+ draw();
+ }
+ }
+
+ // 楕円内判定関数
+ function isPointInEllipse(px, py, mark) {
+ const cx = (mark.x1 + mark.x2) / 2;
+ const cy = (mark.y1 + mark.y2) / 2;
+ const rx = Math.abs(mark.x2 - mark.x1) / 2;
+ const ry = Math.abs(mark.y2 - mark.y1) / 2;
+ if (rx < 1e-2 || ry < 1e-2) return false;
+ return ((px - cx) ** 2) / (rx ** 2) + ((py - cy) ** 2) / (ry ** 2) <= 1;
+ }
// 初期リサイズ
resizeCanvas();
+ // 最初の状態をundoStackにpush
+ pushUndo();