diff --git a/src/rendering/actors/interaction-pane.ts b/src/rendering/actors/interaction-pane.ts index 1e6a2c3..0122627 100644 --- a/src/rendering/actors/interaction-pane.ts +++ b/src/rendering/actors/interaction-pane.ts @@ -24,6 +24,7 @@ import { sprite } from "zcanvas"; import type { Point, Size, Viewport } from "zcanvas"; import type { Document, Layer, Shape, Selection } from "@/definitions/document"; import ToolTypes, { SELECTION_TOOLS } from "@/definitions/tool-types"; +import { isPixelArt } from "@/definitions/editor-properties"; import { getRendererForLayer } from "@/factories/renderer-factory"; import { isPointInRange, translatePoints, snapToAngle, rectToCoordinateList } from "@/math/point-math"; import { rotateRectangleToCoordinates, scaleRectangle } from "@/math/rectangle-math"; @@ -38,7 +39,7 @@ import { invertSelection } from "@/store/actions/selection-invert"; import { applySelection } from "@/store/actions/selection-apply"; import { getPixelRatio, isInsideTransparentArea } from "@/utils/canvas-util"; import { createDocumentSnapshot, createLayerSnapshot } from "@/utils/document-util"; -import { getLastShape, syncSelection } from "@/utils/selection-util"; +import { getLastShape, roundSelection, syncSelection } from "@/utils/selection-util"; import { isShapeClosed, isOverlappingShape, mergeShapes, rectangleToShape, subtractShapes } from "@/utils/shape-util"; export enum InteractionModes { @@ -237,6 +238,9 @@ export default class InteractionPane extends sprite { } else { selectionToSet = activeSelection; // no overlap handling necessary, we can commit the whole active selection to history } + if ( isPixelArt( this.getActiveDocument() )) { + selectionToSet = roundSelection( selectionToSet ); + } this._selectionClosed = true; this.canvas.store.commit( "setActiveSelection", [ ...selectionToSet ]); applySelection( getCanvasInstance().store, this.getActiveDocument(), currentSelection ); diff --git a/src/utils/selection-util.ts b/src/utils/selection-util.ts index 391cd7c..ba034bc 100644 --- a/src/utils/selection-util.ts +++ b/src/utils/selection-util.ts @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Igor Zinken 2021-2025 - https://www.igorski.nl + * Igor Zinken 2021-2026 - https://www.igorski.nl * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -25,7 +25,7 @@ import { type Rectangle } from "zcanvas"; import { type Shape, Selection } from "@/definitions/document"; import { getRendererForLayer } from "@/factories/renderer-factory"; import { type BitMapperyState } from "@/store"; -import { shapeToRectangle, scaleShape } from "@/utils/shape-util"; +import { shapeToRectangle, roundShape, scaleShape } from "@/utils/shape-util"; export const selectionToRectangle = ( selection: Selection ): Rectangle => { if ( selection.length === 1 ) { @@ -49,6 +49,10 @@ export const selectionToRectangle = ( selection: Selection ): Rectangle => { }; }; +export const roundSelection = ( selection: Selection ): Selection => { + return selection.map(( shape: Shape ) => roundShape( shape )); +}; + export const scaleSelection = ( selection: Selection, scale: number ): Selection => { return selection.map(( shape: Shape ) => scaleShape( shape, scale )); }; diff --git a/src/utils/shape-util.ts b/src/utils/shape-util.ts index 80fc8ac..adee419 100644 --- a/src/utils/shape-util.ts +++ b/src/utils/shape-util.ts @@ -28,6 +28,7 @@ import { union } from "martinez-polygon-clipping"; import type { Point, Rectangle } from "zcanvas"; +import { fastRound } from "@/math/unit-math"; import type { Shape } from "@/definitions/document"; export const shapeToRectangle = ( shape: Shape ): Rectangle => { @@ -62,6 +63,10 @@ export const scaleShape = ( shape: Shape, scale: number ): Shape => { return shape.map(( point: Point ) => ({ x: point.x * scale, y: point.y * scale })); }; +export const roundShape = ( shape: Shape ): Shape => { + return shape.map(({ x, y }: Point ) => ({ x: fastRound( x ), y: fastRound( y ) })); +}; + export const isShapeRectangular = ( shape: Shape ): boolean => { if ( shape.length !== 5 ) { return false; diff --git a/tests/unit/utils/selection-util.spec.ts b/tests/unit/utils/selection-util.spec.ts index d40cb46..b9d3ff2 100644 --- a/tests/unit/utils/selection-util.spec.ts +++ b/tests/unit/utils/selection-util.spec.ts @@ -6,7 +6,7 @@ mockZCanvas(); import DocumentFactory from "@/factories/document-factory"; import LayerFactory from "@/factories/layer-factory"; import { createRendererForLayer, flushRendererCache } from "@/factories/renderer-factory"; -import { getLastShape, scaleSelection, selectionToRectangle, syncSelection } from "@/utils/selection-util"; +import { getLastShape, roundSelection, scaleSelection, selectionToRectangle, syncSelection } from "@/utils/selection-util"; describe( "Selection utilities", () => { const selection = [ @@ -47,6 +47,24 @@ describe( "Selection utilities", () => { }); }); + it( "should be able to round the Shape coordinates inside a selection", () => { + expect( roundSelection([ + [ + { x: 4.25, y: 5.56 }, { x: 5.55, y: 5.56 }, { x: 5.55, y: 7.2 }, { x: 4.25, y: 5.56 }, + ], + [ + { x: 2.25, y: 2.56 }, { x: 4.55, y: 5.56 }, { x: 4.55, y: 7.2 }, { x: 2.25, y: 5.56 }, + ] + ])).toEqual([ + [ + { x: 4, y: 6 }, { x: 6, y: 6 }, { x: 6, y: 7 }, { x: 4, y: 6 }, + ], + [ + { x: 2, y: 3 }, { x: 5, y: 6 }, { x: 5, y: 7 }, { x: 2, y: 6 }, + ] + ]); + }); + it( "should be able to scale the Shapes inside a selection", () => { expect( scaleSelection( selection, 1.5 )).toEqual([ [ diff --git a/tests/unit/utils/shape-util.spec.ts b/tests/unit/utils/shape-util.spec.ts index 65e5c8b..1dc96b1 100644 --- a/tests/unit/utils/shape-util.spec.ts +++ b/tests/unit/utils/shape-util.spec.ts @@ -1,7 +1,7 @@ import { it, describe, expect } from "vitest"; import { mergeShapes, - rectangleToShape, scaleShape, shapeToRectangle, subtractShapes, + rectangleToShape, roundShape, scaleShape, shapeToRectangle, subtractShapes, isOverlappingShape, isShapeRectangular, isShapeClosed } from "@/utils/shape-util"; @@ -31,6 +31,22 @@ describe( "Shape utilities", () => { ]); }); + it( "should be able to round the values in a Shape to their nearest integer values", () => { + expect( roundShape([ + { x: 4.250559227410698, y: 5.228564005399 }, + { x: 6.846033445918115, y: 5.228564005399 }, + { x: 6.846033445918115, y: 7.598344813601424 }, + { x: 4.250559227410698, y: 7.598344813601424 }, + { x: 4.250559227410698, y: 5.228564005399 } + ])).toEqual([ + { x: 4, y: 5 }, + { x: 7, y: 5 }, + { x: 7, y: 8 }, + { x: 4, y: 8 }, + { x: 4, y: 5 }, + ]); + }); + it( "should be able to scale a Shape by given factor", () => { const shape = [ { x: 100, y: 150 },