From 49476388e34561bba1d687a9a22998395ba07747 Mon Sep 17 00:00:00 2001 From: Tan YuGin Date: Thu, 18 Apr 2019 19:47:00 +0800 Subject: [PATCH 1/2] Env visualiser --- .../externalLibs/env_visualizer/ConcreteJs.js | 577 ++++++++++ .../env_visualizer/visualizer - Copy.js | 890 ++++++++++++++ .../externalLibs/env_visualizer/visualizer.js | 1022 +++++++++++++++++ public/externalLibs/index.js | 7 +- src/components/Playground.tsx | 12 +- .../workspace/side-content/EnvVisualizer.tsx | 17 + src/sagas/index.ts | 5 +- src/utils/slangHelper.ts | 8 + 8 files changed, 2532 insertions(+), 6 deletions(-) create mode 100644 public/externalLibs/env_visualizer/ConcreteJs.js create mode 100644 public/externalLibs/env_visualizer/visualizer - Copy.js create mode 100644 public/externalLibs/env_visualizer/visualizer.js create mode 100644 src/components/workspace/side-content/EnvVisualizer.tsx diff --git a/public/externalLibs/env_visualizer/ConcreteJs.js b/public/externalLibs/env_visualizer/ConcreteJs.js new file mode 100644 index 0000000000..7adf215dd1 --- /dev/null +++ b/public/externalLibs/env_visualizer/ConcreteJs.js @@ -0,0 +1,577 @@ +/* + * Concrete v3.0.2 + * A lightweight Html5 Canvas framework that enables hit detection, layering, multi buffering, + * pixel ratio management, exports, and image downloads + * Release Date: 11-20-2018 + * https://github.com/ericdrowell/concrete + * Licensed under the MIT or GPL Version 2 licenses. + * + * Copyright (C) 2018 Eric Rowell @ericdrowell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var Concrete = {}, + idCounter = 0; + +Concrete.PIXEL_RATIO = (function() { + // client browsers + if (window && window.navigator && window.navigator.userAgent && !/PhantomJS/.test(window.navigator.userAgent)) { + return 2; + } + // headless browsers + else { + return 1; + } +})(); + +Concrete.viewports = []; + +////////////////////////////////////////////////////////////// VIEWPORT ////////////////////////////////////////////////////////////// + +/** + * Concrete Viewport constructor + * @param {Object} config + * @param {Integer} config.width - viewport width in pixels + * @param {Integer} config.height - viewport height in pixels + */ +Concrete.Viewport = function(config) { + if (!config) { + config = {}; + } + + this.container = config.container; + this.layers = []; + this.id = idCounter++; + this.scene = new Concrete.Scene(); + + this.setSize(config.width || 0, config.height || 0); + + + // clear container + config.container.innerHTML = ''; + config.container.appendChild(this.scene.canvas); + + Concrete.viewports.push(this); +}; + +Concrete.Viewport.prototype = { + /** + * add layer + * @param {Concrete.Layer} layer + * @returns {Concrete.Viewport} + */ + add: function(layer) { + this.layers.push(layer); + layer.setSize(layer.width || this.width, layer.height || this.height); + layer.viewport = this; + return this; + }, + /** + * set viewport size + * @param {Integer} width - viewport width in pixels + * @param {Integer} height - viewport height in pixels + * @returns {Concrete.Viewport} + */ + setSize: function(width, height) { + this.width = width; + this.height = height; + this.scene.setSize(width, height); + return this; + }, + /** + * get key associated to coordinate. This can be used for mouse interactivity. + * @param {Number} x + * @param {Number} y + * @returns {Integer} integer - returns -1 if no pixel is there + */ + getIntersection: function(x, y) { + var layers = this.layers, + len = layers.length, + n, layer, key; + + for (n=len-1; n>=0; n--) { + layer = layers[n]; + key = layer.hit.getIntersection(x, y); + if (key >= 0) { + return key; + } + } + + return -1; + }, + /** + * get viewport index from all Concrete viewports + * @returns {Integer} + */ + getIndex: function() { + var viewports = Concrete.viewports, + len = viewports.length, + n = 0, + viewport; + + for (n=0; n 0) { + // swap + layers[index] = layers[index-1]; + layers[index-1] = this; + } + + return this; + }, + /** + * move to top + * @returns {Concrete.Layer} + */ + moveToTop: function() { + var index = this.getIndex(), + viewport = this.viewport, + layers = viewport.layers; + + layers.splice(index, 1); + layers.push(this); + }, + /** + * move to bottom + * @returns {Concrete.Layer} + */ + moveToBottom: function() { + var index = this.getIndex(), + viewport = this.viewport, + layers = viewport.layers; + + layers.splice(index, 1); + layers.unshift(this); + + return this; + }, + /** + * get layer index from viewport layers + * @returns {Number|null} + */ + getIndex: function() { + var layers = this.viewport.layers, + len = layers.length, + n = 0, + layer; + + for (n=0; n} + */ + intToRGB: function(number) { + var r = (number & 0xff0000) >> 16; + var g = (number & 0x00ff00) >> 8; + var b = (number & 0x0000ff); + return [r, g, b]; + }, +}; + + +// export +(function (global) { + 'use strict'; + + // AMD support + if (typeof define === 'function' && define.amd) { + define(function () { return Concrete; }); + // CommonJS and Node.js module support. + } else if (typeof exports !== 'undefined') { + // Support Node.js specific `module.exports` (which can be a function) + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = Concrete; + } + // But always support CommonJS module 1.1.1 spec (`exports` cannot be a function) + exports.Concrete = Concrete; + } else { + global.Concrete = Concrete; + } +})(this); \ No newline at end of file diff --git a/public/externalLibs/env_visualizer/visualizer - Copy.js b/public/externalLibs/env_visualizer/visualizer - Copy.js new file mode 100644 index 0000000000..d59ac6e3b8 --- /dev/null +++ b/public/externalLibs/env_visualizer/visualizer - Copy.js @@ -0,0 +1,890 @@ +(function(exports) { + /** + * Setup Stage + */ + var stage + var container = document.createElement('div') + container.id = 'env-visualizer-container' + container.hidden = true + document.body.appendChild(container) + var drawDataButton = document.createElement('div') + drawDataButton.id = 'draw-data-button' + drawDataButton.hidden = true + document.body.appendChild(drawDataButton) + + const frameFontSetting = "14px Roboto Mono"; + const fnRadius = 12; + var builtins = [ + 'runtime', + 'display', + 'raw_display', + 'stringify', + 'error', + 'prompt', + 'is_number', + 'is_string', + 'is_function', + 'is_boolean', + 'is_undefined', + 'parse_int', + 'undefined', + 'NaN', + 'Infinity', + 'null', + 'pair', + 'is_pair', + 'head', + 'tail', + 'is_null', + 'is_list', + 'list', + 'length', + 'map', + 'build_list', + 'for_each', + 'list_to_string', + 'reverse', + 'append', + 'member', + 'remove', + 'remove_all', + 'filter', + 'enum_list', + 'list_ref', + 'accumulate', + 'equal', + 'draw_data', + 'set_head', + 'set_tail', + 'array_length', + 'is_array', + 'parse', + 'apply_in_underlying_javascript', + 'is_object', + 'is_NaN', + 'has_own_property', + 'alert', + 'timed', + 'assoc', + 'rawDisplay', + 'prompt', + 'alert', + 'visualiseList', + 'math_abs', + 'math_acos', + 'math_acosh', + 'math_asin', + 'math_asinh', + 'math_atan', + 'math_atanh', + 'math_atan2', + 'math_ceil', + 'math_cbrt', + 'math_expm1', + 'math_clz32', + 'math_cos', + 'math_cosh', + 'math_exp', + 'math_floor', + 'math_fround', + 'math_hypot', + 'math_imul', + 'math_log', + 'math_log1p', + 'math_log2', + 'math_log10', + 'math_max', + 'math_min', + 'math_pow', + 'math_random', + 'math_round', + 'math_sign', + 'math_sin', + 'math_sinh', + 'math_sqrt', + 'math_tan', + 'math_tanh', + 'math_trunc', + 'math_E', + 'math_LN10', + 'math_LN2', + 'math_LOG10E', + 'math_LOG2E', + 'math_PI', + 'math_SQRT1_2', + 'math_SQRT2' + ] + + function drawSceneFnObjects() { + fnObjectLayer.scene.clear(); + for (let i = 0; i < fnObjects.length; i++) { + drawSceneFnObject(i); + } + viewport.render(); + } + + function drawHitFnObjects() { + for (let i = 0; i < fnObjects.length; i++) { + drawHitFnObject(i); + } + } + + function drawSceneDataObjects() { + dataObjectLayer.scene.clear(); + dataObjects.forEach(function(dataObject) { + drawSceneDataObject(dataObject); + }); + viewport.render(); + } + + function drawHitDataObjects() { + dataObjects.forEach(function(dataObject) { + drawHitDataObject(dataObject); + }); + } + + function drawSceneFrames() { + for (let i = 0; i < frames.length; i++) { + drawSceneFrame(i); + } + viewport.render(); + } + + function drawHitFrames() { + frames.forEach(function(frame) { + drawHitFrame(frame); + }); + } + + /* + For each frame, find params which have a function as their value. + For each such param, draw the arrow. + */ + function drawSceneFrameObjectArrows() { + frames.forEach(function(frame) { + let variables = frame.variables; + for (let i = 0; i < variables.length; i++) { + if (typeof(variables[i]) == "function") { + var fn = getFnObjectFromKey(variables[i].key); + drawSceneFrameFnArrow(frame, fn, i); + } else if (typeof(variables[i]) == "object") { + drawSceneFrameDataArrow(frame, i); + } + }; + }); + viewport.render(); + } + + function drawSceneFnFrameArrows() { + fnObjects.forEach(function(fnObject) { + drawSceneFnFrameArrow(fnObject); + }); + viewport.render(); + } + + function drawSceneFrameArrows() { + frames.forEach(function(frame) { + drawSceneFrameArrow(frame); + }); + viewport.render(); + } + + function drawSceneFnObject(pos) { + var config = fnObjects[pos]; + var scene = config.layer.scene, + context = scene.context; + + // find index position of function object within frame + var parent = config.parent; + // var offset = parent.fnObjects.indexOf(config.key); + var offset = config.offset; + var x = parent.x + parent.width + 60; + var y = parent.y + 25 + offset * 30; + config.x = x; + config.y = y; + config.offset = offset; + context.beginPath(); + context.arc(x - fnRadius, y, fnRadius, 0, Math.PI*2, false); + + if (!config.hovered && !config.selected) { + context.strokeStyle = '#999999'; + context.lineWidth = 2; + context.stroke(); + } else { + context.strokeStyle = 'green'; + context.lineWidth = 2; + context.stroke(); + } + + context.beginPath(); + if (config.selected) { + context.font = "14px Roboto Mono Light"; + context.fillStyle = 'white'; + const fnString = config.fun.toString(); + const params = fnString.substring(fnString.indexOf('(') + 1, + fnString.indexOf(')')); + let body = fnString.substring(fnString.indexOf(')') + 1); + body = body.split("\n"); + context.fillText(`params: ${params == "" ? "(none)" : params}`, x + 50, y); + context.fillText(`body:`, x + 50, y + 20); + let i = 0; + while (i < 5 && i < body.length) { + context.fillText(body[i], x + 100, y + 20 * (i + 1)); + i++; + } + if (i < body.length) { + context.fillText("...", x + 120, y + 120); + } + } + context.arc(x + fnRadius, y, fnRadius, 0, Math.PI*2, false); + if (!config.hovered && !config.selected) { + context.strokeStyle = '#999999'; + context.lineWidth = 2; + context.stroke(); + } else { + context.strokeStyle = 'green'; + context.lineWidth = 2; + context.stroke(); + } + + } + + function drawHitFnObject(pos) { + var config = fnObjects[pos]; + var hit = config.layer.hit, + context = hit.context; + var parent = config.parent; + // var offset = parent.fnObjects.indexOf(config.key); + var offset = config.offset; + var x = parent.x + parent.width + 60; + var y = parent.y + 25 + offset * 30; + config.x = x; + config.y = y; + config.offset = offset; + context.save(); + context.beginPath(); + context.arc(x - fnRadius, y, fnRadius, 0, Math.PI*2, false); + context.fillStyle = hit.getColorFromIndex(config.key); + context.fill(); + context.restore(); + + context.beginPath(); + context.arc(x + fnRadius, y, fnRadius, 0, Math.PI*2, false); + context.fillStyle = hit.getColorFromIndex(config.key); + context.fill(); + context.restore(); + } + + function drawSceneDataObject(dataObject) { + var config = dataObject; + var scene = config.layer.scene, + context = scene.context; + var parent = config.parent; + var offset = config.offset; + const x0 = parent.x + parent.width + 48, + y0 = parent.y + 25 + offset * 30 - 12; + const x1 = x0 - 12, + y1 = y0 + 24; + const x2 = x1 + 24, + y2 = y1; + config.offset = offset; + context.beginPath(); + context.moveTo(x0, y0); + context.lineTo(x1, y1); + context.lineTo(x2, y2); + context.lineTo(x0, y0); + context.fillStyle = '#999999'; + context.font = "14px Roboto Mono Light"; + context.fillText("!", x0 - 4, y0 + 20); + + if (!config.hovered && !config.selected) { + context.strokeStyle = '#999999'; + context.lineWidth = 2; + context.stroke(); + } else { + context.strokeStyle = 'green'; + context.lineWidth = 2; + context.stroke(); + } + } + + function drawHitDataObject(dataObject) { + var config = dataObject; + var hit = dataObjectLayer.hit, + context = hit.context; + var parent = config.parent; + var offset = config.offset; + const x0 = parent.x + parent.width + 48, + y0 = parent.y + 25 + offset * 30 - 12; + const x1 = x0 - 12, + y1 = y0 + 24; + const x2 = x1 + 24, + y2 = y1; + context.save(); + context.beginPath(); + context.moveTo(x0, y0); + context.lineTo(x1, y1); + context.lineTo(x2, y2); + context.lineTo(x0, y0); + context.fillStyle = hit.getColorFromIndex(config.key); + context.fill(); + context.restore(); + } + + function drawSceneFrame(pos) { + var config = frames[pos]; + var scene = config.layer.scene, + context = scene.context; + context.save(); + context.font = frameFontSetting; + context.fillStyle = "white"; + var x, y; + x = config.x; + y = config.y; + context.beginPath(); + + let env = config.head; + let i = 0; + for (let k in env) { + if (builtins.indexOf(''+k) < 0) { + if (typeof(env[k]) == "number" + || typeof(env[k]) == "string") { + context.fillText(`${'' + k}: ${'' +env[k]}`, x + 10, y + 30 + (i * 30)); + } else if (typeof(env[k]) == "object") { + context.fillText(`${'' + k}:`, x + 10, y + 30 + (i * 30)); + } else { + context.fillText(`${'' + k}:`, x + 10, y + 30 + (i * 30)); + } + i++; + } + } + + context.rect(x, y, config.width, config.height); + context.lineWidth = 2; + context.strokeStyle = 'white'; + context.stroke(); + + if (config.selected) { + context.strokeStyle = 'white'; + context.lineWidth = 6; + context.stroke(); + } + + if (config.hovered) { + context.strokeStyle = 'green'; + context.lineWidth = 2; + context.stroke(); + } + } + + function drawHitFrame(config) { + var hit = config.layer.hit, + context = hit.context; + + var x, y; + if (config.tail != null) { + x = frames[config.tail].x; + y = frames[config.tail].y + 200; + config.x = x; + config.y = y; + } else { + x = config.x; + y = config.y; + } + + context.beginPath(); + context.rect(x, y, 150, 100); + context.fillStyle = hit.getColorFromIndex(config.key); + context.save(); + context.fill(); + context.restore(); + } + + function drawSceneFrameDataArrow(frame, frameOffset) { + var scene = arrowLayer.scene, + context = scene.context; + const x0 = frame.x + frame.variables[frameOffset].name.length * 10 + 20, + y0 = frame.y + frameOffset * 30 + 25, + xf = frame.x + frame.width + 35, + yf = y0; + context.save(); + context.strokeStyle = "#999999"; + context.beginPath(); + context.moveTo(x0, y0); + context.lineTo(xf, yf); + drawArrowHead(context, x0, y0, xf, yf); + context.stroke(); + } + + function drawSceneFrameFnArrow(frame, fnObject, frameOffset) { + var scene = arrowLayer.scene, + context = scene.context; + const x0 = frame.x + frame.variables[frameOffset].functionName.length * 10 + 20, + y0 = frame.y + frameOffset * 30 + 25, + yf = fnObject.y; + //scene.clear(); + context.save(); + context.strokeStyle = "#999999"; + context.beginPath(); + + if (fnObject.parent == frame) { + // fnObject belongs to current frame + // simply draw straight arrow from frame to function + const xf = fnObject.x - (fnRadius * 2) - 3; // left circle + context.moveTo(x0, y0); + context.lineTo(xf, yf); + + // draw arrow head + drawArrowHead(context, x0, y0, xf, yf); + context.stroke(); + } else { + // fnObject belongs to different frame + + // fnOffset: relative position of target fnObject at target frame + const fnOffset = fnObject.offset; + const xf = fnObject.x + (fnRadius * 2) + 3, + x1 = x0 + 70 + frameOffset * 20, + y1 = y0; + const x2 = x1, + y2 = frame.y - 20 + fnOffset * 5; + let x3 = xf + 10 + fnOffset * 5, + y3 = y2; + /* From this position, the arrow needs to move upward to reach the + target fnObject. Make sure it does not intersect any other object + when doing so, or adjust its position if it does. */ + levels[frame.level - 1].frames.forEach(function(frame) { + const leftBound = frame.x; + let rightBound = frame.x + frame.width; + if (frame.fnObjects.length != 0) { + rightBound += 84; + } + if (x3 > leftBound && x3 < rightBound) { // overlap + x3 = rightBound + 10; + } + }); + const x4 = x3, + y4 = yf; + context.moveTo(x0, y0); + context.lineTo(x1, y1); + context.lineTo(x2, y2); + context.lineTo(x3, y3); + context.lineTo(x4, y4); + context.lineTo(xf, yf); + // draw arrow head + drawArrowHead(context, x4, y4, xf, yf); + context.stroke(); + } + } + + function drawSceneFnFrameArrow(fnObject) { + var scene = arrowLayer.scene, + context = scene.context; + //const offset = frames[pair[0]].fnIndex.indexOf(pair[1]); + + var startCoord = [fnObject.x + 15, fnObject.y]; + + //scene.clear(); + context.save(); + context.strokeStyle = "#999999"; + context.beginPath(); + const x0 = startCoord[0], + y0 = startCoord[1], + x1 = x0, + y1 = y0 - 15, + x2 = x1 - 70, + y2 = y1; + context.moveTo(x0, y0); + context.lineTo(x1, y1); + context.lineTo(x2, y2); + // draw arrow head + drawArrowHead(context, x1, y1, x2, y2); + context.stroke(); + } + + function drawSceneFrameArrow(frame) { + var config = frame; + var scene = arrowLayer.scene, + context = scene.context; + context.save(); + context.strokeStyle = "white"; + + if (config.tail == null) return null; + const offset = levels[frame.tail.level].frames.indexOf(frame.tail); + const x0 = config.x + (config.width / 2), + y0 = config.y, + x1 = x0, + y1 = (frame.tail.y + + frame.tail.height + y0) / 2 + offset * 4, + x2 = frame.tail.x + (frame.tail.width / 2); + y2 = y1, + x3 = x2, + y3 = frame.tail.y + + frame.tail.height + 3; // offset by 3 for aesthetic reasons + context.beginPath(); + context.moveTo(x0, y0); + context.lineTo(x1, y1); + context.lineTo(x2, y2); + context.lineTo(x3, y3); + drawArrowHead(context, x2, y2, x3, y3); + context.stroke(); + } + + // create viewport + var viewport = new Concrete.Viewport({ + width: 1000, + height: 1000, + container: container + }); + + // create layers + var fnObjectLayer = new Concrete.Layer(); + var dataObjectLayer = new Concrete.Layer(); + var frameLayer = new Concrete.Layer(); + var arrowLayer = new Concrete.Layer(); + + // add layers + viewport.add(frameLayer).add(fnObjectLayer).add(dataObjectLayer).add(arrowLayer); + + var fnObjects = []; + var dataObjects = []; + var levels = {}; + + var frames = []; + // parse input from interpreter + function parseInput(input) { + let frames = input.context.context.runtime.environments; + for (let i in frames) { + let frame = frames[i]; + frame.hovered = false; + frame.layer = frameLayer; + frame.color = white; + } + + /* + First pass: + - combine function calls and associated frames + - assign unique keys to frames + - find height level of frames + - store keys of child frames within each frame + - store number of frames in each heigh level for space partitioning + - calculate height and width of frame + */ + let i = frames.length - 1; + let key_counter = 0; + while (i >= 0) { + const frame = frames[i]; + if (i > 0 && frames[i - 1].name == "blockFrame") { + // assume for now that it belongs to prev frame function call + frames[i - 1].fnFrame = frame; + for (j in frames[i - 1].head) { + frame.head[j] = frames[i - 1].head[j]; + } + frames.splice(i - 1, 1); + i--; + } + + // load basic ConcreteJS properties + frame.hovered = false; + frame.selected = false; + frame.layer = frameLayer; + frame.color = 'white'; + + frame.fnObjects = []; + frame.dataObjects = []; + + // assign id to frame + frame.key = key_counter; + key_counter++; + + // change parent pointer from parent blockFrame to parent Function + if (frame.name !== "global" && frame.tail.name === "blockFrame") { + frame.tail = frame.tail.tail; + } + + // update array of children keys of parent frame + frame.children = []; // stores keys of child frames + if (frame.tail !== null) { + frame.tail.children.push(frame.key); + } + + // find frame height level (parent height + 1) + frame.level = frame.name === "global" + ? 0 + : frame.tail.level + 1; + + // update total number of frames in the current level + if (levels[frame.level]) { + levels[frame.level].count++; + levels[frame.level].frames.push(frame); + } else { + levels[frame.level] = {count: 1}; + levels[frame.level].frames = [frame]; + } + + // find height and width of frame from length of contained variable names + // if variable points to a function, add it to list of function objects + let env = frame.head; + frame.variables = []; // array to store non-built-in variables + let maxLength = 0; + let heightFactor = 0; + let pos = 0; + /* dataObject and fnObject keys must be differentiated, + as dataObject keys need to be manually assigned. + Since HTML Canvas uses [0, 2^24) as its range of keys + (and -1 for not found), take 2^24 - 1 as the key for + the first dataObject and decrement from there. + */ + let dataObjectKey = Math.pow(2, 24) - 1; + for (let k in env) { + if (builtins.indexOf(''+k) < 0) { + if (typeof(env[k]) == "object") { + frame.variables.push({ + name: k, + data: env[k] + }); + } else { + frame.variables.push(env[k]); + } + heightFactor++; + let varLength = (typeof(env[k]) == "number" + || typeof(env[k]) == "string") + ? k.length + ("" + env[k]).length + : k.length + 20; + if (k.length > maxLength) { + maxLength = k.length; + } + pos++; + } else { + delete env[k]; // why isn't this working? + } + + if (typeof(env[k]) == "function") { + if (builtins.indexOf('' + k) < 0) { + // check if function was already declared in another frame + let existing = false; + for (let fn in fnObjects) { + if (fnObjects[fn].node.id.start == env[k].node.id.start) { + existing = true; + break; + } + } + if (!existing) { + env[k].hovered = false; + env[k].selected = false; + env[k].layer = fnObjectLayer; + env[k].color = 'white'; + env[k].key = env[k].node.id.start; + env[k].offset = pos - 1; + let fnParent = frame; + while (fnParent.name === "blockFrame") { + fnParent = fnParent.tail; + } + fnParent.fnObjects.push(env[k].key); + env[k].parent = fnParent; + fnObjects.push(env[k]); + } + } + } else if (env[k] != null && typeof(env[k]) == "object") { + let dataParent = frame; + while (dataParent.name === "blockFrame") { + dataParent = dataParent.tail; + } + dataObject = { + hovered: false, + selected: false, + layer: dataObjectLayer, + color: 'white', + key: dataObjectKey--, + offset: pos - 1, + parent: dataParent, + data: env[k] + }; + dataObjects.push(dataObject); + frame.dataObjects.push(dataObject); + } + } + frame.width = maxLength * 20 + 40; + frame.height = heightFactor * 30 + 20; + + // update max height of level + levels[frame.level].height = (!levels[frame.level].height + || frame.height > levels[frame.level].height) + ? frame.height + : levels[frame.level].height; + i--; + } + frames.reverse(); // more natural ordering! Global frame now comes first + + /* + Second pass: + - Assign x- and y-coordinates for each frame + */ + let tempLevels = {}; + Object.keys(levels).forEach(function(level) { + tempLevels[level] = levels[level].count; + }); + + for (f in frames) { + // x-coordinate + let frame = frames[f]; + let partitionWidth = viewport.width / levels[frame.level].count; + frame.x = viewport.width - tempLevels[frame.level] * partitionWidth + + partitionWidth / 2 - frame.width / 2; + tempLevels[frame.level]--; + + // y-coordinate + let level = frame.level; + let y = 0; + for (i = 0; i < level; i++) { + y += levels[i].height + 60; + } + frame.y = y; + } + + return frames; + } + + function drawArrowHead(context, xi, yi, xf, yf) { + const gradient = (yf - yi) / (xf - xi); + const angle = Math.atan(gradient); + if (xf - xi >= 0) { // left to right arrow + const xR = xf - 10 * Math.cos(angle - Math.PI / 6); + const yR = yf - 10 * Math.sin(angle - Math.PI / 6); + context.lineTo(xR, yR); + context.moveTo(xf, yf); + const xL = xf - 10 * Math.cos(angle + Math.PI / 6); + const yL = yf - 10 * Math.sin(angle + Math.PI / 6); + context.lineTo(xL, yL); + } else { // right to left arrow + // draw arrow head + const xR = xf + 10 * Math.cos(angle - Math.PI / 6); + const yR = yf + 10 * Math.sin(angle - Math.PI / 6); + context.lineTo(xR, yR); + context.moveTo(xf, yf); + const xL = xf + 10 * Math.cos(angle + Math.PI / 6); + const yL = yf + 10 * Math.sin(angle + Math.PI / 6); + context.lineTo(xL, yL); + } + } + + function draw_env(context) { + // reset current drawing + fnObjectLayer.scene.clear() + dataObjectLayer.scene.clear() + frameLayer.scene.clear() + arrowLayer.scene.clear() + fnObjects = [] + dataObjects = [] + levels = {} + + frames = parseInput(context) + drawSceneFrames() + drawSceneFnObjects() + drawHitFnObjects() + drawSceneDataObjects() + drawHitDataObjects() + drawSceneFrameArrows() + drawSceneFrameObjectArrows() + drawSceneFnFrameArrows() + + // add concrete container handlers + container.addEventListener('mousemove', function(evt) { + var boundingRect = container.getBoundingClientRect(), + x = evt.clientX - boundingRect.left, + y = evt.clientY - boundingRect.top, + key = viewport.getIntersection(x, y), + fnObject, + dataObject; + // unhover all circles + fnObjects.forEach(function(fnObject) { + fnObject.hovered = false; + }); + + dataObjects.forEach(function(dataObject) { + dataObject.hovered = false; + }); + + if (key >= 0 && key < Math.pow(2, 23)) { + fnObject = getFnObjectFromKey(key); + try { + fnObject.hovered = true; + } catch (e) {}; + } + + else if (key >= Math.pow(2, 23)) { + dataObject = getDataObjectFromKey(key); + try { + dataObject.hovered = true; + } catch (e) {}; + } + drawSceneFnObjects(); + drawSceneDataObjects(); + + }); + + container.addEventListener('click', function(evt) { + var boundingRect = container.getBoundingClientRect(), + x = evt.clientX - boundingRect.left, + y = evt.clientY - boundingRect.top, + key = viewport.getIntersection(x, y), + fnObject, + dataObject; + // unhover all circles + fnObjects.forEach(function(fnObject) { + fnObject.selected = false; + }); + + dataObjects.forEach(function(dataObject) { + dataObject.selected = false; + }); + + if (key >= 0 && key < Math.pow(2, 23)) { + fnObject = getFnObjectFromKey(key); + try { + fnObject.selected = true; + } catch (e) {}; + } + + else if (key > Math.pow(2, 23)) { + dataObject = getDataObjectFromKey(key); + try { + dataObject.selected = true; + draw_data(dataObject.data); + } catch (e) {}; + + } + + drawSceneFnObjects(); + drawSceneDataObjects(); + }); + } + exports.draw_env = draw_env + + + + function getFnObjectFromKey(key) { + for (f in fnObjects) { + if (fnObjects[f].key === key) { + return fnObjects[f]; + } + } + } + + function getDataObjectFromKey(key) { + for (d in dataObjects) { + if (dataObjects[d].key === key) { + return dataObjects[d]; + } + } + } + + exports.EnvVisualizer = { + draw_env: draw_env, + init: function(parent) { + container.hidden = false + parent.appendChild(container) + }, + } + + setTimeout(() => {}, 1000) +})(window) diff --git a/public/externalLibs/env_visualizer/visualizer.js b/public/externalLibs/env_visualizer/visualizer.js new file mode 100644 index 0000000000..e4c29f964e --- /dev/null +++ b/public/externalLibs/env_visualizer/visualizer.js @@ -0,0 +1,1022 @@ +(function(exports) { + /** + * Setup Stage + */ + var stage + var container = document.createElement('div') + container.id = 'env-visualizer-container' + container.hidden = true + document.body.appendChild(container) + var drawDataButton = document.createElement('div') + drawDataButton.id = 'draw-data-button' + drawDataButton.hidden = true + document.body.appendChild(drawDataButton) + + const frameFontSetting = "14px Roboto Mono"; + const fnRadius = 12; + // List of built-in functions to ignore (i.e. not draw) + var builtins = [ + 'runtime', + 'display', + 'raw_display', + 'stringify', + 'error', + 'prompt', + 'is_number', + 'is_string', + 'is_function', + 'is_boolean', + 'is_undefined', + 'parse_int', + 'undefined', + 'NaN', + 'Infinity', + 'null', + 'pair', + 'is_pair', + 'head', + 'tail', + 'is_null', + 'is_list', + 'list', + 'length', + 'map', + 'build_list', + 'for_each', + 'list_to_string', + 'reverse', + 'append', + 'member', + 'remove', + 'remove_all', + 'filter', + 'enum_list', + 'list_ref', + 'accumulate', + 'equal', + 'draw_data', + 'set_head', + 'set_tail', + 'array_length', + 'is_array', + 'parse', + 'apply_in_underlying_javascript', + 'is_object', + 'is_NaN', + 'has_own_property', + 'alert', + 'timed', + 'assoc', + 'rawDisplay', + 'prompt', + 'alert', + 'visualiseList', + 'math_abs', + 'math_acos', + 'math_acosh', + 'math_asin', + 'math_asinh', + 'math_atan', + 'math_atanh', + 'math_atan2', + 'math_ceil', + 'math_cbrt', + 'math_expm1', + 'math_clz32', + 'math_cos', + 'math_cosh', + 'math_exp', + 'math_floor', + 'math_fround', + 'math_hypot', + 'math_imul', + 'math_log', + 'math_log1p', + 'math_log2', + 'math_log10', + 'math_max', + 'math_min', + 'math_pow', + 'math_random', + 'math_round', + 'math_sign', + 'math_sin', + 'math_sinh', + 'math_sqrt', + 'math_tan', + 'math_tanh', + 'math_trunc', + 'math_E', + 'math_LN10', + 'math_LN2', + 'math_LOG10E', + 'math_LOG2E', + 'math_PI', + 'math_SQRT1_2', + 'math_SQRT2' + ] + + /** + * List of built-in functions to draw and not ignore, + * i.e. built-ins that are called during the program's execution. + * Add name of such a function to this list when one is found. + */ + var builtinsToDraw = []; + + function drawSceneFnObjects() { + fnObjectLayer.scene.clear(); + for (let i = 0; i < fnObjects.length; i++) { + drawSceneFnObject(i); + } + viewport.render(); + } + + function drawHitFnObjects() { + for (let i = 0; i < fnObjects.length; i++) { + drawHitFnObject(i); + } + } + + function drawSceneDataObjects() { + dataObjectLayer.scene.clear(); + dataObjects.forEach(function(dataObject) { + drawSceneDataObject(dataObject); + }); + viewport.render(); + } + + function drawHitDataObjects() { + dataObjects.forEach(function(dataObject) { + drawHitDataObject(dataObject); + }); + } + + function drawSceneFrames() { + for (let i = 0; i < frames.length; i++) { + drawSceneFrame(i); + } + viewport.render(); + } + + function drawHitFrames() { + frames.forEach(function(frame) { + drawHitFrame(frame); + }); + } + + /* + For each frame, find params which have a function as their value. + For each such param, draw the arrow. + */ + function drawSceneFrameObjectArrows() { + frames.forEach(function(frame) { + let variables = frame.variables; + for (let i = 0; i < variables.length; i++) { + if (typeof(variables[i]) == "function") { + var fn = getFnObjectFromKey(variables[i].key); + drawSceneFrameFnArrow(frame, fn, i); + } else if (typeof(variables[i]) == "object") { + drawSceneFrameDataArrow(frame, i); + } + }; + }); + viewport.render(); + } + + function drawSceneFnFrameArrows() { + fnObjects.forEach(function(fnObject) { + drawSceneFnFrameArrow(fnObject); + }); + viewport.render(); + } + + function drawSceneFrameArrows() { + frames.forEach(function(frame) { + drawSceneFrameArrow(frame); + }); + viewport.render(); + } + + function drawSceneFnObject(pos) { + var config = fnObjects[pos]; + var scene = config.layer.scene, + context = scene.context; + + // find index position of function object within frame + var parent = config.parent; + var offset = config.offset; + var x = parent.x + parent.width + 60; + var y = parent.y + 25 + offset * 30; + config.x = x; + config.y = y; + //config.offset = offset; + context.beginPath(); + context.arc(x - fnRadius, y, fnRadius, 0, Math.PI*2, false); + + if (!config.hovered && !config.selected) { + context.strokeStyle = '#999999'; + context.lineWidth = 2; + context.stroke(); + } else { + context.strokeStyle = 'green'; + context.lineWidth = 2; + context.stroke(); + } + + context.beginPath(); + if (config.selected) { + context.font = "14px Roboto Mono Light"; + context.fillStyle = 'white'; + let fnString; + try { + fnString = config.fun.toString(); + } catch (e) { + fnString = config.toString(); + } + const params = fnString.substring(fnString.indexOf('(') + 1, + fnString.indexOf(')')); + let body = fnString.substring(fnString.indexOf(')') + 1); + body = body.split("\n"); + context.fillText(`params: ${params == "" ? "()" : params}`, x + 50, y); + context.fillText(`body:`, x + 50, y + 20); + let i = 0; + while (i < 5 && i < body.length) { + context.fillText(body[i], x + 100, y + 20 * (i + 1)); + i++; + } + if (i < body.length) { + context.fillText("...", x + 120, y + 120); + } + } + context.arc(x + fnRadius, y, fnRadius, 0, Math.PI*2, false); + if (!config.hovered && !config.selected) { + context.strokeStyle = '#999999'; + context.lineWidth = 2; + context.stroke(); + } else { + context.strokeStyle = 'green'; + context.lineWidth = 2; + context.stroke(); + } + + } + + function drawHitFnObject(pos) { + var config = fnObjects[pos]; + var hit = config.layer.hit, + context = hit.context; + var parent = config.parent; + // var offset = parent.fnObjects.indexOf(config.key); + var offset = config.offset; + var x = parent.x + parent.width + 60; + var y = parent.y + 25 + offset * 30; + config.x = x; + config.y = y; + config.offset = offset; + context.save(); + context.beginPath(); + context.arc(x - fnRadius, y, fnRadius, 0, Math.PI*2, false); + context.fillStyle = hit.getColorFromIndex(config.key); + context.fill(); + context.restore(); + + context.beginPath(); + context.arc(x + fnRadius, y, fnRadius, 0, Math.PI*2, false); + context.fillStyle = hit.getColorFromIndex(config.key); + context.fill(); + context.restore(); + } + + function drawSceneDataObject(dataObject) { + var config = dataObject; + var scene = dataObjectLayer.scene, + context = scene.context; + var parent = config.parent; + var offset = parent.variables.indexOf(config); + const x0 = parent.x + parent.width + 48, + y0 = parent.y + 25 + offset * 30 - 12; + const x1 = x0 - 12, + y1 = y0 + 24; + const x2 = x1 + 24, + y2 = y1; + config.offset = offset; + context.beginPath(); + context.moveTo(x0, y0); + context.lineTo(x1, y1); + context.lineTo(x2, y2); + context.lineTo(x0, y0); + context.fillStyle = '#999999'; + context.font = "14px Roboto Mono Light"; + context.fillText("!", x0 - 4, y0 + 20); + + if (!config.hovered && !config.selected) { + context.strokeStyle = '#999999'; + context.lineWidth = 2; + context.stroke(); + } else { + context.strokeStyle = 'green'; + context.lineWidth = 2; + context.stroke(); + } + if (config.selected) { + context.font = "14px Roboto Mono Light"; + context.fillStyle = 'white'; + context.fillText(config.data); + /** + * TO-DO: Implement some relevant action when data object is selected. + */ + } + } + + function drawHitDataObject(dataObject) { + var config = dataObject; + var hit = dataObjectLayer.hit, + context = hit.context; + var parent = config.parent; + var offset = config.offset; + const x0 = parent.x + parent.width + 48, + y0 = parent.y + 25 + offset * 30 - 12; + const x1 = x0 - 12, + y1 = y0 + 24; + const x2 = x1 + 24, + y2 = y1; + context.save(); + context.beginPath(); + context.moveTo(x0, y0); + context.lineTo(x1, y1); + context.lineTo(x2, y2); + context.lineTo(x0, y0); + context.fillStyle = hit.getColorFromIndex(config.key); + context.fill(); + context.restore(); + } + + function drawSceneFrame(pos) { + var config = frames[pos]; + var scene = config.layer.scene, + context = scene.context; + context.save(); + context.font = frameFontSetting; + context.fillStyle = "white"; + var x, y; + x = config.x; + y = config.y; + context.beginPath(); + + let env = config.headClone; + let i = 0; + for (let k in env) { + if (builtins.indexOf(''+k) < 0 + || builtinsToDraw.indexOf(''+k) >= 0) { + if (typeof(env[k]) == "number" + || typeof(env[k]) == "string") { + context.fillText(`${'' + k}: ${'' +env[k]}`, x + 10, y + 30 + (i * 30)); + } else if (typeof(env[k]) == "object") { + context.fillText(`${'' + k}:`, x + 10, y + 30 + (i * 30)); + } else { + context.fillText(`${'' + k}:`, x + 10, y + 30 + (i * 30)); + } + i++; + } + } + + context.rect(x, y, config.width, config.height); + context.lineWidth = 2; + context.strokeStyle = 'white'; + context.stroke(); + + if (config.selected) { + context.strokeStyle = 'white'; + context.lineWidth = 6; + context.stroke(); + } + + if (config.hovered) { + context.strokeStyle = 'green'; + context.lineWidth = 2; + context.stroke(); + } + } + + function drawHitFrame(config) { + var hit = config.layer.hit, + context = hit.context; + + var x, y; + if (config.tail != null) { + x = frames[config.tail].x; + y = frames[config.tail].y + 200; + config.x = x; + config.y = y; + } else { + x = config.x; + y = config.y; + } + + context.beginPath(); + context.rect(x, y, 150, 100); + context.fillStyle = hit.getColorFromIndex(config.key); + context.save(); + context.fill(); + context.restore(); + } + + function drawSceneFrameDataArrow(frame, frameOffset) { + var scene = arrowLayer.scene, + context = scene.context; + const x0 = frame.x + frame.variables[frameOffset].name.length * 10 + 20, + y0 = frame.y + frameOffset * 30 + 25, + xf = frame.x + frame.width + 35, + yf = y0; + context.save(); + context.strokeStyle = "#999999"; + context.beginPath(); + context.moveTo(x0, y0); + context.lineTo(xf, yf); + drawArrowHead(context, x0, y0, xf, yf); + context.stroke(); + } + + function drawSceneFrameFnArrow(frame, fnObject, frameOffset) { + var scene = arrowLayer.scene, + context = scene.context; + const yf = fnObject.y; + //scene.clear(); + context.save(); + context.strokeStyle = "#999999"; + context.beginPath(); + + if (fnObject.parent == frame) { + // fnObject belongs to current frame + // simply draw straight arrow from frame to function + const x0 = frame.x + getFnName(fnObject).length * 10 + 20, + y0 = frame.y + fnObject.offset * 30 + 25, + xf = fnObject.x - (fnRadius * 2) - 3; // left circle + context.moveTo(x0, y0); + context.lineTo(xf, yf); + + // draw arrow headClone + drawArrowHead(context, x0, y0, xf, yf); + context.stroke(); + } else { + // fnObject belongs to different frame + + const paramName = getKeyByValue(frame.headClone, frame.variables[frameOffset]); + const x0 = frame.x + + paramName.length * 8 + 22, + y0 = frame.y + frameOffset * 30 + 25; + const xf = fnObject.x + (fnRadius * 2) + 3, + x1 = frame.x + frame.width + 10 + frameOffset * 20, + y1 = y0; + const x2 = x1, + y2 = frame.y - 20 + frameOffset * 5; + // find offset of fnObject at parent frame + const fnOffset = fnObject.parent.variables.indexOf(fnObject); + let x3 = xf + 10 + fnOffset * 7, + y3 = y2; + /* From this position, the arrow needs to move upward to reach the + target fnObject. Make sure it does not intersect any other object + when doing so, or adjust its position if it does. */ + levels[frame.level - 1].frames.forEach(function(frame) { + const leftBound = frame.x; + let rightBound = frame.x + frame.width; + if (frame.fnObjects.length != 0) { + rightBound += 84; + } + if (x3 > leftBound && x3 < rightBound) { // overlap + x3 = rightBound + 10 + fnOffset * 7; + } + }); + const x4 = x3, + y4 = yf; + context.moveTo(x0, y0); + context.lineTo(x1, y1); + context.lineTo(x2, y2); + context.lineTo(x3, y3); + context.lineTo(x4, y4); + context.lineTo(xf, yf); + // draw arrow headClone + drawArrowHead(context, x4, y4, xf, yf); + context.stroke(); + } + } + + function drawSceneFnFrameArrow(fnObject) { + var scene = arrowLayer.scene, + context = scene.context; + //const offset = frames[pair[0]].fnIndex.indexOf(pair[1]); + + var startCoord = [fnObject.x + 15, fnObject.y]; + + //scene.clear(); + context.save(); + context.strokeStyle = "#999999"; + context.beginPath(); + const x0 = startCoord[0], + y0 = startCoord[1], + x1 = x0, + y1 = y0 - 15, + x2 = x1 - 70, + y2 = y1; + context.moveTo(x0, y0); + context.lineTo(x1, y1); + context.lineTo(x2, y2); + // draw arrow headClone + drawArrowHead(context, x1, y1, x2, y2); + context.stroke(); + } + + function drawSceneFrameArrow(frame) { + var config = frame; + var scene = arrowLayer.scene, + context = scene.context; + context.save(); + context.strokeStyle = "white"; + + if (config.tail == null) return null; + const offset = levels[frame.tail.level].frames.indexOf(frame.tail); + const x0 = config.x + (config.width / 2), + y0 = config.y, + x1 = x0, + y1 = (frame.tail.y + + frame.tail.height + y0) / 2 + offset * 4, + x2 = frame.tail.x + (frame.tail.width / 2); + y2 = y1, + x3 = x2, + y3 = frame.tail.y + + frame.tail.height + 3; // offset by 3 for aesthetic reasons + context.beginPath(); + context.moveTo(x0, y0); + context.lineTo(x1, y1); + context.lineTo(x2, y2); + context.lineTo(x3, y3); + drawArrowHead(context, x2, y2, x3, y3); + context.stroke(); + } + + // create viewport + var viewport = new Concrete.Viewport({ + width: 1000, + height: 1000, + container: container + }); + + // create layers + var fnObjectLayer = new Concrete.Layer(); + var dataObjectLayer = new Concrete.Layer(); + var frameLayer = new Concrete.Layer(); + var arrowLayer = new Concrete.Layer(); + + // add layers + viewport.add(frameLayer).add(fnObjectLayer).add(dataObjectLayer).add(arrowLayer); + + fnObjects = [] + dataObjects = [] + levels = {} + builtinsToDraw = [] + + var frames = []; + // parse input from interpreter + function parseInput(input) { + let frames = input.context.context.runtime.environments; + for (let i in frames) { + let frame = frames[i]; + frame.hovered = false; + frame.layer = frameLayer; + frame.color = white; + + // reset frame + frame.dataObjects = []; + frame.fnObjects = []; + frame.variables = []; + frame.children = []; + + // clone frame.head to avoid disrupting rest of interpreter + frame.headClone = {}; + for (k in frame.head) { + frame.headClone[k] = frame.head[k]; + } + } + + /* + First pass: + - combine function calls and associated frames + - assign unique keys to frames + - find height level of frames + - store keys of child frames within each frame + - store number of frames in each heigh level for space partitioning + - calculate height and width of frame + */ + let i = frames.length - 1; + let key_counter = 0; + /** + * Keys for data objects, user-defined function objects, + * and built-in function objects must all be differentiated. + * dataObject and builtinFnObject keys need to be manually assigned. + * Since ConcreteJS hit detection uses [0, 2^24) as its range of keys + * (and -1 for not found), take 2^24 - 1 as the key for + * the first dataObject and decrement from there. + * Take 2^23 - 1 as the key for the first builtinFnObject and + * decrement from there. + */ + let dataObjectKey = Math.pow(2, 24) - 1; + let builtinFnObjectKey = Math.pow(2, 23) - 1; + while (i >= 0) { + const frame = frames[i]; + /* + if (i > 0 && frames[i - 1].name == "blockFrame") { + // assume for now that it belongs to prev frame function call + frames[i - 1].fnFrame = frame; + for (j in frames[i - 1].headClone) { + frame.headClone[j] = frames[i - 1].headClone[j]; + } + frames.splice(i - 1, 1); + i--; + } + */ + // load basic ConcreteJS properties + frame.hovered = false; + frame.selected = false; + frame.layer = frameLayer; + frame.color = 'white'; + + frame.fnObjects = []; + frame.dataObjects = []; + + // assign id to frame + frame.key = key_counter; + key_counter++; + + // change parent pointer from parent blockFrame to parent Function + if (frame.name !== "global" && frame.tail.name === "blockFrame") { + frame.tail = frame.tail.tail; + } + + // update array of children keys of parent frame + frame.children = []; // stores keys of child frames + if (frame.tail !== null) { + frame.tail.children.push(frame.key); + } + + // find frame height level (parent height + 1) + frame.level = frame.name === "global" + ? 0 + : frame.tail.level + 1; + + // update total number of frames in the current level + if (levels[frame.level]) { + levels[frame.level].count++; + levels[frame.level].frames.push(frame); + } else { + levels[frame.level] = {count: 1}; + levels[frame.level].frames = [frame]; + } + + // find height and width of frame from length of contained variable names + // if variable points to a function, add it to list of function objects + let env = frame.headClone; + frame.variables = []; // array to store non-built-in variables + let maxLength = 0; + let heightFactor = 0; + for (let k in env) { + let varLength; + // first sieve out built-in functions in non-global frames + if (frame.name !== "global" + && builtins.indexOf(getFnName(env[k])) >= 0) { + builtinsToDraw.push(getFnName(env[k])); + heightFactor++; + if (k.length > maxLength) { + maxLength = k.length; + } + + // update width of global frame + let newWidth = getFnName(env[k]).length * 10 + 50; + if (frames[frames.length - 1].width < newWidth) { + frames[frames.length - 1].width = newWidth; + } + } else if (builtins.indexOf(''+k) < 0) { + heightFactor++; + varLength = (typeof(env[k]) == "number" + || typeof(env[k]) == "string") + ? k.length + ("" + env[k]).length + : k.length; + if (varLength > maxLength) { + maxLength = varLength; + } + } + + if (typeof(env[k]) == "function") { + const fnName = getFnName(env[k]); + if (builtins.indexOf(fnName) < 0 + || builtinsToDraw.indexOf(fnName) >= 0) { + // check if function was already declared in another frame + let existing = false; + if (builtinsToDraw.indexOf(fnName) < 0) { // not built-in + for (let fn in fnObjects) { + try { + if (fnObjects[fn].node.id.start == env[k].node.id.start) { + existing = true; + break; + } + } catch (e) {} + } + } else { // is built-in + for (let fn in fnObjects) { + if (getFnName(fnObjects[fn]) == fnName) { + existing = true; + break; + } + } + } + + env[k].hovered = false; + env[k].selected = false; + env[k].layer = fnObjectLayer; + env[k].color = 'white'; + if (builtinsToDraw.indexOf(fnName) < 0) { + if (!existing) { + env[k].key = env[k].node.id.start; + env[k].parent = frame; + } + frame.fnObjects.push(env[k].key); + frame.variables.push(env[k]); + } else { + if (!existing) { + env[k].key = builtinFnObjectKey--; + env[k].functionName = fnName; + env[k].parent = frames[frames.length - 1]; + frames[frames.length - 1].fnObjects.push(env[k].key); + frames[frames.length - 1].variables.push(env[k]); + // update height of global frame + frames[frames.length - 1].height += 30; + } + frame.variables.push(env[k]); + } + if (!existing) { + fnObjects.push(env[k]); + } + + } + } else if (builtins.indexOf('' + k) < 0) { + if (env[k] != null && typeof(env[k]) == "object") { + let dataParent = frame; + while (dataParent.name === "blockFrame") { + dataParent = dataParent.tail; + } + dataObject = { + hovered: false, + selected: false, + layer: dataObjectLayer, + color: 'white', + key: dataObjectKey--, + parent: dataParent, + data: env[k], + name: k + }; + dataObjects.push(dataObject); + frame.dataObjects.push(dataObject); + frame.variables.push(dataObject); + } else { + frame.variables.push(env[k]); + } + } + } + frame.width = maxLength * 10 + 50; + frame.height = heightFactor * 30 + 20; + i--; + } + frames.reverse(); // more natural ordering! Global frame now comes first + + /** + * Find and store the height of each level + * (i.e. the height of the tallest frame) + */ + for (let l in levels) { + let level = levels[l]; + let maxHeight = 0; + level.frames.forEach(function(frame) { + if (frame.height > maxHeight) { + maxHeight = frame.height; + } + }); + level.height = maxHeight; + } + + /** + * Second pass: + * - Assign x- and y-coordinates for each frame + */ + let tempLevels = {}; + Object.keys(levels).forEach(function(level) { + tempLevels[level] = levels[level].count; + }); + + for (f in frames) { + /** + * x-coordinate + * Current implementation: Frames are split into distinct height levels; + * global frame is at level 0 and every other frame is one level below + * its parent. + * For each level, split the viewport width into n partitions where + * n = number of frames in that level. Place each frame in the centre + * of each partition. + * + * Potential improvement: Assign space recursively to each frame and its + * "tree" of child frames. + */ + let frame = frames[f]; + let partitionWidth = viewport.width / levels[frame.level].count; + frame.x = viewport.width - tempLevels[frame.level] * partitionWidth + + partitionWidth / 2 - frame.width / 2; + tempLevels[frame.level]--; + + /** + * y-coordinate + * Simply the total height of all levels above the frame, plus a + * fixed factor (60) per level. + */ + let level = frame.level; + let y = 0; + for (i = 0; i < level; i++) { + y += levels[i].height + 60; + } + frame.y = y; + + let pos = 0; + let headClone = frame.headClone; + for (let k in headClone) { + if (typeof(headClone[k]) == "function" + && ((builtins.indexOf('' + k) < 0 + || builtinsToDraw.indexOf('' + k)) >= 0)) { + if (headClone[k].offset == undefined) { + headClone[k].offset = pos; + } + pos++; + } else if (typeof(headClone[k]) !== "function" + && builtins.indexOf('' + k) < 0) { + if (typeof(headClone[k]) == "object") { + headClone[k].offset = pos; + } + pos++; + } + }; + } + return frames; + } + + function drawArrowHead(context, xi, yi, xf, yf) { + const gradient = (yf - yi) / (xf - xi); + const angle = Math.atan(gradient); + if (xf - xi >= 0) { // left to right arrow + const xR = xf - 10 * Math.cos(angle - Math.PI / 6); + const yR = yf - 10 * Math.sin(angle - Math.PI / 6); + context.lineTo(xR, yR); + context.moveTo(xf, yf); + const xL = xf - 10 * Math.cos(angle + Math.PI / 6); + const yL = yf - 10 * Math.sin(angle + Math.PI / 6); + context.lineTo(xL, yL); + } else { // right to left arrow + // draw arrow headClone + const xR = xf + 10 * Math.cos(angle - Math.PI / 6); + const yR = yf + 10 * Math.sin(angle - Math.PI / 6); + context.lineTo(xR, yR); + context.moveTo(xf, yf); + const xL = xf + 10 * Math.cos(angle + Math.PI / 6); + const yL = yf + 10 * Math.sin(angle + Math.PI / 6); + context.lineTo(xL, yL); + } + } + + function draw_env(context) { + + // reset current drawing + fnObjectLayer.scene.clear() + dataObjectLayer.scene.clear() + frameLayer.scene.clear() + arrowLayer.scene.clear() + fnObjects = [] + dataObjects = [] + levels = {} + builtinsToDraw = [] + frames = parseInput(context) + drawSceneFrames() + drawSceneFnObjects() + drawHitFnObjects() + drawSceneDataObjects() + drawHitDataObjects() //(not in use for now) + drawSceneFrameArrows() + drawSceneFrameObjectArrows() + drawSceneFnFrameArrows() + // add concrete container handlers + container.addEventListener('mousemove', function(evt) { + var boundingRect = container.getBoundingClientRect(), + x = evt.clientX - boundingRect.left, + y = evt.clientY - boundingRect.top, + key = viewport.getIntersection(x, y), + fnObject, + dataObject; + // unhover all circles + fnObjects.forEach(function(fnObject) { + fnObject.hovered = false; + }); + + dataObjects.forEach(function(dataObject) { + dataObject.hovered = false; + }); + + if (key >= 0 && key < Math.pow(2, 23)) { + fnObject = getFnObjectFromKey(key); + try { + fnObject.hovered = true; + } catch (e) {}; + } + + else if (key >= Math.pow(2, 23)) { + dataObject = getDataObjectFromKey(key); + try { + dataObject.hovered = true; + } catch (e) {}; + } + drawSceneFnObjects(); + drawSceneDataObjects(); + + }); + + container.addEventListener('click', function(evt) { + var boundingRect = container.getBoundingClientRect(), + x = evt.clientX - boundingRect.left, + y = evt.clientY - boundingRect.top, + key = viewport.getIntersection(x, y), + fnObject, + dataObject; + // unhover all circles + fnObjects.forEach(function(fnObject) { + fnObject.selected = false; + }); + + dataObjects.forEach(function(dataObject) { + dataObject.selected = false; + }); + + if (key >= 0 && key < Math.pow(2, 23)) { + fnObject = getFnObjectFromKey(key); + try { + fnObject.selected = true; + } catch (e) {}; + } + + else if (key > Math.pow(2, 23)) { + dataObject = getDataObjectFromKey(key); + try { + dataObject.selected = true; + draw_data(dataObject.data); + } catch (e) {}; + + } + + drawSceneFnObjects(); + drawSceneDataObjects(); + + }); + frames.reverse(); + } + exports.draw_env = draw_env + + function getFnObjectFromKey(key) { + for (f in fnObjects) { + if (fnObjects[f].key === key) { + return fnObjects[f]; + } + } + } + + function getDataObjectFromKey(key) { + for (d in dataObjects) { + if (dataObjects[d].key === key) { + return dataObjects[d]; + } + } + } + + function getFnName(fn) { + if (fn.functionName != undefined) { + return fn.functionName; + } else { + return fn.toString().split("(")[0].split(" ")[1]; + } + } + + function getKeyByValue(object, value) { + return Object.keys(object).find(key => object[key] === value); + } + + exports.EnvVisualizer = { + draw_env: draw_env, + init: function(parent) { + container.hidden = false + parent.appendChild(container) + }, + } + + setTimeout(() => {}, 1000) +})(window) diff --git a/public/externalLibs/index.js b/public/externalLibs/index.js index f16817fecd..d3ef46e4cb 100644 --- a/public/externalLibs/index.js +++ b/public/externalLibs/index.js @@ -38,8 +38,11 @@ function loadAllLibs() { '/externalLibs/streams/stream.js', '/externalLibs/pe_library.js', '/externalLibs/assert_compiled.js', - //inspector - '/externalLibs/inspector/inspector.js' + // inspector + '/externalLibs/inspector/inspector.js', + // env visualizer + '/externalLibs/env_visualizer/ConcreteJS.js', + '/externalLibs/env_visualizer/visualizer.js' ] for (var i = 0; i < files.length; i++) { diff --git a/src/components/Playground.tsx b/src/components/Playground.tsx index 61f6765a24..48b4c47f33 100755 --- a/src/components/Playground.tsx +++ b/src/components/Playground.tsx @@ -9,6 +9,7 @@ import { ExternalLibraryName } from './assessment/assessmentShape'; import Markdown from './commons/Markdown'; import Workspace, { WorkspaceProps } from './workspace'; import { SideContentTab } from './workspace/side-content'; +import EnvVisualizer from './workspace/side-content/EnvVisualizer'; import Inspector from './workspace/side-content/Inspector'; import ListVisualizer from './workspace/side-content/ListVisualizer'; @@ -155,7 +156,8 @@ class Playground extends React.Component { sideContentProps: { activeTab: this.props.activeTab, handleChangeActiveTab: this.props.handleChangeActiveTab, - tabs: [playgroundIntroductionTab, listVisualizerTab, inspectorTab] + tabs: [playgroundIntroductionTab, listVisualizerTab, inspectorTab, + envVisualizerTab] } }; return ( @@ -181,7 +183,7 @@ const playgroundIntroductionTab: SideContentTab = { }; const listVisualizerTab: SideContentTab = { - label: 'List Visualizer', + label: 'Data Visualizer', icon: IconNames.EYE_OPEN, body: }; @@ -192,4 +194,10 @@ const inspectorTab: SideContentTab = { body: }; +const envVisualizerTab: SideContentTab = { + label: 'Env Visualizer', + icon: IconNames.EYE_OPEN, + body: +}; + export default Playground; diff --git a/src/components/workspace/side-content/EnvVisualizer.tsx b/src/components/workspace/side-content/EnvVisualizer.tsx new file mode 100644 index 0000000000..d252798b09 --- /dev/null +++ b/src/components/workspace/side-content/EnvVisualizer.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; + +class EnvVisualizer extends React.Component<{}, {}> { + private $parent: HTMLElement | null; + + public componentDidMount() { + if (this.$parent) { + (window as any).EnvVisualizer.init(this.$parent); + } + } + + public render() { + return
(this.$parent = r)} className="sa-env-visualizer pt-dark" />; + } +} + +export default EnvVisualizer; diff --git a/src/sagas/index.ts b/src/sagas/index.ts index 8617eb5292..81fc120046 100755 --- a/src/sagas/index.ts +++ b/src/sagas/index.ts @@ -14,7 +14,7 @@ import { externalLibraries } from '../reducers/externalLibraries'; import { defaultEditorValue, IState, IWorkspaceState } from '../reducers/states'; import { IVLE_KEY, USE_BACKEND } from '../utils/constants'; import { showSuccessMessage, showWarningMessage } from '../utils/notification'; -import { highlightLine, inspectorUpdate } from '../utils/slangHelper'; +import { highlightLine, inspectorUpdate, visualiseEnv } from '../utils/slangHelper'; import backendSaga from './backend'; function* mainSaga() { @@ -312,7 +312,8 @@ function* updateInspector() { const start = lastDebuggerResult.context.runtime.nodes[0].loc.start.line - 1; const end = lastDebuggerResult.context.runtime.nodes[0].loc.end.line - 1; yield put(actions.highlightEditorLine([start, end], location)); - inspectorUpdate(lastDebuggerResult); + visualiseEnv(lastDebuggerResult); + inspectorUpdate(lastDebuggerResult); } catch (e) { put(actions.highlightEditorLine([], location)); // most likely harmless, we can pretty much ignore this. diff --git a/src/utils/slangHelper.ts b/src/utils/slangHelper.ts index c05c2c2fdd..58560508e2 100644 --- a/src/utils/slangHelper.ts +++ b/src/utils/slangHelper.ts @@ -82,6 +82,14 @@ function visualiseList(list: any) { } } +export function visualiseEnv(context: Context) { + if ((window as any).EnvVisualizer) { + (window as any).EnvVisualizer.draw_env({ context }); + } else { + throw new Error('Env visualizer is not enabled'); + } +} + export function highlightLine(line: number) { if ((window as any).Inspector) { (window as any).Inspector.highlightClean(); From a259f11372a8d64d442813c37bbca9712a92023e Mon Sep 17 00:00:00 2001 From: Tan YuGin Date: Thu, 18 Apr 2019 20:24:55 +0800 Subject: [PATCH 2/2] fixed formatting --- src/components/Playground.tsx | 3 +-- src/sagas/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/Playground.tsx b/src/components/Playground.tsx index 48b4c47f33..f36e0afd45 100755 --- a/src/components/Playground.tsx +++ b/src/components/Playground.tsx @@ -156,8 +156,7 @@ class Playground extends React.Component { sideContentProps: { activeTab: this.props.activeTab, handleChangeActiveTab: this.props.handleChangeActiveTab, - tabs: [playgroundIntroductionTab, listVisualizerTab, inspectorTab, - envVisualizerTab] + tabs: [playgroundIntroductionTab, listVisualizerTab, inspectorTab, envVisualizerTab] } }; return ( diff --git a/src/sagas/index.ts b/src/sagas/index.ts index 81fc120046..b60b18d36a 100755 --- a/src/sagas/index.ts +++ b/src/sagas/index.ts @@ -312,8 +312,8 @@ function* updateInspector() { const start = lastDebuggerResult.context.runtime.nodes[0].loc.start.line - 1; const end = lastDebuggerResult.context.runtime.nodes[0].loc.end.line - 1; yield put(actions.highlightEditorLine([start, end], location)); - visualiseEnv(lastDebuggerResult); - inspectorUpdate(lastDebuggerResult); + visualiseEnv(lastDebuggerResult); + inspectorUpdate(lastDebuggerResult); } catch (e) { put(actions.highlightEditorLine([], location)); // most likely harmless, we can pretty much ignore this.