Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
576 changes: 8 additions & 568 deletions .github/tasks.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to this project will be documented here.

## [0.29.1] - Oct 12th, 2026
#### Fixed
- `set_annotations()` / `set_annotations_batch()` could paint a frame with no annotations on heavy images. The outgoing annotations now stay on screen until the new ones are drawn.

#### Changed
- Faster `AnnotationList` rebuilds: each row's icon is a cached `<img>` instead of an inline SVG, rows off screen skip layout and paint (`content-visibility: auto`), and the grouped view no longer does a linear search per row.

## [0.29.0] - Oct 12th, 2026
#### Breaking Changes
- **Drag a body to move it.** A left-drag that starts inside a spatial annotation moves it (now including `contour`, `tbar`, and `bbox3`). A drag on a vertex handle edits the vertex, and a drag anywhere else draws. Hold `Alt` to draw over an annotation. Set `allow_body_move: false` for the old draw-everywhere behavior.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ulabel",
"description": "An image annotation tool.",
"version": "0.29.0",
"version": "0.29.1",
"main": "dist/ulabel.min.js",
"module": "dist/ulabel.min.js",
"types": "dist/index.d.ts",
Expand Down
13 changes: 6 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9322,10 +9322,7 @@ export class ULabel {
// The isolated id is about to stop existing; the redraw below repaints
this.isolate_annotation(null, subtask, false);

// Bulk teardown of outgoing annotations: much cheaper than a per-annotation loop.
this._clear_subtask_annotation_canvases(subtask);

// Set new annotations and initialize canvases
const outgoing_annotations = this.subtasks[subtask]["annotations"];
ULabel.process_resume_from(this, subtask, { resume_from: new_annotations });

// Yield the event loop so the loader's reveal timer can fire if the load above
Expand All @@ -9335,6 +9332,8 @@ export class ULabel {
await new Promise((resolve) => setTimeout(resolve, 0));
if (this.is_destroyed) return false;

// The outgoing canvases stay up through the yield, so no paint can show an empty frame.
this._clear_subtask_annotation_canvases(subtask, outgoing_annotations);
initialize_annotation_canvases(this, subtask);
// Redraw all annotations to render them
this.redraw_all_annotations(subtask);
Expand Down Expand Up @@ -9363,9 +9362,9 @@ export class ULabel {
* redraw the remaining siblings on every removal — wasted work when the caller
* is about to replace everything and redraw once.
*/
_clear_subtask_annotation_canvases(subtask) {
const access = this.subtasks[subtask]["annotations"]["access"];
for (const id of this.subtasks[subtask]["annotations"]["ordering"]) {
_clear_subtask_annotation_canvases(subtask, annotations = this.subtasks[subtask]["annotations"]) {
const access = annotations["access"];
for (const id of annotations["ordering"]) {
const anno = access[id];
if (anno?.spatial_type === "bitmask") {
delete anno["_mask"];
Expand Down
31 changes: 22 additions & 9 deletions src/toolbox_items/annotation_list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import {
// Eye icon for the per-entry isolate button; `currentColor` follows the theme
const ISOLATE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 8s2.5-4.5 6.5-4.5S14.5 8 14.5 8s-2.5 4.5-6.5 4.5S1.5 8 1.5 8z"/><circle cx="8" cy="8" r="2"/></svg>`;

// Keyed by `${spatial_type}|${color}`
const ICON_URL_CACHE: Map<string, string> = new Map();

/**
* Toolbox item for displaying and navigating annotations in a list
*/
Expand Down Expand Up @@ -174,6 +177,8 @@ export class AnnotationListToolboxItem extends ToolboxItem {
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
cursor: pointer;
transition: background-color 150ms;
content-visibility: auto;
contain-intrinsic-size: auto 37px;
}

#toolbox .annotation-list-item:last-child {
Expand Down Expand Up @@ -203,7 +208,7 @@ export class AnnotationListToolboxItem extends ToolboxItem {
justify-content: center;
}

#toolbox .annotation-list-item-icon svg {
#toolbox .annotation-list-item-icon img {
width: 100%;
height: 100%;
}
Expand Down Expand Up @@ -497,12 +502,12 @@ export class AnnotationListToolboxItem extends ToolboxItem {
const class_def = class_def_by_id.get(class_id);
const class_name = class_def ? class_def.name : "Unknown";
const color = this.ulabel.color_info[class_id] || "#cccccc";
const svg = this.get_spatial_type_svg(annotation.spatial_type!, color);
const icon_url = this.get_spatial_type_icon_url(annotation.spatial_type!, color);

html += `
<div class="annotation-list-item" data-annotation-id="${annotation.id}" data-annotation-idx="${i}">
<div class="annotation-list-item-header">
<div class="annotation-list-item-icon">${svg}</div>
<div class="annotation-list-item-icon"><img src="${icon_url}" alt=""></div>
<div class="annotation-list-item-text">
<span class="annotation-list-item-class">${class_name}</span>
<span class="annotation-list-item-id">#${i}</span>
Expand All @@ -522,6 +527,7 @@ export class AnnotationListToolboxItem extends ToolboxItem {
*/
private build_grouped_list_html(annotations: ULabelAnnotation[], subtask: ULabelSubtask): string {
const class_def_by_id = this.build_class_def_by_id(subtask);
const overall_idx_by_annotation = new Map(annotations.map((annotation, idx) => [annotation, idx]));

// Group annotations by class
const groups: { [class_id: number]: ULabelAnnotation[] } = {};
Expand Down Expand Up @@ -574,13 +580,13 @@ export class AnnotationListToolboxItem extends ToolboxItem {

for (let i = 0; i < group_annotations.length; i++) {
const annotation = group_annotations[i];
const overall_idx = annotations.indexOf(annotation);
const svg = this.get_spatial_type_svg(annotation.spatial_type!, color);
const overall_idx = overall_idx_by_annotation.get(annotation);
const icon_url = this.get_spatial_type_icon_url(annotation.spatial_type!, color);

html += `
<div class="annotation-list-item" data-annotation-id="${annotation.id}" data-annotation-idx="${overall_idx}">
<div class="annotation-list-item-header">
<div class="annotation-list-item-icon" style="margin-left: 0.5rem;">${svg}</div>
<div class="annotation-list-item-icon" style="margin-left: 0.5rem;"><img src="${icon_url}" alt=""></div>
<div class="annotation-list-item-text">
<span class="annotation-list-item-id">#${i}</span>
</div>
Expand Down Expand Up @@ -612,9 +618,14 @@ export class AnnotationListToolboxItem extends ToolboxItem {
}

/**
* Get the SVG icon for a given spatial type
* Get a data URL for the colored icon of a given spatial type. Rendering it as an
* <img> keeps each row to one DOM node instead of a full inline Inkscape SVG.
*/
private get_spatial_type_svg(spatial_type: string, color: string): string {
private get_spatial_type_icon_url(spatial_type: string, color: string): string {
const cache_key = `${spatial_type}|${color}`;
const cached = ICON_URL_CACHE.get(cache_key);
if (cached !== undefined) return cached;

let svg = "";

switch (spatial_type) {
Expand Down Expand Up @@ -659,7 +670,9 @@ export class AnnotationListToolboxItem extends ToolboxItem {
svg = svg.replace(/stroke:#[0-9a-fA-F]{6}/g, `stroke:${color}`);
svg = svg.replace(/fill:#[0-9a-fA-F]{6}/g, `fill:${color}`);

return svg;
const url = `data:image/svg+xml,${encodeURIComponent(svg)}`;
ICON_URL_CACHE.set(cache_key, url);
return url;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/version.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const ULABEL_VERSION = "0.29.0";
export const ULABEL_VERSION = "0.29.1";
Loading