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
10 changes: 1 addition & 9 deletions .github/tasks.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,2 @@
## Tasks
- [x] Read the discussion in issue [#159](https://github.com/SenteraLLC/ulabel/issues/159)
- [x] Implement a vertex deletion keybind for polygon and polyline spatial types it should:
- [x] Delete the vertex when pressed when hovering over it such that the edit suggestion is showing
- [x] Delete the vertex when pressed when dragging/editing the vertex
- [x] For polylines, if only one point remains in the polyline, it should delete the polyline
- [x] For polygons, if fewer than 3 points remain in a polygon layer, the layer should be removed
- [x] Add a test for the keybind in keybind-functionality.spec.js
- [x] Update the api_spec and changelog

-
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented here.

## [unreleased]

## [0.23.3] - Mar 18th, 2026
- Add `get_keypoint_slider_value()` public API method to get the current keypoint slider value (0-1).
- Add `get_distance_filter_value()` public API method to get the current distance filter slider values.

## [0.23.2] - Mar 18th, 2026
- Fix bug where multiple spaces in a submit button name would cause the button hook to not fire.

Expand Down
8 changes: 8 additions & 0 deletions api_spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,14 @@ Sets the zoom to focus on the provided annotation id, and switches to its subtas
### `fly_to_annotation(annotation, subtask_key, max_zoom)`
Sets the zoom to focus on the provided annotation, and switches to its subtask if provided. Returns `true` on success and `false` on failure (eg, annotation doesn't exist in subtask, is not a spatial annotation, or is deprecated).

### `get_keypoint_slider_value()`

*() => number | null* -- Returns the current keypoint slider value as a number between 0 and 1. Returns `null` if the KeypointSlider toolbox item is not active or the slider element is not found.

### `get_distance_filter_value()`

*() => object | null* -- Returns an object mapping class identifiers to their distance filter values (in pixels). The object always includes a `closest_row` key for the single-class slider. In multi-class mode, additional keys correspond to each class ID. Returns `null` if the FilterDistance toolbox item is not active or no sliders are found.

## Generic Callbacks

Callbacks can be provided by calling `.on(fn, callback)` on a `ULabel` object.
Expand Down
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,8 @@ export class ULabel {
force_filter_all?: boolean,
offset?: Offset,
): void;
public get_keypoint_slider_value(): number | null;
public get_distance_filter_value(): DistanceFromPolylineClasses | null;
public fly_to_next_annotation(increment: number, max_zoom?: number): boolean;
public fly_to_annotation_id(annotation_id: string, subtask_key?: string, max_zoom?: number): boolean;
public fly_to_annotation(annotation: ULabelAnnotation, subtask_key?: string, max_zoom?: number): boolean;
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.23.2",
"version": "0.23.3",
"main": "dist/ulabel.min.js",
"module": "dist/ulabel.min.js",
"types": "index.d.ts",
Expand Down
4 changes: 3 additions & 1 deletion src/annotation_operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
// Import ULabel from ../src/index - TypeScript will find ../src/index.d.ts for types
import { ULabel } from "../src/index";

import { ULabelAnnotation } from "./annotation";
import { ULabelAnnotation, DELETE_CLASS_ID } from "./annotation";
import { ULabelSubtask } from "./subtask";

/**
Expand Down Expand Up @@ -582,6 +582,8 @@ export function findAllPolylineClassDefinitions(ulabel: ULabel) {
if (subtask.allowed_modes.includes("polyline")) {
// Loop through all the classes in the subtask
subtask.class_defs.forEach((current_class_def) => {
// Skip the reserved delete class
if (current_class_def.id === DELETE_CLASS_ID) return;
potential_class_defs.push(current_class_def);
});
}
Expand Down
26 changes: 26 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,32 @@ export class ULabel {
}
}

/**
* Get the current keypoint slider value.
*
* @returns {number|null} The current slider value as a number between 0 and 1,
* or null if the KeypointSlider toolbox item is not active or the slider is not found
*/
get_keypoint_slider_value() {
if (!this.config.toolbox_order.includes(AllowedToolboxItem.KeypointSlider)) return null;
const item = this.toolbox.items.find((item) => item.get_toolbox_item_type() === "KeypointSlider");
if (item === undefined) return null;
return item.get_current_value();
}

/**
* Get the current distance filter slider values.
*
* @returns {object|null} An object mapping class identifiers to their distance values,
* or null if the FilterDistance toolbox item is not active or no sliders are found
*/
get_distance_filter_value() {
if (!this.config.toolbox_order.includes(AllowedToolboxItem.FilterDistance)) return null;
const item = this.toolbox.items.find((item) => item.get_toolbox_item_type() === "FilterDistance");
if (item === undefined) return null;
return item.get_current_values();
}

// Show annotation mode
show_annotation_mode(el = null) {
if (el === null) {
Expand Down
45 changes: 45 additions & 0 deletions src/toolbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1947,6 +1947,17 @@ export class KeypointSliderItem extends ToolboxItem {
`;
}

/**
* Get the current keypoint slider value by reading the DOM slider element.
*
* @returns The current slider value as a number between 0 and 1, or null if the slider is not found
*/
public get_current_value(): number | null {
const slider = document.querySelector<HTMLInputElement>(`#${this.slider_bar_id}`);
if (slider === null) return null;
return slider.valueAsNumber / 100;
}

public after_init() {
// This toolbox item doesn't need to do anything after initialization
}
Expand Down Expand Up @@ -2355,6 +2366,40 @@ export class FilterPointDistanceFromRow extends ToolboxItem {
// This toolbox item doesn't need to do anything after initialization
}

/**
* Get the current distance filter slider values by reading the DOM slider elements.
*
* @returns An object mapping class identifiers to their distance values, or null if no sliders are found
*/
public get_current_values(): DistanceFromPolylineClasses | null {
// Always read the single-class slider for closest_row
const single_container = document.getElementById("filter-single-class-mode");
if (single_container === null) return null;

const single_slider = single_container.querySelector<HTMLInputElement>(".filter-row-distance-slider");
if (single_slider === null) return null;

const filter_values: DistanceFromPolylineClasses = {
closest_row: { distance: single_slider.valueAsNumber },
};

// In multi-class mode, also read the per-class sliders
if (this.multi_class_mode) {
const multi_container = document.getElementById("filter-multi-class-mode");
if (multi_container !== null) {
const sliders = multi_container.querySelectorAll<HTMLInputElement>(".filter-row-distance-slider");
for (let idx = 0; idx < sliders.length; idx++) {
const slider_class_name = /[^-]*$/.exec(sliders[idx].id)[0];
filter_values[slider_class_name] = {
distance: sliders[idx].valueAsNumber,
};
}
}
}

return filter_values;
}

public get_toolbox_item_type() {
return "FilterDistance";
}
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.23.2";
export const ULABEL_VERSION = "0.23.3";
80 changes: 80 additions & 0 deletions tests/e2e/slider-api.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// End-to-end tests for slider public API methods
import { test, expect } from "./fixtures";
import { wait_for_ulabel_init } from "../testing-utils/init_utils";

test.describe("Slider Public API", () => {
test.describe("get_keypoint_slider_value", () => {
test("should return default keypoint slider value after init", async ({ page }) => {
await wait_for_ulabel_init(page);

const value = await page.evaluate(() => window.ulabel.get_keypoint_slider_value());

// Default keypoint_slider_default_value is 0, so the slider should be at 0
expect(value).toBe(0);
});

test("should reflect value after moving the slider", async ({ page }) => {
await wait_for_ulabel_init(page);

// Set the slider to 75 (out of 100) via the DOM
await page.evaluate(() => {
const slider = document.querySelector("#keypoint-slider");
slider.value = "75";
slider.dispatchEvent(new Event("input", { bubbles: true }));
});

const value = await page.evaluate(() => window.ulabel.get_keypoint_slider_value());

// Value should be 0.75 (75 / 100)
expect(value).toBe(0.75);
});

test("should return a number between 0 and 1", async ({ page }) => {
await wait_for_ulabel_init(page);

const value = await page.evaluate(() => window.ulabel.get_keypoint_slider_value());

expect(typeof value).toBe("number");
expect(value).toBeGreaterThanOrEqual(0);
expect(value).toBeLessThanOrEqual(1);
});
});

test.describe("get_distance_filter_value", () => {
test("should return default distance filter value after init", async ({ page }) => {
await wait_for_ulabel_init(page);

const value = await page.evaluate(() => window.ulabel.get_distance_filter_value());

// Should have at least the closest_row key with the default distance of 40
expect(value).not.toBeNull();
expect(value.closest_row).toBeDefined();
expect(value.closest_row.distance).toBe(40);
});

test("should reflect value after moving the slider", async ({ page }) => {
await wait_for_ulabel_init(page);

// Set the distance filter slider to 200
await page.evaluate(() => {
const slider = document.querySelector("#filter-row-distance-closest_row");
slider.value = "200";
slider.dispatchEvent(new Event("input", { bubbles: true }));
});

const value = await page.evaluate(() => window.ulabel.get_distance_filter_value());

expect(value.closest_row.distance).toBe(200);
});

test("should return an object with closest_row key", async ({ page }) => {
await wait_for_ulabel_init(page);

const value = await page.evaluate(() => window.ulabel.get_distance_filter_value());

expect(typeof value).toBe("object");
expect(value).toHaveProperty("closest_row");
expect(typeof value.closest_row.distance).toBe("number");
});
});
});
Loading