` with the same `container_id` *within the same animation frame* can briefly have two `ULabel` instances attached to `document`; when the old instance's teardown runs it will remove `.ulabel`-namespaced document/window handlers belonging to the new instance too. If your SPA does synchronous same-id replacement, set `auto_destroy_on_detach: false` and call `oldUlabel.destroy()` yourself *before* mounting the replacement — `destroy()` is synchronous, so this ordering is race-free.
+### `on_active_class_change`
+*(subtask_key: string, class_id: number) => void* -- Called after a subtask's active class actually changes, whatever the writer: `set_active_class`, a toolbox class-button click, or a class-select keybind (including keybinds users customize through the `Keybinds` toolbox item). Not called for no-op re-selections, rejected ids, or delete-mode toggles (which freeze the selection). Default is `null`.
+
+### `on_subtask_change`
+*(subtask_key: string, old_subtask_key: string) => void* -- Called after the current subtask actually changes, whatever the writer: `set_subtask`, a toolbox tab click, or the `switch_subtask_keybind`. Not called when the target subtask is already current. Default is `null`.
+
+### `on_focus_active_class_change`
+*(subtask_key: string, enabled: boolean) => void* -- Called after a subtask's `focus_active_class` flag actually changes, whatever the writer: `set_focus_active_class` or the `toggle_class_focus_keybind`. Not called when the flag is already at the target value, so a host may re-sync other subtasks from the callback without recursing. Default is `null`.
+
## Display Utility Functions
@@ -651,6 +690,8 @@ Display utilities are provided for a constructed `ULabel` object.
*(string, int) => Promise<string>* -- Changes the image source for a given frame. Displays the loading spinner while the new image loads. Returns a `Promise` that resolves with the old source once the new image has been decoded; `await` it if you need to run code after the swap completes.
+The new image must match the dimensions this instance was initialized with: the canvases, zoom math, and loaded annotations are all in the init-time image's coordinate space. On a mismatch the old image is restored and the returned `Promise` rejects. Rebuild the ULabel instance to change image dimensions.
+
### `swap_anno_bg_color(new_bg_color)`
*(string) => string* -- Changes the background color for the annotation box. Returns the old color.
@@ -667,9 +708,27 @@ Display utilities are provided for a constructed `ULabel` object.
*(string) => array* -- Gets the current list of annotations within the provided subtask.
-### `set_annotations(new_annotations, subtask)`
+### `set_annotations(new_annotations, subtask, skip_toolbox_update=false, show_loader=true)`
+
+*(array, string, bool, bool) => Promise<void>* -- Sets the annotations for the provided subtask. Displays the loading spinner while re-initializing the annotations (similar to a new init); pass `show_loader = false` to swap silently, e.g. when the target subtask isn't the one on screen. Returns a `Promise` that resolves once the annotations have been set and redrawn; `await` it if you need to run code after the update completes.
+
+When batching several per-subtask swaps, prefer [`set_annotations_batch()`](#set_annotations_batchannotations_by_subtask-show_loadertrue); alternatively pass `skip_toolbox_update = true` on each call to suppress the per-call distance-filter and toolbox updates, then call [`refresh_toolbox()`](#refresh_toolbox) once at the end.
+
+### `set_annotations_batch(annotations_by_subtask, show_loader=true)`
+
+*(object, bool) => Promise<void>* -- Replaces several subtasks' annotations as a single update: one loader cycle and one toolbox refresh for the whole set (per-subtask calls would flash the loader once per layer). `annotations_by_subtask` maps subtask keys to annotation arrays in `resume_from` form; unknown keys are warned and skipped. Pass `show_loader = false` to swap silently, e.g. when every changed subtask is a background layer.
+
+### `refresh_toolbox()`
+
+*() => void* -- Runs the deferred half of a batched [`set_annotations()`](#set_annotationsnew_annotations-subtask-skip_toolbox_updatefalse) sequence: recomputes distance filtering and redraws the toolbox items once.
+
+### `set_class_color(class_id, color, redraw=true)`
+
+*(number | string, string, bool) => void* -- Sets a class's color and syncs every view of it: `color_info`, the id-toolbox swatch, and the id-dialog color pies. When `redraw` is `true`, annotations are redrawn immediately; pass `false` when batching several color changes, then call `redraw_all_annotations()` once at the end.
+
+### `set_class_counter_options(options, redraw=true)`
-*(array, string) => Promise<void>* -- Sets the annotations for the provided subtask. Displays the loading spinner while re-initializing the annotations (similar to a new init). Returns a `Promise` that resolves once the annotations have been set and redrawn; `await` it if you need to run code after the update completes.
+*(ClassCounterConfig, bool) => bool* -- Updates the [`ClassCounter`](#class_counter_toolbox_item) toolbox item's options at runtime; omitted options keep their current values. When `redraw` is `true` the counter re-renders immediately. Returns whether the `ClassCounter` toolbox item was found.
### `set_saved(saved)`
diff --git a/demo.js b/demo.js
index 708b5a5a..6c497ab1 100644
--- a/demo.js
+++ b/demo.js
@@ -16,5 +16,6 @@ console.log(`http://localhost:${port}/read-only.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}/class-focus.html`);
console.log(`http://localhost:${port}/live_demo.html`);
console.log(`http://localhost:${port}/offset-container.html`);
\ No newline at end of file
diff --git a/demo/bitmask-example.html b/demo/bitmask-example.html
index babd2792..76eb2a4a 100644
--- a/demo/bitmask-example.html
+++ b/demo/bitmask-example.html
@@ -48,6 +48,23 @@
"resume_from": null,
"task_meta": null,
"annotation_meta": null
+ },
+ // Second mask layer for trying brush_overlap_across_subtasks:
+ // paint here, switch back, and brush over it with exclude/overwrite.
+ "reference": {
+ "display_name": "Reference",
+ "classes": [
+ {
+ "name": "Reference",
+ "color": "#888888",
+ "id": 20
+ }
+ ],
+ "allowed_modes": ["bitmask"],
+ "resume_from": null,
+ "task_meta": null,
+ "annotation_meta": null,
+ "inactive_opacity": 0.6
}
};
@@ -61,7 +78,13 @@
});
// Wait for ULabel instance to finish initialization
ulabel.init(function() {
- // ULabel is now ready for use
+ // The gate reads the config live, so these apply to the next stroke
+ document.getElementById("across-subtasks").addEventListener("change", function() {
+ ulabel.config.brush_overlap_across_subtasks = this.checked;
+ });
+ document.getElementById("reference-read-only").addEventListener("change", function() {
+ ulabel.subtasks.reference.read_only = this.checked;
+ });
});
});
@@ -69,5 +92,10 @@
+