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
+
+
+
+
+
+
+
+
+