workspace_svg.js 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2014 Google Inc.
  6. * https://developers.google.com/blockly/
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. /**
  21. * @fileoverview Object representing a workspace rendered as SVG.
  22. * @author fraser@google.com (Neil Fraser)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.WorkspaceSvg');
  26. // TODO(scr): Fix circular dependencies
  27. //goog.require('Blockly.BlockSvg');
  28. goog.require('Blockly.ConnectionDB');
  29. goog.require('Blockly.Options');
  30. goog.require('Blockly.ScrollbarPair');
  31. goog.require('Blockly.Trashcan');
  32. goog.require('Blockly.Workspace');
  33. goog.require('Blockly.Xml');
  34. goog.require('Blockly.ZoomControls');
  35. goog.require('goog.dom');
  36. goog.require('goog.math.Coordinate');
  37. goog.require('goog.userAgent');
  38. var Blockscad = Blockscad || {};
  39. /**
  40. * Class for a workspace. This is an onscreen area with optional trashcan,
  41. * scrollbars, bubbles, and dragging.
  42. * @param {!Blockly.Options} options Dictionary of options.
  43. * @extends {Blockly.Workspace}
  44. * @constructor
  45. */
  46. Blockly.WorkspaceSvg = function(options) {
  47. Blockly.WorkspaceSvg.superClass_.constructor.call(this, options);
  48. this.getMetrics = options.getMetrics;
  49. this.setMetrics = options.setMetrics;
  50. Blockly.ConnectionDB.init(this);
  51. /**
  52. * Database of pre-loaded sounds.
  53. * @private
  54. * @const
  55. */
  56. this.SOUNDS_ = Object.create(null);
  57. };
  58. goog.inherits(Blockly.WorkspaceSvg, Blockly.Workspace);
  59. /**
  60. * Wrapper function called when a resize event occurs.
  61. * @type {Array.<!Array>} Data that can be passed to unbindEvent_
  62. */
  63. Blockly.WorkspaceSvg.prototype.resizeHandlerWrapper_ = null;
  64. /**
  65. * Svg workspaces are user-visible (as opposed to a headless workspace).
  66. * @type {boolean} True if visible. False if headless.
  67. */
  68. Blockly.WorkspaceSvg.prototype.rendered = true;
  69. /**
  70. * Is this workspace the surface for a flyout?
  71. * @type {boolean}
  72. */
  73. Blockly.WorkspaceSvg.prototype.isFlyout = false;
  74. /**
  75. * Is this workspace currently being dragged around?
  76. * DRAG_NONE - No drag operation.
  77. * DRAG_BEGIN - Still inside the initial DRAG_RADIUS.
  78. * DRAG_FREE - Workspace has been dragged further than DRAG_RADIUS.
  79. * @private
  80. */
  81. Blockly.WorkspaceSvg.prototype.dragMode_ = Blockly.DRAG_NONE;
  82. /**
  83. * Current horizontal scrolling offset.
  84. * @type {number}
  85. */
  86. Blockly.WorkspaceSvg.prototype.scrollX = 0;
  87. /**
  88. * Current vertical scrolling offset.
  89. * @type {number}
  90. */
  91. Blockly.WorkspaceSvg.prototype.scrollY = 0;
  92. /**
  93. * Horizontal scroll value when scrolling started.
  94. * @type {number}
  95. */
  96. Blockly.WorkspaceSvg.prototype.startScrollX = 0;
  97. /**
  98. * Vertical scroll value when scrolling started.
  99. * @type {number}
  100. */
  101. Blockly.WorkspaceSvg.prototype.startScrollY = 0;
  102. /**
  103. * Distance from mouse to object being dragged.
  104. * @type {goog.math.Coordinate}
  105. * @private
  106. */
  107. Blockly.WorkspaceSvg.prototype.dragDeltaXY_ = null;
  108. /**
  109. * Current scale.
  110. * @type {number}
  111. */
  112. Blockly.WorkspaceSvg.prototype.scale = 1;
  113. /**
  114. * The workspace's trashcan (if any).
  115. * @type {Blockly.Trashcan}
  116. */
  117. Blockly.WorkspaceSvg.prototype.trashcan = null;
  118. /**
  119. * This workspace's scrollbars, if they exist.
  120. * @type {Blockly.ScrollbarPair}
  121. */
  122. Blockly.WorkspaceSvg.prototype.scrollbar = null;
  123. /**
  124. * Time that the last sound was played.
  125. * @type {Date}
  126. * @private
  127. */
  128. Blockly.WorkspaceSvg.prototype.lastSound_ = null;
  129. /**
  130. * Inverted screen CTM, for use in mouseToSvg.
  131. * @type {SVGMatrix}
  132. * @private
  133. */
  134. Blockly.WorkspaceSvg.prototype.inverseScreenCTM_ = null;
  135. /**
  136. * Getter for the inverted screen CTM.
  137. * @return {SVGMatrix} The matrix to use in mouseToSvg
  138. */
  139. Blockly.WorkspaceSvg.prototype.getInverseScreenCTM = function() {
  140. return this.inverseScreenCTM_;
  141. };
  142. /**
  143. * Update the inverted screen CTM.
  144. */
  145. Blockly.WorkspaceSvg.prototype.updateInverseScreenCTM = function() {
  146. this.inverseScreenCTM_ = this.getParentSvg().getScreenCTM().inverse();
  147. };
  148. /**
  149. * Save resize handler data so we can delete it later in dispose.
  150. * @param {!Array.<!Array>} handler Data that can be passed to unbindEvent_.
  151. */
  152. Blockly.WorkspaceSvg.prototype.setResizeHandlerWrapper = function(handler) {
  153. this.resizeHandlerWrapper_ = handler;
  154. };
  155. /**
  156. * Create the workspace DOM elements.
  157. * @param {string=} opt_backgroundClass Either 'blocklyMainBackground' or
  158. * 'blocklyMutatorBackground'.
  159. * @return {!Element} The workspace's SVG group.
  160. */
  161. Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
  162. /**
  163. * <g class="blocklyWorkspace">
  164. * <rect class="blocklyMainBackground" height="100%" width="100%"></rect>
  165. * [Trashcan and/or flyout may go here]
  166. * <g class="blocklyBlockCanvas"></g>
  167. * <g class="blocklyBubbleCanvas"></g>
  168. * [Scrollbars may go here]
  169. * </g>
  170. * @type {SVGElement}
  171. */
  172. this.svgGroup_ = Blockly.createSvgElement('g',
  173. {'class': 'blocklyWorkspace'}, null);
  174. if (opt_backgroundClass) {
  175. /** @type {SVGElement} */
  176. this.svgBackground_ = Blockly.createSvgElement('rect',
  177. {'height': '100%', 'width': '100%', 'class': opt_backgroundClass},
  178. this.svgGroup_);
  179. if (opt_backgroundClass == 'blocklyMainBackground') {
  180. this.svgBackground_.style.fill =
  181. 'url(#' + this.options.gridPattern.id + ')';
  182. }
  183. }
  184. /** @type {SVGElement} */
  185. this.svgBlockCanvas_ = Blockly.createSvgElement('g',
  186. {'class': 'blocklyBlockCanvas'}, this.svgGroup_, this);
  187. /** @type {SVGElement} */
  188. this.svgBubbleCanvas_ = Blockly.createSvgElement('g',
  189. {'class': 'blocklyBubbleCanvas'}, this.svgGroup_, this);
  190. var bottom = Blockly.Scrollbar.scrollbarThickness;
  191. if (this.options.hasTrashcan) {
  192. bottom = this.addTrashcan_(bottom);
  193. }
  194. if (this.options.zoomOptions && this.options.zoomOptions.controls) {
  195. bottom = this.addZoomControls_(bottom);
  196. }
  197. Blockly.bindEvent_(this.svgGroup_, 'mousedown', this, this.onMouseDown_);
  198. var thisWorkspace = this;
  199. Blockly.bindEvent_(this.svgGroup_, 'touchstart', null,
  200. function(e) {Blockly.longStart_(e, thisWorkspace);});
  201. if (this.options.zoomOptions && this.options.zoomOptions.wheel) {
  202. // Mouse-wheel.
  203. Blockly.bindEvent_(this.svgGroup_, 'wheel', this, this.onMouseWheel_);
  204. }
  205. // Determine if there needs to be a category tree, or a simple list of
  206. // blocks. This cannot be changed later, since the UI is very different.
  207. if (this.options.hasCategories) {
  208. this.toolbox_ = new Blockly.Toolbox(this);
  209. } else if (this.options.languageTree) {
  210. this.addFlyout_();
  211. }
  212. this.updateGridPattern_();
  213. this.recordDeleteAreas();
  214. return this.svgGroup_;
  215. };
  216. /**
  217. * Dispose of this workspace.
  218. * Unlink from all DOM elements to prevent memory leaks.
  219. */
  220. Blockly.WorkspaceSvg.prototype.dispose = function() {
  221. // Stop rerendering.
  222. this.rendered = false;
  223. Blockly.WorkspaceSvg.superClass_.dispose.call(this);
  224. if (this.svgGroup_) {
  225. goog.dom.removeNode(this.svgGroup_);
  226. this.svgGroup_ = null;
  227. }
  228. this.svgBlockCanvas_ = null;
  229. this.svgBubbleCanvas_ = null;
  230. if (this.toolbox_) {
  231. this.toolbox_.dispose();
  232. this.toolbox_ = null;
  233. }
  234. if (this.flyout_) {
  235. this.flyout_.dispose();
  236. this.flyout_ = null;
  237. }
  238. if (this.trashcan) {
  239. this.trashcan.dispose();
  240. this.trashcan = null;
  241. }
  242. if (this.scrollbar) {
  243. this.scrollbar.dispose();
  244. this.scrollbar = null;
  245. }
  246. if (this.zoomControls_) {
  247. this.zoomControls_.dispose();
  248. this.zoomControls_ = null;
  249. }
  250. if (!this.options.parentWorkspace) {
  251. // Top-most workspace. Dispose of the SVG too.
  252. goog.dom.removeNode(this.getParentSvg());
  253. }
  254. if (this.resizeHandlerWrapper_) {
  255. Blockly.unbindEvent_(this.resizeHandlerWrapper_);
  256. this.resizeHandlerWrapper_ = null;
  257. }
  258. };
  259. /**
  260. * Obtain a newly created block.
  261. * @param {?string} prototypeName Name of the language object containing
  262. * type-specific functions for this block.
  263. * @param {=string} opt_id Optional ID. Use this ID if provided, otherwise
  264. * create a new id.
  265. * @return {!Blockly.BlockSvg} The created block.
  266. */
  267. Blockly.WorkspaceSvg.prototype.newBlock = function(prototypeName, opt_id) {
  268. return new Blockly.BlockSvg(this, prototypeName, opt_id);
  269. };
  270. /**
  271. * Add a trashcan.
  272. * @param {number} bottom Distance from workspace bottom to bottom of trashcan.
  273. * @return {number} Distance from workspace bottom to the top of trashcan.
  274. * @private
  275. */
  276. Blockly.WorkspaceSvg.prototype.addTrashcan_ = function(bottom) {
  277. /** @type {Blockly.Trashcan} */
  278. this.trashcan = new Blockly.Trashcan(this);
  279. var svgTrashcan = this.trashcan.createDom();
  280. this.svgGroup_.insertBefore(svgTrashcan, this.svgBlockCanvas_);
  281. return this.trashcan.init(bottom);
  282. };
  283. /**
  284. * Add zoom controls.
  285. * @param {number} bottom Distance from workspace bottom to bottom of controls.
  286. * @return {number} Distance from workspace bottom to the top of controls.
  287. * @private
  288. */
  289. Blockly.WorkspaceSvg.prototype.addZoomControls_ = function(bottom) {
  290. /** @type {Blockly.ZoomControls} */
  291. this.zoomControls_ = new Blockly.ZoomControls(this);
  292. var svgZoomControls = this.zoomControls_.createDom();
  293. this.svgGroup_.appendChild(svgZoomControls);
  294. return this.zoomControls_.init(bottom);
  295. };
  296. /**
  297. * Add a flyout.
  298. * @private
  299. */
  300. Blockly.WorkspaceSvg.prototype.addFlyout_ = function() {
  301. var workspaceOptions = {
  302. disabledPatternId: this.options.disabledPatternId,
  303. parentWorkspace: this,
  304. RTL: this.RTL,
  305. horizontalLayout: this.horizontalLayout,
  306. toolboxPosition: this.options.toolboxPosition
  307. };
  308. /** @type {Blockly.Flyout} */
  309. this.flyout_ = new Blockly.Flyout(workspaceOptions);
  310. this.flyout_.autoClose = false;
  311. var svgFlyout = this.flyout_.createDom();
  312. this.svgGroup_.insertBefore(svgFlyout, this.svgBlockCanvas_);
  313. };
  314. /**
  315. * Resize the parts of the workspace that change when the workspace
  316. * contents (e.g. block positions) change. This will also scroll the
  317. * workspace contents if needed.
  318. * @package
  319. */
  320. Blockly.WorkspaceSvg.prototype.resizeContents = function() {
  321. if (this.scrollbar) {
  322. // TODO(picklesrus): Once rachel-fenichel's scrollbar refactoring
  323. // is complete, call the method that only resizes scrollbar
  324. // based on contents.
  325. this.scrollbar.resize();
  326. }
  327. this.updateInverseScreenCTM();
  328. };
  329. /**
  330. * Resize and reposition all of the workspace chrome (toolbox,
  331. * trash, scrollbars etc.)
  332. * This should be called when something changes that
  333. * requires recalculating dimensions and positions of the
  334. * trash, zoom, toolbox, etc. (e.g. window resize).
  335. */
  336. Blockly.WorkspaceSvg.prototype.resize = function() {
  337. if (this.toolbox_) {
  338. this.toolbox_.position();
  339. }
  340. if (this.flyout_) {
  341. this.flyout_.position();
  342. }
  343. if (this.trashcan) {
  344. this.trashcan.position();
  345. }
  346. if (this.zoomControls_) {
  347. this.zoomControls_.position();
  348. }
  349. if (this.scrollbar) {
  350. this.scrollbar.resize();
  351. }
  352. this.updateInverseScreenCTM();
  353. this.recordDeleteAreas();
  354. };
  355. /**
  356. * Get the SVG element that forms the drawing surface.
  357. * @return {!Element} SVG element.
  358. */
  359. Blockly.WorkspaceSvg.prototype.getCanvas = function() {
  360. return this.svgBlockCanvas_;
  361. };
  362. /**
  363. * Get the SVG element that forms the bubble surface.
  364. * @return {!SVGGElement} SVG element.
  365. */
  366. Blockly.WorkspaceSvg.prototype.getBubbleCanvas = function() {
  367. return this.svgBubbleCanvas_;
  368. };
  369. /**
  370. * Get the SVG element that contains this workspace.
  371. * @return {!Element} SVG element.
  372. */
  373. Blockly.WorkspaceSvg.prototype.getParentSvg = function() {
  374. if (this.cachedParentSvg_) {
  375. return this.cachedParentSvg_;
  376. }
  377. var element = this.svgGroup_;
  378. while (element) {
  379. if (element.tagName == 'svg') {
  380. this.cachedParentSvg_ = element;
  381. return element;
  382. }
  383. element = element.parentNode;
  384. }
  385. return null;
  386. };
  387. /**
  388. * Translate this workspace to new coordinates.
  389. * @param {number} x Horizontal translation.
  390. * @param {number} y Vertical translation.
  391. */
  392. Blockly.WorkspaceSvg.prototype.translate = function(x, y) {
  393. var translation = 'translate(' + x + ',' + y + ') ' +
  394. 'scale(' + this.scale + ')';
  395. this.svgBlockCanvas_.setAttribute('transform', translation);
  396. this.svgBubbleCanvas_.setAttribute('transform', translation);
  397. };
  398. /**
  399. * Returns the horizontal offset of the workspace.
  400. * Intended for LTR/RTL compatibility in XML.
  401. * @return {number} Width.
  402. */
  403. Blockly.WorkspaceSvg.prototype.getWidth = function() {
  404. var metrics = this.getMetrics();
  405. return metrics ? metrics.viewWidth / this.scale : 0;
  406. };
  407. /**
  408. * Toggles the visibility of the workspace.
  409. * Currently only intended for main workspace.
  410. * @param {boolean} isVisible True if workspace should be visible.
  411. */
  412. Blockly.WorkspaceSvg.prototype.setVisible = function(isVisible) {
  413. this.getParentSvg().style.display = isVisible ? 'block' : 'none';
  414. if (this.toolbox_) {
  415. // Currently does not support toolboxes in mutators.
  416. this.toolbox_.HtmlDiv.style.display = isVisible ? 'block' : 'none';
  417. }
  418. if (isVisible) {
  419. this.render();
  420. if (this.toolbox_) {
  421. this.toolbox_.position();
  422. }
  423. } else {
  424. Blockly.hideChaff(true);
  425. }
  426. };
  427. /**
  428. * Render all blocks in workspace.
  429. */
  430. Blockly.WorkspaceSvg.prototype.render = function() {
  431. // Generate list of all blocks.
  432. var blocks = this.getAllBlocks();
  433. // Render each block.
  434. for (var i = blocks.length - 1; i >= 0; i--) {
  435. blocks[i].render(false);
  436. }
  437. };
  438. /**
  439. * Turn the visual trace functionality on or off.
  440. * @param {boolean} armed True if the trace should be on.
  441. */
  442. Blockly.WorkspaceSvg.prototype.traceOn = function(armed) {
  443. this.traceOn_ = armed;
  444. if (this.traceWrapper_) {
  445. Blockly.unbindEvent_(this.traceWrapper_);
  446. this.traceWrapper_ = null;
  447. }
  448. if (armed) {
  449. this.traceWrapper_ = Blockly.bindEvent_(this.svgBlockCanvas_,
  450. 'blocklySelectChange', this, function() {this.traceOn_ = false;});
  451. }
  452. };
  453. /**
  454. * Highlight a block in the workspace.
  455. * @param {?string} id ID of block to find.
  456. */
  457. Blockly.WorkspaceSvg.prototype.highlightBlock = function(id) {
  458. if (this.traceOn_ && Blockly.dragMode_ != Blockly.DRAG_NONE) {
  459. // The blocklySelectChange event normally prevents this, but sometimes
  460. // there is a race condition on fast-executing apps.
  461. this.traceOn(false);
  462. }
  463. if (!this.traceOn_) {
  464. return;
  465. }
  466. var block = null;
  467. if (id) {
  468. block = this.getBlockById(id);
  469. if (!block) {
  470. return;
  471. }
  472. }
  473. // Temporary turn off the listener for selection changes, so that we don't
  474. // trip the monitor for detecting user activity.
  475. this.traceOn(false);
  476. // Select the current block.
  477. if (block) {
  478. block.select();
  479. } else if (Blockly.selected) {
  480. Blockly.selected.unselect();
  481. }
  482. // Restore the monitor for user activity after the selection event has fired.
  483. var thisWorkspace = this;
  484. setTimeout(function() {thisWorkspace.traceOn(true);}, 1);
  485. };
  486. /**
  487. * Paste the provided block onto the workspace.
  488. * @param {!Element} xmlBlock XML block element.
  489. */
  490. Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) {
  491. if (!this.rendered || xmlBlock.getElementsByTagName('block').length >=
  492. this.remainingCapacity()) {
  493. return;
  494. }
  495. Blockly.terminateDrag_(); // Dragging while pasting? No.
  496. Blockly.Events.disable();
  497. try {
  498. var block = Blockly.Xml.domToBlock(xmlBlock, this);
  499. // Move the duplicate to original position.
  500. var blockX = parseInt(xmlBlock.getAttribute('x'), 10);
  501. var blockY = parseInt(xmlBlock.getAttribute('y'), 10);
  502. if (!isNaN(blockX) && !isNaN(blockY)) {
  503. if (this.RTL) {
  504. blockX = -blockX;
  505. }
  506. // Offset block until not clobbering another block and not in connection
  507. // distance with neighbouring blocks.
  508. do {
  509. var collide = false;
  510. var allBlocks = this.getAllBlocks();
  511. for (var i = 0, otherBlock; otherBlock = allBlocks[i]; i++) {
  512. var otherXY = otherBlock.getRelativeToSurfaceXY();
  513. if (Math.abs(blockX - otherXY.x) <= 1 &&
  514. Math.abs(blockY - otherXY.y) <= 1) {
  515. collide = true;
  516. break;
  517. }
  518. }
  519. if (!collide) {
  520. // Check for blocks in snap range to any of its connections.
  521. var connections = block.getConnections_(false);
  522. for (var i = 0, connection; connection = connections[i]; i++) {
  523. var neighbour = connection.closest(Blockly.SNAP_RADIUS,
  524. new goog.math.Coordinate(blockX, blockY));
  525. if (neighbour.connection) {
  526. collide = true;
  527. break;
  528. }
  529. }
  530. }
  531. if (collide) {
  532. if (this.RTL) {
  533. blockX -= Blockly.SNAP_RADIUS;
  534. } else {
  535. blockX += Blockly.SNAP_RADIUS;
  536. }
  537. blockY += Blockly.SNAP_RADIUS * 2;
  538. }
  539. } while (collide);
  540. block.moveBy(blockX, blockY);
  541. }
  542. } finally {
  543. Blockly.Events.enable();
  544. }
  545. if (Blockly.Events.isEnabled() && !block.isShadow()) {
  546. Blockly.Events.fire(new Blockly.Events.Create(block));
  547. }
  548. block.select();
  549. };
  550. /**
  551. * Make a list of all the delete areas for this workspace.
  552. */
  553. Blockly.WorkspaceSvg.prototype.recordDeleteAreas = function() {
  554. if (this.trashcan) {
  555. this.deleteAreaTrash_ = this.trashcan.getClientRect();
  556. } else {
  557. this.deleteAreaTrash_ = null;
  558. }
  559. if (this.flyout_) {
  560. this.deleteAreaToolbox_ = this.flyout_.getClientRect();
  561. } else if (this.toolbox_) {
  562. this.deleteAreaToolbox_ = this.toolbox_.getClientRect();
  563. } else {
  564. this.deleteAreaToolbox_ = null;
  565. }
  566. };
  567. /**
  568. * Is the mouse event over a delete area (toolbox or non-closing flyout)?
  569. * Opens or closes the trashcan and sets the cursor as a side effect.
  570. * @param {!Event} e Mouse move event.
  571. * @return {boolean} True if event is in a delete area.
  572. */
  573. Blockly.WorkspaceSvg.prototype.isDeleteArea = function(e) {
  574. var xy = new goog.math.Coordinate(e.clientX, e.clientY);
  575. if (this.deleteAreaTrash_) {
  576. if (this.deleteAreaTrash_.contains(xy)) {
  577. this.trashcan.setOpen_(true);
  578. Blockly.Css.setCursor(Blockly.Css.Cursor.DELETE);
  579. return true;
  580. }
  581. this.trashcan.setOpen_(false);
  582. }
  583. if (this.deleteAreaToolbox_) {
  584. if (this.deleteAreaToolbox_.contains(xy)) {
  585. Blockly.Css.setCursor(Blockly.Css.Cursor.DELETE);
  586. return true;
  587. }
  588. }
  589. Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED);
  590. return false;
  591. };
  592. /**
  593. * Handle a mouse-down on SVG drawing surface.
  594. * @param {!Event} e Mouse down event.
  595. * @private
  596. */
  597. Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
  598. this.markFocused();
  599. if (Blockly.isTargetInput_(e)) {
  600. return;
  601. }
  602. Blockly.terminateDrag_(); // In case mouse-up event was lost.
  603. Blockly.hideChaff();
  604. var isTargetWorkspace = e.target && e.target.nodeName &&
  605. (e.target.nodeName.toLowerCase() == 'svg' ||
  606. e.target == this.svgBackground_);
  607. if (isTargetWorkspace && Blockly.selected && !this.options.readOnly) {
  608. // Clicking on the document clears the selection.
  609. Blockly.selected.unselect();
  610. }
  611. if (Blockly.isRightButton(e)) {
  612. // Right-click.
  613. this.showContextMenu_(e);
  614. } else if (this.scrollbar) {
  615. this.dragMode_ = Blockly.DRAG_BEGIN;
  616. // Record the current mouse position.
  617. this.startDragMouseX = e.clientX;
  618. this.startDragMouseY = e.clientY;
  619. this.startDragMetrics = this.getMetrics();
  620. this.startScrollX = this.scrollX;
  621. this.startScrollY = this.scrollY;
  622. // If this is a touch event then bind to the mouseup so workspace drag mode
  623. // is turned off and double move events are not performed on a block.
  624. // See comment in inject.js Blockly.init_ as to why mouseup events are
  625. // bound to the document instead of the SVG's surface.
  626. if ('mouseup' in Blockly.bindEvent_.TOUCH_MAP) {
  627. Blockly.onTouchUpWrapper_ = Blockly.onTouchUpWrapper_ || [];
  628. Blockly.onTouchUpWrapper_ = Blockly.onTouchUpWrapper_.concat(
  629. Blockly.bindEvent_(document, 'mouseup', null, Blockly.onMouseUp_));
  630. }
  631. Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_ || [];
  632. Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_.concat(
  633. Blockly.bindEvent_(document, 'mousemove', null, Blockly.onMouseMove_));
  634. }
  635. // This event has been handled. No need to bubble up to the document.
  636. e.stopPropagation();
  637. e.preventDefault();
  638. };
  639. /**
  640. * Start tracking a drag of an object on this workspace.
  641. * @param {!Event} e Mouse down event.
  642. * @param {!goog.math.Coordinate} xy Starting location of object.
  643. */
  644. Blockly.WorkspaceSvg.prototype.startDrag = function(e, xy) {
  645. // Record the starting offset between the bubble's location and the mouse.
  646. var point = Blockly.mouseToSvg(e, this.getParentSvg(),
  647. this.getInverseScreenCTM());
  648. // Fix scale of mouse event.
  649. point.x /= this.scale;
  650. point.y /= this.scale;
  651. this.dragDeltaXY_ = goog.math.Coordinate.difference(xy, point);
  652. };
  653. /**
  654. * Track a drag of an object on this workspace.
  655. * @param {!Event} e Mouse move event.
  656. * @return {!goog.math.Coordinate} New location of object.
  657. */
  658. Blockly.WorkspaceSvg.prototype.moveDrag = function(e) {
  659. var point = Blockly.mouseToSvg(e, this.getParentSvg(),
  660. this.getInverseScreenCTM());
  661. // Fix scale of mouse event.
  662. point.x /= this.scale;
  663. point.y /= this.scale;
  664. return goog.math.Coordinate.sum(this.dragDeltaXY_, point);
  665. };
  666. /**
  667. * Is the user currently dragging a block or scrolling the flyout/workspace?
  668. * @return {boolean} True if currently dragging or scrolling.
  669. */
  670. Blockly.WorkspaceSvg.prototype.isDragging = function() {
  671. return Blockly.dragMode_ == Blockly.DRAG_FREE ||
  672. (Blockly.Flyout.startFlyout_ &&
  673. Blockly.Flyout.startFlyout_.dragMode_ == Blockly.DRAG_FREE) ||
  674. this.dragMode_ == Blockly.DRAG_FREE;
  675. };
  676. /**
  677. * Handle a mouse-wheel on SVG drawing surface.
  678. * @param {!Event} e Mouse wheel event.
  679. * @private
  680. */
  681. Blockly.WorkspaceSvg.prototype.onMouseWheel_ = function(e) {
  682. // TODO: Remove terminateDrag and compensate for coordinate skew during zoom.
  683. Blockly.terminateDrag_();
  684. var delta = e.deltaY > 0 ? -1 : 1;
  685. var position = Blockly.mouseToSvg(e, this.getParentSvg(),
  686. this.getInverseScreenCTM());
  687. this.zoom(position.x, position.y, delta);
  688. e.preventDefault();
  689. };
  690. /**
  691. * Calculate the bounding box for the blocks on the workspace.
  692. *
  693. * @return {Object} Contains the position and size of the bounding box
  694. * containing the blocks on the workspace.
  695. */
  696. Blockly.WorkspaceSvg.prototype.getBlocksBoundingBox = function() {
  697. var topBlocks = this.getTopBlocks(false);
  698. // There are no blocks, return empty rectangle.
  699. if (!topBlocks.length) {
  700. return {x: 0, y: 0, width: 0, height: 0};
  701. }
  702. // Initialize boundary using the first block.
  703. var boundary = topBlocks[0].getBoundingRectangle();
  704. // Start at 1 since the 0th block was used for initialization
  705. for (var i = 1; i < topBlocks.length; i++) {
  706. var blockBoundary = topBlocks[i].getBoundingRectangle();
  707. if (blockBoundary.topLeft.x < boundary.topLeft.x) {
  708. boundary.topLeft.x = blockBoundary.topLeft.x;
  709. }
  710. if (blockBoundary.bottomRight.x > boundary.bottomRight.x) {
  711. boundary.bottomRight.x = blockBoundary.bottomRight.x;
  712. }
  713. if (blockBoundary.topLeft.y < boundary.topLeft.y) {
  714. boundary.topLeft.y = blockBoundary.topLeft.y;
  715. }
  716. if (blockBoundary.bottomRight.y > boundary.bottomRight.y) {
  717. boundary.bottomRight.y = blockBoundary.bottomRight.y;
  718. }
  719. }
  720. return {
  721. x: boundary.topLeft.x,
  722. y: boundary.topLeft.y,
  723. width: boundary.bottomRight.x - boundary.topLeft.x,
  724. height: boundary.bottomRight.y - boundary.topLeft.y
  725. };
  726. };
  727. /**
  728. * Clean up the workspace by ordering all the blocks in a column.
  729. * @private
  730. */
  731. Blockly.WorkspaceSvg.prototype.cleanUp_ = function() {
  732. Blockly.Events.setGroup(true);
  733. var topBlocks = this.getTopBlocks(true);
  734. var cursorY = 0;
  735. for (var i = 0, block; block = topBlocks[i]; i++) {
  736. var xy = block.getRelativeToSurfaceXY();
  737. block.moveBy(-xy.x, cursorY - xy.y);
  738. block.snapToGrid();
  739. cursorY = block.getRelativeToSurfaceXY().y +
  740. block.getHeightWidth().height + Blockly.BlockSvg.MIN_BLOCK_Y;
  741. }
  742. Blockly.Events.setGroup(false);
  743. // Fire an event to allow scrollbars to resize.
  744. Blockly.resizeSvgContents(this);
  745. };
  746. /**
  747. * Show the context menu for the workspace.
  748. * @param {!Event} e Mouse event.
  749. * @private
  750. */
  751. Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
  752. if (this.options.readOnly || this.isFlyout) {
  753. return;
  754. }
  755. var menuOptions = [];
  756. var topBlocks = this.getTopBlocks(true);
  757. var eventGroup = Blockly.genUid();
  758. // Options to undo/redo previous action.
  759. var undoOption = {};
  760. undoOption.text = Blockly.Msg.UNDO;
  761. undoOption.enabled = this.undoStack_.length > 0;
  762. undoOption.callback = this.undo.bind(this, false);
  763. menuOptions.push(undoOption);
  764. var redoOption = {};
  765. redoOption.text = Blockly.Msg.REDO;
  766. redoOption.enabled = this.redoStack_.length > 0;
  767. redoOption.callback = this.undo.bind(this, true);
  768. menuOptions.push(redoOption);
  769. // Option to clean up blocks.
  770. if (this.scrollbar) {
  771. var cleanOption = {};
  772. cleanOption.text = Blockly.Msg.CLEAN_UP;
  773. cleanOption.enabled = topBlocks.length > 1;
  774. cleanOption.callback = this.cleanUp_.bind(this);
  775. menuOptions.push(cleanOption);
  776. }
  777. // Add a little animation to collapsing and expanding.
  778. var DELAY = 10;
  779. if (this.options.collapse) {
  780. var hasCollapsedBlocks = false;
  781. var hasExpandedBlocks = false;
  782. for (var i = 0; i < topBlocks.length; i++) {
  783. var block = topBlocks[i];
  784. while (block) {
  785. if (block.isCollapsed()) {
  786. hasCollapsedBlocks = true;
  787. } else {
  788. hasExpandedBlocks = true;
  789. }
  790. block = block.getNextBlock();
  791. }
  792. }
  793. /**
  794. * Option to collapse or expand top blocks.
  795. * @param {boolean} shouldCollapse Whether a block should collapse.
  796. * @private
  797. */
  798. var toggleOption = function(shouldCollapse) {
  799. var ms = 0;
  800. for (var i = 0; i < topBlocks.length; i++) {
  801. var block = topBlocks[i];
  802. while (block) {
  803. setTimeout(block.setCollapsed.bind(block, shouldCollapse), ms);
  804. block = block.getNextBlock();
  805. ms += DELAY;
  806. }
  807. }
  808. };
  809. // Option to collapse top blocks.
  810. var collapseOption = {enabled: hasExpandedBlocks};
  811. collapseOption.text = Blockly.Msg.COLLAPSE_ALL;
  812. collapseOption.callback = function() {
  813. toggleOption(true);
  814. };
  815. menuOptions.push(collapseOption);
  816. // Option to expand top blocks.
  817. var expandOption = {enabled: hasCollapsedBlocks};
  818. expandOption.text = Blockly.Msg.EXPAND_ALL;
  819. expandOption.callback = function() {
  820. toggleOption(false);
  821. };
  822. menuOptions.push(expandOption);
  823. }
  824. // for BlocksCAD
  825. if (Blockly.backlight.length) {
  826. // Option to unbacklight all backlit blocks. For BlocksCAD.
  827. var unbacklightOption = {enabled: true};
  828. unbacklightOption.text = Blockscad.Msg.REMOVE_BLOCK_HIGHLIGHTING;
  829. unbacklightOption.callback = function() {
  830. Blockscad.workspace.clearBacklight();
  831. };
  832. menuOptions.push(unbacklightOption);
  833. }
  834. // I'm going to try to add "disable all" and "enable all" options on the context menu.
  835. // for now I'm not going to detect if the options are "available".
  836. if (this.options.disable) {
  837. var hasDisabledBlocks = false;
  838. var hasEnabledBlocks = false;
  839. var topBlocks = this.getTopBlocks(false);
  840. for (var i = 0; i < topBlocks.length; i++) {
  841. var block = topBlocks[i];
  842. while (block) {
  843. if (block.disabled) {
  844. hasDisabledBlocks = true;
  845. } else {
  846. hasEnabledBlocks = true;
  847. }
  848. block = block.getNextBlock();
  849. }
  850. }
  851. var disableAllOption = {enabled: hasEnabledBlocks};
  852. disableAllOption.text = Blockscad.Msg.DISABLE_ALL;
  853. disableAllOption.callback = function() {
  854. for (var i = 0; i < topBlocks.length; i++) {
  855. var block = topBlocks[i];
  856. while (block) {
  857. // I don't want to disable procedure definitions!
  858. if (block.category != 'PROCEDURE')
  859. block.setDisabled(true);
  860. block = block.getNextBlock();
  861. }
  862. }
  863. }
  864. menuOptions.push(disableAllOption);
  865. var enableAllOption = {enabled: hasDisabledBlocks};
  866. enableAllOption.text = Blockscad.Msg.ENABLE_ALL;
  867. enableAllOption.callback = function() {
  868. for (var i = 0; i < topBlocks.length; i++) {
  869. var block = topBlocks[i];
  870. while (block) {
  871. block.setDisabled(false);
  872. block = block.getNextBlock();
  873. }
  874. // should I call an update to disable orphans after this?
  875. }
  876. }
  877. menuOptions.push(enableAllOption);
  878. }
  879. // Option to delete all blocks.
  880. // Count the number of blocks that are deletable.
  881. var deleteList = [];
  882. function addDeletableBlocks(block) {
  883. if (block.isDeletable()) {
  884. deleteList = deleteList.concat(block.getDescendants());
  885. } else {
  886. var children = block.getChildren();
  887. for (var i = 0; i < children.length; i++) {
  888. addDeletableBlocks(children[i]);
  889. }
  890. }
  891. }
  892. for (var i = 0; i < topBlocks.length; i++) {
  893. addDeletableBlocks(topBlocks[i]);
  894. }
  895. function deleteNext() {
  896. Blockly.Events.setGroup(eventGroup);
  897. var block = deleteList.shift();
  898. if (block) {
  899. if (block.workspace) {
  900. block.dispose(false, true);
  901. setTimeout(deleteNext, DELAY);
  902. } else {
  903. deleteNext();
  904. }
  905. }
  906. Blockly.Events.setGroup(false);
  907. }
  908. var deleteOption = {
  909. text: deleteList.length == 1 ? Blockly.Msg.DELETE_BLOCK :
  910. Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(deleteList.length)),
  911. enabled: deleteList.length > 0,
  912. callback: function() {
  913. // use BlocksCAD's nice dialog to delete all.
  914. Blockscad.discard();
  915. // if (deleteList.length < 2 ||
  916. // window.confirm(Blockly.Msg.DELETE_ALL_BLOCKS.replace('%1',
  917. // String(deleteList.length)))) {
  918. // deleteNext();
  919. // }
  920. }
  921. };
  922. menuOptions.push(deleteOption);
  923. Blockly.ContextMenu.show(e, menuOptions, this.RTL);
  924. };
  925. /**
  926. * Load an audio file. Cache it, ready for instantaneous playing.
  927. * @param {!Array.<string>} filenames List of file types in decreasing order of
  928. * preference (i.e. increasing size). E.g. ['media/go.mp3', 'media/go.wav']
  929. * Filenames include path from Blockly's root. File extensions matter.
  930. * @param {string} name Name of sound.
  931. * @private
  932. */
  933. Blockly.WorkspaceSvg.prototype.loadAudio_ = function(filenames, name) {
  934. if (!filenames.length) {
  935. return;
  936. }
  937. try {
  938. var audioTest = new window['Audio']();
  939. } catch (e) {
  940. // No browser support for Audio.
  941. // IE can throw an error even if the Audio object exists.
  942. return;
  943. }
  944. var sound;
  945. for (var i = 0; i < filenames.length; i++) {
  946. var filename = filenames[i];
  947. var ext = filename.match(/\.(\w+)$/);
  948. if (ext && audioTest.canPlayType('audio/' + ext[1])) {
  949. // Found an audio format we can play.
  950. sound = new window['Audio'](filename);
  951. break;
  952. }
  953. }
  954. if (sound && sound.play) {
  955. this.SOUNDS_[name] = sound;
  956. }
  957. };
  958. /**
  959. * Preload all the audio files so that they play quickly when asked for.
  960. * @private
  961. */
  962. Blockly.WorkspaceSvg.prototype.preloadAudio_ = function() {
  963. for (var name in this.SOUNDS_) {
  964. var sound = this.SOUNDS_[name];
  965. sound.volume = .01;
  966. sound.play();
  967. sound.pause();
  968. // iOS can only process one sound at a time. Trying to load more than one
  969. // corrupts the earlier ones. Just load one and leave the others uncached.
  970. if (goog.userAgent.IPAD || goog.userAgent.IPHONE) {
  971. break;
  972. }
  973. }
  974. };
  975. /**
  976. * Play a named sound at specified volume. If volume is not specified,
  977. * use full volume (1).
  978. * @param {string} name Name of sound.
  979. * @param {number=} opt_volume Volume of sound (0-1).
  980. */
  981. Blockly.WorkspaceSvg.prototype.playAudio = function(name, opt_volume) {
  982. var sound = this.SOUNDS_[name];
  983. if (sound) {
  984. // Don't play one sound on top of another.
  985. var now = new Date;
  986. if (now - this.lastSound_ < Blockly.SOUND_LIMIT) {
  987. return;
  988. }
  989. this.lastSound_ = now;
  990. var mySound;
  991. var ie9 = goog.userAgent.DOCUMENT_MODE &&
  992. goog.userAgent.DOCUMENT_MODE === 9;
  993. if (ie9 || goog.userAgent.IPAD || goog.userAgent.ANDROID) {
  994. // Creating a new audio node causes lag in IE9, Android and iPad. Android
  995. // and IE9 refetch the file from the server, iPad uses a singleton audio
  996. // node which must be deleted and recreated for each new audio tag.
  997. mySound = sound;
  998. } else {
  999. mySound = sound.cloneNode();
  1000. }
  1001. }
  1002. else {
  1003. // can I play audio directly? needed for standalone.
  1004. var audio = document.getElementById("audio_" + name);
  1005. // console.log("trying to play ")
  1006. // console.log(audio);
  1007. audio.play();
  1008. }
  1009. };
  1010. /**
  1011. * Modify the block tree on the existing toolbox.
  1012. * @param {Node|string} tree DOM tree of blocks, or text representation of same.
  1013. */
  1014. Blockly.WorkspaceSvg.prototype.updateToolbox = function(tree) {
  1015. tree = Blockly.Options.parseToolboxTree(tree);
  1016. if (!tree) {
  1017. if (this.options.languageTree) {
  1018. throw 'Can\'t nullify an existing toolbox.';
  1019. }
  1020. return; // No change (null to null).
  1021. }
  1022. if (!this.options.languageTree) {
  1023. throw 'Existing toolbox is null. Can\'t create new toolbox.';
  1024. }
  1025. if (tree.getElementsByTagName('category').length) {
  1026. if (!this.toolbox_) {
  1027. throw 'Existing toolbox has no categories. Can\'t change mode.';
  1028. }
  1029. this.options.languageTree = tree;
  1030. this.toolbox_.populate_(tree);
  1031. this.toolbox_.addColour_();
  1032. } else {
  1033. if (!this.flyout_) {
  1034. throw 'Existing toolbox has categories. Can\'t change mode.';
  1035. }
  1036. this.options.languageTree = tree;
  1037. this.flyout_.show(tree.childNodes);
  1038. }
  1039. };
  1040. /**
  1041. * Mark this workspace as the currently focused main workspace.
  1042. */
  1043. Blockly.WorkspaceSvg.prototype.markFocused = function() {
  1044. if (this.options.parentWorkspace) {
  1045. this.options.parentWorkspace.markFocused();
  1046. } else {
  1047. Blockly.mainWorkspace = this;
  1048. }
  1049. };
  1050. /**
  1051. * Zooming the blocks centered in (x, y) coordinate with zooming in or out.
  1052. * @param {number} x X coordinate of center.
  1053. * @param {number} y Y coordinate of center.
  1054. * @param {number} type Type of zooming (-1 zooming out and 1 zooming in).
  1055. */
  1056. Blockly.WorkspaceSvg.prototype.zoom = function(x, y, type) {
  1057. var speed = this.options.zoomOptions.scaleSpeed;
  1058. var metrics = this.getMetrics();
  1059. var center = this.getParentSvg().createSVGPoint();
  1060. center.x = x;
  1061. center.y = y;
  1062. center = center.matrixTransform(this.getCanvas().getCTM().inverse());
  1063. x = center.x;
  1064. y = center.y;
  1065. var canvas = this.getCanvas();
  1066. // Scale factor.
  1067. var scaleChange = (type == 1) ? speed : 1 / speed;
  1068. // Clamp scale within valid range.
  1069. var newScale = this.scale * scaleChange;
  1070. if (newScale > this.options.zoomOptions.maxScale) {
  1071. scaleChange = this.options.zoomOptions.maxScale / this.scale;
  1072. } else if (newScale < this.options.zoomOptions.minScale) {
  1073. scaleChange = this.options.zoomOptions.minScale / this.scale;
  1074. }
  1075. if (this.scale == newScale) {
  1076. return; // No change in zoom.
  1077. }
  1078. if (this.scrollbar) {
  1079. var matrix = canvas.getCTM()
  1080. .translate(x * (1 - scaleChange), y * (1 - scaleChange))
  1081. .scale(scaleChange);
  1082. // newScale and matrix.a should be identical (within a rounding error).
  1083. this.scrollX = matrix.e - metrics.absoluteLeft;
  1084. this.scrollY = matrix.f - metrics.absoluteTop;
  1085. }
  1086. this.setScale(newScale);
  1087. };
  1088. /**
  1089. * Zooming the blocks centered in the center of view with zooming in or out.
  1090. * @param {number} type Type of zooming (-1 zooming out and 1 zooming in).
  1091. */
  1092. Blockly.WorkspaceSvg.prototype.zoomCenter = function(type) {
  1093. var metrics = this.getMetrics();
  1094. var x = metrics.viewWidth / 2;
  1095. var y = metrics.viewHeight / 2;
  1096. this.zoom(x, y, type);
  1097. };
  1098. /**
  1099. * Zoom the blocks to fit in the workspace if possible.
  1100. */
  1101. Blockly.WorkspaceSvg.prototype.zoomToFit = function() {
  1102. var metrics = this.getMetrics();
  1103. var blocksBox = this.getBlocksBoundingBox();
  1104. var blocksWidth = blocksBox.width;
  1105. var blocksHeight = blocksBox.height;
  1106. if (!blocksWidth) {
  1107. return; // Prevents zooming to infinity.
  1108. }
  1109. var workspaceWidth = metrics.viewWidth;
  1110. var workspaceHeight = metrics.viewHeight;
  1111. if (this.flyout_) {
  1112. workspaceWidth -= this.flyout_.width_;
  1113. }
  1114. if (!this.scrollbar) {
  1115. // Orgin point of 0,0 is fixed, blocks will not scroll to center.
  1116. blocksWidth += metrics.contentLeft;
  1117. blocksHeight += metrics.contentTop;
  1118. }
  1119. var ratioX = workspaceWidth / blocksWidth;
  1120. var ratioY = workspaceHeight / blocksHeight;
  1121. this.setScale(Math.min(ratioX, ratioY));
  1122. this.scrollCenter();
  1123. };
  1124. /**
  1125. * Center the workspace.
  1126. */
  1127. Blockly.WorkspaceSvg.prototype.scrollCenter = function() {
  1128. if (!this.scrollbar) {
  1129. // Can't center a non-scrolling workspace.
  1130. return;
  1131. }
  1132. var metrics = this.getMetrics();
  1133. var x = (metrics.contentWidth - metrics.viewWidth) / 2;
  1134. if (this.flyout_) {
  1135. x -= this.flyout_.width_ / 2;
  1136. }
  1137. var y = (metrics.contentHeight - metrics.viewHeight) / 2;
  1138. this.scrollbar.set(x, y);
  1139. };
  1140. /**
  1141. * Set the workspace's zoom factor.
  1142. * @param {number} newScale Zoom factor.
  1143. */
  1144. Blockly.WorkspaceSvg.prototype.setScale = function(newScale) {
  1145. if (this.options.zoomOptions.maxScale &&
  1146. newScale > this.options.zoomOptions.maxScale) {
  1147. newScale = this.options.zoomOptions.maxScale;
  1148. } else if (this.options.zoomOptions.minScale &&
  1149. newScale < this.options.zoomOptions.minScale) {
  1150. newScale = this.options.zoomOptions.minScale;
  1151. }
  1152. this.scale = newScale;
  1153. this.updateGridPattern_();
  1154. if (this.scrollbar) {
  1155. this.scrollbar.resize();
  1156. } else {
  1157. this.translate(this.scrollX, this.scrollY);
  1158. }
  1159. Blockly.hideChaff(false);
  1160. if (this.flyout_) {
  1161. // No toolbox, resize flyout.
  1162. this.flyout_.reflow();
  1163. }
  1164. };
  1165. /**
  1166. * Updates the grid pattern.
  1167. * @private
  1168. */
  1169. Blockly.WorkspaceSvg.prototype.updateGridPattern_ = function() {
  1170. if (!this.options.gridPattern) {
  1171. return; // No grid.
  1172. }
  1173. // MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100.
  1174. var safeSpacing = (this.options.gridOptions['spacing'] * this.scale) || 100;
  1175. this.options.gridPattern.setAttribute('width', safeSpacing);
  1176. this.options.gridPattern.setAttribute('height', safeSpacing);
  1177. var half = Math.floor(this.options.gridOptions['spacing'] / 2) + 0.5;
  1178. var start = half - this.options.gridOptions['length'] / 2;
  1179. var end = half + this.options.gridOptions['length'] / 2;
  1180. var line1 = this.options.gridPattern.firstChild;
  1181. var line2 = line1 && line1.nextSibling;
  1182. half *= this.scale;
  1183. start *= this.scale;
  1184. end *= this.scale;
  1185. if (line1) {
  1186. line1.setAttribute('stroke-width', this.scale);
  1187. line1.setAttribute('x1', start);
  1188. line1.setAttribute('y1', half);
  1189. line1.setAttribute('x2', end);
  1190. line1.setAttribute('y2', half);
  1191. }
  1192. if (line2) {
  1193. line2.setAttribute('stroke-width', this.scale);
  1194. line2.setAttribute('x1', half);
  1195. line2.setAttribute('y1', start);
  1196. line2.setAttribute('x2', half);
  1197. line2.setAttribute('y2', end);
  1198. }
  1199. };
  1200. /*
  1201. * Clears backlight from all blocks in backlight list for the workspace. For BlocksCAD.
  1202. */
  1203. Blockly.WorkspaceSvg.prototype.clearBacklight = function() {
  1204. while (Blockly.backlight.length) {
  1205. var block = this.getBlockById(Blockly.backlight[0]);
  1206. block && block.unbacklight();
  1207. }
  1208. }
  1209. // Export symbols that would otherwise be renamed by Closure compiler.
  1210. Blockly.WorkspaceSvg.prototype['setVisible'] =
  1211. Blockly.WorkspaceSvg.prototype.setVisible;