refactor(frontend) finish signal conversion, fix passing data to VM

Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
Rachel Powers
2024-08-19 22:22:39 -07:00
parent 70833e0b00
commit b1c9db278d
45 changed files with 5009 additions and 2363 deletions

View File

@@ -19,19 +19,23 @@ $accordion-icon-color-dark: #dee2e6;
$accordion-icon-active-color-dark: #dee2e6;
$accordion-button-padding-y: 0.5rem;
// Required
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";
@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
// // Required
// @import "bootstrap/scss/variables";
// @import "bootstrap/scss/variables-dark";
// @import "bootstrap/scss/maps";
// @import "bootstrap/scss/mixins";
// @import "bootstrap/scss/utilities";
// @import "bootstrap/scss/root";
// @import "bootstrap/scss/reboot";
//
// @import "bootstrap/scss/type";
// // @import "bootstrap/scss/images";
// @import "bootstrap/scss/containers";
// @import "bootstrap/scss/grid";
@import "bootstrap/scss/type";
// @import "bootstrap/scss/images";
@import "bootstrap/scss/containers";
@import "bootstrap/scss/grid";
// @import "bootstrap/scss/tables";
// @import "bootstrap/scss/forms";
// @import "bootstrap/scss/buttons";
@@ -59,11 +63,11 @@ $accordion-button-padding-y: 0.5rem;
// @import "bootstrap/scss/offcanvas"; // Requires transitions
// @import "bootstrap/scss/placeholders";
// Helpers
@import "bootstrap/scss/helpers";
// // Helpers
// @import "bootstrap/scss/helpers";
// Utilities
@import "bootstrap/scss/utilities/api";
// // Utilities
// @import "bootstrap/scss/utilities/api";
// Sholace theme
@import "@shoelace-style/shoelace/dist/themes/dark.css";

View File

@@ -72,16 +72,18 @@ export const demoVMState: SessionDB.CurrentDBVmState = {
id: 1,
prefab: "StructureCircuitHousing",
socketed_ic: 2,
slots: {
0: { id: 2, quantity: 1 },
},
connections: {
0: 1,
},
slots: new Map([
[0, { id: 2, quantity: 1 }],
]),
connections: new Map([
[0, 1],
]),
// unused, provided to make compiler happy
name: undefined,
prefab_hash: undefined,
compile_errors: undefined,
parent_slot: undefined,
root_parent_human: undefined,
damage: undefined,
device_pins: undefined,
reagents: undefined,
@@ -106,9 +108,9 @@ export const demoVMState: SessionDB.CurrentDBVmState = {
instruction_pointer: 0,
yield_instruction_count: 0,
state: "Start",
aliases: {},
defines: {},
labels: {},
aliases: new Map(),
defines: new Map(),
labels: new Map(),
registers: new Array(18).fill(0),
},
@@ -117,6 +119,8 @@ export const demoVMState: SessionDB.CurrentDBVmState = {
prefab_hash: undefined,
compile_errors: undefined,
slots: undefined,
parent_slot: undefined,
root_parent_human: undefined,
damage: undefined,
device_pins: undefined,
connections: undefined,

View File

@@ -417,14 +417,16 @@ export namespace SessionDB {
instruction_pointer: ic.ip,
yield_instruction_count: ic.ic,
state: ic.state as ICState,
aliases: Object.fromEntries(ic.aliases.entries()),
defines: Object.fromEntries(ic.defines.entries()),
labels: {},
aliases: ic.aliases,
defines: ic.defines,
labels: new Map(),
registers: ic.registers,
},
// unused
slots: undefined,
parent_slot: undefined,
root_parent_human: undefined,
damage: undefined,
device_pins: undefined,
connections: undefined,
@@ -492,7 +494,7 @@ export namespace SessionDB {
id: template.id,
prefab: template.prefab_name,
prefab_hash: undefined,
slots: Object.fromEntries(
slots: new Map(
Array.from(slotOccupantsPairs.entries()).map(
([index, [obj, quantity]]) => [
index,
@@ -505,17 +507,19 @@ export namespace SessionDB {
),
socketed_ic: socketedIcFn(template.id),
logic_values: Object.fromEntries(
logic_values: new Map(
Object.entries(template.fields).map(([key, val]) => {
return [key, val.value];
return [key as LogicType, val.value];
}),
) as Record<LogicType, number>,
),
// unused
memory: undefined,
source_code: undefined,
compile_errors: undefined,
circuit: undefined,
parent_slot: undefined,
root_parent_human: undefined,
damage: undefined,
device_pins: undefined,
connections: undefined,

View File

@@ -1,4 +1,5 @@
import { Ace } from "ace-builds";
import { TransferHandler } from "comlink";
export function docReady(fn: () => void) {
// see if DOM is already available
@@ -92,6 +93,26 @@ export function fromJson(value: string): any {
return JSON.parse(value, reviver);
}
// this is a hack that *may* not be needed
type SuitableForSpecialJson = any;
export const comlinkSpecialJsonTransferHandler: TransferHandler<any, string> = {
canHandle: (obj: unknown): obj is SuitableForSpecialJson => {
return typeof obj === "object"
|| (
typeof obj === "number"
&& (!Number.isFinite(obj) || Number.isNaN(obj) || isZeroNegative(obj))
)
|| typeof obj === "undefined";
},
serialize: (obj: SuitableForSpecialJson) => {
const sJson = toJson(obj);
return [
sJson,
[],
]
},
deserialize: (obj: string) => fromJson(obj)
};
export function compareMaps(map1: Map<any, any>, map2: Map<any, any>): boolean {
let testVal;

View File

@@ -21,6 +21,7 @@ import { LitElement, PropertyValueMap } from "lit";
import {
computed,
signal,
} from '@lit-labs/preact-signals';
import type { Signal } from '@lit-labs/preact-signals';
@@ -138,7 +139,7 @@ export class ComputedObjectSignals {
return slotsTemplate.map((template, index) => {
const fieldEntryInfos = Array.from(
Object.entries(logicTemplate?.logic_slot_types[index]) ?? [],
Object.entries(logicTemplate?.logic_slot_types.get(index)) ?? [],
);
const logicFields = new Map(
fieldEntryInfos.map(([slt, access]) => {
@@ -321,11 +322,11 @@ export const globalObjectSignalMap = new ObjectComputedSignalMap();
type Constructor<T = {}> = new (...args: any[]) => T;
export declare class VMObjectMixinInterface {
objectID: ObjectID;
activeICId: ObjectID;
objectID: Signal<ObjectID>;
activeICId: Signal<ObjectID>;
objectSignals: ComputedObjectSignals | null;
_handleDeviceModified(e: CustomEvent): void;
updateDevice(): void;
updateObject(): void;
subscribe(...sub: VMObjectMixinSubscription[]): void;
unsubscribe(filter: (sub: VMObjectMixinSubscription) => boolean): void;
}
@@ -338,14 +339,12 @@ export const VMObjectMixin = <T extends Constructor<LitElement>>(
superClass: T,
) => {
class VMObjectMixinClass extends superClass {
private _objectID: number;
get objectID() {
return this._objectID;
}
@property({ type: Number })
set objectID(val: number) {
this._objectID = val;
this.updateDevice();
objectID: Signal<ObjectID | null>;
constructor (...args: any[]) {
super(...args);
this.objectID = signal(null);
this.objectID.subscribe((_) => {this.updateObject()})
}
@state() private objectSubscriptions: VMObjectMixinSubscription[] = [];
@@ -354,16 +353,16 @@ export const VMObjectMixin = <T extends Constructor<LitElement>>(
this.objectSubscriptions = this.objectSubscriptions.concat(sub);
}
// remove subscripotions matching the filter
// remove subscriptions matching the filter
unsubscribe(filter: (sub: VMObjectMixinSubscription) => boolean) {
this.objectSubscriptions = this.objectSubscriptions.filter(
(sub) => !filter(sub),
);
}
@state() objectSignals: ComputedObjectSignals | null;
@state() objectSignals: ComputedObjectSignals | null = null;
@state() activeICId: number;
activeICId: Signal<number> = signal(null);
connectedCallback(): void {
const root = super.connectedCallback();
@@ -385,7 +384,7 @@ export const VMObjectMixin = <T extends Constructor<LitElement>>(
this._handleDevicesRemoved.bind(this),
);
});
this.updateDevice();
this.updateObject();
return root;
}
@@ -413,20 +412,20 @@ export const VMObjectMixin = <T extends Constructor<LitElement>>(
async _handleDeviceModified(e: CustomEvent) {
const id = e.detail;
const activeIcId = window.App.app.session.activeIC;
if (this.objectID === id) {
this.updateDevice();
if (this.objectID.peek() === id) {
this.updateObject();
} else if (
id === activeIcId &&
this.objectSubscriptions.includes("active-ic")
) {
this.updateDevice();
this.updateObject();
this.requestUpdate();
} else if (this.objectSubscriptions.includes("visible-devices")) {
const visibleDevices = await window.VM.vm.visibleDeviceIds(
this.objectID,
this.objectID.peek(),
);
if (visibleDevices.includes(id)) {
this.updateDevice();
this.updateObject();
this.requestUpdate();
}
}
@@ -435,8 +434,8 @@ export const VMObjectMixin = <T extends Constructor<LitElement>>(
async _handleDevicesModified(e: CustomEvent<number[]>) {
const activeIcId = window.App.app.session.activeIC;
const ids = e.detail;
if (ids.includes(this.objectID)) {
this.updateDevice();
if (ids.includes(this.objectID.peek())) {
this.updateObject();
if (this.objectSubscriptions.includes("visible-devices")) {
this.requestUpdate();
}
@@ -444,25 +443,25 @@ export const VMObjectMixin = <T extends Constructor<LitElement>>(
ids.includes(activeIcId) &&
this.objectSubscriptions.includes("active-ic")
) {
this.updateDevice();
this.updateObject();
this.requestUpdate();
} else if (this.objectSubscriptions.includes("visible-devices")) {
const visibleDevices = await window.VM.vm.visibleDeviceIds(
this.objectID,
this.objectID.peek(),
);
if (ids.some((id) => visibleDevices.includes(id))) {
this.updateDevice();
this.updateObject();
this.requestUpdate();
}
}
}
async _handleDeviceIdChange(e: CustomEvent<{ old: number; new: number }>) {
if (this.objectID === e.detail.old) {
this.objectID = e.detail.new;
if (this.objectID.peek() === e.detail.old) {
this.objectID.value = e.detail.new;
} else if (this.objectSubscriptions.includes("visible-devices")) {
const visibleDevices = await window.VM.vm.visibleDeviceIds(
this.objectID,
this.objectID.peek(),
);
if (
visibleDevices.some(
@@ -481,8 +480,9 @@ export const VMObjectMixin = <T extends Constructor<LitElement>>(
}
}
updateDevice() {
const newObjSignals = globalObjectSignalMap.get(this.objectID);
updateObject() {
this.activeICId.value = window.App.app.session.activeIC;
const newObjSignals = globalObjectSignalMap.get(this.objectID.peek());
if (newObjSignals !== this.objectSignals) {
this.objectSignals = newObjSignals
}
@@ -503,9 +503,9 @@ export const VMActiveICMixin = <T extends Constructor<LitElement>>(
superClass: T,
) => {
class VMActiveICMixinClass extends VMObjectMixin(superClass) {
constructor() {
super();
this.objectID = window.App.app.session.activeIC;
constructor(...args: any[]) {
super(...args);
this.objectID.value = window.App.app.session.activeIC;
}
connectedCallback(): void {
@@ -535,10 +535,10 @@ export const VMActiveICMixin = <T extends Constructor<LitElement>>(
_handleActiveIC(e: CustomEvent) {
const id = e.detail;
if (this.objectID !== id) {
this.objectID = id;
if (this.objectID.value !== id) {
this.objectID.value = id;
}
this.updateDevice();
this.updateObject();
}
}
@@ -569,7 +569,7 @@ export const VMTemplateDBMixin = <T extends Constructor<LitElement>>(
disconnectedCallback(): void {
window.VM.vm.removeEventListener(
"vm-device-db-loaded",
"vm-template-db-loaded",
this._handleDeviceDBLoad.bind(this),
);
}

View File

@@ -1,16 +1,28 @@
import { html, css } from "lit";
import { html, css, nothing } from "lit";
import { customElement, query } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMActiveICMixin } from "virtualMachine/baseDevice";
import { ComputedObjectSignals, globalObjectSignalMap, VMActiveICMixin } from "virtualMachine/baseDevice";
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.js";
import { computed, Signal, watch } from "@lit-labs/preact-signals";
import { FrozenObjectFull } from "ic10emu_wasm";
@customElement("vm-ic-controls")
export class VMICControls extends VMActiveICMixin(BaseElement) {
circuitHolders: Signal<ComputedObjectSignals[]>;
constructor() {
super();
this.subscribe("ic", "active-ic")
this.subscribe("active-ic")
this.circuitHolders = computed(() => {
const ids = window.VM.vm.circuitHolderIds.value;
const circuitHolders = [];
for (const id of ids) {
circuitHolders.push(globalObjectSignalMap.get(id));
}
return circuitHolders;
});
}
static styles = [
@@ -64,8 +76,49 @@ export class VMICControls extends VMActiveICMixin(BaseElement) {
@query(".active-ic-select") activeICSelect: SlSelect;
forceSelectUpdate() {
if (this.activeICSelect != null) {
this.activeICSelect.handleValueChange();
}
}
protected render() {
const ics = Array.from(window.VM.vm.circuitHolders);
const icsOptions = computed(() => {
return this.circuitHolders.value.map((circuitHolder) => {
circuitHolder.prefabName.subscribe((_) => {this.forceSelectUpdate()});
circuitHolder.id.subscribe((_) => {this.forceSelectUpdate()});
circuitHolder.displayName.subscribe((_) => {this.forceSelectUpdate()});
const span = circuitHolder.name ? html`<span slot="suffix">${watch(circuitHolder.prefabName)}</span>` : nothing ;
return html`
<sl-option
prefabName=${watch(circuitHolder.prefabName)}
value=${watch(circuitHolder.id)}
>
${span}
Device:${watch(circuitHolder.id)} ${watch(circuitHolder.displayName)}
</sl-option>`
});
});
icsOptions.subscribe((_) => {this.forceSelectUpdate()});
const icErrors = computed(() => {
return this.objectSignals?.errors.value?.map(
(err) =>
typeof err === "object"
&& "ParseError" in err
? html`<div class="hstack">
<span>
Line: ${err.ParseError.line} -
${"ParseError" in err ? err.ParseError.start : "N/A"}:${err.ParseError.end}
</span>
<span class="ms-auto">${err.ParseError.msg}</span>
</div>`
: html`${JSON.stringify(err)}`,
) ?? nothing;
});
return html`
<sl-card class="card">
<div class="controls" slot="header">
@@ -116,57 +169,33 @@ export class VMICControls extends VMActiveICMixin(BaseElement) {
hoist
size="small"
placement="bottom"
value="${this.objectID}"
value="${watch(this.objectID)}"
@sl-change=${this._handleChangeActiveIC}
class="active-ic-select"
>
${ics.map(
([id, device], _index) =>
html`<sl-option
name=${device.obj_info.name}
prefabName=${device.obj_info.prefab}
value=${id}
>
${device.obj_info.name
? html`<span slot="suffix">${device.obj_info.prefab}</span>`
: ""}
Device:${id} ${device.obj_info.name ?? device.obj_info.prefab}
</sl-option>`,
)}
${watch(icsOptions)}
</sl-select>
</div>
</div>
<div class="stats">
<div class="hstack">
<span>Instruction Pointer</span>
<span class="ms-auto">${this.icIP}</span>
<span class="ms-auto">${this.objectSignals ? watch(this.objectSignals.icIP) : nothing}</span>
</div>
<sl-divider></sl-divider>
<div class="hstack">
<span>Last Run Operations Count</span>
<span class="ms-auto">${this.icOpCount}</span>
<span class="ms-auto">${this.objectSignals ? watch(this.objectSignals.icOpCount) : nothing}</span>
</div>
<sl-divider></sl-divider>
<div class="hstack">
<span>Last State</span>
<span class="ms-auto">${this.icState}</span>
<span class="ms-auto">${this.objectSignals ? watch(this.objectSignals.icState) : nothing}</span>
</div>
<sl-divider></sl-divider>
<div class="vstack">
<span>Errors</span>
${this.errors?.map(
(err) =>
typeof err === "object"
&& "ParseError" in err
? html`<div class="hstack">
<span>
Line: ${err.ParseError.line} -
${"ParseError" in err ? err.ParseError.start : "N/A"}:${err.ParseError.end}
</span>
<span class="ms-auto">${err.ParseError.msg}</span>
</div>`
: html`${JSON.stringify(err)}`,
)}
${watch(icErrors)}
</div>
</div>
</sl-card>
@@ -183,18 +212,6 @@ export class VMICControls extends VMActiveICMixin(BaseElement) {
window.VM.get().then((vm) => vm.reset());
}
updateIC(): void {
super.updateIC();
this.activeICSelect?.dispatchEvent(new Event("slotchange"));
// if (this.activeICSelect) {
// const val = this.activeICSelect.value;
// this.activeICSelect.value = "";
// this.activeICSelect.requestUpdate();
// this.activeICSelect.value = val;
// this.activeICSelect.
// }
}
_handleChangeActiveIC(e: CustomEvent) {
const select = e.target as SlSelect;
const icId = parseInt(select.value as string);

View File

@@ -122,7 +122,7 @@ export class VMDeviceCard extends VMTemplateDBMixin(
_handleDeviceDBLoad(e: CustomEvent<any>): void {
super._handleDeviceDBLoad(e);
this.updateDevice();
this.updateObject();
}
onImageErr(e: Event) {
@@ -131,24 +131,38 @@ export class VMDeviceCard extends VMTemplateDBMixin(
}
renderHeader(): HTMLTemplateResult {
const thisIsActiveIc = this.activeICId === this.objectID;
const badges: HTMLTemplateResult[] = [];
if (thisIsActiveIc) {
badges.push(html`<sl-badge variant="primary" pill pulse>db</sl-badge>`);
}
const activeIc = globalObjectSignalMap.get(this.activeICId);
const thisIsActiveIc = computed(() => {
return this.activeICId.value === this.objectID.value;
});
const numPins = activeIc.numPins.value;
const pins = new Array(numPins)
.fill(true)
.map((_, index) => this.objectSignals.pins.value.get(index));
pins.forEach((id, index) => {
if (this.objectID == id) {
badges.push(
html`<sl-badge variant="success" pill>d${index}</sl-badge>`,
);
const activeIc = computed(() => {
return globalObjectSignalMap.get(this.activeICId.value);
});
const numPins = computed(() => {
return activeIc.value.numPins.value;
});
const pins = computed(() => {
return new Array(numPins.value)
.fill(true)
.map((_, index) => this.objectSignals.pins.value.get(index));
});
const badgesHtml = computed(() => {
const badges: HTMLTemplateResult[] = [];
if (thisIsActiveIc.value) {
badges.push(html`<sl-badge variant="primary" pill pulse>db</sl-badge>`);
}
}, this);
pins.value.forEach((id, index) => {
if (this.objectID.value == id) {
badges.push(
html`<sl-badge variant="success" pill>d${index}</sl-badge>`,
);
}
}, this);
return badges
});
return html`
<sl-tooltip content="${watch(this.objectSignals.prefabName)}">
<img
@@ -159,21 +173,21 @@ export class VMDeviceCard extends VMTemplateDBMixin(
</sl-tooltip>
<div class="header-name">
<sl-input
id="vmDeviceCard${this.objectID}Id"
id="vmDeviceCard${watch(this.objectID)}Id"
class="device-id me-1"
size="small"
pill
value=${this.objectID.toString()}
value=${watch(this.objectID)}
@sl-change=${this._handleChangeID}
>
<span slot="prefix">Id</span>
<sl-copy-button
slot="suffix"
.value=${this.objectID.toString()}
.value=${watch(this.objectID)}
></sl-copy-button>
</sl-input>
<sl-input
id="vmDeviceCard${this.objectID}Name"
id="vmDeviceCard${watch(this.objectID)}Name"
class="device-name me-1"
size="small"
pill
@@ -184,11 +198,11 @@ export class VMDeviceCard extends VMTemplateDBMixin(
<span slot="prefix">Name</span>
<sl-copy-button
slot="suffix"
from="vmDeviceCard${this.objectID}Name.value"
from="vmDeviceCard${watch(this.objectID)}Name.value"
></sl-copy-button>
</sl-input>
<sl-input
id="vmDeviceCard${this.objectID}NameHash"
id="vmDeviceCard${watch(this.objectID)}NameHash"
size="small"
pill
class="device-name-hash me-1"
@@ -201,7 +215,7 @@ export class VMDeviceCard extends VMTemplateDBMixin(
from="vmDeviceCard${this.objectID}NameHash.value"
></sl-copy-button>
</sl-input>
${badges.map((badge) => badge)}
${watch(badgesHtml)}
</div>
<div class="ms-auto mt-auto mb-auto me-2">
<sl-tooltip
@@ -256,7 +270,7 @@ export class VMDeviceCard extends VMTemplateDBMixin(
}
renderNetworks() {
const vmNetworks = window.VM.vm.networks;
const vmNetworks = window.VM.vm.networkIds;
const networks = this.objectSignals.connections.value.map((connection, index, _conns) => {
const conn =
typeof connection === "object" && "CableNetwork" in connection
@@ -273,7 +287,7 @@ export class VMDeviceCard extends VMTemplateDBMixin(
@sl-change=${this._handleChangeConnection}
>
<span slot="prefix">Connection:${index} </span>
${vmNetworks.map(
${vmNetworks.value.map(
(net) =>
html`<sl-option value=${net.toString()}
>Network ${net}</sl-option
@@ -421,7 +435,7 @@ export class VMDeviceCard extends VMTemplateDBMixin(
const val = parseIntWithHexOrBinary(input.value);
if (!isNaN(val)) {
window.VM.get().then((vm) => {
if (!vm.changeObjectID(this.objectID, val)) {
if (!vm.changeObjectID(this.objectID.peek(), val)) {
input.value = this.objectID.toString();
}
});
@@ -434,10 +448,10 @@ export class VMDeviceCard extends VMTemplateDBMixin(
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)) {
if (!vm.setObjectName(this.objectID.peek(), name)) {
input.value = this.objectSignals.name.value;
}
this.updateDevice();
this.updateObject();
});
}
_handleDeviceRemoveButton(_e: Event) {
@@ -446,7 +460,7 @@ export class VMDeviceCard extends VMTemplateDBMixin(
_removeDialogRemove() {
this.removeDialog.hide();
window.VM.get().then((vm) => vm.removeDevice(this.objectID));
window.VM.get().then((vm) => vm.removeDevice(this.objectID.peek()));
}
_handleChangeConnection(e: CustomEvent) {
@@ -454,8 +468,8 @@ export class VMDeviceCard extends VMTemplateDBMixin(
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),
vm.setDeviceConnection(this.objectID.peek(), conn, val),
);
this.updateDevice();
this.updateObject();
}
}

View File

@@ -12,7 +12,7 @@ export function connectionFromConnectionInfo(conn: ConnectionInfo): Connection {
) {
connection = {
CableNetwork: {
net: window.VM.vm.defaultNetwork,
net: window.VM.vm.defaultNetwork.peek(),
typ: conn.typ as CableConnectionType,
role: conn.role,
},

View File

@@ -10,10 +10,15 @@ import { default as uFuzzy } from "@leeoniya/ufuzzy";
import { VMSlotAddDialog } from "./slotAddDialog";
import "./addDevice"
import { SlotModifyEvent } from "./slot";
import { computed, Signal, signal, SignalWatcher, watch } from "@lit-labs/preact-signals";
import { globalObjectSignalMap } from "virtualMachine/baseDevice";
import { ObjectID } from "ic10emu_wasm";
@customElement("vm-device-list")
export class VMDeviceList extends BaseElement {
@state() devices: number[];
export class VMDeviceList extends SignalWatcher(BaseElement) {
devices: Signal<ObjectID[]>;
private _filter: Signal<string> = signal("");
private _filteredDeviceIds: Signal<number[] | undefined>;
static styles = [
...defaultCss,
@@ -43,17 +48,50 @@ export class VMDeviceList extends BaseElement {
constructor() {
super();
this.devices = [...window.VM.vm.objectIds];
}
this.devices = computed(() => {
const objIds = window.VM.vm.objectIds.value;
const deviceIds = [];
for (const id of objIds) {
const obj = window.VM.vm.objects.get(id);
const info = obj.value.obj_info;
if (!(info.parent_slot != null || info.root_parent_human != null)) {
deviceIds.push(id)
}
}
deviceIds.sort();
return deviceIds;
});
this._filteredDeviceIds = computed(() => {
if (this._filter.value) {
const datapoints: [string, number][] = [];
for (const device_id of this.devices.value) {
const device = globalObjectSignalMap.get(device_id);
if (device) {
const name = device.name.peek();
const id = device.id.peek();
const prefab = device.prefabName.peek();
if (name != null) {
datapoints.push([name, id]);
}
if (prefab != null) {
datapoints.push([prefab, id]);
}
}
}
const haystack: string[] = datapoints.map((data) => data[0]);
const uf = new uFuzzy({});
const [_idxs, info, order] = uf.search(haystack, this._filter.value, 0, 1e3);
connectedCallback(): void {
super.connectedCallback();
window.VM.get().then((vm) =>
vm.addEventListener(
"vm-devices-update",
this._handleDevicesUpdate.bind(this),
),
);
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) ?? [];
return deviceIds;
} else {
return Array.from(this.devices.value);
}
});
}
protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
@@ -63,27 +101,20 @@ export class VMDeviceList extends BaseElement {
);
}
_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,
this.filteredDeviceIds.value,
(id) => id,
(id) =>
html`<vm-device-card .deviceID=${id} class="device-list-card">
</vm-device-card>`,
);
const numDevices = computed(() => this.devices.value.length);
const result = html`
<div class="header">
<span>
Devices:
<sl-badge variant="neutral" pill>${this.devices.length}</sl-badge>
<sl-badge variant="neutral" pill>${watch(numDevices)}</sl-badge>
</span>
<sl-input
class="device-filter-input"
@@ -118,18 +149,14 @@ export class VMDeviceList extends BaseElement {
}
}
private _filteredDeviceIds: number[] | undefined;
private _filter: string = "";
@query(".device-filter-input") filterInput: SlInput;
get filter() {
return this._filter;
return this._filter.value;
}
@state()
set filter(val: string) {
this._filter = val;
this.performSearch();
this._filter.value = val;
}
private filterTimeout: number | undefined;
@@ -144,34 +171,5 @@ export class VMDeviceList extends BaseElement {
that.filterTimeout = undefined;
}, 500);
}
performSearch() {
if (this._filter) {
const datapoints: [string, number][] = [];
for (const device_id of this.devices) {
const device = window.VM.vm.objects.get(device_id);
if (device) {
if (typeof device.obj_info.name !== "undefined") {
datapoints.push([device.obj_info.name, device.obj_info.id]);
}
if (typeof device.obj_info.prefab !== "undefined") {
datapoints.push([device.obj_info.prefab, device.obj_info.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;
}
}
}

View File

@@ -53,10 +53,10 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(BaseElement))
const field = input.getAttribute("key")! as LogicType;
const val = parseNumber(input.value);
window.VM.get().then((vm) => {
if (!vm.setObjectField(this.objectID, field, val, true)) {
if (!vm.setObjectField(this.objectID.peek(), field, val, true)) {
input.value = this.objectSignals.logicFields.value.get(field).value.toString();
}
this.updateDevice();
this.updateObject();
});
}
}

View File

@@ -63,7 +63,7 @@ export class VMDevicePins extends VMObjectMixin(VMTemplateDBMixin(BaseElement))
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.objectID, pin, val));
this.updateDevice();
window.VM.get().then((vm) => vm.setDevicePin(this.objectID.peek(), pin, val));
this.updateObject();
}
}

View File

@@ -268,7 +268,7 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(
}
_handleSlotOccupantRemove() {
window.VM.vm.removeSlotOccupant(this.objectID, this.slotIndex);
window.VM.vm.removeSlotOccupant(this.objectID.peek(), this.slotIndex);
}
_handleSlotClick(_e: Event) {
@@ -276,7 +276,7 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(
new CustomEvent<SlotModifyEvent>("device-modify-slot", {
bubbles: true,
composed: true,
detail: { deviceID: this.objectID, slotIndex: this.slotIndex },
detail: { deviceID: this.objectID.peek(), slotIndex: this.slotIndex },
}),
);
}
@@ -293,7 +293,7 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(
);
if (
!window.VM.vm.setObjectSlotField(
this.objectID,
this.objectID.peek(),
this.slotIndex,
"Quantity",
val,
@@ -365,7 +365,7 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(
}
window.VM.get().then((vm) => {
if (
!vm.setObjectSlotField(this.objectID, this.slotIndex, field, val, true)
!vm.setObjectSlotField(this.objectID.peek(), this.slotIndex, field, val, true)
) {
input.value = (
this.slotSignal.value.logicFields ??
@@ -374,7 +374,7 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(
.get(field)
.toString();
}
this.updateDevice();
this.updateObject();
});
}

View File

@@ -1,7 +1,7 @@
import { html, css } from "lit";
import { html, css, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMTemplateDBMixin } from "virtualMachine/baseDevice";
import { ComputedObjectSignals, globalObjectSignalMap, VMTemplateDBMixin } from "virtualMachine/baseDevice";
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 { VMDeviceCard } from "./card";
@@ -15,6 +15,8 @@ import {
ObjectInfo,
ObjectTemplate,
} from "ic10emu_wasm";
import { computed, ReadonlySignal, signal, Signal, watch } from "@lit-labs/preact-signals";
import { repeat } from "lit/directives/repeat.js";
type SlotableItemTemplate = Extract<ObjectTemplate, { item: ItemInfo }>;
@@ -38,30 +40,33 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
`,
];
private _items: Map<string, SlotableItemTemplate> = new Map();
private _filteredItems: SlotableItemTemplate[];
private _datapoints: [string, string][] = [];
private _haystack: string[] = [];
private _items: Signal<Record<string, SlotableItemTemplate>> = signal({});
private _filteredItems: ReadonlySignal<SlotableItemTemplate[]>;
private _datapoints: ReadonlySignal<[string, string][]>;
private _haystack: ReadonlySignal<string[]>;
private _filter: string = "";
private _filter: Signal<string> = signal("");
get filter() {
return this._filter;
return this._filter.peek();
}
@state()
set filter(val: string) {
this._filter = val;
this.performSearch();
this._filter.value = val;
}
private _searchResults: {
private _searchResults: ReadonlySignal<{
entry: SlotableItemTemplate;
haystackEntry: string;
ranges: number[];
}[] = [];
}[]>;
constructor() {
super();
this.setupSearch();
}
postDBSetUpdate(): void {
this._items = new Map(
this._items.value = Object.fromEntries(
Array.from(Object.values(this.templateDB)).flatMap((template) => {
if ("item" in template) {
return [[template.prefab.prefab_name, template]] as [
@@ -73,77 +78,84 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
}
}),
);
this.setupSearch();
this.performSearch();
}
setupSearch() {
let filteredItems = Array.from(this._items.values());
if (
typeof this.objectID !== "undefined" &&
typeof this.slotIndex !== "undefined"
) {
const obj = window.VM.vm.objects.get(this.objectID);
const template = obj.template;
const slot = "slots" in template ? template.slots[this.slotIndex] : null;
const typ = slot.typ;
const filteredItems = computed(() => {
let filtered = Array.from(Object.values(this._items.value));
const obj = globalObjectSignalMap.get(this.objectID.value ?? null);
if (obj != null) {
const template = obj.template;
const slot = "slots" in template.value ? template.value.slots[this.slotIndex.value] : null;
const typ = slot.typ;
if (typeof typ === "string" && typ !== "None") {
filteredItems = Array.from(this._items.values()).filter(
(item) => item.item.slot_class === typ,
if (typeof typ === "string" && typ !== "None") {
filtered = Array.from(Object.values(this._items.value)).filter(
(item) => item.item.slot_class === typ,
);
}
}
return filtered;
});
this._filteredItems = filteredItems;
const datapoints = computed(() => {
const datapoints: [string, string][] = [];
for (const entry of this._filteredItems.value) {
datapoints.push(
[entry.prefab.name, entry.prefab.prefab_name],
[entry.prefab.prefab_name, entry.prefab.prefab_name],
[entry.prefab.desc, entry.prefab.prefab_name],
);
}
}
this._filteredItems = filteredItems;
const datapoints: [string, string][] = [];
for (const entry of this._filteredItems) {
datapoints.push(
[entry.prefab.name, entry.prefab.prefab_name],
[entry.prefab.prefab_name, entry.prefab.prefab_name],
[entry.prefab.desc, entry.prefab.prefab_name],
);
}
const haystack: string[] = datapoints.map((data) => data[0]);
return datapoints;
});
this._datapoints = datapoints;
const haystack: Signal<string[]> = computed(() => {
return datapoints.value.map((data) => data[0]);
});
this._haystack = haystack;
}
performSearch() {
if (this._filter) {
const uf = new uFuzzy({});
const [_idxs, info, order] = uf.search(
this._haystack,
this._filter,
0,
1e3,
);
const searchResults = computed(() => {
let results;
if (this._filter.value) {
const uf = new uFuzzy({});
const [_idxs, info, order] = uf.search(
this._haystack.value,
this._filter.value,
0,
1e3,
);
const filtered =
order?.map((infoIdx) => ({
name: this._datapoints[info.idx[infoIdx]][1],
haystackEntry: this._haystack[info.idx[infoIdx]],
ranges: info.ranges[infoIdx],
})) ?? [];
const filtered =
order?.map((infoIdx) => ({
name: this._datapoints.value[info.idx[infoIdx]][1],
haystackEntry: this._haystack.value[info.idx[infoIdx]],
ranges: info.ranges[infoIdx],
})) ?? [];
const uniqueNames = new Set(filtered.map((obj) => obj.name));
const unique = [...uniqueNames].map((result) => {
return filtered.find((obj) => obj.name === result);
});
const uniqueNames = new Set(filtered.map((obj) => obj.name));
const unique = [...uniqueNames].map((result) => {
return filtered.find((obj) => obj.name === result);
});
this._searchResults = unique.map(({ name, haystackEntry, ranges }) => ({
entry: this._items.get(name)!,
haystackEntry,
ranges,
}));
} else {
// return everything
this._searchResults = [...this._filteredItems].map((st) => ({
entry: st,
haystackEntry: st.prefab.prefab_name,
ranges: [],
}));
}
results = unique.map(({ name, haystackEntry, ranges }) => ({
entry: this._items.value[name]!,
haystackEntry,
ranges,
}));
} else {
// return everything
results = [...this._filteredItems.value].map((st) => ({
entry: st,
haystackEntry: st.prefab.prefab_name,
ranges: [],
}));
}
return results;
});
this._searchResults = searchResults;
}
renderSearchResults() {
@@ -156,10 +168,13 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
None
</div>
`;
return html`
<div class="mt-2 max-h-48 overflow-y-auto w-full">
${enableNone ? none : ""}
${this._searchResults.map((result) => {
const resultsHtml = computed(() => {
return repeat(
this._searchResults.value,
(result) => {
return result.entry.prefab.prefab_hash;
},
(result) => {
const imgSrc = `img/stationpedia/${result.entry.prefab.prefab_name}.png`;
const img = html`
<img
@@ -178,22 +193,28 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
<div>${result.entry.prefab.name}</div>
</div>
`;
})}
}
);
});
return html`
<div class="mt-2 max-h-48 overflow-y-auto w-full">
${enableNone ? none : ""}
${watch(resultsHtml)}
</div>
`;
}
_handleClickNone() {
window.VM.vm.removeSlotOccupant(this.objectID, this.slotIndex);
window.VM.vm.removeSlotOccupant(this.objectID.peek(), this.slotIndex.peek());
this.hide();
}
_handleClickItem(e: Event) {
const div = e.currentTarget as HTMLDivElement;
const key = parseInt(div.getAttribute("key"));
const entry = this.templateDB[key] as SlotableItemTemplate;
const obj = window.VM.vm.objects.get(this.objectID);
const dbTemplate = obj.template;
const entry = this.templateDB.get(key) as SlotableItemTemplate;
const obj = window.VM.vm.objects.get(this.objectID.peek());
const dbTemplate = obj.peek().template;
console.log("using entry", dbTemplate);
const template: FrozenObject = {
@@ -203,7 +224,7 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
database_template: true,
template: undefined,
};
window.VM.vm.setSlotOccupant(this.objectID, this.slotIndex, template, 1);
window.VM.vm.setSlotOccupant(this.objectID.peek(), this.slotIndex.peek(), template, 1);
this.hide();
}
@@ -211,12 +232,22 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
@query(".device-search-input") searchInput: SlInput;
render() {
const device = window.VM.vm.objects.get(this.objectID);
const name = device?.obj_info.name ?? device?.obj_info.prefab ?? "";
const id = this.objectID ?? 0;
const device = computed(() => {
return globalObjectSignalMap.get(this.objectID.value) ?? null;
});
const name = computed(() => {
return device.value?.displayName.value ?? nothing;
});
const id = computed(() => this.objectID.value ?? 0);
const resultsHtml = html`
<div class="flex flex-row overflow-x-auto">
${this.renderSearchResults()}
</div>
`;
return html`
<sl-dialog
label="Edit device ${id} : ${name} Slot ${this.slotIndex}"
label="Edit device ${watch(id)} : ${watch(name)} Slot ${watch(this.slotIndex)}"
class="slot-add-dialog"
@sl-hide=${this._handleDialogHide}
>
@@ -230,16 +261,7 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
<span slot="prefix">Search Items</span>
<sl-icon slot="suffix" name="search"></sl-icon>
</sl-input>
${when(
typeof this.objectID !== "undefined" &&
typeof this.slotIndex !== "undefined",
() => html`
<div class="flex flex-row overflow-x-auto">
${this.renderSearchResults()}
</div>
`,
() => html``,
)}
${resultsHtml}
</sl-dialog>
`;
}
@@ -262,14 +284,12 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
this.slotIndex = undefined;
}
@state() private objectID: number;
@state() private slotIndex: number;
private objectID: Signal<number> = signal(null);
private slotIndex: Signal<number> = signal(0);
show(objectID: number, slotIndex: number) {
this.objectID = objectID;
this.slotIndex = slotIndex;
this.setupSearch();
this.performSearch();
this.objectID.value = objectID;
this.slotIndex.value = slotIndex;
this.dialog.show();
this.searchInput.select();
}

View File

@@ -24,7 +24,9 @@ import { crc32, 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";
import { VMTemplateDBMixin } from "virtualMachine/baseDevice";
import { globalObjectSignalMap, VMTemplateDBMixin } from "virtualMachine/baseDevice";
import { computed, Signal, watch } from "@lit-labs/preact-signals";
import { createRef, ref, Ref } from "lit/directives/ref.js";
export interface SlotTemplate {
typ: Class;
@@ -74,13 +76,13 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
`,
];
@state() fields: Map<LogicType, number>;
@state() slots: SlotTemplate[];
@state() pins: (ObjectID | undefined)[];
@state() template: FrozenObject;
@state() objectId: number | undefined;
@state() objectName: string | undefined;
@state() connections: Connection[];
fields: Signal<Record<LogicType, number>>;
slots: Signal<SlotTemplate[]>;
pins: Signal<(ObjectID | undefined)[]>;
template: Signal<FrozenObject>;
objectId: Signal<number | undefined>;
objectName: Signal<string | undefined>;
connections: Signal<Connection[]>;
constructor() {
super();
@@ -105,13 +107,13 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
}
get dbTemplate(): ObjectTemplate {
return this.templateDB[this._prefabHash];
return this.templateDB.get(this._prefabHash);
}
setupState() {
const dbTemplate = this.dbTemplate;
this.fields = new Map(
this.fields.value = Object.fromEntries(
(
Array.from(
"logic" in dbTemplate
@@ -123,9 +125,9 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
lt === "PrefabHash" ? this.dbTemplate.prefab.prefab_hash : 0.0;
return [lt, value];
}),
);
) as Record<LogicType, number>;
this.slots = (
this.slots.value = (
("slots" in dbTemplate ? dbTemplate.slots ?? [] : []) as SlotInfo[]
).map(
(slot, _index) =>
@@ -152,17 +154,18 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
}
});
this.connections = connections.map((conn) => conn[1]);
this.connections.value = connections.map((conn) => conn[1]);
const numPins =
"device" in dbTemplate ? dbTemplate.device.device_pins_length : 0;
this.pins = new Array(numPins).fill(undefined);
this.pins.value = new Array(numPins).fill(undefined);
}
renderFields(): HTMLTemplateResult {
const fields = Object.entries(this.fields);
return html`
${fields.map(([name, field], _index, _fields) => {
return html`
return html`
<sl-input
key="${name}"
value="${displayNumber(field.value)}"
@@ -174,7 +177,7 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
<span slot="suffix">${field.field_type}</span>
</sl-input>
`;
})}
})}
`;
}
@@ -182,13 +185,22 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
const input = e.target as SlInput;
const field = input.getAttribute("key")! as LogicType;
const val = parseNumber(input.value);
this.fields.set(field, val);
this.fields.value = { ...this.fields.value, [field]: val};
if (field === "ReferenceId" && val !== 0) {
this.objectId = val;
this.objectId.value = val;
}
this.requestUpdate();
}
forceSelectUpdate(...slSelects: Ref<SlSelect>[]) {
for (const slSelect of slSelects) {
if (slSelect.value != null && "handleValueChange" in slSelect.value) {
slSelect.value.handleValueChange();
}
}
}
private networksSelectRef: Ref<SlSelect> = createRef();
renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult {
return html`<sl-card class="slot-card"> </sl-card>`;
}
@@ -203,34 +215,37 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
renderNetworks() {
const vm = window.VM.vm;
const vmNetworks = vm.networks;
const connections = this.connections;
const vmNetworks = computed(() => {
return vm.networkIds.value.map((net) => html`<sl-option value=${net}>Network ${net}</sl-option>`);
});
const connections = computed(() => {
this.connections.value.map((connection, index, _conns) => {
const conn =
typeof connection === "object" && "CableNetwork" in connection
? connection.CableNetwork
: null;
return html`
<sl-select
hoist
placement="top"
clearable
key=${index}
value=${conn?.net}
?disabled=${conn === null}
@sl-change=${this._handleChangeConnection}
${ref(this.networksSelectRef)}
>
<span slot="prefix">Connection:${index} </span>
${watch(vmNetworks)}
<span slot="prefix"> ${conn?.typ} </span>
</sl-select>
`;
});
});
vmNetworks.subscribe((_) => { this.forceSelectUpdate(this.networksSelectRef)})
return html`
<div class="networks">
${connections.map((connection, index, _conns) => {
const conn =
typeof connection === "object" && "CableNetwork" in connection
? connection.CableNetwork
: null;
return html`
<sl-select
hoist
placement="top"
clearable
key=${index}
value=${conn?.net}
?disabled=${conn === null}
@sl-change=${this._handleChangeConnection}
>
<span slot="prefix">Connection:${index} </span>
${vmNetworks.map(
(net) =>
html`<sl-option value=${net}>Network ${net}</sl-option>`,
)}
<span slot="prefix"> ${conn?.typ} </span>
</sl-select>
`;
})}
${watch(connections)}
</div>
`;
}
@@ -239,53 +254,89 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
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();
const copy = [...this.connections.value];
(copy[conn] as ConnectionCableNetwork).CableNetwork.net = val;
this.connections.value = copy;
}
private _pinsSelectRefMap: Map<number, Ref<SlSelect>> = new Map();
getPinRef(index: number) : Ref<SlSelect> {
if (!this._pinsSelectRefMap.has(index)) {
this._pinsSelectRefMap.set(index, createRef());
}
return this._pinsSelectRefMap.get(index);
}
forcePinSelectUpdate() {
this.forceSelectUpdate(...this._pinsSelectRefMap.values());
}
renderPins(): HTMLTemplateResult {
const networks = this.connections.flatMap((connection, index) => {
return typeof connection === "object" && "CableNetwork" in connection
? [connection.CableNetwork.net]
: [];
const networks = computed(() => {
return this.connections.value.flatMap((connection, index) => {
return typeof connection === "object" && "CableNetwork" in connection
? [connection.CableNetwork.net]
: [];
});
});
const visibleDeviceIds = [
const visibleDeviceIds = computed(() => {
return [
...new Set(
networks.flatMap((net) => window.VM.vm.networkDataDevices(net)),
networks.value.flatMap((net) => window.VM.vm.networkDataDevicesSignal(net).value),
),
];
const visibleDevices = visibleDeviceIds.map((id) =>
window.VM.vm.objects.get(id),
);
const pinsHtml = this.pins?.map(
(pin, index) =>
html` <sl-select
hoist
placement="top"
clearable
key=${index}
.value=${pin}
@sl-change=${this._handleChangePin}
>
<span slot="prefix">d${index}</span>
${visibleDevices.map(
(device, _index) => html`
<sl-option value=${device.obj_info.id.toString()}>
Device ${device.obj_info.id} :
${device.obj_info.name ?? device.obj_info.prefab}
</sl-option>
`,
)}
</sl-select>`,
);
return html`<div class="pins">${pinsHtml}</div>`;
});
const visibleDevices = computed(() => {
return visibleDeviceIds.value.map((id) =>
globalObjectSignalMap.get(id),
);
});
const visibleDevicesHtml = computed(() => {
return visibleDevices.value.map(
(device, _index) => {
device.id.subscribe((_) => { this.forcePinSelectUpdate(); });
device.displayName.subscribe((_) => { this.forcePinSelectUpdate(); });
return html`
<sl-option value=${watch(device.id)}>
Device ${watch(device.id)} :
${watch(device.displayName)}
</sl-option>
`
}
)
});
visibleDeviceIds.subscribe((_) => { this.forcePinSelectUpdate(); });
const pinsHtml = computed(() => {
this.pins.value.map(
(pin, index) => {
const pinRef = this.getPinRef(index)
return html` <sl-select
hoist
placement="top"
clearable
key=${index}
.value=${pin}
@sl-change=${this._handleChangePin}
${ref(pinRef)}
>
<span slot="prefix">d${index}</span>
${watch(visibleDevicesHtml)}
</sl-select>`
}
);
});
return html`<div class="pins">${watch(pinsHtml)}</div>`;
}
_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;
this.pins[pin] = val;
const val = select.value ? parseInt(select.value as string) : null;
const copy = [...this.pins.value];
copy[pin] = val;
this.pins.value = copy;
}
render() {
@@ -339,13 +390,13 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
);
// Typescript doesn't like fileds defined as `X | undefined` not being present, hence cast
const objInfo: ObjectInfo = {
id: this.objectId,
name: this.objectName,
id: this.objectId.value,
name: this.objectName.value,
prefab: this.prefabName,
} as ObjectInfo;
if (this.slots.length > 0) {
const slotOccupants: [FrozenObject, number][] = this.slots.flatMap(
if (this.slots.value.length > 0) {
const slotOccupants: [FrozenObject, number][] = this.slots.value.flatMap(
(slot, index) => {
return typeof slot.occupant !== "undefined"
? [[slot.occupant, index]]
@@ -367,8 +418,8 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
}),
);
}
objInfo.slots = Object.fromEntries(
this.slots.flatMap((slot, index) => {
objInfo.slots = new Map(
this.slots.value.flatMap((slot, index) => {
const occupantId = slotOccupantIdsMap.get(index);
if (typeof occupantId !== "undefined") {
const info: SlotOccupantInfo = {
@@ -383,9 +434,9 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
);
}
if (this.connections.length > 0) {
objInfo.connections = Object.fromEntries(
this.connections.flatMap((conn, index) => {
if (this.connections.value.length > 0) {
objInfo.connections = new Map(
this.connections.value.flatMap((conn, index) => {
return typeof conn === "object" &&
"CableNetwork" in conn &&
typeof conn.CableNetwork.net !== "undefined"
@@ -395,11 +446,8 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
);
}
if (this.fields.size > 0) {
objInfo.logic_values = Object.fromEntries(this.fields) as Record<
LogicType,
number
>;
if (Object.keys(this.fields.value).length > 0) {
objInfo.logic_values = new Map(Object.entries(this.fields.value) as [LogicType, number][]);
}
const template: FrozenObject = {

View File

@@ -14,7 +14,7 @@ import * as Comlink from "comlink";
import "./baseDevice";
import "./device";
import { App } from "app";
import { structuralEqual, TypedEventTarget } from "utils";
import { comlinkSpecialJsonTransferHandler, structuralEqual, TypedEventTarget } from "utils";
export interface ToastMessage {
variant: "warning" | "danger" | "success" | "primary" | "neutral";
icon: string;
@@ -26,8 +26,10 @@ import {
signal,
computed,
effect,
batch,
} from '@lit-labs/preact-signals';
import type { Signal } from '@lit-labs/preact-signals';
import { getJsonContext } from "./jsonErrorUtils";
export interface VirtualMachineEventMap {
"vm-template-db-loaded": CustomEvent<TemplateDatabase>;
@@ -41,16 +43,23 @@ export interface VirtualMachineEventMap {
"vm-message": CustomEvent<ToastMessage>;
}
Comlink.transferHandlers.set("SpecialJson", comlinkSpecialJsonTransferHandler);
const jsonErrorRegex = /((invalid type: .*)|(missing field .*)) at line (?<errorLine>\d+) column (?<errorColumn>\d+)/;
class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
ic10vm: Comlink.Remote<VMRef>;
templateDBPromise: Promise<TemplateDatabase>;
templateDB: TemplateDatabase;
private _vmState: Signal<FrozenVM>;
private _vmState: Signal<FrozenVM> = signal(null);
private _objects: Map<number, Signal<FrozenObjectFull>>;
private _objectIds: Signal<ObjectID[]>;
private _circuitHolders: Map<number, Signal<FrozenObjectFull>>;
private _circuitHolderIds: Signal<ObjectID[]>;
private _networks: Map<number, Signal<FrozenCableNetwork>>;
private _networkIds: Signal<ObjectID[]>;
private _default_network: Signal<number>;
private vm_worker: Worker;
@@ -62,15 +71,17 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
this.app = app;
this._objects = new Map();
this._objectIds = signal([]);
this._circuitHolders = new Map();
this._circuitHolderIds = signal([]);
this._networks = new Map();
this._networkIds = signal([]);
this._networkDevicesSignals = new Map();
this.setupVM();
}
async setupVM() {
this.templateDBPromise = this.ic10vm.getTemplateDatabase();
this.templateDBPromise.then((db) => this.setupTemplateDatabase(db));
this.vm_worker = new Worker(new URL("./vmWorker.ts", import.meta.url));
const loaded = (w: Worker) =>
@@ -82,6 +93,9 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
this._vmState.value = await this.ic10vm.saveVMState();
window.VM.set(this);
this.templateDBPromise = this.ic10vm.getTemplateDatabase();
this.templateDBPromise.then((db) => this.setupTemplateDatabase(db));
effect(() => {
this.updateObjects(this._vmState.value);
this.updateNetworks(this._vmState.value);
@@ -98,26 +112,24 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
return this._objects;
}
get objectIds(): ObjectID[] {
const ids = Array.from(this._objects.keys());
ids.sort();
return ids;
get objectIds(): Signal<ObjectID[]> {
return this._objectIds;
}
get circuitHolders() {
return this._circuitHolders;
}
get circuitHolderIds(): ObjectID[] {
const ids = Array.from(this._circuitHolders.keys());
ids.sort();
return ids;
get circuitHolderIds(): Signal<ObjectID[]> {
return this._circuitHolderIds;
}
get networks(): ObjectID[] {
const ids = Array.from(this._networks.keys());
ids.sort();
return ids;
get networks() {
return this._networks;
}
get networkIds(): Signal<ObjectID[]> {
return this._networkIds;
}
get defaultNetwork() {
@@ -187,6 +199,9 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
}
this.app.session.save();
}
networkIds.sort();
this._networkIds.value = networkIds;
}
async updateObjects(state: FrozenVM) {
@@ -260,6 +275,15 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
}
this.app.session.save();
}
objectIds.sort();
const circuitHolderIds = Array.from(this._circuitHolders.keys());
circuitHolderIds.sort();
batch(() => {
this._objectIds.value = objectIds;
this._circuitHolderIds.value = circuitHolderIds;
});
}
async updateCode() {
@@ -332,23 +356,52 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
}
}
handleVmError(err: Error) {
console.log("Error in Virtual Machine", err);
const message: ToastMessage = {
handleVmError(err: Error, args: { context?: string, jsonContext?: string, trace?: boolean } = {}) {
const message = args.context ? `Error in Virtual Machine {${args.context}}` : "Error in Virtual Machine";
console.log(message, err);
if (args.jsonContext != null) {
const jsonTypeError = err.message.match(jsonErrorRegex)
if (jsonTypeError) {
console.log(
"Json Error context",
getJsonContext(
parseInt(jsonTypeError.groups["errorLine"]),
parseInt(jsonTypeError.groups["errorColumn"]),
args.jsonContext,
100
)
)
}
}
if (args.trace) {
console.trace();
}
const toastMessage: ToastMessage = {
variant: "danger",
icon: "bug",
title: `Error in Virtual Machine ${err.name}`,
msg: err.message,
id: Date.now().toString(16),
};
this.dispatchCustomEvent("vm-message", message);
this.dispatchCustomEvent("vm-message", toastMessage);
}
// return the data connected oject ids for a network
networkDataDevices(network: ObjectID): number[] {
networkDataDevices(network: ObjectID): ObjectID[] {
return this._networks.get(network)?.peek().devices ?? [];
}
private _networkDevicesSignals: Map<ObjectID, Signal<ObjectID[]>>;
networkDataDevicesSignal(network: ObjectID): Signal<ObjectID[]> {
if (!this._networkDevicesSignals.has(network) && this._networks.get(network) != null) {
this._networkDevicesSignals.set(network, computed(
() => this._networks.get(network).value.devices ?? []
));
}
return this._networkDevicesSignals.get(network);
}
async changeObjectID(oldID: number, newID: number): Promise<boolean> {
try {
await this.ic10vm.changeDeviceId(oldID, newID);
@@ -581,12 +634,13 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
async restoreVMState(state: FrozenVM) {
try {
console.info("Restoring VM State from", state);
await this.ic10vm.restoreVMState(state);
this._objects = new Map();
this._circuitHolders = new Map();
await this.update();
} catch (e) {
this.handleVmError(e);
this.handleVmError(e, {jsonContext: JSON.stringify(state)});
}
}

View File

@@ -0,0 +1,192 @@
function jsonMatchingBrace(char: string): string {
switch (char) {
case "[":
return "]";
case "]":
return "[";
case "{":
return "}";
case "}":
return "{";
default:
return char;
}
}
function readJsonContextBack(ctx: string, maxLen: number, stringCanEnd: boolean): string {
const tokenContexts: string[] = [];
let inStr: boolean = false;
let foundOpen: boolean = false;
let ctxEnd: number = 0;
let lastChar: string | null = null;
let lastNonWhitespaceChar: string | null = null;
let countStringOpens: number = 0;
let countObjectsFound: number = 0;
const chars = ctx.split("");
for (let i = chars.length; i >= 0; i--) {
const c = chars[i];
if (c === ":" && inStr && countStringOpens === 1 && lastNonWhitespaceChar === '"') {
inStr = false;
tokenContexts.pop();
}
if (c === "\\" && !inStr && lastChar === '"') {
if (lastChar != null) {
tokenContexts.push(lastChar);
}
foundOpen = false;
inStr = true;
continue;
}
if (c === '"') {
if (inStr && (tokenContexts.length > 0 ? tokenContexts[tokenContexts.length - 1] : null) === c) {
inStr = false;
if (stringCanEnd) {
foundOpen = true;
}
tokenContexts.pop();
} else {
inStr = true;
countStringOpens += 1;
tokenContexts.push(c);
}
}
if ((c === "]" || c === "}") && !inStr) {
tokenContexts.push(c);
}
if (
(c === "[" || c === "{")
&& !inStr
&& (tokenContexts.length > 0 ? tokenContexts[tokenContexts.length - 1] : null) === jsonMatchingBrace(c)
) {
tokenContexts.pop();
foundOpen = true;
}
if (
(c === ",")
&& !inStr
&& (lastNonWhitespaceChar === "[" || lastNonWhitespaceChar === "{" || lastNonWhitespaceChar === '"')
) {
foundOpen = true;
}
lastChar = c;
if (!(' \t\n\r\v'.indexOf(c) > -1)) {
lastNonWhitespaceChar = c;
}
if (foundOpen && !tokenContexts.length) {
countObjectsFound += 1;
}
if (countObjectsFound >= 2) {
ctxEnd = i;
break;
}
}
if (ctxEnd > 0) {
ctx = ctx.substring(ctxEnd);
}
if (maxLen > 0 && ctx.length > maxLen) {
ctx = "... " + ctx.substring(maxLen);
}
return ctx;
}
function readJsonContextForward(ctx: string, maxLen: number, stringCanEnd: boolean): string {
const tokenContexts: string[] = [];
let inStr: boolean = false;
let foundClose: boolean = false;
let ctxEnd: number = 0;
let lastChar: string | null = null;
let lastNonWhitespaceChar: string | null = null;
let countStringOpens: number = 0;
let countObjectsFound: number = 0;
const chars = ctx.split("");
for (let i = 0; i < chars.length; i++) {
const c = chars[i];
if (c === ":" && inStr && countStringOpens === 1 && lastNonWhitespaceChar === '"') {
inStr = false;
tokenContexts.pop();
}
if (c === "\\" && !inStr && lastChar === "'") {
if (lastChar != null) {
tokenContexts.push(lastChar);
}
foundClose = false;
inStr = true;
continue;
}
if (c === '"') {
if (
inStr
&& (tokenContexts.length > 0 ? tokenContexts[tokenContexts.length - 1] : null) === c
) {
inStr = false;
if (stringCanEnd) {
foundClose = true;
}
tokenContexts.pop();
} else {
inStr = true;
countStringOpens += 1;
tokenContexts.push(c);
}
}
if (
(c === "]" || c === "}")
&& !inStr
) {
tokenContexts.pop();
foundClose = true;
}
if (
(c === "[" || c === "{")
&& !inStr
&& (tokenContexts.length > 0 ? tokenContexts[tokenContexts.length - 1] : null) === jsonMatchingBrace(c)
) {
tokenContexts.push(c);
}
if (
(c === ",")
&& !inStr
&& (lastNonWhitespaceChar === "]" || lastNonWhitespaceChar === "}" || lastNonWhitespaceChar === '"')
) {
foundClose = true;
}
lastChar = c;
if (!(' \t\n\r\v'.indexOf(c) > -1)) {
lastNonWhitespaceChar = c;
}
if (foundClose && !tokenContexts.length) {
countObjectsFound += 1;
}
if (countObjectsFound >= 2) {
ctxEnd = i;
break;
}
}
if (ctxEnd > 0) {
ctx = ctx.substring(0, ctxEnd);
}
if (maxLen > 0 && ctx.length > maxLen) {
ctx = ctx.substring(0, maxLen) + " ...";
}
return ctx
}
export function getJsonContext(errLine: number, errColumn: number, body: string, maxLen: number): string {
if (!body.length) {
return body;
}
const stringCanEnd = body[0] !== '[' && body[0] !== "{";
const lineOffset = (body.split("").map((c, i) => [c, i]).filter(([c, _]) => c === "\n")[errLine - 1] as [string, number] ?? ["", 0] as [string, number])[1];
const preLine = body.substring(0, lineOffset);
const ctxLine = body.substring(lineOffset);
const ctxBefore = preLine + ctxLine.substring(0, errColumn);
const ctxAfter = ctxLine.substring(errColumn);
return readJsonContextBack(ctxBefore, maxLen, stringCanEnd) + "<~~" + readJsonContextForward(ctxAfter, maxLen, stringCanEnd);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import { html, css } from "lit";
import { html, css, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMActiveICMixin } from "virtualMachine/baseDevice";
@@ -6,6 +6,7 @@ import { VMActiveICMixin } from "virtualMachine/baseDevice";
import { RegisterSpec } from "ic10emu_wasm";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
import { displayNumber, parseNumber } from "utils";
import { computed, Signal, watch } from "@lit-labs/preact-signals";
@customElement("vm-ic-registers")
export class VMICRegisters extends VMActiveICMixin(BaseElement) {
@@ -40,12 +41,12 @@ export class VMICRegisters extends VMActiveICMixin(BaseElement) {
constructor() {
super();
this.subscribe("ic", "active-ic")
this.subscribe("active-ic")
}
protected render() {
const registerAliases: [string, number][] =
[...(Array.from(this.aliases?.entries() ?? []))].flatMap(
const registerAliases: Signal<[string, number][]> = computed(() => {
return [...(Array.from(this.objectSignals.aliases.value?.entries() ?? []))].flatMap(
([alias, target]) => {
if ("RegisterSpec" in target && target.RegisterSpec.indirection === 0) {
return [[alias, target.RegisterSpec.target]] as [string, number][];
@@ -54,33 +55,49 @@ export class VMICRegisters extends VMActiveICMixin(BaseElement) {
}
}
).concat(VMICRegisters.defaultAliases);
});
const registerHtml = this.objectSignals?.registers.peek().map((val, index) => {
const aliases = computed(() => {
return registerAliases.value
.filter(([_alias, target]) => index === target)
.map(([alias, _target]) => alias);
});
const aliasesList = computed(() => {
return aliases.value.join(", ");
});
const aliasesText = computed(() => {
return aliasesList.value || "None";
});
const valDisplay = computed(() => {
const val = this.objectSignals.registers.value[index];
return displayNumber(val);
});
return html`
<sl-tooltip placement="left" class="tooltip">
<div slot="content">
<strong>Register r${index}</strong> Aliases:
<em>${watch(aliasesText)}</em>
</div>
<sl-input
type="text"
value="${watch(valDisplay)}"
size="small"
class="reg-input"
@sl-change=${this._handleCellChange}
key=${index}
>
<span slot="prefix">r${index}</span>
<span slot="suffix">${watch(aliasesList)}</span>
</sl-input>
</sl-tooltip>
`;
}) ?? nothing;
return html`
<sl-card class="card">
<div class="card-body">
${this.registers?.map((val, index) => {
const aliases = registerAliases
.filter(([_alias, target]) => index === target)
.map(([alias, _target]) => alias);
return html`
<sl-tooltip placement="left" class="tooltip">
<div slot="content">
<strong>Register r${index}</strong> Aliases:
<em>${aliases.join(", ") || "None"}</em>
</div>
<sl-input
type="text"
value="${displayNumber(val)}"
size="small"
class="reg-input"
@sl-change=${this._handleCellChange}
key=${index}
>
<span slot="prefix">r${index}</span>
<span slot="suffix">${aliases.join(", ")}</span>
</sl-input>
</sl-tooltip>
`;
})}
${registerHtml}
</div>
</sl-card>
`;

View File

@@ -1,10 +1,11 @@
import { html, css } from "lit";
import { html, css, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMActiveICMixin } from "virtualMachine/baseDevice";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
import { displayNumber, parseNumber } from "utils";
import { computed, watch } from "@lit-labs/preact-signals";
@customElement("vm-ic-stack")
export class VMICStack extends VMActiveICMixin(BaseElement) {
@@ -37,35 +38,49 @@ export class VMICStack extends VMActiveICMixin(BaseElement) {
constructor() {
super();
this.subscribe("ic", "active-ic")
this.subscribe("active-ic")
}
protected render() {
const sp = this.registers != null ? this.registers[16] : 0;
const sp = computed(() => {
return this.objectSignals.registers.value != null ? this.objectSignals.registers.value[16] : 0;
});
const memoryHtml = this.objectSignals?.memory.peek()?.map((val, index) => {
const content = computed(() => {
return sp.value === index ? html`<strong>Stack Pointer</strong>` : nothing;
});
const pointerClass = computed(() => {
return sp.value === index ? "stack-pointer" : nothing;
});
const displayVal = computed(() => {
return displayNumber(this.objectSignals.memory.value[index]);
});
return html`
<sl-tooltip placement="left">
<div slot="content">
${watch(content)}
Address ${index}
</div>
<sl-input
type="text"
value="${watch(displayVal)}"
size="small"
class="stack-input ${watch(pointerClass)}"
@sl-change=${this._handleCellChange}
key=${index}
>
<span slot="prefix"> ${index} </span>
</sl-input>
</sl-tooltip>
`;
}) ?? nothing;
return html`
<sl-card class="card">
<div class="card-body">
${this.memory?.map((val, index) => {
return html`
<sl-tooltip placement="left">
<div slot="content">
${sp === index ? html`<strong>Stack Pointer</strong>` : ""}
Address ${index}
</div>
<sl-input
type="text"
value="${displayNumber(val)}"
size="small"
class="stack-input ${sp === index ? "stack-pointer" : ""}"
@sl-change=${this._handleCellChange}
key=${index}
>
<span slot="prefix"> ${index} </span>
</sl-input>
</sl-tooltip>
`;
})}
${memoryHtml}
</div>
</sl-card>
`;

View File

@@ -6,24 +6,24 @@ import type {
import * as Comlink from "comlink";
import prefabDatabase from "./prefabDatabase";
import { parseNumber } from "utils";
import { comlinkSpecialJsonTransferHandler, parseNumber } from "utils";
Comlink.transferHandlers.set("SpecialJson", comlinkSpecialJsonTransferHandler);
console.info("Processing Json prefab Database ", prefabDatabase);
const vm: VMRef = init();
const template_database = Object.fromEntries(
const start_time = performance.now();
const template_database = new Map(
Object.entries(prefabDatabase.prefabsByHash).map(([hash, prefabName]) => [
parseInt(hash),
prefabDatabase.prefabs[prefabName],
]),
) as TemplateDatabase;
console.info("Loading Prefab Template Database into VM", template_database);
try {
console.info("Loading Prefab Template Database into VM", template_database);
const start_time = performance.now();
// vm.importTemplateDatabase(template_database);
vm.importTemplateDatabase(template_database);
const now = performance.now();