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:
@@ -99,7 +99,8 @@ All file paths are relative to the project root: `/home/noti/dev/ai-game-2`
|
||||
|
||||
| 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 |
|
||||
| **Delete** | Remove layer from ORA |
|
||||
| **Reorder** | Move layer up/down in stack (changes z-index) |
|
||||
@@ -110,17 +111,24 @@ All file paths are relative to the project root: `/home/noti/dev/ai-game-2`
|
||||
## Canvas Display
|
||||
|
||||
### Image Rendering
|
||||
- Layers rendered as stacked `<img>` elements with CSS positioning
|
||||
- Base layer at bottom, entity layers stacked above
|
||||
- Visibility controlled by `opacity: 0` or `opacity: 1`
|
||||
- Individual layers rendered as stacked `<img>` elements with CSS positioning
|
||||
- Layers stacked in order from list (visual z-index matches layer order)
|
||||
- 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
|
||||
|
||||
### Layer DOM Structure
|
||||
```html
|
||||
<div class="relative w-full h-full">
|
||||
<img src="/api/image/base" class="absolute inset-0">
|
||||
<img src="/api/image/door_0" class="absolute inset-0" style="opacity: 1">
|
||||
<img src="/api/image/chest_0" class="absolute inset-0" style="opacity: 0">
|
||||
<!-- Individual layers rendered in order (x-show controls visibility) -->
|
||||
<template x-for="layer in layers">
|
||||
<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 -->
|
||||
<canvas id="polygon-canvas" class="absolute inset-0 pointer-events-auto"></canvas>
|
||||
</div>
|
||||
@@ -193,7 +201,10 @@ canvas.addEventListener('click', (e) => {
|
||||
### Mask Preview Modal
|
||||
When mask is ready:
|
||||
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:
|
||||
- **Re-roll**: Re-run extraction with same params
|
||||
- **Use This Mask**: Add masked layer to ORA, close modal
|
||||
@@ -355,6 +366,14 @@ Serve a layer PNG. Returns image data.
|
||||
#### `GET /api/image/base`
|
||||
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`
|
||||
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 │ │
|
||||
│ ☑ base │ │
|
||||
│ ☑ door_0 │ [Image Canvas with │
|
||||
│ ☐ chest_0 │ stacked layers] │
|
||||
│ ☑ □ base │ │
|
||||
│ ☑ □ door_0 │ [Image Canvas with │
|
||||
│ ☐ ☒ chest_0 │ stacked layers - visibility toggles │
|
||||
│ │ show/hide individual layer images] │
|
||||
│ │ │
|
||||
│ [Rename] [Delete] │ │
|
||||
│ [▲ Up] [▼ Down] │ │
|
||||
@@ -528,7 +548,7 @@ Check if temp file was modified.
|
||||
│ MASK EXTRACTION │ │
|
||||
│ Subject: [______]│ │
|
||||
│ ☑ Use polygon │ │
|
||||
│ [Extract Mask] │ │
|
||||
│ [🌀 Extract Mask]│ (spinner shown when extracting) │
|
||||
│ │ │
|
||||
│ ─────────────────│ │
|
||||
│ [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
|
||||
@@ -547,7 +569,8 @@ Check if temp file was modified.
|
||||
│ 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] │
|
||||
│ │
|
||||
@@ -557,6 +580,7 @@ Check if temp file was modified.
|
||||
- **Re-roll**: Re-runs extraction with same params
|
||||
- **Use This Mask**: Calls `POST /api/layer/add`, closes modal
|
||||
- **Cancel**: Closes modal, mask discarded
|
||||
- **Red tint**: Uses CSS filter to render grayscale mask as semi-transparent red
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-track { background: #374151; }
|
||||
::-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>
|
||||
|
||||
<!-- Alpine data component - MUST be defined before x-data directive -->
|
||||
@@ -26,15 +26,21 @@
|
||||
imageWidth: 800,
|
||||
imageHeight: 600,
|
||||
|
||||
// Display scale (percentage, default 25%)
|
||||
scale: 25,
|
||||
|
||||
// Selection state
|
||||
selectedLayer: null,
|
||||
|
||||
|
||||
// Polygon state
|
||||
isDrawing: false,
|
||||
polygonPoints: [],
|
||||
polygonColor: '#FF0000',
|
||||
polygonWidth: 2,
|
||||
polygonPreviewUrl: null,
|
||||
clickTimeout: null,
|
||||
pendingPoint: null,
|
||||
|
||||
// Mask extraction state
|
||||
maskSubject: '',
|
||||
@@ -189,8 +195,22 @@
|
||||
this.isDrawing = true;
|
||||
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
|
||||
setTimeout(() => this.setupCanvas(), 50);
|
||||
setTimeout(() => {
|
||||
this.setupCanvas();
|
||||
const canvas = document.getElementById('polygonCanvas');
|
||||
if (canvas) {
|
||||
canvas.style.display = 'block';
|
||||
}
|
||||
}, 50);
|
||||
},
|
||||
|
||||
setupCanvas() {
|
||||
@@ -220,18 +240,74 @@
|
||||
y = Math.max(0, Math.min(1, y));
|
||||
|
||||
this.polygonPoints.push({ x, y });
|
||||
this.updatePolygonPreview();
|
||||
this.drawPolygonOnCanvas();
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
const pointsToSend = [...this.polygonPoints]; // Make a copy
|
||||
console.log('[UPDATE] Sending points:', JSON.stringify(pointsToSend));
|
||||
|
||||
const response = await fetch('/api/polygon', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
ora_path: this.oraPath,
|
||||
points: this.polygonPoints,
|
||||
points: pointsToSend,
|
||||
color: this.polygonColor,
|
||||
width: this.polygonWidth
|
||||
})
|
||||
@@ -239,14 +315,27 @@
|
||||
|
||||
if (response.ok) {
|
||||
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() {
|
||||
this.isDrawing = false;
|
||||
async finishDrawing() {
|
||||
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) {
|
||||
// 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 {
|
||||
this.clearPolygon();
|
||||
}
|
||||
@@ -257,6 +346,16 @@
|
||||
this.polygonPoints = [];
|
||||
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) {
|
||||
fetch('/api/polygon/clear', {
|
||||
method: 'POST',
|
||||
@@ -299,8 +398,7 @@
|
||||
this.showMaskModal = true;
|
||||
} catch (e) {
|
||||
console.error('[ORA EDITOR] Error extracting mask:', e);
|
||||
document.getElementById('maskError').innerHTML = e.message;
|
||||
document.getElementById('maskError').style.display = 'block';
|
||||
this.lastError = e.message;
|
||||
} finally {
|
||||
this.isExtracting = false;
|
||||
}
|
||||
@@ -383,8 +481,11 @@
|
||||
<button
|
||||
@click="openFile()"
|
||||
:disabled="!filePath || isLoading"
|
||||
class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 px-4 py-2 rounded transition"
|
||||
>Open</button>
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
<button
|
||||
@click="openInKrita()"
|
||||
@@ -401,6 +502,18 @@
|
||||
:disabled="!oraPath || isLoading"
|
||||
class="bg-green-600 hover:bg-green-700 disabled:bg-gray-600 px-4 py-2 rounded transition"
|
||||
>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
|
||||
@click="showSettings = true"
|
||||
class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded transition"
|
||||
@@ -430,8 +543,16 @@
|
||||
:checked="layer.visible"
|
||||
@change="toggleVisibility(layer.name, $event.target.checked)"
|
||||
class="w-4 h-4 rounded"
|
||||
title="Toggle visibility"
|
||||
>
|
||||
<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>
|
||||
</template>
|
||||
</div>
|
||||
@@ -496,8 +617,9 @@
|
||||
<button
|
||||
@click="extractMask()"
|
||||
: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">Extracting...</span>
|
||||
</button>
|
||||
@@ -510,18 +632,25 @@
|
||||
<!-- 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;">
|
||||
|
||||
<div id="imageContainer" x-show="!isLoading && !error && layers.length > 0" class="relative inline-block"
|
||||
:style="`width: ${imageWidth}px; height: ${imageHeight}px; position: relative;`">
|
||||
<div id="imageContainer" x-show="!isLoading && !error && layers.length > 0" class="relative inline-block origin-top-left"
|
||||
:style="`width: ${imageWidth}px; height: ${imageHeight}px; transform: scale(${scale / 100});`">
|
||||
|
||||
<!-- Layer display -->
|
||||
<div class="relative w-full h-full">
|
||||
<!-- Base image -->
|
||||
<!-- Individual layer images (rendered in order) -->
|
||||
<template x-for="layer in layers" :key="'layer-' + layer.name">
|
||||
<img
|
||||
:src="'/api/image/base?ora_path=' + encodeURIComponent(oraPath)"
|
||||
x-show="layer.visible"
|
||||
:src="'/api/image/layer/' + encodeURIComponent(layer.name) + '?ora_path=' + encodeURIComponent(oraPath)"
|
||||
class="absolute inset-0 w-full h-full object-contain"
|
||||
@load="initCanvas"
|
||||
id="baseImage"
|
||||
>
|
||||
<!-- 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 -->
|
||||
<template x-for="(point, idx) in polygonPoints" :key="idx">
|
||||
@@ -545,8 +674,8 @@
|
||||
:width="imageWidth"
|
||||
:height="imageHeight"
|
||||
class="absolute inset-0 cursor-crosshair pointer-events-auto border-2 border-dashed border-blue-500 opacity-90"
|
||||
@click="handleCanvasClick"
|
||||
@dblclick="handleCanvasDoubleClick"
|
||||
@click="handleCanvasClick($event)"
|
||||
@dblclick="handleCanvasDoubleClick($event)"
|
||||
></canvas>
|
||||
|
||||
</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;">
|
||||
<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;`">
|
||||
<!-- Container for properly aligned base + mask -->
|
||||
<div class="relative inline-block">
|
||||
<!-- Base image -->
|
||||
<img :src="'/api/image/base?ora_path=' + encodeURIComponent(oraPath)" class="relative w-full h-auto object-contain">
|
||||
<img
|
||||
:src="'/api/image/base?ora_path=' + encodeURIComponent(oraPath)"
|
||||
class="border border-gray-600"
|
||||
style="max-width: 100%; max-height: calc(50vh - 2rem);"
|
||||
>
|
||||
|
||||
<!-- Mask overlay with tint -->
|
||||
<div x-show="tempMaskUrl" class="absolute inset-0 bg-green-500 opacity-30 pointer-events-none">
|
||||
<img :src="tempMaskUrl" class="w-full h-auto object-contain mix-blend-screen">
|
||||
<!-- 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>
|
||||
|
||||
@@ -621,8 +758,10 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Minimal JS for canvas polygon drawing -->
|
||||
<!-- Canvas click handler - works with Alpine -->
|
||||
<script>
|
||||
let canvasDoubleClickPending = false;
|
||||
|
||||
function initCanvas() {
|
||||
const canvas = document.getElementById('polygonCanvas');
|
||||
if (!canvas) return;
|
||||
@@ -632,39 +771,13 @@ function initCanvas() {
|
||||
canvas.style.height = canvas.parentElement.offsetHeight + 'px';
|
||||
}
|
||||
|
||||
// Access Alpine store for canvas click
|
||||
function handleCanvasClick(e) {
|
||||
const container = e.target.closest('[x-data]');
|
||||
if (!container) 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);
|
||||
}
|
||||
// Skip click if it was part of a double-click
|
||||
if (canvasDoubleClickPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
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');
|
||||
const container = document.getElementById('imageContainer');
|
||||
if (!container) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
@@ -679,8 +792,8 @@ function handleCanvasClick(e) {
|
||||
const alpineEl = container.closest('[x-data]');
|
||||
if (!alpineEl) return;
|
||||
|
||||
// Get the Alpine component instance using $data
|
||||
const store = $data(alpineEl);
|
||||
// Get Alpine component instance using Alpine.$data
|
||||
const store = Alpine.$data(alpineEl);
|
||||
if (!store) return;
|
||||
|
||||
store.addPolygonPoint(x, y);
|
||||
@@ -688,13 +801,16 @@ function handleCanvasClick(e) {
|
||||
|
||||
function handleCanvasDoubleClick(e) {
|
||||
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;
|
||||
|
||||
const alpineEl = container.closest('[x-data]');
|
||||
if (!alpineEl) return;
|
||||
|
||||
const store = $data(alpineEl);
|
||||
const store = Alpine.$data(alpineEl);
|
||||
if (!store) return;
|
||||
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user