From 1c3db256fac34b7c93c375ea354570e96ca23df4 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Thu, 19 Sep 2019 16:15:20 -0700 Subject: [PATCH] Added angle field config (#3038) * Added angle field configuration. --- core/field_angle.js | 190 ++++++++++++++++++++++---------- tests/blocks/test_blocks.js | 98 ++++++++++++++++ tests/mocha/field_angle_test.js | 130 ++++++++++++++++++++++ tests/playground.html | 9 ++ 4 files changed, 369 insertions(+), 58 deletions(-) diff --git a/core/field_angle.js b/core/field_angle.js index af5e3dc4d..f28375ef3 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -42,12 +42,49 @@ goog.require('Blockly.utils.userAgent'); * @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. + * @param {Object=} opt_config A map of options used to configure the field. + * See the [field creation documentation]{@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/angle#creation} + * for a list of properties this parameter supports. * @extends {Blockly.FieldTextInput} * @constructor */ -Blockly.FieldAngle = function(opt_value, opt_validator) { +Blockly.FieldAngle = function(opt_value, opt_validator, opt_config) { + + /** + * Should the angle increase as the angle picker is moved clockwise (true) + * or counterclockwise (false) + * @see Blockly.FieldAngle.CLOCKWISE + * @type {boolean} + * @private + */ + this.clockwise_ = Blockly.FieldAngle.CLOCKWISE; + + /** + * The offset of zero degrees (and all other angles). + * @see Blockly.FieldAngle.OFFSET + * @type {number} + * @private + */ + this.offset_ = Blockly.FieldAngle.OFFSET; + + /** + * The maximum angle to allow before wrapping. + * @see Blockly.FieldAngle.WRAP + * @type {number} + * @private + */ + this.wrap_ = Blockly.FieldAngle.WRAP; + + /** + * The amount to round angles to when using a mouse or keyboard nav input. + * @see Blockly.FieldAngle.ROUND + * @type {number} + * @private + */ + this.round_ = Blockly.FieldAngle.ROUND; + Blockly.FieldAngle.superClass_.constructor.call( - this, opt_value || 0, opt_validator); + this, opt_value || 0, opt_validator, opt_config); }; Blockly.utils.object.inherits(Blockly.FieldAngle, Blockly.FieldTextInput); @@ -59,7 +96,7 @@ Blockly.utils.object.inherits(Blockly.FieldAngle, Blockly.FieldTextInput); * @nocollapse */ Blockly.FieldAngle.fromJson = function(options) { - return new Blockly.FieldAngle(options['angle']); + return new Blockly.FieldAngle(options['angle'], null, options); }; /** @@ -71,51 +108,87 @@ Blockly.FieldAngle.fromJson = function(options) { Blockly.FieldAngle.prototype.SERIALIZABLE = true; /** - * Round angles to the nearest 15 degrees when using mouse. - * Set to 0 to disable rounding. + * The default amount to round angles to when using a mouse or keyboard nav + * input. Must be a positive integer to support keyboard navigation. + * @const {number} */ Blockly.FieldAngle.ROUND = 15; /** * Half the width of protractor image. + * @const {number} */ 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). + * Default property describing which direction makes an angle field's value + * increase. Angle increases clockwise (true) or counterclockwise (false). + * @const {boolean} */ Blockly.FieldAngle.CLOCKWISE = false; /** - * Offset the location of 0 degrees (and all angles) by a constant. + * The default offset of 0 degrees (and all angles). Always offsets in the + * counterclockwise direction, regardless of the field's clockwise property. * Usually either 0 (0 = right) or 90 (0 = up). + * @const {number} */ Blockly.FieldAngle.OFFSET = 0; /** - * Maximum allowed angle before wrapping. + * The default maximum angle to allow before wrapping. * Usually either 360 (for 0 to 359.9) or 180 (for -179.9 to 180). + * @const {number} */ Blockly.FieldAngle.WRAP = 360; /** * Radius of protractor circle. Slightly smaller than protractor size since * otherwise SVG crops off half the border at the edges. + * @const {number} */ Blockly.FieldAngle.RADIUS = Blockly.FieldAngle.HALF - 1; +/** + * Configure the field based on the given map of options. + * @param {!Object} config A map of options to configure the field based on. + * @private + */ +Blockly.FieldAngle.prototype.configure_ = function(config) { + Blockly.FieldAngle.superClass_.configure_.call(this, config); + + switch (config['mode']) { + case 'compass': + this.clockwise_ = true; + this.offset_ = 90; + break; + case 'protractor': + // This is the default mode, so we could do nothing. But just to + // future-proof, we'll set it anyway. + this.clockwise_ = false; + this.offset_ = 0; + break; + } + + // Allow individual settings to override the mode setting. + var clockwise = config['clockwise']; + if (typeof clockwise == 'boolean') { + this.clockwise_ = clockwise; + } + var offset = Number(config['offset']); + if (!isNaN(offset)) { + this.offset_ = offset; + } + var wrap = Number(config['wrap']); + if (!isNaN(wrap)) { + this.wrap_ = wrap; + } + var round = Number(config['round']); + if (!isNaN(round)) { + this.round_ = round; + } +}; + /** * Create the block UI for this field. * @package @@ -261,37 +334,27 @@ Blockly.FieldAngle.prototype.onMouseMove = function(e) { } // Do offsetting. - if (Blockly.FieldAngle.CLOCKWISE) { - angle = Blockly.FieldAngle.OFFSET + 360 - angle; + if (this.clockwise_) { + angle = this.offset_ + 360 - angle; } else { - angle = 360 - (Blockly.FieldAngle.OFFSET - angle); - } - if (angle > 360) { - angle -= 360; + angle = 360 - (this.offset_ - angle); } - this.setAngle(angle); + this.displayMouseOrKeyboardValue_(angle); }; /** - * Set the angle value and update the graph. - * @param {number} angle New angle + * Handles and displays values that are input via mouse or arrow key input. + * These values need to be rounded and wrapped before being displayed so + * that the text input's value is appropriate. + * @param {number} angle New angle. + * @private */ -Blockly.FieldAngle.prototype.setAngle = function(angle) { - // Do rounding. - if (Blockly.FieldAngle.ROUND) { - angle = Math.round(angle / Blockly.FieldAngle.ROUND) * - Blockly.FieldAngle.ROUND; +Blockly.FieldAngle.prototype.displayMouseOrKeyboardValue_ = function(angle) { + if (this.round_) { + angle = Math.round(angle / this.round_) * this.round_; } - - // Do wrapping. - if (angle > Blockly.FieldAngle.WRAP) { - angle -= 360; - } else if (angle < 0) { - angle += 360; - } - - // Update value. + angle = this.wrapValue_(angle); if (angle != this.value_) { this.setEditorValue_(angle); } @@ -306,30 +369,30 @@ Blockly.FieldAngle.prototype.updateGraph_ = function() { return; } // Always display the input (i.e. getText) even if it is invalid. - var angleDegrees = Number(this.getText()) + Blockly.FieldAngle.OFFSET; + var angleDegrees = Number(this.getText()) + this.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 clockwiseFlag = Number(this.clockwise_); + var angle1 = Blockly.utils.math.toRadians(this.offset_); var x1 = Math.cos(angle1) * Blockly.FieldAngle.RADIUS; var y1 = Math.sin(angle1) * -Blockly.FieldAngle.RADIUS; - if (Blockly.FieldAngle.CLOCKWISE) { + if (clockwiseFlag) { 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) { + if (clockwiseFlag) { 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'); + ' 0 ', largeFlag, ' ', clockwiseFlag, ' ', x2, ',', y2, ' z'); } this.gauge_.setAttribute('d', path.join('')); this.line_.setAttribute('x2', x2); @@ -360,8 +423,8 @@ Blockly.FieldAngle.prototype.onHtmlInputKeyDown_ = function(e) { multiplier = 1; } if (multiplier) { - this.setAngle(Number(this.getValue()) + - (multiplier * Blockly.FieldAngle.ROUND)); + this.displayMouseOrKeyboardValue_( + this.getValue() + (multiplier * this.round_)); e.preventDefault(); e.stopPropagation(); } @@ -375,17 +438,28 @@ Blockly.FieldAngle.prototype.onHtmlInputKeyDown_ = function(e) { * @override */ Blockly.FieldAngle.prototype.doClassValidation_ = function(opt_newValue) { - var n = Number(opt_newValue) % 360; - if (isNaN(n)) { + var value = Number(opt_newValue); + if (isNaN(value) || !isFinite(value)) { return null; } - if (n < 0) { - n += 360; + return this.wrapValue_(value); +}; + +/** + * Wraps the value so that it is in the range (-360 + wrap, wrap). + * @param {number} value The value to wrap. + * @return {number} The wrapped value. + * @private + */ +Blockly.FieldAngle.prototype.wrapValue_ = function(value) { + value %= 360; + if (value < 0) { + value += 360; } - if (n > Blockly.FieldAngle.WRAP) { - n -= 360; + if (value > this.wrap_) { + value -= 360; } - return n; + return value; }; Blockly.fieldRegistry.register('field_angle', Blockly.FieldAngle); diff --git a/tests/blocks/test_blocks.js b/tests/blocks/test_blocks.js index e84cbd6e9..b724f5479 100644 --- a/tests/blocks/test_blocks.js +++ b/tests/blocks/test_blocks.js @@ -467,6 +467,104 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT "output": "Note", "tooltip": "A midi note." }, + { + "type": "test_angles_protractor", + "message0": "protractor %1", + "args0": [ + { + "type": "field_angle", + "name": "FIELDNAME", + "angle": 0, + "mode": "protractor" + } + ], + "style": "math_blocks", + "tooltip": "test tooltip" + }, + { + "type": "test_angles_compass", + "message0": "compass %1", + "args0": [ + { + "type": "field_angle", + "name": "FIELDNAME", + "angle": 0, + "mode": "compass" + } + ], + "style": "math_blocks", + "tooltip": "test tooltip" + }, + { + "type": "test_angles_clockwise", + "message0": "clockwise %1", + "args0": [ + { + "type": "field_angle", + "name": "FIELDNAME", + "angle": 0, + "clockwise": true + } + ], + "style": "math_blocks", + "tooltip": "test tooltip" + }, + { + "type": "test_angles_offset", + "message0": "offset 90 %1", + "args0": [ + { + "type": "field_angle", + "name": "FIELDNAME", + "angle": 0, + "offset": 90 + } + ], + "style": "math_blocks", + "tooltip": "test tooltip" + }, + { + "type": "test_angles_wrap", + "message0": "wrap %1", + "args0": [ + { + "type": "field_angle", + "name": "FIELDNAME", + "angle": 0, + "wrap": 180 + } + ], + "style": "math_blocks", + "tooltip": "test tooltip" + }, + { + "type": "test_angles_round_30", + "message0": "round 30 %1", + "args0": [ + { + "type": "field_angle", + "name": "FIELDNAME", + "angle": 0, + "round": 30 + } + ], + "style": "math_blocks", + "tooltip": "test tooltip" + }, + { + "type": "test_angles_round_0", + "message0": "no round %1", + "args0": [ + { + "type": "field_angle", + "name": "FIELDNAME", + "angle": 0, + "round": 0 + } + ], + "style": "math_blocks", + "tooltip": "test tooltip" + }, { "type": "test_images_datauri", "message0": "Image data: URI %1", diff --git a/tests/mocha/field_angle_test.js b/tests/mocha/field_angle_test.js index 86236145c..5e3c04f7e 100644 --- a/tests/mocha/field_angle_test.js +++ b/tests/mocha/field_angle_test.js @@ -262,4 +262,134 @@ suite('Angle Fields', function() { }); }); }); + suite('Customizations', function() { + suite('Clockwise', function() { + test('JS Configuration', function() { + var field = new Blockly.FieldAngle(0, null, { + clockwise: true + }); + chai.assert.isTrue(field.clockwise_); + }); + test('JSON Definition', function() { + var field = Blockly.FieldAngle.fromJson({ + value: 0, + clockwise: true + }); + chai.assert.isTrue(field.clockwise_); + }); + test('Constant', function() { + // Note: Generally constants should be set at compile time, not + // runtime (since they are constants) but for testing purposes we + // can do this. + Blockly.FieldAngle.CLOCKWISE = true; + var field = new Blockly.FieldAngle(); + chai.assert.isTrue(field.clockwise_); + }); + }); + suite('Offset', function() { + test('JS Configuration', function() { + var field = new Blockly.FieldAngle(0, null, { + offset: 90 + }); + chai.assert.equal(field.offset_, 90); + }); + test('JSON Definition', function() { + var field = Blockly.FieldAngle.fromJson({ + value: 0, + offset: 90 + }); + chai.assert.equal(field.offset_, 90); + }); + test('Constant', function() { + // Note: Generally constants should be set at compile time, not + // runtime (since they are constants) but for testing purposes we + // can do this. + Blockly.FieldAngle.OFFSET = 90; + var field = new Blockly.FieldAngle(); + chai.assert.equal(field.offset_, 90); + }); + }); + suite('Wrap', function() { + test('JS Configuration', function() { + var field = new Blockly.FieldAngle(0, null, { + wrap: 180 + }); + chai.assert.equal(field.wrap_, 180); + }); + test('JSON Definition', function() { + var field = Blockly.FieldAngle.fromJson({ + value: 0, + wrap: 180 + }); + chai.assert.equal(field.wrap_, 180); + }); + test('Constant', function() { + // Note: Generally constants should be set at compile time, not + // runtime (since they are constants) but for testing purposes we + // can do this. + Blockly.FieldAngle.WRAP = 180; + var field = new Blockly.FieldAngle(); + chai.assert.equal(field.wrap_, 180); + }); + }); + suite('Round', function() { + test('JS Configuration', function() { + var field = new Blockly.FieldAngle(0, null, { + round: 30 + }); + chai.assert.equal(field.round_, 30); + }); + test('JSON Definition', function() { + var field = Blockly.FieldAngle.fromJson({ + value: 0, + round: 30 + }); + chai.assert.equal(field.round_, 30); + }); + test('Constant', function() { + // Note: Generally constants should be set at compile time, not + // runtime (since they are constants) but for testing purposes we + // can do this. + Blockly.FieldAngle.ROUND = 30; + var field = new Blockly.FieldAngle(); + chai.assert.equal(field.round_, 30); + }); + }); + suite('Mode', function() { + suite('Compass', function() { + test('JS Configuration', function() { + var field = new Blockly.FieldAngle(0, null, { + mode: 'compass' + }); + chai.assert.equal(field.offset_, 90); + chai.assert.isTrue(field.clockwise_); + }); + test('JS Configuration', function() { + var field = Blockly.FieldAngle.fromJson({ + value: 0, + mode: 'compass' + }); + chai.assert.equal(field.offset_, 90); + chai.assert.isTrue(field.clockwise_); + }); + }); + suite('Protractor', function() { + test('JS Configuration', function() { + var field = new Blockly.FieldAngle(0, null, { + mode: 'protractor' + }); + chai.assert.equal(field.offset_, 0); + chai.assert.isFalse(field.clockwise_); + }); + test('JS Configuration', function() { + var field = Blockly.FieldAngle.fromJson({ + value: 0, + mode: 'protractor' + }); + chai.assert.equal(field.offset_, 0); + chai.assert.isFalse(field.clockwise_); + }); + }); + }); + }); }); diff --git a/tests/playground.html b/tests/playground.html index 04835dba7..ac6b31b99 100644 --- a/tests/playground.html +++ b/tests/playground.html @@ -1464,6 +1464,15 @@ var spaghettiXml = [ 60 + + + + + + + + +