From d2400950201f8576ba095d8e0ba7907dccd0a19f Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Mon, 22 Apr 2024 18:28:59 -0700 Subject: [PATCH] refactor device.ts to split up code --- www/src/ts/app/nav.ts | 2 +- www/src/ts/app/save.ts | 4 +- www/src/ts/app/share.ts | 2 +- www/src/ts/app/welcome.ts | 4 +- www/src/ts/virtual_machine/base_device.ts | 3 +- www/src/ts/virtual_machine/controls.ts | 4 +- www/src/ts/virtual_machine/device.ts | 1100 ----------------- .../ts/virtual_machine/device/VMDeviceCard.ts | 458 +++++++ www/src/ts/virtual_machine/device/card.ts | 459 +++++++ .../ts/virtual_machine/device/device_list.ts | 339 +++++ www/src/ts/virtual_machine/device/index.ts | 23 + www/src/ts/virtual_machine/device/template.ts | 306 +++++ .../ts/virtual_machine/{ => device}/utils.ts | 2 +- www/src/ts/virtual_machine/index.ts | 3 +- www/src/ts/virtual_machine/registers.ts | 6 +- www/src/ts/virtual_machine/stack.ts | 6 +- www/src/ts/virtual_machine/ui.ts | 6 +- www/tsconfig.json | 2 + 18 files changed, 1607 insertions(+), 1122 deletions(-) delete mode 100644 www/src/ts/virtual_machine/device.ts create mode 100644 www/src/ts/virtual_machine/device/VMDeviceCard.ts create mode 100644 www/src/ts/virtual_machine/device/card.ts create mode 100644 www/src/ts/virtual_machine/device/device_list.ts create mode 100644 www/src/ts/virtual_machine/device/index.ts create mode 100644 www/src/ts/virtual_machine/device/template.ts rename www/src/ts/virtual_machine/{ => device}/utils.ts (89%) diff --git a/www/src/ts/app/nav.ts b/www/src/ts/app/nav.ts index a607159..6e27179 100644 --- a/www/src/ts/app/nav.ts +++ b/www/src/ts/app/nav.ts @@ -1,6 +1,6 @@ import { HTMLTemplateResult, html, css } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { BaseElement, defaultCss } from "../components"; +import { BaseElement, defaultCss } from "components"; import "@shoelace-style/shoelace/dist/components/icon/icon.js"; import "@shoelace-style/shoelace/dist/components/icon-button/icon-button.js"; diff --git a/www/src/ts/app/save.ts b/www/src/ts/app/save.ts index fd3a2fd..710814d 100644 --- a/www/src/ts/app/save.ts +++ b/www/src/ts/app/save.ts @@ -1,7 +1,7 @@ import { HTMLTemplateResult, html, css, CSSResultGroup } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; -import { BaseElement, defaultCss } from "../components"; -import { VMState } from "../session"; +import { BaseElement, defaultCss } from "components"; +import { VMState } from "session"; import "@shoelace-style/shoelace/dist/components/dialog/dialog.js"; import "@shoelace-style/shoelace/dist/components/format-date/format-date.js"; diff --git a/www/src/ts/app/share.ts b/www/src/ts/app/share.ts index e95ee62..4037d91 100644 --- a/www/src/ts/app/share.ts +++ b/www/src/ts/app/share.ts @@ -1,6 +1,6 @@ import { HTMLTemplateResult, html, css } from "lit"; import { customElement, property, query } from "lit/decorators.js"; -import { BaseElement, defaultCss } from "../components"; +import { BaseElement, defaultCss } from "components"; import "@shoelace-style/shoelace/dist/components/dialog/dialog.js"; import "@shoelace-style/shoelace/dist/components/input/input.js"; diff --git a/www/src/ts/app/welcome.ts b/www/src/ts/app/welcome.ts index 7b6b9a3..55c0c9a 100644 --- a/www/src/ts/app/welcome.ts +++ b/www/src/ts/app/welcome.ts @@ -1,7 +1,7 @@ import { html, css } from "lit"; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; -import { customElement, property, query, state } from "lit/decorators.js"; -import { BaseElement, defaultCss } from "../components"; +import { customElement, property, query } from "lit/decorators.js"; +import { BaseElement, defaultCss } from "components"; import { SlDialog, SlSwitch } from "@shoelace-style/shoelace"; import { until } from "lit/directives/until.js"; diff --git a/www/src/ts/virtual_machine/base_device.ts b/www/src/ts/virtual_machine/base_device.ts index bcc2127..bd0be50 100644 --- a/www/src/ts/virtual_machine/base_device.ts +++ b/www/src/ts/virtual_machine/base_device.ts @@ -13,9 +13,8 @@ import type { Defines, Pins, } from "ic10emu_wasm"; -import { structuralEqual } from "../utils"; +import { structuralEqual } from "utils"; import { LitElement } from "lit"; -import { BaseElement } from "../components/base"; type Constructor = new (...args: any[]) => T; diff --git a/www/src/ts/virtual_machine/controls.ts b/www/src/ts/virtual_machine/controls.ts index ae67c45..f5a3343 100644 --- a/www/src/ts/virtual_machine/controls.ts +++ b/www/src/ts/virtual_machine/controls.ts @@ -1,7 +1,7 @@ import { html, css } from "lit"; import { customElement, query } from "lit/decorators.js"; -import { BaseElement, defaultCss } from "../components"; -import { VMActiveICMixin } from "./base_device"; +import { BaseElement, defaultCss } from "components"; +import { VMActiveICMixin } from "virtual_machine/base_device"; import "@shoelace-style/shoelace/dist/components/card/card.js"; import "@shoelace-style/shoelace/dist/components/button-group/button-group.js"; diff --git a/www/src/ts/virtual_machine/device.ts b/www/src/ts/virtual_machine/device.ts deleted file mode 100644 index 57b8415..0000000 --- a/www/src/ts/virtual_machine/device.ts +++ /dev/null @@ -1,1100 +0,0 @@ -import type { - Connection, - DeviceTemplate, - LogicField, - LogicFields, - LogicType, - Slot, - SlotTemplate, - SlotOccupant, - SlotOccupantTemplate, - SlotLogicType, - ConnectionCableNetwork, - SlotType, -} from "ic10emu_wasm"; -import { html, css, HTMLTemplateResult } from "lit"; -import { customElement, property, query, state } from "lit/decorators.js"; -import { BaseElement, defaultCss } from "../components"; -import { VMDeviceMixin } from "./base_device"; - -import { default as uFuzzy } from "@leeoniya/ufuzzy"; - -import "@shoelace-style/shoelace/dist/components/card/card.js"; -import "@shoelace-style/shoelace/dist/components/icon/icon.js"; -import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js"; -import "@shoelace-style/shoelace/dist/components/input/input.js"; -import "@shoelace-style/shoelace/dist/components/details/details.js"; -import "@shoelace-style/shoelace/dist/components/tab/tab.js"; -import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js"; -import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js"; -import "@shoelace-style/shoelace/dist/components/copy-button/copy-button.js"; -import "@shoelace-style/shoelace/dist/components/select/select.js"; -import "@shoelace-style/shoelace/dist/components/badge/badge.js"; -import "@shoelace-style/shoelace/dist/components/option/option.js"; -import "@shoelace-style/shoelace/dist/components/drawer/drawer.js"; -import "@shoelace-style/shoelace/dist/components/icon/icon.js"; - -import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js"; -import { - displayNumber, - parseIntWithHexOrBinary, - parseNumber, - structuralEqual, -} from "../utils"; -import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.js"; -import SlDrawer from "@shoelace-style/shoelace/dist/components/drawer/drawer.js"; -import type { DeviceDB, DeviceDBEntry } from "./device_db"; -import { connectionFromDeviceDBConnection } from "./utils"; -import { SlDialog } from "@shoelace-style/shoelace"; -import { repeat } from "lit/directives/repeat.js"; -import { cache } from "lit/directives/cache.js"; - -@customElement("vm-device-card") -export class VMDeviceCard extends VMDeviceMixin(BaseElement) { - image_err: boolean; - - @property({ type: Boolean }) open: boolean; - - constructor() { - super(); - this.open = false; - } - - static styles = [ - ...defaultCss, - css` - :host { - display: block; - box-sizing: border-box; - } - .card { - width: 100%; - box-sizing: border-box; - } - .image { - width: 4rem; - height: 4rem; - } - .header { - display: flex; - flex-direction: row; - flex-grow: 1; - } - .header-name { - display: flex; - flex-direction: row; - width: 100%; - flex-grow: 1; - align-items: center; - flex-wrap: wrap; - } - .device-card { - --padding: var(--sl-spacing-small); - } - .device-name::part(input) { - width: 10rem; - } - .device-id::part(input) { - width: 7rem; - } - .device-name-hash::part(input) { - width: 7rem; - } - .slot-header.image { - width: 1.5rem; - height: 1.5rem; - border: var(--sl-panel-border-width) solid var(--sl-panel-border-color); - border-radius: var(--sl-border-radius-medium); - background-color: var(--sl-color-neutral-0); - } - sl-divider { - --spacing: 0.25rem; - } - sl-button[variant="success"] { - /* Changes the success theme color to purple using primitives */ - --sl-color-success-600: var(--sl-color-purple-700); - } - sl-button[variant="primary"] { - /* Changes the success theme color to purple using primitives */ - --sl-color-primary-600: var(--sl-color-cyan-600); - } - sl-button[variant="warning"] { - /* Changes the success theme color to purple using primitives */ - --sl-color-warning-600: var(--sl-color-amber-600); - } - sl-tab-group { - margin-left: 1rem; - margin-right: 1rem; - --indicator-color: var(--sl-color-purple-600); - --sl-color-primary-600: var(--sl-color-purple-600); - } - sl-tab::part(base) { - padding: var(--sl-spacing-small) var(--sl-spacing-medium); - } - sl-tab-group::part(base) { - max-height: 20rem; - overflow-y: auto; - } - sl-icon-button.remove-button::part(base) { - color: var(--sl-color-danger-600); - } - sl-icon-button.remove-button::part(base):hover, - sl-icon-button.remove-button::part(base):focus { - color: var(--sl-color-danger-500); - } - sl-icon-button.remove-button::part(base):active { - color: var(--sl-color-danger-600); - } - .remove-dialog-body { - display: flex; - flex-direction: row; - } - .dialog-image { - width: 3rem; - height: 3rem; - } - `, - ]; - - private _deviceDB: DeviceDB; - - get deviceDB(): DeviceDB { - return this._deviceDB; - } - - @state() - set deviceDB(val: DeviceDB) { - this._deviceDB = val; - this.updateDevice(); - this.requestUpdate(); - } - - connectedCallback(): void { - super.connectedCallback(); - window.VM.vm.addEventListener( - "vm-device-db-loaded", - this._handleDeviceDBLoad.bind(this), - ); - } - - _handleDeviceDBLoad(e: CustomEvent) { - this.deviceDB = e.detail; - } - - onImageErr(e: Event) { - this.image_err = true; - console.log("Image load error", e); - } - - renderHeader(): HTMLTemplateResult { - const activeIc = window.VM.vm.activeIC; - const thisIsActiveIc = activeIc.id === this.deviceID; - const badges: HTMLTemplateResult[] = []; - if (this.deviceID == activeIc?.id) { - badges.push(html`db`); - } - activeIc?.pins?.forEach((id, index) => { - if (this.deviceID == id) { - badges.push( - html`d${index}`, - ); - } - }, this); - return html` - - - -
- - Id - - - - Name - - - - Hash - - - ${badges.map((badge) => badge)} -
-
- - - -
- `; - } - - renderFields(): HTMLTemplateResult { - const fields = Array.from(this.fields.entries()); - const inputIdBase = `vmDeviceCard${this.deviceID}Field`; - return html` - ${fields.map(([name, field], _index, _fields) => { - return html` - ${name} - - ${field.field_type} - `; - })} - `; - } - - lookupSlotOccupantImg( - occupant: SlotOccupant | undefined, - typ: SlotType, - ): string { - if (typeof occupant !== "undefined") { - const hashLookup = (this.deviceDB ?? {}).names_by_hash ?? {}; - const prefabName = hashLookup[occupant.prefab_hash] ?? "UnknownHash"; - return `img/stationpedia/${prefabName}.png`; - } else { - return `img/stationpedia/SlotIcon_${typ}.png`; - } - } - - _onSlotImageErr(e: Event) { - console.log("image_err", e); - } - - static transparentImg = - "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" as const; - - renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult { - const _fields = this.device.getSlotFields(slotIndex); - const fields = Array.from(_fields.entries()); - const inputIdBase = `vmDeviceCard${this.deviceID}Slot${slotIndex}Field`; - const slotImg = this.lookupSlotOccupantImg(slot.occupant, slot.typ); - return html` - - - ${slotIndex} : ${slot.typ} - ${ - typeof slot.occupant !== "undefined" - ? html` - - Occupant: ${slot.occupant.id} : ${slot.occupant.prefab_hash} - - - Quantity: ${slot.occupant.quantity}/ - ${slot.occupant.max_quantity} - - ` - : "" - } -
- ${fields.map( - ([name, field], _index, _fields) => html` - - ${name} - - ${field.field_type} - - `, - )} -
-
- `; - } - - renderSlots(): HTMLTemplateResult { - return html` -
- ${this.slots.map((slot, index, _slots) => this.renderSlot(slot, index))} -
- `; - } - - renderReagents(): HTMLTemplateResult { - return html``; - } - - renderNetworks(): HTMLTemplateResult { - const vmNetworks = window.VM.vm.networks; - const networks = this.connections.map((connection, index, _conns) => { - const conn = - typeof connection === "object" ? connection.CableNetwork : null; - return html` - - Connection:${index} - ${vmNetworks.map( - (net) => - html`Network ${net}`, - )} - ${conn?.typ} - - `; - }); - return html` -
- ${networks} -
- `; - } - renderPins(): HTMLTemplateResult { - const pins = this.pins; - const visibleDevices = window.VM.vm.visibleDevices(this.deviceID); - const pinsHtml = pins?.map( - (pin, index) => - html` - - d${index} - ${visibleDevices.map( - (device, _index) => - html` - - Device ${device.id} : ${device.name ?? device.prefabName} - - `, - )} - `, - ); - return html` -
- ${pinsHtml} -
- `; - } - - render(): HTMLTemplateResult { - return html` - -
${this.renderHeader()}
- - Fields - Slots - Reagents - Networks - Pins - - ${this.renderFields()} - ${this.renderSlots()} - ${this.renderReagents()} - ${this.renderNetworks()} - ${this.renderPins()} - -
- -
- -
-

Are you sure you want to remove this device?

- Id ${this.deviceID} : ${this.name ?? this.prefabName} -
-
-
- Close - Remove -
-
- `; - } - - @query(".remove-device-dialog") removeDialog: SlDialog; - - _preventOverlayClose(event: CustomEvent) { - if (event.detail.source === "overlay") { - event.preventDefault(); - } - } - - _closeRemoveDialog() { - this.removeDialog.hide(); - } - - _handleChangeID(e: CustomEvent) { - const input = e.target as SlInput; - const val = parseIntWithHexOrBinary(input.value); - if (!isNaN(val)) { - window.VM.get().then(vm => { - if (!vm.changeDeviceId(this.deviceID, val)) { - input.value = this.deviceID.toString(); - } - }); - } else { - input.value = this.deviceID.toString(); - } - } - - _handleChangeName(e: CustomEvent) { - const input = e.target as SlInput; - const name = input.value.length === 0 ? undefined : input.value; - window.VM.get().then(vm => { - if (!vm.setDeviceName(this.deviceID, name)) { - input.value = this.name; - } - this.updateDevice(); - }); - } - - _handleChangeField(e: CustomEvent) { - const input = e.target as SlInput; - const field = input.getAttribute("key")! as LogicType; - const val = parseNumber(input.value); - window.VM.get().then((vm) => { - if (!vm.setDeviceField(this.deviceID, field, val, true)) { - input.value = this.fields.get(field).value.toString(); - } - this.updateDevice(); - }); - } - - _handleChangeSlotField(e: CustomEvent) { - const input = e.target as SlInput; - const slot = parseInt(input.getAttribute("slotIndex")!); - const field = input.getAttribute("key")! as SlotLogicType; - const val = parseNumber(input.value); - window.VM.get().then((vm) => { - if (!vm.setDeviceSlotField(this.deviceID, slot, field, val, true)) { - input.value = this.device.getSlotField(slot, field).toString(); - } - this.updateDevice(); - }); - } - - _handleDeviceRemoveButton(_e: Event) { - this.removeDialog.show(); - } - - _removeDialogRemove() { - this.removeDialog.hide(); - window.VM.get().then((vm) => vm.removeDevice(this.deviceID)); - } - - _handleChangeConnection(e: CustomEvent) { - const select = e.target as SlSelect; - const conn = parseInt(select.getAttribute("key")!); - const val = select.value ? parseInt(select.value as string) : undefined; - window.VM.get().then((vm) => - vm.setDeviceConnection(this.deviceID, conn, val), - ); - this.updateDevice(); - } - - _handleChangePin(e: CustomEvent) { - const select = e.target as SlSelect; - const pin = parseInt(select.getAttribute("key")!); - const val = select.value ? parseInt(select.value as string) : undefined; - window.VM.get().then((vm) => vm.setDevicePin(this.deviceID, pin, val)); - this.updateDevice(); - } -} - -@customElement("vm-device-list") -export class VMDeviceList extends BaseElement { - @state() devices: number[]; - - static styles = [ - ...defaultCss, - css` - .header { - margin-bottom: 1rem; - padding: 0.25rem 0.25rem; - align-items: center; - display: flex; - flex-direction: row; - width: 100%; - box-sizing: border-box; - } - .device-list { - display: flex; - flex-direction: column; - box-sizing: border-box; - } - .device-list-card { - width: 100%; - } - .device-filter-input { - margin-left: auto; - } - `, - ]; - - constructor() { - super(); - this.devices = [...window.VM.vm.deviceIds]; - } - - connectedCallback(): void { - const root = super.connectedCallback(); - window.VM.get().then((vm) => - vm.addEventListener( - "vm-devices-update", - this._handleDevicesUpdate.bind(this), - ), - ); - return root; - } - - _handleDevicesUpdate(e: CustomEvent) { - const ids = e.detail; - if (!structuralEqual(this.devices, ids)) { - this.devices = ids; - this.devices.sort(); - } - } - - protected render(): HTMLTemplateResult { - const deviceCards = repeat( - this.filteredDeviceIds, - (id) => id, - (id) => - html` - `, - ); - const result = html` -
- - Devices: - ${this.devices.length} - - - " - - -
-
${deviceCards}
- `; - - return result; - } - - get filteredDeviceIds() { - if (typeof this._filteredDeviceIds !== "undefined") { - return this._filteredDeviceIds; - } else { - return this.devices; - } - } - - private _filteredDeviceIds: number[] | undefined; - private _filter: string = ""; - - @query(".device-filter-input") filterInput: SlInput; - get filter() { - return this._filter; - } - - @state() - set filter(val: string) { - this._filter = val; - this.performSearch(); - } - - private filterTimeout: number | undefined; - - _handleFilterInput(_e: CustomEvent) { - if (this.filterTimeout) { - clearTimeout(this.filterTimeout); - } - const that = this; - this.filterTimeout = setTimeout(() => { - that.filter = that.filterInput.value; - that.filterTimeout = undefined; - }, 500); - } - - performSearch() { - if (this._filter) { - const datapoints: [string, number][] = []; - for (const device_id of this.devices) { - const device = window.VM.vm.devices.get(device_id); - if (device) { - if (typeof device.name !== "undefined") { - datapoints.push([device.name, device.id]); - } - if (typeof device.prefabName !== "undefined") { - datapoints.push([device.prefabName, device.id]); - } - } - } - const haystack: string[] = datapoints.map((data) => data[0]); - const uf = new uFuzzy({}); - const [_idxs, info, order] = uf.search(haystack, this._filter, 0, 1e3); - - const filtered = order?.map((infoIdx) => datapoints[info.idx[infoIdx]]); - const deviceIds: number[] = - filtered - ?.map((data) => data[1]) - ?.filter((val, index, arr) => arr.indexOf(val) === index) ?? []; - this._filteredDeviceIds = deviceIds; - } else { - this._filteredDeviceIds = undefined; - } - } -} - -@customElement("vm-add-device-button") -export class VMAddDeviceButton extends BaseElement { - static styles = [ - ...defaultCss, - css` - .add-device-drawer { - --size: 32rem; - } - - .search-results { - display: flex; - flex-direction: row; - overflow-x: auto; - } - - .card { - margin-top: var(--sl-spacing-small); - margin-right: var(--sl-spacing-small); - } - - .card + .card { - } - `, - ]; - - @query("sl-drawer") drawer: SlDrawer; - @query(".device-search-input") searchInput: SlInput; - - private _deviceDB: DeviceDB; - private _strutures: Map = new Map(); - private _datapoints: [string, string][] = []; - private _haystack: string[] = []; - get deviceDB() { - return this._deviceDB; - } - - @state() - set deviceDB(val: DeviceDB) { - this._deviceDB = val; - this._strutures = new Map( - Object.values(this.deviceDB.db) - .filter((entry) => this.deviceDB.structures.includes(entry.name), this) - .filter( - (entry) => this.deviceDB.logic_enabled.includes(entry.name), - this, - ) - .map((entry) => [entry.name, entry]), - ); - - const datapoints: [string, string][] = []; - for (const entry of this._strutures.values()) { - datapoints.push( - [entry.title, entry.name], - [entry.name, entry.name], - [entry.desc, entry.name], - ); - } - const haystack: string[] = datapoints.map((data) => data[0]); - this._datapoints = datapoints; - this._haystack = haystack; - this.performSearch(); - } - - private _filter: string = ""; - - get filter() { - return this._filter; - } - - @state() - set filter(val: string) { - this._filter = val; - this.performSearch(); - } - - private _searchResults: DeviceDBEntry[]; - - private filterTimeout: number | undefined; - - performSearch() { - if (this._filter) { - const uf = new uFuzzy({}); - const [_idxs, info, order] = uf.search( - this._haystack, - this._filter, - 0, - 1e3, - ); - - const filtered = order?.map( - (infoIdx) => this._datapoints[info.idx[infoIdx]], - ); - const names = - filtered - ?.map((data) => data[1]) - ?.filter((val, index, arr) => arr.indexOf(val) === index) ?? []; - - this._searchResults = names.map((name) => this._strutures.get(name)!); - } else { - // clear our results and prefilter if the filter is empty - this._searchResults = []; - } - } - - connectedCallback(): void { - const root = super.connectedCallback(); - window.VM.get().then((vm) => - vm.addEventListener( - "vm-device-db-loaded", - this._handleDeviceDBLoad.bind(this), - ), - ); - return root; - } - - _handleDeviceDBLoad(e: CustomEvent) { - this.deviceDB = e.detail; - } - - renderSearchResults() { - return repeat( - this._searchResults ?? [], - (result) => result.name, - (result) => cache(html` - - - `) - ); - - } - - _handleDeviceAdd() { - this.drawer.hide(); - } - - render() { - return html` - - Add Device - - - - Search Structures - - -
${this.renderSearchResults()}
- { - this.drawer.hide(); - }} - > - Close - -
- `; - } - - _handleSearchInput(e: CustomEvent) { - if (this.filterTimeout) { - clearTimeout(this.filterTimeout); - } - const that = this; - this.filterTimeout = setTimeout(() => { - that.filter = that.searchInput.value; - that.filterTimeout = undefined; - }, 200); - } - - _handleAddButtonClick() { - this.drawer.show(); - (this.drawer.querySelector(".device-search-input") as SlInput).select(); - } -} - -@customElement("vm-device-template") -export class VmDeviceTemplate extends BaseElement { - private _deviceDB: DeviceDB; - private image_err: boolean = false; - - static styles = [ - ...defaultCss, - css` - .template-card { - --padding: var(--sl-spacing-small); - } - .image { - width: 3rem; - height: 3rem; - } - .header { - display: flex; - flex-direction: row; - } - .card-body { - // height: 18rem; - overflow-y: auto; - } - sl-tab::part(base) { - padding: var(--sl-spacing-small) var(--sl-spacing-medium); - } - sl-tab-group::part(base) { - height: 14rem; - overflow-y: auto; - } - `, - ]; - - @state() fields: { [key in LogicType]?: LogicField }; - @state() slots: SlotTemplate[]; - @state() template: DeviceTemplate; - @state() device_id: number | undefined; - @state() device_name: string | undefined; - @state() connections: Connection[]; - - constructor() { - super(); - this.deviceDB = window.VM.vm.db; - } - - get deviceDB(): DeviceDB { - return this._deviceDB; - } - - @state() - set deviceDB(val: DeviceDB) { - this._deviceDB = val; - this.setupState(); - } - - private _prefab_name: string; - - get prefab_name(): string { - return this._prefab_name; - } - - @property({ type: String }) - set prefab_name(val: string) { - this._prefab_name = val; - this.setupState(); - } - - get dbDevice(): DeviceDBEntry { - return this.deviceDB.db[this.prefab_name]; - } - - setupState() { - const slotlogicmap: { [key: number]: SlotLogicType[] } = {}; - for (const [slt, slotIndexes] of Object.entries( - this.dbDevice?.slotlogic ?? {}, - )) { - for (const slotIndex of slotIndexes) { - const list = slotlogicmap[slotIndex] ?? []; - list.push(slt as SlotLogicType); - slotlogicmap[slotIndex] = list; - } - } - - this.fields = Object.fromEntries( - Object.entries(this.dbDevice?.logic ?? {}).map(([lt, ft]) => { - const value = lt === "PrefabHash" ? this.dbDevice.hash : 0.0; - return [lt, { field_type: ft, value } as LogicField]; - }), - ); - - this.slots = (this.dbDevice?.slots ?? []).map( - (slot, _index) => - ({ - typ: slot.typ, - }) as SlotTemplate, - ); - - const connections = Object.entries(this.dbDevice?.conn ?? {}).map( - ([index, conn]) => - [index, connectionFromDeviceDBConnection(conn)] as const, - ); - connections.sort((a, b) => { - if (a[0] < b[0]) { - return -1; - } else if (a[0] > b[0]) { - return 1; - } else { - return 0; - } - }); - - this.connections = connections.map((conn) => conn[1]); - } - - connectedCallback(): void { - super.connectedCallback(); - window.VM.get().then((vm) => - vm.addEventListener( - "vm-device-db-loaded", - this._handleDeviceDBLoad.bind(this), - ), - ); - } - - _handleDeviceDBLoad(e: CustomEvent) { - this.deviceDB = e.detail; - } - - renderFields(): HTMLTemplateResult { - const fields = Object.entries(this.fields); - return html` - ${fields.map(([name, field], _index, _fields) => { - return html` - - ${name} - ${field.field_type} - - `; - })} - `; - } - - _handleChangeField(e: CustomEvent) { - const input = e.target as SlInput; - const field = input.getAttribute("key")! as LogicType; - const val = parseNumber(input.value); - this.fields[field].value = val; - if (field === "ReferenceId" && val !== 0) { - this.device_id = val; - } - this.requestUpdate(); - } - - renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult { - return html` `; - } - - renderSlots(): HTMLTemplateResult { - return html`
`; - } - - renderReagents(): HTMLTemplateResult { - return html``; - } - - renderNetworks() { - const vm = window.VM.vm; - const vmNetworks = vm.networks; - const connections = this.connections; - return html` -
- ${connections.map((connection, index, _conns) => { - const conn = - typeof connection === "object" ? connection.CableNetwork : null; - return html` - - Connection:${index} - ${vmNetworks.map( - (net) => - html`Network ${net}`, - )} - ${conn?.typ} - - `; - })} -
- `; - } - - _handleChangeConnection(e: CustomEvent) { - const select = e.target as SlSelect; - const conn = parseInt(select.getAttribute("key")!); - const val = select.value ? parseInt(select.value as string) : undefined; - (this.connections[conn] as ConnectionCableNetwork).CableNetwork.net = val; - this.requestUpdate(); - } - - renderPins(): HTMLTemplateResult { - const device = this.deviceDB.db[this.prefab_name]; - return html`
`; - } - - render() { - const device = this.dbDevice; - return html` - -
- - - -
- ${device.title} - ${device?.name} - ${device?.hash} -
- Add - -
-
- - Fields - Slots - - Networks - - - ${this.renderFields()} - ${this.renderSlots()} - - ${this.renderNetworks()} - - -
-
- `; - } - _handleAddButtonClick() { - this.dispatchEvent( - new CustomEvent("add-device-template", { bubbles: true }), - ); - const template: DeviceTemplate = { - id: this.device_id, - name: this.device_name, - prefab_name: this.prefab_name, - slots: this.slots, - connections: this.connections, - fields: this.fields, - }; - window.VM.vm.addDeviceFromTemplate(template); - - // reset state for new device - this.setupState(); - } -} diff --git a/www/src/ts/virtual_machine/device/VMDeviceCard.ts b/www/src/ts/virtual_machine/device/VMDeviceCard.ts new file mode 100644 index 0000000..2c9e247 --- /dev/null +++ b/www/src/ts/virtual_machine/device/VMDeviceCard.ts @@ -0,0 +1,458 @@ +import type { + LogicType, + Slot, SlotOccupant, SlotLogicType, SlotType +} from "ic10emu_wasm"; +import { html, css, HTMLTemplateResult } from "lit"; +import { customElement, property, query, state } from "lit/decorators.js"; +import { BaseElement, defaultCss } from "components"; +import { VMDeviceMixin } from "virtual_machine/base_device"; +import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js"; +import { + displayNumber, + parseIntWithHexOrBinary, + parseNumber +} from "../../utils"; +import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.js"; +import type { DeviceDB } from "virtual_machine/device_db"; +import { SlDialog } from "@shoelace-style/shoelace"; + + +@customElement("vm-device-card") +export class VMDeviceCard extends VMDeviceMixin(BaseElement) { + image_err: boolean; + + @property({ type: Boolean }) open: boolean; + + constructor() { + super(); + this.open = false; + } + + static styles = [ + ...defaultCss, + css` + :host { + display: block; + box-sizing: border-box; + } + .card { + width: 100%; + box-sizing: border-box; + } + .image { + width: 4rem; + height: 4rem; + } + .header { + display: flex; + flex-direction: row; + flex-grow: 1; + } + .header-name { + display: flex; + flex-direction: row; + width: 100%; + flex-grow: 1; + align-items: center; + flex-wrap: wrap; + } + .device-card { + --padding: var(--sl-spacing-small); + } + .device-name::part(input) { + width: 10rem; + } + .device-id::part(input) { + width: 7rem; + } + .device-name-hash::part(input) { + width: 7rem; + } + .slot-header.image { + width: 1.5rem; + height: 1.5rem; + border: var(--sl-panel-border-width) solid var(--sl-panel-border-color); + border-radius: var(--sl-border-radius-medium); + background-color: var(--sl-color-neutral-0); + } + sl-divider { + --spacing: 0.25rem; + } + sl-button[variant="success"] { + /* Changes the success theme color to purple using primitives */ + --sl-color-success-600: var(--sl-color-purple-700); + } + sl-button[variant="primary"] { + /* Changes the success theme color to purple using primitives */ + --sl-color-primary-600: var(--sl-color-cyan-600); + } + sl-button[variant="warning"] { + /* Changes the success theme color to purple using primitives */ + --sl-color-warning-600: var(--sl-color-amber-600); + } + sl-tab-group { + margin-left: 1rem; + margin-right: 1rem; + --indicator-color: var(--sl-color-purple-600); + --sl-color-primary-600: var(--sl-color-purple-600); + } + sl-tab::part(base) { + padding: var(--sl-spacing-small) var(--sl-spacing-medium); + } + sl-tab-group::part(base) { + max-height: 20rem; + overflow-y: auto; + } + sl-icon-button.remove-button::part(base) { + color: var(--sl-color-danger-600); + } + sl-icon-button.remove-button::part(base):hover, + sl-icon-button.remove-button::part(base):focus { + color: var(--sl-color-danger-500); + } + sl-icon-button.remove-button::part(base):active { + color: var(--sl-color-danger-600); + } + .remove-dialog-body { + display: flex; + flex-direction: row; + } + .dialog-image { + width: 3rem; + height: 3rem; + } + `, + ]; + + private _deviceDB: DeviceDB; + + get deviceDB(): DeviceDB { + return this._deviceDB; + } + + @state() + set deviceDB(val: DeviceDB) { + this._deviceDB = val; + this.updateDevice(); + this.requestUpdate(); + } + + connectedCallback(): void { + super.connectedCallback(); + window.VM.vm.addEventListener( + "vm-device-db-loaded", + this._handleDeviceDBLoad.bind(this) + ); + } + + _handleDeviceDBLoad(e: CustomEvent) { + this.deviceDB = e.detail; + } + + onImageErr(e: Event) { + this.image_err = true; + console.log("Image load error", e); + } + + renderHeader(): HTMLTemplateResult { + const activeIc = window.VM.vm.activeIC; + const thisIsActiveIc = activeIc.id === this.deviceID; + const badges: HTMLTemplateResult[] = []; + if (this.deviceID == activeIc?.id) { + badges.push(html`db`); + } + activeIc?.pins?.forEach((id, index) => { + if (this.deviceID == id) { + badges.push( + html`d${index}` + ); + } + }, this); + return html` + + + +
+ + Id + + + + Name + + + + Hash + + + ${badges.map((badge) => badge)} +
+
+ + + +
+ `; + } + + renderFields(): HTMLTemplateResult { + const fields = Array.from(this.fields.entries()); + const inputIdBase = `vmDeviceCard${this.deviceID}Field`; + return html` + ${fields.map(([name, field], _index, _fields) => { + return html` + ${name} + + ${field.field_type} + `; + })} + `; + } + + lookupSlotOccupantImg( + occupant: SlotOccupant | undefined, + typ: SlotType + ): string { + if (typeof occupant !== "undefined") { + const hashLookup = (this.deviceDB ?? {}).names_by_hash ?? {}; + const prefabName = hashLookup[occupant.prefab_hash] ?? "UnknownHash"; + return `img/stationpedia/${prefabName}.png`; + } else { + return `img/stationpedia/SlotIcon_${typ}.png`; + } + } + + _onSlotImageErr(e: Event) { + console.log("image_err", e); + } + + static transparentImg = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" as const; + + renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult { + const _fields = this.device.getSlotFields(slotIndex); + const fields = Array.from(_fields.entries()); + const inputIdBase = `vmDeviceCard${this.deviceID}Slot${slotIndex}Field`; + const slotImg = this.lookupSlotOccupantImg(slot.occupant, slot.typ); + return html` + + + ${slotIndex} : ${slot.typ} + ${typeof slot.occupant !== "undefined" + ? html` + + Occupant: ${slot.occupant.id} : ${slot.occupant.prefab_hash} + + + Quantity: ${slot.occupant.quantity}/ + ${slot.occupant.max_quantity} + + ` + : ""} +
+ ${fields.map( + ([name, field], _index, _fields) => html` + + ${name} + + ${field.field_type} + + ` + )} +
+
+ `; + } + + renderSlots(): HTMLTemplateResult { + return html` +
+ ${this.slots.map((slot, index, _slots) => this.renderSlot(slot, index))} +
+ `; + } + + renderReagents(): HTMLTemplateResult { + return html``; + } + + renderNetworks(): HTMLTemplateResult { + const vmNetworks = window.VM.vm.networks; + const networks = this.connections.map((connection, index, _conns) => { + const conn = typeof connection === "object" ? connection.CableNetwork : null; + return html` + + Connection:${index} + ${vmNetworks.map( + (net) => html`Network ${net}` + )} + ${conn?.typ} + + `; + }); + return html` +
+ ${networks} +
+ `; + } + renderPins(): HTMLTemplateResult { + const pins = this.pins; + const visibleDevices = window.VM.vm.visibleDevices(this.deviceID); + const pinsHtml = pins?.map( + (pin, index) => html` + + d${index} + ${visibleDevices.map( + (device, _index) => html` + + Device ${device.id} : ${device.name ?? device.prefabName} + + ` + )} + ` + ); + return html` +
+ ${pinsHtml} +
+ `; + } + + render(): HTMLTemplateResult { + return html` + +
${this.renderHeader()}
+ + Fields + Slots + Reagents + Networks + Pins + + ${this.renderFields()} + ${this.renderSlots()} + ${this.renderReagents()} + ${this.renderNetworks()} + ${this.renderPins()} + +
+ +
+ +
+

Are you sure you want to remove this device?

+ Id ${this.deviceID} : ${this.name ?? this.prefabName} +
+
+
+ Close + Remove +
+
+ `; + } + + @query(".remove-device-dialog") removeDialog: SlDialog; + + _preventOverlayClose(event: CustomEvent) { + if (event.detail.source === "overlay") { + event.preventDefault(); + } + } + + _closeRemoveDialog() { + this.removeDialog.hide(); + } + + _handleChangeID(e: CustomEvent) { + const input = e.target as SlInput; + const val = parseIntWithHexOrBinary(input.value); + if (!isNaN(val)) { + window.VM.get().then(vm => { + if (!vm.changeDeviceId(this.deviceID, val)) { + input.value = this.deviceID.toString(); + } + }); + } else { + input.value = this.deviceID.toString(); + } + } + + _handleChangeName(e: CustomEvent) { + const input = e.target as SlInput; + const name = input.value.length === 0 ? undefined : input.value; + window.VM.get().then(vm => { + if (!vm.setDeviceName(this.deviceID, name)) { + input.value = this.name; + } + this.updateDevice(); + }); + } + + _handleChangeField(e: CustomEvent) { + const input = e.target as SlInput; + const field = input.getAttribute("key")! as LogicType; + const val = parseNumber(input.value); + window.VM.get().then((vm) => { + if (!vm.setDeviceField(this.deviceID, field, val, true)) { + input.value = this.fields.get(field).value.toString(); + } + this.updateDevice(); + }); + } + + _handleChangeSlotField(e: CustomEvent) { + const input = e.target as SlInput; + const slot = parseInt(input.getAttribute("slotIndex")!); + const field = input.getAttribute("key")! as SlotLogicType; + const val = parseNumber(input.value); + window.VM.get().then((vm) => { + if (!vm.setDeviceSlotField(this.deviceID, slot, field, val, true)) { + input.value = this.device.getSlotField(slot, field).toString(); + } + this.updateDevice(); + }); + } + + _handleDeviceRemoveButton(_e: Event) { + this.removeDialog.show(); + } + + _removeDialogRemove() { + this.removeDialog.hide(); + window.VM.get().then((vm) => vm.removeDevice(this.deviceID)); + } + + _handleChangeConnection(e: CustomEvent) { + const select = e.target as SlSelect; + const conn = parseInt(select.getAttribute("key")!); + const val = select.value ? parseInt(select.value as string) : undefined; + window.VM.get().then((vm) => vm.setDeviceConnection(this.deviceID, conn, val) + ); + this.updateDevice(); + } + + _handleChangePin(e: CustomEvent) { + const select = e.target as SlSelect; + const pin = parseInt(select.getAttribute("key")!); + const val = select.value ? parseInt(select.value as string) : undefined; + window.VM.get().then((vm) => vm.setDevicePin(this.deviceID, pin, val)); + this.updateDevice(); + } +} + diff --git a/www/src/ts/virtual_machine/device/card.ts b/www/src/ts/virtual_machine/device/card.ts new file mode 100644 index 0000000..883c87f --- /dev/null +++ b/www/src/ts/virtual_machine/device/card.ts @@ -0,0 +1,459 @@ + +import { html, css, HTMLTemplateResult } from "lit"; +import { customElement, property, query, state } from "lit/decorators.js"; +import { BaseElement, defaultCss } from "components"; +import { VMDeviceMixin } from "virtual_machine/base_device"; +import type { DeviceDB } from "virtual_machine/device_db"; +import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js"; +import { displayNumber, parseIntWithHexOrBinary, parseNumber } from "utils"; +import { LogicType, Slot, SlotLogicType, SlotOccupant, SlotType } from "ic10emu_wasm"; +import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js"; +import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.component.js"; + +@customElement("vm-device-card") +export class VMDeviceCard extends VMDeviceMixin(BaseElement) { + image_err: boolean; + + @property({ type: Boolean }) open: boolean; + + constructor() { + super(); + this.open = false; + } + + static styles = [ + ...defaultCss, + css` + :host { + display: block; + box-sizing: border-box; + } + .card { + width: 100%; + box-sizing: border-box; + } + .image { + width: 4rem; + height: 4rem; + } + .header { + display: flex; + flex-direction: row; + flex-grow: 1; + } + .header-name { + display: flex; + flex-direction: row; + width: 100%; + flex-grow: 1; + align-items: center; + flex-wrap: wrap; + } + .device-card { + --padding: var(--sl-spacing-small); + } + .device-name::part(input) { + width: 10rem; + } + .device-id::part(input) { + width: 7rem; + } + .device-name-hash::part(input) { + width: 7rem; + } + .slot-header.image { + width: 1.5rem; + height: 1.5rem; + border: var(--sl-panel-border-width) solid var(--sl-panel-border-color); + border-radius: var(--sl-border-radius-medium); + background-color: var(--sl-color-neutral-0); + } + sl-divider { + --spacing: 0.25rem; + } + sl-button[variant="success"] { + /* Changes the success theme color to purple using primitives */ + --sl-color-success-600: var(--sl-color-purple-700); + } + sl-button[variant="primary"] { + /* Changes the success theme color to purple using primitives */ + --sl-color-primary-600: var(--sl-color-cyan-600); + } + sl-button[variant="warning"] { + /* Changes the success theme color to purple using primitives */ + --sl-color-warning-600: var(--sl-color-amber-600); + } + sl-tab-group { + margin-left: 1rem; + margin-right: 1rem; + --indicator-color: var(--sl-color-purple-600); + --sl-color-primary-600: var(--sl-color-purple-600); + } + sl-tab::part(base) { + padding: var(--sl-spacing-small) var(--sl-spacing-medium); + } + sl-tab-group::part(base) { + max-height: 20rem; + overflow-y: auto; + } + sl-icon-button.remove-button::part(base) { + color: var(--sl-color-danger-600); + } + sl-icon-button.remove-button::part(base):hover, + sl-icon-button.remove-button::part(base):focus { + color: var(--sl-color-danger-500); + } + sl-icon-button.remove-button::part(base):active { + color: var(--sl-color-danger-600); + } + .remove-dialog-body { + display: flex; + flex-direction: row; + } + .dialog-image { + width: 3rem; + height: 3rem; + } + `, + ]; + + private _deviceDB: DeviceDB; + + get deviceDB(): DeviceDB { + return this._deviceDB; + } + + @state() + set deviceDB(val: DeviceDB) { + this._deviceDB = val; + this.updateDevice(); + this.requestUpdate(); + } + + connectedCallback(): void { + super.connectedCallback(); + window.VM.vm.addEventListener( + "vm-device-db-loaded", + this._handleDeviceDBLoad.bind(this), + ); + } + + _handleDeviceDBLoad(e: CustomEvent) { + this.deviceDB = e.detail; + } + + onImageErr(e: Event) { + this.image_err = true; + console.log("Image load error", e); + } + + renderHeader(): HTMLTemplateResult { + const activeIc = window.VM.vm.activeIC; + const thisIsActiveIc = activeIc.id === this.deviceID; + const badges: HTMLTemplateResult[] = []; + if (this.deviceID == activeIc?.id) { + badges.push(html`db`); + } + activeIc?.pins?.forEach((id, index) => { + if (this.deviceID == id) { + badges.push( + html`d${index}`, + ); + } + }, this); + return html` + + + +
+ + Id + + + + Name + + + + Hash + + + ${badges.map((badge) => badge)} +
+
+ + + +
+ `; + } + + renderFields(): HTMLTemplateResult { + const fields = Array.from(this.fields.entries()); + const inputIdBase = `vmDeviceCard${this.deviceID}Field`; + return html` + ${fields.map(([name, field], _index, _fields) => { + return html` + ${name} + + ${field.field_type} + `; + })} + `; + } + + lookupSlotOccupantImg( + occupant: SlotOccupant | undefined, + typ: SlotType, + ): string { + if (typeof occupant !== "undefined") { + const hashLookup = (this.deviceDB ?? {}).names_by_hash ?? {}; + const prefabName = hashLookup[occupant.prefab_hash] ?? "UnknownHash"; + return `img/stationpedia/${prefabName}.png`; + } else { + return `img/stationpedia/SlotIcon_${typ}.png`; + } + } + + _onSlotImageErr(e: Event) { + console.log("image_err", e); + } + + static transparentImg = + "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" as const; + + renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult { + const _fields = this.device.getSlotFields(slotIndex); + const fields = Array.from(_fields.entries()); + const inputIdBase = `vmDeviceCard${this.deviceID}Slot${slotIndex}Field`; + const slotImg = this.lookupSlotOccupantImg(slot.occupant, slot.typ); + return html` + + + ${slotIndex} : ${slot.typ} + ${ + typeof slot.occupant !== "undefined" + ? html` + + Occupant: ${slot.occupant.id} : ${slot.occupant.prefab_hash} + + + Quantity: ${slot.occupant.quantity}/ + ${slot.occupant.max_quantity} + + ` + : "" + } +
+ ${fields.map( + ([name, field], _index, _fields) => html` + + ${name} + + ${field.field_type} + + `, + )} +
+
+ `; + } + + renderSlots(): HTMLTemplateResult { + return html` +
+ ${this.slots.map((slot, index, _slots) => this.renderSlot(slot, index))} +
+ `; + } + + renderReagents(): HTMLTemplateResult { + return html``; + } + + renderNetworks(): HTMLTemplateResult { + const vmNetworks = window.VM.vm.networks; + const networks = this.connections.map((connection, index, _conns) => { + const conn = + typeof connection === "object" ? connection.CableNetwork : null; + return html` + + Connection:${index} + ${vmNetworks.map( + (net) => + html`Network ${net}`, + )} + ${conn?.typ} + + `; + }); + return html` +
+ ${networks} +
+ `; + } + renderPins(): HTMLTemplateResult { + const pins = this.pins; + const visibleDevices = window.VM.vm.visibleDevices(this.deviceID); + const pinsHtml = pins?.map( + (pin, index) => + html` + + d${index} + ${visibleDevices.map( + (device, _index) => + html` + + Device ${device.id} : ${device.name ?? device.prefabName} + + `, + )} + `, + ); + return html` +
+ ${pinsHtml} +
+ `; + } + + render(): HTMLTemplateResult { + return html` + +
${this.renderHeader()}
+ + Fields + Slots + Reagents + Networks + Pins + + ${this.renderFields()} + ${this.renderSlots()} + ${this.renderReagents()} + ${this.renderNetworks()} + ${this.renderPins()} + +
+ +
+ +
+

Are you sure you want to remove this device?

+ Id ${this.deviceID} : ${this.name ?? this.prefabName} +
+
+
+ Close + Remove +
+
+ `; + } + + @query(".remove-device-dialog") removeDialog: SlDialog; + + _preventOverlayClose(event: CustomEvent) { + if (event.detail.source === "overlay") { + event.preventDefault(); + } + } + + _closeRemoveDialog() { + this.removeDialog.hide(); + } + + _handleChangeID(e: CustomEvent) { + const input = e.target as SlInput; + const val = parseIntWithHexOrBinary(input.value); + if (!isNaN(val)) { + window.VM.get().then(vm => { + if (!vm.changeDeviceId(this.deviceID, val)) { + input.value = this.deviceID.toString(); + } + }); + } else { + input.value = this.deviceID.toString(); + } + } + + _handleChangeName(e: CustomEvent) { + const input = e.target as SlInput; + const name = input.value.length === 0 ? undefined : input.value; + window.VM.get().then(vm => { + if (!vm.setDeviceName(this.deviceID, name)) { + input.value = this.name; + } + this.updateDevice(); + }); + } + + _handleChangeField(e: CustomEvent) { + const input = e.target as SlInput; + const field = input.getAttribute("key")! as LogicType; + const val = parseNumber(input.value); + window.VM.get().then((vm) => { + if (!vm.setDeviceField(this.deviceID, field, val, true)) { + input.value = this.fields.get(field).value.toString(); + } + this.updateDevice(); + }); + } + + _handleChangeSlotField(e: CustomEvent) { + const input = e.target as SlInput; + const slot = parseInt(input.getAttribute("slotIndex")!); + const field = input.getAttribute("key")! as SlotLogicType; + const val = parseNumber(input.value); + window.VM.get().then((vm) => { + if (!vm.setDeviceSlotField(this.deviceID, slot, field, val, true)) { + input.value = this.device.getSlotField(slot, field).toString(); + } + this.updateDevice(); + }); + } + + _handleDeviceRemoveButton(_e: Event) { + this.removeDialog.show(); + } + + _removeDialogRemove() { + this.removeDialog.hide(); + window.VM.get().then((vm) => vm.removeDevice(this.deviceID)); + } + + _handleChangeConnection(e: CustomEvent) { + const select = e.target as SlSelect; + const conn = parseInt(select.getAttribute("key")!); + const val = select.value ? parseInt(select.value as string) : undefined; + window.VM.get().then((vm) => + vm.setDeviceConnection(this.deviceID, conn, val), + ); + this.updateDevice(); + } + + _handleChangePin(e: CustomEvent) { + const select = e.target as SlSelect; + const pin = parseInt(select.getAttribute("key")!); + const val = select.value ? parseInt(select.value as string) : undefined; + window.VM.get().then((vm) => vm.setDevicePin(this.deviceID, pin, val)); + this.updateDevice(); + } +} + diff --git a/www/src/ts/virtual_machine/device/device_list.ts b/www/src/ts/virtual_machine/device/device_list.ts new file mode 100644 index 0000000..ca197f6 --- /dev/null +++ b/www/src/ts/virtual_machine/device/device_list.ts @@ -0,0 +1,339 @@ + +import { html, css, HTMLTemplateResult } from "lit"; +import { customElement, property, query, state } from "lit/decorators.js"; +import { BaseElement, defaultCss } from "components"; + +import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js"; +import { + structuralEqual, +} from "../../utils"; + +import SlDrawer from "@shoelace-style/shoelace/dist/components/drawer/drawer.js"; +import type { DeviceDB, DeviceDBEntry } from "virtual_machine/device_db"; +import { repeat } from "lit/directives/repeat.js"; +import { cache } from "lit/directives/cache.js"; +import { default as uFuzzy } from "@leeoniya/ufuzzy"; + +@customElement("vm-device-list") +export class VMDeviceList extends BaseElement { + @state() devices: number[]; + + static styles = [ + ...defaultCss, + css` + .header { + margin-bottom: 1rem; + padding: 0.25rem 0.25rem; + align-items: center; + display: flex; + flex-direction: row; + width: 100%; + box-sizing: border-box; + } + .device-list { + display: flex; + flex-direction: column; + box-sizing: border-box; + } + .device-list-card { + width: 100%; + } + .device-filter-input { + margin-left: auto; + } + `, + ]; + + constructor() { + super(); + this.devices = [...window.VM.vm.deviceIds]; + } + + connectedCallback(): void { + const root = super.connectedCallback(); + window.VM.get().then((vm) => + vm.addEventListener( + "vm-devices-update", + this._handleDevicesUpdate.bind(this), + ), + ); + return root; + } + + _handleDevicesUpdate(e: CustomEvent) { + const ids = e.detail; + if (!structuralEqual(this.devices, ids)) { + this.devices = ids; + this.devices.sort(); + } + } + + protected render(): HTMLTemplateResult { + const deviceCards = repeat( + this.filteredDeviceIds, + (id) => id, + (id) => + html` + `, + ); + const result = html` +
+ + Devices: + ${this.devices.length} + + + " + + +
+
${deviceCards}
+ `; + + return result; + } + + get filteredDeviceIds() { + if (typeof this._filteredDeviceIds !== "undefined") { + return this._filteredDeviceIds; + } else { + return this.devices; + } + } + + private _filteredDeviceIds: number[] | undefined; + private _filter: string = ""; + + @query(".device-filter-input") filterInput: SlInput; + get filter() { + return this._filter; + } + + @state() + set filter(val: string) { + this._filter = val; + this.performSearch(); + } + + private filterTimeout: number | undefined; + + _handleFilterInput(_e: CustomEvent) { + if (this.filterTimeout) { + clearTimeout(this.filterTimeout); + } + const that = this; + this.filterTimeout = setTimeout(() => { + that.filter = that.filterInput.value; + that.filterTimeout = undefined; + }, 500); + } + + performSearch() { + if (this._filter) { + const datapoints: [string, number][] = []; + for (const device_id of this.devices) { + const device = window.VM.vm.devices.get(device_id); + if (device) { + if (typeof device.name !== "undefined") { + datapoints.push([device.name, device.id]); + } + if (typeof device.prefabName !== "undefined") { + datapoints.push([device.prefabName, device.id]); + } + } + } + const haystack: string[] = datapoints.map((data) => data[0]); + const uf = new uFuzzy({}); + const [_idxs, info, order] = uf.search(haystack, this._filter, 0, 1e3); + + const filtered = order?.map((infoIdx) => datapoints[info.idx[infoIdx]]); + const deviceIds: number[] = + filtered + ?.map((data) => data[1]) + ?.filter((val, index, arr) => arr.indexOf(val) === index) ?? []; + this._filteredDeviceIds = deviceIds; + } else { + this._filteredDeviceIds = undefined; + } + } +} + +@customElement("vm-add-device-button") +export class VMAddDeviceButton extends BaseElement { + static styles = [ + ...defaultCss, + css` + .add-device-drawer { + --size: 32rem; + } + + .search-results { + display: flex; + flex-direction: row; + overflow-x: auto; + } + + .card { + margin-top: var(--sl-spacing-small); + margin-right: var(--sl-spacing-small); + } + + .card + .card { + } + `, + ]; + + @query("sl-drawer") drawer: SlDrawer; + @query(".device-search-input") searchInput: SlInput; + + private _deviceDB: DeviceDB; + private _strutures: Map = new Map(); + private _datapoints: [string, string][] = []; + private _haystack: string[] = []; + get deviceDB() { + return this._deviceDB; + } + + @state() + set deviceDB(val: DeviceDB) { + this._deviceDB = val; + this._strutures = new Map( + Object.values(this.deviceDB.db) + .filter((entry) => this.deviceDB.structures.includes(entry.name), this) + .filter( + (entry) => this.deviceDB.logic_enabled.includes(entry.name), + this, + ) + .map((entry) => [entry.name, entry]), + ); + + const datapoints: [string, string][] = []; + for (const entry of this._strutures.values()) { + datapoints.push( + [entry.title, entry.name], + [entry.name, entry.name], + [entry.desc, entry.name], + ); + } + const haystack: string[] = datapoints.map((data) => data[0]); + this._datapoints = datapoints; + this._haystack = haystack; + this.performSearch(); + } + + private _filter: string = ""; + + get filter() { + return this._filter; + } + + @state() + set filter(val: string) { + this._filter = val; + this.performSearch(); + } + + private _searchResults: DeviceDBEntry[]; + + private filterTimeout: number | undefined; + + performSearch() { + if (this._filter) { + const uf = new uFuzzy({}); + const [_idxs, info, order] = uf.search( + this._haystack, + this._filter, + 0, + 1e3, + ); + + const filtered = order?.map( + (infoIdx) => this._datapoints[info.idx[infoIdx]], + ); + const names = + filtered + ?.map((data) => data[1]) + ?.filter((val, index, arr) => arr.indexOf(val) === index) ?? []; + + this._searchResults = names.map((name) => this._strutures.get(name)!); + } else { + // clear our results and prefilter if the filter is empty + this._searchResults = []; + } + } + + connectedCallback(): void { + const root = super.connectedCallback(); + window.VM.get().then((vm) => + vm.addEventListener( + "vm-device-db-loaded", + this._handleDeviceDBLoad.bind(this), + ), + ); + return root; + } + + _handleDeviceDBLoad(e: CustomEvent) { + this.deviceDB = e.detail; + } + + renderSearchResults() { + return repeat( + this._searchResults ?? [], + (result) => result.name, + (result) => cache(html` + + + `) + ); + + } + + _handleDeviceAdd() { + this.drawer.hide(); + } + + render() { + return html` + + Add Device + + + + Search Structures + + +
${this.renderSearchResults()}
+ { + this.drawer.hide(); + }} + > + Close + +
+ `; + } + + _handleSearchInput(e: CustomEvent) { + if (this.filterTimeout) { + clearTimeout(this.filterTimeout); + } + const that = this; + this.filterTimeout = setTimeout(() => { + that.filter = that.searchInput.value; + that.filterTimeout = undefined; + }, 200); + } + + _handleAddButtonClick() { + this.drawer.show(); + (this.drawer.querySelector(".device-search-input") as SlInput).select(); + } +} + diff --git a/www/src/ts/virtual_machine/device/index.ts b/www/src/ts/virtual_machine/device/index.ts new file mode 100644 index 0000000..9fb17b2 --- /dev/null +++ b/www/src/ts/virtual_machine/device/index.ts @@ -0,0 +1,23 @@ + +import "@shoelace-style/shoelace/dist/components/card/card.js"; +import "@shoelace-style/shoelace/dist/components/icon/icon.js"; +import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js"; +import "@shoelace-style/shoelace/dist/components/input/input.js"; +import "@shoelace-style/shoelace/dist/components/details/details.js"; +import "@shoelace-style/shoelace/dist/components/tab/tab.js"; +import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js"; +import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js"; +import "@shoelace-style/shoelace/dist/components/copy-button/copy-button.js"; +import "@shoelace-style/shoelace/dist/components/select/select.js"; +import "@shoelace-style/shoelace/dist/components/badge/badge.js"; +import "@shoelace-style/shoelace/dist/components/option/option.js"; +import "@shoelace-style/shoelace/dist/components/drawer/drawer.js"; +import "@shoelace-style/shoelace/dist/components/icon/icon.js"; + + +import { VmDeviceTemplate } from "./template"; +import { VMDeviceCard } from "./card"; +import { VMAddDeviceButton, VMDeviceList } from "./device_list"; + +export { VMDeviceCard, VmDeviceTemplate, VMDeviceList, VMAddDeviceButton }; + diff --git a/www/src/ts/virtual_machine/device/template.ts b/www/src/ts/virtual_machine/device/template.ts new file mode 100644 index 0000000..44f51f9 --- /dev/null +++ b/www/src/ts/virtual_machine/device/template.ts @@ -0,0 +1,306 @@ + + +import type { + Connection, + DeviceTemplate, + LogicField, + LogicFields, + LogicType, + Slot, + SlotTemplate, + SlotOccupant, + SlotOccupantTemplate, + SlotLogicType, + ConnectionCableNetwork, + SlotType, +} from "ic10emu_wasm"; +import { html, css, HTMLTemplateResult } from "lit"; +import { customElement, property, query, state } from "lit/decorators.js"; +import { BaseElement, defaultCss } from "components"; + +import type { DeviceDB, DeviceDBEntry } from "virtual_machine/device_db"; +import { connectionFromDeviceDBConnection } from "./utils"; +import { displayNumber, parseNumber } from "utils"; +import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js"; +import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js"; +import { VMDeviceCard } from "./card"; + +@customElement("vm-device-template") +export class VmDeviceTemplate extends BaseElement { + private _deviceDB: DeviceDB; + private image_err: boolean = false; + + static styles = [ + ...defaultCss, + css` + .template-card { + --padding: var(--sl-spacing-small); + } + .image { + width: 3rem; + height: 3rem; + } + .header { + display: flex; + flex-direction: row; + } + .card-body { + // height: 18rem; + overflow-y: auto; + } + sl-tab::part(base) { + padding: var(--sl-spacing-small) var(--sl-spacing-medium); + } + sl-tab-group::part(base) { + height: 14rem; + overflow-y: auto; + } + `, + ]; + + @state() fields: { [key in LogicType]?: LogicField }; + @state() slots: SlotTemplate[]; + @state() template: DeviceTemplate; + @state() device_id: number | undefined; + @state() device_name: string | undefined; + @state() connections: Connection[]; + + constructor() { + super(); + this.deviceDB = window.VM.vm.db; + } + + get deviceDB(): DeviceDB { + return this._deviceDB; + } + + @state() + set deviceDB(val: DeviceDB) { + this._deviceDB = val; + this.setupState(); + } + + private _prefab_name: string; + + get prefab_name(): string { + return this._prefab_name; + } + + @property({ type: String }) + set prefab_name(val: string) { + this._prefab_name = val; + this.setupState(); + } + + get dbDevice(): DeviceDBEntry { + return this.deviceDB.db[this.prefab_name]; + } + + setupState() { + const slotlogicmap: { [key: number]: SlotLogicType[] } = {}; + for (const [slt, slotIndexes] of Object.entries( + this.dbDevice?.slotlogic ?? {}, + )) { + for (const slotIndex of slotIndexes) { + const list = slotlogicmap[slotIndex] ?? []; + list.push(slt as SlotLogicType); + slotlogicmap[slotIndex] = list; + } + } + + this.fields = Object.fromEntries( + Object.entries(this.dbDevice?.logic ?? {}).map(([lt, ft]) => { + const value = lt === "PrefabHash" ? this.dbDevice.hash : 0.0; + return [lt, { field_type: ft, value } as LogicField]; + }), + ); + + this.slots = (this.dbDevice?.slots ?? []).map( + (slot, _index) => + ({ + typ: slot.typ, + }) as SlotTemplate, + ); + + const connections = Object.entries(this.dbDevice?.conn ?? {}).map( + ([index, conn]) => + [index, connectionFromDeviceDBConnection(conn)] as const, + ); + connections.sort((a, b) => { + if (a[0] < b[0]) { + return -1; + } else if (a[0] > b[0]) { + return 1; + } else { + return 0; + } + }); + + this.connections = connections.map((conn) => conn[1]); + } + + connectedCallback(): void { + super.connectedCallback(); + window.VM.get().then((vm) => + vm.addEventListener( + "vm-device-db-loaded", + this._handleDeviceDBLoad.bind(this), + ), + ); + } + + _handleDeviceDBLoad(e: CustomEvent) { + this.deviceDB = e.detail; + } + + renderFields(): HTMLTemplateResult { + const fields = Object.entries(this.fields); + return html` + ${fields.map(([name, field], _index, _fields) => { + return html` + + ${name} + ${field.field_type} + + `; + })} + `; + } + + _handleChangeField(e: CustomEvent) { + const input = e.target as SlInput; + const field = input.getAttribute("key")! as LogicType; + const val = parseNumber(input.value); + this.fields[field].value = val; + if (field === "ReferenceId" && val !== 0) { + this.device_id = val; + } + this.requestUpdate(); + } + + renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult { + return html` `; + } + + renderSlots(): HTMLTemplateResult { + return html`
`; + } + + renderReagents(): HTMLTemplateResult { + return html``; + } + + renderNetworks() { + const vm = window.VM.vm; + const vmNetworks = vm.networks; + const connections = this.connections; + return html` +
+ ${connections.map((connection, index, _conns) => { + const conn = + typeof connection === "object" ? connection.CableNetwork : null; + return html` + + Connection:${index} + ${vmNetworks.map( + (net) => + html`Network ${net}`, + )} + ${conn?.typ} + + `; + })} +
+ `; + } + + _handleChangeConnection(e: CustomEvent) { + const select = e.target as SlSelect; + const conn = parseInt(select.getAttribute("key")!); + const val = select.value ? parseInt(select.value as string) : undefined; + (this.connections[conn] as ConnectionCableNetwork).CableNetwork.net = val; + this.requestUpdate(); + } + + renderPins(): HTMLTemplateResult { + const device = this.deviceDB.db[this.prefab_name]; + return html`
`; + } + + render() { + const device = this.dbDevice; + return html` + +
+ + + +
+ ${device.title} + ${device?.name} + ${device?.hash} +
+ Add + +
+
+ + Fields + Slots + + Networks + + + ${this.renderFields()} + ${this.renderSlots()} + + ${this.renderNetworks()} + + +
+
+ `; + } + _handleAddButtonClick() { + this.dispatchEvent( + new CustomEvent("add-device-template", { bubbles: true }), + ); + const template: DeviceTemplate = { + id: this.device_id, + name: this.device_name, + prefab_name: this.prefab_name, + slots: this.slots, + connections: this.connections, + fields: this.fields, + }; + window.VM.vm.addDeviceFromTemplate(template); + + // reset state for new device + this.setupState(); + } +} diff --git a/www/src/ts/virtual_machine/utils.ts b/www/src/ts/virtual_machine/device/utils.ts similarity index 89% rename from www/src/ts/virtual_machine/utils.ts rename to www/src/ts/virtual_machine/device/utils.ts index db81c9d..73b4e87 100644 --- a/www/src/ts/virtual_machine/utils.ts +++ b/www/src/ts/virtual_machine/device/utils.ts @@ -1,5 +1,5 @@ import { Connection } from "ic10emu_wasm"; -import { DeviceDBConnection } from "./device_db"; +import { DeviceDBConnection } from "../device_db"; const CableNetworkTypes: readonly string[] = Object.freeze(["Power", "Data", "PowerAndData"]); export function connectionFromDeviceDBConnection(conn: DeviceDBConnection): Connection { diff --git a/www/src/ts/virtual_machine/index.ts b/www/src/ts/virtual_machine/index.ts index f87f1fb..99ee832 100644 --- a/www/src/ts/virtual_machine/index.ts +++ b/www/src/ts/virtual_machine/index.ts @@ -9,8 +9,7 @@ import { } from "ic10emu_wasm"; import { DeviceDB } from "./device_db"; import "./base_device"; -import { fromJson, toJson } from "../utils"; -import { App } from "../app"; +import { App } from "app"; export interface ToastMessage { variant: "warning" | "danger" | "success" | "primary" | "neutral"; icon: string; diff --git a/www/src/ts/virtual_machine/registers.ts b/www/src/ts/virtual_machine/registers.ts index fe1e320..f83bb96 100644 --- a/www/src/ts/virtual_machine/registers.ts +++ b/www/src/ts/virtual_machine/registers.ts @@ -1,7 +1,7 @@ import { html, css } from "lit"; import { customElement } from "lit/decorators.js"; -import { BaseElement, defaultCss } from "../components"; -import { VMActiveICMixin } from "./base_device"; +import { BaseElement, defaultCss } from "components"; +import { VMActiveICMixin } from "virtual_machine/base_device"; import "@shoelace-style/shoelace/dist/components/card/card.js"; import "@shoelace-style/shoelace/dist/components/icon/icon.js"; @@ -9,7 +9,7 @@ import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js"; import "@shoelace-style/shoelace/dist/components/input/input.js"; import { RegisterSpec } from "ic10emu_wasm"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js"; -import { displayNumber, parseNumber } from "../utils"; +import { displayNumber, parseNumber } from "utils"; @customElement("vm-ic-registers") export class VMICRegisters extends VMActiveICMixin(BaseElement) { diff --git a/www/src/ts/virtual_machine/stack.ts b/www/src/ts/virtual_machine/stack.ts index 7d1083e..94341a4 100644 --- a/www/src/ts/virtual_machine/stack.ts +++ b/www/src/ts/virtual_machine/stack.ts @@ -1,14 +1,14 @@ import { html, css } from "lit"; import { customElement } from "lit/decorators.js"; -import { BaseElement, defaultCss } from "../components"; -import { VMActiveICMixin } from "./base_device"; +import { BaseElement, defaultCss } from "components"; +import { VMActiveICMixin } from "virtual_machine/base_device"; import "@shoelace-style/shoelace/dist/components/card/card.js"; import "@shoelace-style/shoelace/dist/components/icon/icon.js"; import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js"; import "@shoelace-style/shoelace/dist/components/input/input.js"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js"; -import { displayNumber, parseNumber } from "../utils"; +import { displayNumber, parseNumber } from "utils"; @customElement("vm-ic-stack") export class VMICStack extends VMActiveICMixin(BaseElement) { diff --git a/www/src/ts/virtual_machine/ui.ts b/www/src/ts/virtual_machine/ui.ts index 9b1f3a9..288b3c8 100644 --- a/www/src/ts/virtual_machine/ui.ts +++ b/www/src/ts/virtual_machine/ui.ts @@ -1,6 +1,6 @@ -import { HTMLTemplateResult, html, css } from "lit"; -import { customElement, property, query, state } from "lit/decorators.js"; -import { BaseElement, defaultCss } from "../components"; +import { html, css } from "lit"; +import { customElement } from "lit/decorators.js"; +import { BaseElement, defaultCss } from "components"; import "@shoelace-style/shoelace/dist/components/details/details.js"; import "@shoelace-style/shoelace/dist/components/tab/tab.js"; import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js"; diff --git a/www/tsconfig.json b/www/tsconfig.json index 2a7db01..34772dd 100644 --- a/www/tsconfig.json +++ b/www/tsconfig.json @@ -1,5 +1,7 @@ { "compilerOptions": { + "baseUrl": "./src/ts", + "rootDir": "./src/ts", "outDir": "./dist/", "sourceMap": true, "noImplicitAny": true,