diff --git a/CHANGELOG.md b/CHANGELOG.md index 85ce014a..37c9575b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented here. ## [unreleased] +## [0.26.2] - Aug 13th, 2026 +- **Fix regression in `set_annotations()` where hover feedback and the brush stopped working after a swap.** The 0.26.1 bulk-teardown path called `$("#canvasses__").empty()`, which removed not just the per-annotation canvases but also the subtask's front canvas and the `#dialogs__` container (which owns the brush circle and polygon ender). `state.front_context` was left pointing at a detached canvas so hover highlights painted into nothing, and the brush had no parent to attach to. The teardown now removes only `> canvas.annotation_canvas` children, leaving the front/back canvases and dialogs container intact. +- New demo: [`demo/set-annotations.html`](demo/set-annotations.html) with buttons that swap through empty / small / medium / large / RLE-bitmask / raw-Uint8Array-bitmask presets so this regression stays visible. + ## [0.26.1] - Aug 13th, 2026 - **Bitmask annotations can now be imported as raw `Uint8Array` payloads.** In addition to the existing RLE `{ counts, size }` shape, `spatial_payload` may now be `{ data: Uint8Array, size: [height, width] }`. This lets callers that already have the mask as a pixel buffer skip the RLE encode step before handing it to ULabel. - **Loader no longer flashes on fast operations.** `ULabelLoader.add_loader_div()` now appends the overlay hidden and reveals it only after a delay (200 ms by default). Callers can pass an explicit `delay_ms` — including `0` to opt back into immediate-show. diff --git a/demo.js b/demo.js index d57087a0..66a3c5b6 100644 --- a/demo.js +++ b/demo.js @@ -14,4 +14,5 @@ console.log(`http://localhost:${port}/box-roi.html`); console.log(`http://localhost:${port}/resume-from.html`); console.log(`http://localhost:${port}/row-filtering-example.html`); console.log(`http://localhost:${port}/bitmask-example.html`); +console.log(`http://localhost:${port}/set-annotations.html`); console.log(`http://localhost:${port}/live_demo.html`); \ No newline at end of file diff --git a/demo/set-annotations.html b/demo/set-annotations.html new file mode 100644 index 00000000..5945de41 --- /dev/null +++ b/demo/set-annotations.html @@ -0,0 +1,294 @@ + + + + + ULabel - set_annotations Demo + + + + + + + + + + + +
+ set_annotations: + + + + + + + + idle +
+ After each swap, verify: hover an annotation shows the id-dialog highlight; + press g to toggle brush — the brush circle must appear at the cursor. + Open devtools and inspect window.ulabel. +
+
+
+ + + + + diff --git a/package-lock.json b/package-lock.json index f383879b..563c4573 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ulabel", - "version": "0.26.1", + "version": "0.26.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ulabel", - "version": "0.26.1", + "version": "0.26.2", "license": "MIT", "devDependencies": { "@eslint/config-inspector": "^1.3.0", diff --git a/package.json b/package.json index b0ec51c0..dd4fa5bd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ulabel", "description": "An image annotation tool.", - "version": "0.26.1", + "version": "0.26.2", "main": "dist/ulabel.min.js", "module": "dist/ulabel.min.js", "types": "dist/index.d.ts", diff --git a/src/index.js b/src/index.js index 2b38453c..10aee928 100644 --- a/src/index.js +++ b/src/index.js @@ -7227,7 +7227,10 @@ export class ULabel { delete anno["_bitmask_box_hint"]; } } - $("#canvasses__" + subtask).empty(); + // Only remove per-annotation canvases. The subtask's front/back canvases and the + // #dialogs__ container (which owns the brush circle, polygon ender, and + // id dialogs) live in the same parent and MUST survive. + $("#canvasses__" + subtask + " > canvas.annotation_canvas").remove(); this.subtasks[subtask]["state"]["annotation_contexts"] = {}; } diff --git a/src/version.js b/src/version.js index 2d6761a6..7e324148 100644 --- a/src/version.js +++ b/src/version.js @@ -1 +1 @@ -export const ULABEL_VERSION = "0.26.1"; +export const ULABEL_VERSION = "0.26.2"; diff --git a/tests/annotation.test.js b/tests/annotation.test.js index 10d71779..a52d5d43 100644 --- a/tests/annotation.test.js +++ b/tests/annotation.test.js @@ -381,11 +381,23 @@ describe("Annotation Processing", () => { const canvasses = document.createElement("div"); canvasses.id = "canvasses__test_task"; document.body.appendChild(canvasses); - const fake_canvas = document.createElement("canvas"); - fake_canvas.id = "canvas__fake"; - canvasses.appendChild(fake_canvas); + // Mirror the init-time DOM: back canvas, front canvas, dialogs container, then + // per-annotation canvases (which are the only children we should be removing). + const back = document.createElement("canvas"); + back.id = "back_canvas"; + canvasses.appendChild(back); + const front = document.createElement("canvas"); + front.id = "front_canvas"; + canvasses.appendChild(front); + const dialogs = document.createElement("div"); + dialogs.id = "dialogs__test_task"; + canvasses.appendChild(dialogs); + const anno_canvas = document.createElement("canvas"); + anno_canvas.id = "canvas__anno"; + anno_canvas.classList.add("annotation_canvas"); + canvasses.appendChild(anno_canvas); ulabel.subtasks.test_task.state.annotation_contexts = { - canvas__fake: { annotation_ids: [anno_id], context: {} }, + canvas__anno: { annotation_ids: [anno_id], context: {} }, }; ulabel._clear_subtask_annotation_canvases("test_task"); @@ -394,8 +406,14 @@ describe("Annotation Processing", () => { expect(annotation._mask).toBeUndefined(); expect(annotation._mask_render).toBeUndefined(); expect(annotation._bitmask_box_hint).toBeUndefined(); - // Canvas DOM subtree wiped. - expect(canvasses.children.length).toBe(0); + // Per-annotation canvas removed. + expect(document.getElementById("canvas__anno")).toBeNull(); + // Back/front canvases and the dialogs container MUST survive; otherwise hover + // feedback (front canvas) and brush/polygon-ender attach points (dialogs div) + // become detached-DOM ghosts. + expect(document.getElementById("back_canvas")).not.toBeNull(); + expect(document.getElementById("front_canvas")).not.toBeNull(); + expect(document.getElementById("dialogs__test_task")).not.toBeNull(); // Annotation-context bookkeeping reset. expect(ulabel.subtasks.test_task.state.annotation_contexts).toEqual({}); });