diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a17cbe2 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +12.2.0 \ No newline at end of file diff --git a/package.json b/package.json index 0efdbb4..1cbb66d 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ }, "homepage": "https://github.com/neverix/html5-game-template#readme", "devDependencies": { + "@types/file-saver": "^2.0.1", + "@types/toastr": "^2.1.37", "css-loader": "^2.1.0", "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^3.0.1", @@ -34,11 +36,14 @@ "webpack-dev-server": "^3.2.0" }, "dependencies": { + "@eix/input": "git+https://github.com/eix-js/input.git", "@eix/utils": "git+https://github.com/eix-js/utils.git", + "file-saver": "^2.0.2", "haunted": "^4.3.0", "lit-html": "^1.0.0", "lit-rx": "0.0.2", "prelude-ts": "^0.8.2", - "rxjs": "^6.5.2" + "rxjs": "^6.5.2", + "toastr": "^2.1.4" } } diff --git a/src/scss/base.scss b/src/scss/base.scss index 7e88921..487d817 100644 --- a/src/scss/base.scss +++ b/src/scss/base.scss @@ -1,3 +1,5 @@ +@import "./toastr.scss"; + html, body { padding: 0; margin: 0; @@ -7,8 +9,52 @@ html, body { } svg { - background-color: #444444; + background-color: #222222; height: 100%; width: 100%; display: block; +} + +.createBar { + z-index:10; + position: absolute; + top:0px; + left:0px; + width:100%; + height:100%; + background-color: rgba(0,0,0,0.5); + transition: all 0.6s ease-in-out 0s; + .topContainer { + height: 30%; + width: 100%; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + div{ + height:25%; + width:75%; + input{ + background-color: #444444; + color: white; + border: none; + font-size: 250%; + height:100%; + width:100%; + padding: 1%; + font-family: "roboto"; + } + } + } + opacity: 0; + visibility: hidden; +} +.createBar#shown{ + opacity: 1; + visibility: visible; +} + +.toasts{ + background-color: #000000; + box-shadow: 0 0 0px black !important; } \ No newline at end of file diff --git a/src/scss/toastr.scss b/src/scss/toastr.scss new file mode 100644 index 0000000..064afd0 --- /dev/null +++ b/src/scss/toastr.scss @@ -0,0 +1 @@ +.toast-title{font-weight:700}.toast-message{-ms-word-wrap:break-word;word-wrap:break-word}.toast-message a,.toast-message label{color:#FFF}.toast-message a:hover{color:#CCC;text-decoration:none}.toast-close-button{position:relative;right:-.3em;top:-.3em;float:right;font-size:20px;font-weight:700;color:#FFF;-webkit-text-shadow:0 1px 0 #fff;text-shadow:0 1px 0 #fff;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80);line-height:1}.toast-close-button:focus,.toast-close-button:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}.rtl .toast-close-button{left:-.3em;float:left;right:.3em}button.toast-close-button{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.toast-top-center{top:0;right:0;width:100%}.toast-bottom-center{bottom:0;right:0;width:100%}.toast-top-full-width{top:0;right:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{top:12px;left:12px}.toast-top-right{top:12px;right:12px}.toast-bottom-right{right:12px;bottom:12px}.toast-bottom-left{bottom:12px;left:12px}#toast-container{position:fixed;z-index:999999;pointer-events:none}#toast-container *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}#toast-container>div{position:relative;pointer-events:auto;overflow:hidden;margin:0 0 6px;padding:15px 15px 15px 50px;width:300px;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-position:15px center;background-repeat:no-repeat;-moz-box-shadow:0 0 12px #999;-webkit-box-shadow:0 0 12px #999;box-shadow:0 0 12px #999;color:#FFF;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}#toast-container>div.rtl{direction:rtl;padding:15px 50px 15px 15px;background-position:right 15px center}#toast-container>div:hover{-moz-box-shadow:0 0 12px #000;-webkit-box-shadow:0 0 12px #000;box-shadow:0 0 12px #000;opacity:1;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);filter:alpha(opacity=100);cursor:pointer}#toast-container>.toast-info{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=)!important}#toast-container>.toast-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=)!important}#toast-container>.toast-success{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==)!important}#toast-container>.toast-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=)!important}#toast-container.toast-bottom-center>div,#toast-container.toast-top-center>div{width:300px;margin-left:auto;margin-right:auto}#toast-container.toast-bottom-full-width>div,#toast-container.toast-top-full-width>div{width:96%;margin-left:auto;margin-right:auto}.toast{background-color:#030303}.toast-success{background-color:#51A351}.toast-error{background-color:#BD362F}.toast-info{background-color:#2F96B4}.toast-warning{background-color:#F89406}.toast-progress{position:absolute;left:0;bottom:0;height:4px;background-color:#000;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}@media all and (max-width:240px){#toast-container>div{padding:8px 8px 8px 50px;width:11em}#toast-container>div.rtl{padding:8px 50px 8px 8px}#toast-container .toast-close-button{right:-.2em;top:-.2em}#toast-container .rtl .toast-close-button{left:-.2em;right:.2em}}@media all and (min-width:241px) and (max-width:480px){#toast-container>div{padding:8px 8px 8px 50px;width:18em}#toast-container>div.rtl{padding:8px 50px 8px 8px}#toast-container .toast-close-button{right:-.2em;top:-.2em}#toast-container .rtl .toast-close-button{left:-.2em;right:.2em}}@media all and (min-width:481px) and (max-width:768px){#toast-container>div{padding:15px 15px 15px 50px;width:25em}#toast-container>div.rtl{padding:15px 50px 15px 15px}} \ No newline at end of file diff --git a/src/ts/common/component/component.ts b/src/ts/common/component/component.ts index 1294ec4..cce6247 100644 --- a/src/ts/common/component/component.ts +++ b/src/ts/common/component/component.ts @@ -1,29 +1,98 @@ import { Vector } from "prelude-ts" -import { Subject, BehaviorSubject } from "rxjs"; -import { ComponentState } from "./interfaces"; -import { map } from "rxjs/operators"; +import { Subject, BehaviorSubject, Subscription, timer } from "rxjs"; +import { ComponentState, activationContext } from "./interfaces"; +import { map, debounce } from "rxjs/operators"; import { Screen } from "../screen.ts"; +import { ComponentTemplateStore } from "../componentManager/componentTemplateStore"; +import { svg } from "lit-html"; +import { subscribe } from "lit-rx"; +import { Pin } from "../pin"; +import { success, error } from "toastr" +import { alertOptions } from "../componentManager/alertOptions"; +import { WireManager } from "../wires"; +import { runCounter } from "./runCounter"; export class Component { + private static store = new ComponentTemplateStore() private static screen = new Screen() + private static wireManager = new WireManager() + private static lastId = runCounter.get() + 1 public position = new BehaviorSubject(null) public scale = new BehaviorSubject(null) public clicked = false private mouserDelta: number[] + private strokeColor = "#888888" + private inputs: number + private outputs: number + private activation: (ctx: activationContext) => any + private subscriptions:Subscription[] = [] - constructor(public activationType: string, + public inputPins: Pin[] = [] + public outputPins: Pin[] = [] + + public id: number + + constructor(private template: string, position: [number, number] = [0, 0], - scale: [number, number] = [0, 0]) { + scale: [number, number] = [0, 0], + id? : number) { + + //set initial props this.position.next(position) this.scale.next(scale) + + //set the correct id + this.id = (typeof id === "number") ? id : Component.lastId++ + + //load template + const data = Component.store.store.get(template) + + if (!data) + throw new Error(`Template ${template} doesnt exist`) + + this.inputs = data.inputs + this.outputs = data.outputs + + this.inputPins = [...Array(this.inputs)].fill(true).map(val => new Pin(false, this)) + this.outputPins = [...Array(this.outputs)].fill(true).map(val => new Pin(true, this)) + + this.activation = new Function(`return (ctx) => { + try{ + ${data.activation} + } + catch(err){ + ctx.error(err,"",ctx.alertOptions) + } + }`)() + + this.inputPins.forEach(val => { + const subscription = val.valueChanges.pipe(debounce(() => timer(1000 / 60))) + .subscribe(val => this.activate()) + this.subscriptions.push(subscription) + }) + + this.activate() } - handleMouseUp(e: MouseEvent) { + public dispose(){ + this.subscriptions.forEach(val => val.unsubscribe()) + } + + public handleMouseUp(e: MouseEvent) { this.clicked = false } + private activate() { + this.activation({ + outputs: this.outputPins, + inputs: this.inputPins, + succes: (mes: string) => { success(mes, "", alertOptions) }, + error: (mes: string) => { error(mes, "", alertOptions) } + } as activationContext) + } + move(e: MouseEvent) { const mousePosition = Component.screen.getWorldPosition(e.clientX, e.clientY) this.position.next(mousePosition.map((value, index) => @@ -40,11 +109,16 @@ export class Component { this.clicked = true } + handlePinClick(e: MouseEvent, pin: Pin) { + Component.wireManager.add(pin) + } + get state(): ComponentState { return { position: this.position.value as [number, number], - scale: this.position.value as [number, number], - activationType: this.activationType + scale: this.scale.value as [number, number], + template: this.template, + id: this.id } } @@ -70,7 +144,57 @@ export class Component { )) } - static fromState(state:ComponentState){ - return new Component(state.activationType, state.position, state.scale) + pinsSvg(pinScale: number, pinLength = 20, mode = "input") { + const stroke = 3 + + return ((mode === "input") ? this.inputPins : this.outputPins) + .map((val, index) => { + const y = subscribe(this.piny(mode === "input",index)) + + const x = subscribe(this.pinx(mode === "input",pinLength)) + + const linex = subscribe(this.x.pipe(map(val => + val + ((mode === "input") ? -pinLength : pinLength + this.scale.value[0]) + ))) + + const middleX = subscribe(this.x.pipe(map(val => + val + this.scale.value[0] / 2 + ))) + + return svg` + + + this.handlePinClick(e, val)} + > + `}) + } + + public pinx(mode = true, pinLength = 15){ + return this.x.pipe( + map(val => val + ( + (mode) ? + -pinLength : + this.scale.value[0] + pinLength + )) + ) + } + + public piny(mode = true, index: number){ + const space = this.scale.value[1] / (mode ? this.inputs : this.outputs) + return this.y.pipe( + map(val => val + space * (2 * index + 1) / 2) + ) + } + + static fromState(state: ComponentState) { + return new Component(state.template, state.position, state.scale, state.id) } } \ No newline at end of file diff --git a/src/ts/common/component/interfaces.ts b/src/ts/common/component/interfaces.ts index 6b4ca7a..73db1cb 100644 --- a/src/ts/common/component/interfaces.ts +++ b/src/ts/common/component/interfaces.ts @@ -1,5 +1,15 @@ +import { Pin } from "../pin"; + export interface ComponentState { position: [number,number] scale: [number,number] - activationType: string + template: string + id: number +} + +export interface activationContext { + inputs: Pin[] + outputs: Pin[] + succes: (mes: string) => any + error: (mes:string) => any } \ No newline at end of file diff --git a/src/ts/common/component/runCounter.ts b/src/ts/common/component/runCounter.ts new file mode 100644 index 0000000..8bc0abc --- /dev/null +++ b/src/ts/common/component/runCounter.ts @@ -0,0 +1,14 @@ +import { Store } from "../store"; + +export const runCounter = { + store: new Store("runCounter"), + get(){ + return runCounter.store.get("main") + }, + increase(){ + runCounter.store.set("main", runCounter.store.get("main") + 1) + } +} + +if (!runCounter.get()) + runCounter.store.set("main",1) \ No newline at end of file diff --git a/src/ts/common/componentManager/alertOptions.ts b/src/ts/common/componentManager/alertOptions.ts new file mode 100644 index 0000000..e36f379 --- /dev/null +++ b/src/ts/common/componentManager/alertOptions.ts @@ -0,0 +1,4 @@ +export const alertOptions = { + positionClass: "toast-bottom-right", + toastClass: "toasts" +} \ No newline at end of file diff --git a/src/ts/common/componentManager/componentManager.ts b/src/ts/common/componentManager/componentManager.ts index 21eb38e..50edc67 100644 --- a/src/ts/common/componentManager/componentManager.ts +++ b/src/ts/common/componentManager/componentManager.ts @@ -1,22 +1,222 @@ import { Singleton } from "@eix/utils"; -import { Component } from "../component/component"; -import { Subject } from "rxjs"; +import { Component } from "../component"; +import { Subject, BehaviorSubject, fromEvent } from "rxjs"; import { svg, SVGTemplateResult } from "lit-html"; import { subscribe } from "lit-rx"; import { Screen } from "../screen.ts"; -import { MnanagerState } from "./interfaces"; +import { ManagerState } from "./interfaces"; +import { Store } from "../store"; +import { KeyboardInput } from "@eix/input" +import { success, error } from "toastr" +import { ComponentTemplateStore } from "./componentTemplateStore"; +import { alertOptions } from "./alertOptions"; +import { WireManager } from "../wires"; +import { runCounter } from "../component/runCounter"; +import { Settings } from "../store/settings"; +import { download } from "./download"; @Singleton export class ComponentManager { public components: Component[] = [] - public svgs = new Subject() + public svgs = new Subject() + public placeholder = new BehaviorSubject("Create simulation") + private temporaryCommnad = "" private onTop: Component private clicked = false + private screen = new Screen() + private wireManager = new WireManager() + private templateStore = new ComponentTemplateStore() + private settings = new Settings() + + private commandHistoryStore = new Store("commandHistory") + private store = new Store("simulationStates") + + private saveEvent = new KeyboardInput("s") + private createEvent = new KeyboardInput("m") + private closeInputEvent = new KeyboardInput("enter") + private ctrlEvent = new KeyboardInput("ctrl") + private palleteEvent = new KeyboardInput("p") + private shiftEvent = new KeyboardInput("shift") + private refreshEvent = new KeyboardInput("r") + private clearEvent = new KeyboardInput("c") + private upEvent = new KeyboardInput("up") + private downEvent = new KeyboardInput("down") + + public name = "current" + public alertOptions = alertOptions + + private commandHistory: string[] = [] + private commands: { + [key: string]: (ctx: ComponentManager, args: string[], flags: string[]) => any + } = { + clear(ctx: ComponentManager) { + ctx.clear() + }, + save(ctx: ComponentManager) { + ctx.save() + }, + ls(ctx: ComponentManager) { + const data = ctx.store.ls() + const message = data.join("\n") + + success(message, "", ctx.alertOptions) + }, + help(ctx: ComponentManager) { + success(`Usage: <command>
+ Where <command> is one of: +
    + ${Object.keys(ctx.commands).map(val => ` +
  • ${val}
  • + `).join("")} +
+ `, "", ctx.alertOptions) + }, + refresh(ctx: ComponentManager) { + ctx.refresh() + }, + ctp: this.templateStore.commands.template, + settings: this.settings.commands, + download + } + private inputMode: string + + public barAlpha = new BehaviorSubject("0"); constructor() { + runCounter.increase() + this.svgs.next(this.render()) + + this.refresh() + + fromEvent(document.body, "keydown").subscribe((e: KeyboardEvent) => { + if (this.barAlpha.value == "1") { + const elem = document.getElementById("nameInput") + elem.focus() + } + else { + e.preventDefault() + } + }) + + fromEvent(document.body, "keyup").subscribe((e: MouseEvent) => { + if (this.barAlpha.value === "1") { + if (this.closeInputEvent.value) + this.create() + else if (this.inputMode === "command") { + const elem = document.getElementById("nameInput") + if (this.upEvent.value) { + document.body.focus() + e.preventDefault() + const index = this.commandHistory.indexOf(elem.value) + + if (index) { + //save drafts + if (index === -1) + this.temporaryCommnad = elem.value + + const newIndex = (index === -1) ? this.commandHistory.length - 1 : index - 1 + elem.value = this.commandHistory[newIndex] + } + } + if (this.downEvent.value) { + document.body.focus() + e.preventDefault() + const index = this.commandHistory.indexOf(elem.value) + + if (index > -1) { + const maxIndex = this.commandHistory.length - 1 + elem.value = (index === maxIndex) ? this.temporaryCommnad : this.commandHistory[index + 1] + } + } + } + } + else { + if (this.ctrlEvent.value) { + if (this.createEvent.value) { + this.preInput() + this.inputMode = "create" + this.placeholder.next("Create simulation") + } + else if (this.shiftEvent.value && this.palleteEvent.value) { + this.preInput() + this.inputMode = "command" + this.placeholder.next("Command palette") + } + else if (this.clearEvent.value) { + this.clear() + } + else if (this.saveEvent.value) { + this.save() + } + else if (this.refreshEvent.value) { + this.refresh() + } + } + } + }) + + this.wireManager.update.subscribe(val => this.update()) + } + + preInput() { + const elem = document.getElementById("nameInput") + elem.value = "" + this.barAlpha.next("1") + } + + create() { + const elem = document.getElementById("nameInput") + this.barAlpha.next("0") + + if (this.inputMode == "create") + success(`Succesfully created simulation ${elem.value}`, "", this.alertOptions) + + else if (this.inputMode == "command") + this.eval(elem.value) + } + + eval(command: string) { + if (!this.commandHistory.includes(command)) // no duplicates + this.commandHistory.push(command) + + while (this.commandHistory.length > 10) // max of 10 elements + this.commandHistory.shift() + + const words = command.split(" ") + + if (words[0] in this.commands) { + const remaining = words.slice(1) + const flags = remaining.filter(val => val[0] == "-") + const args = remaining.filter(val => val[0] != "-") + this.commands[words[0]](this, args, flags) + } + else + error(`Command ${words} doesn't exist. Run help to get a list of all commands.`, + "", this.alertOptions) + } + + clear() { + this.components = [] + this.wireManager.dispose() + this.update() + + success("Succesfully cleared all components", "", this.alertOptions) + } + + refresh() { + if (this.store.get(this.name)) { + this.loadState(this.store.get(this.name)) + } + + for (const i of this.commandHistoryStore.ls()) + this.commandHistory[Number(i)] = this.commandHistoryStore.get(i) + + this.update() + + success("Succesfully refreshed to the latest save", "", this.alertOptions) } update() { @@ -64,25 +264,32 @@ export class ComponentManager { render() { let toRemoveDuplicatesFor: Component + const size = 10 const result = this.components.map(component => svg` - component.handleClick(e)} - @mouseup=${ (e: MouseEvent) => { + + ${component.pinsSvg(10, 20)} + ${component.pinsSvg(10, 20, "output")} + + component.handleClick(e)} + @mouseup=${(e: MouseEvent) => { component.handleMouseUp(e) toRemoveDuplicatesFor = component - }} - > + }}> + `); if (toRemoveDuplicatesFor) this.removeDuplicates(toRemoveDuplicatesFor) - return result + return svg`${this.wireManager.svg} ${result}` } private removeDuplicates(component: Component) { @@ -95,21 +302,47 @@ export class ComponentManager { .filter((val, index) => instances.indexOf(index) != -1) } - get state(): MnanagerState { + get state(): ManagerState { const components = Array.from((new Set(this.components)).values()) return { - components: components.map(value => value.state) + components: components.map(value => value.state), + position: this.screen.position as [number, number], + scale: this.screen.scale as [number, number], + wires: this.wireManager.state } } - loadState(state:MnanagerState) { + public getComponentById(id: number) { + return this.components.find(val => val.id === id) + } + + loadState(state: ManagerState) { + if (!state.wires) //old state + return + + this.wireManager.dispose() this.clicked = false this.components = state.components.map(value => Component.fromState(value)) this.onTop = null + + state.wires.forEach(val => { + this.wireManager.start = this.getComponentById(val.from.owner).outputPins[val.from.index] + this.wireManager.end = this.getComponentById(val.to.owner).inputPins[val.to.index] + this.wireManager.tryResolving() + }) + + this.screen.scale = state.scale + this.screen.position = state.position + this.update() } - save(){ - //TODO: implement + save(name?: string) { + for (let i = 0; i < this.commandHistory.length; i++) { + const element = this.commandHistory[i]; + this.commandHistoryStore.set(i.toString(), element) + } + this.store.set(name || this.name, this.state) + success("Saved the simulation succesfully!", "", this.alertOptions) } } \ No newline at end of file diff --git a/src/ts/common/componentManager/componentTemplateStore.ts b/src/ts/common/componentManager/componentTemplateStore.ts new file mode 100644 index 0000000..2ed960e --- /dev/null +++ b/src/ts/common/componentManager/componentTemplateStore.ts @@ -0,0 +1,105 @@ +import { Singleton } from "@eix/utils"; +import { Store } from "../store"; +import { ComponentTemplate } from "./interfaces"; +import { ComponentManager } from "./componentManager"; +import { success, error } from "toastr" + +@Singleton +export class ComponentTemplateStore { + public store = new Store("componentTemplate") + + public commands = { + template: (ctx: ComponentManager, args: string[], flags: string[]) => { + const command = args[0] + switch (command) { + case (undefined): + for (let i of flags) { + if (i === "--version" || i === "-v") + return success("1.0.1", "", ctx.alertOptions) + } + + error(`Welcome to the component template program! + To get started, try running this basic commands: + ${["--version", "ls"].map(val => `${val}`).join(" ")} + `, "", { + ...ctx.alertOptions, + timeOut: 7500 + }) + + break + case ("ls"): + success(`Here is a list of all the current registered component templates (including ics): +
    + ${this.store.ls().map(val => ` +
  • + ${val} +
  • + `).join(" ")} +
+ `, "", ctx.alertOptions) + break + case ("info"): + if (!args[1]) + return error("You need to specify a template name", "", ctx.alertOptions) + + const data = this.store.get(args[1]) + + if (!data) + return error(`Component ${args[1]} doesnt exist`, "", ctx.alertOptions) + + const showFunction = flags.find(value => + value === "-sf" || value === "--showFunctions" + ) + + success(` + Name: ${data.name}
+ Inputs: ${data.inputs}
+ Outputs: ${data.outputs} + ${showFunction ? `
Activation: ${data.activation}` : ""} + `, "", ctx.alertOptions) + break + default: + error(`${command} is not a valid command for the template program`, "", ctx.alertOptions) + } + } + } + + constructor() { + this.store.set("buffer", { + inputs: 1, + outputs: 1, + name: "buffer", + version: "1.0.0", + activation: ` + ctx.outputs[0].value = ctx.inputs[0].value + `.trim() + }) + this.store.set("not", { + inputs: 1, + outputs: 1, + name: "buffer", + version: "1.0.0", + activation: ` + ctx.outputs[0].value = !ctx.inputs[0].value + `.trim() + }) + this.store.set("and", { + inputs: 2, + outputs: 1, + name: "and", + version: "1.0.0", + activation: ` + ctx.outputs[0].value = ctx.inputs[0].value && ctx.inputs[1].value + `.trim() + }) + this.store.set("true", { + inputs: 0, + outputs: 1, + name: "true", + version: "1.0.0", + activation: ` + ctx.outputs[0].value = true + `.trim() + }) + } +} \ No newline at end of file diff --git a/src/ts/common/componentManager/download.ts b/src/ts/common/componentManager/download.ts new file mode 100644 index 0000000..938e944 --- /dev/null +++ b/src/ts/common/componentManager/download.ts @@ -0,0 +1,31 @@ +import { ComponentManager } from "./componentManager"; +import { success } from "toastr" +import { saveAs } from "file-saver" + +const version = "1.0.0" + +export const download = (ctx: ComponentManager, args: string[], flags:string[]) => { + //important flags + for (let i of flags) { + if (i === "--version" || i === "-v") + return success(`${version}`, "", ctx.alertOptions) + else if (i === "--help" || i === "-h") + return success(`Run "download" to download the save as a json file. + Flags:
    +
  • -v or --version to get the version.
  • +
  • -h or --help to get help (ou are reading that right now)
  • +
  • -s or --save to automatically save before downloading
  • +
`,"",ctx.alertOptions) + } + + const command = args[0] + + if (command === undefined){ + if (flags.includes("-s") || flags.includes("--save")) + ctx.save() + + const data = JSON.stringify(ctx.state) + saveAs(new Blob([data]), `${ctx.name}.json`) + + } +} \ No newline at end of file diff --git a/src/ts/common/componentManager/interfaces.ts b/src/ts/common/componentManager/interfaces.ts index 54b1bc5..53023e4 100644 --- a/src/ts/common/componentManager/interfaces.ts +++ b/src/ts/common/componentManager/interfaces.ts @@ -1,5 +1,17 @@ import { ComponentState } from "../component/interfaces"; +import { WireState } from "../wires/interface"; -export interface MnanagerState { +export interface ManagerState { components: ComponentState[] + scale: [number,number] + position: [number,number] + wires: WireState +} + +export interface ComponentTemplate { + name: string + version: string + activation: string + inputs: number + outputs: number } \ No newline at end of file diff --git a/src/ts/common/pin/index.ts b/src/ts/common/pin/index.ts new file mode 100644 index 0000000..9e72fac --- /dev/null +++ b/src/ts/common/pin/index.ts @@ -0,0 +1 @@ +export * from "./pin" \ No newline at end of file diff --git a/src/ts/common/pin/pin.ts b/src/ts/common/pin/pin.ts new file mode 100644 index 0000000..be35394 --- /dev/null +++ b/src/ts/common/pin/pin.ts @@ -0,0 +1,65 @@ +import { BehaviorSubject, Subject, Subscription } from "rxjs"; +import { map } from "rxjs/operators" +import clamp from "../clamp/clamp"; +import { Component } from "../component"; + +export class Pin { + private static lastId = 0 + + public pair: Pin + private subscriptions: Subscription[] = [] + + public id: number + public _value = 0 + public color = new BehaviorSubject<[number, number, number, number]>([0, 0, 0,0]) + public memory: any = {} + public valueChanges = new Subject() + + public svgColor = this.color.pipe(map(val => + `rgb(${val.join(",")})` + )) + + constructor(public allowWrite = true, public of: Component) { + this.setValue(0) + this.id = Pin.lastId++ + } + + get value() { + return this._value + } + + set value(value: number) { + if (!this.allowWrite) return + this.setValue(value) + + } + + public setValue(value: number) { + this._value = clamp(value, 0, 1) + this.valueChanges.next(this._value) + + const color: [number, number, number, number] = (value > 0.5) ? + [255, 216, 20, 1] : + [90, 90, 90, 1] + + this.color.next((this.pair) ? color : [0,0,0,0]) + } + + public bindTo(pin: Pin){ + this.pair = pin + const subscription = pin.valueChanges.subscribe(val => this.setValue(val)) + + this.subscriptions.push(subscription) + } + + public unbind(pin: Pin) { + if (this.pair == pin){ + this.pair = null + this.subscriptions.forEach(val => val.unsubscribe()) + } + } + + public update(){ + this.setValue(this._value) + } +} \ No newline at end of file diff --git a/src/ts/common/screen.ts/screen.ts b/src/ts/common/screen.ts/screen.ts index a2ca841..775ad34 100644 --- a/src/ts/common/screen.ts/screen.ts +++ b/src/ts/common/screen.ts/screen.ts @@ -10,13 +10,13 @@ export class Screen { viewBox = combineLatest(this.width, this.height).pipe(map((values: [number,number]) => this.getViewBox(...values) )); - - private scrollStep = 1.3 - private position = [0, 0] + + public position = [0, 0] public scale = [2, 2] private zoomLimits: [number,number] = [0.1,10] + private scrollStep = 1.3 public mousePosition = [this.width.value / 2, this.height.value / 2] constructor() { diff --git a/src/ts/common/store/index.ts b/src/ts/common/store/index.ts new file mode 100644 index 0000000..017c10d --- /dev/null +++ b/src/ts/common/store/index.ts @@ -0,0 +1 @@ +export * from "./store" \ No newline at end of file diff --git a/src/ts/common/store/settings.ts b/src/ts/common/store/settings.ts new file mode 100644 index 0000000..e3a9a57 --- /dev/null +++ b/src/ts/common/store/settings.ts @@ -0,0 +1,29 @@ +import { Singleton } from "@eix/utils"; +import { ComponentManager } from "../componentManager"; +import { success, error } from "toastr" + +@Singleton +export class Settings { + version = "1.0.0" + + commands = (ctx: ComponentManager, args: string[], flags: string[]) => { + //important flags + for (let i of flags) { + if (i === "--version" || i === "-v") + return success(`${this.version}`, "", ctx.alertOptions) + } + + const command = args[0] + + if (command === undefined) + return success( + `Welcome to the settings cli. You can use this to tweak settings in any way imaginable!`, + "", + ctx.alertOptions) + + //nothing here + error(`Commands ${args} couldnt be found`,"",ctx.alertOptions) + } + + constructor() { } +} \ No newline at end of file diff --git a/src/ts/common/store/store.ts b/src/ts/common/store/store.ts new file mode 100644 index 0000000..284fe24 --- /dev/null +++ b/src/ts/common/store/store.ts @@ -0,0 +1,28 @@ +export class Store { + constructor(private name: string){ } + + get(key:string):T{ + const data = localStorage[`${this.name}/${key}`] + + if(data) + return JSON.parse(data).value + + return null + } + + set(key:string,value:T){ + localStorage[`${this.name}/${key}`] = JSON.stringify({ value }) + return this + } + + ls() { + let keys = [] + + for (const i in localStorage){ + if (i.indexOf(this.name) == 0) + keys.push(i.substr(this.name.length + 1)) + } + + return keys + } +} \ No newline at end of file diff --git a/src/ts/common/wires/index.ts b/src/ts/common/wires/index.ts new file mode 100644 index 0000000..143b649 --- /dev/null +++ b/src/ts/common/wires/index.ts @@ -0,0 +1 @@ +export * from "./wireManager" \ No newline at end of file diff --git a/src/ts/common/wires/interface.ts b/src/ts/common/wires/interface.ts new file mode 100644 index 0000000..3ee8f50 --- /dev/null +++ b/src/ts/common/wires/interface.ts @@ -0,0 +1,12 @@ +export interface WireStateVal { + from: { + owner: number + index: number + }, + to: { + owner: number + index: number + } +} + +export type WireState = WireStateVal[] \ No newline at end of file diff --git a/src/ts/common/wires/wire.ts b/src/ts/common/wires/wire.ts new file mode 100644 index 0000000..9f6d5c7 --- /dev/null +++ b/src/ts/common/wires/wire.ts @@ -0,0 +1,15 @@ +import { Pin } from "../pin"; + +export class Wire { + constructor (public input:Pin,public output:Pin){ + this.output.bindTo(this.input) + this.input.pair = this.output + this.input.update() + this.output.update() + } + + public dispose(){ + this.output.unbind(this.input) + this.input.pair = null + } +} \ No newline at end of file diff --git a/src/ts/common/wires/wireManager.ts b/src/ts/common/wires/wireManager.ts new file mode 100644 index 0000000..05ba719 --- /dev/null +++ b/src/ts/common/wires/wireManager.ts @@ -0,0 +1,81 @@ +import { Singleton } from "@eix/utils"; +import { Pin } from "../pin"; +import { Wire } from "./wire"; +import { svg } from "lit-html"; +import { subscribe } from "lit-rx"; +import { ComponentManager } from "../componentManager"; +import { Observable, Subject } from "rxjs"; +import { WireState, WireStateVal } from "./interface"; + +@Singleton +export class WireManager { + public start: Pin + public end: Pin + + private wires: Wire[] = [] + + public update = new Subject() + + constructor() { } + + public add(data: Pin) { + if (data.allowWrite) //output + this.start = data + else + this.end = data + + this.tryResolving() + } + + public dispose(){ + for (let i of this.wires) + i.dispose() + + this.wires = [] + } + + public tryResolving() { + if (this.start && this.end && this.start != this.end) { + if (this.canBind(this.start, this.end)) { + this.wires.push(new Wire(this.start, this.end)) + this.start = null + this.end = null + this.update.next(true) + } + } + } + + private canBind(start: Pin, end: Pin) { + if (this.wires.find(val => val.output === end)) + return false + return true + } + + get svg() { + return this.wires.map(val => { + const i = val.input.of + const o = val.output.of + return svg` + + + `}) + } + + get state() { + return this.wires.map((val):WireStateVal => ({ + from: { + owner: val.input.of.id, + index: val.input.of.outputPins.indexOf(val.input) + }, + to: { + owner: val.output.of.id, + index: val.output.of.inputPins.indexOf(val.output) + } + })) + } +} \ No newline at end of file diff --git a/src/ts/main.ts b/src/ts/main.ts index 9e7449f..53789d3 100644 --- a/src/ts/main.ts +++ b/src/ts/main.ts @@ -4,37 +4,59 @@ import { Screen } from "./common/screen.ts"; import { Component } from "./common/component"; import { FunctionStore } from "./common/activation/activationStore"; import { ComponentManager } from "./common/componentManager"; +import { map } from "rxjs/operators"; const screen = new Screen() -const test = new FunctionStore() -test.register("buffer",(data) => { - return true; -}) - const manager = new ComponentManager() -manager.components.push(new Component("none",[200,100],[200,30])) -manager.components.push(new Component("none",[300,100],[200,30])) -manager.components.push(new Component("none",[400,100],[200,30])) -manager.components.push(new Component("none",[500,100],[200,30])) -manager.components.push(new Component("none",[600,100],[200,30])) +manager.components.push(new Component("and",[200,100],[100,100])) +manager.components.push(new Component("not",[200,500],[100,100])) +manager.components.push(new Component("true",[200,500],[100,100])) manager.update() -console.log(manager.state) + +const handleEvent = (e:T,func:(e:T) => any) => { + if (manager.barAlpha.value == "0") + func(e) + else if (manager.barAlpha.value == "1" + && (e as unknown as MouseEvent).type == "mousedown" + && (e as unknown as MouseEvent).target != document.getElementById("nameInput")) + manager.barAlpha.next("0") +} render(html` - { +
handleEvent(e,(e:MouseEvent) => { manager.handleMouseMove(e) screen.updateMouse(e) - }} - viewBox=${subscribe(screen.viewBox)} - @mousedown=${(e:MouseEvent) => manager.handleMouseDown(e)} - @mouseup=${(e:MouseEvent) => manager.handleMouseUp(e)} - @wheel=${(e:WheelEvent) => screen.handleScroll(e)} - > - ${ subscribe(manager.svgs) } - + })} + @mousedown=${(e:MouseEvent) => handleEvent(e,(e:MouseEvent) => + manager.handleMouseDown(e) + )} + @mouseup=${(e:MouseEvent) => handleEvent(e,(e:MouseEvent) => + manager.handleMouseUp(e) + )} + @wheel=${(e:MouseEvent) => handleEvent(e,(e:WheelEvent) => + screen.handleScroll(e) + )}> + +
+ (val == "1") ? "shown" : "" + )))} + class=createBar> +
+
+ +
+
+
+ + ${ subscribe(manager.svgs) } + +
`, document.body) manager.update() \ No newline at end of file