import { html, css, HTMLTemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; import { BaseElement, defaultCss } from "components"; import { VMTemplateDBMixin, VMObjectMixin } from "virtual_machine/base_device"; import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js"; import { parseIntWithHexOrBinary, parseNumber } from "utils"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js"; import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.component.js"; import "./slot"; import "./fields"; import "./pins"; import { until } from "lit/directives/until.js"; import { repeat } from "lit/directives/repeat.js"; export type CardTab = "fields" | "slots" | "reagents" | "networks" | "pins"; @customElement("vm-device-card") export class VMDeviceCard extends VMTemplateDBMixin( VMObjectMixin(BaseElement), ) { image_err: boolean; @property({ type: Boolean }) open: boolean; constructor() { super(); this.open = false; this.subscribe( "prefabName", "name", "nameHash", "reagents", "slots-count", "reagents", "connections", "active-ic", ); } 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; } 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: 30rem; 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; } `, ]; _handleDeviceDBLoad(e: CustomEvent): void { super._handleDeviceDBLoad(e); this.updateDevice(); } onImageErr(e: Event) { this.image_err = true; console.log("Image load error", e); } renderHeader(): HTMLTemplateResult { const thisIsActiveIc = this.activeICId === this.objectID; const badges: HTMLTemplateResult[] = []; if (thisIsActiveIc) { badges.push(html`db`); } const activeIc = window.VM.vm.activeIC; const numPins = "device" in activeIc?.template ? activeIc.template.device.device_pins_length : Math.max( ...Array.from(activeIc?.obj_info.device_pins?.keys() ?? [0]), ); const pins = new Array(numPins) .fill(true) .map((_, index) => this.pins.get(index)); pins.forEach((id, index) => { if (this.objectID == id) { badges.push( html`d${index}`, ); } }, this); return html`
Id Name Hash ${badges.map((badge) => badge)}
`; } renderFields() { return this.delayRenderTab( "fields", html``, ); } _onSlotImageErr(e: Event) { console.log("image_err", e); } static transparentImg = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" as const; async renderSlots() { return this.delayRenderTab( "slots", html`
${repeat( this.slots, (slot, index) => slot.typ + index.toString(), (_slot, index) => html` `, )}
`, ); } renderReagents() { return this.delayRenderTab("reagents", html``); } renderNetworks() { const vmNetworks = window.VM.vm.networks; const networks = this.connections.map((connection, index, _conns) => { const conn = typeof connection === "object" && "CableNetwork" in connection ? connection.CableNetwork : null; return html` Connection:${index} ${vmNetworks.map( (net) => html`Network ${net}`, )} ${conn?.typ} `; }); return this.delayRenderTab( "networks", html`
${networks}
`, ); } renderPins() { return this.delayRenderTab( "pins", html`
`, ); } private tabsShown: CardTab[] = ["fields"]; private tabResolves: { [key in CardTab]: { result?: HTMLTemplateResult; resolver?: (result: HTMLTemplateResult) => void; }; } = { fields: {}, slots: {}, reagents: {}, networks: {}, pins: {}, }; delayRenderTab( name: CardTab, result: HTMLTemplateResult, ): Promise { this.tabResolves[name].result = result; return new Promise((resolve) => { if (this.tabsShown.includes(name)) { this.tabResolves[name].resolver = undefined; resolve(result); } else { this.tabResolves[name].resolver = resolve; } }); } resolveTab(name: CardTab) { if ( typeof this.tabResolves[name].resolver !== "undefined" && typeof this.tabResolves[name].result !== "undefined" ) { this.tabResolves[name].resolver(this.tabResolves[name].result); this.tabsShown.push(name); } } render(): HTMLTemplateResult { return html`
${this.renderHeader()}
Fields Slots Reagents Networks Pins ${until(this.renderFields(), html``)} ${until(this.renderSlots(), html``)} ${until(this.renderReagents(), html``)} ${until(this.renderNetworks(), html``)} ${until(this.renderPins(), html``)}

Are you sure you want to remove this device?

Id ${this.objectID} : ${this.name ?? this.prefabName}
Close Remove
`; } _handleTabChange(e: CustomEvent<{ name: string }>) { setTimeout(() => this.resolveTab(e.detail.name as CardTab), 100); } @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.objectID, val)) { input.value = this.objectID.toString(); } }); } else { input.value = this.objectID.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.setObjectName(this.objectID, name)) { input.value = this.name; } this.updateDevice(); }); } _handleDeviceRemoveButton(_e: Event) { this.removeDialog.show(); } _removeDialogRemove() { this.removeDialog.hide(); window.VM.get().then((vm) => vm.removeDevice(this.objectID)); } _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.objectID, conn, val), ); this.updateDevice(); } }