Compare commits

...

7 Commits

Author SHA1 Message Date
AlexandreSi
21b36b8798 When selecting multiple instances, dynamically show instances that will be (un)selected 2022-12-22 10:48:51 +01:00
AlexandreSi
9b09aa2e51 Use strict types 2022-12-22 10:42:31 +01:00
AlexandreSi
12e7af4952 Respect z ordering of cut instances when pasting them 2022-12-22 09:13:11 +01:00
AlexandreSi
66005be895 Paste cutted instances in the foreground 2022-12-22 09:02:44 +01:00
AlexandreSi
a7f524b3a7 Snap added serialized instance to grid unless asked not 2022-12-21 17:02:44 +01:00
AlexandreSi
a1178364e6 Add instances in the selection adder class 2022-12-21 16:56:57 +01:00
AlexandreSi
4c6bc2c1e3 Do not pollute clipboard when duplicating an object 2022-12-21 15:58:08 +01:00
6 changed files with 357 additions and 112 deletions

View File

@@ -1,5 +1,6 @@
// @flow
import { roundPosition } from '../Utils/GridHelpers';
import { unserializeFromJSObject } from '../Utils/Serializer';
import { type InstancesEditorSettings } from './InstancesEditorSettings';
const gd: libGDevelop = global.gd;
@@ -51,6 +52,67 @@ export default class InstancesAdder {
this._instancesEditorSettings = instancesEditorSettings;
}
addSerializedInstances = ({
position,
copyReferential,
serializedInstances,
preventSnapToGrid = false,
addInstancesInTheForeground = false,
}: {|
position: [number, number],
copyReferential: [number, number],
serializedInstances: Array<Object>,
preventSnapToGrid?: boolean,
addInstancesInTheForeground?: boolean,
|}): Array<gdInitialInstance> => {
this._zOrderFinder.reset();
this._instances.iterateOverInstances(this._zOrderFinder);
const sceneForegroundZOrder = this._zOrderFinder.getHighestZOrder() + 1;
let addedInstancesLowestZOrder = null;
const newInstances = serializedInstances.map(serializedInstance => {
const instance = new gd.InitialInstance();
unserializeFromJSObject(instance, serializedInstance);
const desiredPosition = [
instance.getX() - copyReferential[0] + position[0],
instance.getY() - copyReferential[1] + position[1],
];
const newPos = preventSnapToGrid
? desiredPosition
: roundPositionsToGrid(desiredPosition, this._instancesEditorSettings);
instance.setX(newPos[0]);
instance.setY(newPos[1]);
if (addInstancesInTheForeground) {
if (
addedInstancesLowestZOrder === null ||
addedInstancesLowestZOrder > instance.getZOrder()
) {
addedInstancesLowestZOrder = instance.getZOrder();
}
}
const newInstance = this._instances
.insertInitialInstance(instance)
.resetPersistentUuid();
instance.delete();
return newInstance;
});
if (addInstancesInTheForeground && addedInstancesLowestZOrder !== null) {
newInstances.forEach(instance => {
instance.setZOrder(
instance.getZOrder() -
// Flow is not happy with addedInstancesLowestZOrder possible null value
// so 0 is used as a fallback.
(addedInstancesLowestZOrder || 0) +
sceneForegroundZOrder
);
});
}
return newInstances;
};
/**
* Immediately create new instance at the specified position
* (specified in scene coordinates).

View File

@@ -27,6 +27,7 @@ type Props = {|
onRotateEnd: () => void,
toCanvasCoordinates: (x: number, y: number) => [number, number],
screenType: ScreenType,
showHandles: boolean,
|};
const getButtonSizes = (screenType: ScreenType) => {
@@ -77,6 +78,7 @@ export default class SelectedInstances {
onRotateEnd: () => void;
toCanvasCoordinates: (x: number, y: number) => [number, number];
_screenType: ScreenType;
showHandles: boolean;
pixiContainer = new PIXI.Container();
rectanglesContainer = new PIXI.Container();
@@ -93,6 +95,7 @@ export default class SelectedInstances {
onRotateEnd,
toCanvasCoordinates,
screenType,
showHandles,
}: Props) {
this.instanceMeasurer = instanceMeasurer;
this.onResize = onResize;
@@ -102,33 +105,36 @@ export default class SelectedInstances {
this.toCanvasCoordinates = toCanvasCoordinates;
this.instancesSelection = instancesSelection;
this._screenType = screenType;
this.showHandles = showHandles;
this.pixiContainer.addChild(this.rectanglesContainer);
for (const resizeGrabbingLocation of resizeGrabbingLocationValues) {
const resizeButton = new PIXI.Graphics();
this.resizeButtons[resizeGrabbingLocation] = resizeButton;
if (showHandles) {
for (const resizeGrabbingLocation of resizeGrabbingLocationValues) {
const resizeButton = new PIXI.Graphics();
this.resizeButtons[resizeGrabbingLocation] = resizeButton;
this._makeButton(
resizeButton,
event => {
this.onResize(event.deltaX, event.deltaY, resizeGrabbingLocation);
},
() => {
this.onResizeEnd();
},
resizeGrabbingIconNames[resizeGrabbingLocation]
);
}
this._makeButton(
resizeButton,
this.rotateButton,
event => {
this.onResize(event.deltaX, event.deltaY, resizeGrabbingLocation);
this.onRotate(event.deltaX, event.deltaY);
},
() => {
this.onResizeEnd();
this.onRotateEnd();
},
resizeGrabbingIconNames[resizeGrabbingLocation]
'url("res/actions/rotate24_black.png"),auto'
);
}
this._makeButton(
this.rotateButton,
event => {
this.onRotate(event.deltaX, event.deltaY);
},
() => {
this.onRotateEnd();
},
'url("res/actions/rotate24_black.png"),auto'
);
}
setScreenType(screenType: ScreenType) {
@@ -260,9 +266,10 @@ export default class SelectedInstances {
this.rectanglesContainer.removeChild(this.selectedRectangles.pop());
}
// If there are no unlocked instances, hide the resize buttons.
const show =
selection.filter(instance => !instance.isLocked()).length !== 0;
if (!this.showHandles) return;
// If there is any unlocked instances, show the resize buttons.
const show = selection.some(instance => !instance.isLocked());
// Position the resize buttons.
for (const grabbingLocation of resizeGrabbingLocationValues) {

View File

@@ -4,6 +4,39 @@ import Rectangle from '../Utils/Rectangle';
import { type InstanceMeasurer } from './InstancesRenderer';
const gd: libGDevelop = global.gd;
const getRectangleNormalizedBoundaries = ({
startX,
startY,
endX,
endY,
}: {
startX: number,
startY: number,
endX: number,
endY: number,
}) => {
let normalizedStartX = startX;
let normalizedStartY = startY;
let normalizedEndX = endX;
let normalizedEndY = endY;
if (normalizedStartX > normalizedEndX) {
const tmp = normalizedStartX;
normalizedStartX = normalizedEndX;
normalizedEndX = tmp;
}
if (normalizedStartY > normalizedEndY) {
const tmp = normalizedStartY;
normalizedStartY = normalizedEndY;
normalizedEndY = tmp;
}
return {
startX: normalizedStartX,
startY: normalizedStartY,
endX: normalizedEndX,
endY: normalizedEndY,
};
};
export default class SelectionRectangle {
instances: gdInitialInstancesContainer;
instanceMeasurer: InstanceMeasurer;
@@ -12,9 +45,13 @@ export default class SelectionRectangle {
pixiRectangle: PIXI.Graphics;
selectionRectangleStart: { x: number, y: number } | null;
selectionRectangleEnd: { x: number, y: number } | null;
temporarySelectionRectangleStart: { x: number, y: number } | null;
temporarySelectionRectangleEnd: { x: number, y: number } | null;
_instancesInSelectionRectangle: gdInitialInstance[];
_temporaryInstancesInSelectionRectangle: gdInitialInstance[];
selector: gdInitialInstanceJSFunctor;
temporarySelector: gdInitialInstanceJSFunctor;
/**
* Used to check if an instance is in the selection rectangle
*/
@@ -38,6 +75,7 @@ export default class SelectionRectangle {
this.selectionRectangleStart = null;
this.selectionRectangleEnd = null;
this._instancesInSelectionRectangle = [];
this._temporaryInstancesInSelectionRectangle = [];
this._temporaryAABB = new Rectangle();
this.selector = new gd.InitialInstanceJSFunctor();
@@ -71,6 +109,41 @@ export default class SelectionRectangle {
this._instancesInSelectionRectangle.push(instance);
}
};
this.temporarySelector = new gd.InitialInstanceJSFunctor();
// $FlowFixMe - invoke is not writable
this.temporarySelector.invoke = instancePtr => {
// $FlowFixMe - wrapPointer is not exposed
const instance = gd.wrapPointer(instancePtr, gd.InitialInstance);
const instanceAABB = this.instanceMeasurer.getInstanceAABB(
instance,
this._temporaryAABB
);
const {
temporarySelectionRectangleEnd,
temporarySelectionRectangleStart,
} = this;
if (!temporarySelectionRectangleStart || !temporarySelectionRectangleEnd)
return;
const selectionSceneStart = toSceneCoordinates(
temporarySelectionRectangleStart.x,
temporarySelectionRectangleStart.y
);
const selectionSceneEnd = toSceneCoordinates(
temporarySelectionRectangleEnd.x,
temporarySelectionRectangleEnd.y
);
if (
selectionSceneStart[0] <= instanceAABB.left &&
instanceAABB.right <= selectionSceneEnd[0] &&
selectionSceneStart[1] <= instanceAABB.top &&
instanceAABB.bottom <= selectionSceneEnd[1]
) {
this._temporaryInstancesInSelectionRectangle.push(instance);
}
};
}
hasStartedSelectionRectangle() {
@@ -82,28 +155,55 @@ export default class SelectionRectangle {
this.selectionRectangleEnd = { x, y };
};
updateSelectionRectangle = (lastX: number, lastY: number) => {
updateSelectionRectangle = (
lastX: number,
lastY: number
): Array<gdInitialInstance> => {
if (!this.selectionRectangleStart)
this.selectionRectangleStart = { x: lastX, y: lastY };
this.selectionRectangleEnd = { x: lastX, y: lastY };
this._temporaryInstancesInSelectionRectangle.length = 0;
const normalizedSelectionRectangle = getRectangleNormalizedBoundaries({
startX: this.selectionRectangleStart.x,
startY: this.selectionRectangleStart.y,
endX: this.selectionRectangleEnd.x,
endY: this.selectionRectangleEnd.y,
});
this.temporarySelectionRectangleStart = {
x: normalizedSelectionRectangle.startX,
y: normalizedSelectionRectangle.startY,
};
this.temporarySelectionRectangleEnd = {
x: normalizedSelectionRectangle.endX,
y: normalizedSelectionRectangle.endY,
};
this.instances.iterateOverInstances(
// $FlowFixMe - gd.castObject is not supporting typings.
this.temporarySelector
);
return this._temporaryInstancesInSelectionRectangle;
};
endSelectionRectangle = () => {
endSelectionRectangle = (): Array<gdInitialInstance> => {
if (!this.selectionRectangleStart || !this.selectionRectangleEnd) return [];
this._instancesInSelectionRectangle.length = 0;
if (this.selectionRectangleStart.x > this.selectionRectangleEnd.x) {
const tmp = this.selectionRectangleStart.x;
this.selectionRectangleStart.x = this.selectionRectangleEnd.x;
this.selectionRectangleEnd.x = tmp;
}
if (this.selectionRectangleStart.y > this.selectionRectangleEnd.y) {
const tmp = this.selectionRectangleStart.y;
this.selectionRectangleStart.y = this.selectionRectangleEnd.y;
this.selectionRectangleEnd.y = tmp;
}
const normalizedSelectionRectangle = getRectangleNormalizedBoundaries({
startX: this.selectionRectangleStart.x,
startY: this.selectionRectangleStart.y,
endX: this.selectionRectangleEnd.x,
endY: this.selectionRectangleEnd.y,
});
this.selectionRectangleStart = {
x: normalizedSelectionRectangle.startX,
y: normalizedSelectionRectangle.startY,
};
this.selectionRectangleEnd = {
x: normalizedSelectionRectangle.endX,
y: normalizedSelectionRectangle.endY,
};
this.instances.iterateOverInstances(
// $FlowFixMe - gd.castObject is not supporting typings.
this.selector

View File

@@ -103,6 +103,7 @@ export default class InstancesEditor extends Component<Props> {
lastContextMenuY = 0;
lastCursorX = 0;
lastCursorY = 0;
instancesTemporarySelection = new InstancesSelection();
fpsLimiter = new FpsLimiter(28);
canvasArea: ?HTMLDivElement;
pixiRenderer: PIXI.Renderer;
@@ -112,6 +113,7 @@ export default class InstancesEditor extends Component<Props> {
_instancesAdder: InstancesAdder;
selectionRectangle: SelectionRectangle;
selectedInstances: SelectedInstances;
temporarySelectedInstances: SelectedInstances;
highlightedInstance: HighlightedInstance;
instancesResizer: InstancesResizer;
instancesRotator: InstancesRotator;
@@ -383,6 +385,18 @@ export default class InstancesEditor extends Component<Props> {
onResizeEnd: this._onResizeEnd,
onRotate: this._onRotate,
onRotateEnd: this._onRotateEnd,
showHandles: true,
instanceMeasurer: this.instancesRenderer.getInstanceMeasurer(),
toCanvasCoordinates: this.viewPosition.toCanvasCoordinates,
screenType: this.props.screenType,
});
this.temporarySelectedInstances = new SelectedInstances({
instancesSelection: this.instancesTemporarySelection,
onResize: () => {},
onResizeEnd: () => {},
onRotate: () => {},
onRotateEnd: () => {},
showHandles: false,
instanceMeasurer: this.instancesRenderer.getInstanceMeasurer(),
toCanvasCoordinates: this.viewPosition.toCanvasCoordinates,
screenType: this.props.screenType,
@@ -425,6 +439,9 @@ export default class InstancesEditor extends Component<Props> {
this.pixiContainer.addChild(this.windowBorder.getPixiObject());
this.pixiContainer.addChild(this.windowMask.getPixiObject());
this.pixiContainer.addChild(this.selectedInstances.getPixiContainer());
this.pixiContainer.addChild(
this.temporarySelectedInstances.getPixiContainer()
);
this.pixiContainer.addChild(this.highlightedInstance.getPixiObject());
this.pixiContainer.addChild(this.statusBar.getPixiObject());
}
@@ -488,6 +505,7 @@ export default class InstancesEditor extends Component<Props> {
if (nextProps.screenType !== this.props.screenType) {
this.selectedInstances.setScreenType(this.props.screenType);
this.temporarySelectedInstances.setScreenType(this.props.screenType);
}
if (
@@ -549,12 +567,26 @@ export default class InstancesEditor extends Component<Props> {
};
/**
* Immediately add instances for the specified objects at the given
* Immediately add serialized instances at the given
* position (in scene coordinates).
*/
addSerializedInstances = (options: {|
position: [number, number],
copyReferential: [number, number],
serializedInstances: Array<Object>,
preventSnapToGrid?: boolean,
addInstancesInTheForeground?: boolean,
|}): Array<gdInitialInstance> => {
return this._instancesAdder.addSerializedInstances(options);
};
/**
* Immediately add instances for the specified objects at the given
* position (in scene coordinates) given their names.
*/
addInstances = (
pos /*: [number, number] */,
objectNames /*: Array<string> */
pos: [number, number],
objectNames: Array<string>
): Array<gdInitialInstance> => {
return this._instancesAdder.addInstances(pos, objectNames);
};
@@ -594,7 +626,15 @@ export default class InstancesEditor extends Component<Props> {
this.scrollBy(-sceneDeltaX, -sceneDeltaY);
} else {
this.selectionRectangle.updateSelectionRectangle(x, y);
const temporarySelectedInstances = this.selectionRectangle.updateSelectionRectangle(
x,
y
);
this.instancesTemporarySelection.selectInstances({
instances: temporarySelectedInstances,
multiSelect: false,
layersVisibility: this._getLayersVisibility(),
});
}
};
@@ -622,6 +662,7 @@ export default class InstancesEditor extends Component<Props> {
});
instancesSelected = this.props.instancesSelection.getSelectedInstances();
this.props.onInstancesSelected(instancesSelected);
this.instancesTemporarySelection.clearSelection();
}
};
@@ -946,6 +987,7 @@ export default class InstancesEditor extends Component<Props> {
this.instancesRenderer.render();
this.highlightedInstance.render();
this.selectedInstances.render();
this.temporarySelectedInstances.render();
this.selectionRectangle.render();
this.windowBorder.render();
this.windowMask.render();

View File

@@ -316,6 +316,55 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
[copyObject, deleteObject]
);
const addSerializedObjectToObjectsContainer = React.useCallback(
({
objectName,
positionObjectName,
objectType,
global,
serializedObject,
}: {|
objectName: string,
positionObjectName: string,
objectType: string,
global: boolean,
serializedObject: Object,
|}): ObjectWithContext => {
const newName = newNameGenerator(
objectName,
name =>
objectsContainer.hasObjectNamed(name) ||
project.hasObjectNamed(name),
''
);
const newObject = global
? project.insertNewObject(
project,
objectType,
newName,
project.getObjectPosition(positionObjectName) + 1
)
: objectsContainer.insertNewObject(
project,
objectType,
newName,
objectsContainer.getObjectPosition(positionObjectName) + 1
);
unserializeFromJSObject(
newObject,
serializedObject,
'unserializeFrom',
project
);
newObject.setName(newName); // Unserialization has overwritten the name.
return { object: newObject, global };
},
[objectsContainer, project]
);
const paste = React.useCallback(
(objectWithContext: ObjectWithContext): ?ObjectWithContext => {
if (!Clipboard.has(CLIPBOARD_KIND)) return null;
@@ -336,42 +385,20 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
);
if (!name || !type || !copiedObject) return;
const newName = newNameGenerator(
name,
name =>
objectsContainer.hasObjectNamed(name) ||
project.hasObjectNamed(name),
''
);
const newObject = global
? project.insertNewObject(
project,
type,
newName,
project.getObjectPosition(pasteObject.getName()) + 1
)
: objectsContainer.insertNewObject(
project,
type,
newName,
objectsContainer.getObjectPosition(pasteObject.getName()) + 1
);
unserializeFromJSObject(
newObject,
copiedObject,
'unserializeFrom',
project
);
newObject.setName(newName); // Unserialization has overwritten the name.
const newObjectWithContext = addSerializedObjectToObjectsContainer({
objectName: name,
positionObjectName: pasteObject.getName(),
objectType: type,
serializedObject: copiedObject,
global,
});
onObjectModified(false);
if (onObjectPasted) onObjectPasted(newObject);
if (onObjectPasted) onObjectPasted(newObjectWithContext.object);
return { object: newObject, global };
return newObjectWithContext;
},
[objectsContainer, onObjectModified, onObjectPasted, project]
[addSerializedObjectToObjectsContainer, onObjectModified, onObjectPasted]
);
const editName = React.useCallback(
@@ -383,19 +410,25 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
[]
);
const pasteAndRename = React.useCallback(
(objectWithContext: ObjectWithContext) => {
editName(paste(objectWithContext));
},
[editName, paste]
);
const duplicateObject = React.useCallback(
(objectWithContext: ObjectWithContext) => {
copyObject(objectWithContext);
pasteAndRename(objectWithContext);
const { object, global } = objectWithContext;
const type = object.getType();
const name = object.getName();
const serializedObject = serializeToJSObject(object);
const newObjectWithContext = addSerializedObjectToObjectsContainer({
objectName: name,
positionObjectName: name,
objectType: type,
serializedObject,
global,
});
editName(newObjectWithContext);
},
[copyObject, pasteAndRename]
[addSerializedObjectToObjectsContainer, editName]
);
const rename = React.useCallback(

View File

@@ -24,10 +24,7 @@ import SetupGridDialog from './SetupGridDialog';
import ScenePropertiesDialog from './ScenePropertiesDialog';
import { type ObjectEditorTab } from '../ObjectEditor/ObjectEditorDialog';
import Toolbar from './Toolbar';
import {
serializeToJSObject,
unserializeFromJSObject,
} from '../Utils/Serializer';
import { serializeToJSObject } from '../Utils/Serializer';
import Clipboard, { SafeExtractor } from '../Utils/Clipboard';
import Window from '../Utils/Window';
import FullSizeInstancesEditorWithScrollbars from '../InstancesEditor/FullSizeInstancesEditorWithScrollbars';
@@ -157,7 +154,10 @@ type State = {|
selectedObjectTags: SelectedTags,
|};
type CopyCutPasteOptions = { useLastCursorPosition?: boolean };
type CopyCutPasteOptions = {|
useLastCursorPosition?: boolean,
pasteInTheForeground?: boolean,
|};
export default class SceneEditor extends React.Component<Props, State> {
static defaultProps = {
@@ -1126,7 +1126,10 @@ export default class SceneEditor extends React.Component<Props, State> {
return contextMenuItems;
};
copySelection = ({ useLastCursorPosition }: CopyCutPasteOptions = {}) => {
copySelection = ({
useLastCursorPosition,
pasteInTheForeground,
}: CopyCutPasteOptions = {}) => {
const serializedSelection = this.instancesSelection
.getSelectedInstances()
.map(instance => serializeToJSObject(instance));
@@ -1138,33 +1141,29 @@ export default class SceneEditor extends React.Component<Props, State> {
Clipboard.set(INSTANCES_CLIPBOARD_KIND, {
x: position[0],
y: position[1],
pasteInTheForeground: !!pasteInTheForeground,
instances: serializedSelection,
});
}
};
cutSelection = (options: CopyCutPasteOptions = {}) => {
this.copySelection(options);
cutSelection = ({ useLastCursorPosition }: CopyCutPasteOptions = {}) => {
this.copySelection({ useLastCursorPosition, pasteInTheForeground: true });
this.deleteSelection();
};
duplicateSelection = () => {
const { editor } = this;
if (!editor) return;
const serializedSelection = this.instancesSelection
.getSelectedInstances()
.map(instance => serializeToJSObject(instance));
if (!this.editor) return;
const newInstances = serializedSelection.map(serializedInstance => {
const instance = new gd.InitialInstance();
unserializeFromJSObject(instance, serializedInstance);
instance.setX(instance.getX() + 2 * MOVEMENT_BIG_DELTA);
instance.setY(instance.getY() + 2 * MOVEMENT_BIG_DELTA);
const newInstance = this.props.initialInstances
.insertInitialInstance(instance)
.resetPersistentUuid();
instance.delete();
return newInstance;
const newInstances = editor.addSerializedInstances({
position: [0, 0],
copyReferential: [-2 * MOVEMENT_BIG_DELTA, -2 * MOVEMENT_BIG_DELTA],
serializedInstances: serializedSelection,
preventSnapToGrid: true,
});
this._onInstancesAdded(newInstances);
this.instancesSelection.clearSelection();
@@ -1176,11 +1175,12 @@ export default class SceneEditor extends React.Component<Props, State> {
};
paste = ({ useLastCursorPosition }: CopyCutPasteOptions = {}) => {
if (!this.editor) return;
const { editor } = this;
if (!editor) return;
const position = useLastCursorPosition
? this.editor.getLastCursorSceneCoordinates()
: this.editor.getLastContextMenuSceneCoordinates();
? editor.getLastCursorSceneCoordinates()
: editor.getLastContextMenuSceneCoordinates();
const clipboardContent = Clipboard.get(INSTANCES_CLIPBOARD_KIND);
const instancesContent = SafeExtractor.extractArrayProperty(
@@ -1189,19 +1189,20 @@ export default class SceneEditor extends React.Component<Props, State> {
);
const x = SafeExtractor.extractNumberProperty(clipboardContent, 'x');
const y = SafeExtractor.extractNumberProperty(clipboardContent, 'y');
const pasteInTheForeground =
SafeExtractor.extractBooleanProperty(
clipboardContent,
'pasteInTheForeground'
) || false;
if (x === null || y === null || instancesContent === null) return;
const newInstances = instancesContent.map(serializedInstance => {
const instance = new gd.InitialInstance();
unserializeFromJSObject(instance, serializedInstance);
instance.setX(instance.getX() - x + position[0]);
instance.setY(instance.getY() - y + position[1]);
const newInstance = this.props.initialInstances
.insertInitialInstance(instance)
.resetPersistentUuid();
instance.delete();
return newInstance;
const newInstances = editor.addSerializedInstances({
position,
copyReferential: [x, y],
serializedInstances: instancesContent,
addInstancesInTheForeground: pasteInTheForeground,
});
this._onInstancesAdded(newInstances);
this.instancesSelection.clearSelection();
this.instancesSelection.selectInstances({