From fa4223b7ce185241670f45965e7cc9757dac4ddf Mon Sep 17 00:00:00 2001 From: abakkk Date: Wed, 17 Jun 2020 18:30:57 +0200 Subject: [PATCH] Introduce mirror manipulations * Reflection (symmetry axis) * Inversion (symmetry point) `transformingElement` is locked after selecting it. --- draw.js | 132 +++++++++++++++--- extension.js | 1 + locale/draw-on-your-screen.pot | 6 + prefs.js | 1 + schemas/gschemas.compiled | Bin 4064 -> 4140 bytes ...extensions.draw-on-your-screen.gschema.xml | 5 + 6 files changed, 128 insertions(+), 17 deletions(-) diff --git a/draw.js b/draw.js index ea8cb87..d2a115d 100644 --- a/draw.js +++ b/draw.js @@ -63,11 +63,11 @@ const DASHED_LINE_ICON_PATH = ICON_DIR.get_child('dashed-line-symbolic.svg').get const FULL_LINE_ICON_PATH = ICON_DIR.get_child('full-line-symbolic.svg').get_path(); const Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6 }; -const Manipulations = { MOVE: 100, RESIZE: 101 }; +const Manipulations = { MOVE: 100, RESIZE: 101, MIRROR: 102 }; var Tools = Object.assign({}, Shapes, Manipulations); const TextStates = { WRITTEN: 0, DRAWING: 1, WRITING: 2 }; -const Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, SCALE: 3, SCALE_ANGLE: 4 }; -const ToolNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text", 5: "Polygon", 6: "Polyline", 100: "Move", 101: "Resize" }; +const Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, SCALE: 3, SCALE_ANGLE: 4, REFLECTION: 5, INVERSION: 6 }; +const ToolNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text", 5: "Polygon", 6: "Polyline", 100: "Move", 101: "Resize", 102: "Mirror" }; const LineCapNames = { 0: 'Butt', 1: 'Round', 2: 'Square' }; const LineJoinNames = { 0: 'Miter', 1: 'Round', 2: 'Bevel' }; const FillRuleNames = { 0: 'Nonzero', 1: 'Evenodd' }; @@ -422,7 +422,7 @@ var DrawingArea = new Lang.Class({ _startElementFinder: function() { this.elementFinderHandler = this.connect('motion-event', (actor, event) => { - if (this.motionHandler) { + if (this.motionHandler || this.transformingElementLocked) { this.finderPoint = null; return; } @@ -453,6 +453,16 @@ var DrawingArea = new Lang.Class({ if (!success) return; + if (this.currentTool == Manipulations.MIRROR) { + this.transformingElementLocked = !this.transformingElementLocked; + if (this.transformingElementLocked) { + this.updatePointerCursor(); + return; + } + } + + this.finderPoint = null; + this.buttonReleasedHandler = this.connect('button-release-event', (actor, event) => { this._stopTransforming(); }); @@ -468,8 +478,11 @@ var DrawingArea = new Lang.Class({ this.transformingElement.startTransformation(startX, startY, controlPressed ? Transformations.ROTATION : Transformations.TRANSLATION); else if (this.currentTool == Manipulations.RESIZE) this.transformingElement.startTransformation(startX, startY, controlPressed ? Transformations.SCALE : Transformations.SCALE_PRESERVE); + else if (this.currentTool == Manipulations.MIRROR) { + this.transformingElement.startTransformation(startX, startY, controlPressed ? Transformations.INVERSION : Transformations.REFLECTION); + this._redisplay(); + } - this.finderPoint = null; this.motionHandler = this.connect('motion-event', (actor, event) => { if (this.spaceKeyPressed) @@ -486,21 +499,29 @@ var DrawingArea = new Lang.Class({ _updateTransforming: function(x, y, controlPressed) { if (controlPressed && this.transformingElement.lastTransformation.type == Transformations.TRANSLATION) { - this.transformingElement.stopTransformation(x, y); + this.transformingElement.stopTransformation(); this.transformingElement.startTransformation(x, y, Transformations.ROTATION); } else if (!controlPressed && this.transformingElement.lastTransformation.type == Transformations.ROTATION) { - this.transformingElement.stopTransformation(x, y); + this.transformingElement.stopTransformation(); this.transformingElement.startTransformation(x, y, Transformations.TRANSLATION); } if (controlPressed && this.transformingElement.lastTransformation.type == Transformations.SCALE_PRESERVE) { - this.transformingElement.stopTransformation(x, y); + this.transformingElement.stopTransformation(); this.transformingElement.startTransformation(x, y, Transformations.SCALE); } else if (!controlPressed && this.transformingElement.lastTransformation.type == Transformations.SCALE) { - this.transformingElement.stopTransformation(x, y); + this.transformingElement.stopTransformation(); this.transformingElement.startTransformation(x, y, Transformations.SCALE_PRESERVE); } + if (controlPressed && this.transformingElement.lastTransformation.type == Transformations.REFLECTION) { + this.transformingElement.transformations.pop(); + this.transformingElement.startTransformation(x, y, Transformations.INVERSION); + } else if (!controlPressed && this.transformingElement.lastTransformation.type == Transformations.INVERSION) { + this.transformingElement.transformations.pop(); + this.transformingElement.startTransformation(x, y, Transformations.REFLECTION); + } + this.transformingElement.updateTransformation(x, y); this._redisplay(); }, @@ -517,6 +538,7 @@ var DrawingArea = new Lang.Class({ this.transformingElement.stopTransformation(); this.transformingElement = null; + this.transformingElementLocked = false; this._redisplay(); }, @@ -649,7 +671,9 @@ var DrawingArea = new Lang.Class({ }, updatePointerCursor: function(controlPressed) { - if (Object.values(Manipulations).indexOf(this.currentTool) != -1) + if (this.currentTool == Manipulations.MIRROR && this.transformingElementLocked) + this.setPointerCursor('CROSSHAIR'); + else if (Object.values(Manipulations).indexOf(this.currentTool) != -1) this.setPointerCursor(this.transformingElement ? 'MOVE_OR_RESIZE_WINDOW' : 'DEFAULT'); else if (!this.currentElement || (this.currentElement.shape == Shapes.TEXT && this.currentElement.textState == TextStates.WRITING)) this.setPointerCursor(this.currentTool == Shapes.NONE ? 'POINTING_HAND' : 'CROSSHAIR'); @@ -1070,6 +1094,9 @@ var DrawingArea = new Lang.Class({ } }); +const MIN_REFLECTION_LINE_LENGTH = 10; // px +const INVERSION_CIRCLE_RADIUS = 12; // px + // DrawingElement represents a "brushstroke". // It can be converted into a cairo path as well as a svg element. // See DrawingArea._startDrawing() to know its params. @@ -1119,6 +1146,22 @@ const DrawingElement = new Lang.Class({ }, buildCairo: function(cr, params) { + let [success, color] = Clutter.Color.from_string(this.color); + if (success) + Clutter.cairo_set_source_color(cr, color); + + if (this.showSymmetryElement) { + let transformation = this.lastTransformation; + crSetDummyStroke(cr); + if (transformation.type == Transformations.REFLECTION) { + cr.moveTo(transformation.startX, transformation.startY); + cr.lineTo(transformation.endX, transformation.endY); + } else { + cr.arc(transformation.endX, transformation.endY, INVERSION_CIRCLE_RADIUS, 0, 2 * Math.PI); + } + cr.stroke(); + } + cr.setLineCap(this.line.lineCap); cr.setLineJoin(this.line.lineJoin); cr.setLineWidth(this.line.lineWidth); @@ -1134,11 +1177,8 @@ const DrawingElement = new Lang.Class({ else cr.setOperator(Cairo.Operator.OVER); - let [success, color] = Clutter.Color.from_string(this.color); - if (success) - Clutter.cairo_set_source_color(cr, color); if (SVG_DEBUG_SUPERPOSES_CAIRO) { - Clutter.cairo_set_source_color(cr, Clutter.Color.from_string("red")[1]); + Clutter.cairo_set_source_color(cr, Clutter.Color.new(255, 0, 0, 255)); cr.setLineWidth(this.line.lineWidth / 2 || 1); } @@ -1159,6 +1199,12 @@ const DrawingElement = new Lang.Class({ crRotate(cr, transformation.angle, center[0], center[1]); crScale(cr, transformation.scale, 1, center[0], center[1]); crRotate(cr, -transformation.angle, center[0], center[1]); + } else if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) { + cr.translate(transformation.slideX, transformation.slideY); + cr.rotate(transformation.angle); + cr.scale(transformation.scaleX, transformation.scaleY); + cr.rotate(-transformation.angle); + cr.translate(-transformation.slideX, -transformation.slideY); } }); @@ -1256,7 +1302,7 @@ const DrawingElement = new Lang.Class({ attributes += ` stroke-dasharray="${this.dash.array[0]} ${this.dash.array[1]}" stroke-dashoffset="${this.dash.offset}"`; let transAttribute = ''; - // Do translations first. + // Do translations first, because it works this way. this.transformations.filter(transformation => transformation.type == Transformations.TRANSLATION) .forEach(transformation => { transAttribute += transAttribute ? ' ' : ' transform="'; @@ -1281,6 +1327,12 @@ const DrawingElement = new Lang.Class({ transAttribute += `translate(${-center[0] * (transformation.scale - 1)},0) `; transAttribute += `scale(${transformation.scale},1) `; transAttribute += `rotate(${-transformation.angle * 180 / Math.PI},${center[0]},${center[1]})`; + } else if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) { + transAttribute += `translate(${transformation.slideX}, ${transformation.slideY}) `; + transAttribute += `rotate(${transformation.angle * 180 / Math.PI}) `; + transAttribute += `scale(${transformation.scaleX}, ${transformation.scaleY}) `; + transAttribute += `rotate(${-transformation.angle * 180 / Math.PI}) `; + transAttribute += `translate(${-transformation.slideX}, ${-transformation.slideY})`; } }); transAttribute += transAttribute ? '"' : ''; @@ -1428,6 +1480,16 @@ const DrawingElement = new Lang.Class({ this.transformations.push({ startX: startX, startY: startY, type: type, scaleX: 1, scaleY: 1 }); else if (type == Transformations.SCALE_ANGLE) this.transformations.push({ startX: startX, startY: startY, type: type, scale: 1, angle: 0 }); + else if (type == Transformations.REFLECTION) + this.transformations.push({ startX: startX, startY: startY, endX: startX, endY: startY, type: type, + scaleX: 1, scaleY: 1, slideX: 0, slideY: 0, angle: 0 }); + else if (type == Transformations.INVERSION) + this.transformations.push({ startX: startX, startY: startY, endX: startX, endY: startY, type: type, + scaleX: -1, scaleY: -1, slideX: startX, slideY: startY, + angle: Math.PI + Math.atan(startY / (startX || 1)) }); + + if (type == Transformations.REFLECTION || type == Transformations.INVERSION) + this.showSymmetryElement = true; }, updateTransformation: function(x, y) { @@ -1453,19 +1515,55 @@ const DrawingElement = new Lang.Class({ let center = this._getCenterWithSlideBefore(transformation); transformation.scale = Math.hypot(x - center[0], y - center[1]) / Math.hypot(transformation.startX - center[0], transformation.startY - center[1]); transformation.angle = getAngle(center[0], center[1], center[0] + 1, center[1], x, y); + } else if (transformation.type == Transformations.REFLECTION) { + [transformation.endX, transformation.endY] = [x, y]; + if (getNearness([transformation.startX, transformation.startY], [x, y], MIN_REFLECTION_LINE_LENGTH)) { + // do nothing to avoid jumps (no transformation at starting and locked transformation after) + } else if (Math.abs(y - transformation.startY) <= 5 && Math.abs(x - transformation.startX) >= 5) { + [transformation.scaleX, transformation.scaleY] = [1, -1]; + [transformation.slideX, transformation.slideY] = [0, transformation.startY]; + transformation.angle = Math.PI; + } else if (Math.abs(x - transformation.startX) <= 5 && Math.abs(y - transformation.startY) >= 5) { + [transformation.scaleX, transformation.scaleY] = [-1, 1]; + [transformation.slideX, transformation.slideY] = [transformation.startX, 0]; + transformation.angle = Math.PI; + } else if (x != transformation.startX) { + let tan = (y - transformation.startY) / (x - transformation.startX); + [transformation.scaleX, transformation.scaleY] = [1, -1]; + [transformation.slideX, transformation.slideY] = [0, transformation.startY - transformation.startX * tan]; + transformation.angle = Math.PI + Math.atan(tan); + } else if (y != transformation.startY) { + let tan = (x - transformation.startX) / (y - transformation.startY); + [transformation.scaleX, transformation.scaleY] = [-1, 1]; + [transformation.slideX, transformation.slideY] = [transformation.startX - transformation.startY * tan, 0]; + transformation.angle = Math.PI - Math.atan(tan); + } + } else if (transformation.type == Transformations.INVERSION) { + [transformation.endX, transformation.endY] = [x, y]; + [transformation.scaleX, transformation.scaleY] = [-1, -1]; + [transformation.slideX, transformation.slideY] = [x, y]; + transformation.angle = Math.PI + Math.atan(y / (x || 1)); } }, - stopTransformation: function(x, y) { + stopTransformation: function() { // Clean transformations let transformation = this.lastTransformation; - if (transformation.type == Transformations.TRANSLATION && Math.hypot(transformation.slideX, transformation.slideY) < 1 || + if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) + this.showSymmetryElement = false; + + if (transformation.type == Transformations.REFLECTION && + getNearness([transformation.startX, transformation.startY], [transformation.endX, transformation.endY], MIN_REFLECTION_LINE_LENGTH) || + transformation.type == Transformations.TRANSLATION && Math.hypot(transformation.slideX, transformation.slideY) < 1 || transformation.type == Transformations.ROTATION && Math.abs(transformation.angle) < Math.PI / 1000) { + this.transformations.pop(); } else { delete transformation.startX; delete transformation.startY; + delete transformation.endX; + delete transformation.endY; } }, diff --git a/extension.js b/extension.js index 606636c..cd35c2e 100644 --- a/extension.js +++ b/extension.js @@ -196,6 +196,7 @@ var AreaManager = new Lang.Class({ 'select-polyline-shape': () => this.activeArea.selectTool(Draw.Tools.POLYLINE), 'select-move-tool': () => this.activeArea.selectTool(Draw.Tools.MOVE), 'select-resize-tool': () => this.activeArea.selectTool(Draw.Tools.RESIZE), + 'select-mirror-tool': () => this.activeArea.selectTool(Draw.Tools.MIRROR), 'toggle-font-family': this.activeArea.toggleFontFamily.bind(this.activeArea), 'toggle-font-weight': this.activeArea.toggleFontWeight.bind(this.activeArea), 'toggle-font-style': this.activeArea.toggleFontStyle.bind(this.activeArea), diff --git a/locale/draw-on-your-screen.pot b/locale/draw-on-your-screen.pot index c0ad135..c8d3bd4 100644 --- a/locale/draw-on-your-screen.pot +++ b/locale/draw-on-your-screen.pot @@ -71,6 +71,9 @@ msgstr "" msgid "Resize" msgstr "" +msgid "Mirror" +msgstr "" + msgid "Fill" msgstr "" @@ -191,6 +194,9 @@ msgstr "" msgid "Select resize" msgstr "" +msgid "Select mirror" +msgstr "" + msgid "Toggle fill/stroke" msgstr "" diff --git a/prefs.js b/prefs.js index 386a7dc..1ac439c 100644 --- a/prefs.js +++ b/prefs.js @@ -55,6 +55,7 @@ var INTERNAL_KEYBINDINGS = { 'select-text-shape': "Select text", 'select-move-tool': "Select move", 'select-resize-tool': "Select resize", + 'select-mirror-tool': "Select mirror", 'toggle-fill': "Toggle fill/stroke", '-separator-2': '', 'increment-line-width': "Increment line width", diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index 7a60b580426718781d8919afad21711f8ffad2a7..409cec50e7908f645d9e0283b5b710d2f03ceeb1 100644 GIT binary patch literal 4140 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 diff --git a/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml b/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml index 36bb16a..3dd963c 100644 --- a/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml +++ b/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml @@ -116,6 +116,11 @@ select resize tool select resize tool + + ["<Primary>c"] + select mirror tool + select mirror tool + KP_Add','plus']]]> increment the line width