const assert = require("assert");
|
|
/**
|
* Load custom example
|
* @param {object} params
|
* @param {string} params.config
|
* @param {object} params.data object with property used in config
|
* @param {object[]} params.annotations
|
* @param {object[]} params.predictions
|
*/
|
async function initLabelStudio({
|
config,
|
data,
|
annotations = [{ result: [] }],
|
predictions = [],
|
settings = {},
|
additionalInterfaces = [],
|
params = {},
|
taskId = undefined,
|
}) {
|
if (window.Konva && window.Konva.stages.length) window.Konva.stages.forEach((stage) => stage.destroy());
|
|
const interfaces = [
|
"panel",
|
"update",
|
"submit",
|
"controls",
|
"side-column",
|
"topbar",
|
"annotations:history",
|
"annotations:current",
|
"annotations:tabs",
|
"annotations:menu",
|
"annotations:add-new",
|
"annotations:delete",
|
"predictions:tabs",
|
"predictions:menu",
|
"edit-history",
|
...additionalInterfaces,
|
];
|
const task = { id: taskId, data, annotations, predictions };
|
|
window.LabelStudio.destroyAll();
|
window.labelStudio = new window.LabelStudio("label-studio", { interfaces, config, task, settings, ...params });
|
}
|
|
const createMethodInjectionIntoScript = (fnName, fn) => {
|
const args = new Array(fn.length)
|
.fill()
|
.map((v, idx) => {
|
return `v${idx}`;
|
})
|
.join(", ");
|
let fnBody = fn.toString();
|
|
if (/^(?:(?!function)(?!async)[a-zA-Z])+/.test(fnBody)) {
|
fnBody = `function ${fnBody}`;
|
}
|
return `${fnName}(${args}) {
|
return (${fnBody})(${args});
|
}`;
|
};
|
|
const FN_PREFIX = "fn_";
|
const prepareInitialParams = (value, prefix = FN_PREFIX) => {
|
if (Array.isArray(value)) {
|
const result = [];
|
let functions = [];
|
|
value.forEach((val, key) => {
|
const [resParam, resFns] = prepareInitialParams(val, `${prefix}_${key}`);
|
|
result.push(resParam);
|
functions = [...functions, ...resFns];
|
});
|
return [result, functions];
|
}
|
if (typeof value === "object") {
|
const result = {};
|
let functions = [];
|
|
Object.keys(value).forEach((key) => {
|
const param = value[key];
|
const [resParam, resFns] = prepareInitialParams(param, `${prefix}_${key}`);
|
|
result[key] = resParam;
|
functions = [...functions, ...resFns];
|
});
|
return [result, functions];
|
}
|
if (typeof value === "function") {
|
const injection = createMethodInjectionIntoScript(prefix, value);
|
|
return [prefix, [injection]];
|
}
|
return [value, []];
|
};
|
|
const createLabelStudioInitFunction = (params) => {
|
const [preparedParams, fns] = prepareInitialParams(params);
|
|
return new Function(
|
"",
|
`
|
function linkFunctions(value) {
|
if (Array.isArray(value)) {
|
return value.map(val => linkFunctions(val));
|
}
|
if (typeof value === "object") {
|
const result = {};
|
Object.keys(value).forEach(key => {
|
result[key] = linkFunctions(value[key])
|
})
|
return result;
|
}
|
if (typeof value === "string" && value.startsWith("fn_")) {
|
return fns[value];
|
}
|
return value;
|
}
|
function ${createMethodInjectionIntoScript("initLabelStudio", initLabelStudio)}
|
const fns = {${fns.join(",")}};
|
const params = ${JSON.stringify(preparedParams)};
|
initLabelStudio(linkFunctions(params));
|
`,
|
);
|
};
|
|
const setFeatureFlagsDefaultValue = (value) => {
|
if (!window.APP_SETTINGS) window.APP_SETTINGS = {};
|
window.APP_SETTINGS.feature_flags_default_value = value;
|
return window.APP_SETTINGS.feature_flags_default_value;
|
};
|
|
/**
|
* IMPORTANT NOTE: if your flags change models this helper should be invoked before `I.amOnPage()`
|
* Sets given feature flags before LSF init
|
* @param {object} featureFlags map of feature flags to set with boolean values
|
* @returns {object}
|
*/
|
const setFeatureFlags = (featureFlags) => {
|
if (!window.APP_SETTINGS) window.APP_SETTINGS = {};
|
if (!window.APP_SETTINGS.feature_flags) window.APP_SETTINGS.feature_flags = {};
|
window.APP_SETTINGS.feature_flags = {
|
...window.APP_SETTINGS.feature_flags,
|
...featureFlags,
|
};
|
return window.APP_SETTINGS.feature_flags;
|
};
|
|
const hasFF = (fflag) => {
|
if (!window.APP_SETTINGS || !window.APP_SETTINGS.feature_flags) return true;
|
|
return window.APP_SETTINGS.feature_flags[fflag] === true;
|
};
|
|
const createAddEventListenerScript = (eventName, callback) => {
|
const args = new Array(callback.length)
|
.fill()
|
.map((v, idx) => {
|
return `v${idx}`;
|
})
|
.join(", ");
|
|
return new Function(
|
"",
|
`
|
function ${eventName}(${args}) {
|
return (${callback.toString()})(${args});
|
}
|
window.labelStudio.on("${eventName}",${eventName});
|
`,
|
);
|
};
|
|
/**
|
* Wait for the main Image object to be loaded
|
*/
|
const waitForImage = () => {
|
return new Promise((resolve, reject) => {
|
const img = document.querySelector("[alt=LS]");
|
|
if (!img || img.complete) return resolve();
|
// this should be rewritten to isReady when it is ready
|
img.onload = () => {
|
setTimeout(resolve, 100);
|
};
|
// if image is not loaded in 10 seconds, reject
|
setTimeout(reject, 10000);
|
});
|
};
|
|
/**
|
* Wait for all audio on the page to be loaded
|
*/
|
const waitForAudio = async () => {
|
const audios = document.querySelectorAll("audio");
|
|
console.log(`Found ${audios.length} audio elements to wait for`);
|
|
await Promise.all(
|
[...audios].map((audio, index) => {
|
console.log(`Audio ${index} readyState: ${audio.readyState}, src: ${audio.src}`);
|
|
if (audio.readyState === 4) {
|
console.log(`Audio ${index} already loaded`);
|
return Promise.resolve(true);
|
}
|
|
return Promise.race([
|
new Promise((resolve) => {
|
if (!isNaN(audio.duration)) {
|
resolve(true);
|
}
|
audio.addEventListener("durationchange", () => {
|
console.log(`Audio ${index} durationchange event fired`);
|
resolve(true);
|
});
|
|
// Also listen for canplaythrough as a backup
|
audio.addEventListener("canplaythrough", () => {
|
console.log(`Audio ${index} canplaythrough event fired`);
|
resolve(true);
|
});
|
}),
|
// Add a timeout to prevent hanging indefinitely
|
new Promise((resolve) =>
|
setTimeout(() => {
|
console.log(`Audio ${index} timeout reached, current readyState: ${audio.readyState}`);
|
resolve(true);
|
}, 5000),
|
),
|
]);
|
}),
|
);
|
|
console.log("All audio elements are ready or timed out");
|
};
|
|
const waitForAudioCanvases = async () => {
|
const audioCanvases = document.querySelectorAll("canvas.waveform-layer-main");
|
|
await Promise.all(
|
[...audioCanvases].map((canvas, index) => {
|
return Promise.race([
|
new Promise((resolve) => {
|
const checkCanvas = () => {
|
const isCanvasReady = canvas.width > 0 && canvas.height > 0;
|
if (isCanvasReady) {
|
const ctx = canvas.getContext("2d");
|
const pixel = ctx.getImageData(1, 1, 1, 1);
|
const isTransparent =
|
pixel.datap[0] === 0 && pixel.datap[1] === 0 && pixel.datap[2] === 0 && pixel.data[3] === 0;
|
if (!isTransparent) {
|
console.log(`Audio canvas ${index} is ready`);
|
resolve(true);
|
}
|
} else {
|
console.log(`Audio canvas ${index} not ready yet, checking again...`);
|
setTimeout(checkCanvas, 100);
|
}
|
};
|
|
checkCanvas();
|
}),
|
// Add a timeout to prevent hanging indefinitely
|
new Promise((resolve) =>
|
setTimeout(() => {
|
console.log(`Audio ${index} timeout reached, current readyState: ${audio.readyState}`);
|
resolve(true);
|
}, 5000),
|
),
|
]);
|
}),
|
);
|
};
|
|
/**
|
* Wait for objects ready
|
*/
|
const waitForObjectsReady = async () => {
|
await new Promise((resolve) => {
|
const watchObjectsReady = () => {
|
const isReady = window.Htx?.annotationStore?.selected?.objects.every((object) => object.isReady);
|
|
if (isReady) {
|
resolve(true);
|
} else {
|
setTimeout(watchObjectsReady, 16);
|
}
|
};
|
|
watchObjectsReady();
|
});
|
};
|
|
/**
|
* Get the metadata of media type element(s)
|
*/
|
const getCurrentMedia = (type) => {
|
const media = document.querySelectorAll(type);
|
|
return [...media].map((m) => ({
|
currentTime: m.currentTime,
|
duration: m.duration,
|
playbackRate: m.playbackRate,
|
paused: m.paused,
|
muted: m.muted,
|
volume: m.volume,
|
src: m.src,
|
}));
|
};
|
|
/**
|
* Float numbers can't be compared strictly, so convert any numbers or structures with numbers
|
* to same structures but with rounded numbers (int for ints, fixed(2) for floats)
|
* @param {*} data
|
*/
|
const convertToFixed = (data, fractionDigits = 2) => {
|
if (["string", "number"].includes(typeof data)) {
|
const n = Number(data);
|
|
return Number.isNaN(n) ? data : Number.isInteger(n) ? n : +n.toFixed(fractionDigits);
|
}
|
if (Array.isArray(data)) {
|
return data.map((n) => convertToFixed(n, fractionDigits));
|
}
|
if (typeof data === "object") {
|
const result = {};
|
|
for (const key in data) {
|
result[key] = convertToFixed(data[key], fractionDigits);
|
}
|
return result;
|
}
|
return data;
|
};
|
|
/**
|
* Create convertor for any measures to relative form on image with given dimensions
|
* Accepts numbers, arrays ([x, y] treated as a special coords array) and hash objects
|
* With [706, 882] given as image sizes:
|
* assert.equal(convertToImageSize(123), 17.42);
|
* assert.deepEqual(convertToImageSize([123, 123]), [17.42, 13.95]);
|
* assert.deepEqual(
|
* convertToImageSize({ width: 123, height: 123, radiusY: 123, coords: [123, 123] }),
|
* { width: 17.42, height: 13.95, radiusY: 13.95, coords: [17.42, 13.95] }
|
* );
|
* @param {number} width
|
* @param {number} height
|
*/
|
const getSizeConvertor = (width, height) =>
|
function convert(data, size = width) {
|
if (typeof data === "number") return convertToFixed((data * 100) / size);
|
if (Array.isArray(data)) {
|
if (data.length === 2) return [convert(data[0]), convert(data[1], height)];
|
return data.map((n) => convert(n));
|
}
|
if (typeof data === "object") {
|
const result = {};
|
|
for (const key in data) {
|
if (key === "rotation") result[key] = data[key];
|
else if (key.startsWith("height") || key === "y" || key.endsWith("Y")) result[key] = convert(data[key], height);
|
else result[key] = convert(data[key]);
|
}
|
return result;
|
}
|
return data;
|
};
|
|
const delay = (n) => new Promise((resolve) => setTimeout(resolve, n));
|
|
// good idea, but it doesn't work :(
|
const emulateClick = (source) => {
|
const event = document.createEvent("CustomEvent");
|
|
event.initCustomEvent("click", true, true, null);
|
event.clientX = source.getBoundingClientRect().top / 2;
|
event.clientY = source.getBoundingClientRect().left / 2;
|
source.dispatchEvent(event);
|
};
|
|
const emulateKeypress = (params) => {
|
document.activeElement.dispatchEvent(
|
new KeyboardEvent("keydown", {
|
bubbles: true,
|
cancelable: true,
|
...params,
|
}),
|
);
|
document.activeElement.dispatchEvent(
|
new KeyboardEvent("keyup", {
|
bubbles: true,
|
cancelable: true,
|
...params,
|
}),
|
);
|
};
|
|
// click the Rect on the Konva canvas
|
const clickRect = () => {
|
const rect = window.Konva.stages[0].findOne("Rect");
|
|
rect.fire("click", { clientX: 10, clientY: 10 });
|
};
|
|
/**
|
* Click once on the main Stage
|
* @param {[number, number]} coords
|
*/
|
const clickKonva = ([x, y]) => {
|
const stage = window.Konva.stages[0];
|
|
stage.fire("click", { clientX: x, clientY: y, evt: { offsetX: x, offsetY: y, timeStamp: Date.now() } });
|
};
|
|
/**
|
* Click multiple times on the Stage
|
* @param {number[][]} points array of coords arrays ([[x1, y1], [x2, y2], ...])
|
*/
|
const clickMultipleKonva = async (points) => {
|
const stage = window.Konva.stages[0];
|
const delay = (timeout = 0) => new Promise((resolve) => setTimeout(resolve, timeout));
|
let lastPoint;
|
|
for (const point of points) {
|
if (lastPoint) {
|
stage.fire("mousemove", { evt: { offsetX: point[0], offsetY: point[1], timeStamp: Date.now() } });
|
await delay();
|
}
|
stage.fire("mousedown", { evt: { offsetX: point[0], offsetY: point[1], timeStamp: Date.now() } });
|
await delay();
|
stage.fire("mouseup", { evt: { offsetX: point[0], offsetY: point[1], timeStamp: Date.now() } });
|
await delay();
|
stage.fire("click", { evt: { offsetX: point[0], offsetY: point[1], timeStamp: Date.now() } });
|
lastPoint = point;
|
await delay();
|
}
|
};
|
|
/**
|
* Create Polygon on Stage by clicking multiple times and click on the first point at the end
|
* @param {number[][]} points array of coords arrays ([[x1, y1], [x2, y2], ...])
|
*/
|
const polygonKonva = async (points) => {
|
try {
|
const delay = (timeout = 0) => new Promise((resolve) => setTimeout(resolve, timeout));
|
const stage = window.Konva.stages[0];
|
|
for (const point of points) {
|
stage.fire("mousedown", {
|
evt: { offsetX: point[0], offsetY: point[1], timeStamp: Date.now(), preventDefault: () => {} },
|
});
|
stage.fire("click", {
|
evt: { offsetX: point[0], offsetY: point[1], timeStamp: Date.now(), preventDefault: () => {} },
|
});
|
stage.fire("mouseup", {
|
evt: { offsetX: point[0], offsetY: point[1], timeStamp: Date.now(), preventDefault: () => {} },
|
});
|
await delay(50);
|
}
|
|
// this works in 50% runs for no reason; maybe some async lazy calculations
|
// const firstPoint = stage.getIntersection({ x, y });
|
|
// Circles (polygon points) redraw every new click so we can find it only after last click
|
const lastPoint = stage.find("Circle").slice(-1)[0];
|
const firstPoint = lastPoint.parent.find("Circle")[0];
|
// for closing the Polygon we should place cursor over the first point
|
|
firstPoint.fire("mouseover");
|
await delay(100);
|
// and only after that we can click on it
|
firstPoint.fire("click", { evt: { preventDefault: () => {} } });
|
} catch (e) {
|
return String(e);
|
}
|
};
|
|
/**
|
* Click and hold, move the cursor (with one pause in the middle) and release the mouse
|
* @param {number} x
|
* @param {number} y
|
* @param {number} shiftX
|
* @param {number} shiftY
|
*/
|
const dragKonva = async ([x, y, shiftX, shiftY]) => {
|
const stage = window.Konva.stages[0];
|
const delay = (timeout = 0) => new Promise((resolve) => setTimeout(resolve, timeout));
|
|
stage.fire("mousedown", { evt: { offsetX: x, offsetY: y } });
|
await delay();
|
stage.fire("mousemove", { evt: { offsetX: x + (shiftX >> 1), offsetY: y + (shiftY >> 1) } });
|
await delay();
|
// we should move the cursor to the last point and only after that release the mouse
|
stage.fire("mousemove", { evt: { offsetX: x + shiftX, offsetY: y + shiftY } });
|
await delay();
|
// because some events work on mousemove and not on mouseup
|
stage.fire("mouseup", { evt: { offsetX: x + shiftX, offsetY: y + shiftY } });
|
// looks like Konva needs some time to update image according to dpi
|
await delay(32);
|
};
|
|
/**
|
* Check if there is layer with given color at given coords
|
* @param {number} x
|
* @param {number} y
|
* @param {number[]} rgbArray
|
* @param {number} tolerance
|
*/
|
const hasKonvaPixelColorAtPoint = ([x, y, rgbArray, tolerance]) => {
|
const stage = window.Konva.stages[0];
|
let result = false;
|
|
const areEqualRGB = (a, b) => {
|
for (let i = 3; i--; ) {
|
if (Math.abs(a[i] - b[i]) > tolerance) {
|
return false;
|
}
|
}
|
return true;
|
};
|
|
for (const layer of stage.getLayers()) {
|
const rgba = layer.getContext().getImageData(x, y, 1, 1).data;
|
|
if (!areEqualRGB(rgbArray, rgba)) continue;
|
|
result = true;
|
}
|
|
return result;
|
};
|
|
const areEqualRGB = (a, b, tolerance) => {
|
for (let i = 3; i--; ) {
|
if (Math.abs(a[i] - b[i]) > tolerance) {
|
return false;
|
}
|
}
|
return true;
|
};
|
|
const setKonvaLayersOpacity = ([opacity]) => {
|
const stage = window.Konva.stages[0];
|
|
for (const layer of stage.getLayers()) {
|
layer.canvas._canvas.style.opacity = opacity;
|
}
|
};
|
|
const getKonvaPixelColorFromPoint = ([x, y]) => {
|
const stage = window.Konva.stages[0];
|
const colors = [];
|
|
for (const layer of stage.getLayers()) {
|
const context = layer.getContext();
|
const ratio = context.canvas.pixelRatio;
|
const rgba = context.getImageData(x * ratio, y * ratio, 1, 1).data;
|
|
colors.push(rgba);
|
}
|
|
return colors;
|
};
|
|
const clearModalIfPresent = () => {
|
const modal = window.document.querySelector(".ant-modal-root");
|
|
if (modal) {
|
modal.remove();
|
}
|
};
|
|
/**
|
*
|
* @param {object} bbox
|
* @param {number} bbox.x
|
* @param {number} bbox.y
|
* @param {number} bbox.width
|
* @param {number} bbox.height
|
* @returns {{x: number, y: number}}
|
*/
|
const centerOfBbox = (bbox) => {
|
return {
|
x: bbox.x + bbox.width / 2,
|
y: bbox.y + bbox.height / 2,
|
};
|
};
|
|
/**
|
* Generate the URL of the image of the specified size
|
* @param {object} size
|
* @param {number} size.width
|
* @param {number} size.height
|
* @returns {Promise<string>}
|
*/
|
async function generateImageUrl({ width, height }) {
|
const canvas = document.createElement("canvas");
|
|
canvas.width = width;
|
canvas.height = height;
|
|
const ctx = canvas.getContext("2d");
|
|
const centerX = width / 2;
|
const centerY = height / 2;
|
|
for (let k = 0; k < centerX; k += 50) {
|
ctx.strokeRect(centerX - k, 0, k * 2, height);
|
}
|
for (let k = 0; k < centerY; k += 50) {
|
ctx.strokeRect(0, centerY - k, width, k * 2);
|
}
|
|
return canvas.toDataURL();
|
}
|
|
const getNaturalSize = () => {
|
const imageObject = window.Htx.annotationStore.selected.objects.find((o) => o.type === "image");
|
|
return {
|
width: imageObject.naturalWidth,
|
height: imageObject.naturalHeight,
|
};
|
};
|
const getCanvasSize = () => {
|
const imageObject = window.Htx.annotationStore.selected.objects.find((o) => o.type === "image");
|
|
return {
|
width: imageObject.canvasSize.width,
|
height: imageObject.canvasSize.height,
|
};
|
};
|
const getImageSize = () => {
|
const image = window.document.querySelector('img[alt="LS"]');
|
const clientRect = image.getBoundingClientRect();
|
|
return {
|
width: clientRect.width,
|
height: clientRect.height,
|
};
|
};
|
const getImageFrameSize = () => {
|
const image = window.document.querySelector('img[alt="LS"]').parentElement;
|
const clientRect = image.getBoundingClientRect();
|
|
return {
|
width: Math.round(clientRect.width),
|
height: Math.round(clientRect.height),
|
};
|
};
|
const setZoom = ([scale, x, y]) => {
|
return new Promise((resolve) => {
|
Htx.annotationStore.selected.objects.find((o) => o.type === "image").setZoom(scale);
|
Htx.annotationStore.selected.objects.find((o) => o.type === "image").setZoomPosition(x, y);
|
setTimeout(resolve, 30);
|
});
|
};
|
|
const getZoomProps = () => {
|
return new Promise((resolve) => {
|
const image = Htx.annotationStore.selected.objects.find((o) => o.type === "image");
|
setTimeout(() => {
|
resolve({
|
stageZoom: image.stageZoom,
|
stageZoomX: image.stageZoomX,
|
stageZoomY: image.stageZoomY,
|
currentZoom: image.currentZoom,
|
zoomScale: image.zoomScale,
|
maxScale: image.maxScale,
|
});
|
}, 30);
|
});
|
};
|
|
/**
|
* Count shapes on Konva, founded by selector
|
* @param {string|function} selector from Konva's finding methods params
|
*/
|
const countKonvaShapes = async () => {
|
const stage = window.Konva.stages[0];
|
const regions = Htx.annotationStore.selected.regionStore.regions;
|
let count = 0;
|
|
regions.forEach((region) => {
|
count += stage.find(`.${region.id}`).filter((node) => node.isVisible()).length;
|
});
|
|
return count;
|
};
|
|
const isTransformerExist = async () => {
|
const stage = window.Konva.stages[0];
|
const anchors = stage.find("._anchor").filter((shape) => shape.getAttr("visible") !== false);
|
|
return !!anchors.length;
|
};
|
|
const isRotaterExist = async () => {
|
const stage = window.Konva.stages[0];
|
const rotaters = stage.find(".rotater").filter((shape) => shape.getAttr("visible") !== false);
|
|
return !!rotaters.length;
|
};
|
|
const getRegionAbsoultePosition = async (shapeId) => {
|
const stage = window.Konva.stages[0];
|
const region = stage.findOne((shape) => String(shape._id) === String(shapeId));
|
|
if (!region) return null;
|
|
const regionPosition = region.getAbsolutePosition();
|
|
return {
|
x: regionPosition.x,
|
y: regionPosition.y,
|
width: region.width(),
|
height: region.height(),
|
};
|
};
|
|
const switchRegionTreeView = async (viewName) => {
|
Htx.annotationStore.selected.regionStore.setGrouping(viewName);
|
// Wait a bit for the view to update
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
};
|
|
const serialize = () => window.Htx.annotationStore.selected.serializeAnnotation();
|
|
const selectText = async ({ selector, rangeStart, rangeEnd }) => {
|
let [doc, win] = [document, window];
|
|
let elem = document.querySelector(selector);
|
|
if (elem.matches("iframe")) {
|
doc = elem.contentDocument;
|
win = elem.contentWindow;
|
elem = doc.body;
|
}
|
|
const findOnPosition = (root, position, borderSide = "left") => {
|
const walker = doc.createTreeWalker(root, NodeFilter.SHOW_ALL);
|
|
let lastPosition = 0;
|
let currentNode = walker.nextNode();
|
let nextNode = walker.nextNode();
|
|
while (currentNode) {
|
const isText = currentNode.nodeType === Node.TEXT_NODE;
|
const isBR = currentNode.nodeName === "BR";
|
|
if (isText || isBR) {
|
const length = currentNode.length ? currentNode.length : 1;
|
|
if (length + lastPosition >= position || !nextNode) {
|
if (borderSide === "right" && length + lastPosition === position && nextNode) {
|
return { node: nextNode, position: 0 };
|
}
|
return { node: currentNode, position: isBR ? 0 : Math.min(Math.max(position - lastPosition, 0), length) };
|
}
|
lastPosition += length;
|
}
|
|
currentNode = nextNode;
|
nextNode = walker.nextNode();
|
}
|
};
|
|
const start = findOnPosition(elem, rangeStart, "right");
|
const end = findOnPosition(elem, rangeEnd, "left");
|
|
const range = new win.Range();
|
const selection = win.getSelection();
|
|
range.setStart(start.node, start.position);
|
range.setEnd(end.node, end.position);
|
|
selection.removeAllRanges();
|
selection.addRange(range);
|
|
const evt = new MouseEvent("mouseup");
|
|
evt.initMouseEvent("mouseup", true, true);
|
elem.dispatchEvent(evt);
|
};
|
|
const getSelectionCoordinates = ({ selector, rangeStart, rangeEnd }) => {
|
let [doc, win] = [document, window];
|
let isIFrame = false;
|
let elem = document.querySelector(selector);
|
|
if (elem.matches("iframe")) {
|
doc = elem.contentDocument;
|
win = elem.contentWindow;
|
elem = doc.body;
|
isIFrame = true;
|
}
|
|
const findOnPosition = (root, position, borderSide = "left") => {
|
const walker = doc.createTreeWalker(root, NodeFilter.SHOW_ALL);
|
|
let lastPosition = 0;
|
let currentNode = walker.nextNode();
|
let nextNode = walker.nextNode();
|
|
while (currentNode) {
|
const isText = currentNode.nodeType === Node.TEXT_NODE;
|
const isBR = currentNode.nodeName === "BR";
|
|
if (isText || isBR) {
|
const length = currentNode.length ? currentNode.length : 1;
|
|
if (length + lastPosition >= position || !nextNode) {
|
if (borderSide === "right" && length + lastPosition === position && nextNode) {
|
return { node: nextNode, position: 0 };
|
}
|
return { node: currentNode, position: isBR ? 0 : Math.min(Math.max(position - lastPosition, 0), length) };
|
}
|
lastPosition += length;
|
}
|
|
currentNode = nextNode;
|
nextNode = walker.nextNode();
|
}
|
};
|
|
const start = findOnPosition(elem, rangeStart, "right");
|
const end = findOnPosition(elem, rangeEnd, "left");
|
|
const range = new win.Range();
|
const selection = win.getSelection();
|
|
range.setStart(start.node, start.position);
|
range.setEnd(end.node, end.position);
|
|
selection.removeAllRanges();
|
selection.addRange(range);
|
|
const rangeRects = Array.from(range.getClientRects());
|
const bboxes = [rangeRects.at(0), rangeRects.at(-1)];
|
const iframeOffset = isIFrame ? elem.getBoundingClientRect() : { left: 0, top: 0 };
|
|
return bboxes.map((bbox) => ({
|
x: bbox.left + iframeOffset.left,
|
y: bbox.top + iframeOffset.top,
|
width: bbox.width,
|
height: bbox.height,
|
}));
|
};
|
|
// Only for debugging
|
const whereIsPixel = ([rgbArray, tolerance]) => {
|
const stage = window.Konva.stages[0];
|
const areEqualRGB = (a, b) => {
|
for (let i = 3; i--; ) {
|
if (Math.abs(a[i] - b[i]) > tolerance) {
|
return false;
|
}
|
}
|
return true;
|
};
|
const points = [];
|
|
for (const layer of stage.getLayers()) {
|
const canvas = layer.getCanvas();
|
|
for (let x = 0; x < canvas.width; x++) {
|
for (let y = 0; y < canvas.height; y++) {
|
const rgba = layer.getContext().getImageData(x, y, 1, 1).data;
|
|
if (areEqualRGB(rgbArray, rgba)) {
|
points.push([x, y]);
|
}
|
}
|
}
|
}
|
return points;
|
};
|
|
const dumpJSON = (obj) => {
|
console.log(JSON.stringify(obj, null, " "));
|
};
|
|
function _isObject(value) {
|
const type = typeof value;
|
|
return value !== null && (type === "object" || type === "function");
|
}
|
|
function _pickBy(obj, predicate, path = []) {
|
if (!_isObject(obj) || Array.isArray(obj)) return obj;
|
return Object.keys(obj).reduce((res, key) => {
|
const val = obj[key];
|
const fullPath = [...path, key];
|
|
if (predicate(val, key, fullPath)) {
|
res[key] = _pickBy(val, predicate, fullPath);
|
}
|
return res;
|
}, {});
|
}
|
|
function _not(predicate) {
|
return (...args) => {
|
return !predicate(...args);
|
};
|
}
|
|
function saveDraftLocally(ls, annotation) {
|
window.LSDraft = annotation.serializeAnnotation();
|
}
|
function getLocallySavedDraft() {
|
return window.LSDraft;
|
}
|
|
function omitBy(object, predicate) {
|
return _pickBy(object, _not(predicate));
|
}
|
|
function hasSelectedRegion() {
|
return !!Htx.annotationStore.selected.highlightedNode;
|
}
|
|
async function doDrawingAction(I, { msg, fromX, fromY, toX, toY }) {
|
I.usePlaywrightTo(msg, async ({ browser, browserContext, page }) => {
|
await page.mouse.move(fromX, fromY);
|
await page.mouse.down();
|
await page.mouse.move(toX, toY);
|
await page.mouse.up();
|
});
|
I.waitTicks(3); // Ensure that the tool is fully finished being created.
|
}
|
|
// `mulberry32` (simple generator with a 32-bit state)
|
function createRandomWithSeed(seed) {
|
return () => {
|
let t = (seed += 0x6d2b79f5);
|
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
};
|
}
|
|
function createRandomIntWithSeed(seed) {
|
const random = createRandomWithSeed(seed);
|
|
return (min, max) => Math.floor(random() * (max - min + 1) + min);
|
}
|
|
module.exports = {
|
initLabelStudio,
|
createLabelStudioInitFunction,
|
setFeatureFlagsDefaultValue,
|
setFeatureFlags,
|
hasFF,
|
createAddEventListenerScript,
|
waitForImage,
|
waitForAudio,
|
waitForAudioCanvases,
|
getCurrentMedia,
|
waitForObjectsReady,
|
delay,
|
|
getSizeConvertor,
|
convertToFixed,
|
|
emulateClick,
|
emulateKeypress,
|
clickRect,
|
clickKonva,
|
clickMultipleKonva,
|
polygonKonva,
|
dragKonva,
|
areEqualRGB,
|
hasKonvaPixelColorAtPoint,
|
getKonvaPixelColorFromPoint,
|
getNaturalSize,
|
getCanvasSize,
|
getImageSize,
|
getImageFrameSize,
|
getRegionAbsoultePosition,
|
setKonvaLayersOpacity,
|
setZoom,
|
getZoomProps,
|
whereIsPixel,
|
countKonvaShapes,
|
isTransformerExist,
|
isRotaterExist,
|
switchRegionTreeView,
|
hasSelectedRegion,
|
clearModalIfPresent,
|
centerOfBbox,
|
generateImageUrl,
|
|
serialize,
|
selectText,
|
getSelectionCoordinates,
|
|
saveDraftLocally,
|
getLocallySavedDraft,
|
|
omitBy,
|
dumpJSON,
|
|
doDrawingAction,
|
createRandomWithSeed,
|
createRandomIntWithSeed,
|
};
|