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"
+ >
+
+
+
+