diff --git a/.github/tasks.md b/.github/tasks.md index 8f2b1caf..c54445be 100644 --- a/.github/tasks.md +++ b/.github/tasks.md @@ -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 - +- diff --git a/CHANGELOG.md b/CHANGELOG.md index 986c7cb3..ad7b66fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/api_spec.md b/api_spec.md index 9b243d61..dcd90f80 100644 --- a/api_spec.md +++ b/api_spec.md @@ -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. diff --git a/index.d.ts b/index.d.ts index 3f361bbb..b359ca2f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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; diff --git a/package-lock.json b/package-lock.json index ae657645..0c6e12f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ulabel", - "version": "0.23.2", + "version": "0.23.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ulabel", - "version": "0.23.2", + "version": "0.23.3", "license": "MIT", "devDependencies": { "@eslint/config-inspector": "^1.3.0", diff --git a/package.json b/package.json index bbda6171..03697073 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/annotation_operators.ts b/src/annotation_operators.ts index 3ffdc92a..f272400c 100644 --- a/src/annotation_operators.ts +++ b/src/annotation_operators.ts @@ -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"; /** @@ -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); }); } diff --git a/src/index.js b/src/index.js index a00994bc..fb1f7d36 100644 --- a/src/index.js +++ b/src/index.js @@ -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) { diff --git a/src/toolbox.ts b/src/toolbox.ts index 5e2d9b50..7551188c 100644 --- a/src/toolbox.ts +++ b/src/toolbox.ts @@ -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(`#${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 } @@ -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(".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(".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"; } diff --git a/src/version.js b/src/version.js index 4c7a4f51..ce72e17b 100644 --- a/src/version.js +++ b/src/version.js @@ -1 +1 @@ -export const ULABEL_VERSION = "0.23.2"; +export const ULABEL_VERSION = "0.23.3"; diff --git a/tests/e2e/slider-api.spec.js b/tests/e2e/slider-api.spec.js new file mode 100644 index 00000000..66a5b361 --- /dev/null +++ b/tests/e2e/slider-api.spec.js @@ -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"); + }); + }); +});