From efe72ff065a1f0cf556a47770f041e01d5e8ac48 Mon Sep 17 00:00:00 2001 From: Bryce Date: Fri, 27 Mar 2026 15:26:25 -0700 Subject: [PATCH] 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 --- tools/ora_editor/ORA_EDITOR.md | 56 ++++-- tools/ora_editor/templates/editor.html | 267 +++++++++++++++++-------- 2 files changed, 227 insertions(+), 96 deletions(-) diff --git a/tools/ora_editor/ORA_EDITOR.md b/tools/ora_editor/ORA_EDITOR.md index f60cc84..a228bce 100644 --- a/tools/ora_editor/ORA_EDITOR.md +++ b/tools/ora_editor/ORA_EDITOR.md @@ -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) | @@ -109,18 +110,25 @@ All file paths are relative to the project root: `/home/noti/dev/ai-game-2` ## Canvas Display -### Image Rendering -- Layers rendered as stacked `` elements with CSS positioning -- Base layer at bottom, entity layers stacked above -- Visibility controlled by `opacity: 0` or `opacity: 1` +### Image Rendering +- Individual layers rendered as stacked `` 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
- - - + +
@@ -193,9 +201,12 @@ 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 + - **Re-roll**: Re-run extraction with same params - **Use This Mask**: Add masked layer to ORA, close modal - **Cancel**: Discard mask, 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/` +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 --- diff --git a/tools/ora_editor/templates/editor.html b/tools/ora_editor/templates/editor.html index 5d75889..13743b7 100644 --- a/tools/ora_editor/templates/editor.html +++ b/tools/ora_editor/templates/editor.html @@ -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; } @@ -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: '', @@ -184,13 +190,27 @@ }); }, - startDrawing() { + startDrawing() { console.log('[ORA EDITOR] Starting polygon drawing mode'); 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(); }, - 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; + 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,24 +315,47 @@ 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(); } }, - clearPolygon() { + clearPolygon() { this.isDrawing = false; 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 @@ + 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" + > + + + +
+ Scale: + + +
@@ -510,18 +632,25 @@
-
+
- - + +