diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e48e2d0c..19506aa4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org ## [Unreleased] +### Changed + +- Viewports and simulated regions now have a minimal size to be resized to. Already placed viewports and simulated regions below this size are not affected. +- Viewports and simulated regions can now not be flipped horizontally or vertically during resizing. Due to not completely accurate migrations, it could be that the positions of such regions in imported exercises are now slightly off. + ### Added - There are now radiograms, which can be used by the simulation to send messages to the trainees diff --git a/frontend/src/app/pages/exercises/exercise/shared/core/drag-element.service.ts b/frontend/src/app/pages/exercises/exercise/shared/core/drag-element.service.ts index da9a27dd8..84a85a511 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/core/drag-element.service.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/core/drag-element.service.ts @@ -221,8 +221,8 @@ export class DragElementService { const width = height * Viewport.image.aspectRatio; const viewport = Viewport.create( { - x: position.x - width / 2, - y: position.y + height / 2, + x: position.x, + y: position.y, }, { height, @@ -283,8 +283,8 @@ export class DragElementService { const width = height * SimulatedRegion.image.aspectRatio; const simulatedRegion = SimulatedRegion.create( { - x: position.x - width / 2, - y: position.y + height / 2, + x: position.x, + y: position.y, }, { height, diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/moveable-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/moveable-feature-manager.ts index 35f6acb16..037ec3550 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/moveable-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/moveable-feature-manager.ts @@ -7,13 +7,12 @@ import type VectorSource from 'ol/source/Vector'; import type { Observable } from 'rxjs'; import { Subject } from 'rxjs'; // eslint-disable-next-line @typescript-eslint/no-shadow -import type { Element, UUID } from 'digital-fuesim-manv-shared'; +import type { Element, MapCoordinates, UUID } from 'digital-fuesim-manv-shared'; import type { FeatureManager } from '../utility/feature-manager'; import type { GeometryHelper, GeometryWithCoordinates, PositionableElement, - Positions, } from '../utility/geometry-helper'; import { MovementAnimator } from '../utility/movement-animator'; import type { OlMapInteractionsManager } from '../utility/ol-map-interactions-manager'; @@ -39,7 +38,7 @@ export abstract class MoveableFeatureManager< constructor( protected readonly olMap: OlMap, private readonly proposeMovementAction: ( - newPosition: Positions, + newPosition: MapCoordinates, element: ManagedElement ) => void, protected readonly geometryHelper: GeometryHelper< diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/simulated-region-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/simulated-region-feature-manager.ts index 9561fa0b1..82496db1d 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/simulated-region-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/simulated-region-feature-manager.ts @@ -58,11 +58,11 @@ export class SimulatedRegionFeatureManager ) { super( olMap, - (targetPositions, simulatedRegion) => { + (targetPosition, simulatedRegion) => { exerciseService.proposeAction({ type: '[SimulatedRegion] Move simulated region', simulatedRegionId: simulatedRegion.id, - targetPosition: targetPositions[0]![0]!, + targetPosition, }); }, new PolygonGeometryHelper() @@ -84,7 +84,7 @@ export class SimulatedRegionFeatureManager const feature = super.createFeature(element); ResizeRectangleInteraction.onResize( feature, - ({ topLeftCoordinate, scale }) => { + ({ centerCoordinate, scale }) => { const currentElement = this.getElementFromFeature( feature ) as SimulatedRegion; @@ -93,8 +93,8 @@ export class SimulatedRegionFeatureManager type: '[SimulatedRegion] Resize simulated region', simulatedRegionId: element.id, targetPosition: MapCoordinates.create( - topLeftCoordinate[0]!, - topLeftCoordinate[1]! + centerCoordinate[0]!, + centerCoordinate[1]! ), newSize: Size.create( currentElement.size.width * scale.x, @@ -140,19 +140,16 @@ export class SimulatedRegionFeatureManager return false; } if ( - ['vehicle', 'personnel', 'material', 'patient'].includes( - droppedElement.type - ) + droppedElement.type === 'vehicle' || + droppedElement.type === 'personnel' || + droppedElement.type === 'material' || + droppedElement.type === 'patient' ) { this.exerciseService.proposeAction( { type: '[SimulatedRegion] Add Element', simulatedRegionId: droppedOnSimulatedRegion.id, - elementToBeAddedType: droppedElement.type as - | 'material' - | 'patient' - | 'personnel' - | 'vehicle', + elementToBeAddedType: droppedElement.type, elementToBeAddedId: droppedElement.id, }, true diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/viewport-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/viewport-feature-manager.ts index 4d3de9534..acc1981bd 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/viewport-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/viewport-feature-manager.ts @@ -61,11 +61,11 @@ export class ViewportFeatureManager ) { super( olMap, - (targetPositions, viewport) => { + (targetPosition, viewport) => { exerciseService.proposeAction({ type: '[Viewport] Move viewport', viewportId: viewport.id, - targetPosition: targetPositions[0]![0]!, + targetPosition, }); }, new PolygonGeometryHelper() @@ -85,7 +85,7 @@ export class ViewportFeatureManager const feature = super.createFeature(element); ResizeRectangleInteraction.onResize( feature, - ({ topLeftCoordinate, scale }) => { + ({ centerCoordinate, scale }) => { const currentElement = this.getElementFromFeature( feature ) as Viewport; @@ -94,8 +94,8 @@ export class ViewportFeatureManager type: '[Viewport] Resize viewport', viewportId: element.id, targetPosition: MapCoordinates.create( - topLeftCoordinate[0]!, - topLeftCoordinate[1]! + centerCoordinate[0]!, + centerCoordinate[1]! ), newSize: Size.create( currentElement.size.width * scale.x, diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/geometry-helper.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/geometry-helper.ts index c73200b8a..f475d58e6 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/geometry-helper.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/geometry-helper.ts @@ -32,36 +32,25 @@ export type Coordinates = Exclude< null >; -type ArrayElement = ArrayType extends readonly (infer ElementType)[] - ? ElementType - : never; - -type SubstituteCoordinateForPoint = T extends Coordinate - ? MapCoordinates - : T extends Array> - ? SubstituteCoordinateForPoint>[] - : never; - -export type Positions = - SubstituteCoordinateForPoint>; - export interface CoordinatePair { startPosition: Coordinates; endPosition: Coordinates; } export interface GeometryHelper< - T extends GeometryWithCoordinates, + GeometryType extends GeometryWithCoordinates, Element extends PositionableElement = PositionableElement > { - create: (element: Element) => Feature; - getElementCoordinates: (element: Element) => Coordinates; - getFeatureCoordinates: (feature: Feature) => Coordinates; + create: (element: Element) => Feature; + getElementCoordinates: (element: Element) => Coordinates; + getFeatureCoordinates: ( + feature: Feature + ) => Coordinates; interpolateCoordinates: ( - positions: CoordinatePair, + positions: CoordinatePair, progress: number - ) => Coordinates; - getFeaturePosition: (feature: Feature) => Positions; + ) => Coordinates; + getFeaturePosition: (feature: Feature) => MapCoordinates; } export const interpolate = ( diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/point-geometry-helper.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/point-geometry-helper.ts index a9c65dee8..9ba5d6acc 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/point-geometry-helper.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/point-geometry-helper.ts @@ -1,7 +1,7 @@ import type { WithPosition } from 'digital-fuesim-manv-shared'; import { - MapCoordinates, currentCoordinatesOf, + MapCoordinates, } from 'digital-fuesim-manv-shared'; import { Feature } from 'ol'; import { Point } from 'ol/geom'; @@ -9,7 +9,6 @@ import type { CoordinatePair, Coordinates, GeometryHelper, - Positions, } from './geometry-helper'; import { interpolate } from './geometry-helper'; @@ -31,7 +30,7 @@ export class PointGeometryHelper implements GeometryHelper { ): Coordinates => interpolate(positions.startPosition, positions.endPosition, progress); - getFeaturePosition = (feature: Feature): Positions => + getFeaturePosition = (feature: Feature) => MapCoordinates.create( this.getFeatureCoordinates(feature)[0]!, this.getFeatureCoordinates(feature)[1]! diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/polygon-geometry-helper.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/polygon-geometry-helper.ts index 29fc313d5..044fed407 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/polygon-geometry-helper.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/polygon-geometry-helper.ts @@ -3,12 +3,12 @@ import { MapCoordinates, } from 'digital-fuesim-manv-shared'; import { Feature } from 'ol'; +import { getCenter } from 'ol/extent'; import { Polygon } from 'ol/geom'; import type { CoordinatePair, Coordinates, GeometryHelper, - Positions, ResizableElement, } from './geometry-helper'; import { interpolate } from './geometry-helper'; @@ -21,24 +21,24 @@ export class PolygonGeometryHelper getElementCoordinates = ( element: ResizableElement - ): Coordinates => [ - [ - [currentCoordinatesOf(element).x, currentCoordinatesOf(element).y], + ): Coordinates => { + const center = currentCoordinatesOf(element); + const { width, height } = element.size; + return [ [ - currentCoordinatesOf(element).x + element.size.width, - currentCoordinatesOf(element).y, + // top left + [center.x - width / 2, center.y + height / 2], + // top right + [center.x + width / 2, center.y + height / 2], + // bottom right + [center.x + width / 2, center.y - height / 2], + // bottom left + [center.x - width / 2, center.y - height / 2], + // top left (close the rectangle) + [center.x - width / 2, center.y + height / 2], ], - [ - currentCoordinatesOf(element).x + element.size.width, - currentCoordinatesOf(element).y - element.size.height, - ], - [ - currentCoordinatesOf(element).x, - currentCoordinatesOf(element).y - element.size.height, - ], - [currentCoordinatesOf(element).x, currentCoordinatesOf(element).y], - ], - ]; + ]; + }; getFeatureCoordinates = (feature: Feature): Coordinates => feature.getGeometry()!.getCoordinates(); @@ -57,10 +57,11 @@ export class PolygonGeometryHelper ) ); - getFeaturePosition = (feature: Feature): Positions => - this.getFeatureCoordinates(feature).map((coordinates) => - coordinates.map((coordinate) => - MapCoordinates.create(coordinate[0]!, coordinate[1]!) - ) + getFeaturePosition = (feature: Feature) => { + const centerCoordinates = getCenter(feature.getGeometry()!.getExtent()); + return MapCoordinates.create( + centerCoordinates[0]!, + centerCoordinates[1]! ); + }; } diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/resize-rectangle-interaction.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/resize-rectangle-interaction.ts index 38865567d..5d19061e1 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/resize-rectangle-interaction.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/resize-rectangle-interaction.ts @@ -2,12 +2,14 @@ import type { Feature, MapBrowserEvent } from 'ol'; import type { Coordinate } from 'ol/coordinate'; import { distance } from 'ol/coordinate'; import BaseEvent from 'ol/events/Event'; +import { getCenter } from 'ol/extent'; import type { Polygon } from 'ol/geom'; import PointerInteraction from 'ol/interaction/Pointer'; import type VectorSource from 'ol/source/Vector'; /** * Provides the ability to resize a rectangle by dragging any of its corners. + * It prevents the rectangle from flipping and from getting too small. */ export class ResizeRectangleInteraction extends PointerInteraction { /** @@ -21,7 +23,14 @@ export class ResizeRectangleInteraction extends PointerInteraction { */ private currentResizeValues?: CurrentResizeValues; - constructor(private readonly source: VectorSource) { + constructor( + private readonly source: VectorSource, + /** + * The minimum allowed distance between two corners of the rectangle. + */ + // TODO: Add a proper unit for this. Blocked by #374 + private readonly minimumSize = 10 + ) { super({ handleDownEvent: (event) => this._handleDownEvent(event), handleDragEvent: (event) => this._handleDragEvent(event), @@ -66,47 +75,54 @@ export class ResizeRectangleInteraction extends PointerInteraction { return false; } const mouseCoordinate = event.coordinate; - const newXScale = - (mouseCoordinate[0]! - this.currentResizeValues.originCorner[0]!) / - (this.currentResizeValues.draggedCorner[0]! - - this.currentResizeValues.originCorner[0]!); - const newYScale = - (this.currentResizeValues.originCorner[1]! - mouseCoordinate[1]!) / - (this.currentResizeValues.originCorner[1]! - - this.currentResizeValues.draggedCorner[1]!); - this.currentResizeValues.feature + const { draggedCorner, originCorner, currentScale, feature } = + this.currentResizeValues; + const newXScale = this.calculateNewScale( + draggedCorner[0]!, + originCorner[0]!, + mouseCoordinate[0]! + ); + const newYScale = this.calculateNewScale( + draggedCorner[1]!, + originCorner[1]!, + mouseCoordinate[1]! + ); + feature .getGeometry()! .scale( - newXScale / this.currentResizeValues.currentScale!.x, - newYScale / this.currentResizeValues.currentScale!.y, - this.currentResizeValues.originCorner + newXScale / currentScale.x, + newYScale / currentScale.y, + originCorner ); this.currentResizeValues.currentScale = { x: newXScale, y: newYScale }; return true; } + private calculateNewScale( + draggedCorner: number, + originCorner: number, + mouseCoordinate: number + ) { + const oldLength = draggedCorner - originCorner; + const newLength = mouseCoordinate - originCorner; + return ( + // We also want to prevent flipping the rectangle + (oldLength < 0 + ? Math.min(newLength, -this.minimumSize) + : Math.max(newLength, this.minimumSize)) / oldLength + ); + } + private _handleUpEvent(event: MapBrowserEvent): boolean { if (this.currentResizeValues === undefined) { return true; } - - const coordinates = this.currentResizeValues.feature - .getGeometry()! - .getCoordinates()![0]!; - const topLeftCoordinate = coordinates.reduce( - (smallestCoordinate, coordinate) => - coordinate[0]! <= smallestCoordinate[0]! || - coordinate[1]! >= smallestCoordinate[1]! - ? coordinate - : smallestCoordinate, - [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY] + const { currentScale, feature } = this.currentResizeValues; + const newCenterCoordinate = getCenter( + feature.getGeometry()!.getExtent() ); - this.currentResizeValues.feature.dispatchEvent( - new ResizeEvent( - this.currentResizeValues.currentScale, - this.currentResizeValues.originCorner, - topLeftCoordinate - ) + feature.dispatchEvent( + new ResizeEvent(currentScale, newCenterCoordinate) ); this.currentResizeValues = undefined; return false; @@ -127,6 +143,7 @@ interface CurrentResizeValues { draggedCorner: Coordinate; /** * The corner that doesn't move during the resize. + * It is always opposite to the dragged corner. */ originCorner: Coordinate; /** @@ -142,14 +159,7 @@ const resizeRectangleEventType = 'resizerectangle'; class ResizeEvent extends BaseEvent { constructor( public readonly scale: { x: number; y: number }, - /** - * The coordinate of the corner that didn't move during the resize. - */ - public readonly origin: Coordinate, - /** - * The new top left coordinate of the rectangle. - */ - public readonly topLeftCoordinate: Coordinate + public readonly centerCoordinate: Coordinate ) { super(resizeRectangleEventType); } diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/translate-interaction.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/translate-interaction.ts index 0b949eed7..c8dcb9a64 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/translate-interaction.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/translate-interaction.ts @@ -1,8 +1,9 @@ +import type { MapCoordinates } from 'digital-fuesim-manv-shared'; import { isEqual } from 'lodash-es'; import type { Feature, MapBrowserEvent } from 'ol'; import type { Point } from 'ol/geom'; import { Translate } from 'ol/interaction'; -import type { GeometryWithCoordinates, Positions } from './geometry-helper'; +import type { GeometryWithCoordinates } from './geometry-helper'; /** * Translates (moves) a feature to a new position. @@ -48,8 +49,8 @@ export class TranslateInteraction extends Translate { */ public static onTranslateEnd( feature: Feature, - callback: (newCoordinates: Positions) => void, - getPosition: (feature: Feature) => Positions + callback: (newCoordinates: MapCoordinates) => void, + getPosition: (feature: Feature) => MapCoordinates ) { feature.addEventListener('translateend', (event) => { // The end coordinates in the event are the mouse coordinates and not the feature coordinates. diff --git a/shared/src/models/simulated-region.ts b/shared/src/models/simulated-region.ts index 65635b67f..91a133f6e 100644 --- a/shared/src/models/simulated-region.ts +++ b/shared/src/models/simulated-region.ts @@ -20,7 +20,7 @@ export class SimulatedRegion { public readonly type = 'simulatedRegion'; /** - * top-left position + * The center coordinates of the simulatedRegion * * @deprecated Do not access directly, use helper methods from models/utils/position/position-helpers(-mutable) instead. */ @@ -36,11 +36,10 @@ export class SimulatedRegion { public readonly name: string; /** - * @param position top-left position * @deprecated Use {@link create} instead */ - constructor(position: MapCoordinates, size: Size, name: string) { - this.position = MapPosition.create(position); + constructor(centerCoordinates: MapCoordinates, size: Size, name: string) { + this.position = MapPosition.create(centerCoordinates); this.size = size; this.name = name; } diff --git a/shared/src/models/utils/position/position-helpers.ts b/shared/src/models/utils/position/position-helpers.ts index ce7079b7c..cfd3c562b 100644 --- a/shared/src/models/utils/position/position-helpers.ts +++ b/shared/src/models/utils/position/position-helpers.ts @@ -155,31 +155,19 @@ export function simulatedRegionIdOfPosition(position: Position): UUID { } export function upperLeftCornerOf(element: WithExtent): MapCoordinates { - const corner = { ...currentCoordinatesOf(element) }; - - if (element.size.width < 0) { - corner.x += element.size.width; - } - - if (element.size.height < 0) { - corner.y -= element.size.height; - } - - return MapCoordinates.create(corner.x, corner.y); + const centerCoordinate = currentCoordinatesOf(element); + return MapCoordinates.create( + centerCoordinate.x - element.size.width / 2, + centerCoordinate.y + element.size.height / 2 + ); } export function lowerRightCornerOf(element: WithExtent): MapCoordinates { - const corner = { ...currentCoordinatesOf(element) }; - - if (element.size.width > 0) { - corner.x += element.size.width; - } - - if (element.size.height > 0) { - corner.y -= element.size.height; - } - - return MapCoordinates.create(corner.x, corner.y); + const centerCoordinate = currentCoordinatesOf(element); + return MapCoordinates.create( + centerCoordinate.x + element.size.width / 2, + centerCoordinate.y - element.size.height / 2 + ); } export function nestedCoordinatesOf( diff --git a/shared/src/models/utils/size.ts b/shared/src/models/utils/size.ts index 3c71fb87c..ca049a8b1 100644 --- a/shared/src/models/utils/size.ts +++ b/shared/src/models/utils/size.ts @@ -1,17 +1,17 @@ -import { IsNumber } from 'class-validator'; +import { IsPositive } from 'class-validator'; import { getCreate } from './get-create'; export class Size { /** * The width in meters. */ - @IsNumber() + @IsPositive() public readonly width: number; /** * The height in meters. */ - @IsNumber() + @IsPositive() public readonly height: number; /** diff --git a/shared/src/models/viewport.ts b/shared/src/models/viewport.ts index 400cebdbd..fd7a3798d 100644 --- a/shared/src/models/viewport.ts +++ b/shared/src/models/viewport.ts @@ -6,10 +6,10 @@ import { IsValue } from '../utils/validators'; import { getCreate, lowerRightCornerOf, + upperLeftCornerOf, MapPosition, Position, Size, - upperLeftCornerOf, } from './utils'; import type { ImageProperties, MapCoordinates } from './utils'; @@ -21,7 +21,7 @@ export class Viewport { public readonly type = 'viewport'; /** - * top-left position + * The center coordinates of the viewport * * @deprecated Do not access directly, use helper methods from models/utils/position/position-helpers(-mutable) instead. */ @@ -37,11 +37,10 @@ export class Viewport { public readonly name: string; /** - * @param position top-left position * @deprecated Use {@link create} instead */ - constructor(position: MapCoordinates, size: Size, name: string) { - this.position = MapPosition.create(position); + constructor(centerCoordinates: MapCoordinates, size: Size, name: string) { + this.position = MapPosition.create(centerCoordinates); this.size = size; this.name = name; } diff --git a/shared/src/state-migrations/25-refactor-rectangular-element-positions-to-center.ts b/shared/src/state-migrations/25-refactor-rectangular-element-positions-to-center.ts new file mode 100644 index 000000000..dd45dfc43 --- /dev/null +++ b/shared/src/state-migrations/25-refactor-rectangular-element-positions-to-center.ts @@ -0,0 +1,95 @@ +import { cloneDeep } from 'lodash-es'; +import type { Migration } from './migration-functions'; + +export const refactorRectangularElementPositionsToCenter25: Migration = { + action: (intermediaryState: any, action: any) => { + switch (action.type) { + case '[Viewport] Add viewport': + migrateRectangularElement(action.viewport); + break; + case '[SimulatedRegion] Add simulated region': + migrateRectangularElement(action.simulatedRegion); + break; + case '[Viewport] Move viewport': { + const migratedViewport = cloneDeep( + intermediaryState.viewports[action.viewportId]! + ); + migratePositionWithMigratedSize( + action.targetPosition, + migratedViewport.size + ); + break; + } + case '[SimulatedRegion] Move simulated region': { + const migratedSimulatedViewport = cloneDeep( + intermediaryState.simulatedRegions[ + action.simulatedRegionId + ]! + ); + migratePositionWithMigratedSize( + action.targetPosition, + migratedSimulatedViewport.size + ); + break; + } + case '[Viewport] Resize viewport': + migratePosition( + action.targetPosition, + cloneDeep(action.newSize) + ); + migrateSize(action.newSize); + break; + case '[SimulatedRegion] Resize simulated region': + migratePosition( + action.targetPosition, + cloneDeep(action.newSize) + ); + migrateSize(action.newSize); + break; + } + return true; + }, + state: (state: any) => { + for (const viewport of Object.values(state.viewports)) { + migrateRectangularElement(viewport); + } + for (const simulatedRegion of Object.values(state.simulatedRegions)) { + migrateRectangularElement(simulatedRegion); + } + }, +}; + +function migrateRectangularElement(element: any) { + const { position, size } = element; + if (position.type === 'coordinates') { + migratePosition(position.coordinates, size); + } + migrateSize(size); +} + +/** + * Migrates the position of a rectangular element to the center of the element. + * @param oldSize the size of the element before this migration + */ +function migratePosition(coordinates: any, oldSize: any) { + // The position was previously the top-left corner of the rectangle if the width and height was positive. + coordinates.x = coordinates.x + oldSize.width / 2; + coordinates.y = coordinates.y - oldSize.height / 2; +} + +/** + * Migrates the position of a rectangular element to the center of the element. + * + * This migration is not accurate for previously "flipped" viewports (height and/or width was negative). + * To catch this case, access to the size of the viewport before this migration would be required. + * + * @param migratedSize the size of the element after it had been migrated to the center + */ +function migratePositionWithMigratedSize(coordinates: any, migratedSize: any) { + migratePosition(coordinates, migratedSize); +} + +function migrateSize(size: any) { + size.width = Math.abs(size.width); + size.height = Math.abs(size.height); +} diff --git a/shared/src/state-migrations/migration-functions.ts b/shared/src/state-migrations/migration-functions.ts index df90da29f..63e48e6b3 100644 --- a/shared/src/state-migrations/migration-functions.ts +++ b/shared/src/state-migrations/migration-functions.ts @@ -9,6 +9,7 @@ import { addTypeProperty17 } from './17-add-type-property'; import { replacePositionWithMetaPosition18 } from './18-replace-position-with-meta-position'; import { renameStartPointTypes19 } from './19-rename-start-point-types'; import { addSimulationProperties20 } from './20-add-simulation-properties'; +import { refactorRectangularElementPositionsToCenter25 } from './25-refactor-rectangular-element-positions-to-center'; import { fixTypoInRenameSimulatedRegion21 } from './21-fix-typo-in-rename-simulated-region'; import { removeIllegalVehicleMovementActions22 } from './22-remove-illegal-vehicle-movement-actions'; import { addTransferPointToSimulatedRegion23 } from './23-add-transfer-point-to-simulated-region'; @@ -24,7 +25,7 @@ import { impossibleMigration } from './impossible-migration'; /** * Migrate a single action - * @param intermediaryState - The migrated exercise state just before the action is applied + * @param intermediaryState - The immutable migrated exercise state just before the action is applied * @param action - The action to migrate in place * @returns true if the migration was successful or false to indicate that the action should be deleted * @throws a {@link RestoreError} when a migration is not possible. @@ -71,4 +72,5 @@ export const migrations: { 22: removeIllegalVehicleMovementActions22, 23: addTransferPointToSimulatedRegion23, 24: addRadiograms24, + 25: refactorRectangularElementPositionsToCenter25, }; diff --git a/shared/src/state.ts b/shared/src/state.ts index a6c7a5005..aad59b62b 100644 --- a/shared/src/state.ts +++ b/shared/src/state.ts @@ -160,5 +160,5 @@ export class ExerciseState { * * This number MUST be increased every time a change to any object (that is part of the state or the state itself) is made in a way that there may be states valid before that are no longer valid. */ - static readonly currentStateVersion = 24; + static readonly currentStateVersion = 25; }