Skip to content

Commit 17c9918

Browse files
Merge pull request #255 from SenteraLLC/feature/ui-polish
Feature/UI polish
2 parents d73befb + 8bb9761 commit 17c9918

17 files changed

Lines changed: 1066 additions & 44 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented here.
44

55
## [unreleased]
66

7+
## [0.27.0] - Aug 18th, 2026
8+
- Hovering a spatial annotation now draws a white outline that hugs its shape.
9+
- Polish and update the confidence card to include the class name and fix its positioning.
10+
- Fix shift-hover to properly start a new polygon complex layer.
11+
- 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.
12+
- 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).
13+
714
## [0.26.3] - Aug 13th, 2026
815
- **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.
916
- **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.

demo.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ console.log(`http://localhost:${port}/multi-class.html`);
1212
console.log(`http://localhost:${port}/frames.html`);
1313
console.log(`http://localhost:${port}/box-roi.html`);
1414
console.log(`http://localhost:${port}/resume-from.html`);
15+
console.log(`http://localhost:${port}/read-only.html`);
1516
console.log(`http://localhost:${port}/row-filtering-example.html`);
1617
console.log(`http://localhost:${port}/bitmask-example.html`);
1718
console.log(`http://localhost:${port}/set-annotations.html`);

demo/read-only.html

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>ULabel - Read-Only Demo</title>
5+
6+
<!-- ULabel Library -->
7+
<script src="/ulabel.js"></script>
8+
9+
<!-- JQuery Library -->
10+
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
11+
12+
<!-- ULabel Usage -->
13+
<script>
14+
/* global $ */
15+
/* global ULabel */
16+
17+
$(window).on("load", function() {
18+
19+
function on_submit(annotations) {
20+
var element = document.createElement("a");
21+
element.setAttribute(
22+
"href", ("data:text/plain;charset=utf-8," +
23+
encodeURIComponent(JSON.stringify(annotations, null, 2))),
24+
);
25+
element.setAttribute("download", "annotations.json");
26+
element.style.display = "none";
27+
document.body.appendChild(element);
28+
element.click();
29+
document.body.removeChild(element);
30+
}
31+
32+
// A mix of spatial and nonspatial annotations to exercise every read-only surface:
33+
// - spatial hover shows the confidence card + hover border but no move/reid/delete buttons and no vertex handle
34+
// - nonspatial rows render without reclassify / delete buttons and with a readonly note field
35+
let car_annotations = [
36+
{
37+
"id": "ro-point-1",
38+
"spatial_type": "point",
39+
"spatial_payload": [[300, 300]],
40+
"classification_payloads": [
41+
{ "class_id": 12, "confidence": 0.0 },
42+
{ "class_id": 11, "confidence": 0.9 },
43+
{ "class_id": 10, "confidence": 0.0 },
44+
],
45+
},
46+
{
47+
"id": "ro-bbox-1",
48+
"spatial_type": "bbox",
49+
"spatial_payload": [[500, 400], [800, 650]],
50+
"classification_payloads": [
51+
{ "class_id": 10, "confidence": 0.82 },
52+
],
53+
},
54+
{
55+
"id": "ro-polygon-1",
56+
"spatial_type": "polygon",
57+
"spatial_payload": [
58+
[
59+
[1073.79, 444.87],
60+
[1228.58, 254.65],
61+
[1390.43, 314.56],
62+
[1358.64, 593.99],
63+
[1200.00, 620.00],
64+
[1073.79, 444.87],
65+
],
66+
],
67+
"classification_payloads": [
68+
{ "class_id": 12, "confidence": 0.65 },
69+
],
70+
},
71+
];
72+
73+
let frame_review_annotations = [
74+
{
75+
"id": "ro-whole-image-1",
76+
"spatial_type": "whole-image",
77+
"spatial_payload": null,
78+
"text_payload": "Read-only note: this text area should be uneditable and the row should have no delete or reclassify buttons.",
79+
"classification_payloads": [
80+
{ "class_id": 20, "confidence": 1.0 },
81+
],
82+
},
83+
];
84+
85+
// Both subtasks are read-only, so no user-driven mutations should be possible.
86+
let subtasks = {
87+
"car_detection": {
88+
"display_name": "Car Detection",
89+
"classes": [
90+
{ "name": "Sedan", "color": "blue", "id": 10, "keybind": "1" },
91+
{ "name": "SUV", "color": "green", "id": 11, "keybind": "2" },
92+
{ "name": "Truck", "color": "orange", "id": 12, "keybind": "3" },
93+
],
94+
"allowed_modes": ["bbox", "polygon", "point", "polyline"],
95+
"resume_from": car_annotations,
96+
"read_only": true,
97+
"task_meta": null,
98+
"annotation_meta": null,
99+
},
100+
"frame_review": {
101+
"display_name": "Frame Review",
102+
"classes": [
103+
{ "name": "Blurry", "color": "gray", "id": 20 },
104+
{ "name": "Occluded", "color": "red", "id": 21 },
105+
],
106+
"allowed_modes": ["whole-image"],
107+
"resume_from": frame_review_annotations,
108+
"read_only": true,
109+
"task_meta": null,
110+
"annotation_meta": null,
111+
},
112+
};
113+
114+
const AllowedToolboxItem = ULabel.get_allowed_toolbox_item_enum();
115+
let ulabel = new ULabel({
116+
"container_id": "container",
117+
"image_data": "https://ulabel.s3.us-east-2.amazonaws.com/cs-demo-0.png",
118+
"username": "DemoUser",
119+
"submit_buttons": on_submit,
120+
"subtasks": subtasks,
121+
"toolbox_order": [
122+
AllowedToolboxItem.ModeSelect,
123+
AllowedToolboxItem.ZoomPan,
124+
AllowedToolboxItem.AnnotationID,
125+
AllowedToolboxItem.ClassCounter,
126+
AllowedToolboxItem.AnnotationResize,
127+
AllowedToolboxItem.ConfidenceSlider,
128+
AllowedToolboxItem.SubmitButtons,
129+
],
130+
});
131+
ulabel.init(function() {
132+
// ULabel is now ready for use
133+
});
134+
// Expose the instance for e2e tests
135+
window.ulabel = ulabel;
136+
});
137+
</script>
138+
<style>
139+
body { margin: 0; padding: 0; }
140+
.test-banner {
141+
background-color: #2b4b7a;
142+
color: white;
143+
padding: 6px 12px;
144+
font-family: sans-serif;
145+
}
146+
.test-banner h2 { margin: 0; font-size: 1.1em; }
147+
.test-banner p { margin: 4px 0 0 0; font-size: 0.85em; opacity: 0.85; }
148+
</style>
149+
</head>
150+
<body>
151+
<div class="test-banner">
152+
<h2>Read-Only Demo</h2>
153+
<p>
154+
All subtasks are <code>read_only: true</code>. Hover shows the confidence card and hover outline, but
155+
move/reclassify/delete buttons, vertex handles, and the id color-wheel are suppressed. Creation drags,
156+
brush/erase mode, delete keybinds, and class-reassign keybinds are all blocked.
157+
</p>
158+
</div>
159+
<div id="wrapper">
160+
<div id="container" style="width: 100%; height: calc(100vh - 90px);"></div>
161+
</div>
162+
</body>
163+
</html>

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ export class ULabel {
378378
// Subtasks
379379
public get_current_subtask_key(): string;
380380
public get_current_subtask(): ULabelSubtask;
381+
public is_current_subtask_read_only(): boolean;
381382
public readjust_subtask_opacities(): void;
382383
public set_subtask(st_key: string): void;
383384
public switch_to_next_subtask(): void;

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ulabel",
33
"description": "An image annotation tool.",
4-
"version": "0.26.3",
4+
"version": "0.27.0",
55
"main": "dist/ulabel.min.js",
66
"module": "dist/ulabel.min.js",
77
"types": "dist/index.d.ts",

src/actions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ export function record_finish_move(
156156
undo_payload.diffZ = -diffZ;
157157
redo_payload.finished = true;
158158
redo_payload.move_not_allowed = move_not_allowed;
159+
undo_payload.move_not_allowed = move_not_allowed;
159160
action.redo_payload = JSON.stringify(redo_payload);
160161
action.undo_payload = JSON.stringify(undo_payload);
161162

src/blobs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1862,6 +1862,7 @@ div#${prntid} div.dialogs_container {
18621862
position: absolute;
18631863
top: 0;
18641864
left: 0;
1865+
z-index: ${BACK_Z_INDEX + 1};
18651866
}
18661867
18671868
div.toolbox_inner_cls {

src/html_builder.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -542,24 +542,27 @@ export function build_confidence_dialog(ulabel: ULabel) {
542542
}
543543
global_edit_suggestion_jq.append(`
544544
<div id="${global_id}" class="annotation-confidence gedit-target${mcm_ind}">
545-
<p class="annotation-confidence-title" style="margin: 0.25em; margin-top: 1em; padding-top: 0.3em; opacity: 1;">Annotation Confidence:</p>
546-
<p class="annotation-confidence-value" style="margin: 0.25em; opacity: 1;">
547-
N/A
545+
<p class="annotation-confidence-classname" style="margin: 0.4em 0.6em 0.1em; opacity: 1; font-weight: 600;"></p>
546+
<p class="annotation-confidence-value" style="margin: 0.1em 0.6em 0.4em; opacity: 0.85; font-size: 0.9em;">
548547
</p>
549548
</div>
550549
`);
551550

552551
// Style the dialog
553552
$("#" + global_id).css({
554-
"background-color": "black",
553+
"background-color": "rgba(0, 0, 0, 0.75)",
555554
"color": "white",
556-
"opacity": "0.6",
557-
"height": "3em",
558-
"width": "14.5em",
555+
"height": "auto",
556+
"width": "auto",
557+
"min-width": "8em",
558+
"padding": "0.2em 0.4em",
559559
"margin-top": "-9.5em",
560-
"border-radius": "1em",
561-
"font-size": "1.2em",
562-
"margin-left": "-1.4em",
560+
"border-radius": "0.5em",
561+
"font-size": "1.1em",
562+
"position": "relative",
563+
"left": "50%",
564+
"transform": "translateX(-50%)",
565+
"pointer-events": "none",
563566
});
564567
}
565568
}

0 commit comments

Comments
 (0)