/** * @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.DropDownDiv'); goog.require('Blockly.FieldTextInput'); goog.require('Blockly.utils.dom'); goog.require('Blockly.utils.math'); goog.require('Blockly.utils.userAgent'); /** * Class for an editable angle field. * @param {string|number=} opt_value The initial value of the field. Should cast * to a number. Defaults to 0. * @param {Function=} opt_validator A function that is called to validate * changes to the field's value. Takes in a number & returns a * validated number, or null to abort the change. * @extends {Blockly.FieldTextInput} * @constructor */ Blockly.FieldAngle = function(opt_value, opt_validator) { opt_value = this.doClassValidation_(opt_value); if (opt_value === null) { opt_value = 0; } Blockly.FieldAngle.superClass_.constructor.call( this, opt_value, opt_validator); }; goog.inherits(Blockly.FieldAngle, Blockly.FieldTextInput); /** * Construct a FieldAngle from a JSON arg object. * @param {!Object} options A JSON object with options (angle). * @return {!Blockly.FieldAngle} The new field instance. * @package * @nocollapse */ Blockly.FieldAngle.fromJson = function(options) { return new Blockly.FieldAngle(options['angle']); }; /** * Serializable fields are saved by the XML renderer, non-serializable fields * are not. Editable fields should also be serializable. * @type {boolean} * @const */ Blockly.FieldAngle.prototype.SERIALIZABLE = true; /** * 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; /** * Create the block UI for this field. * @package */ Blockly.FieldAngle.prototype.initView = function() { Blockly.FieldAngle.superClass_.initView.call(this); // Add the degree symbol to the left of the number, even in RTL (issue #2380) this.symbol_ = Blockly.utils.dom.createSvgElement('tspan', {}, null); this.symbol_.appendChild(document.createTextNode('\u00B0')); this.textElement_.appendChild(this.symbol_); }; /** * Updates the graph when the field rerenders. * @private */ Blockly.FieldAngle.prototype.render_ = function() { Blockly.FieldAngle.superClass_.render_.call(this); this.updateGraph_(); }; /** * Create and show the angle field's editor. * @private */ Blockly.FieldAngle.prototype.showEditor_ = function() { // Mobile browsers have issues with in-line textareas (focus & keyboards). var noFocus = Blockly.utils.userAgent.MOBILE || Blockly.utils.userAgent.ANDROID || Blockly.utils.userAgent.IPAD; Blockly.FieldAngle.superClass_.showEditor_.call(this, noFocus); var editor = this.dropdownCreate_(); Blockly.DropDownDiv.getContentDiv().appendChild(editor); var border = this.sourceBlock_.getColourBorder(); border = border.colourBorder || border.colourLight; Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(), border); Blockly.DropDownDiv.showPositionedByField( this, this.dropdownDispose_.bind(this)); this.updateGraph_(); }; /** * Create the angle dropdown editor. * @return {!Element} The newly created angle picker. * @private */ Blockly.FieldAngle.prototype.dropdownCreate_ = function() { var svg = Blockly.utils.dom.createSvgElement('svg', { 'xmlns': Blockly.utils.dom.SVG_NS, 'xmlns:html': Blockly.utils.dom.HTML_NS, 'xmlns:xlink': Blockly.utils.dom.XLINK_NS, 'version': '1.1', 'height': (Blockly.FieldAngle.HALF * 2) + 'px', 'width': (Blockly.FieldAngle.HALF * 2) + 'px' }, null); var circle = Blockly.utils.dom.createSvgElement('circle', { 'cx': Blockly.FieldAngle.HALF, 'cy': Blockly.FieldAngle.HALF, 'r': Blockly.FieldAngle.RADIUS, 'class': 'blocklyAngleCircle' }, svg); this.gauge_ = Blockly.utils.dom.createSvgElement('path', { 'class': 'blocklyAngleGauge' }, svg); this.line_ = Blockly.utils.dom.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.utils.dom.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); } // The angle picker is different from other fields in that it updates on // mousemove even if it's not in the middle of a drag. In future we may // change this behaviour. For now, using bindEvent_ instead of // bindEventWithChecks_ allows it to work without a mousedown/touchstart. this.clickWrapper_ = Blockly.bindEvent_(svg, 'click', this, this.hide_); this.moveWrapper1_ = Blockly.bindEvent_(circle, 'mousemove', this, this.onMouseMove); this.moveWrapper2_ = Blockly.bindEvent_(this.gauge_, 'mousemove', this, this.onMouseMove); return svg; }; /** * Dispose of events belonging to the angle editor. * @private */ Blockly.FieldAngle.prototype.dropdownDispose_ = function() { Blockly.unbindEvent_(this.clickWrapper_); Blockly.unbindEvent_(this.moveWrapper1_); Blockly.unbindEvent_(this.moveWrapper2_); }; /** * Hide the editor. * @private */ Blockly.FieldAngle.prototype.hide_ = function() { Blockly.DropDownDiv.hideIfOwner(this); Blockly.WidgetDiv.hide(); }; /** * Set the angle to match the mouse's position. * @param {!Event} e Mouse move event. */ Blockly.FieldAngle.prototype.onMouseMove = function(e) { // Calculate angle. 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 propagate further. return; } angle = Blockly.utils.math.toDegrees(angle); // 0: East, 90: North, 180: West, 270: South. if (dx < 0) { angle += 180; } else if (dy > 0) { angle += 360; } // Do offsetting. if (Blockly.FieldAngle.CLOCKWISE) { angle = Blockly.FieldAngle.OFFSET + 360 - angle; } else { angle = 360 - (Blockly.FieldAngle.OFFSET - angle); } if (angle > 360) { angle -= 360; } // Do rounding. if (Blockly.FieldAngle.ROUND) { angle = Math.round(angle / Blockly.FieldAngle.ROUND) * Blockly.FieldAngle.ROUND; } // Do wrapping. if (angle > Blockly.FieldAngle.WRAP) { angle -= 360; } // Update value. var angleString = String(angle); if (angleString != this.text_) { this.htmlInput_.value = angle; this.setValue(angle); // Always render the input angle. this.text_ = angleString; this.forceRerender(); } }; /** * Redraw the graph with the current angle. * @private */ Blockly.FieldAngle.prototype.updateGraph_ = function() { if (!this.gauge_) { return; } // Always display the input (i.e. getText) even if it is invalid. var angleDegrees = Number(this.getText()) + Blockly.FieldAngle.OFFSET; angleDegrees %= 360; var angleRadians = Blockly.utils.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 = Blockly.utils.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 the input value is a valid angle. * @param {string|number=} newValue The input value. * @return {?number} A valid angle, or null if invalid. * @protected */ Blockly.FieldAngle.prototype.doClassValidation_ = function(newValue) { if (isNaN(newValue)) { return null; } var n = parseFloat(newValue || 0); n %= 360; if (n < 0) { n += 360; } if (n > Blockly.FieldAngle.WRAP) { n -= 360; } return n; }; Blockly.Field.register('field_angle', Blockly.FieldAngle);