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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented here.

## [unreleased]

## [0.27.0] - Aug 18th, 2026
- Hovering a spatial annotation now draws a white outline that hugs its shape.
- Polish and update the confidence card to include the class name and fix its positioning.
- Fix shift-hover to properly start a new polygon complex layer.
- Fix bitmask moves to the image edge that were permanently truncating the mask. Now a translated mask bounces back to its previous position if it would be moved outside the image bounds.
- Improve read-only subtask handling. Subtasks marked `read_only: true` now actually prevent edits. Additionally, all subtasks may now be read-only (the previous "at least one non-read-only subtask" error has been removed).

## [0.26.3] - Aug 13th, 2026
- **Fix wheel-zoom and drag-zoom focal point when the ULabel container is offset from the viewport origin.** `handle_wheel` and `drag_rezoom` used to pass raw `clientX`/`clientY` (viewport coords) straight into `rezoom`, which treats its focal-point arguments as annbox-local. When the container sat at viewport `(0, 0)` the two frames coincided and the bug was invisible; anywhere else (e.g. hosted inside a centered dialog, a padded panel, or below a header) zoom would snap by roughly the annbox's screen offset. Both call sites now convert to annbox-local via `getBoundingClientRect()` before calling `rezoom`, so zoom stays anchored to the cursor / mousedown point regardless of how the host embeds ULabel.
- **New config option `min_zoom_fit_ratio`.** Sets a zoom-out floor as a multiplier of the "whole image just fits the viewport" zoom. `0` (default) preserves the existing behavior (no floor). `1.0` prevents users from zooming out past the fit-to-viewport level. `>1` forces the image to always overflow. Zoom-in is unaffected. The floor recomputes from live annbox dimensions, so it adapts to browser resize.
Expand Down
1 change: 1 addition & 0 deletions demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ console.log(`http://localhost:${port}/multi-class.html`);
console.log(`http://localhost:${port}/frames.html`);
console.log(`http://localhost:${port}/box-roi.html`);
console.log(`http://localhost:${port}/resume-from.html`);
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`);
Expand Down
163 changes: 163 additions & 0 deletions demo/read-only.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<!DOCTYPE html>
<html>
<head>
<title>ULabel - Read-Only Demo</title>

<!-- ULabel Library -->
<script src="/ulabel.js"></script>

<!-- JQuery Library -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>

<!-- ULabel Usage -->
<script>
/* global $ */
/* global ULabel */

$(window).on("load", function() {

function on_submit(annotations) {
var element = document.createElement("a");
element.setAttribute(
"href", ("data:text/plain;charset=utf-8," +
encodeURIComponent(JSON.stringify(annotations, null, 2))),
);
element.setAttribute("download", "annotations.json");
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}

// A mix of spatial and nonspatial annotations to exercise every read-only surface:
// - spatial hover shows the confidence card + hover border but no move/reid/delete buttons and no vertex handle
// - nonspatial rows render without reclassify / delete buttons and with a readonly note field
let car_annotations = [
{
"id": "ro-point-1",
"spatial_type": "point",
"spatial_payload": [[300, 300]],
"classification_payloads": [
{ "class_id": 12, "confidence": 0.0 },
{ "class_id": 11, "confidence": 0.9 },
{ "class_id": 10, "confidence": 0.0 },
],
},
{
"id": "ro-bbox-1",
"spatial_type": "bbox",
"spatial_payload": [[500, 400], [800, 650]],
"classification_payloads": [
{ "class_id": 10, "confidence": 0.82 },
],
},
{
"id": "ro-polygon-1",
"spatial_type": "polygon",
"spatial_payload": [
[
[1073.79, 444.87],
[1228.58, 254.65],
[1390.43, 314.56],
[1358.64, 593.99],
[1200.00, 620.00],
[1073.79, 444.87],
],
],
"classification_payloads": [
{ "class_id": 12, "confidence": 0.65 },
],
},
];

let frame_review_annotations = [
{
"id": "ro-whole-image-1",
"spatial_type": "whole-image",
"spatial_payload": null,
"text_payload": "Read-only note: this text area should be uneditable and the row should have no delete or reclassify buttons.",
"classification_payloads": [
{ "class_id": 20, "confidence": 1.0 },
],
},
];

// Both subtasks are read-only, so no user-driven mutations should be possible.
let subtasks = {
"car_detection": {
"display_name": "Car Detection",
"classes": [
{ "name": "Sedan", "color": "blue", "id": 10, "keybind": "1" },
{ "name": "SUV", "color": "green", "id": 11, "keybind": "2" },
{ "name": "Truck", "color": "orange", "id": 12, "keybind": "3" },
],
"allowed_modes": ["bbox", "polygon", "point", "polyline"],
"resume_from": car_annotations,
"read_only": true,
"task_meta": null,
"annotation_meta": null,
},
"frame_review": {
"display_name": "Frame Review",
"classes": [
{ "name": "Blurry", "color": "gray", "id": 20 },
{ "name": "Occluded", "color": "red", "id": 21 },
],
"allowed_modes": ["whole-image"],
"resume_from": frame_review_annotations,
"read_only": true,
"task_meta": null,
"annotation_meta": null,
},
};

const AllowedToolboxItem = ULabel.get_allowed_toolbox_item_enum();
let ulabel = new ULabel({
"container_id": "container",
"image_data": "https://ulabel.s3.us-east-2.amazonaws.com/cs-demo-0.png",
"username": "DemoUser",
"submit_buttons": on_submit,
"subtasks": subtasks,
"toolbox_order": [
AllowedToolboxItem.ModeSelect,
AllowedToolboxItem.ZoomPan,
AllowedToolboxItem.AnnotationID,
AllowedToolboxItem.ClassCounter,
AllowedToolboxItem.AnnotationResize,
AllowedToolboxItem.ConfidenceSlider,
AllowedToolboxItem.SubmitButtons,
],
});
ulabel.init(function() {
// ULabel is now ready for use
});
// Expose the instance for e2e tests
window.ulabel = ulabel;
});
</script>
<style>
body { margin: 0; padding: 0; }
.test-banner {
background-color: #2b4b7a;
color: white;
padding: 6px 12px;
font-family: sans-serif;
}
.test-banner h2 { margin: 0; font-size: 1.1em; }
.test-banner p { margin: 4px 0 0 0; font-size: 0.85em; opacity: 0.85; }
</style>
</head>
<body>
<div class="test-banner">
<h2>Read-Only Demo</h2>
<p>
All subtasks are <code>read_only: true</code>. Hover shows the confidence card and hover outline, but
move/reclassify/delete buttons, vertex handles, and the id color-wheel are suppressed. Creation drags,
brush/erase mode, delete keybinds, and class-reassign keybinds are all blocked.
</p>
</div>
<div id="wrapper">
<div id="container" style="width: 100%; height: calc(100vh - 90px);"></div>
</div>
</body>
</html>
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ export class ULabel {
// Subtasks
public get_current_subtask_key(): string;
public get_current_subtask(): ULabelSubtask;
public is_current_subtask_read_only(): boolean;
public readjust_subtask_opacities(): void;
public set_subtask(st_key: string): void;
public switch_to_next_subtask(): void;
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.26.3",
"version": "0.27.0",
"main": "dist/ulabel.min.js",
"module": "dist/ulabel.min.js",
"types": "dist/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export function record_finish_move(
undo_payload.diffZ = -diffZ;
redo_payload.finished = true;
redo_payload.move_not_allowed = move_not_allowed;
undo_payload.move_not_allowed = move_not_allowed;
action.redo_payload = JSON.stringify(redo_payload);
action.undo_payload = JSON.stringify(undo_payload);

Expand Down
1 change: 1 addition & 0 deletions src/blobs.js
Original file line number Diff line number Diff line change
Expand Up @@ -1862,6 +1862,7 @@ div#${prntid} div.dialogs_container {
position: absolute;
top: 0;
left: 0;
z-index: ${BACK_Z_INDEX + 1};
}

div.toolbox_inner_cls {
Expand Down
23 changes: 13 additions & 10 deletions src/html_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,24 +542,27 @@ export function build_confidence_dialog(ulabel: ULabel) {
}
global_edit_suggestion_jq.append(`
<div id="${global_id}" class="annotation-confidence gedit-target${mcm_ind}">
<p class="annotation-confidence-title" style="margin: 0.25em; margin-top: 1em; padding-top: 0.3em; opacity: 1;">Annotation Confidence:</p>
<p class="annotation-confidence-value" style="margin: 0.25em; opacity: 1;">
N/A
<p class="annotation-confidence-classname" style="margin: 0.4em 0.6em 0.1em; opacity: 1; font-weight: 600;"></p>
<p class="annotation-confidence-value" style="margin: 0.1em 0.6em 0.4em; opacity: 0.85; font-size: 0.9em;">
</p>
</div>
`);

// Style the dialog
$("#" + global_id).css({
"background-color": "black",
"background-color": "rgba(0, 0, 0, 0.75)",
"color": "white",
"opacity": "0.6",
"height": "3em",
"width": "14.5em",
"height": "auto",
"width": "auto",
"min-width": "8em",
"padding": "0.2em 0.4em",
"margin-top": "-9.5em",
"border-radius": "1em",
"font-size": "1.2em",
"margin-left": "-1.4em",
"border-radius": "0.5em",
"font-size": "1.1em",
"position": "relative",
"left": "50%",
"transform": "translateX(-50%)",
"pointer-events": "none",
});
}
}
Expand Down
Loading
Loading