@@ -43,6 +43,39 @@ const BBOX_EDGE_PX = 6;
4343const MIN_BBOX_PX = 4 ;
4444const 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+
4679class 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