From bf7c434d400b3cccd6282a0161413a0de37a262b Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Mon, 22 Oct 2018 14:45:39 -0500 Subject: [PATCH 01/22] marquee: round pixels for move target xy --- src/js/window/storyboarder-sketch-pane/marquee-strategy.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index 753e9bd6dd..a628abc27a 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -660,8 +660,8 @@ class OperationStrategy { if (this.state.down) { this.state.position = point - this.state.target.x = this.state.spriteOrigin.x + (this.state.position.x - this.state.origin.x) - this.state.target.y = this.state.spriteOrigin.y + (this.state.position.y - this.state.origin.y) + this.state.target.x = Math.round(this.state.spriteOrigin.x + (this.state.position.x - this.state.origin.x)) + this.state.target.y = Math.round(this.state.spriteOrigin.y + (this.state.position.y - this.state.origin.y)) this.state.moved = ( this.state.target.x != this.state.marqueePath.bounds.x || From 62c1f996e236aee3b01b7090f99d971619bb4187 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Mon, 22 Oct 2018 14:48:33 -0500 Subject: [PATCH 02/22] round each added point to exact pixel --- src/js/window/storyboarder-sketch-pane/marquee-strategy.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index a628abc27a..4e2653d7a1 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -2,6 +2,8 @@ const paper = require('paper') const SketchPaneUtil = require('alchemancy').util const constrainPoint = (point, rectangle) => { + // if paper.Point (and not paper.Segment) + if (point.round) point = point.round() point = paper.Point.max(point, rectangle.topLeft) point = paper.Point.min(point, rectangle.bottomRight) return point From a125e82918c7ad9249de6396e91037bceb68933f Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Mon, 22 Oct 2018 14:52:43 -0500 Subject: [PATCH 03/22] can copy marquee path and image data to clipboard --- .../marquee-strategy.js | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index 4e2653d7a1..704595149a 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -1,5 +1,6 @@ const paper = require('paper') const SketchPaneUtil = require('alchemancy').util +const { clipboard } = require('electron') const constrainPoint = (point, rectangle) => { // if paper.Point (and not paper.Segment) @@ -54,6 +55,44 @@ class MarqueeStrategy { findLayerByName (name) { return this.context.sketchPane.layers.findByName(name) } + + copyToClipboard (marqueePath, spritesByLayerId) { + // flattened image of marquee art + let image + + // marquee data, including path and bounds + let marquee = { + path: JSON.parse(marqueePath.exportJSON()), + } + + // data for each marquee layer + let width = marqueePath.bounds.width + let height = marqueePath.bounds.height + + let imageDataByLayerId = spritesByLayerId.reduce( + (coll, sprite, index) => { + let pixels = this.context.sketchPane.app.renderer.plugins.extract.pixels(sprite.texture) + SketchPaneUtil.arrayPostDivide(pixels) + let canvas = SketchPaneUtil.pixelsToCanvas(pixels, width, height) + coll[this.context.sketchPane.layers[index].name] = canvas.toDataURL() + return coll + }, + {} + ) + + clipboard.clear() + clipboard.write({ + image, + text: JSON.stringify( + { + marquee, + imageDataByLayerId + }, + null, + 2 + ) + }) + } } class SelectionStrategy { From d2490b6f7e0085e3c24d2a100c00bf91fbe0c621 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Mon, 22 Oct 2018 14:54:57 -0500 Subject: [PATCH 04/22] key trigger to copy marquee path and art set to 'c' for now --- .../storyboarder-sketch-pane/marquee-strategy.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index 704595149a..392c86a3b6 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -433,6 +433,18 @@ class SelectionStrategy { this.deselect() } } + + // if (this.context.isCommandPressed('drawing:marquee:copy')) { + if (event.key === 'c') { + if (this.parent.marqueePath) { + this.context.sketchPane.selectedArea.set(this.parent.marqueePath) + this.parent.copyToClipboard( + this.parent.marqueePath, + this.context.sketchPane.selectedArea.copy(this.context.visibleLayersIndices) + ) + } + return + } } _onKeyUp (event) { From 5783a95db3e542606853b2a8e80bffb50362c935 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Mon, 22 Oct 2018 14:57:35 -0500 Subject: [PATCH 05/22] pasteFromClipboard --- src/js/window/main-window.js | 23 +++++++++++++++++-- src/js/window/storyboarder-sketch-pane.js | 6 +++++ .../marquee-strategy.js | 4 ++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/js/window/main-window.js b/src/js/window/main-window.js index 5c90d19d12..d4b5198f63 100644 --- a/src/js/window/main-window.js +++ b/src/js/window/main-window.js @@ -5510,8 +5510,12 @@ let pasteBoards = async () => { if (text !== "") { try { pasted = JSON.parse(clipboard.readText()) - if (!pasted.boards.length || pasted.boards.length < 1) throw new Error('no boards') - if (!pasted.layerDataByBoardIndex.length || pasted.layerDataByBoardIndex.length < 1) throw new Error('no layer data') + if (pasted.marquee) { + // it's a marquee paste + } else { + if (!pasted.boards.length || pasted.boards.length < 1) throw new Error('no boards') + if (!pasted.layerDataByBoardIndex.length || pasted.layerDataByBoardIndex.length < 1) throw new Error('no layer data') + } } catch (err) { console.log('could not parse clipboard as text') console.log(err) @@ -5623,6 +5627,21 @@ let pasteBoards = async () => { notifications.notify({ message: `Whoops. Could not paste boards. ${err.message}`, timing: 8 }) throw err } + } else if (pasted.marquee) { + if (store.getState().toolbar.mode !== 'marquee') { + store.dispatch({ + type: 'TOOLBAR_MODE_SET', + payload: 'marquee', + meta: { scope: 'local' } + }) + + if (store.getState().toolbar.mode === 'marquee') { + sfx.playEffect('metal') + + storyboarderSketchPane.pasteFromClipboard(pasted) + } + } + } else { notifications.notify({ message: "There's nothing in the clipboard that I can paste. Are you sure you copied it right?", timing: 8 }) sfx.error() diff --git a/src/js/window/storyboarder-sketch-pane.js b/src/js/window/storyboarder-sketch-pane.js index e19a679e29..09a1f2ddba 100644 --- a/src/js/window/storyboarder-sketch-pane.js +++ b/src/js/window/storyboarder-sketch-pane.js @@ -296,6 +296,12 @@ class StoryboarderSketchPane extends EventEmitter { ) } + pasteFromClipboard (clipboardContents) { + if (this.strategy.pasteFromClipboard) { + this.strategy.pasteFromClipboard(clipboardContents) + } + } + onKeyDown (e) { if (this.isCommandPressed('drawing:scale-mode')) { // switch to scale strategy diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index 392c86a3b6..7d0ebc1863 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -93,6 +93,10 @@ class MarqueeStrategy { ) }) } + + pasteFromClipboard (contents) { + } + } class SelectionStrategy { From 9960515128175351efd51e9c1e3a50989035f1f4 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Mon, 22 Oct 2018 15:01:11 -0500 Subject: [PATCH 06/22] paste preview and paste commit split into fromSelection/fromClipboard --- .../marquee-strategy.js | 116 +++++++++++++++--- 1 file changed, 102 insertions(+), 14 deletions(-) diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index 7d0ebc1863..bc5f575611 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -95,6 +95,8 @@ class MarqueeStrategy { } pasteFromClipboard (contents) { + this.setStrategy('operation') + this.strategy.fromClipboard(contents) } } @@ -356,6 +358,7 @@ class SelectionStrategy { }) this.parent.setStrategy('operation') + this.parent.strategy.fromSelection() } _addPointFromEvent (event) { @@ -583,16 +586,76 @@ class OperationStrategy { startup () { console.log('OperationStrategy#startup') + document.addEventListener('pointerdown', this._onPointerDown) + document.addEventListener('pointermove', this._onPointerMove) + document.addEventListener('pointerup', this._onPointerUp) + window.addEventListener('keydown', this._onKeyDown) + window.addEventListener('keyup', this._onKeyUp) + this.layer = this.context.sketchPane.layers.findByName('composite') this.backgroundMatte = null this.context.store.dispatch({ type: 'TOOLBAR_MODE_STATUS_SET', payload: 'busy', meta: { scope: 'local' } }) + } + + fromClipboard (contents) { + try { + this.parent.marqueePath = new paper.Path().importJSON(contents.marquee.path) + + this.state = { + marqueePath: this.parent.marqueePath.clone(), + moved: false, + done: false, + commitOperation: 'paste' + } + this.state.target = { + x: this.state.marqueePath.bounds.x, + y: this.state.marqueePath.bounds.y + } + this.context.sketchPane.selectedArea.set(this.state.marqueePath) + // delete ALL cached canvas textures to ensure canvas is re-rendered + PIXI.utils.clearTextureCache() + + this.cutSprite = new PIXI.Sprite(PIXI.Texture.EMPTY) + + Promise.all( + Object.entries(contents.imageDataByLayerId).map(([name, data]) => + new Promise((resolve, reject) => { + let texture = PIXI.Texture.fromImage(data) + texture.baseTexture.once('loaded', () => { + resolve({ name, texture }) + }) + // TODO does this need to be cleaned up? + texture.baseTexture.once('error', () => { + reject(err) + }) + }) + ) + ).then(pairs => { + this.cutSprite = new PIXI.Sprite(PIXI.Texture.EMPTY) + for (let { name, texture } of pairs) { + let sprite = new PIXI.Sprite(texture) + sprite.name = name + this.cutSprite.addChild(sprite) + } + + this.setupOperation() + }).catch(err => { + console.error(err) + throw err + }) + } catch (err) { + console.error(err) + alert('Whoops! Couldn’t paste.') + } + } + fromSelection () { this.state = { marqueePath: this.parent.marqueePath.clone(), moved: false, done: false, - commitOperation: 'move' // move, fill + commitOperation: 'move' // move, fill, paste } this.state.target = { x: this.state.marqueePath.bounds.x, @@ -604,8 +667,20 @@ class OperationStrategy { // delete ALL cached canvas textures to ensure canvas is re-rendered PIXI.utils.clearTextureCache() - this.outlineSprite = this.context.sketchPane.selectedArea.asOutlineSprite() this.cutSprite = this.context.sketchPane.selectedArea.asSprite(this.context.visibleLayersIndices) + + this.setupOperation() + + let maskSprite = this.context.sketchPane.selectedArea.asMaskSprite(true) + this.flattenedLayerSprite.addChild(maskSprite) + this.flattenedLayerSprite.mask = maskSprite + + // HACK force the first pointer down + this._onPointerDown(this.parent.marqueeTransitionEvent) + } + + setupOperation () { + this.outlineSprite = this.context.sketchPane.selectedArea.asOutlineSprite() this.areaPolygons = this.context.sketchPane.selectedArea.asPolygons(false) // TODO should this move to a SelectedArea setup/prepare method? @@ -617,7 +692,6 @@ class OperationStrategy { this.backgroundMatte.drawRect(0, 0, this.context.sketchPane.width, this.context.sketchPane.height) this.layer.sprite.addChild(this.backgroundMatte) - let maskSprite = this.context.sketchPane.selectedArea.asMaskSprite(true) this.flattenedLayerSprite = new PIXI.Sprite( PIXI.Texture.fromCanvas( this.context.sketchPane.layers.asFlattenedCanvas( @@ -627,8 +701,6 @@ class OperationStrategy { ) ) ) - this.flattenedLayerSprite.addChild(maskSprite) - this.flattenedLayerSprite.mask = maskSprite this.layer.sprite.addChild(this.flattenedLayerSprite) @@ -640,15 +712,6 @@ class OperationStrategy { // positioning this.draw() - - document.addEventListener('pointerdown', this._onPointerDown) - document.addEventListener('pointermove', this._onPointerMove) - document.addEventListener('pointerup', this._onPointerUp) - window.addEventListener('keydown', this._onKeyDown) - window.addEventListener('keyup', this._onKeyUp) - - // HACK force the first pointer down - this._onPointerDown(this.parent.marqueeTransitionEvent) this.context.sketchPane.cursor.setEnabled(false) this.context.sketchPane.app.view.style.cursor = 'auto' @@ -843,6 +906,31 @@ class OperationStrategy { this.context.sketchPane.selectedArea.paste(indices, sprites) this.context.emit('markDirty', indices) + } else if (this.state.commitOperation === 'paste') { + this.context.emit('addToUndoStack', indices) + + // look for layers in the clipboard that match layers we have by id + // add any matches to the paste-able sprites list + let indexes = [] + let sprites = [] + for (let sprite of this.cutSprite.children) { + let layer = this.context.sketchPane.layers.findByName(sprite.name) + if (layer) { + indexes.push(layer.index) + sprites[layer.index] = sprite + } else { + console.log('ignoring layer with name', sprite.name) + } + } + // move the sprites + sprites.forEach(sprite => { + sprite.x = this.state.target.x + sprite.y = this.state.target.y + }) + + this.context.sketchPane.selectedArea.paste(indices, sprites) + this.context.emit('markDirty', indices) + } else if (this.state.commitOperation === 'erase') { this.context.emit('addToUndoStack', indices) this.context.sketchPane.selectedArea.erase(indices) From b0262c7b66d27fdde002f921db49638edaeef2f2 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Mon, 22 Oct 2018 16:33:23 -0500 Subject: [PATCH 07/22] notify on selection copy/paste --- src/js/window/storyboarder-sketch-pane/marquee-strategy.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index bc5f575611..d272615440 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -1,6 +1,7 @@ const paper = require('paper') const SketchPaneUtil = require('alchemancy').util const { clipboard } = require('electron') +const notifications = require('../notifications') const constrainPoint = (point, rectangle) => { // if paper.Point (and not paper.Segment) @@ -92,11 +93,14 @@ class MarqueeStrategy { 2 ) }) + + notifications.notify({ message: 'Copied selection' }) } pasteFromClipboard (contents) { this.setStrategy('operation') this.strategy.fromClipboard(contents) + notifications.notify({ message: 'Pasted selection' }) } } From 512301a12f6e86089aab6f266227a49de109f2a4 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Mon, 22 Oct 2018 16:33:36 -0500 Subject: [PATCH 08/22] cleanup console logs --- src/js/window/storyboarder-sketch-pane/marquee-strategy.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index d272615440..bbb9adfa28 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -29,12 +29,10 @@ class MarqueeStrategy { } startup () { - console.log('MarqueeStrategy#startup') this.setStrategy('selection') } shutdown () { - console.log('MarqueeStrategy#shutdown') if (this.strategy) { this.strategy.shutdown() this.strategy = null @@ -102,7 +100,6 @@ class MarqueeStrategy { this.strategy.fromClipboard(contents) notifications.notify({ message: 'Pasted selection' }) } - } class SelectionStrategy { @@ -588,8 +585,6 @@ class OperationStrategy { } startup () { - console.log('OperationStrategy#startup') - document.addEventListener('pointerdown', this._onPointerDown) document.addEventListener('pointermove', this._onPointerMove) document.addEventListener('pointerup', this._onPointerUp) From df05721dc7cc2098fcdfb040ab347718aaefab32 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Tue, 23 Oct 2018 11:48:39 -0500 Subject: [PATCH 09/22] notification timing --- src/js/window/storyboarder-sketch-pane/marquee-strategy.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index bbb9adfa28..92bc697f2d 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -92,13 +92,13 @@ class MarqueeStrategy { ) }) - notifications.notify({ message: 'Copied selection' }) + notifications.notify({ message: 'Copied selection', timing: 5 }) } pasteFromClipboard (contents) { this.setStrategy('operation') this.strategy.fromClipboard(contents) - notifications.notify({ message: 'Pasted selection' }) + notifications.notify({ message: 'Pasted selection', timing: 5 }) } } From 01d1b2f6164a6265b2d06841ffc58a1948cd4cb5 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Tue, 23 Oct 2018 11:48:59 -0500 Subject: [PATCH 10/22] hardcode Meta+c and Ctrl+c for now --- .../window/storyboarder-sketch-pane/marquee-strategy.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index 92bc697f2d..0c2a258b4c 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -442,8 +442,14 @@ class SelectionStrategy { } } + // TODO make this assignable to 'drawing:marquee:copy' + // will require re-working the keyboard command interpreter for macOS + // due to the cmd key bug // if (this.context.isCommandPressed('drawing:marquee:copy')) { - if (event.key === 'c') { + // + // HACK hardcodes key handler to accept either Cmd+C or Ctrl+C + // + if (event.key === 'c' && (event.metaKey || event.ctrlKey)) { if (this.parent.marqueePath) { this.context.sketchPane.selectedArea.set(this.parent.marqueePath) this.parent.copyToClipboard( From e0d25ca7fe40e4f2cba185742a9ddac7dc4baded Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Tue, 23 Oct 2018 12:13:01 -0500 Subject: [PATCH 11/22] always attempt paste play sound only if mode changed --- src/js/window/main-window.js | 4 +--- src/js/window/storyboarder-sketch-pane.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/js/window/main-window.js b/src/js/window/main-window.js index d4b5198f63..2bf2bfe88e 100644 --- a/src/js/window/main-window.js +++ b/src/js/window/main-window.js @@ -5634,13 +5634,11 @@ let pasteBoards = async () => { payload: 'marquee', meta: { scope: 'local' } }) - if (store.getState().toolbar.mode === 'marquee') { sfx.playEffect('metal') - - storyboarderSketchPane.pasteFromClipboard(pasted) } } + storyboarderSketchPane.pasteFromClipboard(pasted) } else { notifications.notify({ message: "There's nothing in the clipboard that I can paste. Are you sure you copied it right?", timing: 8 }) diff --git a/src/js/window/storyboarder-sketch-pane.js b/src/js/window/storyboarder-sketch-pane.js index 09a1f2ddba..3296230b2c 100644 --- a/src/js/window/storyboarder-sketch-pane.js +++ b/src/js/window/storyboarder-sketch-pane.js @@ -297,7 +297,7 @@ class StoryboarderSketchPane extends EventEmitter { } pasteFromClipboard (clipboardContents) { - if (this.strategy.pasteFromClipboard) { + if (this.strategy instanceof MarqueeStrategy && this.strategy.pasteFromClipboard) { this.strategy.pasteFromClipboard(clipboardContents) } } From 228b97f1d9eca415daa86f7618c21e0a7f7bc722 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Tue, 23 Oct 2018 12:14:25 -0500 Subject: [PATCH 12/22] allow paste key command from both marquee modes --- .../marquee-strategy.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index 0c2a258b4c..56b044312a 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -397,7 +397,13 @@ class SelectionStrategy { } _onKeyDown (event) { - event.preventDefault() + // TODO key bindings + if (event.key === 'v' && (event.metaKey || event.ctrlKey)) { + // allow paste command through + return + } else { + event.preventDefault() + } if (this.state.complete) { if (this.context.isCommandPressed('drawing:marquee:add')) { @@ -442,6 +448,7 @@ class SelectionStrategy { } } + // TODO key bindings // TODO make this assignable to 'drawing:marquee:copy' // will require re-working the keyboard command interpreter for macOS // due to the cmd key bug @@ -807,7 +814,15 @@ class OperationStrategy { } _onKeyDown (event) { - event.preventDefault() + // TODO key bindings + if (event.key === 'v' && (event.metaKey || event.ctrlKey)) { + // commit first + this.commit() + // allow paste command through + return + } else { + event.preventDefault() + } if (this.context.isCommandPressed('drawing:marquee:cancel')) { this.cancel() From aeb03c28c00b34a62e608a5670af429202b8aadc Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Tue, 23 Oct 2018 12:33:13 -0500 Subject: [PATCH 13/22] cut --- .../storyboarder-sketch-pane/marquee-strategy.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index 56b044312a..a28232e27e 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -91,8 +91,6 @@ class MarqueeStrategy { 2 ) }) - - notifications.notify({ message: 'Copied selection', timing: 5 }) } pasteFromClipboard (contents) { @@ -463,6 +461,19 @@ class SelectionStrategy { this.parent.marqueePath, this.context.sketchPane.selectedArea.copy(this.context.visibleLayersIndices) ) + notifications.notify({ message: 'Copied selection', timing: 5 }) + } + return + } + if (event.key === 'x' && (event.metaKey || event.ctrlKey)) { + if (this.parent.marqueePath) { + this.context.sketchPane.selectedArea.set(this.parent.marqueePath) + this.parent.copyToClipboard( + this.parent.marqueePath, + this.context.sketchPane.selectedArea.copy(this.context.visibleLayersIndices) + ) + this.context.sketchPane.selectedArea.erase(this.context.visibleLayersIndices) + notifications.notify({ message: 'Cut selection', timing: 5 }) } return } From a79f0dc9449bb35bf10a1095632dea374dc1b51b Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Tue, 23 Oct 2018 15:32:48 -0500 Subject: [PATCH 14/22] copy via menu --- src/js/window/main-window.js | 17 +++++++++++++--- src/js/window/storyboarder-sketch-pane.js | 9 +++++++++ .../marquee-strategy.js | 20 +++++++------------ 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/js/window/main-window.js b/src/js/window/main-window.js index 2bf2bfe88e..ac6ef0feb0 100644 --- a/src/js/window/main-window.js +++ b/src/js/window/main-window.js @@ -4025,7 +4025,9 @@ let renderThumbnailDrawer = () => { contextMenu.on('copy', () => { copyBoards() .then(() => notifications.notify({ - message: 'Copied board(s) to clipboard.', timing: 5 + message: `Copied ${ + store.getState().toolbar.mode === 'marquee' ? 'selection' : 'boards(s)' + } to clipboard.`, timing: 5 })) .catch(err => {}) }) @@ -4639,7 +4641,9 @@ window.onkeydown = (e) => { e.preventDefault() copyBoards() .then(() => notifications.notify({ - message: 'Copied board(s) to clipboard.', timing: 5 + message: `Copied ${ + store.getState().toolbar.mode === 'marquee' ? 'selection' : 'boards(s)' + } to clipboard.`, timing: 5 })) .catch(err => {}) @@ -5144,7 +5148,9 @@ ipcRenderer.on('copy', event => { // console.log('copy boards') copyBoards() .then(() => notifications.notify({ - message: 'Copied board(s) to clipboard.', timing: 5 + message: `Copied ${ + store.getState().toolbar.mode === 'marquee' ? 'selection' : 'boards(s)' + } to clipboard.`, timing: 5 })) .catch(err => { console.error(err) @@ -5280,6 +5286,11 @@ const importImage = async imageDataURL => { let copyBoards = async () => { if (textInputMode) return // ignore copy command in text input mode + if (store.getState().toolbar.mode === 'marquee') { + storyboarderSketchPane.copyToClipboard() + return + } + try { // list the boards, using a copy of the selection indices set to determine order let boards = [...selections].sort(util.compareNumbers).map(n => boardData.boards[n]) diff --git a/src/js/window/storyboarder-sketch-pane.js b/src/js/window/storyboarder-sketch-pane.js index 3296230b2c..c2d26eab22 100644 --- a/src/js/window/storyboarder-sketch-pane.js +++ b/src/js/window/storyboarder-sketch-pane.js @@ -296,6 +296,15 @@ class StoryboarderSketchPane extends EventEmitter { ) } + copyToClipboard () { + if (this.strategy instanceof MarqueeStrategy && this.strategy.marqueePath) { + this.sketchPane.selectedArea.set(this.strategy.marqueePath) + this.strategy.copyToClipboard( + this.strategy.marqueePath, + this.sketchPane.selectedArea.copy(this.visibleLayersIndices) + ) + } + } pasteFromClipboard (clipboardContents) { if (this.strategy instanceof MarqueeStrategy && this.strategy.pasteFromClipboard) { this.strategy.pasteFromClipboard(clipboardContents) diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index a28232e27e..1f8b1a9940 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -396,8 +396,13 @@ class SelectionStrategy { _onKeyDown (event) { // TODO key bindings - if (event.key === 'v' && (event.metaKey || event.ctrlKey)) { - // allow paste command through + if ( + // copy + (event.key === 'c' && (event.metaKey || event.ctrlKey)) || + // paste + (event.key === 'v' && (event.metaKey || event.ctrlKey)) + ) { + // allow command through return } else { event.preventDefault() @@ -454,17 +459,6 @@ class SelectionStrategy { // // HACK hardcodes key handler to accept either Cmd+C or Ctrl+C // - if (event.key === 'c' && (event.metaKey || event.ctrlKey)) { - if (this.parent.marqueePath) { - this.context.sketchPane.selectedArea.set(this.parent.marqueePath) - this.parent.copyToClipboard( - this.parent.marqueePath, - this.context.sketchPane.selectedArea.copy(this.context.visibleLayersIndices) - ) - notifications.notify({ message: 'Copied selection', timing: 5 }) - } - return - } if (event.key === 'x' && (event.metaKey || event.ctrlKey)) { if (this.parent.marqueePath) { this.context.sketchPane.selectedArea.set(this.parent.marqueePath) From 85d935bfd5b92538d6b643ab261fc768efa432be Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Tue, 23 Oct 2018 17:19:33 -0500 Subject: [PATCH 15/22] cut selection via menu --- src/js/main.js | 4 ++ src/js/menu.js | 4 +- src/js/window/main-window.js | 64 ++++++++++++++++++++--- src/js/window/storyboarder-sketch-pane.js | 6 +++ 4 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/js/main.js b/src/js/main.js index 9ee1036f86..30f004f3d5 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -1132,6 +1132,10 @@ ipcMain.on('nextScene', (e, arg)=> { mainWindow.webContents.send('nextScene') }) +ipcMain.on('cut', () => + mainWindow.webContents.send('cut') +) + ipcMain.on('copy', (e, arg)=> { mainWindow.webContents.send('copy') }) diff --git a/src/js/menu.js b/src/js/menu.js index b12c5a4623..95abeb3869 100644 --- a/src/js/menu.js +++ b/src/js/menu.js @@ -210,7 +210,9 @@ AppMenu.Edit = () => ({ { label: 'Cut', accelerator: keystrokeFor('menu:edit:cut'), - role: 'cut' + click (item, focusedWindow, event) { + ipcRenderer.send('cut') + } }, { label: 'Copy', diff --git a/src/js/window/main-window.js b/src/js/window/main-window.js index ac6ef0feb0..0baa8fec29 100644 --- a/src/js/window/main-window.js +++ b/src/js/window/main-window.js @@ -4647,13 +4647,21 @@ window.onkeydown = (e) => { })) .catch(err => {}) - } else if (isCommandPressed('menu:edit:cut')) { - e.preventDefault() - copyBoards() - .then(() => { - deleteBoards() - notifications.notify({ message: 'Cut board(s) to clipboard.', timing: 5 }) - }).catch(err => {}) + // } else if (isCommandPressed('menu:edit:cut')) { + // e.preventDefault() + // + // cutBoards() + // .then(() => { + // notifications.notify({ + // message: `Cut ${ + // store.getState().toolbar.mode === 'marquee' ? 'selection' : 'boards(s)' + // } to clipboard.`, + // timing: 5 + // }) + // }) + // .catch(err => { + // console.warn(err) + // }) } else if (isCommandPressed('menu:edit:paste')) { e.preventDefault() @@ -5136,6 +5144,39 @@ ipcRenderer.on('redo', (e, arg) => { } }) +ipcRenderer.on('cut', event => { + if (remote.getCurrentWindow().webContents.isDevToolsFocused()) { + remote.getCurrentWindow().webContents.devToolsWebContents.executeJavaScript( + `document.execCommand('cut')` + ) + return + } + + if (!textInputMode && remote.getCurrentWindow().isFocused()) { + cutBoards() + .then(() => { + notifications.notify({ + message: `Cut ${ + store.getState().toolbar.mode === 'marquee' ? 'selection' : 'boards(s)' + } to clipboard.`, + timing: 5 + }) + }) + .catch(err => { + console.warn(err) + }) + } else { + // find the focused window (which may be main-window) + for (let w of remote.BrowserWindow.getAllWindows()) { + if (w.isFocused()) { + // console.log('copy to clipboard from window', w.id) + w.webContents.cut() + return + } + } + } +}) + ipcRenderer.on('copy', event => { if (remote.getCurrentWindow().webContents.isDevToolsFocused()) { remote.getCurrentWindow().webContents.devToolsWebContents.executeJavaScript( @@ -5281,6 +5322,15 @@ const importImage = async imageDataURL => { * of all visible layers as an 'image' to the clipboard. * */ + +const cutBoards = async () => { + if (store.getState().toolbar.mode === 'marquee') { + storyboarderSketchPane.cutToClipboard() + } else { + await copyBoards() + await deleteBoards() + } +} // TODO cancel token let copyBoards = async () => { diff --git a/src/js/window/storyboarder-sketch-pane.js b/src/js/window/storyboarder-sketch-pane.js index c2d26eab22..88b4c3aab9 100644 --- a/src/js/window/storyboarder-sketch-pane.js +++ b/src/js/window/storyboarder-sketch-pane.js @@ -296,6 +296,12 @@ class StoryboarderSketchPane extends EventEmitter { ) } + cutToClipboard () { + if (this.strategy.marqueePath) { + this.copyToClipboard() + this.sketchPane.selectedArea.erase(this.visibleLayersIndices) + } + } copyToClipboard () { if (this.strategy instanceof MarqueeStrategy && this.strategy.marqueePath) { this.sketchPane.selectedArea.set(this.strategy.marqueePath) From 04d5870bac835d5366c8e10ff574092299196bb4 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Tue, 23 Oct 2018 17:20:14 -0500 Subject: [PATCH 16/22] don't intercept cut via x in marquee strategy --- .../marquee-strategy.js | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index 1f8b1a9940..7678b01d2b 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -396,7 +396,14 @@ class SelectionStrategy { _onKeyDown (event) { // TODO key bindings + // will require re-working the keyboard command interpreter for macOS + // due to the cmd key bug + // e.g.: if (this.context.isCommandPressed('drawing:marquee:copy')) { + // + // HACK hardcodes key handler if ( + // cut + (event.key === 'x' && (event.metaKey || event.ctrlKey)) || // copy (event.key === 'c' && (event.metaKey || event.ctrlKey)) || // paste @@ -450,27 +457,6 @@ class SelectionStrategy { this.deselect() } } - - // TODO key bindings - // TODO make this assignable to 'drawing:marquee:copy' - // will require re-working the keyboard command interpreter for macOS - // due to the cmd key bug - // if (this.context.isCommandPressed('drawing:marquee:copy')) { - // - // HACK hardcodes key handler to accept either Cmd+C or Ctrl+C - // - if (event.key === 'x' && (event.metaKey || event.ctrlKey)) { - if (this.parent.marqueePath) { - this.context.sketchPane.selectedArea.set(this.parent.marqueePath) - this.parent.copyToClipboard( - this.parent.marqueePath, - this.context.sketchPane.selectedArea.copy(this.context.visibleLayersIndices) - ) - this.context.sketchPane.selectedArea.erase(this.context.visibleLayersIndices) - notifications.notify({ message: 'Cut selection', timing: 5 }) - } - return - } } _onKeyUp (event) { From 5b47a297f01c7b6244d934c98221ab14cffb9576 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Tue, 23 Oct 2018 17:23:53 -0500 Subject: [PATCH 17/22] always paste even when not moved --- src/js/window/storyboarder-sketch-pane/marquee-strategy.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index 7678b01d2b..eb35b976b4 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -903,7 +903,12 @@ class OperationStrategy { commit () { this.state.done = true - if (this.state.moved) { + if ( + // have we moved the artwork at all? + this.state.moved || + // or, was it pasted, so we have to operate even if not moved? + this.state.commitOperation === 'paste' + ) { let indices = this.context.visibleLayersIndices if (this.state.commitOperation === 'move') { From 22a0301d6fb96e1773e2294a5783711d341c2cab Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Wed, 24 Oct 2018 12:05:30 -0500 Subject: [PATCH 18/22] fix comment --- src/js/window/main-window.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/window/main-window.js b/src/js/window/main-window.js index 0baa8fec29..939810c202 100644 --- a/src/js/window/main-window.js +++ b/src/js/window/main-window.js @@ -5169,7 +5169,7 @@ ipcRenderer.on('cut', event => { // find the focused window (which may be main-window) for (let w of remote.BrowserWindow.getAllWindows()) { if (w.isFocused()) { - // console.log('copy to clipboard from window', w.id) + // console.log('cut to clipboard from window', w.id) w.webContents.cut() return } From d4ff3f632dd180a11af832ebfed7c8aa11e4e251 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Wed, 24 Oct 2018 12:09:28 -0500 Subject: [PATCH 19/22] check strategy --- src/js/window/storyboarder-sketch-pane.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/window/storyboarder-sketch-pane.js b/src/js/window/storyboarder-sketch-pane.js index 88b4c3aab9..a1eaba49b8 100644 --- a/src/js/window/storyboarder-sketch-pane.js +++ b/src/js/window/storyboarder-sketch-pane.js @@ -297,7 +297,7 @@ class StoryboarderSketchPane extends EventEmitter { } cutToClipboard () { - if (this.strategy.marqueePath) { + if (this.strategy instanceof MarqueeStrategy && this.strategy.marqueePath) { this.copyToClipboard() this.sketchPane.selectedArea.erase(this.visibleLayersIndices) } From 6ac0309d4bb1a59e89f1cb835206f3ae02e12f18 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Wed, 24 Oct 2018 12:43:57 -0500 Subject: [PATCH 20/22] copy marquee art to clipboard as flat img for copying to external editors --- src/js/window/storyboarder-sketch-pane.js | 19 +++++++++++++++++-- .../marquee-strategy.js | 5 +---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/js/window/storyboarder-sketch-pane.js b/src/js/window/storyboarder-sketch-pane.js index a1eaba49b8..4f37602e47 100644 --- a/src/js/window/storyboarder-sketch-pane.js +++ b/src/js/window/storyboarder-sketch-pane.js @@ -2,7 +2,7 @@ const paper = require('paper') const EventEmitter = require('events').EventEmitter -const { ipcRenderer, remote } = require('electron') +const { ipcRenderer, remote, nativeImage } = require('electron') const fs = require('fs') const path = require('path') @@ -305,9 +305,24 @@ class StoryboarderSketchPane extends EventEmitter { copyToClipboard () { if (this.strategy instanceof MarqueeStrategy && this.strategy.marqueePath) { this.sketchPane.selectedArea.set(this.strategy.marqueePath) + + let spritesByLayerId = this.sketchPane.selectedArea.copy(this.visibleLayersIndices) + + // flattened image of marquee selected art + let sprite = this.sketchPane.selectedArea.asSprite(this.visibleLayersIndices, false) + let pixels = this.sketchPane.app.renderer.plugins.extract.pixels(sprite) + SketchPaneUtil.arrayPostDivide(pixels) + let image = nativeImage.createFromDataURL( + SketchPaneUtil.pixelsToCanvas( + pixels, + sprite.width, + sprite.height + ).toDataURL()) + this.strategy.copyToClipboard( this.strategy.marqueePath, - this.sketchPane.selectedArea.copy(this.visibleLayersIndices) + image, + spritesByLayerId ) } } diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js index eb35b976b4..8178540f82 100644 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js @@ -55,10 +55,7 @@ class MarqueeStrategy { return this.context.sketchPane.layers.findByName(name) } - copyToClipboard (marqueePath, spritesByLayerId) { - // flattened image of marquee art - let image - + copyToClipboard (marqueePath, image, spritesByLayerId) { // marquee data, including path and bounds let marquee = { path: JSON.parse(marqueePath.exportJSON()), From b99fac15dae796a6fc176b8957b8f354ee8e2aa6 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Mon, 29 Oct 2018 14:51:08 -0500 Subject: [PATCH 21/22] update package-lock.json --- package-lock.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 80cf573c1c..98c72b0d5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1473,7 +1473,7 @@ }, "camelcase-keys": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { @@ -4119,7 +4119,7 @@ }, "got": { "version": "6.7.1", - "resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", "dev": true, "requires": { @@ -4460,7 +4460,7 @@ }, "is-builtin-module": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "dev": true, "requires": { @@ -4569,7 +4569,7 @@ }, "is-obj": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, @@ -5003,7 +5003,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -5323,7 +5323,7 @@ }, "meow": { "version": "3.7.0", - "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { @@ -6522,7 +6522,7 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { @@ -6786,7 +6786,7 @@ }, "json5": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "dev": true, "requires": { @@ -8004,7 +8004,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -9215,7 +9215,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { From 4381130fb88c2665440544d375d83b74ade4e5e4 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Tue, 30 Oct 2018 13:45:44 -0500 Subject: [PATCH 22/22] extract strategy classes to their own files --- .../marquee-strategy.js | 1024 ----------------- .../marquee-strategy/index.js | 93 ++ .../marquee-strategy/operation-strategy.js | 449 ++++++++ .../marquee-strategy/selection-strategy.js | 486 ++++++++ .../marquee-strategy/selectors.js | 7 + 5 files changed, 1035 insertions(+), 1024 deletions(-) delete mode 100644 src/js/window/storyboarder-sketch-pane/marquee-strategy.js create mode 100644 src/js/window/storyboarder-sketch-pane/marquee-strategy/index.js create mode 100644 src/js/window/storyboarder-sketch-pane/marquee-strategy/operation-strategy.js create mode 100644 src/js/window/storyboarder-sketch-pane/marquee-strategy/selection-strategy.js create mode 100644 src/js/window/storyboarder-sketch-pane/marquee-strategy/selectors.js diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy.js deleted file mode 100644 index 8178540f82..0000000000 --- a/src/js/window/storyboarder-sketch-pane/marquee-strategy.js +++ /dev/null @@ -1,1024 +0,0 @@ -const paper = require('paper') -const SketchPaneUtil = require('alchemancy').util -const { clipboard } = require('electron') -const notifications = require('../notifications') - -const constrainPoint = (point, rectangle) => { - // if paper.Point (and not paper.Segment) - if (point.round) point = point.round() - point = paper.Point.max(point, rectangle.topLeft) - point = paper.Point.min(point, rectangle.bottomRight) - return point -} - -const getFillColor = state => state.toolbar.tools[state.toolbar.activeTool].color -const getFillAlpha = state => state.toolbar.tools[state.toolbar.activeTool].strokeOpacity - -class MarqueeStrategy { - constructor (context) { - this.context = context - this.name = 'marquee' - - this.strategies = { - selection: new SelectionStrategy(this.context, this), - operation: new OperationStrategy(this.context, this) - } - - this.marqueeTransitionEvent = null - this.marqueePath = null - } - - startup () { - this.setStrategy('selection') - } - - shutdown () { - if (this.strategy) { - this.strategy.shutdown() - this.strategy = null - } - } - - setStrategy (strategy) { - if (this.strategy) this.strategy.shutdown() - this.strategy = this.strategies[strategy] - this.strategy.startup() - } - - fakePointerDown (event) { - if (this.strategy) { - this.strategy._onPointerDown(event) - } - } - - findLayerByName (name) { - return this.context.sketchPane.layers.findByName(name) - } - - copyToClipboard (marqueePath, image, spritesByLayerId) { - // marquee data, including path and bounds - let marquee = { - path: JSON.parse(marqueePath.exportJSON()), - } - - // data for each marquee layer - let width = marqueePath.bounds.width - let height = marqueePath.bounds.height - - let imageDataByLayerId = spritesByLayerId.reduce( - (coll, sprite, index) => { - let pixels = this.context.sketchPane.app.renderer.plugins.extract.pixels(sprite.texture) - SketchPaneUtil.arrayPostDivide(pixels) - let canvas = SketchPaneUtil.pixelsToCanvas(pixels, width, height) - coll[this.context.sketchPane.layers[index].name] = canvas.toDataURL() - return coll - }, - {} - ) - - clipboard.clear() - clipboard.write({ - image, - text: JSON.stringify( - { - marquee, - imageDataByLayerId - }, - null, - 2 - ) - }) - } - - pasteFromClipboard (contents) { - this.setStrategy('operation') - this.strategy.fromClipboard(contents) - notifications.notify({ message: 'Pasted selection', timing: 5 }) - } -} - -class SelectionStrategy { - constructor (context, parent) { - this.context = context - this.parent = parent - - this._onPointerDown = this._onPointerDown.bind(this) - this._onPointerMove = this._onPointerMove.bind(this) - this._onPointerUp = this._onPointerUp.bind(this) - this._onKeyDown = this._onKeyDown.bind(this) - this._onKeyUp = this._onKeyUp.bind(this) - - this._onWindowBlur = this._onWindowBlur.bind(this) - - this.offscreenCanvas = document.createElement('canvas') - this.offscreenContext = this.offscreenCanvas.getContext('2d') - - this.paperScope = paper.setup(this.offscreenCanvas) - this.paperScope.view.setAutoUpdate(false) - this.paperScope.view.remove() - } - - startup () { - this.offscreenCanvas.width = this.context.sketchPane.width - this.offscreenCanvas.height = this.context.sketchPane.height - this.layer = this.context.sketchPane.layers.findByName('composite') - - this.state = { - started: false, - complete: false, - selectionPath: null, - selectionSubPath: null, - draftPoint: null, - straightLinePressed: false, - isPointerDown: false, - - stateName: 'idle' // idle, freeform, line, add, subtract - } - - document.addEventListener('pointerdown', this._onPointerDown) - document.addEventListener('pointermove', this._onPointerMove) - document.addEventListener('pointerup', this._onPointerUp) - window.addEventListener('keydown', this._onKeyDown) - window.addEventListener('keyup', this._onKeyUp) - window.addEventListener('blur', this._onWindowBlur) - - this.context.sketchPane.cursor.setEnabled(false) - this.context.sketchPane.app.view.style.cursor = 'crosshair' - - this.boundingRect = new paper.Rectangle( - new paper.Point(0, 0), - new paper.Point(this.context.sketchPane.width, this.context.sketchPane.height) - ) - } - - shutdown () { - this.boundingRect = null - - document.removeEventListener('pointerdown', this._onPointerDown) - document.removeEventListener('pointermove', this._onPointerMove) - document.removeEventListener('pointerup', this._onPointerUp) - window.removeEventListener('keydown', this._onKeyDown) - window.removeEventListener('keyup', this._onKeyUp) - window.removeEventListener('blur', this._onWindowBlur) - - this.layer.clear() - - this.context.sketchPane.app.view.style.cursor = 'auto' - this.context.sketchPane.cursor.setEnabled(true) - } - - _isLineKeyPressed () { - return this.context.isCommandPressed('drawing:marquee:straight-line') - } - - _onPointerDown (event) { - if (event.target.id === 'toolbar-marquee') return - - if (event.target !== this.context.sketchPaneDOMElement) { - this.cancel() - return - } - - if ( - // not freeform/line/add/subtract - this.state.stateName === 'idle' && - // has already drawn a marquee - this.state.complete && - // pointerdown inside the marquee'd path(s) - this._hit(event)) - { - // transition to operating on the selection - this.parent.marqueeTransitionEvent = event - this._transitionNext() - return - } - - // if this is a new path - if (!this.state.started) { - this.context.store.dispatch({ type: 'TOOLBAR_MODE_STATUS_SET', payload: 'busy', meta: { scope: 'local' } }) - - if (this.state.stateName === 'add') { - this.state.selectionSubPath = new paper.Path() - this.state.isPointerDown = true - this._addPointFromEvent(event) - this._draw() - - } else if (this.state.stateName === 'subtract') { - this.state.selectionSubPath = new paper.Path() - this.state.isPointerDown = true - this._addPointFromEvent(event) - this._draw() - - } else { - // reset - this.state.selectionPath = new paper.Path() - - // if the line key is pressed - if (this._isLineKeyPressed()) { - this.state.stateName = 'line' - } else { - this.state.stateName = 'freeform' - } - - this.state = { - ...this.state, - started: true, - complete: false, - isPointerDown: true - } - - this._addPointFromEvent(event) - this._draw() - - if (this.state.stateName == 'line') { - this.state.draftPoint = this.context.sketchPane.localizePoint(event) - } else { - this.state.draftPoint = null - } - } - - this.context.sketchPane.cursor.setEnabled(false) - this.context.sketchPane.app.view.style.cursor = 'crosshair' - } - } - - _onPointerMove (event) { - if (this.state.stateName === 'add' || this.state.stateName === 'subtract') { - this.context.sketchPane.app.view.style.cursor = 'crosshair' - - if (this.state.isPointerDown) { - this._addPointFromEvent(event) - this._draw() - } - - } else { - if (this._hit(event) && !this.state.isPointerDown && this.state.selectionPath) { - this.context.sketchPane.app.view.style.cursor = '-webkit-grab' - } else { - this.context.sketchPane.app.view.style.cursor = 'crosshair' - } - - if (!this.state.started) return - - if (!this._isLineKeyPressed() && this.state.isPointerDown) { - this.state.stateName = 'freeform' - } - - if (this.state.stateName == 'line') { - this.state.draftPoint = this.context.sketchPane.localizePoint(event) - } else { - this.state.draftPoint = null - this._addPointFromEvent(event) - } - - this._draw() - } - } - - _onPointerUp (event) { - this.state.isPointerDown = false - - if (this.state.stateName === 'add' || this.state.stateName === 'subtract') { - this._addPointFromEvent(event) - this._endDrawnPath() - - } else { - if (!this.state.started) return - - if (this._isLineKeyPressed()) { - this.state.stateName = 'line' - } else { - this.state.stateName = 'freeform' - } - - if (this.state.stateName == 'line') { - this._addPointFromEvent(event) - this._draw() - } else { - this._addPointFromEvent(event) - - this._endDrawnPath() - } - } - } - - _endDrawnPath () { - // close the active path - let activePath = this._getActivePath() - if (activePath.segments.length) { - activePath.add( - constrainPoint( - activePath.segments[0].point.clone(), - this.boundingRect - ) - ) - } - - // avoid self-intersections - activePath = activePath.unite(this.state.selectionPath) - if (!activePath.children) { - if (activePath.segments.length) { - constrainPoint( - activePath.add(activePath.segments[0].point.clone()), - this.boundingRect - ) - } - } - activePath.closePath() - - // selectionPath is now the combined path - this.state.selectionPath = this._getCombinedPath() - // clear the sub path - this.state.selectionSubPath = null - - this.state.started = false - this.state.complete = true - this.state.draftPoint = null - - this._draw() - - this.parent.marqueePath = this.state.selectionPath.clone() - this.state.stateName = 'idle' - } - - _hit (event) { - if (!this.state.selectionPath) return false - - let point = this.context.sketchPane.localizePoint(event) - return this._getCombinedPath().contains(point) - } - - _transitionNext () { - this.context.store.dispatch({ - type: 'TOOLBAR_MODE_STATUS_SET', payload: 'idle', meta: { scope: 'local' } - }) - - this.parent.setStrategy('operation') - this.parent.strategy.fromSelection() - } - - _addPointFromEvent (event) { - let point = this.context.sketchPane.localizePoint(event) - - this._getActivePath().add( - constrainPoint(new paper.Point(point.x, point.y), this.boundingRect) - ) - } - - _getActivePath () { - return (this.state.stateName === 'add' || this.state.stateName === 'subtract') - ? this.state.selectionSubPath - : this.state.selectionPath - } - - _getCombinedPath () { - let result - if (this.state.stateName === 'add') { - result = this.state.selectionPath.clone().unite(this.state.selectionSubPath, { insert: false }) - - } else if (this.state.stateName === 'subtract') { - result = this.state.selectionPath.clone().subtract(this.state.selectionSubPath, { insert: false }) - - } else { - result = this.state.selectionPath.clone() - - } - - return result - } - - _onWindowBlur () { - // this.cancel() - } - - _onKeyDown (event) { - // TODO key bindings - // will require re-working the keyboard command interpreter for macOS - // due to the cmd key bug - // e.g.: if (this.context.isCommandPressed('drawing:marquee:copy')) { - // - // HACK hardcodes key handler - if ( - // cut - (event.key === 'x' && (event.metaKey || event.ctrlKey)) || - // copy - (event.key === 'c' && (event.metaKey || event.ctrlKey)) || - // paste - (event.key === 'v' && (event.metaKey || event.ctrlKey)) - ) { - // allow command through - return - } else { - event.preventDefault() - } - - if (this.state.complete) { - if (this.context.isCommandPressed('drawing:marquee:add')) { - this.state.stateName = 'add' - // this.context.sketchPane.app.view.style.cursor = 'zoom-in' - } - if (this.context.isCommandPressed('drawing:marquee:subtract')) { - this.state.stateName = 'subtract' - // this.context.sketchPane.app.view.style.cursor = 'zoom-out' - } - } - - if (this.context.isCommandPressed('drawing:marquee:cancel')) { - this.cancel() - } - - if (this.context.isCommandPressed('drawing:marquee:erase')) { - if (this.state.complete && this.parent.marqueePath) { - let indices = this.context.visibleLayersIndices - this.context.emit('addToUndoStack', indices) - this.context.sketchPane.selectedArea.set(this.parent.marqueePath) - this.context.sketchPane.selectedArea.erase(indices) - this.context.sketchPane.selectedArea.unset() - this.context.emit('markDirty', indices) - this.deselect() - } - } - - if (this.context.isCommandPressed('drawing:marquee:fill')) { - if (this.state.complete && this.parent.marqueePath) { - // let indices = this.context.visibleLayersIndices - let indices = [this.parent.findLayerByName('fill').index] - let state = this.context.store.getState() - let color = getFillColor(state) - let alpha = getFillAlpha(state) - this.context.emit('addToUndoStack', indices) - this.context.sketchPane.selectedArea.set(this.parent.marqueePath) - this.context.sketchPane.selectedArea.fill(indices, color, alpha) - this.context.sketchPane.selectedArea.unset() - this.context.emit('markDirty', indices) - this.deselect() - } - } - } - - _onKeyUp (event) { - event.preventDefault() - - if (this.state.complete) { - if (this.state.stateName === 'add' && !this.context.isCommandPressed('drawing:marquee:add')) { - this.state.stateName = 'freeform' - this.context.sketchPane.app.view.style.cursor = 'crosshair' - } - if (this.state.stateName === 'subtract' && !this.context.isCommandPressed('drawing:marquee:subtract')) { - this.state.stateName = 'freeform' - this.context.sketchPane.app.view.style.cursor = 'crosshair' - } - } - - if (!this._isLineKeyPressed()) { - if (this.state.started && this.state.stateName == 'line' && !this.state.isPointerDown) { - this._endDrawnPath() - } - } - } - - cancel () { - // attempt to gracefully transition back to drawing - this.context.store.dispatch({ - type: 'TOOLBAR_MODE_STATUS_SET', payload: 'idle', meta: { scope: 'local' } - }) - this.context.store.dispatch({ type: 'TOOLBAR_MODE_SET', payload: 'drawing', meta: { scope: 'local' } }) - } - - deselect () { - this.context.store.dispatch({ type: 'TOOLBAR_MODE_STATUS_SET', payload: 'idle', meta: { scope: 'local' } }) - - this.layer.clear() - - this.parent.marqueePath = null - this.state.stateName = 'idle' - - this.state.selectionPath = new paper.Path() - this.state.selectionSubPath = null - - this.state.started = false - this.state.complete = false - this.state.draftPoint = null - this.state.isPointerDown = false - - this._draw() - } - - _draw () { - let ctx = this.offscreenContext - - ctx.clearRect(0, 0, this.context.sketchPane.width, this.context.sketchPane.height) - ctx.globalAlpha = 1.0 - - let pathToDraw = this._getCombinedPath() - - let children = pathToDraw.children || [pathToDraw] - - for (let n = 0; n < children.length; n++) { - let child = children[n] - - let pointsToDraw = child.segments.map(segment => ({ x: segment.point.x, y: segment.point.y })) - - // draft point added to last child - if (this.state.draftPoint != null) { - if (n === children.length - 1) { - pointsToDraw.push(this.state.draftPoint) - } - } - - if (pointsToDraw.length) { - ctx.save() - - // white - ctx.lineWidth = 1 - ctx.strokeStyle = '#fff' - ctx.setLineDash([]) - ctx.moveTo(pointsToDraw[0].x, pointsToDraw[0].y) - ctx.beginPath() - for (let i = 0; i < pointsToDraw.length; i++) { - let point = pointsToDraw[i] - ctx.lineTo(point.x, point.y) - } - ctx.closePath() - ctx.stroke() - - // purple - ctx.lineWidth = 1 - ctx.strokeStyle = '#6A4DE7' - ctx.setLineDash([2, 5]) - ctx.moveTo(pointsToDraw[0].x, pointsToDraw[0].y) - ctx.beginPath() - for (let i = 0; i < pointsToDraw.length; i++) { - let point = pointsToDraw[i] - ctx.lineTo(point.x, point.y) - } - ctx.closePath() - ctx.stroke() - - ctx.restore() - - // diagnostic circles: - // - // for (let j = 0; j < pointsToDraw.length; j++) { - // let point = pointsToDraw[j] - // ctx.beginPath() - // ctx.arc(point.x, point.y, 10, 0, Math.PI * 2) - // ctx.fillStyle = '#f00' - // ctx.fill() - // } - } - } - - this.layer.replaceTextureFromCanvas(this.offscreenCanvas) - } -} - -class OperationStrategy { - constructor (context, parent) { - this.context = context - this.parent = parent - - this._onPointerDown = this._onPointerDown.bind(this) - this._onPointerMove = this._onPointerMove.bind(this) - this._onPointerUp = this._onPointerUp.bind(this) - this._onKeyDown = this._onKeyDown.bind(this) - this._onKeyUp = this._onKeyUp.bind(this) - } - - startup () { - document.addEventListener('pointerdown', this._onPointerDown) - document.addEventListener('pointermove', this._onPointerMove) - document.addEventListener('pointerup', this._onPointerUp) - window.addEventListener('keydown', this._onKeyDown) - window.addEventListener('keyup', this._onKeyUp) - - this.layer = this.context.sketchPane.layers.findByName('composite') - this.backgroundMatte = null - - this.context.store.dispatch({ type: 'TOOLBAR_MODE_STATUS_SET', payload: 'busy', meta: { scope: 'local' } }) - } - - fromClipboard (contents) { - try { - this.parent.marqueePath = new paper.Path().importJSON(contents.marquee.path) - - this.state = { - marqueePath: this.parent.marqueePath.clone(), - moved: false, - done: false, - commitOperation: 'paste' - } - this.state.target = { - x: this.state.marqueePath.bounds.x, - y: this.state.marqueePath.bounds.y - } - this.context.sketchPane.selectedArea.set(this.state.marqueePath) - // delete ALL cached canvas textures to ensure canvas is re-rendered - PIXI.utils.clearTextureCache() - - this.cutSprite = new PIXI.Sprite(PIXI.Texture.EMPTY) - - Promise.all( - Object.entries(contents.imageDataByLayerId).map(([name, data]) => - new Promise((resolve, reject) => { - let texture = PIXI.Texture.fromImage(data) - texture.baseTexture.once('loaded', () => { - resolve({ name, texture }) - }) - // TODO does this need to be cleaned up? - texture.baseTexture.once('error', () => { - reject(err) - }) - }) - ) - ).then(pairs => { - this.cutSprite = new PIXI.Sprite(PIXI.Texture.EMPTY) - for (let { name, texture } of pairs) { - let sprite = new PIXI.Sprite(texture) - sprite.name = name - this.cutSprite.addChild(sprite) - } - - this.setupOperation() - }).catch(err => { - console.error(err) - throw err - }) - } catch (err) { - console.error(err) - alert('Whoops! Couldn’t paste.') - } - } - - fromSelection () { - this.state = { - marqueePath: this.parent.marqueePath.clone(), - moved: false, - done: false, - commitOperation: 'move' // move, fill, paste - } - this.state.target = { - x: this.state.marqueePath.bounds.x, - y: this.state.marqueePath.bounds.y - } - - this.context.sketchPane.selectedArea.set(this.state.marqueePath) - - // delete ALL cached canvas textures to ensure canvas is re-rendered - PIXI.utils.clearTextureCache() - - this.cutSprite = this.context.sketchPane.selectedArea.asSprite(this.context.visibleLayersIndices) - - this.setupOperation() - - let maskSprite = this.context.sketchPane.selectedArea.asMaskSprite(true) - this.flattenedLayerSprite.addChild(maskSprite) - this.flattenedLayerSprite.mask = maskSprite - - // HACK force the first pointer down - this._onPointerDown(this.parent.marqueeTransitionEvent) - } - - setupOperation () { - this.outlineSprite = this.context.sketchPane.selectedArea.asOutlineSprite() - this.areaPolygons = this.context.sketchPane.selectedArea.asPolygons(false) - - // TODO should this move to a SelectedArea setup/prepare method? - - // solid background - this.backgroundMatte = new PIXI.Graphics() - this.backgroundMatte.beginFill(0xffffff) - // draw a rectangle - this.backgroundMatte.drawRect(0, 0, this.context.sketchPane.width, this.context.sketchPane.height) - this.layer.sprite.addChild(this.backgroundMatte) - - this.flattenedLayerSprite = new PIXI.Sprite( - PIXI.Texture.fromCanvas( - this.context.sketchPane.layers.asFlattenedCanvas( - this.context.sketchPane.width, - this.context.sketchPane.height, - this.context.visibleLayersIndices - ) - ) - ) - - this.layer.sprite.addChild(this.flattenedLayerSprite) - - // draw the cut sprite - this.layer.sprite.addChild(this.cutSprite) - - // draw the outline - this.layer.sprite.addChild(this.outlineSprite) - - // positioning - this.draw() - - this.context.sketchPane.cursor.setEnabled(false) - this.context.sketchPane.app.view.style.cursor = 'auto' - } - - shutdown () { - if (!this.state.done) { - this.cleanup() - this.state.done = true - } - - document.removeEventListener('pointerdown', this._onPointerDown) - document.removeEventListener('pointermove', this._onPointerMove) - document.removeEventListener('pointerup', this._onPointerUp) - window.removeEventListener('keydown', this._onKeyDown) - window.removeEventListener('keyup', this._onKeyUp) - - this.context.sketchPane.app.view.style.cursor = 'auto' - this.context.sketchPane.cursor.setEnabled(true) - } - - hit (polygons, point) { - for (let polygon of polygons) { - if (polygon.contains(point.x, point.y)) { - return true - } - } - return false - } - - _onPointerDown (event) { - if (event.target !== this.context.sketchPaneDOMElement) { - this.cancel() - return - } - - let point = this.context.sketchPane.localizePoint(event) - this.state = { - ...this.state, - done: false, - hitAreaPolygons: this.areaPolygons.map(polygon => polygon.clone()) - } - - // was it outside? - if (!this.hit(this.state.hitAreaPolygons, point)) { - this.commit() - - // NOTE this works because OperationStrategy instance stays in memory after shutdown - this.parent.fakePointerDown(event) - return - } - - this.state = { - ...this.state, - down: true, - spriteOrigin: { x: this.cutSprite.x, y: this.cutSprite.y }, - origin: { x: point.x, y: point.y }, - position: { x: point.x, y: point.y } - } - - this.updateCursor(event) - } - - _onPointerMove (event) { - let point = this.context.sketchPane.localizePoint(event) - - if (this.state.down) { - this.state.position = point - this.state.target.x = Math.round(this.state.spriteOrigin.x + (this.state.position.x - this.state.origin.x)) - this.state.target.y = Math.round(this.state.spriteOrigin.y + (this.state.position.y - this.state.origin.y)) - - this.state.moved = ( - this.state.target.x != this.state.marqueePath.bounds.x || - this.state.target.x != this.state.marqueePath.bounds.y - ) - - this.draw() - } - this.updateCursor(event) - } - - _onPointerUp (event) { - this.state.down = false - - this.areaPolygons = this.state.hitAreaPolygons.map(polygon => polygon.clone()) - - this.updateCursor(event) - } - - _onKeyDown (event) { - // TODO key bindings - if (event.key === 'v' && (event.metaKey || event.ctrlKey)) { - // commit first - this.commit() - // allow paste command through - return - } else { - event.preventDefault() - } - - if (this.context.isCommandPressed('drawing:marquee:cancel')) { - this.cancel() - return - } - - if (this.context.isCommandPressed('drawing:marquee:commit')) { - this.commit() - return - } - - if (this.context.isCommandPressed('drawing:marquee:erase')) { - // we'll be doing an erase - this.state.commitOperation = 'erase' - // erase the cutout contents - this.cutSprite.texture.destroy() - this.cutSprite.texture = this.context.sketchPane.selectedArea.asFilledTexture(0xffffff, 0.0) - this.cutSprite.alpha = 1.0 - return - } - - if (this.context.isCommandPressed('drawing:marquee:fill')) { - // setup a live preview of the fill - - // we'll be doing a fill when we commit - this.state.commitOperation = 'fill' - - // clear existing cut sprite - this.cutSprite.removeChildren() - this.cutSprite.texture.destroy() - this.cutSprite.texture = PIXI.Texture.EMPTY - - let fillLayer = this.parent.findLayerByName('fill') - - let indices = this.context.visibleLayersIndices - - // all layer indexes _except_ for the fill layer - let filtered = indices.filter(n => n != fillLayer.index) - // associative array of layer index -> Sprite cutout - let sprites = this.context.sketchPane.selectedArea.copy(filtered) - - // `fill` layer cutout sprite - let state = this.context.store.getState() - let color = getFillColor(state) - let alpha = getFillAlpha(state) - let texture = this.context.sketchPane.selectedArea.asFilledTexture(color, alpha) - let sprite = new PIXI.Sprite(texture) - // add the `fill` layer cutout sprite back in - sprites[fillLayer.index] = sprite - - for (let index of indices) { - this.cutSprite.addChild(sprites[index]) - } - } - } - - _onKeyUp (event) { - event.preventDefault() - } - - cleanup () { - this.layer.sprite.removeChild(this.backgroundMatte) - this.layer.sprite.removeChild(this.flattenedLayerSprite) - this.layer.sprite.removeChild(this.outlineSprite) - this.cutSprite.removeChildren() - this.layer.sprite.removeChild(this.cutSprite) - this.layer.clear() - - this.parent.marqueePath = null - } - - complete () { - this.cleanup() - - this.context.store.dispatch({ - type: 'TOOLBAR_MODE_STATUS_SET', payload: 'idle', meta: { scope: 'local' } - }) - - this.parent.setStrategy('selection') - } - - cancel () { - this.state.done = true - this.complete() - } - - commit () { - this.state.done = true - - if ( - // have we moved the artwork at all? - this.state.moved || - // or, was it pasted, so we have to operate even if not moved? - this.state.commitOperation === 'paste' - ) { - let indices = this.context.visibleLayersIndices - - if (this.state.commitOperation === 'move') { - this.context.emit('addToUndoStack', indices) - let sprites = this.context.sketchPane.selectedArea.copy(indices) - sprites.forEach(sprite => { - sprite.x = this.state.target.x - sprite.y = this.state.target.y - }) - this.context.sketchPane.selectedArea.erase(indices) - this.context.sketchPane.selectedArea.paste(indices, sprites) - this.context.emit('markDirty', indices) - - } else if (this.state.commitOperation === 'paste') { - this.context.emit('addToUndoStack', indices) - - // look for layers in the clipboard that match layers we have by id - // add any matches to the paste-able sprites list - let indexes = [] - let sprites = [] - for (let sprite of this.cutSprite.children) { - let layer = this.context.sketchPane.layers.findByName(sprite.name) - if (layer) { - indexes.push(layer.index) - sprites[layer.index] = sprite - } else { - console.log('ignoring layer with name', sprite.name) - } - } - // move the sprites - sprites.forEach(sprite => { - sprite.x = this.state.target.x - sprite.y = this.state.target.y - }) - - this.context.sketchPane.selectedArea.paste(indices, sprites) - this.context.emit('markDirty', indices) - - } else if (this.state.commitOperation === 'erase') { - this.context.emit('addToUndoStack', indices) - this.context.sketchPane.selectedArea.erase(indices) - this.context.emit('markDirty', indices) - - } else if (this.state.commitOperation === 'fill') { - let fillLayer = this.parent.findLayerByName('fill') - - // all layer indexes _except_ for the fill layer - let filtered = indices.filter(n => n != fillLayer.index) - // associative array of layer index -> Sprite cutout - let sprites = this.context.sketchPane.selectedArea.copy(filtered) - - // `fill` layer cutout sprite - let state = this.context.store.getState() - let color = getFillColor(state) - let alpha = getFillAlpha(state) - let texture = this.context.sketchPane.selectedArea.asFilledTexture(color, alpha) - let sprite = new PIXI.Sprite(texture) - // add the `fill` layer cutout sprite back in - sprites[fillLayer.index] = sprite - - // set the position of all cutout sprites - sprites.forEach(sprite => { - sprite.x = this.state.target.x - sprite.y = this.state.target.y - }) - - this.context.emit('addToUndoStack', indices) - this.context.sketchPane.selectedArea.erase(indices) - this.context.sketchPane.selectedArea.paste(indices, sprites) - this.context.emit('markDirty', indices) - } - } - - this.complete() - } - - draw () { - this.outlineSprite.x = this.state.target.x - this.outlineSprite.y = this.state.target.y - - this.cutSprite.x = this.state.target.x - this.cutSprite.y = this.state.target.y - - if (this.state.hitAreaPolygons) { - // translate area polygons - for (let i = 0; i < this.state.hitAreaPolygons.length; i++) { - for (let j = 0; j < this.state.hitAreaPolygons[i].points.length; j += 2) { - let offsetX = this.state.target.x - this.state.spriteOrigin.x - let offsetY = this.state.target.y - this.state.spriteOrigin.y - this.state.hitAreaPolygons[i].points[j + 0] = this.areaPolygons[i].points[j + 0] + offsetX - this.state.hitAreaPolygons[i].points[j + 1] = this.areaPolygons[i].points[j + 1] + offsetY - } - } - } - } - - updateCursor (event) { - let point = this.context.sketchPane.localizePoint(event) - // set cursor - if (this.state.hitAreaPolygons && this.hit(this.state.hitAreaPolygons, point)) { - if (this.state.down) { - this.context.sketchPane.app.view.style.cursor = '-webkit-grabbing' - } else { - this.context.sketchPane.app.view.style.cursor = '-webkit-grab' - } - } else { - if (event.target == this.context.sketchPaneDOMElement) { - this.context.sketchPane.app.view.style.cursor = 'crosshair' - } else { - this.context.sketchPane.app.view.style.cursor = 'auto' - } - } - } -} - -module.exports = MarqueeStrategy diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy/index.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy/index.js new file mode 100644 index 0000000000..dd2969532a --- /dev/null +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy/index.js @@ -0,0 +1,93 @@ +const paper = require('paper') +const SketchPaneUtil = require('alchemancy').util +const { clipboard } = require('electron') + +const notifications = require('../../notifications') +const SelectionStrategy = require('./selection-strategy') +const OperationStrategy = require('./operation-strategy') +const { getFillColor, getFillAlpha } = require('./selectors') + +class MarqueeStrategy { + constructor (context) { + this.context = context + this.name = 'marquee' + + this.strategies = { + selection: new SelectionStrategy(this.context, this), + operation: new OperationStrategy(this.context, this) + } + + this.marqueeTransitionEvent = null + this.marqueePath = null + } + + startup () { + this.setStrategy('selection') + } + + shutdown () { + if (this.strategy) { + this.strategy.shutdown() + this.strategy = null + } + } + + setStrategy (strategy) { + if (this.strategy) this.strategy.shutdown() + this.strategy = this.strategies[strategy] + this.strategy.startup() + } + + fakePointerDown (event) { + if (this.strategy) { + this.strategy._onPointerDown(event) + } + } + + findLayerByName (name) { + return this.context.sketchPane.layers.findByName(name) + } + + copyToClipboard (marqueePath, image, spritesByLayerId) { + // marquee data, including path and bounds + let marquee = { + path: JSON.parse(marqueePath.exportJSON()), + } + + // data for each marquee layer + let width = marqueePath.bounds.width + let height = marqueePath.bounds.height + + let imageDataByLayerId = spritesByLayerId.reduce( + (coll, sprite, index) => { + let pixels = this.context.sketchPane.app.renderer.plugins.extract.pixels(sprite.texture) + SketchPaneUtil.arrayPostDivide(pixels) + let canvas = SketchPaneUtil.pixelsToCanvas(pixels, width, height) + coll[this.context.sketchPane.layers[index].name] = canvas.toDataURL() + return coll + }, + {} + ) + + clipboard.clear() + clipboard.write({ + image, + text: JSON.stringify( + { + marquee, + imageDataByLayerId + }, + null, + 2 + ) + }) + } + + pasteFromClipboard (contents) { + this.setStrategy('operation') + this.strategy.fromClipboard(contents) + notifications.notify({ message: 'Pasted selection', timing: 5 }) + } +} + +module.exports = MarqueeStrategy diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy/operation-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy/operation-strategy.js new file mode 100644 index 0000000000..4182bad20a --- /dev/null +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy/operation-strategy.js @@ -0,0 +1,449 @@ +const { getFillColor, getFillAlpha } = require('./selectors') + +module.exports = class OperationStrategy { + constructor (context, parent) { + this.context = context + this.parent = parent + + this._onPointerDown = this._onPointerDown.bind(this) + this._onPointerMove = this._onPointerMove.bind(this) + this._onPointerUp = this._onPointerUp.bind(this) + this._onKeyDown = this._onKeyDown.bind(this) + this._onKeyUp = this._onKeyUp.bind(this) + } + + startup () { + document.addEventListener('pointerdown', this._onPointerDown) + document.addEventListener('pointermove', this._onPointerMove) + document.addEventListener('pointerup', this._onPointerUp) + window.addEventListener('keydown', this._onKeyDown) + window.addEventListener('keyup', this._onKeyUp) + + this.layer = this.context.sketchPane.layers.findByName('composite') + this.backgroundMatte = null + + this.context.store.dispatch({ type: 'TOOLBAR_MODE_STATUS_SET', payload: 'busy', meta: { scope: 'local' } }) + } + + fromClipboard (contents) { + try { + this.parent.marqueePath = new paper.Path().importJSON(contents.marquee.path) + + this.state = { + marqueePath: this.parent.marqueePath.clone(), + moved: false, + done: false, + commitOperation: 'paste' + } + this.state.target = { + x: this.state.marqueePath.bounds.x, + y: this.state.marqueePath.bounds.y + } + this.context.sketchPane.selectedArea.set(this.state.marqueePath) + // delete ALL cached canvas textures to ensure canvas is re-rendered + PIXI.utils.clearTextureCache() + + this.cutSprite = new PIXI.Sprite(PIXI.Texture.EMPTY) + + Promise.all( + Object.entries(contents.imageDataByLayerId).map(([name, data]) => + new Promise((resolve, reject) => { + let texture = PIXI.Texture.fromImage(data) + texture.baseTexture.once('loaded', () => { + resolve({ name, texture }) + }) + // TODO does this need to be cleaned up? + texture.baseTexture.once('error', () => { + reject(err) + }) + }) + ) + ).then(pairs => { + this.cutSprite = new PIXI.Sprite(PIXI.Texture.EMPTY) + for (let { name, texture } of pairs) { + let sprite = new PIXI.Sprite(texture) + sprite.name = name + this.cutSprite.addChild(sprite) + } + + this.setupOperation() + }).catch(err => { + console.error(err) + throw err + }) + } catch (err) { + console.error(err) + alert('Whoops! Couldn’t paste.') + } + } + + fromSelection () { + this.state = { + marqueePath: this.parent.marqueePath.clone(), + moved: false, + done: false, + commitOperation: 'move' // move, fill, paste + } + this.state.target = { + x: this.state.marqueePath.bounds.x, + y: this.state.marqueePath.bounds.y + } + + this.context.sketchPane.selectedArea.set(this.state.marqueePath) + + // delete ALL cached canvas textures to ensure canvas is re-rendered + PIXI.utils.clearTextureCache() + + this.cutSprite = this.context.sketchPane.selectedArea.asSprite(this.context.visibleLayersIndices) + + this.setupOperation() + + let maskSprite = this.context.sketchPane.selectedArea.asMaskSprite(true) + this.flattenedLayerSprite.addChild(maskSprite) + this.flattenedLayerSprite.mask = maskSprite + + // HACK force the first pointer down + this._onPointerDown(this.parent.marqueeTransitionEvent) + } + + setupOperation () { + this.outlineSprite = this.context.sketchPane.selectedArea.asOutlineSprite() + this.areaPolygons = this.context.sketchPane.selectedArea.asPolygons(false) + + // TODO should this move to a SelectedArea setup/prepare method? + + // solid background + this.backgroundMatte = new PIXI.Graphics() + this.backgroundMatte.beginFill(0xffffff) + // draw a rectangle + this.backgroundMatte.drawRect(0, 0, this.context.sketchPane.width, this.context.sketchPane.height) + this.layer.sprite.addChild(this.backgroundMatte) + + this.flattenedLayerSprite = new PIXI.Sprite( + PIXI.Texture.fromCanvas( + this.context.sketchPane.layers.asFlattenedCanvas( + this.context.sketchPane.width, + this.context.sketchPane.height, + this.context.visibleLayersIndices + ) + ) + ) + + this.layer.sprite.addChild(this.flattenedLayerSprite) + + // draw the cut sprite + this.layer.sprite.addChild(this.cutSprite) + + // draw the outline + this.layer.sprite.addChild(this.outlineSprite) + + // positioning + this.draw() + + this.context.sketchPane.cursor.setEnabled(false) + this.context.sketchPane.app.view.style.cursor = 'auto' + } + + shutdown () { + if (!this.state.done) { + this.cleanup() + this.state.done = true + } + + document.removeEventListener('pointerdown', this._onPointerDown) + document.removeEventListener('pointermove', this._onPointerMove) + document.removeEventListener('pointerup', this._onPointerUp) + window.removeEventListener('keydown', this._onKeyDown) + window.removeEventListener('keyup', this._onKeyUp) + + this.context.sketchPane.app.view.style.cursor = 'auto' + this.context.sketchPane.cursor.setEnabled(true) + } + + hit (polygons, point) { + for (let polygon of polygons) { + if (polygon.contains(point.x, point.y)) { + return true + } + } + return false + } + + _onPointerDown (event) { + if (event.target !== this.context.sketchPaneDOMElement) { + this.cancel() + return + } + + let point = this.context.sketchPane.localizePoint(event) + this.state = { + ...this.state, + done: false, + hitAreaPolygons: this.areaPolygons.map(polygon => polygon.clone()) + } + + // was it outside? + if (!this.hit(this.state.hitAreaPolygons, point)) { + this.commit() + + // NOTE this works because OperationStrategy instance stays in memory after shutdown + this.parent.fakePointerDown(event) + return + } + + this.state = { + ...this.state, + down: true, + spriteOrigin: { x: this.cutSprite.x, y: this.cutSprite.y }, + origin: { x: point.x, y: point.y }, + position: { x: point.x, y: point.y } + } + + this.updateCursor(event) + } + + _onPointerMove (event) { + let point = this.context.sketchPane.localizePoint(event) + + if (this.state.down) { + this.state.position = point + this.state.target.x = Math.round(this.state.spriteOrigin.x + (this.state.position.x - this.state.origin.x)) + this.state.target.y = Math.round(this.state.spriteOrigin.y + (this.state.position.y - this.state.origin.y)) + + this.state.moved = ( + this.state.target.x != this.state.marqueePath.bounds.x || + this.state.target.x != this.state.marqueePath.bounds.y + ) + + this.draw() + } + this.updateCursor(event) + } + + _onPointerUp (event) { + this.state.down = false + + this.areaPolygons = this.state.hitAreaPolygons.map(polygon => polygon.clone()) + + this.updateCursor(event) + } + + _onKeyDown (event) { + // TODO key bindings + if (event.key === 'v' && (event.metaKey || event.ctrlKey)) { + // commit first + this.commit() + // allow paste command through + return + } else { + event.preventDefault() + } + + if (this.context.isCommandPressed('drawing:marquee:cancel')) { + this.cancel() + return + } + + if (this.context.isCommandPressed('drawing:marquee:commit')) { + this.commit() + return + } + + if (this.context.isCommandPressed('drawing:marquee:erase')) { + // we'll be doing an erase + this.state.commitOperation = 'erase' + // erase the cutout contents + this.cutSprite.texture.destroy() + this.cutSprite.texture = this.context.sketchPane.selectedArea.asFilledTexture(0xffffff, 0.0) + this.cutSprite.alpha = 1.0 + return + } + + if (this.context.isCommandPressed('drawing:marquee:fill')) { + // setup a live preview of the fill + + // we'll be doing a fill when we commit + this.state.commitOperation = 'fill' + + // clear existing cut sprite + this.cutSprite.removeChildren() + this.cutSprite.texture.destroy() + this.cutSprite.texture = PIXI.Texture.EMPTY + + let fillLayer = this.parent.findLayerByName('fill') + + let indices = this.context.visibleLayersIndices + + // all layer indexes _except_ for the fill layer + let filtered = indices.filter(n => n != fillLayer.index) + // associative array of layer index -> Sprite cutout + let sprites = this.context.sketchPane.selectedArea.copy(filtered) + + // `fill` layer cutout sprite + let state = this.context.store.getState() + let color = getFillColor(state) + let alpha = getFillAlpha(state) + let texture = this.context.sketchPane.selectedArea.asFilledTexture(color, alpha) + let sprite = new PIXI.Sprite(texture) + // add the `fill` layer cutout sprite back in + sprites[fillLayer.index] = sprite + + for (let index of indices) { + this.cutSprite.addChild(sprites[index]) + } + } + } + + _onKeyUp (event) { + event.preventDefault() + } + + cleanup () { + this.layer.sprite.removeChild(this.backgroundMatte) + this.layer.sprite.removeChild(this.flattenedLayerSprite) + this.layer.sprite.removeChild(this.outlineSprite) + this.cutSprite.removeChildren() + this.layer.sprite.removeChild(this.cutSprite) + this.layer.clear() + + this.parent.marqueePath = null + } + + complete () { + this.cleanup() + + this.context.store.dispatch({ + type: 'TOOLBAR_MODE_STATUS_SET', payload: 'idle', meta: { scope: 'local' } + }) + + this.parent.setStrategy('selection') + } + + cancel () { + this.state.done = true + this.complete() + } + + commit () { + this.state.done = true + + if ( + // have we moved the artwork at all? + this.state.moved || + // or, was it pasted, so we have to operate even if not moved? + this.state.commitOperation === 'paste' + ) { + let indices = this.context.visibleLayersIndices + + if (this.state.commitOperation === 'move') { + this.context.emit('addToUndoStack', indices) + let sprites = this.context.sketchPane.selectedArea.copy(indices) + sprites.forEach(sprite => { + sprite.x = this.state.target.x + sprite.y = this.state.target.y + }) + this.context.sketchPane.selectedArea.erase(indices) + this.context.sketchPane.selectedArea.paste(indices, sprites) + this.context.emit('markDirty', indices) + + } else if (this.state.commitOperation === 'paste') { + this.context.emit('addToUndoStack', indices) + + // look for layers in the clipboard that match layers we have by id + // add any matches to the paste-able sprites list + let indexes = [] + let sprites = [] + for (let sprite of this.cutSprite.children) { + let layer = this.context.sketchPane.layers.findByName(sprite.name) + if (layer) { + indexes.push(layer.index) + sprites[layer.index] = sprite + } else { + console.log('ignoring layer with name', sprite.name) + } + } + // move the sprites + sprites.forEach(sprite => { + sprite.x = this.state.target.x + sprite.y = this.state.target.y + }) + + this.context.sketchPane.selectedArea.paste(indices, sprites) + this.context.emit('markDirty', indices) + + } else if (this.state.commitOperation === 'erase') { + this.context.emit('addToUndoStack', indices) + this.context.sketchPane.selectedArea.erase(indices) + this.context.emit('markDirty', indices) + + } else if (this.state.commitOperation === 'fill') { + let fillLayer = this.parent.findLayerByName('fill') + + // all layer indexes _except_ for the fill layer + let filtered = indices.filter(n => n != fillLayer.index) + // associative array of layer index -> Sprite cutout + let sprites = this.context.sketchPane.selectedArea.copy(filtered) + + // `fill` layer cutout sprite + let state = this.context.store.getState() + let color = getFillColor(state) + let alpha = getFillAlpha(state) + let texture = this.context.sketchPane.selectedArea.asFilledTexture(color, alpha) + let sprite = new PIXI.Sprite(texture) + // add the `fill` layer cutout sprite back in + sprites[fillLayer.index] = sprite + + // set the position of all cutout sprites + sprites.forEach(sprite => { + sprite.x = this.state.target.x + sprite.y = this.state.target.y + }) + + this.context.emit('addToUndoStack', indices) + this.context.sketchPane.selectedArea.erase(indices) + this.context.sketchPane.selectedArea.paste(indices, sprites) + this.context.emit('markDirty', indices) + } + } + + this.complete() + } + + draw () { + this.outlineSprite.x = this.state.target.x + this.outlineSprite.y = this.state.target.y + + this.cutSprite.x = this.state.target.x + this.cutSprite.y = this.state.target.y + + if (this.state.hitAreaPolygons) { + // translate area polygons + for (let i = 0; i < this.state.hitAreaPolygons.length; i++) { + for (let j = 0; j < this.state.hitAreaPolygons[i].points.length; j += 2) { + let offsetX = this.state.target.x - this.state.spriteOrigin.x + let offsetY = this.state.target.y - this.state.spriteOrigin.y + this.state.hitAreaPolygons[i].points[j + 0] = this.areaPolygons[i].points[j + 0] + offsetX + this.state.hitAreaPolygons[i].points[j + 1] = this.areaPolygons[i].points[j + 1] + offsetY + } + } + } + } + + updateCursor (event) { + let point = this.context.sketchPane.localizePoint(event) + // set cursor + if (this.state.hitAreaPolygons && this.hit(this.state.hitAreaPolygons, point)) { + if (this.state.down) { + this.context.sketchPane.app.view.style.cursor = '-webkit-grabbing' + } else { + this.context.sketchPane.app.view.style.cursor = '-webkit-grab' + } + } else { + if (event.target == this.context.sketchPaneDOMElement) { + this.context.sketchPane.app.view.style.cursor = 'crosshair' + } else { + this.context.sketchPane.app.view.style.cursor = 'auto' + } + } + } +} diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy/selection-strategy.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy/selection-strategy.js new file mode 100644 index 0000000000..e1f7f542b3 --- /dev/null +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy/selection-strategy.js @@ -0,0 +1,486 @@ +const paper = require('paper') +const { getFillColor, getFillAlpha } = require('./selectors') + +const constrainPoint = (point, rectangle) => { + // if paper.Point (and not paper.Segment) + if (point.round) point = point.round() + point = paper.Point.max(point, rectangle.topLeft) + point = paper.Point.min(point, rectangle.bottomRight) + return point +} + +module.exports = class SelectionStrategy { + constructor (context, parent) { + this.context = context + this.parent = parent + + this._onPointerDown = this._onPointerDown.bind(this) + this._onPointerMove = this._onPointerMove.bind(this) + this._onPointerUp = this._onPointerUp.bind(this) + this._onKeyDown = this._onKeyDown.bind(this) + this._onKeyUp = this._onKeyUp.bind(this) + + this._onWindowBlur = this._onWindowBlur.bind(this) + + this.offscreenCanvas = document.createElement('canvas') + this.offscreenContext = this.offscreenCanvas.getContext('2d') + + this.paperScope = paper.setup(this.offscreenCanvas) + this.paperScope.view.setAutoUpdate(false) + this.paperScope.view.remove() + } + + startup () { + this.offscreenCanvas.width = this.context.sketchPane.width + this.offscreenCanvas.height = this.context.sketchPane.height + this.layer = this.context.sketchPane.layers.findByName('composite') + + this.state = { + started: false, + complete: false, + selectionPath: null, + selectionSubPath: null, + draftPoint: null, + straightLinePressed: false, + isPointerDown: false, + + stateName: 'idle' // idle, freeform, line, add, subtract + } + + document.addEventListener('pointerdown', this._onPointerDown) + document.addEventListener('pointermove', this._onPointerMove) + document.addEventListener('pointerup', this._onPointerUp) + window.addEventListener('keydown', this._onKeyDown) + window.addEventListener('keyup', this._onKeyUp) + window.addEventListener('blur', this._onWindowBlur) + + this.context.sketchPane.cursor.setEnabled(false) + this.context.sketchPane.app.view.style.cursor = 'crosshair' + + this.boundingRect = new paper.Rectangle( + new paper.Point(0, 0), + new paper.Point(this.context.sketchPane.width, this.context.sketchPane.height) + ) + } + + shutdown () { + this.boundingRect = null + + document.removeEventListener('pointerdown', this._onPointerDown) + document.removeEventListener('pointermove', this._onPointerMove) + document.removeEventListener('pointerup', this._onPointerUp) + window.removeEventListener('keydown', this._onKeyDown) + window.removeEventListener('keyup', this._onKeyUp) + window.removeEventListener('blur', this._onWindowBlur) + + this.layer.clear() + + this.context.sketchPane.app.view.style.cursor = 'auto' + this.context.sketchPane.cursor.setEnabled(true) + } + + _isLineKeyPressed () { + return this.context.isCommandPressed('drawing:marquee:straight-line') + } + + _onPointerDown (event) { + if (event.target.id === 'toolbar-marquee') return + + if (event.target !== this.context.sketchPaneDOMElement) { + this.cancel() + return + } + + if ( + // not freeform/line/add/subtract + this.state.stateName === 'idle' && + // has already drawn a marquee + this.state.complete && + // pointerdown inside the marquee'd path(s) + this._hit(event)) + { + // transition to operating on the selection + this.parent.marqueeTransitionEvent = event + this._transitionNext() + return + } + + // if this is a new path + if (!this.state.started) { + this.context.store.dispatch({ type: 'TOOLBAR_MODE_STATUS_SET', payload: 'busy', meta: { scope: 'local' } }) + + if (this.state.stateName === 'add') { + this.state.selectionSubPath = new paper.Path() + this.state.isPointerDown = true + this._addPointFromEvent(event) + this._draw() + + } else if (this.state.stateName === 'subtract') { + this.state.selectionSubPath = new paper.Path() + this.state.isPointerDown = true + this._addPointFromEvent(event) + this._draw() + + } else { + // reset + this.state.selectionPath = new paper.Path() + + // if the line key is pressed + if (this._isLineKeyPressed()) { + this.state.stateName = 'line' + } else { + this.state.stateName = 'freeform' + } + + this.state = { + ...this.state, + started: true, + complete: false, + isPointerDown: true + } + + this._addPointFromEvent(event) + this._draw() + + if (this.state.stateName == 'line') { + this.state.draftPoint = this.context.sketchPane.localizePoint(event) + } else { + this.state.draftPoint = null + } + } + + this.context.sketchPane.cursor.setEnabled(false) + this.context.sketchPane.app.view.style.cursor = 'crosshair' + } + } + + _onPointerMove (event) { + if (this.state.stateName === 'add' || this.state.stateName === 'subtract') { + this.context.sketchPane.app.view.style.cursor = 'crosshair' + + if (this.state.isPointerDown) { + this._addPointFromEvent(event) + this._draw() + } + + } else { + if (this._hit(event) && !this.state.isPointerDown && this.state.selectionPath) { + this.context.sketchPane.app.view.style.cursor = '-webkit-grab' + } else { + this.context.sketchPane.app.view.style.cursor = 'crosshair' + } + + if (!this.state.started) return + + if (!this._isLineKeyPressed() && this.state.isPointerDown) { + this.state.stateName = 'freeform' + } + + if (this.state.stateName == 'line') { + this.state.draftPoint = this.context.sketchPane.localizePoint(event) + } else { + this.state.draftPoint = null + this._addPointFromEvent(event) + } + + this._draw() + } + } + + _onPointerUp (event) { + this.state.isPointerDown = false + + if (this.state.stateName === 'add' || this.state.stateName === 'subtract') { + this._addPointFromEvent(event) + this._endDrawnPath() + + } else { + if (!this.state.started) return + + if (this._isLineKeyPressed()) { + this.state.stateName = 'line' + } else { + this.state.stateName = 'freeform' + } + + if (this.state.stateName == 'line') { + this._addPointFromEvent(event) + this._draw() + } else { + this._addPointFromEvent(event) + + this._endDrawnPath() + } + } + } + + _endDrawnPath () { + // close the active path + let activePath = this._getActivePath() + if (activePath.segments.length) { + activePath.add( + constrainPoint( + activePath.segments[0].point.clone(), + this.boundingRect + ) + ) + } + + // avoid self-intersections + activePath = activePath.unite(this.state.selectionPath) + if (!activePath.children) { + if (activePath.segments.length) { + constrainPoint( + activePath.add(activePath.segments[0].point.clone()), + this.boundingRect + ) + } + } + activePath.closePath() + + // selectionPath is now the combined path + this.state.selectionPath = this._getCombinedPath() + // clear the sub path + this.state.selectionSubPath = null + + this.state.started = false + this.state.complete = true + this.state.draftPoint = null + + this._draw() + + this.parent.marqueePath = this.state.selectionPath.clone() + this.state.stateName = 'idle' + } + + _hit (event) { + if (!this.state.selectionPath) return false + + let point = this.context.sketchPane.localizePoint(event) + return this._getCombinedPath().contains(point) + } + + _transitionNext () { + this.context.store.dispatch({ + type: 'TOOLBAR_MODE_STATUS_SET', payload: 'idle', meta: { scope: 'local' } + }) + + this.parent.setStrategy('operation') + this.parent.strategy.fromSelection() + } + + _addPointFromEvent (event) { + let point = this.context.sketchPane.localizePoint(event) + + this._getActivePath().add( + constrainPoint(new paper.Point(point.x, point.y), this.boundingRect) + ) + } + + _getActivePath () { + return (this.state.stateName === 'add' || this.state.stateName === 'subtract') + ? this.state.selectionSubPath + : this.state.selectionPath + } + + _getCombinedPath () { + let result + if (this.state.stateName === 'add') { + result = this.state.selectionPath.clone().unite(this.state.selectionSubPath, { insert: false }) + + } else if (this.state.stateName === 'subtract') { + result = this.state.selectionPath.clone().subtract(this.state.selectionSubPath, { insert: false }) + + } else { + result = this.state.selectionPath.clone() + + } + + return result + } + + _onWindowBlur () { + // this.cancel() + } + + _onKeyDown (event) { + // TODO key bindings + // will require re-working the keyboard command interpreter for macOS + // due to the cmd key bug + // e.g.: if (this.context.isCommandPressed('drawing:marquee:copy')) { + // + // HACK hardcodes key handler + if ( + // cut + (event.key === 'x' && (event.metaKey || event.ctrlKey)) || + // copy + (event.key === 'c' && (event.metaKey || event.ctrlKey)) || + // paste + (event.key === 'v' && (event.metaKey || event.ctrlKey)) + ) { + // allow command through + return + } else { + event.preventDefault() + } + + if (this.state.complete) { + if (this.context.isCommandPressed('drawing:marquee:add')) { + this.state.stateName = 'add' + // this.context.sketchPane.app.view.style.cursor = 'zoom-in' + } + if (this.context.isCommandPressed('drawing:marquee:subtract')) { + this.state.stateName = 'subtract' + // this.context.sketchPane.app.view.style.cursor = 'zoom-out' + } + } + + if (this.context.isCommandPressed('drawing:marquee:cancel')) { + this.cancel() + } + + if (this.context.isCommandPressed('drawing:marquee:erase')) { + if (this.state.complete && this.parent.marqueePath) { + let indices = this.context.visibleLayersIndices + this.context.emit('addToUndoStack', indices) + this.context.sketchPane.selectedArea.set(this.parent.marqueePath) + this.context.sketchPane.selectedArea.erase(indices) + this.context.sketchPane.selectedArea.unset() + this.context.emit('markDirty', indices) + this.deselect() + } + } + + if (this.context.isCommandPressed('drawing:marquee:fill')) { + if (this.state.complete && this.parent.marqueePath) { + // let indices = this.context.visibleLayersIndices + let indices = [this.parent.findLayerByName('fill').index] + let state = this.context.store.getState() + let color = getFillColor(state) + let alpha = getFillAlpha(state) + this.context.emit('addToUndoStack', indices) + this.context.sketchPane.selectedArea.set(this.parent.marqueePath) + this.context.sketchPane.selectedArea.fill(indices, color, alpha) + this.context.sketchPane.selectedArea.unset() + this.context.emit('markDirty', indices) + this.deselect() + } + } + } + + _onKeyUp (event) { + event.preventDefault() + + if (this.state.complete) { + if (this.state.stateName === 'add' && !this.context.isCommandPressed('drawing:marquee:add')) { + this.state.stateName = 'freeform' + this.context.sketchPane.app.view.style.cursor = 'crosshair' + } + if (this.state.stateName === 'subtract' && !this.context.isCommandPressed('drawing:marquee:subtract')) { + this.state.stateName = 'freeform' + this.context.sketchPane.app.view.style.cursor = 'crosshair' + } + } + + if (!this._isLineKeyPressed()) { + if (this.state.started && this.state.stateName == 'line' && !this.state.isPointerDown) { + this._endDrawnPath() + } + } + } + + cancel () { + // attempt to gracefully transition back to drawing + this.context.store.dispatch({ + type: 'TOOLBAR_MODE_STATUS_SET', payload: 'idle', meta: { scope: 'local' } + }) + this.context.store.dispatch({ type: 'TOOLBAR_MODE_SET', payload: 'drawing', meta: { scope: 'local' } }) + } + + deselect () { + this.context.store.dispatch({ type: 'TOOLBAR_MODE_STATUS_SET', payload: 'idle', meta: { scope: 'local' } }) + + this.layer.clear() + + this.parent.marqueePath = null + this.state.stateName = 'idle' + + this.state.selectionPath = new paper.Path() + this.state.selectionSubPath = null + + this.state.started = false + this.state.complete = false + this.state.draftPoint = null + this.state.isPointerDown = false + + this._draw() + } + + _draw () { + let ctx = this.offscreenContext + + ctx.clearRect(0, 0, this.context.sketchPane.width, this.context.sketchPane.height) + ctx.globalAlpha = 1.0 + + let pathToDraw = this._getCombinedPath() + + let children = pathToDraw.children || [pathToDraw] + + for (let n = 0; n < children.length; n++) { + let child = children[n] + + let pointsToDraw = child.segments.map(segment => ({ x: segment.point.x, y: segment.point.y })) + + // draft point added to last child + if (this.state.draftPoint != null) { + if (n === children.length - 1) { + pointsToDraw.push(this.state.draftPoint) + } + } + + if (pointsToDraw.length) { + ctx.save() + + // white + ctx.lineWidth = 1 + ctx.strokeStyle = '#fff' + ctx.setLineDash([]) + ctx.moveTo(pointsToDraw[0].x, pointsToDraw[0].y) + ctx.beginPath() + for (let i = 0; i < pointsToDraw.length; i++) { + let point = pointsToDraw[i] + ctx.lineTo(point.x, point.y) + } + ctx.closePath() + ctx.stroke() + + // purple + ctx.lineWidth = 1 + ctx.strokeStyle = '#6A4DE7' + ctx.setLineDash([2, 5]) + ctx.moveTo(pointsToDraw[0].x, pointsToDraw[0].y) + ctx.beginPath() + for (let i = 0; i < pointsToDraw.length; i++) { + let point = pointsToDraw[i] + ctx.lineTo(point.x, point.y) + } + ctx.closePath() + ctx.stroke() + + ctx.restore() + + // diagnostic circles: + // + // for (let j = 0; j < pointsToDraw.length; j++) { + // let point = pointsToDraw[j] + // ctx.beginPath() + // ctx.arc(point.x, point.y, 10, 0, Math.PI * 2) + // ctx.fillStyle = '#f00' + // ctx.fill() + // } + } + } + + this.layer.replaceTextureFromCanvas(this.offscreenCanvas) + } +} diff --git a/src/js/window/storyboarder-sketch-pane/marquee-strategy/selectors.js b/src/js/window/storyboarder-sketch-pane/marquee-strategy/selectors.js new file mode 100644 index 0000000000..c9f358c10c --- /dev/null +++ b/src/js/window/storyboarder-sketch-pane/marquee-strategy/selectors.js @@ -0,0 +1,7 @@ +const getFillColor = state => state.toolbar.tools[state.toolbar.activeTool].color +const getFillAlpha = state => state.toolbar.tools[state.toolbar.activeTool].strokeOpacity + +module.exports = { + getFillColor, + getFillAlpha +}