field_angle.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2013 Google Inc.
  6. * https://developers.google.com/blockly/
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. /**
  21. * @fileoverview Angle input field.
  22. * @author fraser@google.com (Neil Fraser)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.FieldAngle');
  26. goog.require('Blockly.FieldTextInput');
  27. goog.require('goog.math');
  28. goog.require('goog.userAgent');
  29. /**
  30. * Class for an editable angle field.
  31. * @param {string} text The initial content of the field.
  32. * @param {Function=} opt_validator An optional function that is called
  33. * to validate any constraints on what the user entered. Takes the new
  34. * text as an argument and returns the accepted text or null to abort
  35. * the change.
  36. * @extends {Blockly.FieldTextInput}
  37. * @constructor
  38. */
  39. Blockly.FieldAngle = function(text, opt_validator) {
  40. // Add degree symbol: "360°" (LTR) or "°360" (RTL)
  41. this.symbol_ = Blockly.createSvgElement('tspan', {}, null);
  42. this.symbol_.appendChild(document.createTextNode('\u00B0'));
  43. Blockly.FieldAngle.superClass_.constructor.call(this, text, opt_validator);
  44. };
  45. goog.inherits(Blockly.FieldAngle, Blockly.FieldTextInput);
  46. /**
  47. * Round angles to the nearest 15 degrees when using mouse.
  48. * Set to 0 to disable rounding.
  49. */
  50. Blockly.FieldAngle.ROUND = 15;
  51. /**
  52. * Half the width of protractor image.
  53. */
  54. Blockly.FieldAngle.HALF = 100 / 2;
  55. /* The following two settings work together to set the behaviour of the angle
  56. * picker. While many combinations are possible, two modes are typical:
  57. * Math mode.
  58. * 0 deg is right, 90 is up. This is the style used by protractors.
  59. * Blockly.FieldAngle.CLOCKWISE = false;
  60. * Blockly.FieldAngle.OFFSET = 0;
  61. * Compass mode.
  62. * 0 deg is up, 90 is right. This is the style used by maps.
  63. * Blockly.FieldAngle.CLOCKWISE = true;
  64. * Blockly.FieldAngle.OFFSET = 90;
  65. */
  66. /**
  67. * Angle increases clockwise (true) or counterclockwise (false).
  68. */
  69. Blockly.FieldAngle.CLOCKWISE = false;
  70. /**
  71. * Offset the location of 0 degrees (and all angles) by a constant.
  72. * Usually either 0 (0 = right) or 90 (0 = up).
  73. */
  74. Blockly.FieldAngle.OFFSET = 0;
  75. /**
  76. * Maximum allowed angle before wrapping.
  77. * Usually either 360 (for 0 to 359.9) or 180 (for -179.9 to 180).
  78. */
  79. Blockly.FieldAngle.WRAP = 360;
  80. /**
  81. * Radius of protractor circle. Slightly smaller than protractor size since
  82. * otherwise SVG crops off half the border at the edges.
  83. */
  84. Blockly.FieldAngle.RADIUS = Blockly.FieldAngle.HALF - 1;
  85. /**
  86. * Clean up this FieldAngle, as well as the inherited FieldTextInput.
  87. * @return {!Function} Closure to call on destruction of the WidgetDiv.
  88. * @private
  89. */
  90. Blockly.FieldAngle.prototype.dispose_ = function() {
  91. var thisField = this;
  92. return function() {
  93. Blockly.FieldAngle.superClass_.dispose_.call(thisField)();
  94. thisField.gauge_ = null;
  95. if (thisField.clickWrapper_) {
  96. Blockly.unbindEvent_(thisField.clickWrapper_);
  97. }
  98. if (thisField.moveWrapper1_) {
  99. Blockly.unbindEvent_(thisField.moveWrapper1_);
  100. }
  101. if (thisField.moveWrapper2_) {
  102. Blockly.unbindEvent_(thisField.moveWrapper2_);
  103. }
  104. };
  105. };
  106. /**
  107. * Show the inline free-text editor on top of the text.
  108. * @private
  109. */
  110. Blockly.FieldAngle.prototype.showEditor_ = function() {
  111. var noFocus =
  112. goog.userAgent.MOBILE || goog.userAgent.ANDROID || goog.userAgent.IPAD;
  113. // Mobile browsers have issues with in-line textareas (focus & keyboards).
  114. Blockly.FieldAngle.superClass_.showEditor_.call(this, noFocus);
  115. var div = Blockly.WidgetDiv.DIV;
  116. if (!div.firstChild) {
  117. // Mobile interface uses Blockly.prompt.
  118. return;
  119. }
  120. // Build the SVG DOM.
  121. var svg = Blockly.createSvgElement('svg', {
  122. 'xmlns': 'http://www.w3.org/2000/svg',
  123. 'xmlns:html': 'http://www.w3.org/1999/xhtml',
  124. 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
  125. 'version': '1.1',
  126. 'height': (Blockly.FieldAngle.HALF * 2) + 'px',
  127. 'width': (Blockly.FieldAngle.HALF * 2) + 'px'
  128. }, div);
  129. var circle = Blockly.createSvgElement('circle', {
  130. 'cx': Blockly.FieldAngle.HALF, 'cy': Blockly.FieldAngle.HALF,
  131. 'r': Blockly.FieldAngle.RADIUS,
  132. 'class': 'blocklyAngleCircle'
  133. }, svg);
  134. this.gauge_ = Blockly.createSvgElement('path',
  135. {'class': 'blocklyAngleGauge'}, svg);
  136. this.line_ = Blockly.createSvgElement('line',
  137. {'x1': Blockly.FieldAngle.HALF,
  138. 'y1': Blockly.FieldAngle.HALF,
  139. 'class': 'blocklyAngleLine'}, svg);
  140. // Draw markers around the edge.
  141. for (var angle = 0; angle < 360; angle += 15) {
  142. Blockly.createSvgElement('line', {
  143. 'x1': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS,
  144. 'y1': Blockly.FieldAngle.HALF,
  145. 'x2': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS -
  146. (angle % 45 == 0 ? 10 : 5),
  147. 'y2': Blockly.FieldAngle.HALF,
  148. 'class': 'blocklyAngleMarks',
  149. 'transform': 'rotate(' + angle + ',' +
  150. Blockly.FieldAngle.HALF + ',' + Blockly.FieldAngle.HALF + ')'
  151. }, svg);
  152. }
  153. svg.style.marginLeft = (15 - Blockly.FieldAngle.RADIUS) + 'px';
  154. // The angle picker is different from other fields in that it updates on
  155. // mousemove even if it's not in the middle of a drag. In future we may
  156. // change this behavior. For now, using bindEvent_ instead of
  157. // bindEventWithChecks_ allows it to work without a mousedown/touchstart.
  158. this.clickWrapper_ =
  159. Blockly.bindEvent_(svg, 'click', this, Blockly.WidgetDiv.hide);
  160. this.moveWrapper1_ =
  161. Blockly.bindEvent_(circle, 'mousemove', this, this.onMouseMove);
  162. this.moveWrapper2_ =
  163. Blockly.bindEvent_(this.gauge_, 'mousemove', this,
  164. this.onMouseMove);
  165. this.updateGraph_();
  166. };
  167. /**
  168. * Set the angle to match the mouse's position.
  169. * @param {!Event} e Mouse move event.
  170. */
  171. Blockly.FieldAngle.prototype.onMouseMove = function(e) {
  172. var bBox = this.gauge_.ownerSVGElement.getBoundingClientRect();
  173. var dx = e.clientX - bBox.left - Blockly.FieldAngle.HALF;
  174. var dy = e.clientY - bBox.top - Blockly.FieldAngle.HALF;
  175. var angle = Math.atan(-dy / dx);
  176. if (isNaN(angle)) {
  177. // This shouldn't happen, but let's not let this error propogate further.
  178. return;
  179. }
  180. angle = goog.math.toDegrees(angle);
  181. // 0: East, 90: North, 180: West, 270: South.
  182. if (dx < 0) {
  183. angle += 180;
  184. } else if (dy > 0) {
  185. angle += 360;
  186. }
  187. if (Blockly.FieldAngle.CLOCKWISE) {
  188. angle = Blockly.FieldAngle.OFFSET + 360 - angle;
  189. } else {
  190. angle -= Blockly.FieldAngle.OFFSET;
  191. }
  192. if (Blockly.FieldAngle.ROUND) {
  193. angle = Math.round(angle / Blockly.FieldAngle.ROUND) *
  194. Blockly.FieldAngle.ROUND;
  195. }
  196. angle = this.callValidator(angle);
  197. Blockly.FieldTextInput.htmlInput_.value = angle;
  198. this.setValue(angle);
  199. this.validate_();
  200. this.resizeEditor_();
  201. };
  202. /**
  203. * Insert a degree symbol.
  204. * @param {?string} text New text.
  205. */
  206. Blockly.FieldAngle.prototype.setText = function(text) {
  207. Blockly.FieldAngle.superClass_.setText.call(this, text);
  208. if (!this.textElement_) {
  209. // Not rendered yet.
  210. return;
  211. }
  212. this.updateGraph_();
  213. // Insert degree symbol.
  214. if (this.sourceBlock_.RTL) {
  215. this.textElement_.insertBefore(this.symbol_, this.textElement_.firstChild);
  216. } else {
  217. this.textElement_.appendChild(this.symbol_);
  218. }
  219. // Cached width is obsolete. Clear it.
  220. this.size_.width = 0;
  221. };
  222. /**
  223. * Redraw the graph with the current angle.
  224. * @private
  225. */
  226. Blockly.FieldAngle.prototype.updateGraph_ = function() {
  227. if (!this.gauge_) {
  228. return;
  229. }
  230. var angleDegrees = Number(this.getText()) + Blockly.FieldAngle.OFFSET;
  231. var angleRadians = goog.math.toRadians(angleDegrees);
  232. var path = ['M ', Blockly.FieldAngle.HALF, ',', Blockly.FieldAngle.HALF];
  233. var x2 = Blockly.FieldAngle.HALF;
  234. var y2 = Blockly.FieldAngle.HALF;
  235. if (!isNaN(angleRadians)) {
  236. var angle1 = goog.math.toRadians(Blockly.FieldAngle.OFFSET);
  237. var x1 = Math.cos(angle1) * Blockly.FieldAngle.RADIUS;
  238. var y1 = Math.sin(angle1) * -Blockly.FieldAngle.RADIUS;
  239. if (Blockly.FieldAngle.CLOCKWISE) {
  240. angleRadians = 2 * angle1 - angleRadians;
  241. }
  242. x2 += Math.cos(angleRadians) * Blockly.FieldAngle.RADIUS;
  243. y2 -= Math.sin(angleRadians) * Blockly.FieldAngle.RADIUS;
  244. // Don't ask how the flag calculations work. They just do.
  245. var largeFlag = Math.abs(Math.floor((angleRadians - angle1) / Math.PI) % 2);
  246. if (Blockly.FieldAngle.CLOCKWISE) {
  247. largeFlag = 1 - largeFlag;
  248. }
  249. var sweepFlag = Number(Blockly.FieldAngle.CLOCKWISE);
  250. path.push(' l ', x1, ',', y1,
  251. ' A ', Blockly.FieldAngle.RADIUS, ',', Blockly.FieldAngle.RADIUS,
  252. ' 0 ', largeFlag, ' ', sweepFlag, ' ', x2, ',', y2, ' z');
  253. }
  254. this.gauge_.setAttribute('d', path.join(''));
  255. this.line_.setAttribute('x2', x2);
  256. this.line_.setAttribute('y2', y2);
  257. };
  258. /**
  259. * Ensure that only an angle may be entered.
  260. * @param {string} text The user's text.
  261. * @return {?string} A string representing a valid angle, or null if invalid.
  262. */
  263. Blockly.FieldAngle.prototype.classValidator = function(text) {
  264. if (text === null) {
  265. return null;
  266. }
  267. var n = parseFloat(text || 0);
  268. if (isNaN(n)) {
  269. return null;
  270. }
  271. n = n % 360;
  272. if (n < 0) {
  273. n += 360;
  274. }
  275. if (n > Blockly.FieldAngle.WRAP) {
  276. n -= 360;
  277. }
  278. return String(n);
  279. };