From 89f61b4262f5c722325214bfb67040804bf35c26 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 28 Nov 2024 17:42:13 +0100 Subject: [PATCH] [Editor] Improve drawing on a touch screen. - it's now possible to start a drawing with a pen and use fingers to zoom or scroll without interacting with the current drawing; - it's now possible to draw with a finger and them zoom with two fingers. --- src/display/editor/annotation_editor_layer.js | 6 +- src/display/editor/draw.js | 194 ++++++++++++++---- src/display/editor/drawers/inkdraw.js | 6 + web/annotation_editor_layer_builder.css | 1 - 4 files changed, 162 insertions(+), 45 deletions(-) diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 6ca2a79ec1d63..8a775a940f93f 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -422,9 +422,9 @@ class AnnotationEditorLayer { this.div.addEventListener("pointerdown", this.pointerdown.bind(this), { signal, }); - this.div.addEventListener("pointerup", this.pointerup.bind(this), { - signal, - }); + const pointerup = this.pointerup.bind(this); + this.div.addEventListener("pointerup", pointerup, { signal }); + this.div.addEventListener("pointercancel", pointerup, { signal }); } disableClick() { diff --git a/src/display/editor/draw.js b/src/display/editor/draw.js index d5f1c4bb02835..e2fed072a0310 100644 --- a/src/display/editor/draw.js +++ b/src/display/editor/draw.js @@ -69,11 +69,21 @@ class DrawingEditor extends AnnotationEditor { static _currentDrawId = -1; - static _currentDraw = null; + static _currentParent = null; - static _currentDrawingOptions = null; + static #currentDraw = null; - static _currentParent = null; + static #currentDrawingAC = null; + + static #currentDrawingOptions = null; + + static #currentPointerId = NaN; + + static #currentPointerType = null; + + static #currentPointerIds = null; + + static #currentMoveTimestamp = NaN; static _INNER_MARGIN = 3; @@ -168,7 +178,7 @@ class DrawingEditor extends AnnotationEditor { this._defaultDrawingOptions.updateProperty(propertyName, value); } if (this._currentParent) { - this._currentDraw.updateProperty(propertyName, value); + DrawingEditor.#currentDraw.updateProperty(propertyName, value); this._currentParent.drawLayer.updateProperties( this._currentDrawId, this._defaultDrawingOptions.toSVGProperties() @@ -639,32 +649,81 @@ class DrawingEditor extends AnnotationEditor { unreachable("Not implemented"); } - static startDrawing( - parent, - uiManager, - _isLTR, - { target, offsetX: x, offsetY: y } - ) { + static startDrawing(parent, uiManager, _isLTR, event) { + // The _currentPointerType is set when the user starts an empty drawing + // session. If, in the same drawing session, the user starts using a + // different type of pointer (e.g. a pen and then a finger), we just return. + // + // The _currentPointerId and _currentPointerIds are used to keep track of + // the pointers with a same type (e.g. two fingers). If the user starts to + // draw with a finger and then uses a second finger, we just stop the + // current drawing and let the user zoom the document. + + const { target, offsetX: x, offsetY: y, pointerId, pointerType } = event; + if ( + DrawingEditor.#currentPointerType && + DrawingEditor.#currentPointerType !== pointerType + ) { + return; + } + const { viewport: { rotation }, } = parent; const { width: parentWidth, height: parentHeight } = target.getBoundingClientRect(); - const ac = new AbortController(); + + const ac = (DrawingEditor.#currentDrawingAC = new AbortController()); const signal = parent.combinedSignal(ac); + DrawingEditor.#currentPointerId ||= pointerId; + DrawingEditor.#currentPointerType ??= pointerType; + window.addEventListener( "pointerup", e => { - ac.abort(); - parent.toggleDrawing(true); - this._endDraw(e); + if (DrawingEditor.#currentPointerId === e.pointerId) { + this._endDraw(e); + } else { + DrawingEditor.#currentPointerIds?.delete(e.pointerId); + } + }, + { signal } + ); + window.addEventListener( + "pointercancel", + e => { + if (DrawingEditor.#currentPointerId === e.pointerId) { + this._currentParent.endDrawingSession(); + } else { + DrawingEditor.#currentPointerIds?.delete(e.pointerId); + } }, { signal } ); window.addEventListener( "pointerdown", - stopEvent /* Avoid to have undesired clicks during drawing. */, + e => { + if (DrawingEditor.#currentPointerType !== e.pointerType) { + // For example, we started with a pen and the user + // is now using a finger. + return; + } + + // For example, the user is using a second finger. + (DrawingEditor.#currentPointerIds ||= new Set()).add(e.pointerId); + + // The first finger created a first point and a second finger just + // started, so we stop the drawing and remove this only point. + if (DrawingEditor.#currentDraw.isCancellable()) { + DrawingEditor.#currentDraw.removeLastElement(); + if (DrawingEditor.#currentDraw.isEmpty()) { + this._currentParent.endDrawingSession(/* isAborted = */ true); + } else { + this._endDraw(null); + } + } + }, { capture: true, passive: false, @@ -675,54 +734,114 @@ class DrawingEditor extends AnnotationEditor { target.addEventListener("pointermove", this._drawMove.bind(this), { signal, }); + target.addEventListener( + "touchmove", + e => { + if (e.timeStamp === DrawingEditor.#currentMoveTimestamp) { + // This move event is used to draw so we don't want to scroll. + stopEvent(e); + } + }, + { signal } + ); parent.toggleDrawing(); uiManager._editorUndoBar?.hide(); - if (this._currentDraw) { + if (DrawingEditor.#currentDraw) { parent.drawLayer.updateProperties( this._currentDrawId, - this._currentDraw.startNew(x, y, parentWidth, parentHeight, rotation) + DrawingEditor.#currentDraw.startNew( + x, + y, + parentWidth, + parentHeight, + rotation + ) ); return; } uiManager.updateUIForDefaultProperties(this); - this._currentDraw = this.createDrawerInstance( + DrawingEditor.#currentDraw = this.createDrawerInstance( x, y, parentWidth, parentHeight, rotation ); - this._currentDrawingOptions = this.getDefaultDrawingOptions(); + DrawingEditor.#currentDrawingOptions = this.getDefaultDrawingOptions(); this._currentParent = parent; ({ id: this._currentDrawId } = parent.drawLayer.draw( this._mergeSVGProperties( - this._currentDrawingOptions.toSVGProperties(), - this._currentDraw.defaultSVGProperties + DrawingEditor.#currentDrawingOptions.toSVGProperties(), + DrawingEditor.#currentDraw.defaultSVGProperties ), /* isPathUpdatable = */ true, /* hasClip = */ false )); } - static _drawMove({ offsetX, offsetY }) { + static _drawMove(event) { + DrawingEditor.#currentMoveTimestamp = -1; + if (!DrawingEditor.#currentDraw) { + return; + } + const { offsetX, offsetY, pointerId } = event; + + if (DrawingEditor.#currentPointerId !== pointerId) { + return; + } + if (DrawingEditor.#currentPointerIds?.size >= 1) { + // The user is using multiple fingers and the first one is moving. + this._endDraw(event); + return; + } this._currentParent.drawLayer.updateProperties( this._currentDrawId, - this._currentDraw.add(offsetX, offsetY) + DrawingEditor.#currentDraw.add(offsetX, offsetY) ); + // We track the timestamp to know if the touchmove event is used to draw. + DrawingEditor.#currentMoveTimestamp = event.timeStamp; + stopEvent(event); + } + + static _cleanup(all) { + if (all) { + this._currentDrawId = -1; + this._currentParent = null; + DrawingEditor.#currentDraw = null; + DrawingEditor.#currentDrawingOptions = null; + DrawingEditor.#currentPointerType = null; + DrawingEditor.#currentMoveTimestamp = NaN; + } + + if (DrawingEditor.#currentDrawingAC) { + DrawingEditor.#currentDrawingAC.abort(); + DrawingEditor.#currentDrawingAC = null; + DrawingEditor.#currentPointerId = NaN; + DrawingEditor.#currentPointerIds = null; + } } - static _endDraw({ offsetX, offsetY }) { + static _endDraw(event) { const parent = this._currentParent; - parent.drawLayer.updateProperties( - this._currentDrawId, - this._currentDraw.end(offsetX, offsetY) - ); + if (!parent) { + return; + } + + parent.toggleDrawing(true); + this._cleanup(false); + + if (event) { + parent.drawLayer.updateProperties( + this._currentDrawId, + DrawingEditor.#currentDraw.end(event.offsetX, event.offsetY) + ); + } if (this.supportMultipleDrawings) { - const draw = this._currentDraw; + const draw = DrawingEditor.#currentDraw; const drawId = this._currentDrawId; const lastElement = draw.getLastElement(); parent.addCommands({ @@ -753,7 +872,7 @@ class DrawingEditor extends AnnotationEditor { parent.toggleDrawing(true); parent.cleanUndoStack(AnnotationEditorParamsType.DRAW_STEP); - if (!this._currentDraw.isEmpty()) { + if (!DrawingEditor.#currentDraw.isEmpty()) { const { pageDimensions: [pageWidth, pageHeight], scale, @@ -764,32 +883,25 @@ class DrawingEditor extends AnnotationEditor { false, { drawId: this._currentDrawId, - drawOutlines: this._currentDraw.getOutlines( + drawOutlines: DrawingEditor.#currentDraw.getOutlines( pageWidth * scale, pageHeight * scale, scale, this._INNER_MARGIN ), - drawingOptions: this._currentDrawingOptions, + drawingOptions: DrawingEditor.#currentDrawingOptions, mustBeCommitted: !isAborted, } ); - this._cleanup(); + this._cleanup(true); return editor; } parent.drawLayer.remove(this._currentDrawId); - this._cleanup(); + this._cleanup(true); return null; } - static _cleanup() { - this._currentDrawId = -1; - this._currentDraw = null; - this._currentDrawingOptions = null; - this._currentParent = null; - } - /** * Create the drawing options. * @param {Object} _data diff --git a/src/display/editor/drawers/inkdraw.js b/src/display/editor/drawers/inkdraw.js index 8e0314c7576c7..b746f1a536cc4 100644 --- a/src/display/editor/drawers/inkdraw.js +++ b/src/display/editor/drawers/inkdraw.js @@ -74,6 +74,12 @@ class InkDrawOutliner { return !this.#lines || this.#lines.length === 0; } + isCancellable() { + // The user a second finger after drawing 5 points: it's small enough + // to not be a real drawing. + return this.#points.length <= 10; + } + add(x, y) { // The point is in canvas coordinates which means that there is no rotation. // It's the same as parent coordinates. diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index 5edf1639b43ad..d778c7e83d2d8 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -156,7 +156,6 @@ .annotationEditorLayer.inkEditing { cursor: var(--editorInk-editing-cursor); - touch-action: none; } .annotationEditorLayer .draw {