From a8f28c6b110db206f516b7c25efcf42e7ff0a7cc Mon Sep 17 00:00:00 2001 From: alschmiedt Date: Wed, 10 Feb 2021 16:19:56 -0800 Subject: [PATCH] Metrics refactor (#4627) --- core/bubble.js | 80 ++++---- core/metrics_manager.js | 372 ++++++++++++++++++++++++++++++++++++ core/mutator.js | 4 +- core/workspace.js | 2 +- core/workspace_svg.js | 257 ++++--------------------- tests/mocha/metrics_test.js | 36 ++-- 6 files changed, 470 insertions(+), 281 deletions(-) create mode 100644 core/metrics_manager.js diff --git a/core/bubble.js b/core/bubble.js index a67502131..7922230c9 100644 --- a/core/bubble.js +++ b/core/bubble.js @@ -241,8 +241,8 @@ Blockly.Bubble.prototype.createDom_ = function(content, hasResize) { [...content goes here...] */ - this.bubbleGroup_ = Blockly.utils.dom.createSvgElement( - Blockly.utils.Svg.G, {}, null); + this.bubbleGroup_ = + Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.G, {}, null); var filter = { 'filter': 'url(#' + this.workspace_.getRenderer().getConstants().embossFilterId + ')' @@ -458,14 +458,10 @@ Blockly.Bubble.prototype.setAnchorLocation = function(xy) { */ Blockly.Bubble.prototype.layoutBubble_ = function() { // Get the metrics in workspace units. - var metrics = this.workspace_.getMetrics(); - metrics.viewLeft /= this.workspace_.scale; - metrics.viewWidth /= this.workspace_.scale; - metrics.viewTop /= this.workspace_.scale; - metrics.viewHeight /= this.workspace_.scale; + var viewMetrics = this.workspace_.getMetricsManager().getViewMetrics(true); - var optimalLeft = this.getOptimalRelativeLeft_(metrics); - var optimalTop = this.getOptimalRelativeTop_(metrics); + var optimalLeft = this.getOptimalRelativeLeft_(viewMetrics); + var optimalTop = this.getOptimalRelativeTop_(viewMetrics); var bbox = this.shape_.getBBox(); var topPosition = { @@ -480,10 +476,10 @@ Blockly.Bubble.prototype.layoutBubble_ = function() { var closerPosition = bbox.width < bbox.height ? endPosition : bottomPosition; var fartherPosition = bbox.width < bbox.height ? bottomPosition : endPosition; - var topPositionOverlap = this.getOverlap_(topPosition, metrics); - var startPositionOverlap = this.getOverlap_(startPosition, metrics); - var closerPositionOverlap = this.getOverlap_(closerPosition, metrics); - var fartherPositionOverlap = this.getOverlap_(fartherPosition, metrics); + var topPositionOverlap = this.getOverlap_(topPosition, viewMetrics); + var startPositionOverlap = this.getOverlap_(startPosition, viewMetrics); + var closerPositionOverlap = this.getOverlap_(closerPosition, viewMetrics); + var fartherPositionOverlap = this.getOverlap_(fartherPosition, viewMetrics); // Set the position to whichever position shows the most of the bubble, // with tiebreaks going in the order: top > start > close > far. @@ -517,12 +513,12 @@ Blockly.Bubble.prototype.layoutBubble_ = function() { * workspace (what percentage of the bubble is visible). * @param {!{x: number, y: number}} relativeMin The position of the top-left * corner of the bubble relative to the anchor point. - * @param {!Blockly.utils.Metrics} metrics The metrics of the workspace the - * bubble will appear in. + * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics The view metrics + * of the workspace the bubble will appear in. * @return {number} The percentage of the bubble that is visible. * @private */ -Blockly.Bubble.prototype.getOverlap_ = function(relativeMin, metrics) { +Blockly.Bubble.prototype.getOverlap_ = function(relativeMin, viewMetrics) { // The position of the top-left corner of the bubble in workspace units. var bubbleMin = { x: this.workspace_.RTL ? (this.anchorXY_.x - relativeMin.x - this.width_) : @@ -538,11 +534,11 @@ Blockly.Bubble.prototype.getOverlap_ = function(relativeMin, metrics) { // calculation. // The position of the top-left corner of the workspace. - var workspaceMin = {x: metrics.viewLeft, y: metrics.viewTop}; + var workspaceMin = {x: viewMetrics.left, y: viewMetrics.top}; // The position of the bottom-right corner of the workspace. var workspaceMax = { - x: metrics.viewLeft + metrics.viewWidth, - y: metrics.viewTop + metrics.viewHeight + x: viewMetrics.left + viewMetrics.width, + y: viewMetrics.top + viewMetrics.height }; var overlapWidth = Math.min(bubbleMax.x, workspaceMax.x) - @@ -559,17 +555,17 @@ Blockly.Bubble.prototype.getOverlap_ = function(relativeMin, metrics) { * Calculate what the optimal horizontal position of the top-left corner of the * bubble is (relative to the anchor point) so that the most area of the * bubble is shown. - * @param {!Blockly.utils.Metrics} metrics The metrics of the workspace the - * bubble will appear in. + * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics The view metrics of the + * workspace the bubble will appear in. * @return {number} The optimal horizontal position of the top-left corner * of the bubble. * @private */ -Blockly.Bubble.prototype.getOptimalRelativeLeft_ = function(metrics) { +Blockly.Bubble.prototype.getOptimalRelativeLeft_ = function(viewMetrics) { var relativeLeft = -this.width_ / 4; // No amount of sliding left or right will give us a better overlap. - if (this.width_ > metrics.viewWidth) { + if (this.width_ > viewMetrics.width) { return relativeLeft; } @@ -578,16 +574,16 @@ Blockly.Bubble.prototype.getOptimalRelativeLeft_ = function(metrics) { var bubbleRight = this.anchorXY_.x - relativeLeft; var bubbleLeft = bubbleRight - this.width_; - var workspaceRight = metrics.viewLeft + metrics.viewWidth; - var workspaceLeft = metrics.viewLeft + + var workspaceRight = viewMetrics.left + viewMetrics.width; + var workspaceLeft = viewMetrics.left + // Thickness in workspace units. (Blockly.Scrollbar.scrollbarThickness / this.workspace_.scale); } else { var bubbleLeft = relativeLeft + this.anchorXY_.x; var bubbleRight = bubbleLeft + this.width_; - var workspaceLeft = metrics.viewLeft; - var workspaceRight = metrics.viewLeft + metrics.viewWidth - + var workspaceLeft = viewMetrics.left; + var workspaceRight = viewMetrics.left + viewMetrics.width - // Thickness in workspace units. (Blockly.Scrollbar.scrollbarThickness / this.workspace_.scale); } @@ -617,24 +613,24 @@ Blockly.Bubble.prototype.getOptimalRelativeLeft_ = function(metrics) { * Calculate what the optimal vertical position of the top-left corner of * the bubble is (relative to the anchor point) so that the most area of the * bubble is shown. - * @param {!Blockly.utils.Metrics} metrics The metrics of the workspace the - * bubble will appear in. + * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics The view metrics of the + * workspace the bubble will appear in. * @return {number} The optimal vertical position of the top-left corner * of the bubble. * @private */ -Blockly.Bubble.prototype.getOptimalRelativeTop_ = function(metrics) { +Blockly.Bubble.prototype.getOptimalRelativeTop_ = function(viewMetrics) { var relativeTop = -this.height_ / 4; // No amount of sliding up or down will give us a better overlap. - if (this.height_ > metrics.viewHeight) { + if (this.height_ > viewMetrics.height) { return relativeTop; } var bubbleTop = this.anchorXY_.y + relativeTop; var bubbleBottom = bubbleTop + this.height_; - var workspaceTop = metrics.viewTop; - var workspaceBottom = metrics.viewTop + metrics.viewHeight - + var workspaceTop = viewMetrics.top; + var workspaceBottom = viewMetrics.top + viewMetrics.height - // Thickness in workspace units. (Blockly.Scrollbar.scrollbarThickness / this.workspace_.scale); @@ -891,8 +887,7 @@ Blockly.Bubble.prototype.setAutoLayout = function(enable) { */ Blockly.Bubble.textToDom = function(text) { var paragraph = Blockly.utils.dom.createSvgElement( - Blockly.utils.Svg.TEXT, - { + Blockly.utils.Svg.TEXT, { 'class': 'blocklyText blocklyBubbleText blocklyNoPointerEvents', 'y': Blockly.Bubble.BORDER_WIDTH }, @@ -910,16 +905,18 @@ Blockly.Bubble.textToDom = function(text) { /** * Creates a bubble that can not be edited. - * @param {!SVGTextElement} paragraphElement The text element for the non editable bubble. + * @param {!SVGTextElement} paragraphElement The text element for the non + * editable bubble. * @param {!Blockly.BlockSvg} block The block that the bubble is attached to. * @param {!Blockly.utils.Coordinate} iconXY The coordinate of the icon. * @return {!Blockly.Bubble} The non editable bubble. * @package */ -Blockly.Bubble.createNonEditableBubble = function(paragraphElement, block, iconXY) { +Blockly.Bubble.createNonEditableBubble = function( + paragraphElement, block, iconXY) { var bubble = new Blockly.Bubble( - /** @type {!Blockly.WorkspaceSvg} */ (block.workspace), - paragraphElement, block.pathObject.svgPath, + /** @type {!Blockly.WorkspaceSvg} */ (block.workspace), paragraphElement, + block.pathObject.svgPath, /** @type {!Blockly.utils.Coordinate} */ (iconXY), null, null); // Expose this bubble's block's ID on its top-level SVG group. bubble.setSvgId(block.id); @@ -927,9 +924,8 @@ Blockly.Bubble.createNonEditableBubble = function(paragraphElement, block, iconX // Right-align the paragraph. // This cannot be done until the bubble is rendered on screen. var maxWidth = paragraphElement.getBBox().width; - for (var i = 0, textElement; - (textElement = paragraphElement.childNodes[i]); i++) { - + for (var i = 0, textElement; (textElement = paragraphElement.childNodes[i]); + i++) { textElement.setAttribute('text-anchor', 'end'); textElement.setAttribute('x', maxWidth + Blockly.Bubble.BORDER_WIDTH); } diff --git a/core/metrics_manager.js b/core/metrics_manager.js new file mode 100644 index 000000000..aa46f1cb8 --- /dev/null +++ b/core/metrics_manager.js @@ -0,0 +1,372 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Calculates and reports workspace metrics. + * @author aschmiedt@google.com (Abby Schmiedt) + */ +'use strict'; + +goog.provide('Blockly.MetricsManager'); + +goog.require('Blockly.utils.Size'); + +goog.requireType('Blockly.utils.Metrics'); +goog.requireType('Blockly.utils.toolbox'); + + +/** + * The manager for all workspace metrics calculations. + * @param {!Blockly.WorkspaceSvg} workspace The workspace to calculate metrics + * for. + * @constructor + */ +Blockly.MetricsManager = function(workspace) { + /** + * The workspace to calculate metrics for. + * @type {!Blockly.WorkspaceSvg} + * @protected + */ + this.workspace_ = workspace; +}; + +/** + * Describes the width, height and location of the toolbox on the main + * workspace. + * @typedef {{ + * width: number, + * height: number, + * position: !Blockly.utils.toolbox.Position + * }} + */ +Blockly.MetricsManager.ToolboxMetrics; + +/** + * Describes where the viewport starts in relation to the workspace svg. + * @typedef {{ + * left: number, + * top: number + * }} + */ +Blockly.MetricsManager.AbsoluteMetrics; + + +/** + * All the measurements needed to describe the size and location of a container. + * @typedef {{ + * height: number, + * width: number, + * top: number, + * left: number + * }} + */ +Blockly.MetricsManager.ContainerRegion; + +/** + * Gets the dimensions of the given workspace component, in pixel coordinates. + * @param {?Blockly.IToolbox|?Blockly.IFlyout} elem The element to get the + * dimensions of, or null. It should be a toolbox or flyout, and should + * implement getWidth() and getHeight(). + * @return {!Blockly.utils.Size} An object containing width and height + * attributes, which will both be zero if elem did not exist. + * @protected + */ +Blockly.MetricsManager.prototype.getDimensionsPx_ = function(elem) { + var width = 0; + var height = 0; + if (elem) { + width = elem.getWidth(); + height = elem.getHeight(); + } + return new Blockly.utils.Size(width, height); +}; + +/** + * Calculates the size of a scrollable workspace, which should include + * room for a half screen border around the workspace contents. In pixel + * coordinates. + * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics An object + * containing height and width attributes in CSS pixels. Together they + * specify the size of the visible workspace, not including areas covered up + * by the toolbox. + * @return {!Blockly.MetricsManager.ContainerRegion} The dimensions of the + * contents of the given workspace, as an object containing + * - height and width in pixels + * - left and top in pixels relative to the workspace origin. + * @protected + */ +Blockly.MetricsManager.prototype.getContentDimensionsBounded_ = function( + viewMetrics) { + var content = this.getContentDimensionsExact_(); + var contentRight = content.left + content.width; + var contentBottom = content.top + content.height; + + // View height and width are both in pixels, and are the same as the SVG size. + var viewWidth = viewMetrics.width; + var viewHeight = viewMetrics.height; + var halfWidth = viewWidth / 2; + var halfHeight = viewHeight / 2; + + // Add a border around the content that is at least half a screen wide. + // Ensure border is wide enough that blocks can scroll over entire screen. + var left = Math.min(content.left - halfWidth, contentRight - viewWidth); + var right = Math.max(contentRight + halfWidth, content.left + viewWidth); + + var top = Math.min(content.top - halfHeight, contentBottom - viewHeight); + var bottom = Math.max(contentBottom + halfHeight, content.top + viewHeight); + + return {left: left, top: top, height: bottom - top, width: right - left}; +}; + +/** + * Gets the bounding box for all workspace contents, in pixel coordinates. + * @return {!Blockly.MetricsManager.ContainerRegion} The dimensions of the + * contents of the given workspace in pixel coordinates, as an object + * containing + * - height and width in pixels + * - left and top in pixels relative to the workspace origin. + * @protected + */ +Blockly.MetricsManager.prototype.getContentDimensionsExact_ = function() { + // Block bounding box is in workspace coordinates. + var blockBox = this.workspace_.getBlocksBoundingBox(); + var scale = this.workspace_.scale; + + // Convert to pixels. + var top = blockBox.top * scale; + var bottom = blockBox.bottom * scale; + var left = blockBox.left * scale; + var right = blockBox.right * scale; + + return {top: top, left: left, width: right - left, height: bottom - top}; +}; + +/** + * Gets the width and the height of the flyout on the workspace in pixel + * coordinates. Returns 0 for the width and height if the workspace has a + * category toolbox instead of a simple toolbox. + * @param {boolean=} opt_own Only return the workspace's own flyout if True. + * @return {!Blockly.utils.Size} The width and height of the flyout. + * @public + */ +Blockly.MetricsManager.prototype.getFlyoutMetrics = function(opt_own) { + var flyoutDimensions = + this.getDimensionsPx_(this.workspace_.getFlyout(opt_own)); + return new Blockly.utils.Size( + flyoutDimensions.width, flyoutDimensions.height); +}; + +/** + * Gets the width, height and position of the toolbox on the workspace in pixel + * coordinates. Returns 0 for the width and height if the workspace has a simple + * toolbox instead of a category toolbox. To get the width and height of a + * simple toolbox @see {@link getFlyoutMetrics}. + * @return {!Blockly.MetricsManager.ToolboxMetrics} The object with the width, + * height and position of the toolbox. + * @public + */ +Blockly.MetricsManager.prototype.getToolboxMetrics = function() { + var toolboxDimensions = this.getDimensionsPx_(this.workspace_.getToolbox()); + + return { + width: toolboxDimensions.width, + height: toolboxDimensions.height, + position: this.workspace_.toolboxPosition + }; +}; + +/** + * Gets the width and height of the workspace's parent svg element in pixel + * coordinates. This area includes the toolbox and the visible workspace area. + * @return {!Blockly.utils.Size} The width and height of the workspace's parent + * svg element. + * @public + */ +Blockly.MetricsManager.prototype.getSvgMetrics = function() { + var svgSize = Blockly.svgSize(this.workspace_.getParentSvg()); + return new Blockly.utils.Size(svgSize.width, svgSize.height); +}; + +/** + * Gets the absolute left and absolute top in pixel coordinates. + * This is where the visible workspace starts in relation to the svg container. + * @return {!Blockly.MetricsManager.AbsoluteMetrics} The absolute metrics for + * the workspace. + * @public + */ +Blockly.MetricsManager.prototype.getAbsoluteMetrics = function() { + var absoluteLeft = 0; + var toolboxMetrics = this.getToolboxMetrics(); + var flyoutMetrics = this.getFlyoutMetrics(true); + var doesToolboxExist = !!this.workspace_.getToolbox(); + var toolboxPosition = this.workspace_.toolboxPosition; + var doesFlyoutExist = !!this.workspace_.getFlyout(true); + + if (doesToolboxExist && toolboxPosition == Blockly.TOOLBOX_AT_LEFT) { + absoluteLeft = toolboxMetrics.width; + } else if (doesFlyoutExist && toolboxPosition == Blockly.TOOLBOX_AT_LEFT) { + absoluteLeft = flyoutMetrics.width; + } + var absoluteTop = 0; + if (doesToolboxExist && toolboxPosition == Blockly.TOOLBOX_AT_TOP) { + absoluteTop = toolboxMetrics.height; + } else if (doesFlyoutExist && toolboxPosition == Blockly.TOOLBOX_AT_TOP) { + absoluteTop = flyoutMetrics.height; + } + + return { + top: absoluteTop, + left: absoluteLeft, + }; +}; + +/** + * Gets the metrics for the visible workspace in either pixel or workspace + * coordinates. The visible workspace does not include the toolbox or flyout. + * @param {boolean=} opt_getWorkspaceCoordinates True to get the view metrics in + * workspace coordinates, false to get them in pixel coordinates. + * @return {!Blockly.MetricsManager.ContainerRegion} The width, height, top and + * left of the viewport in either workspace coordinates or pixel + * coordinates. + * @public + */ +Blockly.MetricsManager.prototype.getViewMetrics = function( + opt_getWorkspaceCoordinates) { + var scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1; + var svgMetrics = this.getSvgMetrics(); + var toolboxMetrics = this.getToolboxMetrics(); + var flyoutMetrics = this.getFlyoutMetrics(true); + var toolboxPosition = this.workspace_.toolboxPosition; + + if (this.workspace_.getToolbox()) { + if (toolboxPosition == Blockly.TOOLBOX_AT_TOP || + toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) { + svgMetrics.height -= toolboxMetrics.height; + } else if (toolboxPosition == Blockly.TOOLBOX_AT_LEFT || + toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { + svgMetrics.width -= toolboxMetrics.width; + } + } else if (this.workspace_.getFlyout(true)) { + if (toolboxPosition == Blockly.TOOLBOX_AT_TOP || + toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) { + svgMetrics.height -= flyoutMetrics.height; + } else if (toolboxPosition == Blockly.TOOLBOX_AT_LEFT || + toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { + svgMetrics.width -= flyoutMetrics.width; + } + } + return { + height: svgMetrics.height / scale, + width: svgMetrics.width / scale, + top: -this.workspace_.scrollY / scale, + left: -this.workspace_.scrollX / scale, + }; +}; + +/** + * Gets content metrics in either pixel or workspace coordinates. + * + * This can mean two things: + * If the workspace has a fixed width and height then the content + * area is rectangle around all the top bounded elements on the workspace + * (workspace comments and blocks). + * + * If the workspace does not have a fixed width and height then it is the + * metrics of the area that content can be placed. This area is computed by + * getting the rectangle around the top bounded elements on the workspace and + * adding padding to all sides. + * @param {!Blockly.MetricsManager.ContainerRegion=} opt_viewMetrics The view + * metrics if they have been previously computed. Passing in null will cause + * the view metrics to be computed again. + * @param {boolean=} opt_getWorkspaceCoordinates True to get the content metrics + * in workspace coordinates, false to get them in pixel coordinates. + * @return {!Blockly.MetricsManager.ContainerRegion} The + * metrics for the content container. + * @public + */ +Blockly.MetricsManager.prototype.getContentMetrics = function( + opt_viewMetrics, opt_getWorkspaceCoordinates) { + var scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1; + var contentDimensions = null; + if (this.workspace_.isContentBounded()) { + opt_viewMetrics = opt_viewMetrics || this.getViewMetrics(false); + contentDimensions = this.getContentDimensionsBounded_(opt_viewMetrics); + } else { + contentDimensions = this.getContentDimensionsExact_(); + } + return { + height: contentDimensions.height / scale, + width: contentDimensions.width / scale, + top: contentDimensions.top / scale, + left: contentDimensions.left / scale, + }; +}; + +/** + * Returns an object with all the metrics required to size scrollbars for a + * top level workspace. The following properties are computed: + * Coordinate system: pixel coordinates, -left, -up, +right, +down + * .viewHeight: Height of the visible portion of the workspace. + * .viewWidth: Width of the visible portion of the workspace. + * .contentHeight: Height of the content. + * .contentWidth: Width of the content. + * .svgHeight: Height of the Blockly div (the view + the toolbox, + * simple or otherwise), + * .svgWidth: Width of the Blockly div (the view + the toolbox, + * simple or otherwise), + * .viewTop: Top-edge of the visible portion of the workspace, relative to + * the workspace origin. + * .viewLeft: Left-edge of the visible portion of the workspace, relative to + * the workspace origin. + * .contentTop: Top-edge of the content, relative to the workspace origin. + * .contentLeft: Left-edge of the content relative to the workspace origin. + * .absoluteTop: Top-edge of the visible portion of the workspace, relative + * to the blocklyDiv. + * .absoluteLeft: Left-edge of the visible portion of the workspace, relative + * to the blocklyDiv. + * .toolboxWidth: Width of the toolbox, if it exists. Otherwise zero. + * .toolboxHeight: Height of the toolbox, if it exists. Otherwise zero. + * .flyoutWidth: Width of the flyout if it is always open. Otherwise zero. + * .flyoutHeight: Height of the flyout if it is always open. Otherwise zero. + * .toolboxPosition: Top, bottom, left or right. Use TOOLBOX_AT constants to + * compare. + * @return {!Blockly.utils.Metrics} Contains size and position metrics of a top + * level workspace. + * @public + */ +Blockly.MetricsManager.prototype.getMetrics = function() { + var toolboxMetrics = this.getToolboxMetrics(); + var flyoutMetrics = this.getFlyoutMetrics(true); + var svgMetrics = this.getSvgMetrics(); + var absoluteMetrics = this.getAbsoluteMetrics(); + var viewMetrics = this.getViewMetrics(); + var contentMetrics = this.getContentMetrics(viewMetrics); + + return { + contentHeight: contentMetrics.height, + contentWidth: contentMetrics.width, + contentTop: contentMetrics.top, + contentLeft: contentMetrics.left, + + viewHeight: viewMetrics.height, + viewWidth: viewMetrics.width, + viewTop: viewMetrics.top, + viewLeft: viewMetrics.left, + + absoluteTop: absoluteMetrics.top, + absoluteLeft: absoluteMetrics.left, + + svgHeight: svgMetrics.height, + svgWidth: svgMetrics.width, + + toolboxWidth: toolboxMetrics.width, + toolboxHeight: toolboxMetrics.height, + toolboxPosition: toolboxMetrics.position, + + flyoutWidth: flyoutMetrics.width, + flyoutHeight: flyoutMetrics.height + }; +}; diff --git a/core/mutator.js b/core/mutator.js index f6032a86d..86d5bff45 100644 --- a/core/mutator.js +++ b/core/mutator.js @@ -441,8 +441,8 @@ Blockly.Mutator.prototype.workspaceChanged_ = function(e) { }; /** - * Return an object with all the metrics required to size scrollbars for the - * mutator flyout. The following properties are computed: + * Returns an object with all the metrics required to correctly position the + * mutator's flyout. The following properties are computed: * .viewHeight: Height of the visible rectangle, * .viewWidth: Width of the visible rectangle, * .absoluteTop: Top-edge of view. diff --git a/core/workspace.js b/core/workspace.js index e5e8b8c52..08e5940e2 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -41,7 +41,7 @@ Blockly.Workspace = function(opt_options) { this.RTL = !!this.options.RTL; /** @type {boolean} */ this.horizontalLayout = !!this.options.horizontalLayout; - /** @type {number} */ + /** @type {Blockly.utils.toolbox.Position} */ this.toolboxPosition = this.options.toolboxPosition; var connectionCheckerClass = Blockly.registry.getClassFromOptions( diff --git a/core/workspace_svg.js b/core/workspace_svg.js index 8b5a47106..2f1f8f2c5 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -24,6 +24,7 @@ goog.require('Blockly.Events.ViewportChange'); goog.require('Blockly.Gesture'); goog.require('Blockly.Grid'); goog.require('Blockly.MarkerManager'); +goog.require('Blockly.MetricsManager'); goog.require('Blockly.Msg'); goog.require('Blockly.Options'); goog.require('Blockly.registry'); @@ -71,17 +72,33 @@ goog.requireType('Blockly.ZoomControls'); * @implements {Blockly.IASTNodeLocationSvg} * @constructor */ -Blockly.WorkspaceSvg = function(options, - opt_blockDragSurface, opt_wsDragSurface) { +Blockly.WorkspaceSvg = function( + options, opt_blockDragSurface, opt_wsDragSurface) { Blockly.WorkspaceSvg.superClass_.constructor.call(this, options); - /** @type {function():!Blockly.utils.Metrics} */ - this.getMetrics = - options.getMetrics || Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_; - /** @type {function(!{x:number, y:number}):void} */ + + /** + * Object in charge of calculating metrics for the workspace. + * @type {!Blockly.MetricsManager} + * @private + */ + this.metricsManager_ = new Blockly.MetricsManager(this); + + /** + * Method to get all the metrics that have to do with a workspace. + * @type {function():!Blockly.utils.Metrics} + * @package + */ + this.getMetrics = options.getMetrics || + this.metricsManager_.getMetrics.bind(this.metricsManager_); + + /** + * Translates the workspace. + * @type {function(!{x:number, y:number}):void} + * @package + */ this.setMetrics = options.setMetrics || Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_; - this.connectionDBList = Blockly.ConnectionDB.init(this.connectionChecker); if (opt_blockDragSurface) { @@ -477,6 +494,15 @@ Blockly.WorkspaceSvg.prototype.getMarkerManager = function() { return this.markerManager_; }; +/** + * Gets the metrics manager for this workspace. + * @return {!Blockly.MetricsManager} The marker manager. + * @public + */ +Blockly.WorkspaceSvg.prototype.getMetricsManager = function() { + return this.metricsManager_; +}; + /** * Add the cursor svg to this workspaces svg group. * @param {SVGElement} cursorSvg The svg root of the cursor to be added to the @@ -613,7 +639,6 @@ Blockly.WorkspaceSvg.prototype.updateBlockStyles_ = function(blocks) { * @return {SVGMatrix} The matrix to use in mouseToSvg */ Blockly.WorkspaceSvg.prototype.getInverseScreenCTM = function() { - // Defer getting the screen CTM until we actually need it, this should // avoid forced reflows from any calls to updateInverseScreenCTM. if (this.inverseScreenCTMDirty_) { @@ -2165,222 +2190,6 @@ Blockly.WorkspaceSvg.prototype.scroll = function(x, y) { this.translate(x, y); }; -/** - * Get the dimensions of the given workspace component, in pixels. - * @param {Blockly.IToolbox|Blockly.IFlyout} elem The element to get the - * dimensions of, or null. It should be a toolbox or flyout, and should - * implement getWidth() and getHeight(). - * @return {!Blockly.utils.Size} An object containing width and height - * attributes, which will both be zero if elem did not exist. - * @private - */ -Blockly.WorkspaceSvg.getDimensionsPx_ = function(elem) { - var width = 0; - var height = 0; - if (elem) { - width = elem.getWidth(); - height = elem.getHeight(); - } - return new Blockly.utils.Size(width, height); -}; - -/** - * Get the content dimensions of the given workspace, taking into account - * whether or not it is scrollable and what size the workspace div is on screen. - * @param {!Blockly.WorkspaceSvg} ws The workspace to measure. - * @param {!Object} svgSize An object containing height and width attributes in - * CSS pixels. Together they specify the size of the visible workspace, not - * including areas covered up by the toolbox. - * @return {!Object} The dimensions of the contents of the given workspace, as - * an object containing at least - * - height and width in pixels - * - left and top in pixels relative to the workspace origin. - * @private - */ -Blockly.WorkspaceSvg.getContentDimensions_ = function(ws, svgSize) { - if (ws.isContentBounded()) { - return Blockly.WorkspaceSvg.getContentDimensionsBounded_(ws, svgSize); - } else { - return Blockly.WorkspaceSvg.getContentDimensionsExact_(ws); - } -}; - -/** - * Get the bounding box for all workspace contents, in pixels. - * @param {!Blockly.WorkspaceSvg} ws The workspace to inspect. - * @return {!Object} The dimensions of the contents of the given workspace, as - * an object containing - * - height and width in pixels - * - left, right, top and bottom in pixels relative to the workspace origin. - * @private - */ -Blockly.WorkspaceSvg.getContentDimensionsExact_ = function(ws) { - // Block bounding box is in workspace coordinates. - var blockBox = ws.getBlocksBoundingBox(); - var scale = ws.scale; - - // Convert to pixels. - var top = blockBox.top * scale; - var bottom = blockBox.bottom * scale; - var left = blockBox.left * scale; - var right = blockBox.right * scale; - - return { - top: top, - bottom: bottom, - left: left, - right: right, - width: right - left, - height: bottom - top - }; -}; - -/** - * Calculate the size of a scrollable workspace, which should include room for a - * half screen border around the workspace contents. - * @param {!Blockly.WorkspaceSvg} ws The workspace to measure. - * @param {!Object} svgSize An object containing height and width attributes in - * CSS pixels. Together they specify the size of the visible workspace, not - * including areas covered up by the toolbox. - * @return {!Object} The dimensions of the contents of the given workspace, as - * an object containing - * - height and width in pixels - * - left and top in pixels relative to the workspace origin. - * @private - */ -Blockly.WorkspaceSvg.getContentDimensionsBounded_ = function(ws, svgSize) { - var content = Blockly.WorkspaceSvg.getContentDimensionsExact_(ws); - - // View height and width are both in pixels, and are the same as the SVG size. - var viewWidth = svgSize.width; - var viewHeight = svgSize.height; - var halfWidth = viewWidth / 2; - var halfHeight = viewHeight / 2; - - // Add a border around the content that is at least half a screen wide. - // Ensure border is wide enough that blocks can scroll over entire screen. - var left = Math.min(content.left - halfWidth, content.right - viewWidth); - var right = Math.max(content.right + halfWidth, content.left + viewWidth); - - var top = Math.min(content.top - halfHeight, content.bottom - viewHeight); - var bottom = Math.max(content.bottom + halfHeight, content.top + viewHeight); - - var dimensions = { - left: left, - top: top, - height: bottom - top, - width: right - left - }; - return dimensions; -}; - -/** - * Return an object with all the metrics required to size scrollbars for a - * top level workspace. The following properties are computed: - * Coordinate system: pixel coordinates, -left, -up, +right, +down - * .viewHeight: Height of the visible portion of the workspace. - * .viewWidth: Width of the visible portion of the workspace. - * .contentHeight: Height of the content. - * .contentWidth: Width of the content. - * .svgHeight: Height of the Blockly div (the view + the toolbox, - * simple or otherwise), - * .svgWidth: Width of the Blockly div (the view + the toolbox, - * simple or otherwise), - * .viewTop: Top-edge of the visible portion of the workspace, relative to - * the workspace origin. - * .viewLeft: Left-edge of the visible portion of the workspace, relative to - * the workspace origin. - * .contentTop: Top-edge of the content, relative to the workspace origin. - * .contentLeft: Left-edge of the content relative to the workspace origin. - * .absoluteTop: Top-edge of the visible portion of the workspace, relative - * to the blocklyDiv. - * .absoluteLeft: Left-edge of the visible portion of the workspace, relative - * to the blocklyDiv. - * .toolboxWidth: Width of the toolbox, if it exists. Otherwise zero. - * .toolboxHeight: Height of the toolbox, if it exists. Otherwise zero. - * .flyoutWidth: Width of the flyout if it is always open. Otherwise zero. - * .flyoutHeight: Height of the flyout if it is always open. Otherwise zero. - * .toolboxPosition: Top, bottom, left or right. Use TOOLBOX_AT constants to - * compare. - * @return {!Blockly.utils.Metrics} Contains size and position metrics of a top - * level workspace. - * @private - * @this {Blockly.WorkspaceSvg} - */ -Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_ = function() { - - var toolboxDimensions = - Blockly.WorkspaceSvg.getDimensionsPx_(this.toolbox_); - var flyoutDimensions = - Blockly.WorkspaceSvg.getDimensionsPx_(this.flyout_); - - // Contains height and width in CSS pixels. - // svgSize is equivalent to the size of the injectionDiv at this point. - var svgSize = Blockly.svgSize(this.getParentSvg()); - var viewSize = {height: svgSize.height, width: svgSize.width}; - if (this.toolbox_) { - if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP || - this.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) { - viewSize.height -= toolboxDimensions.height; - } else if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT || - this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { - viewSize.width -= toolboxDimensions.width; - } - } else if (this.flyout_) { - if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP || - this.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) { - viewSize.height -= flyoutDimensions.height; - } else if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT || - this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { - viewSize.width -= flyoutDimensions.width; - } - } - - // svgSize is now the space taken up by the Blockly workspace, not including - // the toolbox. - var contentDimensions = - Blockly.WorkspaceSvg.getContentDimensions_(this, viewSize); - - var absoluteLeft = 0; - if (this.toolbox_ && this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) { - absoluteLeft = toolboxDimensions.width; - } else if (this.flyout_ && this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) { - absoluteLeft = flyoutDimensions.width; - } - var absoluteTop = 0; - if (this.toolbox_ && this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) { - absoluteTop = toolboxDimensions.height; - } else if (this.flyout_ && this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) { - absoluteTop = flyoutDimensions.height; - } - - var metrics = { - contentHeight: contentDimensions.height, - contentWidth: contentDimensions.width, - contentTop: contentDimensions.top, - contentLeft: contentDimensions.left, - - viewHeight: viewSize.height, - viewWidth: viewSize.width, - viewTop: -this.scrollY, - viewLeft: -this.scrollX, - - absoluteTop: absoluteTop, - absoluteLeft: absoluteLeft, - - svgHeight: svgSize.height, - svgWidth: svgSize.width, - - toolboxWidth: toolboxDimensions.width, - toolboxHeight: toolboxDimensions.height, - toolboxPosition: this.toolboxPosition, - - flyoutWidth: flyoutDimensions.width, - flyoutHeight: flyoutDimensions.height - }; - return metrics; -}; - /** * Sets the X/Y translations of a top level workspace. * @param {!Object} xyRatio Contains an x and/or y property which is a float diff --git a/tests/mocha/metrics_test.js b/tests/mocha/metrics_test.js index f252b17a9..016210322 100644 --- a/tests/mocha/metrics_test.js +++ b/tests/mocha/metrics_test.js @@ -43,70 +43,82 @@ suite('Metrics', function() { test('GetContentDimensionsExact - empty', function() { var ws = makeMockWs(1, 0, 0, 0, 0); - var defaultZoom = Blockly.WorkspaceSvg.getContentDimensionsExact_(ws); + var metricsManager = new Blockly.MetricsManager(ws); + var defaultZoom = metricsManager.getContentDimensionsExact_(ws); assertDimensionsMatch(defaultZoom, 0, 0, 0, 0); }); test('GetContentDimensionsExact - empty zoom in', function() { var ws = makeMockWs(2, 0, 0, 0, 0); - var zoomIn = Blockly.WorkspaceSvg.getContentDimensionsExact_(ws); + var metricsManager = new Blockly.MetricsManager(ws); + var zoomIn = metricsManager.getContentDimensionsExact_(ws); assertDimensionsMatch(zoomIn, 0, 0, 0, 0); }); test('GetContentDimensionsExact - empty zoom out', function() { var ws = makeMockWs(.5, 0, 0, 0, 0); - var zoomOut = Blockly.WorkspaceSvg.getContentDimensionsExact_(ws); + var metricsManager = new Blockly.MetricsManager(ws); + var zoomOut = metricsManager.getContentDimensionsExact_(ws); assertDimensionsMatch(zoomOut, 0, 0, 0, 0); }); test('GetContentDimensionsExact - non empty at origin', function() { var ws = makeMockWs(1, 0, 0, 100, 100); - var defaultZoom = Blockly.WorkspaceSvg.getContentDimensionsExact_(ws); + var metricsManager = new Blockly.MetricsManager(ws); + var defaultZoom = metricsManager.getContentDimensionsExact_(ws); // Pixel and ws units are the same at default zoom. assertDimensionsMatch(defaultZoom, 0, 0, 100, 100); }); test('GetContentDimensionsExact - non empty at origin zoom in', function() { var ws = makeMockWs(2, 0, 0, 100, 100); - var zoomIn = Blockly.WorkspaceSvg.getContentDimensionsExact_(ws); + var metricsManager = new Blockly.MetricsManager(ws); + var zoomIn = metricsManager.getContentDimensionsExact_(ws); // 1 ws unit = 2 pixels at this zoom level. assertDimensionsMatch(zoomIn, 0, 0, 200, 200); }); test('GetContentDimensionsExact - non empty at origin zoom out', function() { var ws = makeMockWs(.5, 0, 0, 100, 100); - var zoomOut = Blockly.WorkspaceSvg.getContentDimensionsExact_(ws); + var metricsManager = new Blockly.MetricsManager(ws); + var zoomOut = metricsManager.getContentDimensionsExact_(ws); // 1 ws unit = 0.5 pixels at this zoom level. assertDimensionsMatch(zoomOut, 0, 0, 50, 50); }); test('GetContentDimensionsExact - non empty positive origin', function() { var ws = makeMockWs(1, 10, 10, 100, 100); - var defaultZoom = Blockly.WorkspaceSvg.getContentDimensionsExact_(ws); + var metricsManager = new Blockly.MetricsManager(ws); + var defaultZoom = metricsManager.getContentDimensionsExact_(ws); // Pixel and ws units are the same at default zoom. assertDimensionsMatch(defaultZoom, 10, 10, 100, 100); }); test('GetContentDimensionsExact - non empty positive origin zoom in', function() { var ws = makeMockWs(2, 10, 10, 100, 100); - var zoomIn = Blockly.WorkspaceSvg.getContentDimensionsExact_(ws); + var metricsManager = new Blockly.MetricsManager(ws); + var zoomIn = metricsManager.getContentDimensionsExact_(ws); // 1 ws unit = 2 pixels at this zoom level. assertDimensionsMatch(zoomIn, 20, 20, 200, 200); }); test('GetContentDimensionsExact - non empty positive origin zoom out', function() { var ws = makeMockWs(.5, 10, 10, 100, 100); - var zoomOut = Blockly.WorkspaceSvg.getContentDimensionsExact_(ws); + var metricsManager = new Blockly.MetricsManager(ws); + var zoomOut = metricsManager.getContentDimensionsExact_(ws); // 1 ws unit = 0.5 pixels at this zoom level. assertDimensionsMatch(zoomOut, 5, 5, 50, 50); }); test('GetContentDimensionsExact - non empty negative origin', function() { var ws = makeMockWs(1, -10, -10, 100, 100); - var defaultZoom = Blockly.WorkspaceSvg.getContentDimensionsExact_(ws); + var metricsManager = new Blockly.MetricsManager(ws); + var defaultZoom = metricsManager.getContentDimensionsExact_(ws); // Pixel and ws units are the same at default zoom. assertDimensionsMatch(defaultZoom, -10, -10, 100, 100); }); test('GetContentDimensionsExact - non empty negative origin zoom in', function() { var ws = makeMockWs(2, -10, -10, 100, 100); - var zoomIn = Blockly.WorkspaceSvg.getContentDimensionsExact_(ws); + var metricsManager = new Blockly.MetricsManager(ws); + var zoomIn = metricsManager.getContentDimensionsExact_(ws); // 1 ws unit = 2 pixels at this zoom level. assertDimensionsMatch(zoomIn, -20, -20, 200, 200); }); test('GetContentDimensionsExact - non empty negative origin zoom out', function() { var ws = makeMockWs(.5, -10, -10, 100, 100); - var zoomOut = Blockly.WorkspaceSvg.getContentDimensionsExact_(ws); + var metricsManager = new Blockly.MetricsManager(ws); + var zoomOut = metricsManager.getContentDimensionsExact_(ws); // 1 ws unit = 0.5 pixels at this zoom level. assertDimensionsMatch(zoomOut, -5, -5, 50, 50); });