提交 c7ec8961 编写于 作者: J Jason Park

Add layout feature

上级 7271b346
......@@ -38,7 +38,7 @@ const trace = lang => (req, res, next) => {
}).finally(() => clearTimeout(timer));
})
.then(() => new Promise((resolve, reject) => {
const visualizationPath = path.resolve(tempPath, 'traces.json');
const visualizationPath = path.resolve(tempPath, 'visualization.json');
res.sendFile(visualizationPath, err => {
if (err) return reject(new Error('Visualization Not Found'));
resolve();
......
......@@ -10,5 +10,5 @@ onmessage = e => {
const lines = e.data.split('\n').map((line, i) => line.replace(/(\.\s*delay\s*)\(\s*\)/g, `$1(${i})`));
const code = lines.join('\n');
sandbox(code);
postMessage(AlgorithmVisualizer.Tracer.traces);
postMessage(AlgorithmVisualizer.Commander.commands);
};
......@@ -69,11 +69,11 @@ const GitHubApi = {
const TracerApi = {
md: ({ code }) => Promise.resolve([{
tracerKey: '0-MarkdownTracer-Markdown',
method: 'construct',
args: ['MarkdownTracer', 'Markdown'],
key: 'markdown',
method: 'MarkdownTracer',
args: ['Markdown'],
}, {
tracerKey: '0-MarkdownTracer-Markdown',
key: 'markdown',
method: 'set',
args: [code],
}]),
......
......@@ -27,13 +27,16 @@ class App extends BaseComponent {
super(props);
this.state = {
navigatorOpened: true,
workspaceVisibles: [true, true, true],
workspaceWeights: [1, 2, 2],
};
this.codeEditorRef = React.createRef();
this.ignoreHistoryBlock = this.ignoreHistoryBlock.bind(this);
this.handleClickTitleBar = this.handleClickTitleBar.bind(this);
this.loadScratchPapers = this.loadScratchPapers.bind(this);
this.handleChangeWorkspaceWeights = this.handleChangeWorkspaceWeights.bind(this);
}
componentDidMount() {
......@@ -161,7 +164,7 @@ class App extends BaseComponent {
login: undefined,
gistId,
title: 'Untitled',
files: [CONTRIBUTING_MD, createUserFile('traces.json', JSON.stringify(content))],
files: [CONTRIBUTING_MD, createUserFile('visualization.json', JSON.stringify(content))],
});
});
} else if (gistId === 'new') {
......@@ -182,7 +185,10 @@ class App extends BaseComponent {
return Promise.resolve();
};
fetch()
.then(() => this.selectDefaultTab())
.then(() => {
this.selectDefaultTab();
return null; // to suppress unnecessary bluebird warning
})
.catch(error => {
this.handleError(error);
this.props.history.push('/');
......@@ -204,15 +210,22 @@ class App extends BaseComponent {
this.codeEditorRef.current.getWrappedInstance().handleResize();
}
toggleNavigatorOpened(navigatorOpened = !this.state.navigatorOpened) {
this.setState({ navigatorOpened });
toggleNavigatorOpened(navigatorOpened = !this.state.workspaceVisibles[0]) {
const workspaceVisibles = [...this.state.workspaceVisibles];
workspaceVisibles[0] = navigatorOpened;
this.setState({ workspaceVisibles });
}
render() {
const { navigatorOpened, workspaceWeights } = this.state;
handleClickTitleBar() {
this.toggleNavigatorOpened();
}
render() {
const { workspaceVisibles, workspaceWeights } = this.state;
const { titles, description, saved } = this.props.current;
const title = `${saved ? '' : '(Unsaved) '}${titles.join(' - ')}`;
const [navigatorOpened] = workspaceVisibles;
return (
<div className={styles.app}>
......@@ -220,12 +233,11 @@ class App extends BaseComponent {
<title>{title}</title>
<meta name="description" content={description} />
</Helmet>
<Header className={styles.header} onClickTitleBar={() => this.toggleNavigatorOpened()}
navigatorOpened={navigatorOpened} loadScratchPapers={() => this.loadScratchPapers()}
<Header className={styles.header} onClickTitleBar={this.handleClickTitleBar}
navigatorOpened={navigatorOpened} loadScratchPapers={this.loadScratchPapers}
ignoreHistoryBlock={this.ignoreHistoryBlock} />
<ResizableContainer className={styles.workspace} horizontal weights={workspaceWeights}
visibles={[navigatorOpened, true, true]}
onChangeWeights={weights => this.handleChangeWorkspaceWeights(weights)}>
visibles={workspaceVisibles} onChangeWeights={this.handleChangeWorkspaceWeights}>
<Navigator />
<VisualizationViewer className={styles.visualization_viewer} />
<TabContainer className={styles.editor_tab_container}>
......
......@@ -17,7 +17,7 @@ class CodeEditor extends React.Component {
}
handleResize() {
this.aceEditorRef.current.editor.resize();
this.aceEditorRef.current.getWrappedInstance().resize();
}
render() {
......
......@@ -11,7 +11,7 @@ import 'brace/theme/tomorrow_night_eighties';
import 'brace/ext/searchbox';
import { actions } from '/reducers';
@connect(({ current }) => ({ current }), actions)
@connect(({ current }) => ({ current }), actions, null, { withRef: true })
class FoldableAceEditor extends AceEditor {
componentDidMount() {
super.componentDidMount();
......@@ -40,6 +40,10 @@ class FoldableAceEditor extends AceEditor {
}
}
}
resize() {
this.editor.resize();
}
}
export default FoldableAceEditor;
......@@ -41,22 +41,23 @@ class Player extends BaseComponent {
}
}
reset(traces = []) {
reset(commands = []) {
const chunks = [{
traces: [],
commands: [],
lineNumber: undefined,
}];
while (traces.length) {
const trace = traces.shift();
if (trace.method === 'delay') {
const [lineNumber] = trace.args;
while (commands.length) {
const command = commands.shift();
const { key, method, args } = command;
if (key === null && method === 'delay') {
const [lineNumber] = args;
chunks[chunks.length - 1].lineNumber = lineNumber;
chunks.push({
traces: [],
commands: [],
lineNumber: undefined,
});
} else {
chunks[chunks.length - 1].traces.push(trace);
chunks[chunks.length - 1].commands.push(command);
}
}
this.props.setChunks(chunks);
......@@ -76,10 +77,10 @@ class Player extends BaseComponent {
const ext = extension(file.name);
if (ext in TracerApi) {
TracerApi[ext]({ code: file.content }, undefined, this.tracerApiSource.token)
.then(traces => {
.then(commands => {
this.tracerApiSource = null;
this.setState({ building: false });
this.reset(traces);
this.reset(commands);
this.next();
})
.catch(error => {
......
......@@ -16,7 +16,7 @@ class ResizableContainer extends React.Component {
let totalWeight = 0;
let subtotalWeight = 0;
weights.forEach((weight, i) => {
if (!visibles[i]) return;
if (visibles && !visibles[i]) return;
totalWeight += weight;
if (i < index) subtotalWeight += weight;
});
......@@ -34,9 +34,10 @@ class ResizableContainer extends React.Component {
const elements = [];
let lastIndex = -1;
const totalWeight = weights.filter((weight, i) => visibles[i]).reduce((sumWeight, weight) => sumWeight + weight, 0);
const totalWeight = weights.filter((weight, i) => !visibles || visibles[i])
.reduce((sumWeight, weight) => sumWeight + weight, 0);
children.forEach((child, i) => {
if (visibles[i]) {
if (!visibles || visibles[i]) {
if (~lastIndex) {
const prevIndex = lastIndex;
elements.push(
......
import React from 'react';
import { connect } from 'react-redux';
import { classes } from '/common/util';
import { BaseComponent, ResizableContainer } from '/components';
import { BaseComponent } from '/components';
import { actions } from '/reducers';
import styles from './stylesheet.scss';
import { Array1DData, Array2DData, ChartData, Data, GraphData, LogData, MarkdownData } from '/core/datas';
import * as TracerClasses from '/core/tracers';
import * as LayoutClasses from '/core/layouts';
import { classes } from '/common/util';
@connect(({ player }) => ({ player }), actions)
class VisualizationViewer extends BaseComponent {
constructor(props) {
super(props);
this.state = {
dataWeights: {},
};
this.reset();
}
this.datas = [];
reset() {
this.root = null;
this.objects = {};
}
componentDidMount() {
......@@ -36,19 +38,11 @@ class VisualizationViewer extends BaseComponent {
if (cursor > oldCursor) {
applyingChunks = chunks.slice(oldCursor, cursor);
} else {
this.datas = [];
this.reset();
applyingChunks = chunks.slice(0, cursor);
}
applyingChunks.forEach(chunk => this.applyChunk(chunk));
const dataWeights = chunks === oldChunks ? { ...this.state.dataWeights } : {};
this.datas.forEach(data => {
if (!(data.tracerKey in dataWeights)) {
dataWeights[data.tracerKey] = 1;
}
});
this.setState({ dataWeights });
const lastChunk = applyingChunks[applyingChunks.length - 1];
if (lastChunk && lastChunk.lineNumber !== undefined) {
this.props.setLineIndicator({ lineNumber: lastChunk.lineNumber, cursor });
......@@ -57,29 +51,24 @@ class VisualizationViewer extends BaseComponent {
}
}
addTracer(className, tracerKey, title) {
const DataClass = {
Tracer: Data,
MarkdownTracer: MarkdownData,
LogTracer: LogData,
Array2DTracer: Array2DData,
Array1DTracer: Array1DData,
ChartTracer: ChartData,
GraphTracer: GraphData,
}[className];
const data = new DataClass(tracerKey, title, this.datas);
this.datas.push(data);
}
applyTrace(trace) {
const { tracerKey, method, args } = trace;
applyCommand(command) {
const { key, method, args } = command;
try {
if (method === 'construct') {
const [className, title] = args;
this.addTracer(className, tracerKey, title);
if (key === null && method === 'setRoot') {
const [root] = args;
this.root = this.objects[root];
} else if (method === 'destroy') {
delete this.objects[key];
} else if (method in LayoutClasses) {
const [children] = args;
const LayoutClass = LayoutClasses[method];
this.objects[key] = new LayoutClass(key, key => this.objects[key], children);
} else if (method in TracerClasses) {
const [title] = args;
const TracerClass = TracerClasses[method];
this.objects[key] = new TracerClass(key, key => this.objects[key], title);
} else {
const data = this.datas.find(data => data.tracerKey === tracerKey);
data[method](...args);
this.objects[key][method](...args);
}
} catch (error) {
this.handleError(error);
......@@ -87,30 +76,18 @@ class VisualizationViewer extends BaseComponent {
}
applyChunk(chunk) {
chunk.traces.forEach(trace => this.applyTrace(trace));
}
handleChangeWeights(weights) {
const dataWeights = {};
weights.forEach((weight, i) => {
dataWeights[this.datas[i].tracerKey] = weight;
});
this.setState({ dataWeights });
chunk.commands.forEach(command => this.applyCommand(command));
}
render() {
const { className } = this.props;
const { dataWeights } = this.state;
return (
<ResizableContainer className={classes(styles.visualization_viewer, className)}
weights={this.datas.map(data => dataWeights[data.tracerKey])}
visibles={this.datas.map(() => true)}
onChangeWeights={weights => this.handleChangeWeights(weights)}>
<div className={classes(styles.visualization_viewer, className)}>
{
this.datas.map(data => data.render())
this.root && this.root.render()
}
</ResizableContainer>
</div>
);
}
}
......
@import "~/common/stylesheet/index";
.visualization_viewer {
}
\ No newline at end of file
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
}
export { default as Data } from './Data';
export { default as MarkdownData } from './MarkdownData';
export { default as LogData } from './LogData';
export { default as Array2DData } from './Array2DData';
export { default as Array1DData } from './Array1DData';
export { default as ChartData } from './ChartData';
export { default as GraphData } from './GraphData';
import { Layout } from '/core/layouts';
class HorizontalLayout extends Layout {
}
export default HorizontalLayout;
import React from 'react';
import { ResizableContainer } from '/components';
import { HorizontalLayout } from '/core/layouts';
class Layout {
constructor(key, getObject, children) {
this.key = key;
this.getObject = getObject;
this.children = children.map(key => this.getObject(key));
this.weights = children.map(() => 1);
this.ref = React.createRef();
this.handleChangeWeights = this.handleChangeWeights.bind(this);
}
add(key, index = this.children.length) {
const child = this.getObject(key);
this.children.splice(index, 0, child);
this.weights.splice(index, 0, 1);
}
remove(key) {
const child = this.getObject(key);
const index = this.children.indexOf(child);
if (~index) {
this.children.splice(index, 1);
this.weights.splice(index, 1);
}
}
removeAll() {
this.children = [];
this.weights = [];
}
handleChangeWeights(weights) {
this.weights.splice(0, this.weights.length, ...weights);
this.ref.current.forceUpdate();
}
render() {
const horizontal = this instanceof HorizontalLayout;
return (
<ResizableContainer key={this.key} ref={this.ref} weights={this.weights} horizontal={horizontal}
onChangeWeights={this.handleChangeWeights}>
{
this.children.map(tracer => tracer && tracer.render())
}
</ResizableContainer>
);
}
}
export default Layout;
import { Layout } from '/core/layouts';
class VerticalLayout extends Layout {
}
export default VerticalLayout;
export { default as Layout } from './Layout';
export { default as HorizontalLayout } from './HorizontalLayout';
export { default as VerticalLayout } from './VerticalLayout';
......@@ -7,7 +7,7 @@ class GraphRenderer extends Renderer {
constructor(props) {
super(props);
this.element = React.createRef();
this.elementRef = React.createRef();
this.selectedNode = null;
this.togglePan(true);
......@@ -35,7 +35,7 @@ class GraphRenderer extends Renderer {
}
computeCoords(e) {
const svg = this.element.current;
const svg = this.elementRef.current;
const s = svg.createSVGPoint();
s.x = e.clientX;
s.y = e.clientY;
......@@ -53,7 +53,7 @@ class GraphRenderer extends Renderer {
baseHeight * this.zoom,
];
return (
<svg className={styles.graph} viewBox={viewBox} ref={this.element}>
<svg className={styles.graph} viewBox={viewBox} ref={this.elementRef}>
<defs>
<marker id="markerArrow" markerWidth="4" markerHeight="4" refX="2" refY="2" orient="auto">
<path d="M0,0 L0,4 L4,2 L0,0" className={styles.arrow} />
......
......@@ -6,12 +6,12 @@ class LogRenderer extends Renderer {
constructor(props) {
super(props);
this.element = React.createRef();
this.elementRef = React.createRef();
}
componentDidUpdate(prevProps, prevState, snapshot) {
super.componentDidUpdate(prevProps, prevState, snapshot);
const div = this.element.current;
const div = this.elementRef.current;
div.scrollTop = div.scrollHeight;
}
......@@ -19,7 +19,7 @@ class LogRenderer extends Renderer {
const { log } = this.props.data;
return (
<div className={styles.log} ref={this.element} dangerouslySetInnerHTML={{ __html: log }} />
<div className={styles.log} ref={this.elementRef} dangerouslySetInnerHTML={{ __html: log }} />
);
}
}
......
import { Array2DData } from '/core/datas';
import { Array2DTracer } from '/core/tracers';
import { Array1DRenderer } from '/core/renderers';
class Array1DData extends Array2DData {
class Array1DTracer extends Array2DTracer {
getRendererClass() {
return Array1DRenderer;
}
init() {
super.init();
this.chartData = null;
this.chartTracer = null;
}
set(array1d = []) {
const array2d = [array1d];
super.set(array2d);
this.syncChartData();
this.syncChartTracer();
}
patch(x, v) {
......@@ -33,14 +33,14 @@ class Array1DData extends Array2DData {
super.deselect(0, sx, 0, ex);
}
chart(tracerKey) {
this.chartData = tracerKey ? this.findData(tracerKey) : null;
this.syncChartData();
chart(key) {
this.chartTracer = key ? this.getObject(key) : null;
this.syncChartTracer();
}
syncChartData() {
if (this.chartData) this.chartData.data = this.data;
syncChartTracer() {
if (this.chartTracer) this.chartTracer.data = this.data;
}
}
export default Array1DData;
export default Array1DTracer;
import { Data } from '/core/datas';
import { Tracer } from '/core/tracers';
import { Array2DRenderer } from '/core/renderers';
class Element {
......@@ -9,7 +9,7 @@ class Element {
}
}
class Array2DData extends Data {
class Array2DTracer extends Tracer {
getRendererClass() {
return Array2DRenderer;
}
......@@ -62,4 +62,4 @@ class Array2DData extends Data {
}
}
export default Array2DData;
export default Array2DTracer;
import { Array1DData } from '/core/datas';
import { Array1DTracer } from '/core/tracers';
import { ChartRenderer } from '/core/renderers';
class ChartData extends Array1DData {
class ChartTracer extends Array1DTracer {
getRendererClass() {
return ChartRenderer;
}
}
export default ChartData;
export default ChartTracer;
import { Data } from '/core/datas';
import { Tracer } from '/core/tracers';
import { distance } from '/common/util';
import { GraphRenderer } from '/core/renderers';
class GraphData extends Data {
class GraphTracer extends Tracer {
getRendererClass() {
return GraphRenderer;
}
......@@ -21,7 +21,7 @@ class GraphData extends Data {
this.isDirected = true;
this.isWeighted = false;
this.callLayout = { method: this.layoutCircle, args: [] };
this.logData = null;
this.logTracer = null;
}
set(array2d = []) {
......@@ -230,8 +230,8 @@ class GraphData extends Data {
const node = this.findNode(target);
if (weight !== undefined) node.weight = weight;
node.visitedCount += visit ? 1 : -1;
if (this.logData) {
this.logData.println(visit ? (source || '') + ' -> ' + target : (source || '') + ' <- ' + target);
if (this.logTracer) {
this.logTracer.println(visit ? (source || '') + ' -> ' + target : (source || '') + ' <- ' + target);
}
}
......@@ -248,14 +248,14 @@ class GraphData extends Data {
if (edge) edge.selectedCount += select ? 1 : -1;
const node = this.findNode(target);
node.selectedCount += select ? 1 : -1;
if (this.logData) {
this.logData.println(select ? (source || '') + ' => ' + target : (source || '') + ' <= ' + target);
if (this.logTracer) {
this.logTracer.println(select ? (source || '') + ' => ' + target : (source || '') + ' <= ' + target);
}
}
log(tracerKey) {
this.logData = tracerKey ? this.findData(tracerKey) : null;
log(key) {
this.logTracer = key ? this.getObject(key) : null;
}
}
export default GraphData;
export default GraphTracer;
import { sprintf } from 'sprintf-js';
import { Data } from '/core/datas';
import { Tracer } from '/core/tracers';
import { LogRenderer } from '/core/renderers';
class LogData extends Data {
class LogTracer extends Tracer {
getRendererClass() {
return LogRenderer;
}
......@@ -25,4 +25,4 @@ class LogData extends Data {
}
}
export default LogData;
export default LogTracer;
import { Data } from '/core/datas';
import { Tracer } from '/core/tracers';
import { MarkdownRenderer } from '/core/renderers';
class MarkdownData extends Data {
class MarkdownTracer extends Tracer {
getRendererClass() {
return MarkdownRenderer;
}
......@@ -12,4 +12,4 @@ class MarkdownData extends Data {
}
}
export default MarkdownData;
export default MarkdownTracer;
import React from 'react';
import { Renderer } from '/core/renderers';
class Data {
constructor(tracerKey, title, datas) {
this.tracerKey = tracerKey;
class Tracer {
constructor(key, getObject, title = this.constructor.name) {
this.key = key;
this.getObject = getObject;
this.title = title;
this.datas = datas;
this.init();
this.reset();
}
findData(tracerKey) {
return this.datas.find(data => data.tracerKey === tracerKey);
}
getRendererClass() {
return Renderer;
}
......@@ -24,7 +20,7 @@ class Data {
render() {
const RendererClass = this.getRendererClass();
return (
<RendererClass key={this.tracerKey} title={this.title} data={this} />
<RendererClass key={this.key} title={this.title} data={this} />
);
}
......@@ -36,4 +32,4 @@ class Data {
}
}
export default Data;
export default Tracer;
export { default as Tracer } from './Tracer';
export { default as MarkdownTracer } from './MarkdownTracer';
export { default as LogTracer } from './LogTracer';
export { default as Array2DTracer } from './Array2DTracer';
export { default as Array1DTracer } from './Array1DTracer';
export { default as ChartTracer } from './ChartTracer';
export { default as GraphTracer } from './GraphTracer';
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册