2017-12-18 22:04:17 +01:00
|
|
|
/// <reference path="./view.ts" />
|
2017-12-22 23:00:23 +01:00
|
|
|
/// <reference path="./nodes/moduleView.ts" />
|
2017-12-18 22:04:17 +01:00
|
|
|
/// <reference path="./nodes/portView.ts" />
|
|
|
|
|
|
|
|
namespace pxsim.visuals {
|
|
|
|
export const BRICK_HEIGHT_RATIO = 1 / 3;
|
|
|
|
export const MODULE_AND_WIRING_HEIGHT_RATIO = 1 / 3; // For inputs and outputs
|
|
|
|
|
2017-12-22 23:00:23 +01:00
|
|
|
export const MODULE_HEIGHT_RATIO = MODULE_AND_WIRING_HEIGHT_RATIO * 4 / 5;
|
|
|
|
export const WIRING_HEIGHT_RATIO = MODULE_AND_WIRING_HEIGHT_RATIO / 5;
|
2017-12-18 22:04:17 +01:00
|
|
|
|
|
|
|
export const MODULE_INNER_PADDING_RATIO = 1 / 35;
|
|
|
|
|
2017-12-22 23:00:23 +01:00
|
|
|
export const MAX_MODULE_WIDTH = 100;
|
|
|
|
|
2017-12-18 22:04:17 +01:00
|
|
|
export interface LayoutElement extends View {
|
|
|
|
getId(): number;
|
|
|
|
getPort(): number;
|
|
|
|
getPaddingRatio(): number;
|
|
|
|
getWiringRatio(): number;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class LayoutView extends ViewContainer {
|
|
|
|
private inputs: LayoutElement[] = [];
|
|
|
|
private outputs: LayoutElement[] = [];
|
|
|
|
|
2017-12-22 23:00:23 +01:00
|
|
|
private inputContainers: ViewContainer[] = [];
|
|
|
|
private outputContainers: ViewContainer[] = [];
|
|
|
|
|
|
|
|
private inputControls: View[] = [];
|
|
|
|
private outputControls: View[] = [];
|
|
|
|
|
|
|
|
private inputCloseIcons: View[] = [];
|
|
|
|
private outputCloseIcons: View[] = [];
|
|
|
|
|
2017-12-18 22:04:17 +01:00
|
|
|
private inputWires: WireView[] = [];
|
|
|
|
private outputWires: WireView[] = [];
|
|
|
|
|
2018-04-10 01:21:41 +02:00
|
|
|
private brick: BrickViewPortrait;
|
|
|
|
private brickLandscape: BrickViewLandscape;
|
|
|
|
private brickInLandscape: boolean;
|
2018-01-31 07:22:21 +01:00
|
|
|
|
2017-12-18 22:04:17 +01:00
|
|
|
private offsets: number[];
|
|
|
|
private contentGroup: SVGGElement;
|
|
|
|
private scrollGroup: SVGGElement;
|
|
|
|
private renderedViews: Map<boolean> = {};
|
|
|
|
private hasDimensions = false;
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
|
2017-12-22 23:00:23 +01:00
|
|
|
this.outputContainers = [new ViewContainer(), new ViewContainer, new ViewContainer(), new ViewContainer()];
|
|
|
|
this.inputContainers = [new ViewContainer(), new ViewContainer, new ViewContainer(), new ViewContainer()];
|
2017-12-18 22:04:17 +01:00
|
|
|
|
2018-04-10 01:21:41 +02:00
|
|
|
this.brick = new BrickViewPortrait(0);
|
|
|
|
this.brickLandscape = new BrickViewLandscape(0);
|
2017-12-18 22:04:17 +01:00
|
|
|
|
|
|
|
for (let port = 0; port < DAL.NUM_OUTPUTS; port++) {
|
|
|
|
this.outputWires[port] = new WireView(port);
|
|
|
|
}
|
|
|
|
for (let port = 0; port < DAL.NUM_INPUTS; port++) {
|
|
|
|
this.inputWires[port] = new WireView(port);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public layout(width: number, height: number) {
|
|
|
|
this.hasDimensions = true;
|
2017-12-22 23:00:23 +01:00
|
|
|
this.resize(width, height);
|
2017-12-18 22:04:17 +01:00
|
|
|
this.scrollGroup.setAttribute("width", width.toString());
|
|
|
|
this.scrollGroup.setAttribute("height", height.toString());
|
|
|
|
this.position();
|
|
|
|
}
|
|
|
|
|
2018-04-10 01:21:41 +02:00
|
|
|
public setBrick(brick: BrickView) {
|
2017-12-18 22:04:17 +01:00
|
|
|
this.brick = brick;
|
2017-12-22 23:00:23 +01:00
|
|
|
this.brick.inject(this.scrollGroup);
|
2018-04-10 01:21:41 +02:00
|
|
|
this.brickLandscape.inject(this.scrollGroup);
|
|
|
|
this.brick.setSelected(false);
|
|
|
|
this.brickLandscape.setSelected(true);
|
|
|
|
this.brickLandscape.setVisible(false);
|
2018-04-13 23:01:37 +02:00
|
|
|
this.position();
|
2017-12-18 22:04:17 +01:00
|
|
|
}
|
|
|
|
|
2018-04-10 01:21:41 +02:00
|
|
|
public isBrickLandscape() {
|
|
|
|
return this.brickInLandscape;
|
|
|
|
}
|
|
|
|
|
2017-12-18 22:04:17 +01:00
|
|
|
public getBrick() {
|
2018-04-13 23:01:37 +02:00
|
|
|
return this.brickInLandscape ? this.getLandscapeBrick() : this.getPortraitBrick();
|
|
|
|
}
|
|
|
|
|
|
|
|
public getPortraitBrick() {
|
|
|
|
return this.brick;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getLandscapeBrick() {
|
|
|
|
return this.brickLandscape;
|
2017-12-18 22:04:17 +01:00
|
|
|
}
|
|
|
|
|
2018-01-31 07:22:21 +01:00
|
|
|
public unselectBrick() {
|
|
|
|
this.brick.setSelected(false);
|
2018-04-10 01:21:41 +02:00
|
|
|
this.brickLandscape.setSelected(true);
|
|
|
|
this.brickLandscape.setVisible(false);
|
|
|
|
this.brickInLandscape = false;
|
2018-01-31 07:22:21 +01:00
|
|
|
this.position();
|
|
|
|
}
|
|
|
|
|
2018-04-10 01:21:41 +02:00
|
|
|
public setlectBrick() {
|
2018-01-31 07:22:21 +01:00
|
|
|
this.brick.setSelected(true);
|
2018-04-10 01:21:41 +02:00
|
|
|
this.brickLandscape.setSelected(false);
|
|
|
|
this.brickLandscape.setVisible(true);
|
|
|
|
this.brickInLandscape = true;
|
2018-01-31 07:22:21 +01:00
|
|
|
this.position();
|
|
|
|
}
|
|
|
|
|
2018-04-10 01:21:41 +02:00
|
|
|
public toggleBrickSelect() {
|
|
|
|
const selected = this.brickInLandscape;
|
|
|
|
if (selected) this.unselectBrick();
|
|
|
|
else this.setlectBrick();
|
|
|
|
}
|
|
|
|
|
2017-12-22 23:00:23 +01:00
|
|
|
public setInput(port: number, view: LayoutElement, control?: View, closeIcon?: View) {
|
|
|
|
if (this.inputs[port] != view || this.inputControls[port] != control) {
|
|
|
|
if (this.inputs[port]) {
|
|
|
|
// Remove current input
|
|
|
|
this.inputs[port].dispose();
|
|
|
|
}
|
|
|
|
this.inputs[port] = view;
|
|
|
|
if (this.inputControls[port]) {
|
|
|
|
this.inputControls[port].dispose();
|
|
|
|
}
|
|
|
|
this.inputControls[port] = control;
|
|
|
|
this.inputCloseIcons[port] = closeIcon;
|
|
|
|
|
|
|
|
this.inputContainers[port].clear();
|
|
|
|
this.inputContainers[port].addView(view);
|
|
|
|
|
|
|
|
if (control) this.inputContainers[port].addView(control);
|
|
|
|
|
|
|
|
if (view.hasClick()) view.registerClick((ev: any) => {
|
|
|
|
view.setSelected(true);
|
|
|
|
runtime.queueDisplayUpdate();
|
|
|
|
}, true);
|
|
|
|
|
|
|
|
if (control && closeIcon) {
|
|
|
|
this.inputContainers[port].addView(closeIcon);
|
|
|
|
closeIcon.registerClick(() => {
|
|
|
|
// Clear selection
|
|
|
|
view.setSelected(false);
|
|
|
|
runtime.queueDisplayUpdate();
|
|
|
|
})
|
|
|
|
}
|
2017-12-18 22:04:17 +01:00
|
|
|
}
|
2017-12-22 23:00:23 +01:00
|
|
|
|
2017-12-18 22:04:17 +01:00
|
|
|
this.position();
|
|
|
|
}
|
|
|
|
|
2017-12-22 23:00:23 +01:00
|
|
|
public setOutput(port: number, view: LayoutElement, control?: View, closeIcon?: View) {
|
|
|
|
if (this.outputs[port] != view || this.outputControls[port] != control) {
|
|
|
|
if (this.outputs[port]) {
|
|
|
|
// Remove current output
|
|
|
|
this.outputs[port].dispose();
|
|
|
|
}
|
|
|
|
this.outputs[port] = view;
|
|
|
|
if (this.outputControls[port]) {
|
|
|
|
this.outputControls[port].dispose();
|
|
|
|
}
|
|
|
|
this.outputControls[port] = control;
|
|
|
|
this.outputCloseIcons[port] = closeIcon;
|
|
|
|
|
|
|
|
this.outputContainers[port].clear();
|
|
|
|
this.outputContainers[port].addView(view);
|
|
|
|
|
|
|
|
if (control) this.outputContainers[port].addView(control);
|
|
|
|
|
|
|
|
if (view.hasClick()) view.registerClick((ev: any) => {
|
|
|
|
view.setSelected(true);
|
|
|
|
runtime.queueDisplayUpdate();
|
|
|
|
}, true)
|
|
|
|
|
|
|
|
if (control && closeIcon) {
|
|
|
|
this.outputContainers[port].addView(closeIcon);
|
|
|
|
closeIcon.registerClick(() => {
|
|
|
|
// Clear selection
|
|
|
|
view.setSelected(false);
|
|
|
|
runtime.queueDisplayUpdate();
|
|
|
|
})
|
|
|
|
}
|
2017-12-18 22:04:17 +01:00
|
|
|
}
|
2017-12-22 23:00:23 +01:00
|
|
|
|
2017-12-18 22:04:17 +01:00
|
|
|
this.position();
|
|
|
|
}
|
|
|
|
|
2017-12-22 23:00:23 +01:00
|
|
|
protected buildDom() {
|
|
|
|
this.contentGroup = svg.elt("g") as SVGGElement;
|
|
|
|
this.scrollGroup = svg.child(this.contentGroup, "g") as SVGGElement;
|
2017-12-18 22:04:17 +01:00
|
|
|
|
2017-12-22 23:00:23 +01:00
|
|
|
this.inputs = [];
|
|
|
|
this.outputs = [];
|
|
|
|
this.inputControls = [];
|
|
|
|
this.outputControls = [];
|
2017-12-18 22:04:17 +01:00
|
|
|
|
2017-12-22 23:00:23 +01:00
|
|
|
// Inject all wires
|
|
|
|
for (let port = 0; port < DAL.NUM_OUTPUTS; port++) {
|
|
|
|
this.outputWires[port].inject(this.scrollGroup);
|
2017-12-18 22:04:17 +01:00
|
|
|
}
|
2017-12-22 23:00:23 +01:00
|
|
|
for (let port = 0; port < DAL.NUM_INPUTS; port++) {
|
|
|
|
this.inputWires[port].inject(this.scrollGroup);
|
2017-12-18 22:04:17 +01:00
|
|
|
}
|
|
|
|
|
2017-12-27 08:19:04 +01:00
|
|
|
// Inject all view containers
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
|
|
this.inputContainers[i].inject(this.scrollGroup);
|
|
|
|
this.outputContainers[i].inject(this.scrollGroup);
|
|
|
|
}
|
|
|
|
|
2017-12-22 23:00:23 +01:00
|
|
|
// Inject all ports
|
2018-01-09 23:43:01 +01:00
|
|
|
this.setInput(0, new PortView(0, '1'));
|
|
|
|
this.setInput(1, new PortView(1, '2'));
|
|
|
|
this.setInput(2, new PortView(2, '3'));
|
|
|
|
this.setInput(3, new PortView(3, '4'));
|
|
|
|
|
|
|
|
this.setOutput(0, new PortView(0, 'A'));
|
|
|
|
this.setOutput(1, new PortView(1, 'B'));
|
|
|
|
this.setOutput(2, new PortView(2, 'C'));
|
|
|
|
this.setOutput(3, new PortView(3, 'D'));
|
2017-12-22 23:00:23 +01:00
|
|
|
|
2017-12-18 22:04:17 +01:00
|
|
|
return this.contentGroup;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getInnerWidth() {
|
|
|
|
if (!this.hasDimensions) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return this.width;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getInnerHeight() {
|
|
|
|
if (!this.hasDimensions) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return this.height;
|
|
|
|
}
|
|
|
|
|
|
|
|
public updateTheme(theme: IBoardTheme) {
|
|
|
|
this.inputs.forEach(n => {
|
|
|
|
n.updateTheme(theme);
|
|
|
|
})
|
|
|
|
this.brick.updateTheme(theme);
|
2018-04-10 01:21:41 +02:00
|
|
|
this.brickLandscape.updateTheme(theme);
|
2017-12-18 22:04:17 +01:00
|
|
|
this.outputs.forEach(n => {
|
|
|
|
n.updateTheme(theme);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
private position() {
|
|
|
|
if (!this.hasDimensions) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.offsets = [];
|
|
|
|
|
2017-12-24 20:59:01 +01:00
|
|
|
const contentWidth = this.width;
|
|
|
|
if (!contentWidth) return;
|
|
|
|
const contentHeight = this.height;
|
|
|
|
if (!contentHeight) return;
|
2017-12-18 22:04:17 +01:00
|
|
|
|
2017-12-27 23:48:15 +01:00
|
|
|
const noConnections = this.outputs.concat(this.inputs).filter(m => m.getId() != NodeType.Port).length == 0;
|
|
|
|
|
2018-01-31 07:22:21 +01:00
|
|
|
this.outputs.concat(this.inputs).forEach(m => m.setVisible(true));
|
2017-12-27 23:48:15 +01:00
|
|
|
|
2017-12-18 22:04:17 +01:00
|
|
|
const moduleHeight = this.getModuleHeight();
|
|
|
|
|
|
|
|
const brickHeight = this.getBrickHeight();
|
|
|
|
const brickWidth = this.brick.getInnerWidth() / this.brick.getInnerHeight() * brickHeight;
|
|
|
|
const brickPadding = (contentWidth - brickWidth) / 2;
|
|
|
|
|
2017-12-22 23:00:23 +01:00
|
|
|
const modulePadding = this.getModulePadding();
|
2017-12-18 22:04:17 +01:00
|
|
|
const moduleSpacing = contentWidth / 4;
|
2017-12-22 23:00:23 +01:00
|
|
|
const moduleWidth = this.getInnerModuleWidth();
|
|
|
|
let currentX = this.getModulePadding();
|
2017-12-18 22:04:17 +01:00
|
|
|
let currentY = 0;
|
|
|
|
this.outputs.forEach((n, i) => {
|
2017-12-22 23:00:23 +01:00
|
|
|
this.outputContainers[i].translate(currentX, currentY);
|
|
|
|
if (this.outputs[i]) {
|
|
|
|
const view = this.outputs[i];
|
|
|
|
const outputPadding = this.getInnerModuleWidth() * view.getPaddingRatio();
|
|
|
|
const desiredOutputWidth = this.getInnerModuleWidth() - outputPadding * 2;
|
|
|
|
const outputWidth = Math.min(desiredOutputWidth, MAX_MODULE_WIDTH);
|
|
|
|
const outputHeight = this.getModuleHeight();
|
|
|
|
|
|
|
|
// Translate and resize view
|
|
|
|
view.resize(outputWidth, outputHeight);
|
|
|
|
const viewHeight = view.getInnerHeight() / view.getInnerWidth() * outputWidth;
|
|
|
|
view.translate(outputPadding + ((desiredOutputWidth - outputWidth) / 2), outputHeight - viewHeight, true);
|
|
|
|
|
|
|
|
// Resize control
|
|
|
|
const control = this.outputControls[i];
|
|
|
|
if (control) {
|
|
|
|
control.resize(this.getInnerModuleWidth(), outputHeight);
|
|
|
|
|
|
|
|
// Translate close icon
|
|
|
|
const closeIcon = this.outputCloseIcons[i];
|
|
|
|
if (closeIcon) {
|
|
|
|
const closeIconWidth = closeIcon.getWidth();
|
|
|
|
closeIcon.translate(this.getInnerModuleWidth() / 2 - closeIconWidth / 2, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-12-18 22:04:17 +01:00
|
|
|
currentX += moduleSpacing;
|
|
|
|
})
|
|
|
|
|
|
|
|
currentX = 0;
|
|
|
|
currentY = moduleHeight;
|
|
|
|
|
|
|
|
const wireBrickSpacing = brickWidth / 5;
|
2017-12-27 08:19:04 +01:00
|
|
|
const wiringYPadding = 5;
|
2017-12-18 22:04:17 +01:00
|
|
|
let wireStartX = 0;
|
|
|
|
let wireEndX = brickPadding + wireBrickSpacing;
|
|
|
|
let wireEndY = currentY + this.getWiringHeight() + wiringYPadding;
|
|
|
|
let wireStartY = currentY - wiringYPadding;
|
|
|
|
|
|
|
|
// Draw output lines
|
|
|
|
for (let port = 0; port < DAL.NUM_OUTPUTS; port++) {
|
|
|
|
this.outputWires[port].updateDimensions(wireStartX + moduleSpacing * this.outputs[port].getWiringRatio(), wireStartY, wireEndX, wireEndY);
|
|
|
|
this.outputWires[port].setSelected(this.outputs[port].getId() == NodeType.Port);
|
|
|
|
wireStartX += moduleSpacing;
|
|
|
|
wireEndX += wireBrickSpacing;
|
|
|
|
}
|
|
|
|
|
|
|
|
currentX = brickPadding;
|
|
|
|
currentY += this.getWiringHeight();
|
|
|
|
|
|
|
|
// Render the brick in the middle
|
2017-12-22 23:00:23 +01:00
|
|
|
this.brick.resize(brickWidth, brickHeight);
|
2017-12-18 22:04:17 +01:00
|
|
|
this.brick.translate(currentX, currentY);
|
2018-04-10 01:21:41 +02:00
|
|
|
this.brickLandscape.resize(contentWidth, brickHeight);
|
|
|
|
this.brickLandscape.translate((contentWidth - this.brickLandscape.getContentWidth()) / 2, currentY);
|
2017-12-18 22:04:17 +01:00
|
|
|
|
|
|
|
currentX = modulePadding;
|
|
|
|
currentY += brickHeight + this.getWiringHeight();
|
|
|
|
|
|
|
|
this.inputs.forEach((n, i) => {
|
2017-12-22 23:00:23 +01:00
|
|
|
this.inputContainers[i].translate(currentX, currentY);
|
|
|
|
if (this.inputs[i]) {
|
|
|
|
const view = this.inputs[i];
|
|
|
|
const inputPadding = this.getInnerModuleWidth() * view.getPaddingRatio();
|
|
|
|
const desiredInputWidth = this.getInnerModuleWidth() - inputPadding * 2;
|
|
|
|
const inputWidth = Math.min(desiredInputWidth, MAX_MODULE_WIDTH);
|
|
|
|
const inputHeight = this.getModuleHeight();
|
|
|
|
|
|
|
|
// Translate and resize view
|
|
|
|
view.resize(inputWidth, inputHeight);
|
|
|
|
view.translate(inputPadding + ((desiredInputWidth - inputWidth) / 2), 0, true);
|
|
|
|
|
|
|
|
// Resize control
|
|
|
|
const control = this.inputControls[i];
|
|
|
|
if (control) {
|
|
|
|
control.resize(this.getInnerModuleWidth(), inputHeight);
|
|
|
|
|
|
|
|
// Translate and resize close icon
|
|
|
|
const closeIcon = this.inputCloseIcons[i];
|
|
|
|
if (closeIcon) {
|
|
|
|
const closeIconWidth = closeIcon.getWidth();
|
|
|
|
const closeIconHeight = closeIcon.getHeight();
|
|
|
|
closeIcon.translate(this.getInnerModuleWidth() / 2 - closeIconWidth / 2, this.getModuleHeight() - closeIconHeight);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-12-18 22:04:17 +01:00
|
|
|
currentX += moduleSpacing;
|
|
|
|
})
|
|
|
|
|
|
|
|
wireStartX = moduleSpacing / 2;
|
|
|
|
wireEndX = brickPadding + wireBrickSpacing;
|
|
|
|
wireEndY = currentY - this.getWiringHeight() - wiringYPadding;
|
|
|
|
wireStartY = currentY + wiringYPadding;
|
|
|
|
|
|
|
|
// Draw input lines
|
|
|
|
for (let port = 0; port < DAL.NUM_INPUTS; port++) {
|
|
|
|
this.inputWires[port].updateDimensions(wireStartX, wireStartY, wireEndX, wireEndY);
|
|
|
|
this.inputWires[port].setSelected(this.inputs[port].getId() == NodeType.Port);
|
|
|
|
wireStartX += moduleSpacing;
|
|
|
|
wireEndX += wireBrickSpacing;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public getBrickHeight() {
|
2017-12-24 20:59:01 +01:00
|
|
|
return this.height * BRICK_HEIGHT_RATIO;
|
2017-12-18 22:04:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public getWiringHeight() {
|
2017-12-24 20:59:01 +01:00
|
|
|
return this.height * WIRING_HEIGHT_RATIO;
|
2017-12-18 22:04:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public getModuleBounds() {
|
|
|
|
return {
|
2017-12-24 20:59:01 +01:00
|
|
|
width: this.width / 4,
|
2017-12-18 22:04:17 +01:00
|
|
|
height: this.getModuleHeight()
|
|
|
|
}
|
|
|
|
}
|
2017-12-22 23:00:23 +01:00
|
|
|
|
|
|
|
public getModulePadding() {
|
|
|
|
return this.getModuleBounds().width / 35;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getInnerModuleWidth() {
|
|
|
|
return this.getModuleBounds().width - (this.getModulePadding() * 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
public getModuleHeight() {
|
2017-12-24 20:59:01 +01:00
|
|
|
return this.height * MODULE_HEIGHT_RATIO;
|
2017-12-22 23:00:23 +01:00
|
|
|
}
|
2017-12-18 22:04:17 +01:00
|
|
|
}
|
|
|
|
}
|