Skip to content
135 changes: 80 additions & 55 deletions src/components/mixins/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const annotationMixin = {
updates: [],
isShowingPalette: false,
isShowingPencilPalette: false,
notSave: false,
notSaved: false,
pencilColor: '#ff3860',
pencilWidth: 'big',
textColor: '#ff3860',
Expand All @@ -104,14 +104,19 @@ export const annotationMixin = {
mouseDrawingMaxChangeRate: 0.03,
mouseDrawingPrevPoint: null,
mouseDrawingPrevPressure: null,
mouseDrawingDynamicDistanceMult: null
mouseDrawingDynamicDistanceMult: null,
// Per-instance to avoid mixing actions between sibling players
// (e.g. ProductionNewsFeed mounts many in a v-for). markRaw skips
// Vue's deep proxy on fabric.Object refs.
doneActionStack: markRaw([]),
undoneActionStack: markRaw([]),
silentAnnotation: false,
annotatedPreview: null,
annotationToSave: null,
changesToSave: null
}
},

created() {
this.resetUndoStacks()
},

computed: {
annotationCanvas() {
return this.$refs['annotation-canvas'] // Canvas used by fabric
Expand Down Expand Up @@ -179,13 +184,13 @@ export const annotationMixin = {
if (activeObject._objects) {
activeObject._objects.forEach(obj => {
this.fabricCanvas.add(obj)
this.$options.doneActionStack.pop()
this.doneActionStack.pop()
})
} else {
this.fabricCanvas.add(activeObject)
}
if (persist) {
this.$options.doneActionStack.push({ type: 'add', obj: activeObject })
this.doneActionStack.push({ type: 'add', obj: activeObject })
this.saveAnnotations()
}
},
Expand Down Expand Up @@ -270,18 +275,23 @@ export const annotationMixin = {
*/
deleteObject(activeObject) {
if (activeObject && activeObject._objects) {
activeObject._objects.forEach(obj => {
// ActiveSelection children carry coords relative to the selection's
// center. discardActiveObject() restores them to absolute before we
// remove, so undo re-injects them at their original positions.
const children = [...activeObject._objects]
this.fabricCanvas.discardActiveObject()
children.forEach(obj => {
this.fabricCanvas.remove(obj)
this.addToDeletions(obj)
this.$options.doneActionStack.push({
this.doneActionStack.push({
type: 'remove',
obj
})
})
} else if (activeObject) {
this.fabricCanvas.remove(activeObject)
this.addToDeletions(activeObject)
this.$options.doneActionStack.push({
this.doneActionStack.push({
type: 'remove',
obj: activeObject
})
Expand Down Expand Up @@ -646,9 +656,9 @@ export const annotationMixin = {
tr: false,
mtr: !this.isCurrentUserArtist
})
this.$options.silentAnnnotation = true
this.silentAnnotation = true
canvas.add(path)
this.$options.silentAnnnotation = false
this.silentAnnotation = false
} else if (obj.type === 'i-text' || obj.type === 'text') {
text = new fabric.IText(obj.text, {
...base,
Expand All @@ -675,9 +685,9 @@ export const annotationMixin = {
tr: false,
mtr: false
})
this.$options.silentAnnnotation = true
this.silentAnnotation = true
canvas.add(text)
this.$options.silentAnnnotation = false
this.silentAnnotation = false
} else if (obj.type === 'PSStroke') {
if (obj.canvasWidth) {
let strokeMultiplier = canvasWidth / canvas.width
Expand Down Expand Up @@ -712,9 +722,9 @@ export const annotationMixin = {
tr: false,
mtr: !this.isCurrentUserArtist
})
this.$options.silentAnnnotation = true
this.silentAnnotation = true
canvas.add(psstroke)
this.$options.silentAnnnotation = false
this.silentAnnotation = false
}
} else if (
obj.type === 'rect' ||
Expand Down Expand Up @@ -752,9 +762,9 @@ export const annotationMixin = {
tr: false,
mtr: !this.isCurrentUserArtist
})
this.$options.silentAnnnotation = true
this.silentAnnotation = true
canvas.add(shape)
this.$options.silentAnnnotation = false
this.silentAnnotation = false
return shape
}
return path || text || psstroke
Expand Down Expand Up @@ -943,7 +953,7 @@ export const annotationMixin = {
* stacks.
*/
onObjectAdded(obj) {
if (this.$options.silentAnnnotation) return
if (this.silentAnnotation) return
let o = obj.target ? obj.target : obj.targets[0]
o = this.setObjectData(o)
// if (this.fabricCanvas.width < 420) o.strokeWidth *= 2
Expand Down Expand Up @@ -1002,59 +1012,75 @@ export const annotationMixin = {
* Clear all action stacks.
*/
resetUndoStacks() {
this.$options.doneActionStack = []
this.$options.undoneActionStack = []
this.doneActionStack.length = 0
this.undoneActionStack.length = 0
},

/*
* Add a add action to the stack.
*/
stackAddAction({ target }) {
this.$options.doneActionStack.push({ type: 'add', obj: target })
this.doneActionStack.push({ type: 'add', obj: target })
target.lockScalingX = true
target.lockScalingY = true
target.rotation = true
},

/*
* Resolve the fabric.Object an action targets. After a canvas reload
* (e.g. Esc-exit fullscreen) the entry holds a stale instance; look it
* up by id on the live canvas. Groups aren't in the canvas as a whole,
* fall back to the stored reference.
*/
resolveActionObject(action) {
if (action.obj?._objects) return action.obj
return this.getObjectById(action.obj.id) ?? action.obj
},

/*
* Undo last action, update actions stack.
*/
undoLastAction() {
const action = this.$options.doneActionStack.pop()
if (action && action.obj) {
if (action.type === 'add') {
this.deleteObject(action.obj)
this.addToDeletions(action.obj)
this.removeFromAdditions(action.obj)
} else if (action.type === 'remove') {
this.addObject(action.obj)
this.addToAdditions(action.obj)
this.removeFromDeletions(action.obj)
}
this.$options.doneActionStack.pop()
this.$options.undoneActionStack.push(action)
const action = this.doneActionStack.pop()
if (!action?.obj) return
const obj = this.resolveActionObject(action)
// Snapshot length to drop side-effect pushes from deleteObject/addObject (incl. groups)
const stackLengthBefore = this.doneActionStack.length
if (action.type === 'add') {
this.deleteObject(obj)
this.removeFromAdditions(obj)
} else if (action.type === 'remove') {
// addObject's 'object:added' fires onObjectAdded → addToAdditions.
this.addObject(obj)
this.removeFromDeletions(obj)
}
this.doneActionStack.length = stackLengthBefore
this.undoneActionStack.push(action)
},

/*
* Apply last undone action, update actions stack.
*/
redoLastAction() {
const action = this.$options.undoneActionStack.pop()
if (action) {
if (action.type === 'add') {
this.addObject(action.obj)
} else if (action.type === 'remove') {
this.deleteObject(action.obj)
}
const action = this.undoneActionStack.pop()
if (!action?.obj) return
const obj = this.resolveActionObject(action)
// Snapshot length to drop side-effect pushes from deleteObject/addObject (incl. groups)
const stackLengthBefore = this.doneActionStack.length
if (action.type === 'add') {
this.addObject(obj)
} else if (action.type === 'remove') {
this.deleteObject(obj)
}
this.doneActionStack.length = stackLengthBefore
this.doneActionStack.push(action)
},

/*
* Clear all actions in the undone stack.
*/
clearUndoneStack() {
this.$options.undoneActionStack = []
this.undoneActionStack.length = 0
},

// Canvas
Expand Down Expand Up @@ -1153,11 +1179,10 @@ export const annotationMixin = {
setupFabricCanvas() {
if (!this.annotationCanvas) return

const canvasId = this.annotationCanvas.id

// Pass the DOM element via $refs to avoid id collisions across instances.
// Use markRaw() to avoid reactivity on Fabric Canvas
this.fabricCanvas = markRaw(
new fabric.Canvas(canvasId, {
new fabric.Canvas(this.annotationCanvas, {
fireRightClick: true
})
)
Expand Down Expand Up @@ -1456,8 +1481,8 @@ export const annotationMixin = {
*/
startAnnotationSaving(preview, annotations) {
this.notSaved = true
this.$options.annotatedPreview = preview
this.$options.annotationToSave = setTimeout(() => {
this.annotatedPreview = markRaw(preview)
this.annotationToSave = setTimeout(() => {
this.endAnnotationSaving()
}, 3000)
},
Expand All @@ -1468,19 +1493,19 @@ export const annotationMixin = {
*/
endAnnotationSaving() {
if (this.notSaved) {
const preview = this.$options.annotatedPreview
this.$options.changesToSave = {
const preview = this.annotatedPreview
this.changesToSave = markRaw({
preview,
additions: [...this.additions],
updates: [...this.updates],
deletions: [...this.deletions]
}
})
this.clearModifications()
clearTimeout(this.$options.annotationToSave)
clearTimeout(this.annotationToSave)
this.notSaved = false
this.$emit('annotation-changed', this.$options.changesToSave)
this.$emit('annotation-changed', this.changesToSave)
if (this.onAnnotationChanged) {
this.onAnnotationChanged(this.$options.changesToSave)
this.onAnnotationChanged(this.changesToSave)
}
}
},
Expand Down
4 changes: 3 additions & 1 deletion src/components/mixins/player.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { markRaw } from 'vue'
import { mapActions, mapGetters } from 'vuex'

import {
formatTime,
formatFrame,
Expand Down Expand Up @@ -1360,7 +1362,7 @@ export const playerMixin = {
if (!this.notSaved) {
this.startAnnotationSaving(preview, annotations)
} else {
this.$options.changesToSave = { preview, annotations }
this.changesToSave = markRaw({ preview, annotations })
}

// Update information locally
Expand Down
6 changes: 4 additions & 2 deletions src/components/previews/PreviewPlayer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1362,9 +1362,11 @@ export default {
const width = dimensions.width
const height = dimensions.height

// Pass DOM elements via $refs to avoid id collisions across instances.

// Use markRaw() to avoid reactivity on Fabric Canvas
this.fabricCanvas = markRaw(
new fabric.Canvas(this.canvasId, {
new fabric.Canvas(this.$refs['annotation-canvas'], {
fireRightClick: true,
width,
height,
Expand All @@ -1379,7 +1381,7 @@ export default {
brush.pressureManager.fallback = 0.5 // Fallback value for mouse/touch
this.fabricCanvas.freeDrawingBrush = brush
this.fabricCanvasComparison = new fabric.StaticCanvas(
this.canvasId + '-comparison'
this.$refs['annotation-canvas-comparison']
)
this.configureCanvas()
},
Expand Down
Loading