fix: canvas fits image exactly, chained annotation loading, arrow move tracking

- Canvas now sizes to match the scaled image dimensions (zero empty space)
  instead of using a fixed aspect ratio, fixing coordinate mismatch between
  editor and viewer
- Annotations load only after background image is ready via _pendingAnnotations
  pattern, preventing placement at wrong coordinates
- Arrow endpoints (arrowX1/Y1/X2/Y2) update on object:modified using
  transform delta, so moved arrows serialize at correct position
- Coordinate scaling on load: coordScale = currentImageScale / savedImageScale
  handles annotations saved at different screen widths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Adriano
2026-02-08 16:10:21 +01:00
parent f2df6be060
commit 9e1bb20b36
6 changed files with 336 additions and 85 deletions
+1 -1
View File
@@ -436,7 +436,7 @@
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
</script>
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=4"></script>
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
<script>
/**
* Recipe Preview - Minimal Alpine.js component
+38 -7
View File
@@ -34,8 +34,9 @@
cursor: not-allowed;
}
/* Canvas container */
.canvas-container {
/* Canvas container — use custom class name to avoid conflict with
Fabric.js internal .canvas-container wrapper div */
.anno-canvas-wrap {
position: relative;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
@@ -43,6 +44,10 @@
overflow: hidden;
min-height: 400px;
}
.anno-canvas-wrap canvas {
display: block;
}
/* Fabric.js internal wrapper also needs display:block on canvases */
.canvas-container canvas {
display: block;
}
@@ -141,7 +146,7 @@
<span x-text="saving ? window.__i18n_taskDrawing.saving : window.__i18n_taskDrawing.saveAnnotations"></span>
</button>
<a href="{{ url_for('maker.task_editor', recipe_id=recipe.id) }}"
<a :href="'{{ url_for('maker.task_editor', recipe_id=recipe.id) }}' + '?_=' + Date.now()"
class="btn btn-secondary gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 17l-5-5m0 0l5-5m-5 5h12"/>
@@ -245,7 +250,7 @@
<!-- Separator -->
<div class="w-px h-6 bg-[var(--border-color)] mx-1"></div>
<!-- Delete -->
<!-- Delete selected -->
<button type="button"
class="anno-btn text-red-500 hover:text-red-600"
@click="deleteAnnotation()"
@@ -255,7 +260,21 @@
<path stroke-linecap="round" stroke-linejoin="round"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
<span class="hidden sm:inline">{{ _('Elimina') }}</span>
{{ _('Elimina') }}
</button>
<!-- Clear All -->
<button type="button"
class="anno-btn text-red-500 hover:text-red-600"
@click="if (confirm('{{ _('Eliminare tutte le annotazioni?') }}')) clearAllAnnotations()"
title="{{ _('Cancella tutto') }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9 13h6m2 9H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
<path stroke-linecap="round" stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"/>
</svg>
{{ _('Cancella tutto') }}
</button>
</div>
@@ -337,7 +356,7 @@
<div class="tmf-card">
<div class="tmf-card-body p-0">
<div x-data="annotationEditor()"
class="canvas-container"
class="anno-canvas-wrap"
id="annotationCanvasContainer">
<canvas id="annotationCanvas" x-ref="fabricCanvas"></canvas>
</div>
@@ -353,7 +372,7 @@
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
</script>
<script src="{{ url_for('static', filename='js/annotation-editor.js') }}?v=10"></script>
<script src="{{ url_for('static', filename='js/annotation-editor.js') }}?v=25"></script>
<script>
function taskDrawing() {
const taskData = window.__taskData || {};
@@ -459,6 +478,14 @@ function taskDrawing() {
this.errorMessage = data.detail || window.__i18n_taskDrawing.saveError;
} else {
this.successMessage = window.__i18n_taskDrawing.saveSuccess;
// Store updated annotations in sessionStorage so task_editor
// can pick them up and refresh the preview immediately.
try {
sessionStorage.setItem('taskDrawingUpdated', JSON.stringify({
taskId: this.taskId,
annotations_json: this.annotationsJson
}));
} catch (_e) { /* sessionStorage may be unavailable */ }
}
} catch (err) {
console.error('saveAnnotations error:', err);
@@ -480,6 +507,10 @@ function taskDrawing() {
window.dispatchEvent(new CustomEvent('anno-delete-selected'));
},
clearAllAnnotations() {
window.dispatchEvent(new CustomEvent('anno-clear-all'));
},
setAnnoColor(color) {
this.annoColor = color;
window.dispatchEvent(new CustomEvent('anno-color-change', {
+31 -1
View File
@@ -1075,7 +1075,14 @@
{% block extra_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script>if(typeof pdfjsLib!=='undefined')pdfjsLib.GlobalWorkerOptions.workerSrc='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';</script>
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=4"></script>
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
<script>
// Force reload when page is restored from browser bfcache (back/forward navigation).
// This ensures fresh data is displayed after editing annotations in task_drawing.
window.addEventListener('pageshow', function (event) {
if (event.persisted) { window.location.reload(); }
});
</script>
<script>
function taskEditor() {
return {
@@ -1126,6 +1133,29 @@ function taskEditor() {
// Sort tasks by order_index on load
this.tasks.sort((a, b) => (a.order_index || 0) - (b.order_index || 0));
// Check if returning from task_drawing with updated annotations
try {
var updatedRaw = sessionStorage.getItem('taskDrawingUpdated');
if (updatedRaw) {
sessionStorage.removeItem('taskDrawingUpdated');
var updated = JSON.parse(updatedRaw);
if (updated.taskId) {
var task = this.tasks.find(function(t) { return t.id === updated.taskId; });
if (task) {
// Merge updated annotations into local state
if (updated.annotations_json) {
task.annotations_json = (typeof updated.annotations_json === 'string')
? JSON.parse(updated.annotations_json) : updated.annotations_json;
}
// Auto-expand the edited task and render preview
this.expandedTask = task.id;
this.renderTaskPreview(task);
return; // skip default expansion logic
}
}
}
} catch (_e) { /* sessionStorage parse error - ignore */ }
// Auto-expand the first task if there is only one
if (this.tasks.length === 1) {
this.expandedTask = this.tasks[0].id;