Skip to content

Commit 4d35aea

Browse files
committed
fix(points-bbox): wipe annotations only when image identity changes (filename for /view URLs, content fingerprint for data URLs); preserve across re-runs of the same image
1 parent 42b48ab commit 4d35aea

1 file changed

Lines changed: 47 additions & 6 deletions

File tree

js/points_bbox_editor.js

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,39 @@ const BBOX_EDGE_PX = 6;
4343
const MIN_BBOX_PX = 4;
4444
const CLICK_THRESHOLD_PX = 4;
4545

46+
// Reduce a reference-image URL to a stable identity key that survives
47+
// graph re-runs but flips when the user actually swaps the input image.
48+
// /view?filename=foo.png&subfolder=&type=input → "view:input/foo.png"
49+
// data:image/jpeg;base64,/9j/4AAQ… → "data:<len>:<head>:<tail>"
50+
// anything else (raw path / blob / http URL) → the URL itself
51+
// For data URLs we sample length + head + tail of the base64 payload —
52+
// identical content + identical JPEG/PNG encoder settings produce
53+
// identical bytes, so this catches "same image, re-run" without paying
54+
// a full hash. Real image swaps virtually always change the length.
55+
function _refIdentityKey(url) {
56+
if (typeof url !== "string" || !url) return "";
57+
if (url.startsWith("data:")) {
58+
const comma = url.indexOf(",");
59+
const payload = comma >= 0 ? url.slice(comma + 1) : url;
60+
const head = payload.slice(0, 24);
61+
const tail = payload.slice(-24);
62+
return `data:${payload.length}:${head}:${tail}`;
63+
}
64+
const qIdx = url.indexOf("?");
65+
if (qIdx >= 0 && url.slice(0, qIdx).endsWith("/view")) {
66+
try {
67+
const params = new URLSearchParams(url.slice(qIdx + 1));
68+
const fn = params.get("filename") || "";
69+
const sub = params.get("subfolder") || "";
70+
const type = params.get("type") || "";
71+
return `view:${type}/${sub}/${fn}`;
72+
} catch (_) {
73+
// fall through
74+
}
75+
}
76+
return url;
77+
}
78+
4679
class Editor {
4780
constructor(node) {
4881
this.node = node;
@@ -205,7 +238,11 @@ class Editor {
205238

206239
setRefImage(url, origW, origH) {
207240
if (url === this.refUrl && this.refImg && this.refImg.complete) return;
241+
const newKey = _refIdentityKey(url);
242+
const prevKey = this.refKey;
243+
const isReplacement = prevKey != null && newKey !== prevKey;
208244
this.refUrl = url;
245+
this.refKey = newKey;
209246
const img = new Image();
210247
img.crossOrigin = "anonymous";
211248
img.onload = () => {
@@ -220,12 +257,16 @@ class Editor {
220257
if (wW) wW.value = w;
221258
if (hW) hW.value = h;
222259
}
223-
// NOTE: previously we wiped points/bboxes whenever the URL
224-
// changed, but that fires every time the upstream node
225-
// re-executes (it returns a fresh base64 data URL each run)
226-
// even when the underlying image is identical. The user's
227-
// annotations would silently disappear. Keep them — the
228-
// toolbar's Clear-all button is the explicit way to dump them.
260+
// Only wipe annotations when the *identity* of the upstream
261+
// image changes (filename for /view URLs, content fingerprint
262+
// for data URLs). Re-running the graph with the same image
263+
// keeps the user's points/bboxes intact.
264+
if (isReplacement && (this.points.length || this.bboxes.length)) {
265+
this.pushUndo();
266+
this.points = [];
267+
this.bboxes = [];
268+
this.save?.();
269+
}
229270
this._fitted = false;
230271
this.onLoaded?.();
231272
};

0 commit comments

Comments
 (0)