Fix ORA editor layer visibility and mask preview

- Layer visibility toggles now show/hide individual layer images on canvas
- Added tint red checkbox per layer for manual mask verification
- Mask preview modal uses CSS mask-image to show red overlay only on selected areas (white pixels become semi-transparent red, black pixels remain transparent)
- Added spinner icons to Open and Extract Mask buttons during long operations
This commit is contained in:
2026-03-27 15:26:25 -07:00
parent 0c1fb8ccca
commit efe72ff065
2 changed files with 227 additions and 96 deletions

View File

@@ -99,7 +99,8 @@ All file paths are relative to the project root: `/home/noti/dev/ai-game-2`
| Operation | Description | | Operation | Description |
|-----------|-------------| |-----------|-------------|
| **Toggle Visibility** | Show/hide layer on canvas | | **Toggle Visibility** | Show/hide specific layer image on canvas |
| **Tint Red Preview** | Apply red tint to layer for mask verification (client-only) |
| **Rename** | Change layer/entity name | | **Rename** | Change layer/entity name |
| **Delete** | Remove layer from ORA | | **Delete** | Remove layer from ORA |
| **Reorder** | Move layer up/down in stack (changes z-index) | | **Reorder** | Move layer up/down in stack (changes z-index) |
@@ -109,18 +110,25 @@ All file paths are relative to the project root: `/home/noti/dev/ai-game-2`
## Canvas Display ## Canvas Display
### Image Rendering ### Image Rendering
- Layers rendered as stacked `<img>` elements with CSS positioning - Individual layers rendered as stacked `<img>` elements with CSS positioning
- Base layer at bottom, entity layers stacked above - Layers stacked in order from list (visual z-index matches layer order)
- Visibility controlled by `opacity: 0` or `opacity: 1` - Visibility togglecheckbox hides/shows each layer's image on canvas
- Tint checkbox applies semi-transparent red overlay for mask verification (client-only, visual aid)
- No server-side compositing for preview - No server-side compositing for preview
### Layer DOM Structure ### Layer DOM Structure
```html ```html
<div class="relative w-full h-full"> <div class="relative w-full h-full">
<img src="/api/image/base" class="absolute inset-0"> <!-- Individual layers rendered in order (x-show controls visibility) -->
<img src="/api/image/door_0" class="absolute inset-0" style="opacity: 1"> <template x-for="layer in layers">
<img src="/api/image/chest_0" class="absolute inset-0" style="opacity: 0"> <img
x-show="layer.visible"
:src="'/api/image/layer/' + layer.name"
class="absolute inset-0"
:style="layer.tintRed ? 'mix-blend-multiply; opacity: 0.6;' : ''"
>
</template>
<!-- Polygon overlay when active --> <!-- Polygon overlay when active -->
<canvas id="polygon-canvas" class="absolute inset-0 pointer-events-auto"></canvas> <canvas id="polygon-canvas" class="absolute inset-0 pointer-events-auto"></canvas>
</div> </div>
@@ -193,9 +201,12 @@ canvas.addEventListener('click', (e) => {
### Mask Preview Modal ### Mask Preview Modal
When mask is ready: When mask is ready:
1. Full-screen modal appears 1. Full-screen modal appears
2. Shows base image with mask applied as colored tint overlay 2. Shows base image with semi-transparent red overlay where the mask is white (selected area)
- Uses CSS `mask-image` property to use grayscale values as alpha channel
- White pixels = fully opaque red tint, black/dark pixels = transparent (no tint)
- Dark/gray areas of the mask remain invisible so you can clearly see the mask boundary
3. Three buttons: 3. Three buttons:
- **Re-roll**: Re-run extraction with same params - **Re-roll**: Re-run extraction with same params
- **Use This Mask**: Add masked layer to ORA, close modal - **Use This Mask**: Add masked layer to ORA, close modal
- **Cancel**: Discard mask, close modal - **Cancel**: Discard mask, close modal
@@ -355,6 +366,14 @@ Serve a layer PNG. Returns image data.
#### `GET /api/image/base` #### `GET /api/image/base`
Serve base/merged image. Serve base/merged image.
#### `GET /api/image/layer/<layer_name>`
Serve a specific layer as image.
**Query params:**
- `ora_path`: Path to ORA file
**Response:** PNG image data or 404 if layer not found.
#### `GET /api/image/polygon` #### `GET /api/image/polygon`
Serve polygon overlay image (for drawing mode). Serve polygon overlay image (for drawing mode).
@@ -506,12 +525,13 @@ Check if temp file was modified.
``` ```
┌──────────────────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────────────┐
│ [Open: ________________________] [Open File] [Settings ⚙] │ │ [Open: ________________________] [🌀 Open] [Settings ⚙]
├───────────────────┬──────────────────────────────────────────────────┤ ├───────────────────┬──────────────────────────────────────────────────┤
│ LAYERS │ │ │ LAYERS │ │
│ ☑ base │ │ │ ☑ base │ │
│ ☑ door_0 │ [Image Canvas with │ │ ☑ door_0 │ [Image Canvas with │
│ ☐ chest_0 │ stacked layers] │ ☐ chest_0 │ stacked layers - visibility toggles
│ │ show/hide individual layer images] │
│ │ │ │ │ │
│ [Rename] [Delete] │ │ │ [Rename] [Delete] │ │
│ [▲ Up] [▼ Down] │ │ │ [▲ Up] [▼ Down] │ │
@@ -528,7 +548,7 @@ Check if temp file was modified.
│ MASK EXTRACTION │ │ │ MASK EXTRACTION │ │
│ Subject: [______]│ │ │ Subject: [______]│ │
│ ☑ Use polygon │ │ │ ☑ Use polygon │ │
│ [Extract Mask] │ [🌀 Extract Mask]│ (spinner shown when extracting)
│ │ │ │ │ │
│ ─────────────────│ │ │ ─────────────────│ │
│ [Open in Krita] │ │ │ [Open in Krita] │ │
@@ -538,6 +558,8 @@ Check if temp file was modified.
└──────────────────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────────────┘
``` ```
Legend: ☑ = visible checkbox, ☒ = tint red checkbox (red when checked)
--- ---
## Mask Preview Modal ## Mask Preview Modal
@@ -547,7 +569,8 @@ Check if temp file was modified.
│ EXTRACTED MASK │ │ EXTRACTED MASK │
│ ───────────────────────────────────────────────────── │ │ ───────────────────────────────────────────────────── │
│ │ │ │
│ [Base image with mask applied as tinted overlay] │ [Base image with RED mask overlay]
│ (mask shown in semi-transparent red) │
│ │ │ │
│ [Re-roll] [Use This Mask] [Cancel] │ │ [Re-roll] [Use This Mask] [Cancel] │
│ │ │ │
@@ -557,6 +580,7 @@ Check if temp file was modified.
- **Re-roll**: Re-runs extraction with same params - **Re-roll**: Re-runs extraction with same params
- **Use This Mask**: Calls `POST /api/layer/add`, closes modal - **Use This Mask**: Calls `POST /api/layer/add`, closes modal
- **Cancel**: Closes modal, mask discarded - **Cancel**: Closes modal, mask discarded
- **Red tint**: Uses CSS filter to render grayscale mask as semi-transparent red
--- ---

View File

@@ -10,7 +10,7 @@
::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #374151; } ::-webkit-scrollbar-track { background: #374151; }
::-webkit-scrollbar-thumb { background: #6B7280; border-radius: 4px; } ::-webkit-scrollbar-thumb { background: #6B7280; border-radius: 4px; }
.polygon-point { position: absolute; width: 8px; height: 8px; background: #0F0; border-radius: 50%; transform: translate(-50%, -50%); pointer-events: none; } .polygon-point { position: absolute; width: 20px; height: 20px; background: #FFFFFF; border: 3px solid #FF0000; border-radius: 50%; transform: translate(-50%, -50%); pointer-events: none; z-index: 100; }
</style> </style>
<!-- Alpine data component - MUST be defined before x-data directive --> <!-- Alpine data component - MUST be defined before x-data directive -->
@@ -26,15 +26,21 @@
imageWidth: 800, imageWidth: 800,
imageHeight: 600, imageHeight: 600,
// Display scale (percentage, default 25%)
scale: 25,
// Selection state // Selection state
selectedLayer: null, selectedLayer: null,
// Polygon state // Polygon state
isDrawing: false, isDrawing: false,
polygonPoints: [], polygonPoints: [],
polygonColor: '#FF0000', polygonColor: '#FF0000',
polygonWidth: 2, polygonWidth: 2,
polygonPreviewUrl: null, polygonPreviewUrl: null,
clickTimeout: null,
pendingPoint: null,
// Mask extraction state // Mask extraction state
maskSubject: '', maskSubject: '',
@@ -184,13 +190,27 @@
}); });
}, },
startDrawing() { startDrawing() {
console.log('[ORA EDITOR] Starting polygon drawing mode'); console.log('[ORA EDITOR] Starting polygon drawing mode');
this.isDrawing = true; this.isDrawing = true;
this.polygonPoints = []; this.polygonPoints = [];
// Reset click/double-click state for new drawing session
canvasDoubleClickPending = false;
// Clear the preview image from previous drawings (hide old overlay)
this.polygonPreviewUrl = null;
// Note: We don't clear backend storage here - let updatePolygonPreview() overwrite it later
// Setup canvas after it's created // Setup canvas after it's created
setTimeout(() => this.setupCanvas(), 50); setTimeout(() => {
this.setupCanvas();
const canvas = document.getElementById('polygonCanvas');
if (canvas) {
canvas.style.display = 'block';
}
}, 50);
}, },
setupCanvas() { setupCanvas() {
@@ -220,18 +240,74 @@
y = Math.max(0, Math.min(1, y)); y = Math.max(0, Math.min(1, y));
this.polygonPoints.push({ x, y }); this.polygonPoints.push({ x, y });
this.updatePolygonPreview(); this.drawPolygonOnCanvas();
}, },
async updatePolygonPreview() { drawPolygonOnCanvas() {
const canvas = document.getElementById('polygonCanvas');
if (!canvas) return;
if (this.polygonPoints.length < 2) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (this.polygonPoints.length < 2) return;
// Draw line segments connecting points
ctx.beginPath();
ctx.strokeStyle = this.polygonColor;
ctx.lineWidth = this.polygonWidth;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
const startPoint = this.polygonPoints[0];
ctx.moveTo(startPoint.x * canvas.width, startPoint.y * canvas.height);
for (let i = 1; i < this.polygonPoints.length; i++) {
const point = this.polygonPoints[i];
ctx.lineTo(point.x * canvas.width, point.y * canvas.height);
}
// Close the polygon if we have 3+ points
if (this.polygonPoints.length >= 3) {
ctx.closePath();
}
ctx.stroke();
// Draw point markers
ctx.fillStyle = '#FFFFFF';
for (const point of this.polygonPoints) {
const px = point.x * canvas.width;
const py = point.y * canvas.height;
ctx.beginPath();
ctx.arc(px, py, 6, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.strokeStyle = this.polygonColor;
ctx.lineWidth = 2;
ctx.arc(px, py, 6, 0, Math.PI * 2);
ctx.stroke();
}
},
async updatePolygonPreview() {
console.log('[UPDATE] Updating preview with', this.polygonPoints.length, 'points');
if (!this.oraPath || this.polygonPoints.length < 3) return; if (!this.oraPath || this.polygonPoints.length < 3) return;
const pointsToSend = [...this.polygonPoints]; // Make a copy
console.log('[UPDATE] Sending points:', JSON.stringify(pointsToSend));
const response = await fetch('/api/polygon', { const response = await fetch('/api/polygon', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
ora_path: this.oraPath, ora_path: this.oraPath,
points: this.polygonPoints, points: pointsToSend,
color: this.polygonColor, color: this.polygonColor,
width: this.polygonWidth width: this.polygonWidth
}) })
@@ -239,24 +315,47 @@
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
this.polygonPreviewUrl = data.overlay_url; console.log('[UPDATE] Got preview URL:', data.overlay_url);
this.polygonPreviewUrl = data.overlay_url + '&ts=' + Date.now(); // Bust cache
} }
}, },
finishDrawing() { async finishDrawing() {
this.isDrawing = false; console.log('[FINISH] finishDrawing called with', this.polygonPoints.length, 'points');
// Don't change isDrawing until after we've updated the preview!
if (this.polygonPoints.length >= 3) { if (this.polygonPoints.length >= 3) {
// Keep polygon visible await this.updatePolygonPreview();
console.log('[FINISH] Preview URL set to:', this.polygonPreviewUrl);
// Now exit drawing mode - this will show the img with new URL
this.isDrawing = false;
// Hide canvas
const canvas = document.getElementById('polygonCanvas');
if (canvas) {
canvas.style.display = 'none';
}
} else { } else {
this.clearPolygon(); this.clearPolygon();
} }
}, },
clearPolygon() { clearPolygon() {
this.isDrawing = false; this.isDrawing = false;
this.polygonPoints = []; this.polygonPoints = [];
this.polygonPreviewUrl = null; this.polygonPreviewUrl = null;
// Hide canvas
const canvas = document.getElementById('polygonCanvas');
if (canvas) {
canvas.style.display = 'none';
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
if (this.oraPath) { if (this.oraPath) {
fetch('/api/polygon/clear', { fetch('/api/polygon/clear', {
method: 'POST', method: 'POST',
@@ -299,8 +398,7 @@
this.showMaskModal = true; this.showMaskModal = true;
} catch (e) { } catch (e) {
console.error('[ORA EDITOR] Error extracting mask:', e); console.error('[ORA EDITOR] Error extracting mask:', e);
document.getElementById('maskError').innerHTML = e.message; this.lastError = e.message;
document.getElementById('maskError').style.display = 'block';
} finally { } finally {
this.isExtracting = false; this.isExtracting = false;
} }
@@ -383,8 +481,11 @@
<button <button
@click="openFile()" @click="openFile()"
:disabled="!filePath || isLoading" :disabled="!filePath || isLoading"
class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 px-4 py-2 rounded transition" class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 px-4 py-2 rounded transition flex items-center justify-center gap-2"
>Open</button> >
<span x-show="isLoading" class="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-white"></span>
<span x-text="isLoading ? 'Opening...' : 'Open'"></span>
</button>
</div> </div>
<button <button
@click="openInKrita()" @click="openInKrita()"
@@ -401,6 +502,18 @@
:disabled="!oraPath || isLoading" :disabled="!oraPath || isLoading"
class="bg-green-600 hover:bg-green-700 disabled:bg-gray-600 px-4 py-2 rounded transition" class="bg-green-600 hover:bg-green-700 disabled:bg-gray-600 px-4 py-2 rounded transition"
>Save</button> >Save</button>
<div class="flex items-center gap-2 bg-gray-800 rounded px-3 py-2 border border-gray-700">
<span class="text-sm text-gray-300">Scale:</span>
<input
type="range"
x-model.number="scale"
min="10"
max="100"
step="5"
class="w-32 accent-blue-500"
>
<span class="text-sm text-gray-300 w-12" x-text="scale + '%'"></span>
</div>
<button <button
@click="showSettings = true" @click="showSettings = true"
class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded transition" class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded transition"
@@ -430,8 +543,16 @@
:checked="layer.visible" :checked="layer.visible"
@change="toggleVisibility(layer.name, $event.target.checked)" @change="toggleVisibility(layer.name, $event.target.checked)"
class="w-4 h-4 rounded" class="w-4 h-4 rounded"
title="Toggle visibility"
> >
<span class="flex-1 text-sm truncate" x-text="layer.name"></span> <span class="flex-1 text-sm truncate" x-text="layer.name"></span>
<input
type="checkbox"
:checked="layer.tintRed || false"
@change="layer.tintRed = $event.target.checked"
class="w-4 h-4 rounded"
title="Tint red for preview"
>
</div> </div>
</template> </template>
</div> </div>
@@ -496,8 +617,9 @@
<button <button
@click="extractMask()" @click="extractMask()"
:disabled="!maskSubject.trim() || isExtracting || !oraPath" :disabled="!maskSubject.trim() || isExtracting || !oraPath"
class="w-full bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-600 px-4 py-2 rounded transition" class="w-full bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-600 px-4 py-2 rounded transition flex items-center justify-center gap-2"
> >
<span x-show="isExtracting" class="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-white"></span>
<span x-show="!isExtracting">Extract Mask</span> <span x-show="!isExtracting">Extract Mask</span>
<span x-show="isExtracting">Extracting...</span> <span x-show="isExtracting">Extracting...</span>
</button> </button>
@@ -510,18 +632,25 @@
<!-- Main canvas area --> <!-- Main canvas area -->
<main class="flex-1 bg-gray-800 rounded-lg border border-gray-700 p-4 relative overflow-auto" style="min-height: 600px;"> <main class="flex-1 bg-gray-800 rounded-lg border border-gray-700 p-4 relative overflow-auto" style="min-height: 600px;">
<div id="imageContainer" x-show="!isLoading && !error && layers.length > 0" class="relative inline-block" <div id="imageContainer" x-show="!isLoading && !error && layers.length > 0" class="relative inline-block origin-top-left"
:style="`width: ${imageWidth}px; height: ${imageHeight}px; position: relative;`"> :style="`width: ${imageWidth}px; height: ${imageHeight}px; transform: scale(${scale / 100});`">
<!-- Layer display --> <!-- Layer display -->
<div class="relative w-full h-full"> <div class="relative w-full h-full">
<!-- Base image --> <!-- Individual layer images (rendered in order) -->
<img <template x-for="layer in layers" :key="'layer-' + layer.name">
:src="'/api/image/base?ora_path=' + encodeURIComponent(oraPath)" <img
class="absolute inset-0 w-full h-full object-contain" x-show="layer.visible"
@load="initCanvas" :src="'/api/image/layer/' + encodeURIComponent(layer.name) + '?ora_path=' + encodeURIComponent(oraPath)"
id="baseImage" class="absolute inset-0 w-full h-full object-contain"
> >
<!-- Red tint overlay when enabled -->
<div
x-show="layer.visible && layer.tintRed"
class="absolute inset-0 bg-red-500 mix-blend-multiply"
style="opacity: 0.5;"
></div>
</template>
<!-- Polygon points markers when drawing --> <!-- Polygon points markers when drawing -->
<template x-for="(point, idx) in polygonPoints" :key="idx"> <template x-for="(point, idx) in polygonPoints" :key="idx">
@@ -545,8 +674,8 @@
:width="imageWidth" :width="imageWidth"
:height="imageHeight" :height="imageHeight"
class="absolute inset-0 cursor-crosshair pointer-events-auto border-2 border-dashed border-blue-500 opacity-90" class="absolute inset-0 cursor-crosshair pointer-events-auto border-2 border-dashed border-blue-500 opacity-90"
@click="handleCanvasClick" @click="handleCanvasClick($event)"
@dblclick="handleCanvasDoubleClick" @dblclick="handleCanvasDoubleClick($event)"
></canvas> ></canvas>
</div> </div>
@@ -578,13 +707,21 @@
<div class="bg-gray-800 rounded-lg p-6 max-w-2xl w-full mx-4 border border-gray-600" style="max-height: 90vh; display: flex; flex-direction: column;"> <div class="bg-gray-800 rounded-lg p-6 max-w-2xl w-full mx-4 border border-gray-600" style="max-height: 90vh; display: flex; flex-direction: column;">
<h2 class="text-xl font-bold mb-4">Extracted Mask</h2> <h2 class="text-xl font-bold mb-4">Extracted Mask</h2>
<div class="flex-1 overflow-auto bg-gray-700 rounded mb-4 relative" :style="`min-height: 300px; max-height: 50vh;`"> <div class="flex-1 overflow-auto bg-gray-700 rounded mb-4 flex items-center justify-center p-4" :style="`min-height: 300px; max-height: 50vh;`">
<!-- Base image --> <!-- Container for properly aligned base + mask -->
<img :src="'/api/image/base?ora_path=' + encodeURIComponent(oraPath)" class="relative w-full h-auto object-contain"> <div class="relative inline-block">
<!-- Base image -->
<!-- Mask overlay with tint --> <img
<div x-show="tempMaskUrl" class="absolute inset-0 bg-green-500 opacity-30 pointer-events-none"> :src="'/api/image/base?ora_path=' + encodeURIComponent(oraPath)"
<img :src="tempMaskUrl" class="w-full h-auto object-contain mix-blend-screen"> class="border border-gray-600"
style="max-width: 100%; max-height: calc(50vh - 2rem);"
>
<!-- Semi-transparent red overlay with mask applied as alpha channel -->
<div x-show="tempMaskUrl"
class="absolute inset-0 pointer-events-none border border-gray-600"
:style="`background-color: rgba(255, 80, 80, 0.6); -webkit-mask-image: url(${tempMaskUrl}); mask-image: url(${tempMaskUrl}); -webkit-mask-size: cover; mask-size: cover; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;`">
</div>
</div> </div>
</div> </div>
@@ -621,8 +758,10 @@
</div> </div>
<!-- Minimal JS for canvas polygon drawing --> <!-- Canvas click handler - works with Alpine -->
<script> <script>
let canvasDoubleClickPending = false;
function initCanvas() { function initCanvas() {
const canvas = document.getElementById('polygonCanvas'); const canvas = document.getElementById('polygonCanvas');
if (!canvas) return; if (!canvas) return;
@@ -632,39 +771,13 @@ function initCanvas() {
canvas.style.height = canvas.parentElement.offsetHeight + 'px'; canvas.style.height = canvas.parentElement.offsetHeight + 'px';
} }
// Access Alpine store for canvas click
function handleCanvasClick(e) { function handleCanvasClick(e) {
const container = e.target.closest('[x-data]'); // Skip click if it was part of a double-click
if (!container) return; if (canvasDoubleClickPending) {
return;
// Get click position
const rect = container.getBoundingClientRect();
let x = (e.clientX - rect.left) / rect.width;
let y = (e.clientY - rect.top) / rect.height;
// Normalize to 0-1 range
x = Math.max(0, Math.min(1, x));
y = Math.max(0, Math.min(1, y));
// Get Alpine component and add point
const alpineComponent = $data(container);
if (alpineComponent) {
alpineComponent.addPolygonPoint(x, y);
} }
}
const container = document.getElementById('imageContainer');
function handleCanvasDoubleClick(e) {
e.preventDefault();
const alpineComponent = $data(e.target.closest('[x-data]'));
if (alpineComponent) {
alpineComponent.finishDrawing();
}
}
<!-- Canvas click handler - works with Alpine -->
<script>
function handleCanvasClick(e) {
const container = e.target.closest('#imageContainer');
if (!container) return; if (!container) return;
const rect = container.getBoundingClientRect(); const rect = container.getBoundingClientRect();
@@ -679,8 +792,8 @@ function handleCanvasClick(e) {
const alpineEl = container.closest('[x-data]'); const alpineEl = container.closest('[x-data]');
if (!alpineEl) return; if (!alpineEl) return;
// Get the Alpine component instance using $data // Get Alpine component instance using Alpine.$data
const store = $data(alpineEl); const store = Alpine.$data(alpineEl);
if (!store) return; if (!store) return;
store.addPolygonPoint(x, y); store.addPolygonPoint(x, y);
@@ -688,13 +801,16 @@ function handleCanvasClick(e) {
function handleCanvasDoubleClick(e) { function handleCanvasDoubleClick(e) {
e.preventDefault(); e.preventDefault();
const container = e.target.closest('#imageContainer'); // Mark that a double-click happened so we skip the subsequent clicks
canvasDoubleClickPending = true;
const container = document.getElementById('imageContainer');
if (!container) return; if (!container) return;
const alpineEl = container.closest('[x-data]'); const alpineEl = container.closest('[x-data]');
if (!alpineEl) return; if (!alpineEl) return;
const store = $data(alpineEl); const store = Alpine.$data(alpineEl);
if (!store) return; if (!store) return;
if (store.isDrawing && store.polygonPoints.length >= 3) { if (store.isDrawing && store.polygonPoints.length >= 3) {
@@ -702,16 +818,7 @@ function handleCanvasDoubleClick(e) {
} }
} }
// Wait for Alpine to initialize, then setup canvas
document.addEventListener('alpine:init', () => {
setTimeout(() => {
const canvas = document.getElementById('polygonCanvas');
if (canvas) {
canvas.addEventListener('click', handleCanvasClick);
canvas.addEventListener('dblclick', handleCanvasDoubleClick);
}
}, 200);
});
</script> </script>
</body> </body>
</html> </html>