scratch-blocks/core/block_render_svg_horizontal.js
2018-06-21 10:03:37 -04:00

890 lines
28 KiB
JavaScript

/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Methods for graphically rendering a block as SVG.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.BlockSvg.render');
goog.require('Blockly.BlockSvg');
// UI constants for rendering blocks.
/**
* Grid unit to pixels conversion
* @const
*/
Blockly.BlockSvg.GRID_UNIT = 4;
/**
* Horizontal space between elements.
* @const
*/
Blockly.BlockSvg.SEP_SPACE_X = 3 * Blockly.BlockSvg.GRID_UNIT;
/**
* Vertical space between elements.
* @const
*/
Blockly.BlockSvg.SEP_SPACE_Y = 3 * Blockly.BlockSvg.GRID_UNIT;
/**
* Vertical space above blocks with statements.
* @const
*/
Blockly.BlockSvg.STATEMENT_BLOCK_SPACE = 3 * Blockly.BlockSvg.GRID_UNIT;
/**
* Height of user inputs
* @const
*/
Blockly.BlockSvg.FIELD_HEIGHT = 8 * Blockly.BlockSvg.GRID_UNIT;
/**
* Width of user inputs
* @const
*/
Blockly.BlockSvg.FIELD_WIDTH = 12 * Blockly.BlockSvg.GRID_UNIT;
/**
* Editable field padding (left/right of the text).
* @const
*/
Blockly.BlockSvg.EDITABLE_FIELD_PADDING = 0;
/**
* Minimum width of user inputs during editing
* @const
*/
Blockly.BlockSvg.FIELD_WIDTH_MIN_EDIT = 13 * Blockly.BlockSvg.GRID_UNIT;
/**
* Maximum width of user inputs during editing
* @const
*/
Blockly.BlockSvg.FIELD_WIDTH_MAX_EDIT = 24 * Blockly.BlockSvg.GRID_UNIT;
/**
* Maximum height of user inputs during editing
* @const
*/
Blockly.BlockSvg.FIELD_HEIGHT_MAX_EDIT = 10 * Blockly.BlockSvg.GRID_UNIT;
/**
* Top padding of user inputs
* @const
*/
Blockly.BlockSvg.FIELD_TOP_PADDING = 0.25 * Blockly.BlockSvg.GRID_UNIT;
/**
* Corner radius of number inputs
* @const
*/
Blockly.BlockSvg.NUMBER_FIELD_CORNER_RADIUS = 4 * Blockly.BlockSvg.GRID_UNIT;
/**
* Corner radius of text inputs
* @const
*/
Blockly.BlockSvg.TEXT_FIELD_CORNER_RADIUS = 1 * Blockly.BlockSvg.GRID_UNIT;
/**
* Default radius for a field, in px.
* @const
*/
Blockly.BlockSvg.FIELD_DEFAULT_CORNER_RADIUS = 4 * Blockly.BlockSvg.GRID_UNIT;
/**
* Minimum width of a block.
* @const
*/
Blockly.BlockSvg.MIN_BLOCK_X = 1 / 2 * 16 * Blockly.BlockSvg.GRID_UNIT;
/**
* Minimum height of a block.
* @const
*/
Blockly.BlockSvg.MIN_BLOCK_Y = 16 * Blockly.BlockSvg.GRID_UNIT;
/**
* Width of horizontal puzzle tab.
* @const
*/
Blockly.BlockSvg.TAB_WIDTH = 2 * Blockly.BlockSvg.GRID_UNIT;
/**
* Rounded corner radius.
* @const
*/
Blockly.BlockSvg.CORNER_RADIUS = 1 * Blockly.BlockSvg.GRID_UNIT;
/**
* Rounded corner radius.
* @const
*/
Blockly.BlockSvg.HAT_CORNER_RADIUS = 8 * Blockly.BlockSvg.GRID_UNIT;
/**
* Full height of connector notch including rounded corner.
* @const
*/
Blockly.BlockSvg.NOTCH_HEIGHT = 8 * Blockly.BlockSvg.GRID_UNIT + 2;
/**
* Width of connector notch
* @const
*/
Blockly.BlockSvg.NOTCH_WIDTH = 2 * Blockly.BlockSvg.GRID_UNIT;
/**
* SVG path for drawing next/previous notch from top to bottom.
* Drawn in pixel units since Bezier control points are off the grid.
* @const
*/
Blockly.BlockSvg.NOTCH_PATH_DOWN =
'c 0,2 1,3 2,4 ' +
'l 4,4 ' +
'c 1,1 2,2 2,4 ' +
'v 12 ' +
'c 0,2 -1,3 -2,4 ' +
'l -4,4 ' +
'c -1,1 -2,2 -2,4';
/**
* SVG path for drawing next/previous notch from bottom to top.
* Drawn in pixel units since Bezier control points are off the grid.
* @const
*/
Blockly.BlockSvg.NOTCH_PATH_UP =
'c 0,-2 1,-3 2,-4 ' +
'l 4,-4 ' +
'c 1,-1 2,-2 2,-4 ' +
'v -12 ' +
'c 0,-2 -1,-3 -2,-4 ' +
'l -4,-4 ' +
'c -1,-1 -2,-2 -2,-4';
/**
* Width of rendered image field in px
* @const
*/
Blockly.BlockSvg.IMAGE_FIELD_WIDTH = 10 * Blockly.BlockSvg.GRID_UNIT;
/**
* Height of rendered image field in px
* @const
*/
Blockly.BlockSvg.IMAGE_FIELD_HEIGHT = 10 * Blockly.BlockSvg.GRID_UNIT;
/**
* y-offset of the top of the field shadow block from the bottom of the block.
* @const
*/
Blockly.BlockSvg.FIELD_Y_OFFSET = -2 * Blockly.BlockSvg.GRID_UNIT;
/**
* SVG start point for drawing the top-left corner.
* @const
*/
Blockly.BlockSvg.TOP_LEFT_CORNER_START =
'm ' + Blockly.BlockSvg.CORNER_RADIUS + ',0';
/**
* SVG path for drawing the rounded top-left corner.
* @const
*/
Blockly.BlockSvg.TOP_LEFT_CORNER =
'A ' + Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' +
'0,' + Blockly.BlockSvg.CORNER_RADIUS;
/**
* SVG start point for drawing the top-left corner.
* @const
*/
Blockly.BlockSvg.HAT_TOP_LEFT_CORNER_START =
'm ' + Blockly.BlockSvg.HAT_CORNER_RADIUS + ',0';
/**
* SVG path for drawing the rounded top-left corner.
* @const
*/
Blockly.BlockSvg.HAT_TOP_LEFT_CORNER =
'A ' + Blockly.BlockSvg.HAT_CORNER_RADIUS + ',' +
Blockly.BlockSvg.HAT_CORNER_RADIUS + ' 0 0,0 ' +
'0,' + Blockly.BlockSvg.HAT_CORNER_RADIUS;
/**
* @type {Object} An object containing computed measurements of this block.
* @private
*/
Blockly.BlockSvg.renderingMetrics_ = null;
/**
* Max text display length for a field (per-horizontal/vertical)
* @const
*/
Blockly.BlockSvg.MAX_DISPLAY_LENGTH = 4;
/**
* Point size of text field before animation. Must match size in CSS.
* See implementation in field_textinput.
*/
Blockly.BlockSvg.FIELD_TEXTINPUT_FONTSIZE_INITIAL = 12;
/**
* Point size of text field after animation.
* See implementation in field_textinput.
*/
Blockly.BlockSvg.FIELD_TEXTINPUT_FONTSIZE_FINAL = 14;
/**
* Whether text fields are allowed to expand past their truncated block size.
* @const{boolean}
*/
Blockly.BlockSvg.FIELD_TEXTINPUT_EXPAND_PAST_TRUNCATION = true;
/**
* Whether text fields should animate their positioning.
* @const{boolean}
*/
Blockly.BlockSvg.FIELD_TEXTINPUT_ANIMATE_POSITIONING = true;
/**
* @param {!Object} first An object containing computed measurements of a
* block.
* @param {!Object} second Another object containing computed measurements of a
* block.
* @return {boolean} Whether the two sets of metrics are equivalent.
* @private
*/
Blockly.BlockSvg.metricsAreEquivalent_ = function(first, second) {
if (first.statement != second.statement) {
return false;
}
if (first.imageField != second.imageField) {
return false;
}
if ((first.height != second.height) ||
(first.width != second.width) ||
(first.bayHeight != second.bayHeight) ||
(first.bayWidth != second.bayWidth) ||
(first.fieldRadius != second.fieldRadius) ||
(first.startHat != second.startHat)) {
return false;
}
return true;
};
/**
* Play some UI effects (sound) after a connection has been established.
*/
Blockly.BlockSvg.prototype.connectionUiEffect = function() {
this.workspace.getAudioManager().play('click');
};
/**
* Change the colour of a block.
*/
Blockly.BlockSvg.prototype.updateColour = function() {
var fillColour = (this.isGlowing_) ? this.getColourSecondary() : this.getColour();
var strokeColour = this.getColourTertiary();
// Render block stroke
this.svgPath_.setAttribute('stroke', strokeColour);
// Render block fill
var fillColour = (this.isGlowingBlock_) ? this.getColourSecondary() : this.getColour();
this.svgPath_.setAttribute('fill', fillColour);
// Render opacity
this.svgPath_.setAttribute('fill-opacity', this.getOpacity());
// Bump every dropdown to change its colour.
for (var x = 0, input; input = this.inputList[x]; x++) {
for (var y = 0, field; field = input.fieldRow[y]; y++) {
field.setText(null);
}
}
};
/**
* Visual effect to show that if the dragging block is dropped, this block will
* be replaced. If a shadow block it will disappear. Otherwise it will bump.
* @param {boolean} add True if highlighting should be added.
*/
Blockly.BlockSvg.prototype.highlightForReplacement = function(add) {
if (add) {
var replacementGlowFilterId = this.workspace.options.replacementGlowFilterId
|| 'blocklyReplacementGlowFilter';
this.svgPath_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')');
Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyReplaceable');
} else {
this.svgPath_.removeAttribute('filter');
Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyReplaceable');
}
};
/**
* Returns a bounding box describing the dimensions of this block
* and any blocks stacked below it.
* @param {boolean=} opt_ignoreFields True if we should ignore fields in the
* size calculation, and just give the size of the base block(s).
* @return {!{height: number, width: number}} Object with height and width properties.
*/
Blockly.BlockSvg.prototype.getHeightWidth = function(opt_ignoreFields) {
var height = this.height;
var width = this.width;
// Add the size of the field shadow block.
if (!opt_ignoreFields && this.getFieldShadowBlock_()) {
height += Blockly.BlockSvg.FIELD_Y_OFFSET;
height += Blockly.BlockSvg.FIELD_HEIGHT;
}
// Recursively add size of subsequent blocks.
var nextBlock = this.getNextBlock();
if (nextBlock) {
var nextHeightWidth = nextBlock.getHeightWidth(opt_ignoreFields);
width += nextHeightWidth.width;
width -= Blockly.BlockSvg.NOTCH_WIDTH; // Exclude width of connected notch.
height = Math.max(height, nextHeightWidth.height);
}
return {height: height, width: width};
};
/**
* Render the block.
* Lays out and reflows a block based on its contents and settings.
* @param {boolean=} opt_bubble If false, just render this block.
* If true, also render block's parent, grandparent, etc. Defaults to true.
*/
Blockly.BlockSvg.prototype.render = function(opt_bubble) {
Blockly.Field.startCache();
this.rendered = true;
var oldMetrics = this.renderingMetrics_;
var metrics = this.renderCompute_();
// Don't redraw if we don't need to.
if (oldMetrics &&
Blockly.BlockSvg.metricsAreEquivalent_(oldMetrics, metrics)) {
// Skipping the redraw is fine, but we may still have to tighten up our
// connections with child blocks.
if (metrics.statement && metrics.statement.connection &&
metrics.statement.targetConnection) {
metrics.statement.connection.tighten_();
}
if (this.nextConnection && this.nextConnection.targetConnection) {
this.nextConnection.tighten_();
}
} else {
this.height = metrics.height;
this.width = metrics.width;
this.renderDraw_(metrics);
this.renderClassify_(metrics);
this.renderingMetrics_ = metrics;
}
if (opt_bubble !== false) {
// Render all blocks above this one (propagate a reflow).
var parentBlock = this.getParent();
if (parentBlock) {
parentBlock.render(true);
} else {
// Top-most block. Fire an event to allow scrollbars to resize.
Blockly.resizeSvgContents(this.workspace);
}
}
Blockly.Field.stopCache();
};
/**
* Computes the height and widths for each row and field.
* @return {!Array.<!Array.<!Object>>} 2D array of objects, each containing
* position information.
* @private
*/
Blockly.BlockSvg.prototype.renderCompute_ = function() {
var metrics = {
statement: null,
imageField: null,
iconMenu: null,
width: 0,
height: 0,
bayHeight: 0,
bayWidth: 0,
bayNotchAtRight: true,
fieldRadius: 0,
startHat: false,
endCap: false
};
// Does block have a statement?
for (var i = 0, input; input = this.inputList[i]; i++) {
if (input.type == Blockly.NEXT_STATEMENT) {
metrics.statement = input;
// Compute minimum input size.
metrics.bayHeight = Blockly.BlockSvg.MIN_BLOCK_Y;
metrics.bayWidth = Blockly.BlockSvg.MIN_BLOCK_X;
// Expand input size if there is a connection.
if (input.connection && input.connection.targetConnection) {
var linkedBlock = input.connection.targetBlock();
var bBox = linkedBlock.getHeightWidth(true);
metrics.bayHeight = Math.max(metrics.bayHeight, bBox.height);
metrics.bayWidth = Math.max(metrics.bayWidth, bBox.width);
}
var linkedBlock = input.connection.targetBlock();
if (linkedBlock && !linkedBlock.lastConnectionInStack()) {
metrics.bayNotchAtRight = false;
} else {
metrics.bayWidth -= Blockly.BlockSvg.NOTCH_WIDTH;
}
}
// Find image field, input fields
for (var j = 0, field; field = input.fieldRow[j]; j++) {
if (field instanceof Blockly.FieldImage) {
metrics.imageField = field;
}
if (field instanceof Blockly.FieldIconMenu) {
metrics.iconMenu = field;
}
if (field instanceof Blockly.FieldTextInput) {
metrics.fieldRadius = field.getBorderRadius();
} else {
metrics.fieldRadius = Blockly.BlockSvg.FIELD_DEFAULT_CORNER_RADIUS;
}
}
}
// Determine whether a block is a start hat or end cap by checking connections.
if (this.nextConnection && !this.previousConnection) {
metrics.startHat = true;
}
// End caps have no bay, a previous, no output, and no next.
if (!this.nextConnection && this.previousConnection &&
!this.outputConnection && !metrics.statement) {
metrics.endCap = true;
}
// If this block is an icon menu shadow, attempt to set the parent's
// ImageField src to the one that represents the current value of the field.
if (metrics.iconMenu) {
var currentSrc = metrics.iconMenu.getSrcForValue(metrics.iconMenu.getValue());
if (currentSrc) {
metrics.iconMenu.setParentFieldImage(currentSrc);
}
}
// Always render image field at 40x40 px
// Normal block sizing
metrics.width = Blockly.BlockSvg.SEP_SPACE_X * 2 + Blockly.BlockSvg.IMAGE_FIELD_WIDTH;
metrics.height = Blockly.BlockSvg.SEP_SPACE_Y * 2 + Blockly.BlockSvg.IMAGE_FIELD_HEIGHT;
if (this.outputConnection) {
// Field shadow block
metrics.height = Blockly.BlockSvg.FIELD_HEIGHT;
metrics.width = Blockly.BlockSvg.FIELD_WIDTH;
}
if (metrics.statement) {
// Block with statement (e.g., repeat, forever)
metrics.width += metrics.bayWidth + 4 * Blockly.BlockSvg.CORNER_RADIUS + 2 * Blockly.BlockSvg.GRID_UNIT;
metrics.height = metrics.bayHeight + Blockly.BlockSvg.STATEMENT_BLOCK_SPACE;
}
if (metrics.startHat) {
// Start hats are 1 unit wider to account for optical effect of curve.
metrics.width += 1 * Blockly.BlockSvg.GRID_UNIT;
}
if (metrics.endCap) {
// End caps are 1 unit wider to account for optical effect of no notch.
metrics.width += 1 * Blockly.BlockSvg.GRID_UNIT;
}
return metrics;
};
/**
* Draw the path of the block.
* Move the fields to the correct locations.
* @param {!Object} metrics An object containing computed measurements of the
* block.
* @private
*/
Blockly.BlockSvg.prototype.renderDraw_ = function(metrics) {
// Fetch the block's coordinates on the surface for use in anchoring
// the connections.
var connectionsXY = this.getRelativeToSurfaceXY();
// Assemble the block's path.
var steps = [];
this.renderDrawLeft_(steps, connectionsXY, metrics);
this.renderDrawBottom_(steps, connectionsXY, metrics);
this.renderDrawRight_(steps, connectionsXY, metrics);
this.renderDrawTop_(steps, connectionsXY, metrics);
var pathString = steps.join(' ');
this.svgPath_.setAttribute('d', pathString);
if (this.RTL) {
// Mirror the block's path.
// This is awesome.
this.svgPath_.setAttribute('transform', 'scale(-1 1)');
}
// Horizontal blocks have a single Image Field that is specially positioned
if (metrics.imageField) {
var imageField = metrics.imageField.getSvgRoot();
var imageFieldSize = metrics.imageField.getSize();
// Image field's position is calculated relative to the "end" edge of the
// block.
var imageFieldX = metrics.width - imageFieldSize.width -
Blockly.BlockSvg.SEP_SPACE_X / 1.5;
var imageFieldY = metrics.height - imageFieldSize.height -
Blockly.BlockSvg.SEP_SPACE_Y;
if (metrics.endCap) {
// End-cap image is offset by a grid unit to account for optical effect of no notch.
imageFieldX -= Blockly.BlockSvg.GRID_UNIT;
}
var imageFieldScale = "scale(1 1)";
if (this.RTL) {
// Do we want to mirror the Image Field left-to-right?
if (metrics.imageField.getFlipRTL()) {
imageFieldScale = "scale(-1 1)";
imageFieldX = -metrics.width + imageFieldSize.width +
Blockly.BlockSvg.SEP_SPACE_X / 1.5;
} else {
// If not, don't offset by imageFieldSize.width
imageFieldX = -metrics.width + Blockly.BlockSvg.SEP_SPACE_X / 1.5;
}
}
if (imageField) {
// Fields are invisible on insertion marker.
if (this.isInsertionMarker()) {
imageField.setAttribute('display', 'none');
}
imageField.setAttribute('transform',
'translate(' + imageFieldX + ',' + imageFieldY + ') ' +
imageFieldScale);
}
}
// Position value input
if (this.getFieldShadowBlock_()) {
var input = this.getFieldShadowBlock_().getSvgRoot();
var valueX = (Blockly.BlockSvg.NOTCH_WIDTH +
(metrics.bayWidth ? 2 * Blockly.BlockSvg.GRID_UNIT +
Blockly.BlockSvg.NOTCH_WIDTH * 2 : 0) + metrics.bayWidth);
if (metrics.startHat) {
// Start hats add some left margin to field for visual balance
valueX += Blockly.BlockSvg.GRID_UNIT * 2;
}
if (this.RTL) {
valueX = -valueX;
}
var valueY = (metrics.height + Blockly.BlockSvg.FIELD_Y_OFFSET);
var transformation = 'translate(' + valueX + ',' + valueY + ')';
input.setAttribute('transform', transformation);
}
};
/**
* Give the block an attribute 'data-shapes' that lists its shape[s], and an
* attribute 'data-category' with its category.
* @param {!Object} metrics An object containing computed measurements of the
* block.
* @private
*/
Blockly.BlockSvg.prototype.renderClassify_ = function(metrics) {
var shapes = [];
if (this.isShadow_) {
shapes.push('argument');
} else {
if(metrics.statement) {
shapes.push('c-block');
}
if (metrics.startHat) {
shapes.push('hat'); // c-block+hats are possible (e.x. reprter procedures)
} else if (!metrics.statement) {
shapes.push('stack'); //only call it "stack" if it's not a c-block
}
if (!this.nextConnection) {
shapes.push('end');
}
}
this.svgGroup_.setAttribute('data-shapes', shapes.join(' '));
if (this.getCategory()) {
this.svgGroup_.setAttribute('data-category', this.getCategory());
}
};
/**
* Render the left edge of the block.
* @param {!Array.<string>} steps Path of block outline.
* @param {!Object} connectionsXY Location of block.
* @param {!Object} metrics An object containing computed measurements of the
* block.
* @private
*/
Blockly.BlockSvg.prototype.renderDrawLeft_ = function(steps, connectionsXY, metrics) {
// Top edge.
if (metrics.startHat) {
// Hat block
// Position the cursor at the top-left starting point.
steps.push(Blockly.BlockSvg.HAT_TOP_LEFT_CORNER_START);
// Top-left rounded corner.
steps.push(Blockly.BlockSvg.HAT_TOP_LEFT_CORNER);
} else if (this.previousConnection) {
// Regular block
// Position the cursor at the top-left starting point.
steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_START);
// Top-left rounded corner.
steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER);
var cursorY = metrics.height - Blockly.BlockSvg.CORNER_RADIUS -
Blockly.BlockSvg.SEP_SPACE_Y - Blockly.BlockSvg.NOTCH_HEIGHT;
steps.push('V', cursorY);
steps.push(Blockly.BlockSvg.NOTCH_PATH_DOWN);
// Create previous block connection.
var connectionX = connectionsXY.x;
var connectionY = connectionsXY.y + metrics.height -
Blockly.BlockSvg.CORNER_RADIUS * 2;
this.previousConnection.moveTo(connectionX, connectionY);
// This connection will be tightened when the parent renders.
steps.push('V', metrics.height - Blockly.BlockSvg.CORNER_RADIUS);
} else {
// Input
// Position the cursor at the top-left starting point.
steps.push('m', metrics.fieldRadius + ',0');
// Top-left rounded corner.
steps.push(
'A', metrics.fieldRadius + ',' + metrics.fieldRadius,
'0', '0,0', '0,' + metrics.fieldRadius);
steps.push(
'V', metrics.height - metrics.fieldRadius);
}
};
/**
* Render the bottom edge of the block.
* @param {!Array.<string>} steps Path of block outline.
* @param {!Object} connectionsXY Location of block.
* @param {!Object} metrics An object containing computed measurements of the
* block.
* @private
*/
Blockly.BlockSvg.prototype.renderDrawBottom_ = function(steps,
connectionsXY, metrics) {
if (metrics.startHat) {
steps.push('a', Blockly.BlockSvg.HAT_CORNER_RADIUS + ',' +
Blockly.BlockSvg.HAT_CORNER_RADIUS + ' 0 0,0 ' +
Blockly.BlockSvg.HAT_CORNER_RADIUS + ',' +
Blockly.BlockSvg.HAT_CORNER_RADIUS);
} else if (this.previousConnection) {
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS);
} else {
// Input
steps.push(
'a', metrics.fieldRadius + ',' + metrics.fieldRadius,
'0', '0,0', metrics.fieldRadius + ',' + metrics.fieldRadius);
}
// Has statement
if (metrics.statement) {
steps.push('h', 4 * Blockly.BlockSvg.GRID_UNIT);
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',-' +
Blockly.BlockSvg.CORNER_RADIUS);
steps.push('v', -2.5 * Blockly.BlockSvg.GRID_UNIT);
steps.push(Blockly.BlockSvg.NOTCH_PATH_UP);
// @todo Why 3?
steps.push('v', -metrics.bayHeight + (Blockly.BlockSvg.CORNER_RADIUS * 3) +
Blockly.BlockSvg.NOTCH_HEIGHT + 2 * Blockly.BlockSvg.GRID_UNIT);
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',-' +
Blockly.BlockSvg.CORNER_RADIUS);
steps.push('h', metrics.bayWidth - (Blockly.BlockSvg.CORNER_RADIUS * 2));
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS);
if (metrics.bayNotchAtRight) {
steps.push('v', metrics.bayHeight - (Blockly.BlockSvg.CORNER_RADIUS * 3) -
Blockly.BlockSvg.NOTCH_HEIGHT - 2 * Blockly.BlockSvg.GRID_UNIT);
steps.push(Blockly.BlockSvg.NOTCH_PATH_DOWN);
}
steps.push('V', metrics.bayHeight + 2 * Blockly.BlockSvg.GRID_UNIT);
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS);
// Create statement connection.
var connectionX = connectionsXY.x + Blockly.BlockSvg.CORNER_RADIUS * 2 +
4 * Blockly.BlockSvg.GRID_UNIT;
if (this.RTL) {
connectionX = connectionsXY.x - Blockly.BlockSvg.CORNER_RADIUS * 2 -
4 * Blockly.BlockSvg.GRID_UNIT;
}
var connectionY = connectionsXY.y + metrics.height -
Blockly.BlockSvg.CORNER_RADIUS * 2;
metrics.statement.connection.moveTo(connectionX, connectionY);
if (metrics.statement.connection.targetConnection) {
metrics.statement.connection.tighten_();
}
}
if (!this.isShadow()) {
steps.push('H', metrics.width - Blockly.BlockSvg.CORNER_RADIUS);
} else {
// input
steps.push('H', metrics.width - metrics.fieldRadius);
}
};
/**
* Render the right edge of the block.
* @param {!Array.<string>} steps Path of block outline.
* @param {!Object} connectionsXY Location of block.
* @param {!Object} metrics An object containing computed measurements of the
* block.
* @private
*/
Blockly.BlockSvg.prototype.renderDrawRight_ = function(steps, connectionsXY, metrics) {
if (!this.isShadow()) {
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',-' +
Blockly.BlockSvg.CORNER_RADIUS);
steps.push('v', -2.5 * Blockly.BlockSvg.GRID_UNIT);
} else {
// Input
steps.push(
'a', metrics.fieldRadius + ',' + metrics.fieldRadius,
'0', '0,0', metrics.fieldRadius + ',' + -1 * metrics.fieldRadius);
steps.push('v', -1 * (metrics.height - metrics.fieldRadius * 2));
}
if (this.nextConnection) {
steps.push(Blockly.BlockSvg.NOTCH_PATH_UP);
// Include width of notch in block width.
this.width += Blockly.BlockSvg.NOTCH_WIDTH;
// Create next block connection.
var connectionX;
if (this.RTL) {
connectionX = connectionsXY.x - metrics.width;
} else {
connectionX = connectionsXY.x + metrics.width;
}
var connectionY = connectionsXY.y + metrics.height -
Blockly.BlockSvg.CORNER_RADIUS * 2;
this.nextConnection.moveTo(connectionX, connectionY);
if (this.nextConnection.targetConnection) {
this.nextConnection.tighten_();
}
steps.push('V', Blockly.BlockSvg.CORNER_RADIUS);
} else if (!this.isShadow()) {
steps.push('V', Blockly.BlockSvg.CORNER_RADIUS);
}
};
/**
* Render the top edge of the block.
* @param {!Array.<string>} steps Path of block outline.
* @param {!Object} connectionsXY Location of block.
* @param {!Object} metrics An object containing computed measurements of the
* block.
* @private
*/
Blockly.BlockSvg.prototype.renderDrawTop_ = function(steps, connectionsXY, metrics) {
if (!this.isShadow()) {
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 -' +
Blockly.BlockSvg.CORNER_RADIUS + ',-' +
Blockly.BlockSvg.CORNER_RADIUS);
} else {
steps.push(
'a', metrics.fieldRadius + ',' + metrics.fieldRadius,
'0', '0,0', '-' + metrics.fieldRadius + ',-' + metrics.fieldRadius);
}
steps.push('z');
};
/**
* Get the field shadow block, if this block has one.
* <p>This is horizontal Scratch-specific, as "fields" are implemented as inputs
* with shadow blocks, and there is only one per block.
* @return {Blockly.BlockSvg} The field shadow block, or null if not found.
* @private
*/
Blockly.BlockSvg.prototype.getFieldShadowBlock_ = function() {
for (var i = 0, child; child = this.childBlocks_[i]; i++) {
if (child.isShadow()) {
return child;
}
}
return null;
};
/**
* Position an new block correctly, so that it doesn't move the existing block
* when connected to it.
* @param {!Blockly.Block} newBlock The block to position - either the first
* block in a dragged stack or an insertion marker.
* @param {!Blockly.Connection} newConnection The connection on the new block's
* stack - either a connection on newBlock, or the last NEXT_STATEMENT
* connection on the stack if the stack's being dropped before another
* block.
* @param {!Blockly.Connection} existingConnection The connection on the
* existing block, which newBlock should line up with.
*/
Blockly.BlockSvg.prototype.positionNewBlock = function(newBlock, newConnection, existingConnection) {
// We only need to position the new block if it's before the existing one,
// otherwise its position is set by the previous block.
if (newConnection.type == Blockly.NEXT_STATEMENT) {
var dx = existingConnection.x_ - newConnection.x_;
var dy = existingConnection.y_ - newConnection.y_;
// When putting a c-block around another c-block, the outer block must
// positioned above the inner block, as its connection point will stretch
// downwards when connected.
if (newConnection == newBlock.getFirstStatementConnection()) {
dy -= existingConnection.sourceBlock_.getHeightWidth(true).height -
Blockly.BlockSvg.MIN_BLOCK_Y;
}
newBlock.moveBy(dx, dy);
}
};