diff options
Diffstat (limited to 'src/blockly/core/field_angle.js')
-rw-r--r-- | src/blockly/core/field_angle.js | 294 |
1 files changed, 294 insertions, 0 deletions
diff --git a/src/blockly/core/field_angle.js b/src/blockly/core/field_angle.js new file mode 100644 index 0000000..a294948 --- /dev/null +++ b/src/blockly/core/field_angle.js @@ -0,0 +1,294 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2013 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Angle input field. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.FieldAngle'); + +goog.require('Blockly.FieldTextInput'); +goog.require('goog.math'); +goog.require('goog.userAgent'); + + +/** + * Class for an editable angle field. + * @param {string} text The initial content of the field. + * @param {Function=} opt_validator An optional function that is called + * to validate any constraints on what the user entered. Takes the new + * text as an argument and returns the accepted text or null to abort + * the change. + * @extends {Blockly.FieldTextInput} + * @constructor + */ +Blockly.FieldAngle = function(text, opt_validator) { + // Add degree symbol: "360°" (LTR) or "°360" (RTL) + this.symbol_ = Blockly.createSvgElement('tspan', {}, null); + this.symbol_.appendChild(document.createTextNode('\u00B0')); + + Blockly.FieldAngle.superClass_.constructor.call(this, text, opt_validator); +}; +goog.inherits(Blockly.FieldAngle, Blockly.FieldTextInput); + +/** + * Round angles to the nearest 15 degrees when using mouse. + * Set to 0 to disable rounding. + */ +Blockly.FieldAngle.ROUND = 15; + +/** + * Half the width of protractor image. + */ +Blockly.FieldAngle.HALF = 100 / 2; + +/* The following two settings work together to set the behaviour of the angle + * picker. While many combinations are possible, two modes are typical: + * Math mode. + * 0 deg is right, 90 is up. This is the style used by protractors. + * Blockly.FieldAngle.CLOCKWISE = false; + * Blockly.FieldAngle.OFFSET = 0; + * Compass mode. + * 0 deg is up, 90 is right. This is the style used by maps. + * Blockly.FieldAngle.CLOCKWISE = true; + * Blockly.FieldAngle.OFFSET = 90; + */ + +/** + * Angle increases clockwise (true) or counterclockwise (false). + */ +Blockly.FieldAngle.CLOCKWISE = false; + +/** + * Offset the location of 0 degrees (and all angles) by a constant. + * Usually either 0 (0 = right) or 90 (0 = up). + */ +Blockly.FieldAngle.OFFSET = 0; + +/** + * Maximum allowed angle before wrapping. + * Usually either 360 (for 0 to 359.9) or 180 (for -179.9 to 180). + */ +Blockly.FieldAngle.WRAP = 360; + + +/** + * Radius of protractor circle. Slightly smaller than protractor size since + * otherwise SVG crops off half the border at the edges. + */ +Blockly.FieldAngle.RADIUS = Blockly.FieldAngle.HALF - 1; + +/** + * Clean up this FieldAngle, as well as the inherited FieldTextInput. + * @return {!Function} Closure to call on destruction of the WidgetDiv. + * @private + */ +Blockly.FieldAngle.prototype.dispose_ = function() { + var thisField = this; + return function() { + Blockly.FieldAngle.superClass_.dispose_.call(thisField)(); + thisField.gauge_ = null; + if (thisField.clickWrapper_) { + Blockly.unbindEvent_(thisField.clickWrapper_); + } + if (thisField.moveWrapper1_) { + Blockly.unbindEvent_(thisField.moveWrapper1_); + } + if (thisField.moveWrapper2_) { + Blockly.unbindEvent_(thisField.moveWrapper2_); + } + }; +}; + +/** + * Show the inline free-text editor on top of the text. + * @private + */ +Blockly.FieldAngle.prototype.showEditor_ = function() { + var noFocus = + goog.userAgent.MOBILE || goog.userAgent.ANDROID || goog.userAgent.IPAD; + // Mobile browsers have issues with in-line textareas (focus & keyboards). + Blockly.FieldAngle.superClass_.showEditor_.call(this, noFocus); + var div = Blockly.WidgetDiv.DIV; + if (!div.firstChild) { + // Mobile interface uses window.prompt. + return; + } + // Build the SVG DOM. + var svg = Blockly.createSvgElement('svg', { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlns:html': 'http://www.w3.org/1999/xhtml', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + 'version': '1.1', + 'height': (Blockly.FieldAngle.HALF * 2) + 'px', + 'width': (Blockly.FieldAngle.HALF * 2) + 'px' + }, div); + var circle = Blockly.createSvgElement('circle', { + 'cx': Blockly.FieldAngle.HALF, 'cy': Blockly.FieldAngle.HALF, + 'r': Blockly.FieldAngle.RADIUS, + 'class': 'blocklyAngleCircle' + }, svg); + this.gauge_ = Blockly.createSvgElement('path', + {'class': 'blocklyAngleGauge'}, svg); + this.line_ = Blockly.createSvgElement('line', + {'x1': Blockly.FieldAngle.HALF, + 'y1': Blockly.FieldAngle.HALF, + 'class': 'blocklyAngleLine'}, svg); + // Draw markers around the edge. + for (var angle = 0; angle < 360; angle += 15) { + Blockly.createSvgElement('line', { + 'x1': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS, + 'y1': Blockly.FieldAngle.HALF, + 'x2': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS - + (angle % 45 == 0 ? 10 : 5), + 'y2': Blockly.FieldAngle.HALF, + 'class': 'blocklyAngleMarks', + 'transform': 'rotate(' + angle + ',' + + Blockly.FieldAngle.HALF + ',' + Blockly.FieldAngle.HALF + ')' + }, svg); + } + svg.style.marginLeft = (15 - Blockly.FieldAngle.RADIUS) + 'px'; + this.clickWrapper_ = + Blockly.bindEvent_(svg, 'click', this, Blockly.WidgetDiv.hide); + this.moveWrapper1_ = + Blockly.bindEvent_(circle, 'mousemove', this, this.onMouseMove); + this.moveWrapper2_ = + Blockly.bindEvent_(this.gauge_, 'mousemove', this, this.onMouseMove); + this.updateGraph_(); +}; + +/** + * Set the angle to match the mouse's position. + * @param {!Event} e Mouse move event. + */ +Blockly.FieldAngle.prototype.onMouseMove = function(e) { + var bBox = this.gauge_.ownerSVGElement.getBoundingClientRect(); + var dx = e.clientX - bBox.left - Blockly.FieldAngle.HALF; + var dy = e.clientY - bBox.top - Blockly.FieldAngle.HALF; + var angle = Math.atan(-dy / dx); + if (isNaN(angle)) { + // This shouldn't happen, but let's not let this error propogate further. + return; + } + angle = goog.math.toDegrees(angle); + // 0: East, 90: North, 180: West, 270: South. + if (dx < 0) { + angle += 180; + } else if (dy > 0) { + angle += 360; + } + if (Blockly.FieldAngle.CLOCKWISE) { + angle = Blockly.FieldAngle.OFFSET + 360 - angle; + } else { + angle -= Blockly.FieldAngle.OFFSET; + } + if (Blockly.FieldAngle.ROUND) { + angle = Math.round(angle / Blockly.FieldAngle.ROUND) * + Blockly.FieldAngle.ROUND; + } + angle = this.callValidator(angle); + Blockly.FieldTextInput.htmlInput_.value = angle; + this.setValue(angle); + this.validate_(); + this.resizeEditor_(); +}; + +/** + * Insert a degree symbol. + * @param {?string} text New text. + */ +Blockly.FieldAngle.prototype.setText = function(text) { + Blockly.FieldAngle.superClass_.setText.call(this, text); + if (!this.textElement_) { + // Not rendered yet. + return; + } + this.updateGraph_(); + // Insert degree symbol. + if (this.sourceBlock_.RTL) { + this.textElement_.insertBefore(this.symbol_, this.textElement_.firstChild); + } else { + this.textElement_.appendChild(this.symbol_); + } + // Cached width is obsolete. Clear it. + this.size_.width = 0; +}; + +/** + * Redraw the graph with the current angle. + * @private + */ +Blockly.FieldAngle.prototype.updateGraph_ = function() { + if (!this.gauge_) { + return; + } + var angleDegrees = Number(this.getText()) + Blockly.FieldAngle.OFFSET; + var angleRadians = goog.math.toRadians(angleDegrees); + var path = ['M ', Blockly.FieldAngle.HALF, ',', Blockly.FieldAngle.HALF]; + var x2 = Blockly.FieldAngle.HALF; + var y2 = Blockly.FieldAngle.HALF; + if (!isNaN(angleRadians)) { + var angle1 = goog.math.toRadians(Blockly.FieldAngle.OFFSET); + var x1 = Math.cos(angle1) * Blockly.FieldAngle.RADIUS; + var y1 = Math.sin(angle1) * -Blockly.FieldAngle.RADIUS; + if (Blockly.FieldAngle.CLOCKWISE) { + angleRadians = 2 * angle1 - angleRadians; + } + x2 += Math.cos(angleRadians) * Blockly.FieldAngle.RADIUS; + y2 -= Math.sin(angleRadians) * Blockly.FieldAngle.RADIUS; + // Don't ask how the flag calculations work. They just do. + var largeFlag = Math.abs(Math.floor((angleRadians - angle1) / Math.PI) % 2); + if (Blockly.FieldAngle.CLOCKWISE) { + largeFlag = 1 - largeFlag; + } + var sweepFlag = Number(Blockly.FieldAngle.CLOCKWISE); + path.push(' l ', x1, ',', y1, + ' A ', Blockly.FieldAngle.RADIUS, ',', Blockly.FieldAngle.RADIUS, + ' 0 ', largeFlag, ' ', sweepFlag, ' ', x2, ',', y2, ' z'); + } + this.gauge_.setAttribute('d', path.join('')); + this.line_.setAttribute('x2', x2); + this.line_.setAttribute('y2', y2); +}; + +/** + * Ensure that only an angle may be entered. + * @param {string} text The user's text. + * @return {?string} A string representing a valid angle, or null if invalid. + */ +Blockly.FieldAngle.prototype.classValidator = function(text) { + if (text === null) { + return null; + } + var n = parseFloat(text || 0); + if (isNaN(n)) { + return null; + } + n = n % 360; + if (n < 0) { + n += 360; + } + if (n > Blockly.FieldAngle.WRAP) { + n -= 360; + } + return String(n); +}; |