fix(annotation-editor): ricalcola marker/frecce/aree al resize del canvas (#28)
_fitCanvasToImage() prima scalava solo canvas e sfondo: gli oggetti già piazzati restavano alle vecchie coordinate pixel, e un salvataggio subito dopo scriveva il nuovo imageScale accanto a posizioni mai spostate - markers/frecce/aree finivano fuori posto anche nel viewer dell'operatore. Ora, quando la scala cambia (f = newScale/oldScale, conservata in this._currentScale), ogni oggetto viene ricalcolato coerentemente con getAnnotationsJson/loadAnnotationsJson (il riferimento): - left/top sempre *= f - area (fabric.Rect): width/height *= f, non scaleX/scaleY - è quello che getAnnotationsJson legge e loadAnnotationsJson ricostruisce - text (IText): fontSize *= f, stesso motivo - marker/arrow (Group): scaleX/scaleY *= f, il loro width/height non viene mai riletto al caricamento - arrow: arrowX1/Y1/X2/Y2 *= f, sono nello stesso spazio di left/top (li aggiorna anche object:modified con un delta) e finiscono nel JSON Aggiunta guardia beforeunload quando isDirty (annotazioni non salvate). Test in tests/js/test_annotation_editor_resize.js (node, senza Fabric.js reale: richiede jsdom non installato e il progetto non ha npm deps - canvas stand-in minimale che copre solo la superficie usata da _fitCanvasToImage). annotation-viewer.js non toccato. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,12 @@ function annotationEditor() {
|
||||
_bgImageNatW: 0,
|
||||
_bgImageNatH: 0,
|
||||
|
||||
/** Background image scale applied at the last _fitCanvasToImage() call
|
||||
* (#28) - the ratio between this and the newly computed scale is how
|
||||
* existing annotation objects get repositioned/resized on a resize,
|
||||
* instead of being left at their old pixel coordinates. */
|
||||
_currentScale: 0,
|
||||
|
||||
/** Annotations JSON to load after background image is ready */
|
||||
_pendingAnnotations: null,
|
||||
|
||||
@@ -218,6 +224,16 @@ function annotationEditor() {
|
||||
this._debouncedResize = debounce(this.resizeCanvas.bind(this), 200);
|
||||
window.addEventListener('resize', this._debouncedResize);
|
||||
|
||||
// Unsaved annotations are easy to lose by accident (a stray back-nav,
|
||||
// an accidental tab close) - isDirty already tracks exactly that (#28).
|
||||
this._onBeforeUnload = function (e) {
|
||||
if (!self.isDirty) return;
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
return '';
|
||||
};
|
||||
window.addEventListener('beforeunload', this._onBeforeUnload);
|
||||
|
||||
// ---- Bridge: listen for commands from recipeEditor ----
|
||||
|
||||
this._onToolChange = function (e) {
|
||||
@@ -471,6 +487,15 @@ function annotationEditor() {
|
||||
* so there is zero empty space and canvas coords map 1:1 to
|
||||
* scaled-image coords.
|
||||
*
|
||||
* Existing annotation objects are repositioned/resized by the same
|
||||
* factor the background just changed by (#28): before this, only the
|
||||
* canvas and the background image were rescaled on a resize
|
||||
* (window resize, sidebar toggle, ...) - every marker/arrow/area stayed
|
||||
* at its old pixel coordinates, drifting visually off the drawing, and
|
||||
* a save right after wrote the *new* imageScale next to coordinates
|
||||
* that had never actually moved to match it, corrupting the position
|
||||
* for the operator's viewer too.
|
||||
*
|
||||
* @param {fabric.Image} img - Fabric image object (background)
|
||||
*/
|
||||
_fitCanvasToImage(img) {
|
||||
@@ -486,9 +511,12 @@ function annotationEditor() {
|
||||
if (!natW || !natH) return;
|
||||
|
||||
// Scale to fit container width; never upscale
|
||||
var scale = Math.min(containerWidth / natW, 1);
|
||||
var canvasW = Math.round(natW * scale);
|
||||
var canvasH = Math.round(natH * scale);
|
||||
var newScale = Math.min(containerWidth / natW, 1);
|
||||
var oldScale = this._currentScale || newScale;
|
||||
var f = newScale / oldScale;
|
||||
|
||||
var canvasW = Math.round(natW * newScale);
|
||||
var canvasH = Math.round(natH * newScale);
|
||||
|
||||
this.canvas.setDimensions({ width: canvasW, height: canvasH });
|
||||
|
||||
@@ -496,15 +524,55 @@ function annotationEditor() {
|
||||
img,
|
||||
this.canvas.renderAll.bind(this.canvas),
|
||||
{
|
||||
scaleX: scale,
|
||||
scaleY: scale,
|
||||
scaleX: newScale,
|
||||
scaleY: newScale,
|
||||
originX: 'left',
|
||||
originY: 'top',
|
||||
}
|
||||
);
|
||||
|
||||
this.canvas.calcOffset();
|
||||
this.canvas.forEachObject(function (obj) { obj.setCoords(); });
|
||||
this.canvas.forEachObject(function (obj) {
|
||||
if (f !== 1) {
|
||||
obj.left *= f;
|
||||
obj.top *= f;
|
||||
|
||||
if (obj.objectType === 'area') {
|
||||
// fabric.Rect: getAnnotationsJson serialises obj.width/height
|
||||
// directly (not width * scaleX), and loadAnnotationsJson
|
||||
// reconstructs the rect from that same width/height - so the
|
||||
// size has to travel through width/height too, or a save right
|
||||
// after this resize would silently drop it.
|
||||
obj.width = (obj.width || 0) * f;
|
||||
obj.height = (obj.height || 0) * f;
|
||||
} else if (obj.objectType === 'text') {
|
||||
// fabric.IText: same reasoning, fontSize is the serialised field.
|
||||
var newFontSize = (obj.fontSize || 18) * f;
|
||||
obj.set('fontSize', newFontSize);
|
||||
obj.textFontSize = newFontSize;
|
||||
} else {
|
||||
// Groups (marker, arrow): their own width/height are never read
|
||||
// back on load (addMarker/addArrow only use position and, for
|
||||
// arrows, the endpoints below), so scaleX/scaleY - which drives
|
||||
// 100% of their rendered size - is what has to change.
|
||||
obj.scaleX = (obj.scaleX || 1) * f;
|
||||
obj.scaleY = (obj.scaleY || 1) * f;
|
||||
}
|
||||
|
||||
if (obj.objectType === 'arrow') {
|
||||
// Stored endpoints live in the same coordinate space as
|
||||
// obj.left/top (see the object:modified handler, which adds a
|
||||
// plain pixel delta to them) - so they scale by f exactly like
|
||||
// left/top do, independently of the group's own scaleX/scaleY.
|
||||
obj.arrowX1 = (obj.arrowX1 || 0) * f;
|
||||
obj.arrowY1 = (obj.arrowY1 || 0) * f;
|
||||
obj.arrowX2 = (obj.arrowX2 || 0) * f;
|
||||
obj.arrowY2 = (obj.arrowY2 || 0) * f;
|
||||
}
|
||||
}
|
||||
obj.setCoords();
|
||||
});
|
||||
this._currentScale = newScale;
|
||||
this.canvas.renderAll();
|
||||
},
|
||||
|
||||
@@ -1269,6 +1337,9 @@ function annotationEditor() {
|
||||
if (this._debouncedResize) {
|
||||
window.removeEventListener('resize', this._debouncedResize);
|
||||
}
|
||||
if (this._onBeforeUnload) {
|
||||
window.removeEventListener('beforeunload', this._onBeforeUnload);
|
||||
}
|
||||
if (this._onToolChange) {
|
||||
window.removeEventListener('anno-tool-change', this._onToolChange);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user