Initial commit of 001code-html Scratch frontend project.
Includes scratch-gui, scratch-vm, scratch-blocks, scratch-render, scratch-l10n, and deployment config. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
398
scratch-blocks/core/field_angle.js
Normal file
398
scratch-blocks/core/field_angle.js
Normal file
@@ -0,0 +1,398 @@
|
||||
/**
|
||||
* @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('goog.math');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
/**
|
||||
* Class for an editable angle field.
|
||||
* @param {(string|number)=} opt_value The initial content of the field. The
|
||||
* value should cast to a number, and if it does not, '0' will be used.
|
||||
* @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(opt_value, opt_validator) {
|
||||
// Add degree symbol: '360°' (LTR) or '°360' (RTL)
|
||||
this.symbol_ = Blockly.utils.createSvgElement('tspan', {}, null);
|
||||
this.symbol_.appendChild(document.createTextNode('\u00B0'));
|
||||
|
||||
var numRestrictor = new RegExp("[\\d]|[\\.]|[-]|[eE]");
|
||||
|
||||
opt_value = (opt_value && !isNaN(opt_value)) ? String(opt_value) : '0';
|
||||
Blockly.FieldAngle.superClass_.constructor.call(
|
||||
this, opt_value, opt_validator, numRestrictor);
|
||||
this.addArgType('angle');
|
||||
};
|
||||
goog.inherits(Blockly.FieldAngle, Blockly.FieldTextInput);
|
||||
|
||||
/**
|
||||
* Construct a FieldAngle from a JSON arg object.
|
||||
* @param {!Object} options A JSON object with options (angle).
|
||||
* @returns {!Blockly.FieldAngle} The new field instance.
|
||||
* @package
|
||||
* @nocollapse
|
||||
*/
|
||||
Blockly.FieldAngle.fromJson = function(options) {
|
||||
return new Blockly.FieldAngle(options['angle']);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = 120 / 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 = true;
|
||||
|
||||
/**
|
||||
* Offset the location of 0 degrees (and all angles) by a constant.
|
||||
* Usually either 0 (0 = right) or 90 (0 = up).
|
||||
*/
|
||||
Blockly.FieldAngle.OFFSET = 90;
|
||||
|
||||
/**
|
||||
* Maximum allowed angle before wrapping.
|
||||
* Usually either 360 (for 0 to 359.9) or 180 (for -179.9 to 180).
|
||||
*/
|
||||
Blockly.FieldAngle.WRAP = 180;
|
||||
|
||||
/**
|
||||
* Radius of drag handle
|
||||
*/
|
||||
Blockly.FieldAngle.HANDLE_RADIUS = 10;
|
||||
|
||||
/**
|
||||
* Width of drag handle arrow
|
||||
*/
|
||||
Blockly.FieldAngle.ARROW_WIDTH = Blockly.FieldAngle.HANDLE_RADIUS;
|
||||
|
||||
/**
|
||||
* Half the stroke-width used for the "glow" around the drag handle, rounded up to nearest whole pixel
|
||||
*/
|
||||
|
||||
Blockly.FieldAngle.HANDLE_GLOW_WIDTH = 3;
|
||||
|
||||
/**
|
||||
* 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
|
||||
- Blockly.FieldAngle.HANDLE_RADIUS - Blockly.FieldAngle.HANDLE_GLOW_WIDTH;
|
||||
|
||||
/**
|
||||
* Radius of central dot circle.
|
||||
*/
|
||||
Blockly.FieldAngle.CENTER_RADIUS = 2;
|
||||
|
||||
/**
|
||||
* Path to the arrow svg icon.
|
||||
*/
|
||||
Blockly.FieldAngle.ARROW_SVG_PATH = 'icons/arrow.svg';
|
||||
|
||||
/**
|
||||
* 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.mouseDownWrapper_) {
|
||||
Blockly.unbindEvent_(thisField.mouseDownWrapper_);
|
||||
}
|
||||
if (thisField.mouseUpWrapper_) {
|
||||
Blockly.unbindEvent_(thisField.mouseUpWrapper_);
|
||||
}
|
||||
if (thisField.mouseMoveWrapper_) {
|
||||
Blockly.unbindEvent_(thisField.mouseMoveWrapper_);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the inline free-text editor on top of the text.
|
||||
* @private
|
||||
*/
|
||||
Blockly.FieldAngle.prototype.showEditor_ = function() {
|
||||
// Mobile browsers have issues with in-line textareas (focus & keyboards).
|
||||
Blockly.FieldAngle.superClass_.showEditor_.call(this, this.useTouchInteraction_);
|
||||
// If there is an existing drop-down someone else owns, hide it immediately and clear it.
|
||||
Blockly.DropDownDiv.hideWithoutAnimation();
|
||||
Blockly.DropDownDiv.clearContent();
|
||||
var div = Blockly.DropDownDiv.getContentDiv();
|
||||
// Build the SVG DOM.
|
||||
var svg = Blockly.utils.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);
|
||||
Blockly.utils.createSvgElement('circle', {
|
||||
'cx': Blockly.FieldAngle.HALF, 'cy': Blockly.FieldAngle.HALF,
|
||||
'r': Blockly.FieldAngle.RADIUS,
|
||||
'class': 'blocklyAngleCircle'
|
||||
}, svg);
|
||||
this.gauge_ = Blockly.utils.createSvgElement('path',
|
||||
{'class': 'blocklyAngleGauge'}, svg);
|
||||
// The moving line, x2 and y2 are set in updateGraph_
|
||||
this.line_ = Blockly.utils.createSvgElement('line',{
|
||||
'x1': Blockly.FieldAngle.HALF,
|
||||
'y1': Blockly.FieldAngle.HALF,
|
||||
'class': 'blocklyAngleLine'
|
||||
}, svg);
|
||||
// The fixed vertical line at the offset
|
||||
var offsetRadians = Math.PI * Blockly.FieldAngle.OFFSET / 180;
|
||||
Blockly.utils.createSvgElement('line', {
|
||||
'x1': Blockly.FieldAngle.HALF,
|
||||
'y1': Blockly.FieldAngle.HALF,
|
||||
'x2': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS * Math.cos(offsetRadians),
|
||||
'y2': Blockly.FieldAngle.HALF - Blockly.FieldAngle.RADIUS * Math.sin(offsetRadians),
|
||||
'class': 'blocklyAngleLine'
|
||||
}, svg);
|
||||
// Draw markers around the edge.
|
||||
for (var angle = 0; angle < 360; angle += 15) {
|
||||
Blockly.utils.createSvgElement('line', {
|
||||
'x1': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS - 13,
|
||||
'y1': Blockly.FieldAngle.HALF,
|
||||
'x2': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS - 7,
|
||||
'y2': Blockly.FieldAngle.HALF,
|
||||
'class': 'blocklyAngleMarks',
|
||||
'transform': 'rotate(' + angle + ',' +
|
||||
Blockly.FieldAngle.HALF + ',' + Blockly.FieldAngle.HALF + ')'
|
||||
}, svg);
|
||||
}
|
||||
// Center point
|
||||
Blockly.utils.createSvgElement('circle', {
|
||||
'cx': Blockly.FieldAngle.HALF, 'cy': Blockly.FieldAngle.HALF,
|
||||
'r': Blockly.FieldAngle.CENTER_RADIUS,
|
||||
'class': 'blocklyAngleCenterPoint'
|
||||
}, svg);
|
||||
// Handle group: a circle and the arrow image
|
||||
this.handle_ = Blockly.utils.createSvgElement('g', {}, svg);
|
||||
Blockly.utils.createSvgElement('circle', {
|
||||
'cx': 0,
|
||||
'cy': 0,
|
||||
'r': Blockly.FieldAngle.HANDLE_RADIUS,
|
||||
'class': 'blocklyAngleDragHandle'
|
||||
}, this.handle_);
|
||||
this.arrowSvg_ = Blockly.utils.createSvgElement('image',
|
||||
{
|
||||
'width': Blockly.FieldAngle.ARROW_WIDTH,
|
||||
'height': Blockly.FieldAngle.ARROW_WIDTH,
|
||||
'x': -Blockly.FieldAngle.ARROW_WIDTH / 2,
|
||||
'y': -Blockly.FieldAngle.ARROW_WIDTH / 2,
|
||||
'class': 'blocklyAngleDragArrow'
|
||||
},
|
||||
this.handle_);
|
||||
this.arrowSvg_.setAttributeNS(
|
||||
'http://www.w3.org/1999/xlink',
|
||||
'xlink:href',
|
||||
Blockly.mainWorkspace.options.pathToMedia + Blockly.FieldAngle.ARROW_SVG_PATH
|
||||
);
|
||||
|
||||
Blockly.DropDownDiv.setColour(this.sourceBlock_.parentBlock_.getColour(),
|
||||
this.sourceBlock_.getColourTertiary());
|
||||
Blockly.DropDownDiv.setCategory(this.sourceBlock_.parentBlock_.getCategory());
|
||||
Blockly.DropDownDiv.showPositionedByBlock(this, this.sourceBlock_);
|
||||
|
||||
this.mouseDownWrapper_ =
|
||||
Blockly.bindEvent_(this.handle_, 'mousedown', this, this.onMouseDown);
|
||||
|
||||
this.updateGraph_();
|
||||
};
|
||||
/**
|
||||
* Set the angle to match the mouse's position.
|
||||
* @param {!Event} e Mouse move event.
|
||||
*/
|
||||
Blockly.FieldAngle.prototype.onMouseDown = function() {
|
||||
this.mouseMoveWrapper_ = Blockly.bindEvent_(document.body, 'mousemove', this, this.onMouseMove);
|
||||
this.mouseUpWrapper_ = Blockly.bindEvent_(document.body, 'mouseup', this, this.onMouseUp);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the angle to match the mouse's position.
|
||||
* @param {!Event} e Mouse move event.
|
||||
*/
|
||||
Blockly.FieldAngle.prototype.onMouseUp = function() {
|
||||
Blockly.unbindEvent_(this.mouseMoveWrapper_);
|
||||
Blockly.unbindEvent_(this.mouseUpWrapper_);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the angle to match the mouse's position.
|
||||
* @param {!Event} e Mouse move event.
|
||||
*/
|
||||
Blockly.FieldAngle.prototype.onMouseMove = function(e) {
|
||||
e.preventDefault();
|
||||
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 = 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_();
|
||||
// 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()) % 360 + 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;
|
||||
// Use large arc only if input value is greater than wrap
|
||||
var largeFlag = Math.abs(angleDegrees - Blockly.FieldAngle.OFFSET) > 180 ? 1 : 0;
|
||||
var sweepFlag = Number(Blockly.FieldAngle.CLOCKWISE);
|
||||
if (angleDegrees < Blockly.FieldAngle.OFFSET) {
|
||||
sweepFlag = 1 - sweepFlag; // Sweep opposite direction if less than the offset
|
||||
}
|
||||
path.push(' l ', x1, ',', y1,
|
||||
' A ', Blockly.FieldAngle.RADIUS, ',', Blockly.FieldAngle.RADIUS,
|
||||
' 0 ', largeFlag, ' ', sweepFlag, ' ', x2, ',', y2, ' z');
|
||||
|
||||
// Image rotation needs to be set in degrees
|
||||
if (Blockly.FieldAngle.CLOCKWISE) {
|
||||
var imageRotation = angleDegrees + 2 * Blockly.FieldAngle.OFFSET;
|
||||
} else {
|
||||
var imageRotation = -angleDegrees;
|
||||
}
|
||||
this.arrowSvg_.setAttribute('transform', 'rotate(' + (imageRotation) + ')');
|
||||
}
|
||||
this.gauge_.setAttribute('d', path.join(''));
|
||||
this.line_.setAttribute('x2', x2);
|
||||
this.line_.setAttribute('y2', y2);
|
||||
this.handle_.setAttribute('transform', 'translate(' + x2 + ',' + 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);
|
||||
};
|
||||
|
||||
Blockly.Field.register('field_angle', Blockly.FieldAngle);
|
||||
Reference in New Issue
Block a user