Initial undo/redo support

This commit is contained in:
Skyler Lehmkuhl 2024-11-19 21:45:47 -05:00
parent 709bd46ab8
commit 45a055250b
2 changed files with 286 additions and 52 deletions

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg7384"
height="16"
width="16"
version="1.1"
sodipodi:docname="gimp-prefs-systemc.svg"
viewBox="0 0 16 16"
inkscape:version="0.92pre1 unknown">
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview8"
showgrid="true"
inkscape:zoom="43.1875"
inkscape:cx="5.725592"
inkscape:cy="8"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg7384">
<inkscape:grid
type="xygrid"
id="grid4233" />
</sodipodi:namedview>
<metadata
id="metadata90">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Gnome Symbolic Icon Theme</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<title
id="title9167">Gnome Symbolic Icon Theme</title>
<g
id="g4953"
transform="matrix(1.0088031,0,0,1.0014609,-445.94089,-462.66315)">
<path
id="path3908"
style="color:#000000;text-indent:0;text-transform:none;fill:#bebebe"
d="m 445.55,462.09 c -0.39933,0 -0.78638,0.0916 -1.1433,0.21572 l 1.8849,1.8797 c 0.38735,0.38627 0.38735,1.0004 0,1.3867 l -0.71069,0.70874 c -0.38735,0.38628 -1.0031,0.38628 -1.3905,0 l -1.8849,-1.8797 c -0.12444,0.35591 -0.2163,0.74191 -0.2163,1.1402 0,1.9061 1.5494,3.4513 3.4608,3.4513 0.39933,0 0.78638,-0.0916 1.1433,-0.2157 l 1.1742,1.171 a 2.4722,2.4654 0 0 1 0.0618,0 l 2.0703,-2.0646 -1.2051,-1.2018 c 0.12444,-0.35592 0.2163,-0.74191 0.2163,-1.1402 0,-1.9061 -1.5494,-3.4513 -3.4608,-3.4513 z m 6.5507,7.8886 -2.0703,2.0646 a 2.4722,2.4654 0 0 1 0.0309,0.0924 l 1.1433,1.1402 c -0.12444,0.35596 -0.2163,0.74196 -0.2163,1.1402 0,1.9061 1.5494,3.4513 3.4608,3.4513 0.43346,0 0.8536,-0.10141 1.236,-0.24653 l -2.0085,-2.003 c -0.38735,-0.38629 -0.38735,-1.0312 0,-1.4175 l 0.67979,-0.67792 c 0.19367,-0.19315 0.45794,-0.30816 0.71069,-0.30816 0.25276,0 0.51702,0.11501 0.7107,0.30816 l 1.9467,1.9413 c 0.10485,-0.32958 0.1854,-0.68351 0.1854,-1.0477 0,-1.9061 -1.5494,-3.4513 -3.4608,-3.4513 -0.39933,0 -0.78639,0.0916 -1.1433,0.2157 l -1.2051,-1.2018 z"
inkscape:connector-curvature="0" />
<path
id="path3910"
style="color:#000000;text-indent:0;text-transform:none;fill:#bebebe"
d="m 455.86,462 -1.5425,1.4375 c -0.45151,0.42079 -0.5292,1.1488 -0.2663,1.7065 l -5.8882,5.9958 a 1.4917,1.4876 0 0 0 -0.0311,2.5e-4 1.4917,1.4876 0 0 0 -0.84016,-0.1484 1.4917,1.4876 0 0 0 -0.86663,0.44059 l -3.9462,3.9973 a 1.49411,1.49001 0 1 0 2.1294,2.0907 l 3.9462,-3.9973 a 1.4917,1.4876 0 0 0 0.29713,-1.7377 l 5.8885,-5.9648 c 0.55782,0.24837 1.2732,0.14697 1.7068,-0.2922 l 1.41,-1.57 -2,-1.97 z"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -16,6 +16,9 @@ let mode = "draw"
let minSegmentSize = 5;
let maxSmoothAngle = 0.6;
let undoStack = [];
let redoStack = [];
let tools = {
select: {
icon: "/assets/select.svg",
@ -84,6 +87,55 @@ let context = {
let config = {
shortcuts: {
playAnimation: " ",
// undo: "<ctrl>+z"
undo: "z",
redo: "Z",
}
}
// Pointers to all objects
let pointerList = {}
let actions = {
addShape: {
create: (parent, shape) => {
redoStack.length = 0; // Clear redo stack
let serializableCurves = []
for (let curve of shape.curves) {
serializableCurves.push({ points: curve.points })
}
let action = {
parent: parent.idx,
curves: serializableCurves,
startx: shape.startx,
starty: shape.starty,
uuid: uuidv4()
}
undoStack.push({name: "addShape", action: action})
actions.addShape.execute(action)
},
execute: (action) => {
let object = pointerList[action.parent]
console.log(object)
let curvesList = action.curves
let shape = new Shape(action.startx, action.starty, context, action.uuid)
for (let curve of curvesList) {
shape.addCurve(
new Bezier(
curve.points[0].x, curve.points[0].y,
curve.points[1].x, curve.points[1].y,
curve.points[2].x, curve.points[2].y,
curve.points[3].x, curve.points[3].y
))
}
object.addShape(shape)
},
rollback: (action) => {
let object = pointerList[action.parent]
let shape = pointerList[action.uuid]
object.removeShape(shape)
delete pointerList[action.uuid]
}
}
}
@ -125,7 +177,7 @@ function setProperty(context, path, value) {
function selectCurve(context, mouse) {
let mouseTolerance = 15;
for (let shape of context.activeObject.frames[context.activeObject.currentFrame].shapes) {
for (let shape of context.activeObject.currentFrame.shapes) {
if (mouse.x > shape.boundingBox.x.min - mouseTolerance &&
mouse.x < shape.boundingBox.x.max + mouseTolerance &&
mouse.y > shape.boundingBox.y.min - mouseTolerance &&
@ -185,6 +237,36 @@ function hitTest(candidate, object) {
}
}
function pushState() {
// console.log(context)
// let ctx = context.ctx
// context.ctx = undefined
// undoStack.push(window.structuredClone([root,context]))
// context.ctx = ctx
}
function undo() {
let action = undoStack.pop()
if (action) {
actions[action.name].rollback(action.action)
redoStack.push(action)
updateUI()
} else {
console.log("No actions to undo")
}
}
function redo() {
let action = redoStack.pop()
if (action) {
actions[action.name].execute(action.action)
undoStack.push(action)
updateUI()
} else {
console.log("No actions to redo")
}
}
class Curve {
constructor(startx, starty, cp1x, cp1y, cp2x, cp2y, x, y) {
this.startx = startx
@ -199,14 +281,33 @@ class Curve {
}
class Frame {
constructor() {
constructor(uuid) {
this.keys = {}
this.shapes = []
if (!uuid) {
this.idx = uuidv4()
} else {
this.idx = uuid
}
pointerList[this.idx] = this
}
}
class Layer {
constructor(uuid) {
this.frames = [new Frame()]
this.children = []
if (!uuid) {
this.idx = uuidv4()
} else {
this.idx = uuid
}
pointerList[this.idx] = this
}
}
class Shape {
constructor(startx, starty, context, stroked=true) {
constructor(startx, starty, context, uuid=undefined) {
this.startx = startx;
this.starty = starty;
this.curves = [];
@ -215,15 +316,21 @@ class Shape {
this.strokeStyle = context.strokeStyle;
this.lineWidth = context.lineWidth
this.filled = context.fillShape;
this.stroked = stroked;
this.stroked = context.strokeShape || true;
this.boundingBox = {
x: {min: startx, max: starty},
y: {min: starty, max: starty}
}
if (!uuid) {
this.idx = uuidv4()
} else {
this.idx = uuid
}
pointerList[this.idx] = this
}
addCurve(curve) {
this.curves.push(curve)
this.growBoundingBox(curve.bbox())
growBoundingBox(this.boundingBox, curve.bbox())
}
addLine(x, y) {
let lastpoint;
@ -287,27 +394,79 @@ class Shape {
}
this.recalculateBoundingBox()
}
draw(context) {
let ctx = context.ctx;
ctx.beginPath()
ctx.lineWidth = this.lineWidth
ctx.moveTo(this.startx, this.starty)
for (let curve of this.curves) {
ctx.bezierCurveTo(curve.points[1].x, curve.points[1].y,
curve.points[2].x, curve.points[2].y,
curve.points[3].x, curve.points[3].y)
// Debug, show curve endpoints
// ctx.beginPath()
// ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI)
// ctx.fill()
}
if (this.filled) {
if (this.fillImage) {
let pat = ctx.createPattern(this.fillImage, "no-repeat")
ctx.fillStyle = pat
} else {
ctx.fillStyle = this.fillStyle
}
ctx.fill()
}
if (this.stroked) {
ctx.strokeStyle = this.strokeStyle
ctx.stroke()
}
}
}
class GraphicsObject {
constructor() {
constructor(uuid) {
this.x = 0;
this.y = 0;
this.rotation = 0; // in radians
this.scale = 1;
this.idx = uuidv4()
if (!uuid) {
this.idx = uuidv4()
} else {
this.idx = uuid
}
pointerList[this.idx] = this
this.frames = [new Frame()]
this.currentFrame = 0;
this.children = []
this.currentFrameNum = 0;
this.currentLayer = 0;
this.layers = [new Layer()]
// this.children = []
this.shapes = []
}
get activeLayer() {
return this.layers[this.currentLayer]
}
get children() {
return this.layers[this.currentLayer].children
}
get currentFrame() {
return this.layers[this.currentLayer].frames[this.currentFrameNum]
}
get maxFrame() {
let maxFrames = []
for (let layer of this.layers) {
maxFrames.push(layer.frames.length)
}
return Math.max(maxFrames)
}
bbox() {
let bbox;
if (this.frames[this.currentFrame].shapes.length > 0) {
bbox = this.frames[this.currentFrame].shapes[0].boundingBox
for (let shape of this.frames[this.currentFrame].shapes) {
if (this.currentFrame.shapes.length > 0) {
bbox = this.currentFrame.shapes[0].boundingBox
for (let shape of this.currentFrame.shapes) {
growBoundingBox(bbox, shape.boundingBox)
}
}
@ -325,50 +484,24 @@ class GraphicsObject {
let ctx = context.ctx;
ctx.translate(this.x, this.y)
ctx.rotate(this.rotation)
if (this.currentFrame>=this.frames.length) {
this.currentFrame = 0;
if (this.currentFrameNum>=this.maxFrame) {
this.currentFrameNum = 0;
}
for (let shape of this.currentFrame.shapes) {
shape.draw(context)
}
for (let child of this.children) {
let idx = child.idx
if (idx in this.frames[this.currentFrame].keys) {
child.x = this.frames[this.currentFrame].keys[idx].x;
child.y = this.frames[this.currentFrame].keys[idx].y;
child.rotation = this.frames[this.currentFrame].keys[idx].rotation;
child.scale = this.frames[this.currentFrame].keys[idx].scale;
if (idx in this.currentFrame.keys) {
child.x = this.currentFrame.keys[idx].x;
child.y = this.currentFrame.keys[idx].y;
child.rotation = this.currentFrame.keys[idx].rotation;
child.scale = this.currentFrame.keys[idx].scale;
ctx.save()
child.draw(context)
ctx.restore()
}
}
for (let shape of this.frames[this.currentFrame].shapes) {
ctx.beginPath()
ctx.lineWidth = shape.lineWidth
ctx.moveTo(shape.startx, shape.starty)
for (let curve of shape.curves) {
// ctx.moveTo(curve.points[0].x, curve.points[0].y)
ctx.bezierCurveTo(curve.points[1].x, curve.points[1].y,
curve.points[2].x, curve.points[2].y,
curve.points[3].x, curve.points[3].y)
// Debug, show curve endpoints
// ctx.beginPath()
// ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI)
// ctx.fill()
}
if (shape.filled) {
if (shape.fillImage) {
let pat = ctx.createPattern(shape.fillImage, "no-repeat")
ctx.fillStyle = pat
} else {
ctx.fillStyle = shape.fillStyle
}
ctx.fill()
}
if (shape.stroked) {
ctx.strokeStyle = shape.strokeStyle
ctx.stroke()
}
}
if (this == context.activeObject) {
if (context.activeCurve) {
ctx.strokeStyle = "magenta"
@ -405,18 +538,28 @@ class GraphicsObject {
}
}
addShape(shape) {
this.frames[this.currentFrame].shapes.push(shape)
this.currentFrame.shapes.push(shape)
}
addObject(object, x=0, y=0) {
this.children.push(object)
let idx = object.idx
this.frames[this.currentFrame].keys[idx] = {
this.currentFrame.keys[idx] = {
x: x,
y: y,
rotation: 0,
scale: 1,
}
}
removeShape(shape) {
for (let layer of this.layers) {
for (let frame of layer.frames) {
let shapeIndex = frame.shapes.indexOf(shape)
if (shapeIndex >= 0) {
frame.shapes.splice(shapeIndex, 1)
}
}
}
}
}
let root = new GraphicsObject();
@ -443,8 +586,18 @@ window.addEventListener("resize", () => {
})
window.addEventListener("keypress", (e) => {
// let shortcuts = {}
// for (let shortcut of config.shortcuts) {
// shortcut = shortcut.split("+")
// TODO
// }
console.log(e)
if (e.key == config.shortcuts.playAnimation) {
console.log("Spacebar pressed")
} else if (e.key == config.shortcuts.undo && e.ctrlKey == true) {
undo()
} else if (e.key == config.shortcuts.redo && e.ctrlKey == true) {
redo()
}
})
@ -509,9 +662,10 @@ function stage() {
switch (mode) {
case "rectangle":
case "draw":
pushState()
context.mouseDown = true
context.activeShape = new Shape(mouse.x, mouse.y, context, true, true)
context.activeObject.addShape(context.activeShape)
console.log(context.activeObject)
context.lastMouse = mouse
break;
case "select":
@ -558,7 +712,11 @@ function stage() {
if (context.activeShape) {
context.activeShape.addLine(mouse.x, mouse.y)
context.activeShape.simplify(context.simplifyMode)
actions.addShape.create(context.activeObject, context.activeShape)
// context.activeObject.addShape(context.activeShape)
context.activeShape = undefined
console.log(pointerList)
console.log(undoStack)
}
break;
case "rectangle":
@ -863,6 +1021,9 @@ function updateUI() {
context.ctx = ctx;
root.draw(context)
if (context.activeShape) {
context.activeShape.draw(context)
}
// let mouse;
// if (mouseEvent) {