From 1fa3f59a771ca1a55a4de36f473684a007cc4e24 Mon Sep 17 00:00:00 2001
From: AlexandreS <32449369+AlexandreSi@users.noreply.github.com>
Date: Mon, 23 Sep 2024 17:37:20 +0200
Subject: [PATCH] Add possibility to paint tilemap with a rectangle selection
from the tileset (#6977)
---
.../CompactInstancePropertiesEditor/index.js | 13 +-
.../InstancesEditor/TileMapPaintingPreview.js | 406 +++++++--------
.../src/InstancesEditor/TileSetVisualizer.js | 316 ++++++------
newIDE/app/src/InstancesEditor/index.js | 197 ++++----
.../Editors/SimpleTileMapEditor.js | 13 +-
newIDE/app/src/UI/ScrollView.js | 15 +-
newIDE/app/src/Utils/TileMap.js | 463 ++++++++++++++++++
newIDE/app/src/Utils/TileMap.spec.js | 244 +++++++++
newIDE/app/src/Utils/UseLongTouch.js | 6 +-
9 files changed, 1208 insertions(+), 465 deletions(-)
create mode 100644 newIDE/app/src/Utils/TileMap.js
create mode 100644 newIDE/app/src/Utils/TileMap.spec.js
diff --git a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js
index 142eacc0c4bf..e040aac36b31 100644
--- a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js
+++ b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js
@@ -15,7 +15,7 @@ import IconButton from '../../UI/IconButton';
import { Line, Column, Spacer, marginsSize } from '../../UI/Grid';
import Text from '../../UI/Text';
import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext';
-import ScrollView from '../../UI/ScrollView';
+import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView';
import EventsRootVariablesFinder from '../../Utils/EventsRootVariablesFinder';
import VariablesList, {
type HistoryHandler,
@@ -95,7 +95,7 @@ const CompactInstancePropertiesEditor = ({
onSelectTileMapTile,
}: Props) => {
const forceUpdate = useForceUpdate();
-
+ const scrollViewRef = React.useRef(null);
const instance = instances[0];
/**
* TODO: multiple instances support for variables list. Expected behavior should be:
@@ -105,6 +105,12 @@ const CompactInstancePropertiesEditor = ({
*/
const shouldDisplayVariablesList = instances.length === 1;
+ const onScrollY = React.useCallback(deltaY => {
+ if (scrollViewRef.current) {
+ scrollViewRef.current.scrollBy(deltaY);
+ }
+ }, []);
+
const { object, instanceSchema } = React.useMemo<{|
object?: gdObject,
instanceSchema?: Schema,
@@ -220,6 +226,7 @@ const CompactInstancePropertiesEditor = ({
scope="scene-editor-instance-properties"
>
diff --git a/newIDE/app/src/InstancesEditor/TileMapPaintingPreview.js b/newIDE/app/src/InstancesEditor/TileMapPaintingPreview.js
index 56b530349870..a8e4edd3820c 100644
--- a/newIDE/app/src/InstancesEditor/TileMapPaintingPreview.js
+++ b/newIDE/app/src/InstancesEditor/TileMapPaintingPreview.js
@@ -9,6 +9,12 @@ import RenderedInstance from '../ObjectsRendering/Renderers/RenderedInstance';
import Rendered3DInstance from '../ObjectsRendering/Renderers/Rendered3DInstance';
import { type TileMapTileSelection } from './TileSetVisualizer';
import { AffineTransformation } from '../Utils/AffineTransformation';
+import {
+ getTileSet,
+ getTilesGridCoordinatesFromPointerSceneCoordinates,
+ isTileSetBadlyConfigured,
+ type TileSet,
+} from '../Utils/TileMap';
export const updateSceneToTileMapTransformation = (
instance: gdInitialInstance,
@@ -62,136 +68,6 @@ export const updateSceneToTileMapTransformation = (
return { scaleX, scaleY };
};
-export const getTileSet = (object: gdObject) => {
- const objectConfigurationProperties = object
- .getConfiguration()
- .getProperties();
- const columnCount = parseFloat(
- objectConfigurationProperties.get('columnCount').getValue()
- );
- const rowCount = parseFloat(
- objectConfigurationProperties.get('rowCount').getValue()
- );
- const tileSize = parseFloat(
- objectConfigurationProperties.get('tileSize').getValue()
- );
- const atlasImage = objectConfigurationProperties.get('atlasImage').getValue();
- return { rowCount, columnCount, tileSize, atlasImage };
-};
-
-export const isTileSetBadlyConfigured = ({
- rowCount,
- columnCount,
- tileSize,
- atlasImage,
-}: {|
- rowCount: number,
- columnCount: number,
- tileSize: number,
- atlasImage: string,
-|}) => {
- return (
- !Number.isInteger(columnCount) ||
- columnCount <= 0 ||
- !Number.isInteger(rowCount) ||
- rowCount <= 0
- );
-};
-
-/**
- * Returns the list of tiles corresponding to the user selection.
- * If only one coordinate is present, only one tile is placed at the slot the
- * pointer points to.
- * If two coordinates are present, tiles are displayed to form a rectangle with the
- * two coordinates being the top left and bottom right corner of the rectangle.
- */
-export const getTilesGridCoordinatesFromPointerSceneCoordinates = ({
- coordinates,
- tileSize,
- sceneToTileMapTransformation,
-}: {|
- coordinates: Array<{| x: number, y: number |}>,
- tileSize: number,
- sceneToTileMapTransformation: AffineTransformation,
-|}): Array<{| x: number, y: number |}> => {
- if (coordinates.length === 0) return [];
-
- const tilesCoordinatesInTileMapGrid = [];
-
- if (coordinates.length === 1) {
- const coordinatesInTileMapGrid = [0, 0];
- sceneToTileMapTransformation.transform(
- [coordinates[0].x, coordinates[0].y],
- coordinatesInTileMapGrid
- );
- coordinatesInTileMapGrid[0] = Math.floor(
- coordinatesInTileMapGrid[0] / tileSize
- );
- coordinatesInTileMapGrid[1] = Math.floor(
- coordinatesInTileMapGrid[1] / tileSize
- );
- tilesCoordinatesInTileMapGrid.push({
- x: coordinatesInTileMapGrid[0],
- y: coordinatesInTileMapGrid[1],
- });
- }
- if (coordinates.length === 2) {
- const firstPointCoordinatesInTileMap = [0, 0];
- sceneToTileMapTransformation.transform(
- [coordinates[0].x, coordinates[0].y],
- firstPointCoordinatesInTileMap
- );
- const secondPointCoordinatesInTileMap = [0, 0];
- sceneToTileMapTransformation.transform(
- [coordinates[1].x, coordinates[1].y],
- secondPointCoordinatesInTileMap
- );
- const topLeftCornerCoordinatesInTileMap = [
- Math.min(
- firstPointCoordinatesInTileMap[0],
- secondPointCoordinatesInTileMap[0]
- ),
- Math.min(
- firstPointCoordinatesInTileMap[1],
- secondPointCoordinatesInTileMap[1]
- ),
- ];
- const bottomRightCornerCoordinatesInTileMap = [
- Math.max(
- firstPointCoordinatesInTileMap[0],
- secondPointCoordinatesInTileMap[0]
- ),
- Math.max(
- firstPointCoordinatesInTileMap[1],
- secondPointCoordinatesInTileMap[1]
- ),
- ];
- const topLeftCornerCoordinatesInTileMapGrid = [
- Math.floor(topLeftCornerCoordinatesInTileMap[0] / tileSize),
- Math.floor(topLeftCornerCoordinatesInTileMap[1] / tileSize),
- ];
- const bottomRightCornerCoordinatesInTileMapGrid = [
- Math.floor(bottomRightCornerCoordinatesInTileMap[0] / tileSize),
- Math.floor(bottomRightCornerCoordinatesInTileMap[1] / tileSize),
- ];
-
- for (
- let columnIndex = topLeftCornerCoordinatesInTileMapGrid[0];
- columnIndex <= bottomRightCornerCoordinatesInTileMapGrid[0];
- columnIndex++
- ) {
- for (
- let rowIndex = topLeftCornerCoordinatesInTileMapGrid[1];
- rowIndex <= bottomRightCornerCoordinatesInTileMapGrid[1];
- rowIndex++
- ) {
- tilesCoordinatesInTileMapGrid.push({ x: columnIndex, y: rowIndex });
- }
- }
- }
- return tilesCoordinatesInTileMapGrid;
-};
-
type Props = {|
project: gdProject,
layout: gdLayout | null,
@@ -249,72 +125,100 @@ class TileMapPaintingPreview {
return this.preview;
}
- render() {
- this.preview.removeChildren(0);
- const tileMapTileSelection = this.getTileMapTileSelection();
- if (!tileMapTileSelection) {
- return;
- }
- const selection = this.instancesSelection.getSelectedInstances();
- if (selection.length !== 1) return;
- const instance = selection[0];
- const associatedObjectName = instance.getObjectName();
- const object = getObjectByName(
- this.project.getObjects(),
- this.layout ? this.layout.getObjects() : null,
- associatedObjectName
+ _getTextureInAtlas({
+ tileSet,
+ x,
+ y,
+ }: {
+ tileSet: TileSet,
+ x: number,
+ y: number,
+ }): ?PIXI.Texture {
+ const { atlasImage, tileSize } = tileSet;
+ if (!atlasImage) return;
+ const cacheKey = `${atlasImage}-${tileSize}-${x}-${y}`;
+ const cachedTexture = this.cache.get(cacheKey);
+ if (cachedTexture) return cachedTexture;
+
+ const atlasTexture = PixiResourcesLoader.getPIXITexture(
+ this.project,
+ atlasImage
);
- if (!object || object.getType() !== 'TileMap::SimpleTileMap') return;
- const tileSet = getTileSet(object);
- const isBadlyConfigured = isTileSetBadlyConfigured(tileSet);
- const { tileSize } = tileSet;
- let texture;
- if (isBadlyConfigured) {
- texture = PixiResourcesLoader.getInvalidPIXITexture();
- } else {
- if (tileMapTileSelection.kind === 'single') {
- const atlasResourceName = object
- .getConfiguration()
- .getProperties()
- .get('atlasImage')
- .getValue();
- if (!atlasResourceName) return;
- const cacheKey = `${atlasResourceName}-${tileSize}-${
- tileMapTileSelection.coordinates.x
- }-${tileMapTileSelection.coordinates.y}`;
- texture = this.cache.get(cacheKey);
- if (!texture) {
- const atlasTexture = PixiResourcesLoader.getPIXITexture(
- this.project,
- atlasResourceName
- );
- const rect = new PIXI.Rectangle(
- tileMapTileSelection.coordinates.x * tileSize,
- tileMapTileSelection.coordinates.y * tileSize,
- tileSize,
- tileSize
- );
+ const rect = new PIXI.Rectangle(
+ x * tileSize,
+ y * tileSize,
+ tileSize,
+ tileSize
+ );
- try {
- texture = new PIXI.Texture(atlasTexture, rect);
- } catch (error) {
- console.error(
- `Tile could not be extracted from atlas texture:`,
- error
- );
- texture = PixiResourcesLoader.getInvalidPIXITexture();
- }
- this.cache.set(cacheKey, texture);
- }
- } else if (tileMapTileSelection.kind === 'erase') {
- texture = PIXI.Texture.from(
- 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAARSURBVHgBY7h58+Z/BhgAcQA/VAcVLiw46wAAAABJRU5ErkJggg=='
- );
- texture.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST;
- }
+ try {
+ const texture = new PIXI.Texture(atlasTexture, rect);
+ this.cache.set(cacheKey, texture);
+ } catch (error) {
+ console.error(`Tile could not be extracted from atlas texture:`, error);
+ return PixiResourcesLoader.getInvalidPIXITexture();
}
+ }
+
+ _getTilingSpriteForRectangle({
+ bottomRightCorner,
+ topLeftCorner,
+ texture,
+ scaleX,
+ scaleY,
+ flipHorizontally,
+ flipVertically,
+ tileSize,
+ angle,
+ }: {|
+ bottomRightCorner: {| x: number, y: number |},
+ topLeftCorner: {| x: number, y: number |},
+ scaleX: number,
+ scaleY: number,
+ tileSize: number,
+ flipHorizontally: boolean,
+ flipVertically: boolean,
+ angle: number,
+ texture: PIXI.Texture,
+ |}) {
+ const sprite = new PIXI.TilingSprite(texture);
+ const workingPoint = [0, 0];
+ sprite.tileScale.x =
+ (flipHorizontally ? -1 : +1) * this.viewPosition.toCanvasScale(scaleX);
+ sprite.tileScale.y =
+ (flipVertically ? -1 : +1) * this.viewPosition.toCanvasScale(scaleY);
+
+ this.tileMapToSceneTransformation.transform(
+ [topLeftCorner.x * tileSize, topLeftCorner.y * tileSize],
+ workingPoint
+ );
+ const tileSizeInCanvas = this.viewPosition.toCanvasScale(tileSize);
+
+ sprite.x = this.viewPosition.toCanvasScale(workingPoint[0]);
+ sprite.y = this.viewPosition.toCanvasScale(workingPoint[1]);
+ sprite.width =
+ (bottomRightCorner.x - topLeftCorner.x + 1) * tileSizeInCanvas * scaleX;
+ sprite.height =
+ (bottomRightCorner.y - topLeftCorner.y + 1) * tileSizeInCanvas * scaleY;
+
+ sprite.angle = angle;
+
+ return sprite;
+ }
+
+ _getPreviewSprites({
+ instance,
+ tileSet,
+ isBadlyConfigured,
+ tileMapTileSelection,
+ }: {
+ instance: gdInitialInstance,
+ tileSet: TileSet,
+ isBadlyConfigured: boolean,
+ tileMapTileSelection: TileMapTileSelection,
+ }): ?PIXI.Container {
const renderedInstance = this.getRendererOfInstance(instance);
if (
!renderedInstance ||
@@ -324,7 +228,7 @@ class TileMapPaintingPreview {
console.error(
`Instance of ${instance.getObjectName()} seems to not be a RenderedSimpleTileMapInstance (method getEditableTileMap does not exist).`
);
- return;
+ return null;
}
const scales = updateSceneToTileMapTransformation(
@@ -334,65 +238,97 @@ class TileMapPaintingPreview {
this.sceneToTileMapTransformation,
this.tileMapToSceneTransformation
);
- if (!scales) return;
+ if (!scales) return null;
const { scaleX, scaleY } = scales;
const coordinates = this.getCoordinatesToRender();
- if (coordinates.length === 0) return;
- const tileSizeInCanvas = this.viewPosition.toCanvasScale(tileSize);
- const spriteWidth = tileSizeInCanvas * scaleX;
- const spriteHeight = tileSizeInCanvas * scaleY;
+ if (coordinates.length === 0) return null;
+ const { tileSize } = tileSet;
- const spritesCoordinatesInTileMapGrid = getTilesGridCoordinatesFromPointerSceneCoordinates(
+ const tilesCoordinatesInTileMapGrid = getTilesGridCoordinatesFromPointerSceneCoordinates(
{
+ tileMapTileSelection,
coordinates,
tileSize,
sceneToTileMapTransformation: this.sceneToTileMapTransformation,
}
);
- if (spritesCoordinatesInTileMapGrid.length === 0) {
+ if (tilesCoordinatesInTileMapGrid.length === 0) {
console.warn("Could't get coordinates to render in tile map grid.");
- return;
+ return null;
}
+ const container = new PIXI.Container();
+ tilesCoordinatesInTileMapGrid.forEach(tilesCoordinates => {
+ const {
+ bottomRightCorner,
+ topLeftCorner,
+ tileCoordinates,
+ } = tilesCoordinates;
+ let texture;
+ if (isBadlyConfigured) {
+ texture = PixiResourcesLoader.getInvalidPIXITexture();
+ } else {
+ if (tileMapTileSelection.kind === 'rectangle' && tileCoordinates) {
+ texture = this._getTextureInAtlas({
+ tileSet,
+ ...tileCoordinates,
+ });
+ if (!texture) return null;
+ } else if (tileMapTileSelection.kind === 'erase') {
+ texture = PIXI.Texture.from(
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAARSURBVHgBY7h58+Z/BhgAcQA/VAcVLiw46wAAAABJRU5ErkJggg=='
+ );
+ texture.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST;
+ }
+ }
+ const sprite = this._getTilingSpriteForRectangle({
+ bottomRightCorner,
+ topLeftCorner,
+ texture,
+ scaleX,
+ scaleY,
+ flipHorizontally: tileMapTileSelection.flipHorizontally || false,
+ flipVertically: tileMapTileSelection.flipVertically || false,
+ tileSize,
+ angle: instance.getAngle(),
+ });
+ container.addChild(sprite);
+ });
- const workingPoint = [0, 0];
-
- const sprite = new PIXI.TilingSprite(texture);
-
- sprite.tileScale.x =
- (tileMapTileSelection.flipHorizontally ? -1 : +1) *
- this.viewPosition.toCanvasScale(scaleX);
- sprite.tileScale.y =
- (tileMapTileSelection.flipVertically ? -1 : +1) *
- this.viewPosition.toCanvasScale(scaleY);
- sprite.width = spriteWidth;
- sprite.height = spriteHeight;
+ return container;
+ }
- let minX = Infinity;
- let minY = Infinity;
- let maxX = -Infinity;
- let maxY = -Infinity;
- for (const { x, y } of spritesCoordinatesInTileMapGrid) {
- if (x < minX) minX = x;
- if (y < minY) minY = y;
- if (x > maxX) maxX = x;
- if (y > maxY) maxY = y;
+ render() {
+ this.preview.removeChildren(0);
+ const tileMapTileSelection = this.getTileMapTileSelection();
+ if (!tileMapTileSelection) {
+ return;
}
-
- this.tileMapToSceneTransformation.transform(
- [minX * tileSize, minY * tileSize],
- workingPoint
+ const selection = this.instancesSelection.getSelectedInstances();
+ if (selection.length !== 1) return;
+ const instance = selection[0];
+ const associatedObjectName = instance.getObjectName();
+ const object = getObjectByName(
+ this.project.getObjects(),
+ this.layout ? this.layout.getObjects() : null,
+ associatedObjectName
);
+ if (!object || object.getType() !== 'TileMap::SimpleTileMap') return;
+ const tileSet = getTileSet(object);
+ const isBadlyConfigured = isTileSetBadlyConfigured(tileSet);
- sprite.x = this.viewPosition.toCanvasScale(workingPoint[0]);
- sprite.y = this.viewPosition.toCanvasScale(workingPoint[1]);
- sprite.width =
- (maxX - minX + 1) * this.viewPosition.toCanvasScale(tileSize) * scaleX;
- sprite.height =
- (maxY - minY + 1) * this.viewPosition.toCanvasScale(tileSize) * scaleY;
-
- sprite.angle = instance.getAngle();
-
- this.preview.addChild(sprite);
+ if (
+ isBadlyConfigured ||
+ tileMapTileSelection.kind === 'rectangle' ||
+ tileMapTileSelection.kind === 'erase'
+ ) {
+ const container = this._getPreviewSprites({
+ instance,
+ tileSet,
+ tileMapTileSelection,
+ isBadlyConfigured,
+ });
+ if (container) this.preview.addChild(container);
+ }
const canvasCoordinates = this.viewPosition.toCanvasCoordinates(0, 0);
this.preview.position.x = canvasCoordinates[0];
diff --git a/newIDE/app/src/InstancesEditor/TileSetVisualizer.js b/newIDE/app/src/InstancesEditor/TileSetVisualizer.js
index ee98eb1913a1..1da3f02bcfb0 100644
--- a/newIDE/app/src/InstancesEditor/TileSetVisualizer.js
+++ b/newIDE/app/src/InstancesEditor/TileSetVisualizer.js
@@ -16,7 +16,7 @@ import useForceUpdate from '../Utils/UseForceUpdate';
import { useLongTouch, type ClientCoordinates } from '../Utils/UseLongTouch';
import Text from '../UI/Text';
import EmptyMessage from '../UI/EmptyMessage';
-import { isTileSetBadlyConfigured } from './TileMapPaintingPreview';
+import { isTileSetBadlyConfigured } from '../Utils/TileMap';
const styles = {
tilesetAndTooltipsContainer: {
@@ -30,6 +30,7 @@ const styles = {
position: 'relative',
display: 'flex',
overflow: 'auto',
+ touchAction: 'none',
},
atlasImage: { flex: 1, imageRendering: 'pixelated' },
icon: { fontSize: 18 },
@@ -64,12 +65,12 @@ type TileMapCoordinates = {| x: number, y: number |};
/**
* Returns the tile id in a tile set.
* This id corresponds to the index of the tile if the tile set
- * is flattened so that each column is put right after the previous one.
+ * is flattened so that each row is put right after the previous one.
* Example:
- * 1 | 4 | 7
- * 2 | 5 | 8
- * 3 | 6 | 9
- * @param argument Object that contains x the horizontal position of the tile, y the vertical position and rowCount the number of rows in the tile set.
+ * 0 | 1 | 2
+ * 3 | 4 | 5
+ * 6 | 7 | 8
+ * @param argument Object that contains x the horizontal position of the tile, y the vertical position and columnCount the number of columns in the tile set.
* @returns the id of the tile.
*/
export const getTileIdFromGridCoordinates = ({
@@ -85,12 +86,12 @@ export const getTileIdFromGridCoordinates = ({
/**
* Returns the coordinates of a tile in a tile set given its id.
* This id corresponds to the index of the tile if the tile set
- * is flattened so that each column is put right after the previous one.
+ * is flattened so that each row is put right after the previous one.
* Example:
- * 1 | 4 | 7
- * 2 | 5 | 8
- * 3 | 6 | 9
- * @param argument Object that contains id the id of the tile and rowCount the number of rows in the tile set.
+ * 0 | 1 | 2
+ * 3 | 4 | 5
+ * 6 | 7 | 8
+ * @param argument Object that contains the id of the tile and columnCount the number of columns in the tile set.
* @returns the id of the tile.
*/
export const getGridCoordinatesFromTileId = ({
@@ -191,14 +192,14 @@ const Tile = ({
export type TileMapTileSelection =
| {|
- kind: 'single',
- coordinates: TileMapCoordinates,
- flipHorizontally: boolean,
- flipVertically: boolean,
+ kind: 'multiple',
+ coordinates: TileMapCoordinates[],
|}
| {|
- kind: 'multiple',
+ kind: 'rectangle',
coordinates: TileMapCoordinates[],
+ flipHorizontally: boolean,
+ flipVertically: boolean,
|}
| {|
kind: 'erase',
@@ -210,12 +211,18 @@ type Props = {|
tileMapTileSelection: ?TileMapTileSelection,
onSelectTileMapTile: (?TileMapTileSelection) => void,
allowMultipleSelection: boolean,
+ allowRectangleSelection: boolean,
showPaintingToolbar: boolean,
interactive: boolean,
onAtlasImageLoaded?: (
e: SyntheticEvent,
atlasResourceName: string
) => void,
+ /**
+ * Needed to enable scrolling on touch devices when the user is not using
+ * a long touch to make a tile selection on the tile set.
+ */
+ onScrollY: number => void,
|};
const TileSetVisualizer = ({
@@ -224,9 +231,11 @@ const TileSetVisualizer = ({
tileMapTileSelection,
onSelectTileMapTile,
allowMultipleSelection,
+ allowRectangleSelection,
showPaintingToolbar,
interactive,
onAtlasImageLoaded,
+ onScrollY,
}: Props) => {
const forceUpdate = useForceUpdate();
const atlasResourceName = objectConfiguration
@@ -242,9 +251,9 @@ const TileSetVisualizer = ({
setShouldFlipHorizontally,
] = React.useState(false);
const [
- lastSelectedTile,
- setLastSelectedTile,
- ] = React.useState(null);
+ lastSelection,
+ setLastSelection,
+ ] = React.useState(null);
const tilesetContainerRef = React.useRef(null);
const tilesetAndTooltipContainerRef = React.useRef(null);
const [tooltipContent, setTooltipContent] = React.useState{|
@@ -268,17 +277,11 @@ const TileSetVisualizer = ({
rowCount,
tileSize,
});
- const [clickStartCoordinates, setClickStartCoordinates] = React.useState{|
+ const startCoordinatesRef = React.useRef{|
x: number,
y: number,
|}>(null);
- const [touchStartCoordinates, setTouchStartCoordinates] = React.useState{|
- x: number,
- y: number,
- |}>(null);
- const [shouldCancelClick, setShouldCancelClick] = React.useState(
- false
- );
+ const isLongTouchRef = React.useRef(false);
const tooltipDisplayTimeoutId = React.useRef(null);
const [
rectangularSelectionTilePreview,
@@ -314,7 +317,6 @@ const TileSetVisualizer = ({
const displayTileIdTooltip = React.useCallback(
(e: ClientCoordinates) => {
- setShouldCancelClick(true);
if (!displayedTileSize || isBadlyConfigured) return;
const imageCoordinates = getImageCoordinatesFromPointerEvent(e);
@@ -339,7 +341,17 @@ const TileSetVisualizer = ({
[displayedTileSize, columnCount, rowCount, isBadlyConfigured]
);
- const longTouchProps = useLongTouch(displayTileIdTooltip);
+ const handleLongTouch = React.useCallback(
+ (e: ClientCoordinates) => {
+ isLongTouchRef.current = true;
+ displayTileIdTooltip(e);
+ },
+ [displayTileIdTooltip]
+ );
+
+ const longTouchProps = useLongTouch(handleLongTouch, {
+ doNotCancelOnScroll: true,
+ });
React.useEffect(
() => {
@@ -353,15 +365,12 @@ const TileSetVisualizer = ({
const onPointerDown = React.useCallback(
(event: PointerEvent) => {
if (isBadlyConfigured) return;
- if (event.pointerType === 'touch') {
- setTouchStartCoordinates({ x: event.pageX, y: event.pageY });
- }
- const imageCoordinates = getImageCoordinatesFromPointerEvent(event);
- if (!imageCoordinates) return;
- setClickStartCoordinates({
- x: imageCoordinates.mouseX,
- y: imageCoordinates.mouseY,
- });
+ const coordinates = getImageCoordinatesFromPointerEvent(event);
+ if (!coordinates) return;
+ startCoordinatesRef.current = {
+ x: coordinates.mouseX,
+ y: coordinates.mouseY,
+ };
},
[isBadlyConfigured]
);
@@ -370,13 +379,33 @@ const TileSetVisualizer = ({
(event: PointerEvent) => {
if (
isBadlyConfigured ||
- !clickStartCoordinates ||
+ !startCoordinatesRef ||
!displayedTileSize ||
- !allowMultipleSelection ||
- event.pointerType === 'touch'
+ (!allowMultipleSelection && !allowRectangleSelection)
) {
return;
}
+
+ const startCoordinates = startCoordinatesRef.current;
+ if (!startCoordinates) return;
+
+ const isTouchDevice = event.pointerType === 'touch';
+
+ if (isTouchDevice) {
+ // Distinguish between a long touch (to multi select tiles) and a scroll.
+ if (!isLongTouchRef.current) {
+ const coordinates = getImageCoordinatesFromPointerEvent(event);
+ if (!coordinates) return;
+ if (tilesetContainerRef.current) {
+ const deltaY = -event.movementY;
+ const deltaX =
+ startCoordinates.x - coordinates.mouseXWithoutScrollLeft;
+ tilesetContainerRef.current.scrollLeft = deltaX;
+ onScrollY(deltaY);
+ }
+ return;
+ }
+ }
const imageCoordinates = getImageCoordinatesFromPointerEvent(event);
if (!imageCoordinates) return;
@@ -387,10 +416,11 @@ const TileSetVisualizer = ({
rowCount,
displayedTileSize,
});
+
const { x: startX, y: startY } = getGridCoordinatesFromPointerCoordinates(
{
- pointerX: clickStartCoordinates.x,
- pointerY: clickStartCoordinates.y,
+ pointerX: startCoordinates.x,
+ pointerY: startCoordinates.y,
columnCount,
rowCount,
displayedTileSize,
@@ -412,7 +442,8 @@ const TileSetVisualizer = ({
columnCount,
rowCount,
allowMultipleSelection,
- clickStartCoordinates,
+ allowRectangleSelection,
+ onScrollY,
]
);
@@ -420,22 +451,13 @@ const TileSetVisualizer = ({
(event: PointerEvent) => {
try {
if (!displayedTileSize || isBadlyConfigured) return;
- if (shouldCancelClick) {
- setShouldCancelClick(false);
- return;
- }
- let isTouchDevice = false;
+ const isTouchDevice = event.pointerType === 'touch';
+ const startCoordinates = startCoordinatesRef.current;
+ if (!startCoordinates) return;
- if (event.pointerType === 'touch') {
- isTouchDevice = true;
- if (
- !touchStartCoordinates ||
- Math.abs(event.pageX - touchStartCoordinates.x) > 30 ||
- Math.abs(event.pageY - touchStartCoordinates.y) > 30
- ) {
- return;
- }
+ if (isTouchDevice && !isLongTouchRef.current) {
+ return;
}
const imageCoordinates = getImageCoordinatesFromPointerEvent(event);
@@ -448,80 +470,89 @@ const TileSetVisualizer = ({
rowCount,
displayedTileSize,
});
- if (!allowMultipleSelection) {
- if (
- tileMapTileSelection &&
- tileMapTileSelection.kind === 'single' &&
- tileMapTileSelection.coordinates.x === x &&
- tileMapTileSelection.coordinates.y === y
- ) {
- onSelectTileMapTile(null);
- } else {
- onSelectTileMapTile({
- kind: 'single',
- coordinates: { x, y },
- flipHorizontally: shouldFlipHorizontally,
- flipVertically: shouldFlipVertically,
- });
- }
- return;
- }
- if (!clickStartCoordinates) return;
+ if (!startCoordinates) return;
const {
x: startX,
y: startY,
} = getGridCoordinatesFromPointerCoordinates({
- pointerX: clickStartCoordinates.x,
- pointerY: clickStartCoordinates.y,
+ pointerX: startCoordinates.x,
+ pointerY: startCoordinates.y,
columnCount,
rowCount,
displayedTileSize,
});
- const newSelection =
- tileMapTileSelection && tileMapTileSelection.kind === 'multiple'
- ? { ...tileMapTileSelection }
- : { kind: 'multiple', coordinates: [] };
- // Click on a tile.
- if (
- (startX === x && startY === y) ||
- // Do not allow rectangular select on touch device as it conflicts with basic scrolling gestures.
- isTouchDevice
- ) {
- if (
- tileMapTileSelection &&
- tileMapTileSelection.kind === 'multiple'
- ) {
- addOrRemoveCoordinatesInArray(newSelection.coordinates, {
- x,
- y,
- });
- }
- } else {
- for (
- let columnIndex = Math.min(startX, x);
- columnIndex <= Math.max(startX, x);
- columnIndex++
- ) {
+ if (allowMultipleSelection) {
+ const newSelection =
+ tileMapTileSelection && tileMapTileSelection.kind === 'multiple'
+ ? { ...tileMapTileSelection }
+ : { kind: 'multiple', coordinates: [] };
+ if (startX === x && startY === y) {
+ if (
+ tileMapTileSelection &&
+ tileMapTileSelection.kind === 'multiple'
+ ) {
+ addOrRemoveCoordinatesInArray(newSelection.coordinates, {
+ x,
+ y,
+ });
+ }
+ } else {
for (
- let rowIndex = Math.min(startY, y);
- rowIndex <= Math.max(startY, y);
- rowIndex++
+ let columnIndex = Math.min(startX, x);
+ columnIndex <= Math.max(startX, x);
+ columnIndex++
) {
- if (newSelection && newSelection.kind === 'multiple') {
- addOrRemoveCoordinatesInArray(newSelection.coordinates, {
- x: columnIndex,
- y: rowIndex,
- });
+ for (
+ let rowIndex = Math.min(startY, y);
+ rowIndex <= Math.max(startY, y);
+ rowIndex++
+ ) {
+ if (newSelection && newSelection.kind === 'multiple') {
+ addOrRemoveCoordinatesInArray(newSelection.coordinates, {
+ x: columnIndex,
+ y: rowIndex,
+ });
+ }
}
}
}
+ onSelectTileMapTile(newSelection);
+ } else if (allowRectangleSelection) {
+ const shouldRemoveSelection =
+ tileMapTileSelection &&
+ tileMapTileSelection.kind === 'rectangle' &&
+ startX === x &&
+ startY === y &&
+ x <= tileMapTileSelection.coordinates[1].x &&
+ x >= tileMapTileSelection.coordinates[0].x &&
+ y <= tileMapTileSelection.coordinates[1].y &&
+ y >= tileMapTileSelection.coordinates[0].y;
+ if (shouldRemoveSelection) {
+ // Remove selection when user selects a single tile in the current tile selection.
+ onSelectTileMapTile(null);
+ } else {
+ const topLeftCorner = {
+ x: Math.min(startX, x),
+ y: Math.min(startY, y),
+ };
+ const bottomRightCorner = {
+ x: Math.max(startX, x),
+ y: Math.max(startY, y),
+ };
+ const newSelection = {
+ kind: 'rectangle',
+ coordinates: [topLeftCorner, bottomRightCorner],
+ flipHorizontally: shouldFlipHorizontally,
+ flipVertically: shouldFlipVertically,
+ };
+ onSelectTileMapTile(newSelection);
+ }
}
- onSelectTileMapTile(newSelection);
} finally {
- setClickStartCoordinates(null);
+ startCoordinatesRef.current = null;
setRectangularSelectionTilePreview(null);
- setTouchStartCoordinates(null);
+ isLongTouchRef.current = false;
}
},
[
@@ -534,19 +565,14 @@ const TileSetVisualizer = ({
shouldFlipHorizontally,
shouldFlipVertically,
allowMultipleSelection,
- clickStartCoordinates,
- shouldCancelClick,
- touchStartCoordinates,
+ allowRectangleSelection,
]
);
React.useEffect(
() => {
- if (tileMapTileSelection && tileMapTileSelection.kind === 'single') {
- setLastSelectedTile({
- x: tileMapTileSelection.coordinates.x,
- y: tileMapTileSelection.coordinates.y,
- });
+ if (tileMapTileSelection && tileMapTileSelection.kind === 'rectangle') {
+ setLastSelection(tileMapTileSelection);
}
},
[tileMapTileSelection]
@@ -651,21 +677,23 @@ const TileSetVisualizer = ({
tooltip={t`Paint`}
selected={
!!tileMapTileSelection &&
- tileMapTileSelection.kind === 'single'
+ tileMapTileSelection.kind === 'rectangle'
}
onClick={e => {
if (
!!tileMapTileSelection &&
- tileMapTileSelection.kind === 'single'
+ tileMapTileSelection.kind === 'rectangle'
)
onSelectTileMapTile(null);
else
- onSelectTileMapTile({
- kind: 'single',
- coordinates: lastSelectedTile || { x: 0, y: 0 },
- flipHorizontally: shouldFlipHorizontally,
- flipVertically: shouldFlipVertically,
- });
+ onSelectTileMapTile(
+ lastSelection || {
+ kind: 'rectangle',
+ coordinates: [{ x: 0, y: 0 }, { x: 0, y: 0 }],
+ flipHorizontally: shouldFlipHorizontally,
+ flipVertically: shouldFlipVertically,
+ }
+ );
}}
disabled={!isAtlasImageSet}
>
@@ -676,15 +704,14 @@ const TileSetVisualizer = ({
tooltip={t`Horizontal flip`}
selected={shouldFlipHorizontally}
disabled={
- !tileMapTileSelection ||
- tileMapTileSelection.kind !== 'single'
+ !tileMapTileSelection || tileMapTileSelection.kind === 'erase'
}
onClick={e => {
const newShouldFlipHorizontally = !shouldFlipHorizontally;
setShouldFlipHorizontally(newShouldFlipHorizontally);
if (
!!tileMapTileSelection &&
- tileMapTileSelection.kind === 'single'
+ tileMapTileSelection.kind === 'rectangle'
) {
onSelectTileMapTile({
...tileMapTileSelection,
@@ -700,15 +727,14 @@ const TileSetVisualizer = ({
tooltip={t`Vertical flip`}
selected={shouldFlipVertically}
disabled={
- !tileMapTileSelection ||
- tileMapTileSelection.kind !== 'single'
+ !tileMapTileSelection || tileMapTileSelection.kind === 'erase'
}
onClick={e => {
const newShouldFlipVertically = !shouldFlipVertically;
setShouldFlipVertically(newShouldFlipVertically);
if (
!!tileMapTileSelection &&
- tileMapTileSelection.kind === 'single'
+ tileMapTileSelection.kind === 'rectangle'
) {
onSelectTileMapTile({
...tileMapTileSelection,
@@ -776,14 +802,24 @@ const TileSetVisualizer = ({
/>
)}
{tileMapTileSelection &&
- tileMapTileSelection.kind === 'single' &&
+ tileMapTileSelection.kind === 'rectangle' &&
displayedTileSize && (
)}
{tileMapTileSelection &&
diff --git a/newIDE/app/src/InstancesEditor/index.js b/newIDE/app/src/InstancesEditor/index.js
index 6ee6d2f99e2b..82ff5a7f1c3d 100644
--- a/newIDE/app/src/InstancesEditor/index.js
+++ b/newIDE/app/src/InstancesEditor/index.js
@@ -43,9 +43,6 @@ import {
} from '../Utils/ZoomUtils';
import Background from './Background';
import TileMapPaintingPreview, {
- getTileSet,
- getTilesGridCoordinatesFromPointerSceneCoordinates,
- isTileSetBadlyConfigured,
updateSceneToTileMapTransformation,
} from './TileMapPaintingPreview';
import {
@@ -58,6 +55,11 @@ import { AffineTransformation } from '../Utils/AffineTransformation';
import { ErrorFallbackComponent } from '../UI/ErrorBoundary';
import { Trans } from '@lingui/macro';
import { generateUUID } from 'three/src/math/MathUtils';
+import {
+ getTilesGridCoordinatesFromPointerSceneCoordinates,
+ getTileSet,
+ isTileSetBadlyConfigured,
+} from '../Utils/TileMap';
const gd: libGDevelop = global.gd;
@@ -839,6 +841,7 @@ export default class InstancesEditor extends Component {
}
const tileMapGridCoordinates = getTilesGridCoordinatesFromPointerSceneCoordinates(
{
+ tileMapTileSelection,
coordinates: sceneCoordinates,
tileSize: tileSet.tileSize,
sceneToTileMapTransformation,
@@ -847,96 +850,126 @@ export default class InstancesEditor extends Component {
let shouldTrimAfterOperations = false;
- if (tileMapTileSelection.kind === 'single') {
+ if (tileMapTileSelection.kind === 'rectangle') {
shouldTrimAfterOperations = editableTileMap.isEmpty();
// TODO: Optimize list execution to make sure the most important size changing operations are done first.
let cumulatedUnshiftedRows = 0,
cumulatedUnshiftedColumns = 0;
- const tileId = getTileIdFromGridCoordinates({
- columnCount: tileSet.columnCount,
- ...tileMapTileSelection.coordinates,
- });
-
- const tileDefinition = editableTileMap.getTileDefinition(tileId);
- if (!tileDefinition) return;
const layer = editableTileMap.getTileLayer(0);
if (!layer) return;
- tileMapGridCoordinates.forEach(({ x: gridX, y: gridY }) => {
- // If rows or columns have been unshifted in the previous tile setting operations,
- // we have to take them into account for the current coordinates.
- const x = gridX + cumulatedUnshiftedColumns;
- const y = gridY + cumulatedUnshiftedRows;
- const rowsToAppend = Math.max(
- 0,
- y - (editableTileMap.getDimensionY() - 1)
- );
- const columnsToAppend = Math.max(
- 0,
- x - (editableTileMap.getDimensionX() - 1)
- );
- const rowsToUnshift = Math.abs(Math.min(0, y));
- const columnsToUnshift = Math.abs(Math.min(0, x));
- if (
- rowsToAppend > 0 ||
- columnsToAppend > 0 ||
- rowsToUnshift > 0 ||
- columnsToUnshift > 0
+ tileMapGridCoordinates.forEach(
+ ({ bottomRightCorner, topLeftCorner, tileCoordinates }) => {
+ if (!tileCoordinates) return;
+ const tileId = getTileIdFromGridCoordinates({
+ columnCount: tileSet.columnCount,
+ ...tileCoordinates,
+ });
+
+ const tileDefinition = editableTileMap.getTileDefinition(tileId);
+ if (!tileDefinition) return;
+
+ for (
+ let gridX = topLeftCorner.x;
+ gridX <= bottomRightCorner.x;
+ gridX++
+ ) {
+ for (
+ let gridY = topLeftCorner.y;
+ gridY <= bottomRightCorner.y;
+ gridY++
+ ) {
+ // If rows or columns have been unshifted in the previous tile setting operations,
+ // we have to take them into account for the current coordinates.
+ const x = gridX + cumulatedUnshiftedColumns;
+ const y = gridY + cumulatedUnshiftedRows;
+ const rowsToAppend = Math.max(
+ 0,
+ y - (editableTileMap.getDimensionY() - 1)
+ );
+ const columnsToAppend = Math.max(
+ 0,
+ x - (editableTileMap.getDimensionX() - 1)
+ );
+ const rowsToUnshift = Math.abs(Math.min(0, y));
+ const columnsToUnshift = Math.abs(Math.min(0, x));
+ if (
+ rowsToAppend > 0 ||
+ columnsToAppend > 0 ||
+ rowsToUnshift > 0 ||
+ columnsToUnshift > 0
+ ) {
+ editableTileMap.increaseDimensions(
+ columnsToAppend,
+ columnsToUnshift,
+ rowsToAppend,
+ rowsToUnshift
+ );
+ }
+ const newX = x + columnsToUnshift;
+ const newY = y + rowsToUnshift;
+
+ editableTileMap.setTile(newX, newY, 0, tileId);
+ editableTileMap.flipTileOnX(
+ newX,
+ newY,
+ 0,
+ tileMapTileSelection.flipHorizontally
+ );
+ editableTileMap.flipTileOnY(
+ newX,
+ newY,
+ 0,
+ tileMapTileSelection.flipVertically
+ );
+
+ cumulatedUnshiftedRows += rowsToUnshift;
+ cumulatedUnshiftedColumns += columnsToUnshift;
+ // The instance angle is not considered when moving the instance after
+ // rows/columns were added/removed because the instance position does not
+ // include the rotation transformation. Otherwise, we could have used
+ // tileMapToSceneTransformation to get the new position.
+ selectedInstance.setX(
+ selectedInstance.getX() -
+ columnsToUnshift * (tileSet.tileSize * scaleX)
+ );
+ selectedInstance.setY(
+ selectedInstance.getY() -
+ rowsToUnshift * (tileSet.tileSize * scaleY)
+ );
+ if (selectedInstance.hasCustomSize()) {
+ selectedInstance.setCustomWidth(
+ selectedInstance.getCustomWidth() +
+ tileSet.tileSize *
+ scaleX *
+ (columnsToAppend + columnsToUnshift)
+ );
+ selectedInstance.setCustomHeight(
+ selectedInstance.getCustomHeight() +
+ tileSet.tileSize * scaleY * (rowsToAppend + rowsToUnshift)
+ );
+ }
+ }
+ }
+ }
+ );
+ } else if (tileMapTileSelection.kind === 'erase') {
+ const { bottomRightCorner, topLeftCorner } = tileMapGridCoordinates[0];
+ for (
+ let gridX = topLeftCorner.x;
+ gridX <= bottomRightCorner.x;
+ gridX++
+ ) {
+ for (
+ let gridY = topLeftCorner.y;
+ gridY <= bottomRightCorner.y;
+ gridY++
) {
- editableTileMap.increaseDimensions(
- columnsToAppend,
- columnsToUnshift,
- rowsToAppend,
- rowsToUnshift
- );
+ editableTileMap.removeTile(gridX, gridY, 0);
}
- const newX = x + columnsToUnshift;
- const newY = y + rowsToUnshift;
-
- editableTileMap.setTile(newX, newY, 0, tileId);
- editableTileMap.flipTileOnX(
- newX,
- newY,
- 0,
- tileMapTileSelection.flipHorizontally
- );
- editableTileMap.flipTileOnY(
- newX,
- newY,
- 0,
- tileMapTileSelection.flipVertically
- );
+ }
- cumulatedUnshiftedRows += rowsToUnshift;
- cumulatedUnshiftedColumns += columnsToUnshift;
- // The instance angle is not considered when moving the instance after
- // rows/columns were added/removed because the instance position does not
- // include the rotation transformation. Otherwise, we could have used
- // tileMapToSceneTransformation to get the new position.
- selectedInstance.setX(
- selectedInstance.getX() -
- columnsToUnshift * (tileSet.tileSize * scaleX)
- );
- selectedInstance.setY(
- selectedInstance.getY() -
- rowsToUnshift * (tileSet.tileSize * scaleY)
- );
- if (selectedInstance.hasCustomSize()) {
- selectedInstance.setCustomWidth(
- selectedInstance.getCustomWidth() +
- tileSet.tileSize * scaleX * (columnsToAppend + columnsToUnshift)
- );
- selectedInstance.setCustomHeight(
- selectedInstance.getCustomHeight() +
- tileSet.tileSize * scaleY * (rowsToAppend + rowsToUnshift)
- );
- }
- });
- } else if (tileMapTileSelection.kind === 'erase') {
- tileMapGridCoordinates.forEach(({ x: gridX, y: gridY }) => {
- editableTileMap.removeTile(gridX, gridY, 0);
- });
shouldTrimAfterOperations = true;
} else {
return;
diff --git a/newIDE/app/src/ObjectEditor/Editors/SimpleTileMapEditor.js b/newIDE/app/src/ObjectEditor/Editors/SimpleTileMapEditor.js
index dc5e6a4354a6..51a8665bddeb 100644
--- a/newIDE/app/src/ObjectEditor/Editors/SimpleTileMapEditor.js
+++ b/newIDE/app/src/ObjectEditor/Editors/SimpleTileMapEditor.js
@@ -2,7 +2,7 @@
import * as React from 'react';
import type { EditorProps } from './EditorProps.flow';
-import ScrollView from '../../UI/ScrollView';
+import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView';
import { ColumnStackLayout } from '../../UI/Layout';
import SemiControlledTextField from '../../UI/SemiControlledTextField';
import { Trans } from '@lingui/macro';
@@ -27,6 +27,7 @@ const SimpleTileMapEditor = ({
resourceManagementProps,
renderObjectNameField,
}: EditorProps) => {
+ const scrollViewRef = React.useRef(null);
const forceUpdate = useForceUpdate();
const objectProperties = objectConfiguration.getProperties();
const tileSize = parseFloat(objectProperties.get('tileSize').getValue());
@@ -128,6 +129,12 @@ const SimpleTileMapEditor = ({
[columnCount, objectConfiguration, forceUpdate, onObjectUpdated]
);
+ const onScrollY = React.useCallback(deltaY => {
+ if (scrollViewRef.current) {
+ scrollViewRef.current.scrollBy(deltaY);
+ }
+ }, []);
+
const onChangeAtlasImage = React.useCallback(
() => {
if (onObjectUpdated) onObjectUpdated();
@@ -168,7 +175,7 @@ const SimpleTileMapEditor = ({
);
return (
-
+
{!!renderObjectNameField && renderObjectNameField()}
{/* TODO: Should this be a Select field with all possible values given the atlas image size? */}
@@ -208,8 +215,10 @@ const SimpleTileMapEditor = ({
onSelectTileMapTile={onChangeTilesWithHitBox}
showPaintingToolbar={false}
allowMultipleSelection
+ allowRectangleSelection={false}
onAtlasImageLoaded={onAtlasImageLoaded}
interactive={true}
+ onScrollY={onScrollY}
/>
>
)}
diff --git a/newIDE/app/src/UI/ScrollView.js b/newIDE/app/src/UI/ScrollView.js
index 8f396f354d2b..e94cd064cf32 100644
--- a/newIDE/app/src/UI/ScrollView.js
+++ b/newIDE/app/src/UI/ScrollView.js
@@ -27,6 +27,7 @@ export type ScrollViewInterface = {|
target: ?React$Component | ?React.ElementRef
) => void,
scrollToPosition: (number: number) => void,
+ scrollBy: (deltaY: number) => void,
scrollToBottom: () => void,
|};
@@ -66,15 +67,23 @@ export default React.forwardRef(
}
},
/**
- * Scroll the view to the target position.
+ * Scroll the view to the target component.
+ */
+ scrollBy: (deltaY: number) => {
+ const scrollViewElement = scrollView.current;
+ if (!scrollViewElement) return;
+ scrollViewElement.scrollBy(0, deltaY);
+ },
+ /**
+ * Scroll the view vertically by the offset passed as argument.
*/
- scrollToPosition: (y: number) => {
+ scrollToPosition: (deltaY: number) => {
const scrollViewElement = scrollView.current;
if (!scrollViewElement) return;
const scrollViewYPosition = scrollViewElement.getBoundingClientRect()
.top;
- scrollViewElement.scrollTop = y - scrollViewYPosition;
+ scrollViewElement.scrollTop = deltaY - scrollViewYPosition;
},
/**
* Scroll the view to the bottom.
diff --git a/newIDE/app/src/Utils/TileMap.js b/newIDE/app/src/Utils/TileMap.js
new file mode 100644
index 000000000000..b27dea1ff5dc
--- /dev/null
+++ b/newIDE/app/src/Utils/TileMap.js
@@ -0,0 +1,463 @@
+// @flow
+import { AffineTransformation } from './AffineTransformation';
+import { type TileMapTileSelection } from '../InstancesEditor/TileSetVisualizer';
+
+export type TileMapTilePatch = {|
+ tileCoordinates?: {| x: number, y: number |},
+ erase?: boolean,
+ topLeftCorner: {| x: number, y: number |},
+ bottomRightCorner: {| x: number, y: number |},
+|};
+
+export type TileSet = {|
+ rowCount: number,
+ columnCount: number,
+ tileSize: number,
+ atlasImage: string,
+|};
+
+const areSameCoordinates = (
+ tileA: {| x: number, y: number |},
+ tileB: {| x: number, y: number |}
+): boolean => tileA.x === tileB.x && tileA.y === tileB.y;
+
+/**
+ * This method gathers tiles into a big rectangle when possible.
+ * Hypothesis taken on the input list:
+ * - The list contains only tiles of size 1
+ * - The list is a flattened view of the grid in order to have the following grid:
+ * A, C, E, G
+ * B, D, F, H
+ * given as:
+ * [A, B, C, D, E, F, G, H]
+ *
+ * Note: This method won't handle perfectly nested rectangles. For instance, this layout:
+ * A D D D D D D D D D D D E
+ * B J J J J J J J J J J J G
+ * B J J J K K K K K J J J G
+ * B J J J K K K K K J J J G
+ * B J J J K K K K K J J J G
+ * B J J J J J J J J J J J G
+ * C F F F F F F F F F F F H
+ * might result in something like:
+ * A ╾ ─ ─ ─ ─ D ─ ─ ─ ─ ╼ E
+ * ╿ J ╾ ─ ─ ─ J ─ ─ ─ ─ ╼ ╿
+ * │ ┌ ─ ┐ ┌ ─ ─ ─ ┐ ┌ ─ ┐ │
+ * B │ J │ │ K │ │ J │ G
+ * │ │ │ └ ─ ─ ─ ┘ └ ─ ┘ │
+ * ╽ └ ─ ┘ ╾ ─ ─ J ─ ─ ─ ╼ ╽
+ * C ╾ ─ ─ ─ ─ F ─ ─ ─ ─ ╼ H
+ */
+export const optimizeTilesGridCoordinates = ({
+ tileMapTilePatches,
+ minX,
+ minY,
+ maxX,
+ maxY,
+}: {|
+ tileMapTilePatches: TileMapTilePatch[],
+ minX: number,
+ maxX: number,
+ minY: number,
+ maxY: number,
+|}): TileMapTilePatch[] => {
+ const newTileMapTilePatches = [];
+
+ while (tileMapTilePatches[0]) {
+ const referencePatch = tileMapTilePatches[0];
+ if (!referencePatch || !referencePatch.tileCoordinates) break;
+ const referencePatchTileCoordinates = referencePatch.tileCoordinates;
+ if (!referencePatchTileCoordinates) break;
+ const patchesWithSameTile = tileMapTilePatches
+ .slice(1)
+ .filter(
+ patch =>
+ patch.tileCoordinates &&
+ areSameCoordinates(
+ referencePatchTileCoordinates,
+ patch.tileCoordinates
+ )
+ );
+ let expandRight = 0;
+ let expandBottom = 0;
+ const patchesOnRight = patchesWithSameTile.filter(
+ patch =>
+ patch.topLeftCorner.x > referencePatch.topLeftCorner.x &&
+ patch.topLeftCorner.y === referencePatch.topLeftCorner.y &&
+ areSameCoordinates(patch.topLeftCorner, patch.bottomRightCorner)
+ );
+ const patchesOnRightX = patchesOnRight.map(patch => patch.topLeftCorner.x);
+ for (
+ let deltaX = 1;
+ deltaX <= maxX - referencePatch.topLeftCorner.x;
+ deltaX++
+ ) {
+ if (patchesOnRightX.includes(deltaX + referencePatch.topLeftCorner.x))
+ expandRight = deltaX;
+ else break;
+ }
+ const patchesOnBottom = patchesWithSameTile.filter(
+ patch =>
+ patch.topLeftCorner.x === referencePatch.topLeftCorner.x &&
+ patch.topLeftCorner.y > referencePatch.topLeftCorner.y &&
+ areSameCoordinates(patch.topLeftCorner, patch.bottomRightCorner)
+ );
+ const patchesOnBottomY = patchesOnBottom.map(
+ patch => patch.topLeftCorner.y
+ );
+ for (
+ let deltaY = 1;
+ deltaY <= maxY - referencePatch.topLeftCorner.y;
+ deltaY++
+ ) {
+ if (patchesOnBottomY.includes(deltaY + referencePatch.topLeftCorner.y))
+ expandBottom = deltaY;
+ else break;
+ }
+ if (expandRight === 0 && expandBottom === 0) {
+ newTileMapTilePatches.push(tileMapTilePatches.shift());
+ continue;
+ }
+
+ let isWholeRectangleOfSameTile = true;
+ const patchIndices = [];
+ for (let deltaX = 0; deltaX <= expandRight; deltaX++) {
+ for (let deltaY = 0; deltaY <= expandBottom; deltaY++) {
+ if (deltaX === 0 && deltaY === 0) {
+ patchIndices.push(0);
+ continue;
+ }
+
+ const patchIndex = tileMapTilePatches.findIndex(
+ patch =>
+ patch.topLeftCorner.x === deltaX + referencePatch.topLeftCorner.x &&
+ patch.topLeftCorner.y === deltaY + referencePatch.topLeftCorner.y &&
+ patch.tileCoordinates &&
+ areSameCoordinates(
+ referencePatchTileCoordinates,
+ patch.tileCoordinates
+ )
+ );
+ if (patchIndex === -1) {
+ isWholeRectangleOfSameTile = false;
+ break;
+ } else {
+ patchIndices.push(patchIndex);
+ }
+ }
+ if (!isWholeRectangleOfSameTile) break;
+ }
+ if (!isWholeRectangleOfSameTile) {
+ newTileMapTilePatches.push(tileMapTilePatches.shift());
+ } else {
+ newTileMapTilePatches.push({
+ tileCoordinates: referencePatchTileCoordinates,
+ topLeftCorner: referencePatch.topLeftCorner,
+ bottomRightCorner: {
+ x: referencePatch.topLeftCorner.x + expandRight,
+ y: referencePatch.topLeftCorner.y + expandBottom,
+ },
+ });
+ patchIndices.sort((a, b) => (a > b ? -1 : 1));
+ patchIndices.forEach(index => tileMapTilePatches.splice(index, 1));
+ }
+ }
+ return newTileMapTilePatches;
+};
+
+export const isSelectionASingleTileRectangle = (
+ tileMapTileSelection: TileMapTileSelection
+): boolean => {
+ return (
+ tileMapTileSelection.kind === 'rectangle' &&
+ tileMapTileSelection.coordinates.length === 2 &&
+ tileMapTileSelection.coordinates[0].x ===
+ tileMapTileSelection.coordinates[1].x &&
+ tileMapTileSelection.coordinates[0].y ===
+ tileMapTileSelection.coordinates[1].y
+ );
+};
+
+/**
+ * When flipping is activated, the tile texture should be flipped but
+ * the tiles should be flipped as well in the selection
+ * (the left tiles should be replaced by the right tiles if the horizontal flip
+ * is activated).
+ */
+const getTileCorrespondingToFlippingInstructions = ({
+ tileMapTileSelection,
+ tileCoordinates,
+}: {|
+ tileMapTileSelection: TileMapTileSelection,
+ tileCoordinates: {| x: number, y: number |},
+|}): {| x: number, y: number |} => {
+ if (tileMapTileSelection.kind === 'rectangle') {
+ const selectionTopLeftCorner = tileMapTileSelection.coordinates[0];
+ const selectionBottomRightCorner = tileMapTileSelection.coordinates[1];
+ const selectionWidth =
+ selectionBottomRightCorner.x - selectionTopLeftCorner.x + 1;
+ const selectionHeight =
+ selectionBottomRightCorner.y - selectionTopLeftCorner.y + 1;
+ const deltaX = tileCoordinates.x - selectionTopLeftCorner.x;
+ const deltaY = tileCoordinates.y - selectionTopLeftCorner.y;
+ const newX =
+ selectionTopLeftCorner.x +
+ (tileMapTileSelection.flipHorizontally
+ ? selectionWidth - deltaX - 1
+ : deltaX);
+ const newY =
+ selectionTopLeftCorner.y +
+ (tileMapTileSelection.flipVertically
+ ? selectionHeight - deltaY - 1
+ : deltaY);
+ return { x: newX, y: newY };
+ }
+ return tileCoordinates;
+};
+
+/**
+ * Returns the list of tiles corresponding to the user selection.
+ * This method maps tiles from the tileset selection to a grid position on the
+ * tilemap corresponding to the user selection. This operation is done coordinate
+ * coordinate (on the tilemap) and is then optimized to gather rectangles of same tile
+ * to speed up consequential operations.
+ */
+export const getTilesGridCoordinatesFromPointerSceneCoordinates = ({
+ tileMapTileSelection,
+ coordinates,
+ tileSize,
+ sceneToTileMapTransformation,
+}: {|
+ tileMapTileSelection: TileMapTileSelection,
+ coordinates: Array<{| x: number, y: number |}>,
+ tileSize: number,
+ sceneToTileMapTransformation: AffineTransformation,
+|}): TileMapTilePatch[] => {
+ if (coordinates.length === 0) return [];
+
+ if (coordinates.length === 1) {
+ // One coordinate corresponds to the pointer over the canvas.
+ const coordinatesInTileMapGrid = [0, 0];
+ sceneToTileMapTransformation.transform(
+ [coordinates[0].x, coordinates[0].y],
+ coordinatesInTileMapGrid
+ );
+ const x = Math.floor(coordinatesInTileMapGrid[0] / tileSize);
+ const y = Math.floor(coordinatesInTileMapGrid[1] / tileSize);
+ let tileCoordinates;
+ if (tileMapTileSelection.kind === 'rectangle') {
+ const topLeftCorner = tileMapTileSelection.coordinates[0];
+ tileCoordinates = getTileCorrespondingToFlippingInstructions({
+ tileMapTileSelection,
+ tileCoordinates: topLeftCorner,
+ });
+ }
+ return [
+ {
+ erase: tileMapTileSelection.kind === 'erase',
+ tileCoordinates,
+ topLeftCorner: { x, y },
+ bottomRightCorner: { x, y },
+ },
+ ];
+ }
+
+ const tilesCoordinatesInTileMapGrid: TileMapTilePatch[] = [];
+
+ if (coordinates.length === 2) {
+ const firstPointCoordinatesInTileMap = [0, 0];
+ sceneToTileMapTransformation.transform(
+ [coordinates[0].x, coordinates[0].y],
+ firstPointCoordinatesInTileMap
+ );
+ const secondPointCoordinatesInTileMap = [0, 0];
+ sceneToTileMapTransformation.transform(
+ [coordinates[1].x, coordinates[1].y],
+ secondPointCoordinatesInTileMap
+ );
+ const topLeftCornerCoordinatesInTileMap = [
+ Math.min(
+ firstPointCoordinatesInTileMap[0],
+ secondPointCoordinatesInTileMap[0]
+ ),
+ Math.min(
+ firstPointCoordinatesInTileMap[1],
+ secondPointCoordinatesInTileMap[1]
+ ),
+ ];
+ const bottomRightCornerCoordinatesInTileMap = [
+ Math.max(
+ firstPointCoordinatesInTileMap[0],
+ secondPointCoordinatesInTileMap[0]
+ ),
+ Math.max(
+ firstPointCoordinatesInTileMap[1],
+ secondPointCoordinatesInTileMap[1]
+ ),
+ ];
+ const topLeftCornerCoordinatesInTileMapGrid = [
+ Math.floor(topLeftCornerCoordinatesInTileMap[0] / tileSize),
+ Math.floor(topLeftCornerCoordinatesInTileMap[1] / tileSize),
+ ];
+ const bottomRightCornerCoordinatesInTileMapGrid = [
+ Math.floor(bottomRightCornerCoordinatesInTileMap[0] / tileSize),
+ Math.floor(bottomRightCornerCoordinatesInTileMap[1] / tileSize),
+ ];
+ if (tileMapTileSelection.kind === 'erase') {
+ tilesCoordinatesInTileMapGrid.push({
+ erase: true,
+ topLeftCorner: {
+ x: topLeftCornerCoordinatesInTileMapGrid[0],
+ y: topLeftCornerCoordinatesInTileMapGrid[1],
+ },
+ bottomRightCorner: {
+ x: bottomRightCornerCoordinatesInTileMapGrid[0],
+ y: bottomRightCornerCoordinatesInTileMapGrid[1],
+ },
+ });
+ return tilesCoordinatesInTileMapGrid;
+ }
+ if (tileMapTileSelection.kind === 'rectangle') {
+ const selectionTopLeftCorner = tileMapTileSelection.coordinates[0];
+ const selectionBottomRightCorner = tileMapTileSelection.coordinates[1];
+ const selectionWidth =
+ selectionBottomRightCorner.x - selectionTopLeftCorner.x + 1;
+ const selectionHeight =
+ selectionBottomRightCorner.y - selectionTopLeftCorner.y + 1;
+
+ if (isSelectionASingleTileRectangle(tileMapTileSelection)) {
+ tilesCoordinatesInTileMapGrid.push({
+ tileCoordinates: getTileCorrespondingToFlippingInstructions({
+ tileMapTileSelection,
+ tileCoordinates: selectionTopLeftCorner,
+ }),
+ topLeftCorner: {
+ x: topLeftCornerCoordinatesInTileMapGrid[0],
+ y: topLeftCornerCoordinatesInTileMapGrid[1],
+ },
+ bottomRightCorner: {
+ x: bottomRightCornerCoordinatesInTileMapGrid[0],
+ y: bottomRightCornerCoordinatesInTileMapGrid[1],
+ },
+ });
+ return tilesCoordinatesInTileMapGrid;
+ }
+
+ for (
+ let x = topLeftCornerCoordinatesInTileMapGrid[0];
+ x <= bottomRightCornerCoordinatesInTileMapGrid[0];
+ x++
+ ) {
+ for (
+ let y = topLeftCornerCoordinatesInTileMapGrid[1];
+ y <= bottomRightCornerCoordinatesInTileMapGrid[1];
+ y++
+ ) {
+ const deltaX = x - topLeftCornerCoordinatesInTileMapGrid[0];
+ const deltaY = y - topLeftCornerCoordinatesInTileMapGrid[1];
+ const invertedDeltaX =
+ bottomRightCornerCoordinatesInTileMapGrid[0] - x;
+ const invertedDeltaY =
+ bottomRightCornerCoordinatesInTileMapGrid[1] - y;
+ if (deltaX === 0 && deltaY === 0) {
+ tilesCoordinatesInTileMapGrid.push({
+ tileCoordinates: getTileCorrespondingToFlippingInstructions({
+ tileMapTileSelection,
+ tileCoordinates: selectionTopLeftCorner,
+ }),
+ topLeftCorner: { x, y },
+ bottomRightCorner: { x, y },
+ });
+ continue;
+ }
+ if (invertedDeltaX === 0 && invertedDeltaY === 0) {
+ tilesCoordinatesInTileMapGrid.push({
+ tileCoordinates: getTileCorrespondingToFlippingInstructions({
+ tileMapTileSelection,
+ tileCoordinates: selectionBottomRightCorner,
+ }),
+ topLeftCorner: { x, y },
+ bottomRightCorner: { x, y },
+ });
+ continue;
+ }
+
+ let tileX, tileY;
+ if (deltaX === 0 || selectionWidth === 1) {
+ tileX = selectionTopLeftCorner.x;
+ } else if (invertedDeltaX === 0 || selectionWidth === 2) {
+ tileX = selectionBottomRightCorner.x;
+ } else {
+ tileX =
+ ((deltaX - 1) % (selectionWidth - 2)) +
+ 1 +
+ selectionTopLeftCorner.x;
+ }
+ if (deltaY === 0 || selectionHeight === 1) {
+ tileY = selectionTopLeftCorner.y;
+ } else if (invertedDeltaY === 0 || selectionHeight === 2) {
+ tileY = selectionBottomRightCorner.y;
+ } else {
+ tileY =
+ ((deltaY - 1) % (selectionHeight - 2)) +
+ 1 +
+ selectionTopLeftCorner.y;
+ }
+
+ tilesCoordinatesInTileMapGrid.push({
+ tileCoordinates: getTileCorrespondingToFlippingInstructions({
+ tileMapTileSelection,
+ tileCoordinates: { x: tileX, y: tileY },
+ }),
+ topLeftCorner: { x, y },
+ bottomRightCorner: { x, y },
+ });
+ }
+ }
+ if (selectionWidth >= 4 && selectionHeight >= 4) {
+ // In this case, each cell in the grid will contain a tile that is different
+ // from all the adjacent ones, so there is no need to optimize the list.
+ return tilesCoordinatesInTileMapGrid;
+ }
+ return optimizeTilesGridCoordinates({
+ tileMapTilePatches: tilesCoordinatesInTileMapGrid,
+ minX: topLeftCornerCoordinatesInTileMapGrid[0],
+ minY: topLeftCornerCoordinatesInTileMapGrid[1],
+ maxX: bottomRightCornerCoordinatesInTileMapGrid[0],
+ maxY: bottomRightCornerCoordinatesInTileMapGrid[1],
+ });
+ }
+ }
+ return [];
+};
+
+export const getTileSet = (object: gdObject): TileSet => {
+ const objectConfigurationProperties = object
+ .getConfiguration()
+ .getProperties();
+ const columnCount = parseFloat(
+ objectConfigurationProperties.get('columnCount').getValue()
+ );
+ const rowCount = parseFloat(
+ objectConfigurationProperties.get('rowCount').getValue()
+ );
+ const tileSize = parseFloat(
+ objectConfigurationProperties.get('tileSize').getValue()
+ );
+ const atlasImage = objectConfigurationProperties.get('atlasImage').getValue();
+ return { rowCount, columnCount, tileSize, atlasImage };
+};
+
+export const isTileSetBadlyConfigured = ({
+ rowCount,
+ columnCount,
+ tileSize,
+ atlasImage,
+}: TileSet) => {
+ return (
+ !Number.isInteger(columnCount) ||
+ columnCount <= 0 ||
+ !Number.isInteger(rowCount) ||
+ rowCount <= 0
+ );
+};
diff --git a/newIDE/app/src/Utils/TileMap.spec.js b/newIDE/app/src/Utils/TileMap.spec.js
new file mode 100644
index 000000000000..19e3f75f6f32
--- /dev/null
+++ b/newIDE/app/src/Utils/TileMap.spec.js
@@ -0,0 +1,244 @@
+// @flow
+
+import { optimizeTilesGridCoordinates } from './TileMap';
+
+describe('optimizeTilesGridCoordinates', () => {
+ test('Selection of 2x1 is expanded on the right', () => {
+ const minX = 4;
+ const maxX = 7;
+ const minY = 12;
+ const maxY = 12;
+ const result = optimizeTilesGridCoordinates({
+ minX,
+ maxX,
+ minY,
+ maxY,
+ tileMapTilePatches: [
+ {
+ tileCoordinates: { x: 4, y: 4 },
+ topLeftCorner: { x: minX, y: minY },
+ bottomRightCorner: { x: minX, y: minY },
+ },
+ ...[1, 2, 3].map(deltaX => ({
+ tileCoordinates: { x: 4, y: 1 },
+ topLeftCorner: { x: minX + deltaX, y: minY },
+ bottomRightCorner: { x: minX + deltaX, y: minY },
+ })),
+ ],
+ });
+ expect(result).toEqual([
+ {
+ tileCoordinates: { x: 4, y: 4 },
+ topLeftCorner: { x: minX, y: minY },
+ bottomRightCorner: { x: minX, y: minY },
+ },
+ {
+ tileCoordinates: { x: 4, y: 1 },
+ topLeftCorner: { x: minX + 1, y: minY },
+ bottomRightCorner: { x: maxX, y: minY },
+ },
+ ]);
+ });
+ test('Selection of 1x1 is expanded on the right', () => {
+ const minX = 4;
+ const maxX = 7;
+ const minY = 12;
+ const maxY = 12;
+ const result = optimizeTilesGridCoordinates({
+ minX,
+ maxX,
+ minY,
+ maxY,
+ tileMapTilePatches: [
+ {
+ tileCoordinates: { x: 4, y: 4 },
+ topLeftCorner: { x: minX, y: minY },
+ bottomRightCorner: { x: minX, y: minY },
+ },
+ ...[1, 2, 3].map(deltaX => ({
+ tileCoordinates: { x: 4, y: 4 },
+ topLeftCorner: { x: minX + deltaX, y: minY },
+ bottomRightCorner: { x: minX + deltaX, y: minY },
+ })),
+ ],
+ });
+ expect(result).toEqual([
+ {
+ tileCoordinates: { x: 4, y: 4 },
+ topLeftCorner: { x: minX, y: minY },
+ bottomRightCorner: { x: maxX, y: maxY },
+ },
+ ]);
+ });
+ test('Selection of 1x1 is expanded on the bottom', () => {
+ const minX = 4;
+ const maxX = 4;
+ const minY = 12;
+ const maxY = 16;
+ const result = optimizeTilesGridCoordinates({
+ minX,
+ maxX,
+ minY,
+ maxY,
+ tileMapTilePatches: [
+ {
+ tileCoordinates: { x: 4, y: 4 },
+ topLeftCorner: { x: minX, y: minY },
+ bottomRightCorner: { x: minX, y: minY },
+ },
+ ...[1, 2, 3, 4].map(deltaY => ({
+ tileCoordinates: { x: 4, y: 4 },
+ topLeftCorner: { x: minX, y: minY + deltaY },
+ bottomRightCorner: { x: minX, y: minY + deltaY },
+ })),
+ ],
+ });
+ expect(result).toEqual([
+ {
+ tileCoordinates: { x: 4, y: 4 },
+ topLeftCorner: { x: minX, y: minY },
+ bottomRightCorner: { x: maxX, y: maxY },
+ },
+ ]);
+ });
+ test('Selection of 1x1 is expanded on the right and on the bottom', () => {
+ const minX = 4;
+ const maxX = 7;
+ const minY = 12;
+ const maxY = 15;
+ const result = optimizeTilesGridCoordinates({
+ minX,
+ maxX,
+ minY,
+ maxY,
+ tileMapTilePatches: [
+ {
+ tileCoordinates: { x: 4, y: 4 },
+ topLeftCorner: { x: minX, y: minY },
+ bottomRightCorner: { x: minX, y: minY },
+ },
+ ...[0, 1, 2, 3]
+ .map(deltaX => {
+ return [0, 1, 2, 3].map(deltaY =>
+ deltaX === 0 && deltaY === 0
+ ? null
+ : {
+ tileCoordinates: { x: 4, y: 4 },
+ topLeftCorner: { x: minX + deltaX, y: minY + deltaY },
+ bottomRightCorner: { x: minX + deltaX, y: minY + deltaY },
+ }
+ );
+ })
+ .flat()
+ .filter(Boolean),
+ ],
+ });
+ expect(result).toEqual([
+ {
+ tileCoordinates: { x: 4, y: 4 },
+ topLeftCorner: { x: minX, y: minY },
+ bottomRightCorner: { x: maxX, y: maxY },
+ },
+ ]);
+ });
+ test('Selection of 3x2 is expanded on the right and on the bottom', () => {
+ const minX = 4;
+ const maxX = 7;
+ const minY = 12;
+ const maxY = 13;
+ const topLeftCorner = { x: 4, y: 4 };
+ const topMiddleCorner = { x: 5, y: 4 };
+ const topRightCorner = { x: 6, y: 4 };
+ const bottomLeftCorner = { x: 4, y: 5 };
+ const bottomMiddleCorner = { x: 5, y: 5 };
+ const bottomRightCorner = { x: 6, y: 5 };
+ const result = optimizeTilesGridCoordinates({
+ minX,
+ maxX,
+ minY,
+ maxY,
+ tileMapTilePatches: [
+ // First column
+ {
+ tileCoordinates: topLeftCorner,
+ topLeftCorner: { x: minX, y: minY },
+ bottomRightCorner: { x: minX, y: minY },
+ },
+ {
+ tileCoordinates: bottomLeftCorner,
+ topLeftCorner: { x: minX, y: minY + 1 },
+ bottomRightCorner: { x: minX, y: minY + 1 },
+ },
+ // Second column
+ {
+ tileCoordinates: topMiddleCorner,
+ topLeftCorner: { x: minX + 1, y: minY },
+ bottomRightCorner: { x: minX + 1, y: minY },
+ },
+ {
+ tileCoordinates: bottomMiddleCorner,
+ topLeftCorner: { x: minX + 1, y: minY + 1 },
+ bottomRightCorner: { x: minX + 1, y: minY + 1 },
+ },
+ // Third column
+ {
+ tileCoordinates: topMiddleCorner,
+ topLeftCorner: { x: minX + 2, y: minY },
+ bottomRightCorner: { x: minX + 2, y: minY },
+ },
+ {
+ tileCoordinates: bottomMiddleCorner,
+ topLeftCorner: { x: minX + 2, y: minY + 1 },
+ bottomRightCorner: { x: minX + 2, y: minY + 1 },
+ },
+ // Fourth column
+ {
+ tileCoordinates: topRightCorner,
+ topLeftCorner: { x: minX + 3, y: minY },
+ bottomRightCorner: { x: minX + 3, y: minY },
+ },
+ {
+ tileCoordinates: bottomRightCorner,
+ topLeftCorner: { x: minX + 3, y: minY + 1 },
+ bottomRightCorner: { x: minX + 3, y: minY + 1 },
+ },
+ ],
+ });
+ expect(result).toEqual([
+ // First column
+ {
+ tileCoordinates: topLeftCorner,
+ topLeftCorner: { x: minX, y: minY },
+ bottomRightCorner: { x: minX, y: minY },
+ },
+ {
+ tileCoordinates: bottomLeftCorner,
+ topLeftCorner: { x: minX, y: minY + 1 },
+ bottomRightCorner: { x: minX, y: minY + 1 },
+ },
+ // Second and third column first line
+ {
+ tileCoordinates: topMiddleCorner,
+ topLeftCorner: { x: minX + 1, y: minY },
+ bottomRightCorner: { x: minX + 2, y: minY },
+ },
+ // Second and third column second line
+ {
+ tileCoordinates: bottomMiddleCorner,
+ topLeftCorner: { x: minX + 1, y: minY + 1 },
+ bottomRightCorner: { x: minX + 2, y: minY + 1 },
+ },
+ // Fourth column
+ {
+ tileCoordinates: topRightCorner,
+ topLeftCorner: { x: minX + 3, y: minY },
+ bottomRightCorner: { x: minX + 3, y: minY },
+ },
+ {
+ tileCoordinates: bottomRightCorner,
+ topLeftCorner: { x: minX + 3, y: minY + 1 },
+ bottomRightCorner: { x: minX + 3, y: minY + 1 },
+ },
+ ]);
+ });
+});
diff --git a/newIDE/app/src/Utils/UseLongTouch.js b/newIDE/app/src/Utils/UseLongTouch.js
index b292332e578b..6224baea0631 100644
--- a/newIDE/app/src/Utils/UseLongTouch.js
+++ b/newIDE/app/src/Utils/UseLongTouch.js
@@ -47,6 +47,7 @@ export const useLongTouch = (
*/
context?: string,
delay?: number,
+ doNotCancelOnScroll?: boolean,
}
) => {
const timeout = React.useRef(null);
@@ -61,8 +62,11 @@ export const useLongTouch = (
[context]
);
+ const cancelOnScroll = !options || !options.doNotCancelOnScroll;
+
React.useEffect(
() => {
+ if (!cancelOnScroll) return;
// Cancel the long touch if scrolling (otherwise we can get a long touch
// being activated while scroll and maintaining the touch on an element,
// which is weird for the user that just want to scroll).
@@ -84,7 +88,7 @@ export const useLongTouch = (
document.removeEventListener('scroll', clear, { capture: true });
};
},
- [clear]
+ [clear, cancelOnScroll]
);
const start = React.useCallback(