view/paper-ext/Paper.SelectionGUI.js

  1. /*
  2. * Copyright 2020 WICKLETS LLC
  3. *
  4. * This file is part of Wick Engine.
  5. *
  6. * Wick Engine is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * Wick Engine is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with Wick Engine. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. /**
  20. * This is a utility class for creating a selection box GUI. this will give you:
  21. * - a bounding box
  22. * - handles for scaling
  23. * - hotspots for rotating
  24. * - ...and much more!
  25. */
  26. paper.SelectionGUI = class {
  27. static get BOX_STROKE_WIDTH () {
  28. return 1;
  29. }
  30. static get BOX_STROKE_COLOR () {
  31. return 'rgba(100,150,255,1.0)';
  32. }
  33. static get HANDLE_RADIUS () {
  34. return 5;
  35. }
  36. static get HANDLE_STROKE_WIDTH () {
  37. return paper.SelectionGUI.BOX_STROKE_WIDTH;
  38. }
  39. static get HANDLE_STROKE_COLOR () {
  40. return paper.SelectionGUI.BOX_STROKE_COLOR;
  41. }
  42. static get HANDLE_FILL_COLOR () {
  43. return 'rgba(255,255,255,0.3)';
  44. }
  45. static get PIVOT_STROKE_WIDTH () {
  46. return paper.SelectionGUI.BOX_STROKE_WIDTH;
  47. }
  48. static get PIVOT_FILL_COLOR () {
  49. return 'rgba(255,255,255,0.5)';
  50. }
  51. static get PIVOT_STROKE_COLOR () {
  52. return 'rgba(0,0,0,1)';
  53. }
  54. static get PIVOT_RADIUS () {
  55. return paper.SelectionGUI.HANDLE_RADIUS;
  56. }
  57. static get ROTATION_HOTSPOT_RADIUS () {
  58. return 20;
  59. }
  60. static get ROTATION_HOTSPOT_FILLCOLOR () {
  61. return 'rgba(100,150,255,0.5)';
  62. // don't show rotation hotspots:
  63. //return 'rgba(255,0,0,0.0001)';
  64. }
  65. /**
  66. * Create a selection GUI.
  67. * @param {paper.Item[]} items - (required) the items to create a GUI around.
  68. */
  69. constructor (args) {
  70. if(!args) args = {};
  71. if(!args.items) args.items = [];
  72. if(!args.rotation) args.rotation = 0;
  73. if(!args.originX) args.originX = 0;
  74. if(!args.originY) args.originY = 0;
  75. if(!args.layer) args.layer = paper.project.activeLayer;
  76. this.items = args.items;
  77. this.rotation = args.rotation;
  78. this.originX = args.originX;
  79. this.originY = args.originY;
  80. this.matrix = new paper.Matrix();
  81. this.bounds = this._getBoundsOfItems(this.items);
  82. this.matrix.translate(this.bounds.center.x, this.bounds.center.y);
  83. this.matrix.rotate(this.rotation);
  84. this.matrix.translate(new paper.Point(0,0).subtract(new paper.Point(this.bounds.center.x, this.bounds.center.y)));
  85. this.bounds = this._getBoundsOfItems(this.items);
  86. this.item = new paper.Group({
  87. applyMatrix: true,
  88. });
  89. args.layer.addChild(this.item);
  90. this.item.addChild(this._createBorder());
  91. if(this.items.length > 1) {
  92. this.item.addChildren(this._createItemOutlines());
  93. }
  94. this.item.addChild(this._createRotationHotspot('topLeft'));
  95. this.item.addChild(this._createRotationHotspot('topRight'));
  96. this.item.addChild(this._createRotationHotspot('bottomLeft'));
  97. this.item.addChild(this._createRotationHotspot('bottomRight'));
  98. this.item.addChild(this._createScalingHandle('topLeft'));
  99. this.item.addChild(this._createScalingHandle('topRight'));
  100. this.item.addChild(this._createScalingHandle('bottomLeft'));
  101. this.item.addChild(this._createScalingHandle('bottomRight'));
  102. this.item.addChild(this._createScalingHandle('topCenter'));
  103. this.item.addChild(this._createScalingHandle('bottomCenter'));
  104. this.item.addChild(this._createScalingHandle('leftCenter'));
  105. this.item.addChild(this._createScalingHandle('rightCenter'));
  106. this.item.addChild(this._createOriginPointHandle());
  107. // Set a flag just so we don't accidentily treat these GUI elements as actual paths...
  108. this.item.children.forEach(child => {
  109. child.data.isSelectionBoxGUI = true;
  110. });
  111. this.item.transform(this.matrix);
  112. }
  113. /**
  114. * Destroy the GUI.
  115. */
  116. destroy () {
  117. this.item.remove();
  118. }
  119. /**
  120. * Move a handle and use the new handle position to scale the selection.
  121. * @param {string} handleName - the name of the handle to move
  122. * @param {paper.Point} position - the position to move the handle to
  123. */
  124. moveHandleAndScale (handleName, position) {
  125. }
  126. /**
  127. * Move a handle and use the new position of the handle to rotate the selection.
  128. * @param {string} handleName - the name of the handle to move
  129. * @param {paper.Point} position - the position to move the handle to
  130. */
  131. moveHandleAndRotate (handleName, position) {
  132. }
  133. _createBorder () {
  134. var border = new paper.Path.Rectangle({
  135. name: 'border',
  136. from: this.bounds.topLeft,
  137. to: this.bounds.bottomRight,
  138. strokeWidth: paper.SelectionGUI.BOX_STROKE_WIDTH,
  139. strokeColor: paper.SelectionGUI.BOX_STROKE_COLOR,
  140. insert: false,
  141. });
  142. border.data.isBorder = true;
  143. return border;
  144. }
  145. _createItemOutlines () {
  146. return [];//TODO replace
  147. return this.items.map(item => {
  148. var outline = new paper.Path.Rectangle(item.bounds);
  149. outline.fillColor = 'rgba(0,0,0,0)';
  150. outline.strokeColor = paper.SelectionGUI.BOX_STROKE_COLOR;
  151. outline.strokeWidth = paper.SelectionGUI.BOX_STROKE_WIDTH;
  152. outline.data.isBorder = true;
  153. return outline;
  154. });
  155. }
  156. _createScalingHandle (edge) {
  157. return this._createHandle({
  158. name: edge,
  159. type: 'scale',
  160. center: this.bounds[edge],
  161. fillColor: paper.SelectionGUI.HANDLE_FILL_COLOR,
  162. strokeColor: paper.SelectionGUI.HANDLE_STROKE_COLOR,
  163. });
  164. }
  165. _createOriginPointHandle () {
  166. return this._createHandle({
  167. name: 'pivot',
  168. type: 'pivot',
  169. center: new paper.Point(this.originX, this.originY),
  170. fillColor: paper.SelectionGUI.PIVOT_FILL_COLOR,
  171. strokeColor: paper.SelectionGUI.PIVOT_STROKE_COLOR,
  172. });
  173. }
  174. _createHandle (args) {
  175. if(!args) console.error('_createHandle: args is required');
  176. if(!args.name) console.error('_createHandle: args.name is required');
  177. if(!args.type) console.error('_createHandle: args.type is required');
  178. if(!args.center) console.error('_createHandle: args.center is required');
  179. if(!args.fillColor) console.error('_createHandle: args.fillColor is required');
  180. if(!args.strokeColor) console.error('_createHandle: args.strokeColor is required');
  181. var circle = new paper.Path.Circle({
  182. center: args.center,
  183. radius: paper.SelectionGUI.HANDLE_RADIUS / paper.view.zoom,
  184. strokeWidth: paper.SelectionGUI.HANDLE_STROKE_WIDTH / paper.view.zoom,
  185. strokeColor: args.strokeColor,
  186. fillColor: args.fillColor,
  187. insert: false,
  188. });
  189. // Transform the handle a bit so it doesn't get squished when the selection box is scaled.
  190. circle.applyMatrix = false;
  191. circle.data.handleType = args.type;
  192. circle.data.handleEdge = args.name;
  193. return circle;
  194. }
  195. _createRotationHotspot (cornerName) {
  196. // Build the not-yet-rotated hotspot, which starts out like this:
  197. // |
  198. // +---+
  199. // | |
  200. // ---+--+ |---
  201. // | |
  202. // +------+
  203. // |
  204. var r = paper.SelectionGUI.ROTATION_HOTSPOT_RADIUS / paper.view.zoom;
  205. var hotspot = new paper.Path([
  206. new paper.Point(0,0),
  207. new paper.Point(0, r),
  208. new paper.Point(r, r),
  209. new paper.Point(r, -r),
  210. new paper.Point(-r, -r),
  211. new paper.Point(-r, 0),
  212. ]);
  213. hotspot.fillColor = paper.SelectionGUI.ROTATION_HOTSPOT_FILLCOLOR;
  214. hotspot.position.x = this.bounds[cornerName].x;
  215. hotspot.position.y = this.bounds[cornerName].y;
  216. // Orient the rotation handles in the correct direction, even if the selection is flipped
  217. hotspot.rotate({
  218. 'topRight': 0,
  219. 'bottomRight': 90,
  220. 'bottomLeft': 180,
  221. 'topLeft': 270,
  222. }[cornerName]);
  223. // Some metadata.
  224. hotspot.data.handleType = 'rotation';
  225. hotspot.data.handleEdge = cornerName;
  226. return hotspot;
  227. }
  228. /* helper function: calculate the bounds of the smallest rectangle that contains all given items. */
  229. _getBoundsOfItems () {
  230. if(this.items.length === 0)
  231. return new paper.Rectangle();
  232. var itemsForBoundsCalc = this.items.map(item => {
  233. var clone = item.clone();
  234. clone.transform(this.matrix);
  235. clone.remove();
  236. return clone;
  237. });
  238. var bounds = null;
  239. itemsForBoundsCalc.forEach(item => {
  240. bounds = bounds ? bounds.unite(item.bounds) : item.bounds;
  241. });
  242. return bounds;
  243. }
  244. }
  245. paper.PaperScope.inject({
  246. SelectionGUI: paper.SelectionGUI,
  247. });