This commit is contained in:
psychedelicious 2024-07-24 19:09:33 +10:00
parent e8b0b6cef5
commit 5fa65e5cc6
4 changed files with 287 additions and 71 deletions

View File

@ -1,3 +1,4 @@
import { deepClone } from 'common/util/deepClone';
import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine';
import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine';
import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
@ -10,6 +11,8 @@ import Konva from 'konva';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
const MIN_LAYER_SIZE_PX = 10;
export class CanvasLayer { export class CanvasLayer {
static NAME_PREFIX = 'layer'; static NAME_PREFIX = 'layer';
static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`; static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`;
@ -19,6 +22,8 @@ export class CanvasLayer {
static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`; static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`;
static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`; static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`;
private static BBOX_PADDING_PX = 5;
private drawingBuffer: BrushLine | EraserLine | RectShape | null; private drawingBuffer: BrushLine | EraserLine | RectShape | null;
private state: LayerEntity; private state: LayerEntity;
@ -27,10 +32,11 @@ export class CanvasLayer {
konva: { konva: {
layer: Konva.Layer; layer: Konva.Layer;
group: Konva.Group;
bbox: Konva.Rect; bbox: Konva.Rect;
objectGroup: Konva.Group; objectGroup: Konva.Group;
objectGroupBbox: Konva.Rect;
positionXLine: Konva.Line;
positionYLine: Konva.Line;
transformer: Konva.Transformer; transformer: Konva.Transformer;
interactionRect: Konva.Rect; interactionRect: Konva.Rect;
}; };
@ -44,7 +50,6 @@ export class CanvasLayer {
this.manager = manager; this.manager = manager;
this.konva = { this.konva = {
layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }), layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }),
group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: true, draggable: true }),
bbox: new Konva.Rect({ bbox: new Konva.Rect({
listening: false, listening: false,
draggable: false, draggable: false,
@ -54,68 +59,252 @@ export class CanvasLayer {
strokeHitEnabled: false, strokeHitEnabled: false,
}), }),
objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }),
objectGroupBbox: new Konva.Rect({ fill: 'green', opacity: 0.5, listening: false }),
positionXLine: new Konva.Line({ stroke: 'white', strokeWidth: 1 }),
positionYLine: new Konva.Line({ stroke: 'white', strokeWidth: 1 }),
transformer: new Konva.Transformer({ transformer: new Konva.Transformer({
name: CanvasLayer.TRANSFORMER_NAME, name: CanvasLayer.TRANSFORMER_NAME,
draggable: false, draggable: false,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], // enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false, rotateEnabled: false,
flipEnabled: false, flipEnabled: false,
listening: false, listening: false,
padding: CanvasLayer.BBOX_PADDING_PX,
stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400
}), }),
interactionRect: new Konva.Rect({ interactionRect: new Konva.Rect({
name: CanvasLayer.INTERACTION_RECT_NAME, name: CanvasLayer.INTERACTION_RECT_NAME,
listening: false, listening: false,
draggable: false, draggable: true,
fill: 'rgba(255,0,0,0.5)', fill: 'rgba(255,0,0,0.5)',
}), }),
}; };
this.konva.layer.add(this.konva.group); this.konva.layer.add(this.konva.objectGroup);
this.konva.layer.add(this.konva.transformer); this.konva.layer.add(this.konva.transformer);
this.konva.group.add(this.konva.objectGroup); this.konva.layer.add(this.konva.interactionRect);
this.konva.group.add(this.konva.interactionRect); this.konva.layer.add(this.konva.bbox);
this.konva.group.add(this.konva.bbox); this.konva.layer.add(this.konva.objectGroupBbox);
this.konva.layer.add(this.konva.positionXLine);
this.konva.layer.add(this.konva.positionYLine);
this.konva.transformer.on('transformstart', () => {
console.log('>>> transformstart');
console.log('interactionRect', {
x: this.konva.interactionRect.x(),
y: this.konva.interactionRect.y(),
scaleX: this.konva.interactionRect.scaleX(),
scaleY: this.konva.interactionRect.scaleY(),
width: this.konva.interactionRect.width(),
height: this.konva.interactionRect.height(),
});
console.log('this.bbox', deepClone(this.bbox));
console.log('this.state.position', this.state.position);
});
this.konva.transformer.on('transform', () => {
// Always snap the interaction rect to the nearest pixel when transforming
const x = Math.round(this.konva.interactionRect.x());
const y = Math.round(this.konva.interactionRect.y());
// Snap its position
this.konva.interactionRect.x(x);
this.konva.interactionRect.y(y);
// Calculate the new scale of the interaction rect such that its width and height snap to the nearest pixel
const targetWidth = Math.max(
Math.round(this.konva.interactionRect.width() * Math.abs(this.konva.interactionRect.scaleX())),
MIN_LAYER_SIZE_PX
);
const scaleX = targetWidth / this.konva.interactionRect.width();
const targetHeight = Math.max(
Math.round(this.konva.interactionRect.height() * Math.abs(this.konva.interactionRect.scaleY())),
MIN_LAYER_SIZE_PX
);
const scaleY = targetHeight / this.konva.interactionRect.height();
// Snap the width and height (via scale) of the interaction rect
this.konva.interactionRect.scaleX(scaleX);
this.konva.interactionRect.scaleY(scaleY);
this.konva.interactionRect.rotation(0);
console.log('>>> transform');
console.log('activeAnchor', this.konva.transformer.getActiveAnchor());
console.log('interactionRect', {
x: this.konva.interactionRect.x(),
y: this.konva.interactionRect.y(),
scaleX: this.konva.interactionRect.scaleX(),
scaleY: this.konva.interactionRect.scaleY(),
width: this.konva.interactionRect.width(),
height: this.konva.interactionRect.height(),
rotation: this.konva.interactionRect.rotation(),
});
// Handle anchor-specific transformations of the layer's objects
const anchor = this.konva.transformer.getActiveAnchor();
// 'top-left'
// 'top-center'
// 'top-right'
// 'middle-right'
// 'middle-left'
// 'bottom-left'
// 'bottom-center'
// 'bottom-right'
if (anchor === 'middle-right') {
// Dragging the anchor to the right
this.konva.objectGroup.setAttrs({
scaleX,
x: x - (x - this.state.position.x) * scaleX,
});
} else if (anchor === 'middle-left') {
// Dragging the anchor to the right
this.konva.objectGroup.setAttrs({
scaleX,
x: x - (x - this.state.position.x) * scaleX,
});
} else if (anchor === 'bottom-center') {
// Resize the interaction rect downwards
this.konva.objectGroup.setAttrs({
scaleY,
y: y - (y - this.state.position.y) * scaleY,
});
} else if (anchor === 'bottom-right') {
// Resize the interaction rect to the right and downwards via scale
this.konva.objectGroup.setAttrs({
scaleX,
scaleY,
x: x - (x - this.state.position.x) * scaleX,
y: y - (y - this.state.position.y) * scaleY,
});
} else if (anchor === 'top-center') {
// Resize the interaction rect to the upwards via scale & y position
this.konva.objectGroup.setAttrs({
y,
scaleY,
});
}
this.konva.objectGroupBbox.setAttrs({
x: this.konva.objectGroup.x(),
y: this.konva.objectGroup.y(),
rotation: this.konva.objectGroup.rotation(),
scaleX: this.konva.objectGroup.scaleX(),
scaleY: this.konva.objectGroup.scaleY(),
});
});
// this.konva.transformer.on('transform', () => { // this.konva.transformer.on('transform', () => {
// console.log(this.konva.interactionRect.position()); // // We need to snap the transform to the nearest pixel - both the position and the scale
// this.konva.objectGroup.setAttrs({
// // Snap the interaction rect to the nearest pixel
// this.konva.interactionRect.x(Math.round(this.konva.interactionRect.x()));
// this.konva.interactionRect.y(Math.round(this.konva.interactionRect.y()));
// // Calculate the new scale of the interaction rect such that its width and height snap to the nearest pixel
// const roundedScaledWidth = Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX());
// const correctedScaleX = roundedScaledWidth / this.konva.interactionRect.width();
// const roundedScaledHeight = Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY());
// const correctedScaleY = roundedScaledHeight / this.konva.interactionRect.height();
// // Update the interaction rect's scale to the corrected scale
// this.konva.interactionRect.scaleX(correctedScaleX);
// this.konva.interactionRect.scaleY(correctedScaleY);
// console.log('>>> transform');
// console.log('activeAnchor', this.konva.transformer.getActiveAnchor());
// console.log('interactionRect', {
// x: this.konva.interactionRect.x(),
// y: this.konva.interactionRect.y(),
// scaleX: this.konva.interactionRect.scaleX(), // scaleX: this.konva.interactionRect.scaleX(),
// scaleY: this.konva.interactionRect.scaleY(), // scaleY: this.konva.interactionRect.scaleY(),
// // rotation: this.konva.interactionRect.rotation(), // width: this.konva.interactionRect.width(),
// x: this.konva.interactionRect.x(), // height: this.konva.interactionRect.height(),
// t: this.konva.interactionRect.y(), // rotation: this.konva.interactionRect.rotation(),
// });
// // Update the object group to reflect the new scale and position of the interaction rect
// this.konva.objectGroup.setAttrs({
// // The scale is the same as the interaction rect
// scaleX: this.konva.interactionRect.scaleX(),
// scaleY: this.konva.interactionRect.scaleY(),
// rotation: this.konva.interactionRect.rotation(),
// // We need to do some compensation for the new position. The bounds of the object group may be different from the
// // interaction rect/bbox, because the object group may have eraser strokes that are not included in the bbox.
// x:
// this.konva.interactionRect.x() -
// Math.abs(this.konva.interactionRect.x() - this.state.position.x) * this.konva.interactionRect.scaleX(),
// y:
// this.konva.interactionRect.y() -
// Math.abs(this.konva.interactionRect.y() - this.state.position.y) * this.konva.interactionRect.scaleY(),
// // x: this.konva.interactionRect.x() + (this.konva.interactionRect.x() - this.state.position.x) * this.konva.interactionRect.scaleX(),
// // y: this.konva.interactionRect.y() + (this.konva.interactionRect.y() - this.state.position.y) * this.konva.interactionRect.scaleY(),
// });
// this.konva.objectGroupBbox.setAttrs({
// x: this.konva.objectGroup.x(),
// y: this.konva.objectGroup.y(),
// scaleX: this.konva.objectGroup.scaleX(),
// scaleY: this.konva.objectGroup.scaleY(),
// }); // });
// }); // });
this.konva.transformer.on('transformend', () => { this.konva.transformer.on('transformend', () => {
console.log(this.bbox);
this.bbox = { this.bbox = {
x: this.bbox.x * this.konva.group.scaleX(), x: this.konva.interactionRect.x(),
y: this.bbox.y * this.konva.group.scaleY(), y: this.konva.interactionRect.y(),
width: this.bbox.width * this.konva.group.scaleX(), width: Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX()),
height: this.bbox.height * this.konva.group.scaleY(), height: Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY()),
}; };
console.log(this.bbox);
this.renderBbox(); // this.manager.stateApi.onPosChanged(
this.manager.stateApi.onScaleChanged( // {
// id: this.id,
// position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() },
// },
// 'layer'
// );
});
this.konva.interactionRect.on('dragmove', () => {
// Snap the interaction rect to the nearest pixel
this.konva.interactionRect.x(Math.round(this.konva.interactionRect.x()));
this.konva.interactionRect.y(Math.round(this.konva.interactionRect.y()));
// The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding
// and border
this.konva.bbox.setAttrs({
x: this.konva.interactionRect.x() - CanvasLayer.BBOX_PADDING_PX / this.manager.stage.scaleX(),
y: this.konva.interactionRect.y() - CanvasLayer.BBOX_PADDING_PX / this.manager.stage.scaleX(),
});
// The object group is translated by the difference between the interaction rect's new and old positions (which is
// stored as this.bbox)
this.konva.objectGroup.setAttrs({
x: this.state.position.x + this.konva.interactionRect.x() - this.bbox.x,
y: this.state.position.y + this.konva.interactionRect.y() - this.bbox.y,
});
const rect = this.konva.objectGroup.getClientRect({ skipTransform: true });
this.konva.objectGroupBbox.setAttrs({ ...rect, x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() });
});
this.konva.interactionRect.on('dragend', () => {
// Update the bbox
this.bbox.x = this.konva.interactionRect.x();
this.bbox.y = this.konva.interactionRect.y();
// Update internal state
this.manager.stateApi.onPosChanged(
{ {
id: this.id, id: this.id,
scale: this.konva.group.scaleX(), position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() },
position: { x: this.konva.group.x(), y: this.konva.group.y() },
}, },
'layer' 'layer'
); );
}); });
this.konva.group.on('dragend', () => {
this.manager.stateApi.onPosChanged(
{ id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } },
'layer'
);
});
this.objects = new Map(); this.objects = new Map();
this.drawingBuffer = null; this.drawingBuffer = null;
this.state = state; this.state = state;
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
console.log(this);
} }
private static get DEFAULT_BBOX_RECT() { private static get DEFAULT_BBOX_RECT() {
@ -158,12 +347,24 @@ export class CanvasLayer {
this.state = state; this.state = state;
// Update the layer's position and listening state // Update the layer's position and listening state
this.konva.group.setAttrs({ this.konva.objectGroup.setAttrs({
x: state.position.x, x: state.position.x,
y: state.position.y, y: state.position.y,
scaleX: 1, scaleX: 1,
scaleY: 1, scaleY: 1,
}); });
this.konva.positionXLine.points([
state.position.x,
-this.manager.stage.y(),
state.position.x,
this.manager.stage.y() + this.manager.stage.height() / this.manager.stage.scaleY(),
]);
this.konva.positionYLine.points([
-this.manager.stage.x(),
state.position.y,
this.manager.stage.x() + this.manager.stage.width() / this.manager.stage.scaleX(),
state.position.y,
]);
let didDraw = false; let didDraw = false;
@ -264,7 +465,7 @@ export class CanvasLayer {
if (didDraw) { if (didDraw) {
if (this.objects.size > 0) { if (this.objects.size > 0) {
this.getBbox(); // this.getBbox();
} else { } else {
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
this.renderBbox(); this.renderBbox();
@ -296,8 +497,8 @@ export class CanvasLayer {
if (!this.konva.objectGroup.isCached() || didDraw) { if (!this.konva.objectGroup.isCached() || didDraw) {
// this.konva.objectGroup.cache(); // this.konva.objectGroup.cache();
} }
// Activate the transformer // Activate the transformer - it *must* be transforming the interactionRect, not the group!
this.konva.transformer.nodes([this.konva.group]); this.konva.transformer.nodes([this.konva.interactionRect]);
this.konva.transformer.forceUpdate(); this.konva.transformer.forceUpdate();
this.konva.transformer.visible(true); this.konva.transformer.visible(true);
} else if (selectedTool === 'move') { } else if (selectedTool === 'move') {
@ -341,16 +542,24 @@ export class CanvasLayer {
renderBbox() { renderBbox() {
const isSelected = this.manager.stateApi.getIsSelected(this.id); const isSelected = this.manager.stateApi.getIsSelected(this.id);
const selectedTool = this.manager.stateApi.getToolState().selected; const selectedTool = this.manager.stateApi.getToolState().selected;
const scale = this.manager.stage.scaleX();
const hasBbox = this.bbox.width !== 0 && this.bbox.height !== 0; const hasBbox = this.bbox.width !== 0 && this.bbox.height !== 0;
this.konva.bbox.visible(hasBbox); this.konva.bbox.visible(hasBbox && isSelected && selectedTool === 'move');
this.konva.interactionRect.visible(hasBbox); this.konva.interactionRect.visible(hasBbox);
const rect = this.konva.objectGroup.getClientRect({ skipTransform: true });
this.konva.objectGroupBbox.setAttrs({
...rect,
x: this.konva.objectGroup.x(),
y: this.konva.objectGroup.y(),
scaleX: 1,
scaleY: 1,
});
this.konva.bbox.setAttrs({ this.konva.bbox.setAttrs({
x: this.bbox.x, x: this.bbox.x - CanvasLayer.BBOX_PADDING_PX / scale,
y: this.bbox.y, y: this.bbox.y - CanvasLayer.BBOX_PADDING_PX / scale,
width: this.bbox.width, width: this.bbox.width + (CanvasLayer.BBOX_PADDING_PX / scale) * 2,
height: this.bbox.height, height: this.bbox.height + (CanvasLayer.BBOX_PADDING_PX / scale) * 2,
scaleX: 1, scaleX: 1,
scaleY: 1, scaleY: 1,
strokeWidth: 1 / this.manager.stage.scaleX(), strokeWidth: 1 / this.manager.stage.scaleX(),
@ -374,7 +583,9 @@ export class CanvasLayer {
let needsPixelBbox = false; let needsPixelBbox = false;
const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); const rect = this.konva.objectGroup.getClientRect({ skipTransform: true });
// console.log('rect', rect);
console.log('getBbox rect', rect);
// If there are no eraser strokes, we can use the client rect directly // If there are no eraser strokes, we can use the client rect directly
for (const obj of this.objects.values()) { for (const obj of this.objects.values()) {
if (obj instanceof CanvasEraserLine) { if (obj instanceof CanvasEraserLine) {
@ -387,7 +598,12 @@ export class CanvasLayer {
if (rect.width === 0 || rect.height === 0) { if (rect.width === 0 || rect.height === 0) {
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
} else { } else {
this.bbox = rect; this.bbox = {
x: this.konva.objectGroup.x() + rect.x,
y: this.konva.objectGroup.y() + rect.y,
width: rect.width,
height: rect.height,
};
} }
this.renderBbox(); this.renderBbox();
return; return;
@ -395,41 +611,31 @@ export class CanvasLayer {
// We have eraser strokes - we must calculate the bbox using pixel data // We have eraser strokes - we must calculate the bbox using pixel data
// const a = window.performance.now();
const clone = this.konva.objectGroup.clone(); const clone = this.konva.objectGroup.clone();
// const b = window.performance.now();
// console.log('cloned layer', b - a);
// const c = window.performance.now();
const canvas = clone.toCanvas(); const canvas = clone.toCanvas();
// const d = window.performance.now();
// console.log('got canvas', d - c);
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) { if (!ctx) {
return; return;
} }
const imageData = ctx.getImageData(0, 0, rect.width, rect.height); const imageData = ctx.getImageData(0, 0, rect.width, rect.height);
// const e = window.performance.now();
// console.log('got image data', e - d);
this.manager.requestBbox( this.manager.requestBbox(
{ buffer: imageData.data.buffer, width: imageData.width, height: imageData.height }, { buffer: imageData.data.buffer, width: imageData.width, height: imageData.height },
(extents) => { (extents) => {
// console.log('extents', extents);
if (extents) { if (extents) {
const { minX, minY, maxX, maxY } = extents; const { minX, minY, maxX, maxY } = extents;
this.bbox = { this.bbox = {
x: rect.x + minX, x: this.konva.objectGroup.x() + rect.x + minX,
y: rect.y + minY, y: this.konva.objectGroup.y() + rect.y + minY,
width: maxX - minX, width: maxX - minX,
height: maxY - minY, height: maxY - minY,
}; };
} else { } else {
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
} }
console.log('new bbox', deepClone(this.bbox));
this.renderBbox(); this.renderBbox();
clone.destroy(); clone.destroy();
// console.log('bbox', this.bbox);
} }
); );
// console.log('transferred message', window.performance.now() - e);
} }
} }

View File

@ -1,5 +1,5 @@
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getScaledCursorPosition } from 'features/controlLayers/konva/util'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util';
import type { import type {
CanvasV2State, CanvasV2State,
Coordinate, Coordinate,
@ -24,7 +24,7 @@ import { getBrushLineId, getEraserLineId, getRectShapeId } from './naming';
* @param setLastCursorPos The callback to store the cursor pos * @param setLastCursorPos The callback to store the cursor pos
*/ */
const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: CanvasManager['stateApi']['setLastCursorPos']) => { const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: CanvasManager['stateApi']['setLastCursorPos']) => {
const pos = getScaledCursorPosition(stage); const pos = getScaledFlooredCursorPosition(stage);
if (!pos) { if (!pos) {
return null; return null;
} }

View File

@ -216,24 +216,25 @@ export const layersReducers = {
} }
for (const obj of layer.objects) { for (const obj of layer.objects) {
if (obj.type === 'brush_line') { if (obj.type === 'brush_line') {
obj.points = obj.points.map((point) => point * scale); obj.points = obj.points.map((point) => Math.round(point * scale));
obj.strokeWidth *= scale; obj.strokeWidth = Math.round(obj.strokeWidth * scale);
} else if (obj.type === 'eraser_line') { } else if (obj.type === 'eraser_line') {
obj.points = obj.points.map((point) => point * scale); obj.points = obj.points.map((point) => Math.round(point * scale));
obj.strokeWidth *= scale; obj.strokeWidth = Math.round(obj.strokeWidth * scale);
} else if (obj.type === 'rect_shape') { } else if (obj.type === 'rect_shape') {
obj.x *= scale; obj.x = Math.round(obj.x * scale);
obj.y *= scale; obj.y = Math.round(obj.y * scale);
obj.height *= scale; obj.height = Math.round(obj.height * scale);
obj.width *= scale; obj.width = Math.round(obj.width * scale);
} else if (obj.type === 'image') { } else if (obj.type === 'image') {
obj.x *= scale; obj.x = Math.round(obj.x * scale);
obj.y *= scale; obj.y = Math.round(obj.y * scale);
obj.height *= scale; obj.height = Math.round(obj.height * scale);
obj.width *= scale; obj.width = Math.round(obj.width * scale);
} }
} }
layer.position = position; layer.position.x = Math.round(position.x);
layer.position.y = Math.round(position.y);
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
state.layers.imageCache = null; state.layers.imageCache = null;
}, },
@ -265,3 +266,12 @@ export const layersReducers = {
state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
}, },
} satisfies SliceCaseReducers<CanvasV2State>; } satisfies SliceCaseReducers<CanvasV2State>;
const scalePoints = (points: number[], scaleX: number, scaleY: number) => {
const newPoints: number[] = [];
for (let i = 0; i < points.length; i += 2) {
newPoints.push(points[i]! * scaleX);
newPoints.push(points[i + 1]! * scaleY);
}
return newPoints;
};

View File

@ -924,7 +924,7 @@ export type CanvasV2State = {
export type StageAttrs = { position: Coordinate; dimensions: Dimensions; scale: number }; export type StageAttrs = { position: Coordinate; dimensions: Dimensions; scale: number };
export type PositionChangedArg = { id: string; position: Coordinate }; export type PositionChangedArg = { id: string; position: Coordinate };
export type ScaleChangedArg = { id: string; scale: number; position: Coordinate }; export type ScaleChangedArg = { id: string; scale: Coordinate; position: Coordinate };
export type BboxChangedArg = { id: string; bbox: Rect | null }; export type BboxChangedArg = { id: string; bbox: Rect | null };
export type EraserLineAddedArg = { export type EraserLineAddedArg = {
id: string; id: string;