Skip to content

Commit a0bde0a

Browse files
Merge pull request #2030 from frankrousseau/main
[players] Add a vector eraser to the annotation players
2 parents 816d0f9 + f5a7254 commit a0bde0a

31 files changed

Lines changed: 1349 additions & 36 deletions

src/components/modals/ShortcutModal.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ const shortcutGroups = computed(() => [
112112
icon: markRaw(Pencil),
113113
shortcuts: [
114114
{ keys: ['d'], text: t('keyboard.draw') },
115+
{ keys: ['e'], text: t('keyboard.erase') },
115116
{ keys: ['Shift', 'Mouse Drag'], text: t('keyboard.straight_line') },
116117
{ keys: ['Ctrl', 'Mouse Drag'], text: t('keyboard.constant_width') },
117118
{ keys: ['Ctrl', 'z'], text: t('keyboard.undo') },

src/components/pages/Playlist.vue

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,22 +1238,30 @@ export default {
12381238
onNewEntityDropped(info) {
12391239
let entity
12401240
this.setSilent()
1241+
const entityId = info.after.entity_id
12411242
if (this.isAssetPlaylist) {
1242-
entity = assetStore.cache.assetMap.get(info.after)
1243+
entity = assetStore.cache.assetMap.get(entityId)
12431244
} else if (this.isSequencePlaylist) {
1244-
entity = sequenceStore.cache.sequenceMap.get(info.after)
1245+
entity = sequenceStore.cache.sequenceMap.get(entityId)
12451246
} else if (this.isEditPlaylist) {
1246-
entity = editStore.cache.editMap.get(info.after)
1247+
entity = editStore.cache.editMap.get(entityId)
12471248
} else if (this.isEpisodePlaylist) {
1248-
entity = episodeStore.cache.episodeMap.get(info.after)
1249+
entity = episodeStore.cache.episodeMap.get(entityId)
12491250
} else {
1250-
entity = shotStore.cache.shotMap.get(info.after)
1251+
entity = shotStore.cache.shotMap.get(entityId)
12511252
}
12521253
12531254
if (entity && !this.currentEntitiesMap[entity.id]) {
12541255
const notScrollRight = false
12551256
const playlist = this.currentPlaylist
1256-
this.addEntity(entity, playlist, notScrollRight).then(() => {
1257+
this.addEntity(entity, playlist, notScrollRight).then(addedEntity => {
1258+
// The preview file id is only resolved when the entity is added, so
1259+
// the dragged payload can't carry it. Patch it in before replaying
1260+
// the drop so findEntity can locate the newly added entity and move
1261+
// it to the drop position instead of leaving it at the end.
1262+
if (addedEntity) {
1263+
info.after.preview_file_id = addedEntity.preview_file_id
1264+
}
12571265
this.playlistPlayer.onEntityDropped(info)
12581266
this.clearSilent()
12591267
})

src/components/players/annotations/AnnotationCanvas.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ watch(
196196
canvas.defaultCursor = value
197197
canvas.freeDrawingCursor = value
198198
canvas.hoverCursor = value
199+
// Fabric only flushes its cursor properties to the DOM during its own
200+
// mouse events, so a tool switch via keyboard / button (pointer idle)
201+
// wouldn't show until the next move. Push it to the element directly.
202+
if (canvas.upperCanvasEl) canvas.upperCanvasEl.style.cursor = value
199203
}
200204
)
201205

src/components/players/bars/PlayerAnnotationBar.vue

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,34 @@
132132
"
133133
/>
134134

135+
<transition name="slide">
136+
<div
137+
class="annotation-tools"
138+
v-show="isEraserModeOn && (!light || fullScreen)"
139+
>
140+
<pencil-picker
141+
v-bind="pickerState('eraser-pencil')"
142+
:pencil="pencilWidth"
143+
:sizes="pencilPalette"
144+
@change="$emit('change-pencil-width', $event)"
145+
/>
146+
</div>
147+
</transition>
148+
149+
<button-simple
150+
class="flexrow-item"
151+
icon="eraser"
152+
:active="isEraserModeOn"
153+
:title="$t('playlists.actions.annotation_erase')"
154+
@click="$emit('erase-clicked')"
155+
v-if="
156+
isEraserModeOn !== undefined &&
157+
!readOnly &&
158+
(!light || fullScreen) &&
159+
!isConcept
160+
"
161+
/>
162+
135163
<button-simple
136164
class="flexrow-item"
137165
icon="delete"
@@ -292,6 +320,7 @@ defineEmits([
292320
'change-text-color',
293321
'comment-clicked',
294322
'delete-clicked',
323+
'erase-clicked',
295324
'object-background-selected',
296325
'pencil-annotate-clicked',
297326
'redo',
@@ -307,6 +336,7 @@ const isEnvironmentSkybox = defineModel('isEnvironmentSkybox', {
307336
})
308337
const isWireframe = defineModel('isWireframe', { default: false })
309338
const isLaserModeOn = defineModel('isLaserModeOn', { default: undefined })
339+
const isEraserModeOn = defineModel('isEraserModeOn', { default: undefined })
310340
const isShapeMode = defineModel('isShapeMode', { default: undefined })
311341
const currentShape = defineModel('currentShape', { default: undefined })
312342

src/components/players/players/PlaylistPlayer.vue

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@
627627
v-model:current-background="currentBackground"
628628
v-model:current-shape="currentShape"
629629
v-model:is-environment-skybox="isEnvironmentSkybox"
630+
v-model:is-eraser-mode-on="isEraserModeOn"
630631
v-model:is-laser-mode-on="isLaserModeOn"
631632
v-model:is-shape-mode="isShapeMode"
632633
v-model:is-wireframe="isWireframe"
@@ -639,6 +640,7 @@
639640
@change-text-color="onChangeTextColor"
640641
@comment-clicked="onCommentClicked"
641642
@delete-clicked="onDeleteClicked"
643+
@erase-clicked="onEraseClicked"
642644
@object-background-selected="onObjectBackgroundSelected"
643645
@pencil-annotate-clicked="onAnnotateClicked"
644646
@redo="redoLastAction"
@@ -1529,6 +1531,7 @@ const {
15291531
clearComparisonCanvas,
15301532
currentShape,
15311533
isShapeMode,
1534+
isEraserModeOn,
15321535
onChangePencilColor,
15331536
onChangePencilWidth,
15341537
onChangeTextColor,
@@ -1666,7 +1669,9 @@ const { cursor: annotationCursor } = useAnnotationCursor({
16661669
isDrawing,
16671670
isTyping,
16681671
isShapeMode,
1669-
isLaserModeOn
1672+
isLaserModeOn,
1673+
isEraserModeOn,
1674+
pencilWidth
16701675
})
16711676
16721677
// DOM utility helpers (inlined from domMixin)
@@ -2915,8 +2920,14 @@ const extractPicturePreviewSnapshots = async ({ withLabel = false } = {}) => {
29152920
for (const { preview, index } of picturePreviews) {
29162921
if (currentPreviewIndex.value !== index) {
29172922
currentPreviewIndex.value = index
2918-
await new Promise(resolve => setTimeout(resolve, 500))
29192923
}
2924+
// Always wait for the live canvas to settle before compositing.
2925+
// Iteration #1 often skips the index switch (the user is already
2926+
// on the main preview), but the live canvas can still be mid-load
2927+
// when the snapshot is triggered right after opening the task —
2928+
// composite would then capture an empty canvas and the resulting
2929+
// PNG would come out without any annotation.
2930+
await new Promise(resolve => setTimeout(resolve, 500))
29202931
const canvas = document.getElementById('annotation-snapshot')
29212932
extractPicture(canvas)
29222933
await compositeLiveAnnotationsOntoCanvas(canvas)
@@ -3621,6 +3632,7 @@ const onAnnotateClicked = () => {
36213632
} else {
36223633
isShapeMode.value = false
36233634
isTyping.value = false
3635+
isEraserModeOn.value = false
36243636
if (fabricCanvas.value) {
36253637
fabricCanvas.value.isDrawingMode = true
36263638
const brush = new PSBrush(fabricCanvas.value)
@@ -3633,6 +3645,19 @@ const onAnnotateClicked = () => {
36333645
}
36343646
}
36353647
3648+
// Eraser is a fourth mutually-exclusive tool. The wrapper clears the
3649+
// player-owned mode refs on entry, then delegates the brush swap and the
3650+
// isEraserModeOn toggle to the composable.
3651+
const onEraseClicked = () => {
3652+
showCanvas()
3653+
if (!isEraserModeOn.value) {
3654+
isDrawing.value = false
3655+
isShapeMode.value = false
3656+
isTyping.value = false
3657+
}
3658+
annotation.onEraseClicked()
3659+
}
3660+
36363661
const onTypeClicked = () => {
36373662
const clickarea =
36383663
canvasWrapper.value?.getElementsByClassName('upper-canvas')[0]
@@ -3644,6 +3669,7 @@ const onTypeClicked = () => {
36443669
if (fabricCanvas.value) fabricCanvas.value.isDrawingMode = false
36453670
isShapeMode.value = false
36463671
isDrawing.value = false
3672+
isEraserModeOn.value = false
36473673
isTyping.value = true
36483674
clickarea?.addEventListener('dblclick', addText)
36493675
}
@@ -3654,6 +3680,7 @@ const onShapeModeClicked = () => {
36543680
if (isShapeMode.value) {
36553681
isDrawing.value = false
36563682
isTyping.value = false
3683+
isEraserModeOn.value = false
36573684
if (!isAnnotationsDisplayed.value) isAnnotationsDisplayed.value = true
36583685
}
36593686
}
@@ -3818,6 +3845,7 @@ watch(isAnnotationsDisplayed, () => {
38183845
if (!isAnnotationsDisplayed.value) {
38193846
if (isDrawing.value) onAnnotateClicked()
38203847
else if (isTyping.value) onTypeClicked()
3848+
else if (isEraserModeOn.value) onEraseClicked()
38213849
}
38223850
isRoomSilent = false
38233851
resetCanvasVisibility()

src/components/players/players/PreviewPlayer.vue

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
:panzoom-transform="panzoomTransform"
1616
:interactive="isOverlayInteractive"
1717
:wheel-target="mainMediaElement"
18-
v-show="isAnnotationsDisplayed"
18+
v-show="
19+
isAnnotationsDisplayed &&
20+
(isMovie || isPicture) &&
21+
mainMediaElement
22+
"
1923
@click="onCanvasClicked"
2024
@resized="onMainCanvasResized"
2125
/>
@@ -30,7 +34,9 @@
3034
isAnnotationsDisplayed &&
3135
isComparing &&
3236
previewToCompare &&
33-
!isComparisonOverlay
37+
!isComparisonOverlay &&
38+
(isMovie || isPicture) &&
39+
comparisonMediaElement
3440
"
3541
@click="onCanvasClicked"
3642
@resized="onComparisonCanvasResized"
@@ -215,6 +221,7 @@
215221
v-model:current-background="currentBackground"
216222
v-model:current-shape="currentShape"
217223
v-model:is-environment-skybox="isEnvironmentSkybox"
224+
v-model:is-eraser-mode-on="isEraserModeOn"
218225
v-model:is-shape-mode="isShapeMode"
219226
v-model:is-wireframe="isWireframe"
220227
@annotation-displayed-clicked="onAnnotationDisplayedClicked"
@@ -224,6 +231,7 @@
224231
@change-text-color="onChangeTextColor"
225232
@comment-clicked="onCommentClicked"
226233
@delete-clicked="onDeleteClicked"
234+
@erase-clicked="onEraseClicked"
227235
@object-background-selected="onObjectBackgroundSelected"
228236
@pencil-annotate-clicked="onPencilAnnotateClicked"
229237
@redo="redoLastAction"
@@ -650,6 +658,7 @@ const {
650658
currentShape,
651659
deleteSelection,
652660
isShapeMode,
661+
isEraserModeOn,
653662
isWriting,
654663
getNewAnnotations,
655664
loadSingleAnnotation,
@@ -1213,17 +1222,32 @@ const onPencilAnnotateClicked = () => {
12131222
_resetPencil()
12141223
isShapeMode.value = false
12151224
isTyping.value = false
1225+
isEraserModeOn.value = false
12161226
isDrawing.value = true
12171227
}
12181228
}
12191229
1230+
// Eraser is a fourth mutually-exclusive tool. The wrapper clears the
1231+
// player-owned mode refs on entry, then delegates the brush swap and the
1232+
// isEraserModeOn toggle to the composable.
1233+
const onEraseClicked = () => {
1234+
clearFocus()
1235+
if (!isEraserModeOn.value) {
1236+
isDrawing.value = false
1237+
isShapeMode.value = false
1238+
isTyping.value = false
1239+
}
1240+
annotation.onEraseClicked()
1241+
}
1242+
12201243
const onTypeClicked = () => {
12211244
clearFocus()
12221245
if (isTyping.value) {
12231246
isTyping.value = false
12241247
} else {
12251248
isDrawing.value = false
12261249
isShapeMode.value = false
1250+
isEraserModeOn.value = false
12271251
isTyping.value = true
12281252
}
12291253
}
@@ -1235,6 +1259,7 @@ const onShapeModeClicked = () => {
12351259
if (isShapeMode.value) {
12361260
isDrawing.value = false
12371261
isTyping.value = false
1262+
isEraserModeOn.value = false
12381263
if (!isAnnotationsDisplayed.value) isAnnotationsDisplayed.value = true
12391264
}
12401265
}
@@ -1468,12 +1493,15 @@ const extractPicturePreviewSnapshots = async ({ withLabel = false } = {}) => {
14681493
for (const { preview, index } of picturePreviews) {
14691494
if (currentIndex.value !== index) {
14701495
currentIndex.value = index
1471-
// Wait for the picture to swap and annotations to reload. The
1472-
// chain is async (image load -> resetPlayerPositions ->
1473-
// AnnotationCanvas resized -> reloadAnnotations) so a single tick
1474-
// isn't enough.
1475-
await new Promise(resolve => setTimeout(resolve, 500))
14761496
}
1497+
// Always wait for the picture to load and the live canvas to
1498+
// settle before compositing. Iteration #1 often skips the index
1499+
// switch (the user is already on the main preview), but the chain
1500+
// is async (image load -> resetPlayerPositions -> AnnotationCanvas
1501+
// resized -> reloadAnnotations) and composite would otherwise
1502+
// capture an empty live canvas, producing a PNG without any
1503+
// annotation.
1504+
await new Promise(resolve => setTimeout(resolve, 500))
14771505
const canvas = document.getElementById('annotation-snapshot')
14781506
previewViewer.value.extractPicture(canvas)
14791507
await compositeLiveAnnotationsOntoCanvas(canvas)
@@ -1536,6 +1564,10 @@ const { isAltHeld } = usePreviewShortcuts({
15361564
container.value.focus()
15371565
onPencilAnnotateClicked()
15381566
},
1567+
onErase: () => {
1568+
container.value.focus()
1569+
onEraseClicked()
1570+
},
15391571
onUndo: () => undoLastAction(),
15401572
onRedo: () => redoLastAction(),
15411573
onPrevPreview: () => onPreviousClicked(),
@@ -1549,7 +1581,9 @@ const { cursor: annotationCursor } = useAnnotationCursor({
15491581
isAltHeld,
15501582
isDrawing,
15511583
isTyping,
1552-
isShapeMode
1584+
isShapeMode,
1585+
isEraserModeOn,
1586+
pencilWidth
15531587
})
15541588
15551589
const onCommentClicked = () => {
@@ -1875,6 +1909,7 @@ watch(isTyping, () => {
18751909
watch(isAnnotationsDisplayed, () => {
18761910
if (!isAnnotationsDisplayed.value) {
18771911
isDrawing.value = false
1912+
if (isEraserModeOn.value) onEraseClicked()
18781913
}
18791914
})
18801915

src/components/widgets/ButtonSimple.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
<list-icon class="icon" v-else-if="icon === 'list'" />
4444
<triangle-icon class="icon" v-else-if="icon === 'triangle'" />
4545
<music-icon class="icon" v-else-if="icon === 'music'" />
46-
<square-icon class="icon" v-else-if="icon === 'eraser'" />
46+
<eraser-icon class="icon" v-else-if="icon === 'eraser'" />
4747
<key-icon class="icon" v-else-if="icon === 'key'" />
4848
<zoom-in-icon class="icon" v-else-if="icon === 'loupe'" />
4949
<globe-icon class="icon" v-else-if="icon === 'globe'" />
@@ -106,6 +106,7 @@ import {
106106
FileDigitIcon,
107107
EditIcon,
108108
Edit2Icon,
109+
EraserIcon,
109110
GlobeIcon,
110111
GridIcon,
111112
FileDownIcon,
@@ -129,7 +130,6 @@ import {
129130
SkipBackIcon,
130131
SkipForwardIcon,
131132
SmileIcon,
132-
SquareIcon,
133133
TriangleIcon,
134134
XIcon,
135135
ZoomInIcon

0 commit comments

Comments
 (0)