Fix rotation point and add migration for old files

This commit is contained in:
Skyler Lehmkuhl 2024-12-30 14:44:11 -05:00
parent df32b43915
commit f610ef733d
2 changed files with 153 additions and 12 deletions

View File

@ -3,7 +3,7 @@ import * as fitCurve from '/fit-curve.js';
import { Bezier } from "/bezier.js";
import { Quadtree } from './quadtree.js';
import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.js';
import { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels, lerpColor, lerp, camelToWords, generateWaveform, floodFillRegion, getShapeAtPoint, hslToRgb, drawCheckerboardBackground, hexToHsl, hsvToRgb, hexToHsv, rgbToHex, clamp, drawBorderedRect, drawCenteredText, drawHorizontallyCenteredText, deepMerge, getPointNearBox, arraysAreEqual, drawRegularPolygon, getFileExtension, createModal, deeploop, signedAngleBetweenVectors } from './utils.js';
import { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels, lerpColor, lerp, camelToWords, generateWaveform, floodFillRegion, getShapeAtPoint, hslToRgb, drawCheckerboardBackground, hexToHsl, hsvToRgb, hexToHsv, rgbToHex, clamp, drawBorderedRect, drawCenteredText, drawHorizontallyCenteredText, deepMerge, getPointNearBox, arraysAreEqual, drawRegularPolygon, getFileExtension, createModal, deeploop, signedAngleBetweenVectors, rotateAroundPoint, getRotatedBoundingBox, rotateAroundPointIncremental } from './utils.js';
import { backgroundColor, darkMode, foregroundColor, frameWidth, gutterHeight, highlight, iconSize, triangleSize, labelColor, layerHeight, layerWidth, scrubberColor, shade, shadow } from './styles.js';
import { Icon } from './icon.js';
const { writeTextFile: writeTextFile, readTextFile: readTextFile, writeFile: writeFile, readFile: readFile }= window.__TAURI__.fs;
@ -772,6 +772,7 @@ let actions = {
editFrame: {
create: (frame) => {
redoStack.length = 0; // Clear redo stack
console.log(frame.idx in startProps)
if (!(frame.idx in startProps)) return;
let action = {
newState: structuredClone(frame.keys),
@ -1036,11 +1037,23 @@ let actions = {
redoStack.length = 0
let serializableShapes = []
let serializableObjects = []
let bbox;
for (let shape of context.shapeselection) {
serializableShapes.push(shape.idx)
if (bbox==undefined) {
bbox = shape.bbox()
} else {
growBoundingBox(bbox, shape.bbox())
}
}
for (let object of context.selection) {
serializableObjects.push(object.idx)
// TODO: rotated bbox
if (bbox==undefined) {
bbox = object.bbox()
} else {
growBoundingBox(bbox, object.bbox())
}
}
context.shapeselection = []
context.selection = []
@ -1049,7 +1062,8 @@ let actions = {
objects: serializableObjects,
groupUuid: uuidv4(),
parent: context.activeObject.idx,
frame: context.activeObject.currentFrame.idx
frame: context.activeObject.currentFrame.idx,
position: {x: (bbox.x.min+bbox.x.max)/2, y: (bbox.y.min+bbox.y.max)/2}
}
undoStack.push({name: 'group', action: action})
actions.group.execute(action)
@ -1062,15 +1076,16 @@ let actions = {
let frame = action.frame ? pointerList[action.frame] : parent.currentFrame
for (let shapeIdx of action.shapes) {
let shape = pointerList[shapeIdx]
shape.translate(-action.position.x, -action.position.y)
group.currentFrame.addShape(shape)
frame.removeShape(shape)
}
for (let objectIdx of action.objects) {
let object = pointerList[objectIdx]
group.addObject(object, object.x, object.y)
group.addObject(object, object.x - position.x, object.y - position.y)
parent.removeChild(object)
}
parent.addObject(group)
parent.addObject(group, action.position.x, action.position.y)
if (context.activeObject==parent && context.selection.length==0 && context.shapeselection.length==0) {
context.selection.push(group)
}
@ -2011,7 +2026,11 @@ class BaseShape {
}
}
recalculateBoundingBox() {
this.boundingBox = undefined
for (let curve of this.curves) {
if (!this.boundingBox) {
this.boundingBox = curve.bbox();
}
growBoundingBox(this.boundingBox, curve.bbox())
}
}
@ -2365,6 +2384,15 @@ class Shape extends BaseShape {
const pointsSorted = angles.sort((a, b) => a.angle - b.angle);
return pointsSorted
}
translate(x, y) {
for (let curve of this.curves) {
for (let i in curve.points) {
const point = curve.points[i]
curve.points[i] = {x: point.x + x, y: point.y + y}
}
}
this.update()
}
updateVertices() {
this.vertices = []
let utils = Bezier.getUtils()
@ -2728,9 +2756,9 @@ class GraphicsObject {
let bbox = undefined;
for (let item of context.selection) {
if (bbox==undefined) {
bbox = structuredClone(item.bbox())
bbox = getRotatedBoundingBox(item, debugPoints)
} else {
growBoundingBox(bbox, item.bbox())
growBoundingBox(bbox, getRotatedBoundingBox(item, debugPoints))
}
}
if (bbox != undefined) {
@ -3039,8 +3067,11 @@ async function _save(path) {
}
return value;
}
for (let action of undoStack) {
console.log(action.name)
}
const fileData = {
version: "1.5",
version: "1.6",
width: config.fileWidth,
height: config.fileHeight,
fps: config.framerate,
@ -3109,15 +3140,55 @@ async function _open(path, returnJson=false) {
await messageDialog("File has no content!", {title: "Parse error", kind: 'error'})
return
}
const objectOffsets = {}
for (let action of file.actions) {
if (!(action.name in actions)) {
await messageDialog(`Invalid action ${action.name}. File may be corrupt.`, { title: "Error", kind: 'error'})
return
}
console.log(action.name)
// Data fixes
if (file.version <= "1.5") {
// Fix coordinates of objects
if (action.name=="group") {
let bbox;
for (let i of action.action.shapes) {
const shape = pointerList[i]
if (bbox==undefined) {
bbox = shape.bbox()
} else {
growBoundingBox(bbox, shape.bbox())
}
}
for (let i of action.action.objects) {
const object = pointerList[i]// TODO: rotated bbox
if (bbox==undefined) {
bbox = object.bbox()
} else {
growBoundingBox(bbox, object.bbox())
}
}
const position = {x: (bbox.x.min+bbox.x.max)/2, y: (bbox.y.min+bbox.y.max)/2}
action.action.position = position
objectOffsets[action.action.groupUuid] = position
} else if (action.name=="editFrame") {
for (let key in action.action.newState) {
if (key in objectOffsets) {
action.action.newState[key].x += objectOffsets[key].x
action.action.newState[key].y += objectOffsets[key].y
action.action.oldState[key].x += objectOffsets[key].x
action.action.oldState[key].y += objectOffsets[key].y
}
}
}
}
await actions[action.name].execute(action.action)
undoStack.push(action)
}
lastSaveIndex = undoStack.length;
filePath = path
// Tauri thinks it is setting the title here, but it isn't getting updated
@ -3776,6 +3847,7 @@ function stage() {
selection: structuredClone(selection)
}
}
context.activeObject.currentFrame.saveState()
} else {
stage.style.cursor = "default"
}
@ -4077,9 +4149,9 @@ function stage() {
let bbox = undefined;
for (let item of context.selection) {
if (bbox==undefined) {
bbox = structuredClone(item.bbox())
bbox = getRotatedBoundingBox(item)
} else {
growBoundingBox(bbox, item.bbox())
growBoundingBox(bbox, getRotatedBoundingBox(item))
}
}
if (bbox==undefined) break;
@ -4108,16 +4180,21 @@ function stage() {
} else if (context.dragDirection.indexOf('e') != -1) {
current.x.max = mouse.x
}
// Calculate the translation difference between current and initial values
let delta_x = current.x.min - initial.x.min;
let delta_y = current.y.min - initial.y.min;
if (context.dragDirection == 'r') {
let pivot = {
x: (initial.x.min+initial.x.max)/2,
y: (initial.y.min+initial.y.max)/2,
}
current.rotation = signedAngleBetweenVectors(pivot, initial.mouse, mouse)
const {dx, dy} = rotateAroundPointIncremental(current.x.min, current.y.min, pivot, current.rotation)
// delta_x -= dx
// delta_y -= dy
// console.log(dx, dy)
}
// Calculate the translation difference between current and initial values
const delta_x = current.x.min - initial.x.min;
const delta_y = current.y.min - initial.y.min;
// This is probably unnecessary since initial rotation is 0
const delta_rot = current.rotation - initial.rotation

View File

@ -581,6 +581,67 @@ function signedAngleBetweenVectors(a, b, c) {
return signedAngle;
}
function rotateAroundPointIncremental(x, y, point, angle) {
const { x: newX, y: newY } = rotateAroundPoint(x, y, point, angle)
const dx = newX - x
const dy = newY - y
return { dx, dy }
}
function rotateAroundPoint(x, y, point, angle) {
const dx = x - point.x;
const dy = y - point.y;
const cosAngle = Math.cos(angle);
const sinAngle = Math.sin(angle);
const rotatedX = point.x + (dx * cosAngle - dy * sinAngle);
const rotatedY = point.y + (dx * sinAngle + dy * cosAngle);
return { x: rotatedX, y: rotatedY };
}
function getRotatedBoundingBox(object, debugPoints=[]) {
const bbox = object.bbox(); // Get the bounding box of the object without transformation
const { x: { min: xMin, max: xMax }, y: { min: yMin, max: yMax } } = bbox;
// Calculate the four corners of the bounding box
const corners = [
{ x: xMin, y: yMin }, // Bottom-left
{ x: xMax, y: yMin }, // Bottom-right
{ x: xMin, y: yMax }, // Top-left
{ x: xMax, y: yMax } // Top-right
];
const center = {
x: (xMin + xMax) / 2,
y: (yMin + yMax) / 2
}
// Rotate each corner and track the min/max x and y values
let rotatedCorners = corners.map(corner => {
return rotateAroundPoint(corner.x, corner.y, center, object.rotation);
});
debugPoints.length = 0
for (let corner of rotatedCorners) {
debugPoints.push(corner)
}
// Find the new bounding box after rotation
let rotatedXMin = Math.min(...rotatedCorners.map(corner => corner.x));
let rotatedXMax = Math.max(...rotatedCorners.map(corner => corner.x));
let rotatedYMin = Math.min(...rotatedCorners.map(corner => corner.y));
let rotatedYMax = Math.max(...rotatedCorners.map(corner => corner.y));
// Return the new bounding box with min/max x and y values
return {
x: { min: rotatedXMin, max: rotatedXMax },
y: { min: rotatedYMin, max: rotatedYMax }
};
}
function drawBorderedRect(ctx, x, y, width, height, top, bottom, left, right) {
ctx.fillRect(x, y, width, height)
if (top) {
@ -832,6 +893,9 @@ export {
drawCheckerboardBackground,
clamp,
signedAngleBetweenVectors,
rotateAroundPoint,
rotateAroundPointIncremental,
getRotatedBoundingBox,
drawBorderedRect,
drawCenteredText,
drawHorizontallyCenteredText,