Skip to content
This repository has been archived by the owner on Apr 18, 2024. It is now read-only.

fix: LSDV-5248: BBoxes optimizations #1523

Merged
merged 22 commits into from
Sep 4, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e7d39ca
fix: LSDV-5248: Make dblclick in Image Segmentation works without delay
Gondragos Aug 7, 2023
b74e000
Add missed file
Gondragos Aug 7, 2023
b48a0c5
fix tests
Gondragos Aug 8, 2023
a402ec1
Try to fix events sequence
Gondragos Aug 9, 2023
eadde62
Try to fix events logic
Gondragos Aug 11, 2023
23a9b3e
Fix false click generation
Gondragos Aug 11, 2023
1c3c2ba
Fix the problem of double clicking by using portals instead of recrea…
Gondragos Aug 14, 2023
899515e
Remove unused utils
Gondragos Aug 14, 2023
d7ff12e
Fix OCR test
Gondragos Aug 14, 2023
a804a52
Fix brush selection logic
Gondragos Aug 16, 2023
349ebc4
Merge branch 'master' into fb-lsdv-5248/bbox-optimization
Gondragos Aug 16, 2023
1f48dcc
Merge branch 'master' into fb-lsdv-5248/bbox-optimization
Gondragos Aug 24, 2023
2c9e536
Merge remote-tracking branch 'origin/master' into fb-lsdv-5248/bbox-o…
Gondragos Aug 28, 2023
0dc92be
Add feature flag
Gondragos Aug 28, 2023
052057c
Fix eslint problems
Gondragos Aug 28, 2023
6dae02f
Use detail for catching double click interactions
Gondragos Aug 29, 2023
d0dd603
Add comments, tests, fixes and refactor
Gondragos Aug 31, 2023
da5875b
Remove unused import
Gondragos Aug 31, 2023
84d52cf
Merge branch 'master' into fb-lsdv-5248/bbox-optimization
Gondragos Sep 4, 2023
1d6dd4c
Update @heartexlabs/ls-test
Gondragos Sep 4, 2023
a496762
Fix double click interaction in codecept tests
Gondragos Sep 4, 2023
05f5ee6
Merge branch 'fb-lsdv-5248/bbox-optimization' of github.com:heartexla…
Gondragos Sep 4, 2023
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
3 changes: 3 additions & 0 deletions e2e/fragments/AtOutliner.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ module.exports = {
locateRegionItemIndex(idx) {
return locate(this._regionListItemIndex).withText(`${idx}`).inside(this.locateRegionItemList());
},
locateRegionIndex(idx) {
return this.locateRegionItemList().withDescendant(locate(this._regionListItemIndex).toXPath() + `[text()='${idx}']`);
},
locateSelectedItem(locator) {
const selectedLocator = locate(this._regionListItemSelectedSelector).inside(this.locateRegionList());

Expand Down
1 change: 1 addition & 0 deletions e2e/setup/feature-flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ module.exports = {
fflag_feat_front_lsdv_4620_richtext_opimization_060423_short: true,
fflag_fix_front_lsdv_4620_memory_leaks_100723_short: true,
fflag_feat_front_lsdv_4620_outliner_optimization_310723_short: true,
fflag_fix_front_lsdv_5248_double_click_delay_280823_short: true,
};
16 changes: 10 additions & 6 deletions e2e/tests/ocr.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,10 @@ const REGIONS = [
},
];

Scenario('Drawing multiple blank regions and then attaching labels', async ({ I, LabelStudio, AtImageView, AtSettings, AtLabels, AtSidebar }) => {
Scenario('Drawing multiple blank regions and then attaching labels', async ({ I, LabelStudio, AtImageView, AtSettings, AtLabels, AtOutliner }) => {
LabelStudio.setFeatureFlags({
'ff_front_1170_outliner_030222_short': true,
});
I.amOnPage('/');
LabelStudio.init({ config: createConfig( ), data });
AtImageView.waitForImage();
Expand All @@ -168,13 +171,14 @@ Scenario('Drawing multiple blank regions and then attaching labels', async ({ I,
for (const region of regions) {
AtImageView.drawByDrag(region.x, region.y, region.width, region.height);
}
AtSidebar.seeRegions(regions.length);
AtOutliner.seeRegions(regions.length);

I.say('Labeling');
for (const [idx, region] of Object.entries(regions)) {
for (const region of Object.values(regions)) {
AtImageView.dblClickAt(region.x + region.width / 2, region.y + region.height / 2);
AtLabels.clickLabel(region.label);
if (region.text) {
I.fillField(AtSidebar.locate('.lsf-region-item').withText(`${+idx+1}`).find('.lsf-textarea-tag__input'), region.text);
I.fillField(AtOutliner.locateSelectedItem().find('.lsf-textarea-tag__input'), region.text);
}
}
const results = await LabelStudio.serialize();
Expand All @@ -190,10 +194,10 @@ Scenario('Drawing multiple blank regions and then attaching labels', async ({ I,
I.amOnPage('/');
LabelStudio.init({ config: createConfig( ), data, annotations: [{ id: 'test', result: results }] });
AtImageView.waitForImage();
AtSidebar.seeRegions(regions.length);
AtOutliner.seeRegions(regions.length);
for (const [idx, region] of Object.entries(regions)) {
if (region.text) {
I.seeInField(AtSidebar.locate('.lsf-region-item').withText(`${+idx+1}`).find('.lsf-textarea-tag__input'), region.text);
I.seeInField(AtOutliner.locateRegionIndex((+idx)+1).find('.lsf-textarea-tag__input'), region.text);
}
}
});
Expand Down
29 changes: 27 additions & 2 deletions src/components/ImageView/ImageView.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { debounce } from '../../utils/debounce';
import Constants from '../../core/Constants';
import { fixRectToFit } from '../../utils/image';
import {
FF_DBLCLICK_DELAY,
FF_DEV_1285,
FF_DEV_1442,
FF_DEV_3077,
Expand Down Expand Up @@ -64,6 +65,9 @@ const splitRegions = (regions) => {
};

const Region = memo(({ region, showSelected = false }) => {
if (isFF(FF_DBLCLICK_DELAY)) {
return useObserver(() => Tree.renderItem(region, region.annotation, true));
}
return useObserver(() => region.inSelection !== showSelected ? null : Tree.renderItem(region, region.annotation, false));
});

Expand Down Expand Up @@ -388,7 +392,10 @@ const SelectionLayer = observer(({ item, selectionArea }) => {
const Selection = observer(({ item, selectionArea, ...triggeredOnResize }) => {
return (
<>
<SelectedRegions item={item} selectedRegions={item.selectedRegions} {...triggeredOnResize} />
{ isFF(FF_DBLCLICK_DELAY)
? <Layer name="selection-regions-layer" />
: <SelectedRegions item={item} selectedRegions={item.selectedRegions} {...triggeredOnResize} />
}
<SelectionLayer item={item} selectionArea={selectionArea} />
</>
);
Expand Down Expand Up @@ -555,6 +562,7 @@ export default observer(
handleMouseDown = e => {
const { item } = this.props;
const isPanTool = item.getToolsManager().findSelectedTool()?.fullName === 'ZoomPanTool';
const isMoveTool = item.getToolsManager().findSelectedTool()?.fullName === 'MoveTool';

if (isFF(FF_LSDV_4930)) {
this.mouseDownPoint = { x: e.evt.offsetX, y: e.evt.offsetY };
Expand All @@ -568,13 +576,30 @@ export default observer(
if (p && p.className === 'Transformer') return;

const handleMouseDown = () => {
const isRightElementToCatchToolInteractions = el => {
// It could be ruler ot segmentation
if (el.nodeType === 'Group') {
if ('ruler' === el?.attrs?.name) {
return true;
}
// segmentation is specific for Brushes
// but click interaction on the region covers the case of the same MoveTool interaction here,
// so it should ignore move tool interaction to prevent conflicts
if ((!isFF(FF_DBLCLICK_DELAY) || !isMoveTool)
&& 'segmentation' === el?.attrs?.name) {
return true;
}
}
return false;
};

if (
// create regions over another regions with Cmd/Ctrl pressed
item.getSkipInteractions() ||
e.target === item.stageRef ||
findClosestParent(
e.target,
el => el.nodeType === 'Group' && ['ruler', 'segmentation'].indexOf(el?.attrs?.name) > -1,
isRightElementToCatchToolInteractions,
)
) {
window.addEventListener('mousemove', this.handleGlobalMouseMove);
Expand Down
46 changes: 32 additions & 14 deletions src/mixins/KonvaRegion.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { types } from 'mobx-state-tree';
import { FF_DEV_3793, isFF } from '../utils/feature-flags';

import { FF_DBLCLICK_DELAY, FF_DEV_3793, isFF } from '../utils/feature-flags';
export const KonvaRegionMixin = types.model({})
.views((self) => {
return {
Expand Down Expand Up @@ -103,6 +102,15 @@ export const KonvaRegionMixin = types.model({})

if (e) e.cancelBubble = true;

if (isFF(FF_DBLCLICK_DELAY)) {
const isDoubleClick = ev.detail === 2;

if (isDoubleClick) {
self.onDoubleClickRegion();
return;
}
}

const selectAction = () => {
self._selectArea(additiveMode);
deferredSelectId = null;
Expand All @@ -113,21 +121,31 @@ export const KonvaRegionMixin = types.model({})
annotation.stopRelationMode();
annotation.regionStore.unselectAll();
} else {
// Skip double click emulation when there is nothing to focus
if (!self.perRegionFocusTarget) {
selectAction();
return;
}
// Double click emulation
if (deferredSelectId) {
clearTimeout(deferredSelectId);
self.requestPerRegionFocus();
deferredSelectId = null;
annotation.selectArea(self);
if (isFF(FF_DBLCLICK_DELAY)) {
self._selectArea(additiveMode);
} else {
deferredSelectId = setTimeout(selectAction, 300);
// Skip double click emulation when there is nothing to focus
if (!self.perRegionFocusTarget) {
selectAction();
return;
}
// Double click emulation
if (deferredSelectId) {
clearTimeout(deferredSelectId);
self.requestPerRegionFocus();
deferredSelectId = null;
annotation.selectArea(self);
} else {
deferredSelectId = setTimeout(selectAction, 300);
}
}
}
},
onDoubleClickRegion() {
self.requestPerRegionFocus();
// `selectArea` does nothing when there's a selected region already, but it should rerender to make `requestPerRegionFocus` work,
// so it needs to use `selectAreas` instead. It contains `unselectAll` for this purpose.
self.annotation.selectAreas([self]);
hlomzik marked this conversation as resolved.
Show resolved Hide resolved
},
};
});
21 changes: 19 additions & 2 deletions src/regions/AliveRegion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import { observer } from 'mobx-react';
import { isAlive } from 'mobx-state-tree';

import { IReactComponent } from 'mobx-react/dist/types/IReactComponent';
import { useCallback } from 'react';
import { ExoticComponent, Fragment, ReactNode, useCallback } from 'react';
import { Portal } from 'react-konva-utils';
import { FF_DBLCLICK_DELAY, isFF } from '../utils/feature-flags';

type Region = {
annotation: any,
hidden: boolean,
// ...
setShapeRef(ref: any): void,
inSelection: boolean,
}

type RegionComponentProps = {
Expand All @@ -18,8 +21,15 @@ type RegionComponentProps = {

type Options = {
renderHidden?: boolean,
shouldNotUsePortal?: boolean,
}

type PortalProps = {
selector?: string,
enabled?: boolean,
children: ReactNode,
};

export const AliveRegion = (
RegionComponent: IReactComponent<RegionComponentProps>,
options?: Options,
Expand All @@ -28,13 +38,20 @@ export const AliveRegion = (

return observer(({ item, ...rest }: RegionComponentProps) => {
const canRender = options?.renderHidden || !item.hidden;
const shouldNotUsePortal = !isFF(FF_DBLCLICK_DELAY) || options?.shouldNotUsePortal;
const Wrapper = (shouldNotUsePortal ? Fragment : Portal) as ExoticComponent<PortalProps>;
hlomzik marked this conversation as resolved.
Show resolved Hide resolved
const wrapperProps = shouldNotUsePortal ? {} : { selector: '.selection-regions-layer', enabled: item.inSelection };
const isInTree = !!item.annotation;
const setShapeRef = useCallback((ref) => {
if (isAlive(item)) {
item.setShapeRef(ref);
}
}, [item]);

return isInTree && isAlive(item) && canRender ? <ObservableRegion item={item} {...rest} setShapeRef={setShapeRef} /> : null;
return isInTree && isAlive(item) && canRender ? (
<Wrapper {...wrapperProps}>
<ObservableRegion item={item} {...rest} setShapeRef={setShapeRef} />
</Wrapper>
): null;
});
};
5 changes: 4 additions & 1 deletion src/regions/BrushRegion.js
Original file line number Diff line number Diff line change
Expand Up @@ -725,7 +725,10 @@ const HtxBrushView = ({ item, setShapeRef }) => {
);
};

const HtxBrush = AliveRegion(HtxBrushView, { renderHidden: true });
const HtxBrush = AliveRegion(HtxBrushView, {
renderHidden: true,
shouldNotUsePortal: true,
});

Registry.addTag('brushregion', BrushRegionModel, HtxBrush);
Registry.addRegionType(BrushRegionModel, 'image', value => value.rle || value.touches || value.maskDataURL);
Expand Down
3 changes: 2 additions & 1 deletion src/regions/KeyPointRegion.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { createDragBoundFunc } from '../utils/image';
import { AliveRegion } from './AliveRegion';
import { EditableRegion } from './EditableRegion';
import { RELATIVE_STAGE_HEIGHT, RELATIVE_STAGE_WIDTH } from '../components/ImageView/Image';
import Constants from '../core/Constants';

const KeyPointRegionAbsoluteCoordsDEV3793 = types
.model({
Expand Down Expand Up @@ -261,7 +262,7 @@ const HtxKeyPointView = ({ item, setShapeRef }) => {
if (item.parent.getSkipInteractions()) return;

if (store.annotationStore.selected.relationMode) {
stage.container().style.cursor = 'default';
stage.container().style.cursor = Constants.DEFAULT_CURSOR;
}

item.setHighlight(false);
Expand Down
6 changes: 6 additions & 0 deletions src/utils/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,12 @@ export const FF_LSDV_4998 = 'fflag_fix_front_lsdv_4998_missed_dynamic_children_0
*/
export const FF_PROD_309 = 'fflag_feat_front_prod_309_choice_hint_080523_short';

/**
* Fix delay on double-click interactions in Image Segmentation
* @link https://app.launchdarkly.com/default/production/features/fflag_fix_front_lsdv_5248_double_click_delay_280823_short
*/
export const FF_DBLCLICK_DELAY = 'fflag_fix_front_lsdv_5248_double_click_delay_280823_short';

/**
* Allow to load Taxonomy from remote API
* @link https://app.launchdarkly.com/default/production/features/fflag_feat_front_lsdv_5451_async_taxonomy_110823_short
Expand Down
48 changes: 48 additions & 0 deletions tests/functional/data/image_segmentation/layers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export const simpleConfig = `
<View>
<Image name="img" value="$image"></Image>
<RectangleLabels name="tag" toName="img">
<Label value="Label 1" background="green"></Label>
<Label value="Label 2" background="blue"></Label>
</RectangleLabels>
</View>
`;

export const simpleImageData = {
image: 'https://htx-misc.s3.amazonaws.com/opensource/label-studio/examples/images/nick-owuor-astro-nic-visuals-wDifg5xc9Z4-unsplash.jpg',
};

export const simpleResult = [
{
id: 'Dx_aB91ISN',
source: '$image',
from_name: 'tag',
to_name: 'img',
type: 'rectanglelabels',
origin: 'manual',
value: {
height: 20,
rotation: 0,
width: 20,
x: 40,
y: 40,
rectanglelabels: ['Label 1'],
},
},
{
id: 'Dx_aB91INs',
source: '$image',
from_name: 'tag',
to_name: 'img',
type: 'rectanglelabels',
origin: 'manual',
value: {
height: 90,
rotation: 0,
width: 90,
x: 5,
y: 5,
rectanglelabels: ['Label 2'],
},
},
];
1 change: 1 addition & 0 deletions tests/functional/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export const CURRENT_FLAGS = {
[FLAGS.FF_LSDV_4620_3]: true,
[FLAGS.FF_LSDV_4620_3_ML]: true,
[FLAGS.FF_OUTLINER_OPTIM]: true,
[FLAGS.FF_DBLCLICK_DELAY]: true,
};

25 changes: 25 additions & 0 deletions tests/functional/specs/image_segmentation/layers.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ImageView, LabelStudio, Sidebar } from '@heartexlabs/ls-test/helpers/LSF';
import { simpleConfig, simpleImageData, simpleResult } from 'data/image_segmentation/layers';

describe('Image Segmentation - Layers', () => {
it('should keep selected region over unselected one', () => {
LabelStudio.params()
.config(simpleConfig)
.data(simpleImageData)
.withResult(simpleResult)
.init();

ImageView.waitForImage();
Sidebar.hasRegions(2);
Sidebar.toggleRegionSelection(0);

Sidebar.hasSelectedRegions(1);

// A selected region should be over all unselected regions,
// so the test should click in it and clear selection
ImageView.clickAtRelative(.5,.5);

Sidebar.hasSelectedRegions(0);
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ImageView, LabelStudio, Sidebar } from '@heartexlabs/ls-test/helpers/LS
import { fourRectanglesResult, simpleEllipseConfig, simpleEllipseResult,
simpleImageData,
simplePointConfig,
simplePointResult, simplePolygonConfig, simplePolygonResult, simpleRectangleConfig, simpleRectangleResult } from 'data/image_segmentstion/tools/selection-tool';
simplePointResult, simplePolygonConfig, simplePolygonResult, simpleRectangleConfig, simpleRectangleResult } from 'data/image_segmentation/tools/selection-tool';
import { FF_DEV_1442 } from '../../../../../src/utils/feature-flags';

describe('Image segmentation - Tools - Selection tool', () => {
Expand Down
Loading