refactor(frontend): fix signal graph

This commit is contained in:
Rachel Powers
2024-08-24 16:38:07 -07:00
parent b1c9db278d
commit 6e503f957a
22 changed files with 1770 additions and 1716 deletions

View File

@@ -155,7 +155,7 @@ impl VM {
obj_ids.push(obj_id)
}
transaction.finialize()?;
transaction.finalize()?;
let transaction_ids = transaction.id_space.in_use_ids();
self.id_space.borrow_mut().use_new_ids(&transaction_ids);
@@ -200,15 +200,12 @@ impl VM {
/// current database.
/// Errors if the object can not be built do to a template error
/// Returns the built object's ID
pub fn add_object_frozen(
self: &Rc<Self>,
frozen: FrozenObject,
) -> Result<ObjectID, VMError> {
pub fn add_object_frozen(self: &Rc<Self>, frozen: FrozenObject) -> Result<ObjectID, VMError> {
let mut transaction = VMTransaction::new(self);
let obj_id = transaction.add_object_from_frozen(frozen)?;
transaction.finialize()?;
transaction.finalize()?;
let transaction_ids = transaction.id_space.in_use_ids();
self.id_space.borrow_mut().use_new_ids(&transaction_ids);
@@ -1351,17 +1348,7 @@ impl VM {
.objects
.borrow()
.iter()
.filter_map(|(_obj_id, obj)| {
if obj
.borrow()
.as_item()
.is_some_and(|item| item.get_parent_slot().is_some())
{
None
} else {
Some(FrozenObject::freeze_object_sparse(obj, self))
}
})
.map(|(_obj_id, obj)| FrozenObject::freeze_object_sparse(obj, self))
.collect::<Result<Vec<_>, _>>()?,
networks: self
.networks
@@ -1406,7 +1393,7 @@ impl VM {
for frozen in state.objects {
let _ = transaction.add_object_from_frozen(frozen)?;
}
transaction.finialize()?;
transaction.finalize()?;
self.circuit_holders.borrow_mut().clear();
self.program_holders.borrow_mut().clear();
@@ -1423,6 +1410,7 @@ impl VM {
let transaction_ids = transaction.id_space.in_use_ids();
self.id_space.borrow_mut().use_ids(&transaction_ids)?;
self.objects.borrow_mut().extend(transaction.objects);
self.circuit_holders
.borrow_mut()
.extend(transaction.circuit_holders);
@@ -1557,7 +1545,7 @@ impl VMTransaction {
Ok(obj_id)
}
pub fn finialize(&mut self) -> Result<(), VMError> {
pub fn finalize(&mut self) -> Result<(), VMError> {
for (child, (slot, parent)) in &self.object_parents {
let child_obj = self
.objects

View File

@@ -203,9 +203,11 @@ pub struct SlotInfo {
#[cfg_attr(feature = "tsify", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct LogicInfo {
#[serde_as( as = "BTreeMap<DisplayFromStr, _>")]
#[cfg_attr(feature = "tsify", tsify(type = "Map<string, Map<LogicSlotType, MemoryAccess>>"))]
pub logic_slot_types: BTreeMap<u32, BTreeMap<LogicSlotType, MemoryAccess>>,
pub logic_types: BTreeMap<LogicType, MemoryAccess>,
#[serde_as( as = "Option<BTreeMap<DisplayFromStr, _>>")]
#[cfg_attr(feature = "tsify", tsify(type = "Map<string, string> | undefined"))]
pub modes: Option<BTreeMap<u32, String>>,
pub transmission_receiver: bool,
pub wireless_logic: bool,

View File

@@ -132,6 +132,7 @@
"trunc",
"ufuzzy",
"VMIC",
"VMUI",
"vstack",
"whos"
],

View File

@@ -243,7 +243,7 @@ export class IC10Editor extends BaseElement {
app.session.onLoad((_e) => {
const session = app.session;
const updated_ids: number[] = [];
for (const [id, code] of session.programs) {
for (const [id, code] of session.programs.value) {
updated_ids.push(id);
that.createOrSetSession(id, code);
}
@@ -271,7 +271,7 @@ export class IC10Editor extends BaseElement {
that.activeLineMarkers.set(
id,
session.addMarker(
new Range(active_line, 0, active_line, 1),
new Range(active_line.value, 0, active_line.value, 1),
"vm_ic_active_line",
"fullLine",
true,

View File

@@ -39,7 +39,7 @@ import "@shoelace-style/shoelace/dist/components/relative-time/relative-time.js"
import "ace-builds";
import "ace-builds/esm-resolver";
class DeferedApp {
class DeferredApp {
app: App;
private resolvers: ((value: App) => void)[];
@@ -69,7 +69,7 @@ class DeferedApp {
}
class DeferedVM {
class DeferredVM {
vm: VirtualMachine;
private resolvers: ((value: VirtualMachine) => void)[];
@@ -102,13 +102,13 @@ class DeferedVM {
declare global {
interface Window
{
App: DeferedApp;
VM: DeferedVM;
App: DeferredApp;
VM: DeferredVM;
}
}
window.App = new DeferedApp();
window.VM = new DeferedVM();
window.App = new DeferredApp();
window.VM = new DeferredVM();
import type { App } from "./app";
import type { VirtualMachine } from "./virtualMachine";

View File

@@ -25,6 +25,7 @@ import {
} from "./utils";
import * as presets from "./presets";
import { batch, computed, effect, signal, Signal } from "@lit-labs/preact-signals";
const { demoVMState } = presets;
export interface SessionEventMap {
@@ -37,63 +38,64 @@ export interface SessionEventMap {
}
export class Session extends TypedEventTarget<SessionEventMap>() {
private _programs: Map<number, string>;
private _errors: Map<number, ICError[]>;
private _activeIC: number;
private _activeLines: Map<number, number>;
private _programs: Signal<Map<ObjectID, string>>;
private _errors: Signal<Map<ObjectID, ICError[]>>;
private _activeIC: Signal<ObjectID>;
private _activeLines: Signal<Map<ObjectID, number>>;
private _save_timeout?: ReturnType<typeof setTimeout>;
private _vm_state: FrozenVM;
private app: App;
constructor(app: App) {
super();
this.app = app;
this._programs = new Map();
this._errors = new Map();
this._programs = signal(new Map());
this._errors = signal(new Map());
this._save_timeout = undefined;
this._activeIC = 1;
this._activeLines = new Map();
this._vm_state = undefined;
this._activeIC = signal(null);
this._activeLines = signal(new Map());
this.loadFromFragment();
const that = this;
window.addEventListener("hashchange", (_event) => {
that.loadFromFragment();
});
this._programs.subscribe((_) => {this._fireOnLoad()});
}
get programs(): Map<number, string> {
get programs(): Signal<Map<number, string>> {
return this._programs;
}
set programs(programs: Iterable<[number, string]>) {
this._programs = new Map([...programs]);
this._fireOnLoad();
this._programs.value = new Map(programs);
}
get activeIC() {
get activeIC(): Signal<ObjectID> {
return this._activeIC;
}
set activeIC(val: number) {
this._activeIC = val;
this.dispatchCustomEvent("session-active-ic", this.activeIC);
set activeIC(val: ObjectID) {
this._activeIC.value = val;
this.dispatchCustomEvent("session-active-ic", this.activeIC.peek());
}
changeID(oldID: number, newID: number) {
if (this.programs.has(oldID)) {
this.programs.set(newID, this.programs.get(oldID));
this.programs.delete(oldID);
changeID(oldID: ObjectID, newID: ObjectID) {
if (this.programs.peek().has(oldID)) {
const newVal = new Map(this.programs.value);
newVal.set(newID, newVal.get(oldID));
newVal.delete(oldID);
this.programs.value = newVal;
}
this.dispatchCustomEvent("session-id-change", { old: oldID, new: newID });
}
onIDChange(callback: (e: CustomEvent<{ old: number; new: number }>) => any) {
onIDChange(callback: (e: CustomEvent<{ old: ObjectID; new: ObjectID}>) => any) {
this.addEventListener("session-id-change", callback);
}
onActiveIc(callback: (e: CustomEvent<number>) => any) {
onActiveIc(callback: (e: CustomEvent<ObjectID>) => any) {
this.addEventListener("session-active-ic", callback);
}
@@ -101,36 +103,36 @@ export class Session extends TypedEventTarget<SessionEventMap>() {
return this._errors;
}
getActiveLine(id: number) {
return this._activeLines.get(id);
getActiveLine(id: ObjectID) {
return computed(() => this._activeLines.value.get(id));
}
setActiveLine(id: number, line: number) {
const last = this._activeLines.get(id);
setActiveLine(id: ObjectID, line: number) {
const last = this._activeLines.peek().get(id);
if (last !== line) {
this._activeLines.set(id, line);
this._activeLines.value = new Map([ ... this._activeLines.value.entries(), [id, line]]);
this._fireOnActiveLine(id);
}
}
setProgramCode(id: number, code: string) {
this._programs.set(id, code);
setProgramCode(id: ObjectID, code: string) {
this._programs.value = new Map([ ...this._programs.value.entries(), [id, code]]);
if (this.app.vm) {
this.app.vm.updateCode();
}
this.save();
}
setProgramErrors(id: number, errors: ICError[]) {
this._errors.set(id, errors);
setProgramErrors(id: ObjectID, errors: ICError[]) {
this._errors.value = new Map([ ...this._errors.value.entries(), [id, errors]]);
this._fireOnErrors([id]);
}
_fireOnErrors(ids: number[]) {
_fireOnErrors(ids: ObjectID[]) {
this.dispatchCustomEvent("session-errors", ids);
}
onErrors(callback: (e: CustomEvent<number[]>) => any) {
onErrors(callback: (e: CustomEvent<ObjectID[]>) => any) {
this.addEventListener("session-errors", callback);
}
@@ -142,7 +144,7 @@ export class Session extends TypedEventTarget<SessionEventMap>() {
this.dispatchCustomEvent("session-load", this);
}
onActiveLine(callback: (e: CustomEvent<number>) => any) {
onActiveLine(callback: (e: CustomEvent<ObjectID>) => any) {
this.addEventListener("active-line", callback);
}
@@ -159,7 +161,8 @@ export class Session extends TypedEventTarget<SessionEventMap>() {
}
async saveToFragment() {
const toSave = { vm: this.app.vm.saveVMState(), activeIC: this.activeIC };
const vm = await window.VM.get()
const toSave = { vm: vm.state.vm.value, activeIC: this.activeIC };
const bytes = new TextEncoder().encode(toJson(toSave));
try {
const c_bytes = await compress(bytes, defaultCompression);
@@ -172,21 +175,21 @@ export class Session extends TypedEventTarget<SessionEventMap>() {
}
async load(data: SessionDB.CurrentDBVmState | OldPrograms | string) {
const vm = await window.VM.get()
if (typeof data === "string") {
this._activeIC = 1;
this.app.vm.restoreVMState(demoVMState.vm);
this._programs = new Map([[1, data]]);
this.activeIC = 1;
await vm.restoreVMState(demoVMState.vm);
this.programs = [[1, data]];
} else if ("programs" in data) {
this._activeIC = 1;
this.app.vm.restoreVMState(demoVMState.vm);
this._programs = new Map(data.programs);
this.activeIC = 1;
await vm.restoreVMState(demoVMState.vm);
this.programs = data.programs;
} else if ("vm" in data) {
this._programs = new Map();
this.programs = [];
const state = data.vm;
// assign first so it's present when the
// vm fires events
this._activeIC = data.activeIC;
const vm = await window.VM.get()
this._activeIC.value = data.activeIC;
await vm.restoreVMState(state);
this.programs = vm.getPrograms();
// assign again to fire event
@@ -259,7 +262,7 @@ export class Session extends TypedEventTarget<SessionEventMap>() {
async saveLocal(name: string) {
const state: SessionDB.CurrentDBVmState = {
vm: await (await window.VM.get()).ic10vm.saveVMState(),
activeIC: this.activeIC,
activeIC: this.activeIC.peek(),
};
const db = await this.openIndexDB();
const transaction = db.transaction(

View File

@@ -1,6 +1,18 @@
import { Ace } from "ace-builds";
import { TransferHandler } from "comlink";
export function isSome<T>(object: T | null | undefined): object is T {
return typeof object !== "undefined" && object !== null;
}
export function range(size: number, start: number = 0): number[] {
const base = [...Array(size ?? 0).keys()]
if (start != 0) {
return base.map(i => i + start);
}
return base
}
export function docReady(fn: () => void) {
// see if DOM is already available
if (

View File

@@ -1,334 +1,22 @@
import { property, state } from "lit/decorators.js";
import type {
Slot,
Connection,
ICError,
LogicType,
LogicField,
Operand,
ObjectID,
TemplateDatabase,
FrozenObjectFull,
Class,
LogicSlotType,
SlotOccupantInfo,
ICState,
ObjectTemplate,
} from "ic10emu_wasm";
import { crc32, structuralEqual } from "utils";
import { LitElement, PropertyValueMap } from "lit";
import { LitElement } from "lit";
import {
computed,
signal,
} from '@lit-labs/preact-signals';
import type { Signal } from '@lit-labs/preact-signals';
export interface VmObjectSlotInfo {
parent: ObjectID;
index: number;
name: string;
typ: Class;
logicFields: Map<LogicSlotType, LogicField>;
quantity: number;
occupant: ComputedObjectSignals | null;
}
export class ComputedObjectSignals {
obj: Signal<FrozenObjectFull>;
id: Signal<number>;
template: Signal<ObjectTemplate>;
name: Signal<string | null>;
nameHash: Signal<number | null>;
prefabName: Signal<string | null>;
prefabHash: Signal<number | null>;
displayName: Signal<string>;
logicFields: Signal<Map<LogicType, LogicField> | null>;
slots: Signal<VmObjectSlotInfo[] | null>;
slotsCount: Signal<number | null>;
reagents: Signal<Map<number, number> | null>;
connections: Signal<Connection[] | null>;
visibleDevices: Signal<ComputedObjectSignals[]>;
memory: Signal<number[] | null>;
icIP: Signal<number | null>;
icOpCount: Signal<number | null>;
icState: Signal<ICState | null>;
errors: Signal<ICError[] | null>;
registers: Signal<number[] | null>;
aliases: Signal<Map<string, Operand> | null>;
defines: Signal<Map<string, number> | null>;
numPins: Signal<number | null>;
pins: Signal<Map<number, ObjectID> | null>;
constructor(obj: Signal<FrozenObjectFull>) {
this.obj = obj
this.id = computed(() => { return this.obj.value.obj_info.id; });
this.template = computed(() => { return this.obj.value.template; });
this.name = computed(() => { return this.obj.value.obj_info.name; });
this.nameHash = computed(() => { return this.name.value !== "undefined" ? crc32(this.name.value) : null; });
this.prefabName = computed(() => { return this.obj.value.obj_info.prefab; });
this.prefabHash = computed(() => { return this.obj.value.obj_info.prefab_hash; });
this.displayName = computed(() => { return this.obj.value.obj_info.name ?? this.obj.value.obj_info.prefab; });
this.logicFields = computed(() => {
const obj_info = this.obj.value.obj_info;
const template = this.obj.value.template;
const logicValues =
obj_info.logic_values != null
? (new Map(Object.entries(obj_info.logic_values)) as Map<
LogicType,
number
>)
: null;
const logicTemplate =
"logic" in template ? template.logic : null;
return new Map(
Array.from(Object.entries(logicTemplate?.logic_types) ?? []).map(
([lt, access]) => {
let field: LogicField = {
field_type: access,
value: logicValues.get(lt as LogicType) ?? 0,
};
return [lt as LogicType, field];
},
),
)
});
this.slots = computed(() => {
const obj_info = this.obj.value.obj_info;
const template = this.obj.value.template;
const slotsOccupantInfo =
obj_info.slots != null
? new Map(
Object.entries(obj_info.slots).map(([key, val]) => [
parseInt(key),
val,
]),
)
: null;
const slotsLogicValues =
obj_info.slot_logic_values != null
? new Map<number, Map<LogicSlotType, number>>(
Object.entries(obj_info.slot_logic_values).map(
([index, values]) => [
parseInt(index),
new Map(Object.entries(values)) as Map<
LogicSlotType,
number
>,
],
),
)
: null;
const logicTemplate =
"logic" in template ? template.logic : null;
const slotsTemplate =
"slots" in template ? template.slots : [];
return slotsTemplate.map((template, index) => {
const fieldEntryInfos = Array.from(
Object.entries(logicTemplate?.logic_slot_types.get(index)) ?? [],
);
const logicFields = new Map(
fieldEntryInfos.map(([slt, access]) => {
let field: LogicField = {
field_type: access,
value:
slotsLogicValues.get(index)?.get(slt as LogicSlotType) ?? 0,
};
return [slt as LogicSlotType, field];
}),
);
let occupantInfo = slotsOccupantInfo.get(index);
let occupant =
typeof occupantInfo !== "undefined"
? globalObjectSignalMap.get(occupantInfo.id) ?? null
: null;
let slot: VmObjectSlotInfo = {
parent: obj_info.id,
index: index,
name: template.name,
typ: template.typ,
logicFields: logicFields,
occupant: occupant,
quantity: occupantInfo?.quantity ?? 0,
};
return slot;
});
});
this.slotsCount = computed(() => {
const slotsTemplate =
"slots" in this.obj.value.template ? this.obj.value.template.slots : [];
return slotsTemplate.length;
});
this.reagents = computed(() => {
const reagents =
this.obj.value.obj_info.reagents != null
? new Map(
Object.entries(this.obj.value.obj_info.reagents).map(
([key, val]) => [parseInt(key), val],
),
)
: null;
return reagents;
});
this.connections = computed(() => {
const obj_info = this.obj.value.obj_info;
const template = this.obj.value.template;
const connectionsMap =
obj_info.connections != null
? new Map(
Object.entries(obj_info.connections).map(
([key, val]) => [parseInt(key), val],
),
)
: null;
const connectionList =
"device" in template
? template.device.connection_list
: [];
let connections: Connection[] | null = null;
if (connectionList.length !== 0) {
connections = connectionList.map((conn, index) => {
if (conn.typ === "Data") {
return {
CableNetwork: {
typ: "Data",
role: conn.role,
net: connectionsMap.get(index),
},
};
} else if (conn.typ === "Power") {
return {
CableNetwork: {
typ: "Power",
role: conn.role,
net: connectionsMap.get(index),
},
};
} else if (conn.typ === "PowerAndData") {
return {
CableNetwork: {
typ: "Data",
role: conn.role,
net: connectionsMap.get(index),
},
};
} else if (conn.typ === "Pipe") {
return { Pipe: { role: conn.role } };
} else if (conn.typ === "Chute") {
return { Chute: { role: conn.role } };
} else if (conn.typ === "Elevator") {
return { Elevator: { role: conn.role } };
} else if (conn.typ === "LaunchPad") {
return { LaunchPad: { role: conn.role } };
} else if (conn.typ === "LandingPad") {
return { LandingPad: { role: conn.role } };
} else if (conn.typ === "PipeLiquid") {
return { PipeLiquid: { role: conn.role } };
}
return "None";
});
}
return connections;
});
this.visibleDevices = computed(() => {
return this.obj.value.obj_info.visible_devices.map((id) => globalObjectSignalMap.get(id))
});
this.memory = computed(() => {
return this.obj.value.obj_info.memory ?? null;
});
this.icIP = computed(() => {
return this.obj.value.obj_info.circuit?.instruction_pointer ?? null;
});
this.icOpCount = computed(() => {
return this.obj.value.obj_info.circuit?.yield_instruction_count ?? null;
});
this.icState = computed(() => {
return this.obj.value.obj_info.circuit?.state ?? null;
});
this.errors = computed(() => {
return this.obj.value.obj_info.compile_errors ?? null;
});
this.registers = computed(() => {
return this.obj.value.obj_info.circuit?.registers ?? null;
});
this.aliases = computed(() => {
const aliases = this.obj.value.obj_info.circuit?.aliases ?? null;
return aliases != null ? new Map(Object.entries(aliases)) : null;
});
this.defines = computed(() => {
const defines = this.obj.value.obj_info.circuit?.defines ?? null;
return defines != null ? new Map(Object.entries(defines)) : null;
});
this.pins = computed(() => {
const pins = this.obj.value.obj_info.device_pins;
return pins != null ? new Map(Object.entries(pins).map(([key, val]) => [parseInt(key), val])) : null;
});
this.numPins = computed(() => {
return "device" in this.obj.value.template
? this.obj.value.template.device.device_pins_length
: Math.max(...Array.from(this.pins.value?.keys() ?? [0]));
});
}
}
class ObjectComputedSignalMap extends Map {
get(id: ObjectID): ComputedObjectSignals {
if (!this.has(id)) {
const obj = window.VM.vm.objects.get(id)
if (typeof obj !== "undefined") {
this.set(id, new ComputedObjectSignals(obj));
}
}
return super.get(id);
}
set(id: ObjectID, value: ComputedObjectSignals): this {
super.set(id, value);
return this
}
}
export const globalObjectSignalMap = new ObjectComputedSignalMap();
import { VirtualMachine } from "virtualMachine";
import { property } from "lit/decorators.js";
type Constructor<T = {}> = new (...args: any[]) => T;
export declare class VMObjectMixinInterface {
objectID: Signal<ObjectID>;
activeICId: Signal<ObjectID>;
objectSignals: ComputedObjectSignals | null;
_handleDeviceModified(e: CustomEvent): void;
updateObject(): void;
subscribe(...sub: VMObjectMixinSubscription[]): void;
unsubscribe(filter: (sub: VMObjectMixinSubscription) => boolean): void;
objectIDSignal: Signal<ObjectID>;
objectID: ObjectID;
vm: Signal<VirtualMachine>;
}
export type VMObjectMixinSubscription =
@@ -339,259 +27,33 @@ export const VMObjectMixin = <T extends Constructor<LitElement>>(
superClass: T,
) => {
class VMObjectMixinClass extends superClass {
objectID: Signal<ObjectID | null>;
objectIDSignal: Signal<ObjectID | null> = signal(null);
vm: Signal<VirtualMachine> = signal(null);
@property({type: Number})
get objectID(): number {
return this.objectIDSignal.peek();
}
set objectID(value: number) {
this.objectIDSignal.value = value;
}
constructor (...args: any[]) {
super(...args);
this.objectID = signal(null);
this.objectID.subscribe((_) => {this.updateObject()})
this.setupVM();
}
@state() private objectSubscriptions: VMObjectMixinSubscription[] = [];
subscribe(...sub: VMObjectMixinSubscription[]) {
this.objectSubscriptions = this.objectSubscriptions.concat(sub);
}
// remove subscriptions matching the filter
unsubscribe(filter: (sub: VMObjectMixinSubscription) => boolean) {
this.objectSubscriptions = this.objectSubscriptions.filter(
(sub) => !filter(sub),
);
}
@state() objectSignals: ComputedObjectSignals | null = null;
activeICId: Signal<number> = signal(null);
connectedCallback(): void {
const root = super.connectedCallback();
window.VM.get().then((vm) => {
vm.addEventListener(
"vm-object-modified",
this._handleDeviceModified.bind(this),
);
vm.addEventListener(
"vm-objects-update",
this._handleDevicesModified.bind(this),
);
vm.addEventListener(
"vm-object-id-change",
this._handleDeviceIdChange.bind(this),
);
vm.addEventListener(
"vm-objects-removed",
this._handleDevicesRemoved.bind(this),
);
});
this.updateObject();
return root;
}
disconnectedCallback(): void {
window.VM.get().then((vm) => {
vm.removeEventListener(
"vm-object-modified",
this._handleDeviceModified.bind(this),
);
vm.removeEventListener(
"vm-objects-update",
this._handleDevicesModified.bind(this),
);
vm.removeEventListener(
"vm-object-id-change",
this._handleDeviceIdChange.bind(this),
);
vm.removeEventListener(
"vm-objects-removed",
this._handleDevicesRemoved.bind(this),
);
});
}
async _handleDeviceModified(e: CustomEvent) {
const id = e.detail;
const activeIcId = window.App.app.session.activeIC;
if (this.objectID.peek() === id) {
this.updateObject();
} else if (
id === activeIcId &&
this.objectSubscriptions.includes("active-ic")
) {
this.updateObject();
this.requestUpdate();
} else if (this.objectSubscriptions.includes("visible-devices")) {
const visibleDevices = await window.VM.vm.visibleDeviceIds(
this.objectID.peek(),
);
if (visibleDevices.includes(id)) {
this.updateObject();
this.requestUpdate();
}
}
}
async _handleDevicesModified(e: CustomEvent<number[]>) {
const activeIcId = window.App.app.session.activeIC;
const ids = e.detail;
if (ids.includes(this.objectID.peek())) {
this.updateObject();
if (this.objectSubscriptions.includes("visible-devices")) {
this.requestUpdate();
}
} else if (
ids.includes(activeIcId) &&
this.objectSubscriptions.includes("active-ic")
) {
this.updateObject();
this.requestUpdate();
} else if (this.objectSubscriptions.includes("visible-devices")) {
const visibleDevices = await window.VM.vm.visibleDeviceIds(
this.objectID.peek(),
);
if (ids.some((id) => visibleDevices.includes(id))) {
this.updateObject();
this.requestUpdate();
}
}
}
async _handleDeviceIdChange(e: CustomEvent<{ old: number; new: number }>) {
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.peek(),
);
if (
visibleDevices.some(
(id) => id === e.detail.old || id === e.detail.new,
)
) {
this.requestUpdate();
}
}
}
_handleDevicesRemoved(e: CustomEvent<number[]>) {
const _ids = e.detail;
if (this.objectSubscriptions.includes("visible-devices")) {
this.requestUpdate();
}
}
updateObject() {
this.activeICId.value = window.App.app.session.activeIC;
const newObjSignals = globalObjectSignalMap.get(this.objectID.peek());
if (newObjSignals !== this.objectSignals) {
this.objectSignals = newObjSignals
}
if (typeof this.objectSignals === "undefined") {
return;
}
// other updates needed
private async setupVM() {
this.vm.value = await window.VM.get();
}
}
return VMObjectMixinClass as Constructor<VMObjectMixinInterface> & T;
};
export const VMActiveICMixin = <T extends Constructor<LitElement>>(
superClass: T,
) => {
class VMActiveICMixinClass extends VMObjectMixin(superClass) {
constructor(...args: any[]) {
super(...args);
this.objectID.value = window.App.app.session.activeIC;
}
connectedCallback(): void {
const root = super.connectedCallback();
window.VM.get().then((vm) =>
vm.addEventListener("vm-run-ic", this._handleDeviceModified.bind(this)),
);
window.App.app.session.addEventListener(
"session-active-ic",
this._handleActiveIC.bind(this),
);
return root;
}
disconnectedCallback(): void {
window.VM.get().then((vm) =>
vm.removeEventListener(
"vm-run-ic",
this._handleDeviceModified.bind(this),
),
);
window.App.app.session.removeEventListener(
"session-active-ic",
this._handleActiveIC.bind(this),
);
}
_handleActiveIC(e: CustomEvent) {
const id = e.detail;
if (this.objectID.value !== id) {
this.objectID.value = id;
}
this.updateObject();
}
}
return VMActiveICMixinClass as Constructor<VMObjectMixinInterface> & T;
};
export declare class VMTemplateDBMixinInterface {
templateDB: TemplateDatabase;
templateDB: Signal<TemplateDatabase>;
_handleDeviceDBLoad(e: CustomEvent): void;
postDBSetUpdate(): void;
}
export const VMTemplateDBMixin = <T extends Constructor<LitElement>>(
superClass: T,
) => {
class VMTemplateDBMixinClass extends superClass {
connectedCallback(): void {
const root = super.connectedCallback();
window.VM.vm.addEventListener(
"vm-template-db-loaded",
this._handleDeviceDBLoad.bind(this),
);
if (typeof window.VM.vm.templateDB !== "undefined") {
this.templateDB = window.VM.vm.templateDB!;
}
return root;
}
disconnectedCallback(): void {
window.VM.vm.removeEventListener(
"vm-template-db-loaded",
this._handleDeviceDBLoad.bind(this),
);
}
_handleDeviceDBLoad(e: CustomEvent) {
this.templateDB = e.detail;
}
private _templateDB: TemplateDatabase;
get templateDB(): TemplateDatabase {
return this._templateDB;
}
postDBSetUpdate(): void { }
@state()
set templateDB(val: TemplateDatabase) {
this._templateDB = val;
this.postDBSetUpdate();
}
}
return VMTemplateDBMixinClass as Constructor<VMTemplateDBMixinInterface> & T;
};

View File

@@ -1,29 +1,15 @@
import { html, css, nothing } from "lit";
import { customElement, query } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
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 { computed, Signal, SignalWatcher, watch } from "@lit-labs/preact-signals";
import { FrozenObjectFull } from "ic10emu_wasm";
import { VMObjectMixin } from "./baseDevice";
import { createRef, Ref, ref } from "lit/directives/ref.js";
@customElement("vm-ic-controls")
export class VMICControls extends VMActiveICMixin(BaseElement) {
circuitHolders: Signal<ComputedObjectSignals[]>;
constructor() {
super();
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;
});
}
export class VMICControls extends VMObjectMixin(SignalWatcher(BaseElement)) {
static styles = [
...defaultCss,
@@ -74,37 +60,89 @@ export class VMICControls extends VMActiveICMixin(BaseElement) {
`,
];
@query(".active-ic-select") activeICSelect: SlSelect;
forceSelectUpdate() {
if (this.activeICSelect != null) {
this.activeICSelect.handleValueChange();
}
constructor() {
super();
this.activeIC.subscribe(() => this.forceSelectUpdate());
this.icOptions.subscribe(() => this.forceSelectUpdate());
}
protected render() {
const icsOptions = computed(() => {
return this.circuitHolders.value.map((circuitHolder) => {
activeICSelect: Ref<SlSelect> = createRef();
circuitHolder.prefabName.subscribe((_) => {this.forceSelectUpdate()});
circuitHolder.id.subscribe((_) => {this.forceSelectUpdate()});
circuitHolder.displayName.subscribe((_) => {this.forceSelectUpdate()});
selectUpdateTimeout: ReturnType<typeof setTimeout> = null;
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>`
forceSelectUpdate() {
if (this.selectUpdateTimeout) {
clearTimeout(this.selectUpdateTimeout);
}
this.selectUpdateTimeout = setTimeout(() => {
if (this.activeICSelect.value != null) {
this.activeICSelect.value.value = this.activeIC.value.toString();
this.activeICSelect.value.handleValueChange();
}
}, 100);
}
activeIC = computed(() => {
return this.vm.value?.activeIC.value
})
circuitHolderIds = computed(() => {
return this.vm.value?.state.circuitHolderIds.value ?? [];
});
errors = computed(() => {
const obj = this.vm.value?.state.getObject(this.activeIC.value).value;
return obj?.obj_info.compile_errors ?? [];
});
icIP = computed(() => {
const circuit = this.vm.value?.state.getCircuitInfo(this.activeIC.value).value;
return circuit?.instruction_pointer ?? null;
});
icOpCount = computed(() => {
const circuit = this.vm.value?.state.getCircuitInfo(this.activeIC.value).value;
return circuit?.yield_instruction_count ?? 0;
});
icState = computed(() => {
const circuit = this.vm.value?.state.getCircuitInfo(this.activeIC.value).value;
return circuit?.state ?? null;
});
icOptions = computed(() => {
return this.circuitHolderIds.value.map(id => {
const circuitHolder = computed(() => {
return this.vm.value?.state.getObject(id).value;
});
const prefabName = computed(() => {
return circuitHolder.value?.obj_info.prefab ?? "";
});
const displayName = computed(() => {
return circuitHolder.value?.obj_info.name ?? circuitHolder.value?.obj_info.prefab ?? "";
});
prefabName.subscribe(() => this.forceSelectUpdate());
displayName.subscribe(() => this.forceSelectUpdate());
const span = html`<span slot="suffix">${watch(displayName)}</span>`;
return html`
<sl-option
prefabName=${watch(prefabName)}
.value=${id}
>
${span}
Device:${id} ${watch(displayName)}
</sl-option>`
});
icsOptions.subscribe((_) => {this.forceSelectUpdate()});
});
render() {
const icErrors = computed(() => {
return this.objectSignals?.errors.value?.map(
return this.errors.value.map(
(err) =>
typeof err === "object"
&& "ParseError" in err
@@ -169,28 +207,29 @@ export class VMICControls extends VMActiveICMixin(BaseElement) {
hoist
size="small"
placement="bottom"
value="${watch(this.objectID)}"
value="${this.activeIC.value}"
@sl-change=${this._handleChangeActiveIC}
class="active-ic-select"
${ref(this.activeICSelect)}
>
${watch(icsOptions)}
${watch(this.icOptions)}
</sl-select>
</div>
</div>
<div class="stats">
<div class="hstack">
<span>Instruction Pointer</span>
<span class="ms-auto">${this.objectSignals ? watch(this.objectSignals.icIP) : nothing}</span>
<span class="ms-auto">${watch(this.icIP)}</span>
</div>
<sl-divider></sl-divider>
<div class="hstack">
<span>Last Run Operations Count</span>
<span class="ms-auto">${this.objectSignals ? watch(this.objectSignals.icOpCount) : nothing}</span>
<span class="ms-auto">${watch(this.icOpCount)}</span>
</div>
<sl-divider></sl-divider>
<div class="hstack">
<span>Last State</span>
<span class="ms-auto">${this.objectSignals ? watch(this.objectSignals.icState) : nothing}</span>
<span class="ms-auto">${watch(this.icState)}</span>
</div>
<sl-divider></sl-divider>
<div class="vstack">

View File

@@ -1,4 +1,4 @@
import { html, css } from "lit";
import { html, css, nothing } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
@@ -10,16 +10,24 @@ import { cache } from "lit/directives/cache.js";
import { default as uFuzzy } from "@leeoniya/ufuzzy";
import { when } from "lit/directives/when.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { VMTemplateDBMixin } from "virtualMachine/baseDevice";
import { LogicInfo, ObjectTemplate, StructureInfo } from "ic10emu_wasm";
import { VMObjectMixin } from "virtualMachine/baseDevice";
import { computed, ReadonlySignal, signal, Signal, watch } from "@lit-labs/preact-signals";
import { isSome, range, structuralEqual } from "utils";
type LogicableStructureTemplate = Extract<
ObjectTemplate,
{ structure: StructureInfo; logic: LogicInfo }
>;
type SearchResult = {
entry: LogicableStructureTemplate;
haystackEntry: string;
ranges: number[];
};
@customElement("vm-add-device-button")
export class VMAddDeviceButton extends VMTemplateDBMixin(BaseElement) {
export class VMAddDeviceButton extends VMObjectMixin(BaseElement) {
static styles = [
...defaultCss,
css`
@@ -38,214 +46,262 @@ export class VMAddDeviceButton extends VMTemplateDBMixin(BaseElement) {
@query("sl-drawer") drawer: SlDrawer;
@query(".device-search-input") searchInput: SlInput;
private _structures: Map<string, LogicableStructureTemplate> = new Map();
private _datapoints: [string, string][] = [];
private _haystack: string[] = [];
templateDB = computed(() => {
return this.vm.value?.state.templateDB.value ?? null;
});
postDBSetUpdate(): void {
this._structures = new Map(
Array.from(Object.values(this.templateDB)).flatMap((template) => {
if ("structure" in template && "logic" in template) {
return [[template.prefab.prefab_name, template]] as [
string,
LogicableStructureTemplate,
][];
} else {
return [] as [string, LogicableStructureTemplate][];
}
}),
);
const datapoints: [string, string][] = [];
for (const entry of this._structures.values()) {
datapoints.push(
[entry.prefab.name, entry.prefab.prefab_name],
[entry.prefab.prefab_name, entry.prefab.prefab_name],
[entry.prefab.desc, entry.prefab.prefab_name],
structures = (() => {
let last: Map<string, LogicableStructureTemplate> = null
return computed(() => {
const next = new Map(
Array.from(Object.values(this.templateDB.value ?? {})).flatMap((template) => {
if ("structure" in template && "logic" in template) {
return [[template.prefab.prefab_name, template]] as [
string,
LogicableStructureTemplate,
][];
} else {
return [] as [string, LogicableStructureTemplate][];
}
}),
);
}
const haystack: string[] = datapoints.map((data) => data[0]);
this._datapoints = datapoints;
this._haystack = haystack;
this.performSearch();
}
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
})();
private _filter: string = "";
datapoints = (() => {
let last: [string, string][] = null;
return computed(() => {
const next = [...this.structures.value.values()].flatMap((entry): [string, string][] => {
return [
[entry.prefab.name, entry.prefab.prefab_name],
[entry.prefab.prefab_name, entry.prefab.prefab_name],
[entry.prefab.desc, entry.prefab.prefab_name],
]
});
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
})();
haystack = (() => {
let last: string[] = null;
return computed(() => {
const next = this.datapoints.value.map(data => data[0]);
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
})();
private _filter: Signal<string> = signal("");
private page = signal(0);
get filter() {
return this._filter;
return this._filter.peek();
}
@state()
set filter(val: string) {
this._filter = val;
this.page = 0;
this.performSearch();
this._filter.value = val;
this.page.value = 0;
}
private _searchResults: {
entry: LogicableStructureTemplate;
haystackEntry: string;
ranges: number[];
}[] = [];
private searchResults: ReadonlySignal<SearchResult[]> = (() => {
let last: SearchResult[] = null;
return computed((): SearchResult[] => {
let next: SearchResult[];
if (this._filter.value) {
const uf = new uFuzzy({});
const [_idxs, info, order] = uf.search(
this.haystack.value,
this._filter.value,
0,
1e3,
);
private filterTimeout: number | undefined;
const filtered = order?.map((infoIdx) => ({
name: this.datapoints.value[info.idx[infoIdx]][1],
haystackEntry: this.haystack.value[info.idx[infoIdx]],
ranges: info.ranges[infoIdx],
}));
performSearch() {
if (this._filter) {
const uf = new uFuzzy({});
const [_idxs, info, order] = uf.search(
this._haystack,
this._filter,
0,
1e3,
);
const unique = [...new Set(filtered.map((obj) => obj.name))].map(
(result) => {
return filtered.find((obj) => obj.name === result);
},
);
const filtered = order?.map((infoIdx) => ({
name: this._datapoints[info.idx[infoIdx]][1],
haystackEntry: this._haystack[info.idx[infoIdx]],
ranges: info.ranges[infoIdx],
}));
next = unique.map(({ name, haystackEntry, ranges }) => ({
entry: this.structures.value.get(name)!,
haystackEntry,
ranges,
}));
} else {
// return everything
next = [...this.structures.value.values()].map((st) => ({
entry: st,
haystackEntry: st.prefab.prefab_name,
ranges: [],
}));
}
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
})();
const unique = [...new Set(filtered.map((obj) => obj.name))].map(
(result) => {
return filtered.find((obj) => obj.name === result);
},
);
numSearchResults = computed(() => {
return this.searchResults.value?.length ?? 0;
})
this._searchResults = unique.map(({ name, haystackEntry, ranges }) => ({
entry: this._structures.get(name)!,
haystackEntry,
ranges,
}));
} else {
// return everything
this._searchResults = [...this._structures.values()].map((st) => ({
entry: st,
haystackEntry: st.prefab.prefab_name,
ranges: [],
}));
}
}
private filterTimeout: ReturnType<typeof setTimeout>;
connectedCallback(): void {
super.connectedCallback();
window.VM.get().then((vm) =>
vm.addEventListener(
"vm-template-db-loaded",
this._handleDeviceDBLoad.bind(this),
),
);
}
_handleDeviceDBLoad(e: CustomEvent) {
this.templateDB = e.detail;
}
@state() private page = 0;
perPage: Signal<number> = signal(40);
maxResultsRendered: Signal<number> = signal(20);
renderSearchResults() {
const perPage = 40;
const totalPages = Math.ceil((this._searchResults?.length ?? 0) / perPage);
let pageKeys = Array.from({ length: totalPages }, (_, index) => index);
const extra: {
const totalPages = computed(() => Math.ceil((this.searchResults.value?.length ?? 0) / this.perPage.value));
const pageKeys = computed(() => range(totalPages.value));
const extra = computed((): {
entry: { prefab: { name: string; prefab_name: string } };
haystackEntry: string;
ranges: number[];
}[] = [];
if (this.page < totalPages - 1) {
extra.push({
entry: { prefab: { name: "", prefab_name: this.filter } },
haystackEntry: "...",
ranges: [],
});
}
return when(
typeof this._searchResults !== "undefined" &&
this._searchResults.length < 20,
() =>
repeat(
this._searchResults ?? [],
}[] => {
const next: {
entry: { prefab: { name: string; prefab_name: string } };
haystackEntry: string;
ranges: number[];
}[] = [];
if (this.page.value < totalPages.value - 1) {
next.push({
entry: { prefab: { name: "", prefab_name: this.filter } },
haystackEntry: "...",
ranges: [],
});
}
return next;
});
const pageKeyButtons = computed(() => {
return pageKeys.value.map(
(key, index) => {
const textColorClass = computed(() => index === this.page.value ? "text-purple-500" : nothing)
return html`
<span
class="p-2 cursor-pointer hover:text-purple-400 ${watch(textColorClass)}"
key=${key}
@click=${this._handlePageChange}
>
${key + 1}${index < totalPages.value - 1 ? "," : nothing}
</span>
`
}
)
})
const results = computed(() => [
...this.searchResults.value.slice(
this.perPage.value * this.page.value,
this.perPage.value * this.page.value + this.perPage.value,
),
...extra.value,
].map((result) => {
let hay = result.haystackEntry.slice(0, 15);
if (result.haystackEntry.length > 15) hay += "...";
const ranges = result.ranges.filter((pos) => pos < 20);
const key = result.entry.prefab.prefab_name;
return html`
<div
class="m-2 text-neutral-200/90 italic cursor-pointer rounded bg-neutral-700 hover:bg-purple-500 px-1"
key=${key}
@click=${this._handleHaystackClick}
>
${result.entry.prefab.name} (<small class="text-sm">
${ranges.length
? unsafeHTML(uFuzzy.highlight(hay, ranges))
: hay
} </small>)
</div>
`;
}));
const cards = computed(() => {
if (this.numSearchResults.value <= this.maxResultsRendered.value) {
return repeat(
this.searchResults.value ?? [],
(result) => result.entry.prefab.prefab_name,
(result) =>
cache(html`
<vm-device-template
prefab_name=${result.entry.prefab.prefab_name}
class="card"
@add-device-template=${this._handleDeviceAdd}
>
</vm-device-template>
`),
),
() => html`
html`
<vm-device-template
prefab_name=${result.entry.prefab.prefab_name}
class="card"
@add-device-template=${this._handleDeviceAdd}
>
</vm-device-template>
`,
);
} else {
return nothing;
}
});
const searchResultsHtml = computed(() => {
if (this.numSearchResults.value > 0 && this.numSearchResults.value <= this.maxResultsRendered.value) {
return html`${watch(cards)}`
} else {
const excessResults = this.numSearchResults.value - this.maxResultsRendered.value
const filterText = (() => {
if (this.numSearchResults.value > this.maxResultsRendered.value) {
return html`, filter <span class="font-mono">${excessResults}</span> more to get cards`
}
return nothing
})();
return html`
<div class="p-2">
<div class="flex flex-row">
<p class="p-2">
<sl-format-number
.value=${this._searchResults?.length}
.value=${this.numSearchResults.value}
></sl-format-number>
results, filter more to get cards
results${filterText}
</p>
<div class="p-2 ml-2">
Page:
${pageKeys.map(
(key, index) => html`
<span
class="p-2 cursor-pointer hover:text-purple-400 ${index ===
this.page
? " text-purple-500"
: ""}"
key=${key}
@click=${this._handlePageChange}
>${key + 1}${index < totalPages - 1 ? "," : ""}</span
>
`,
)}
${watch(pageKeyButtons)}
</div>
</div>
<div class="flex flex-row flex-wrap">
${[
...this._searchResults.slice(
perPage * this.page,
perPage * this.page + perPage,
),
...extra,
].map((result) => {
let hay = result.haystackEntry.slice(0, 15);
if (result.haystackEntry.length > 15) hay += "...";
const ranges = result.ranges.filter((pos) => pos < 20);
const key = result.entry.prefab.prefab_name;
return html`
<div
class="m-2 text-neutral-200/90 italic cursor-pointer rounded bg-neutral-700 hover:bg-purple-500 px-1"
key=${key}
@click=${this._handleHaystackClick}
>
${result.entry.prefab.name} (<small class="text-sm">
${ranges.length
? unsafeHTML(uFuzzy.highlight(hay, ranges))
: hay} </small
>)
</div>
`;
})}
${watch(results)}
</div>
</div>
`,
);
`
}
});
return html`${watch(searchResultsHtml)}`;
}
_handlePageChange(e: Event) {
const span = e.currentTarget as HTMLSpanElement;
const key = parseInt(span.getAttribute("key"));
this.page = key;
this.page.value = key;
}
_handleHaystackClick(e: Event) {
const div = e.currentTarget as HTMLDivElement;
const key = div.getAttribute("key");
if (key === this.filter) {
this.page += 1;
this.page.value += 1;
} else {
this.filter = key;
this.searchInput.value = key;
@@ -284,8 +340,8 @@ export class VMAddDeviceButton extends VMTemplateDBMixin(BaseElement) {
slot="footer"
variant="primary"
@click=${() => {
this.drawer.hide();
}}
this.drawer.hide();
}}
>
Close
</sl-button>

View File

@@ -1,10 +1,10 @@
import { html, css, HTMLTemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { watch, SignalWatcher, computed } from '@lit-labs/preact-signals';
import { html, css, HTMLTemplateResult, nothing } from "lit";
import { customElement, property, query } from "lit/decorators.js";
import { watch, computed } from '@lit-labs/preact-signals';
import { BaseElement, defaultCss } from "components";
import { VMTemplateDBMixin, VMObjectMixin, globalObjectSignalMap } from "virtualMachine/baseDevice";
import { VMObjectMixin } from "virtualMachine/baseDevice";
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js";
import { parseIntWithHexOrBinary, parseNumber } from "utils";
import { crc32, isSome, parseIntWithHexOrBinary, range } 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";
@@ -12,13 +12,13 @@ import "./fields";
import "./pins";
import { until } from "lit/directives/until.js";
import { repeat } from "lit/directives/repeat.js";
import { Connection } from "ic10emu_wasm";
import { createRef, ref, Ref } from "lit/directives/ref.js";
export type CardTab = "fields" | "slots" | "reagents" | "networks" | "pins";
@customElement("vm-device-card")
export class VMDeviceCard extends VMTemplateDBMixin(
VMObjectMixin(SignalWatcher(BaseElement)),
) {
export class VMDeviceCard extends VMObjectMixin(BaseElement) {
image_err: boolean;
@property({ type: Boolean }) open: boolean;
@@ -26,9 +26,6 @@ export class VMDeviceCard extends VMTemplateDBMixin(
constructor() {
super();
this.open = false;
this.subscribe(
"active-ic",
);
}
static styles = [
@@ -120,114 +117,117 @@ export class VMDeviceCard extends VMTemplateDBMixin(
`,
];
_handleDeviceDBLoad(e: CustomEvent<any>): void {
super._handleDeviceDBLoad(e);
this.updateObject();
}
onImageErr(e: Event) {
this.image_err = true;
console.log("Image load error", e);
}
thisIsActiveIc = computed(() => {
return this.vm.value?.activeIC.value === this.objectIDSignal.value;
});
activeIcPins = computed(() => {
return this.vm.value?.state.getDevicePins(this.vm.value?.activeIC.value).value ?? [];
});
prefabName = computed(() => {
return this.vm.value?.state.getObject(this.objectIDSignal.value).value?.obj_info.prefab ?? "unknown";
});
objectName = computed(() => {
return this.vm.value?.state.getObject(this.objectIDSignal.value).value?.obj_info.name ?? "";
});
objectNameHash = computed(() => {
return crc32(this.vm.value?.state.getObject(this.objectIDSignal.value).value?.obj_info.name ?? "");
});
renderHeader(): HTMLTemplateResult {
const thisIsActiveIc = computed(() => {
return this.activeICId.value === this.objectID.value;
});
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) {
if (this.thisIsActiveIc.value) {
badges.push(html`<sl-badge variant="primary" pill pulse>db</sl-badge>`);
}
pins.value.forEach((id, index) => {
if (this.objectID.value == id) {
this.activeIcPins.value.forEach(([pin, id]) => {
if (this.objectIDSignal.value == id) {
badges.push(
html`<sl-badge variant="success" pill>d${index}</sl-badge>`,
html`<sl-badge variant="success" pill>d${pin}</sl-badge>`,
);
}
}, this);
return badges
});
const removeText = computed(() => {
return this.thisIsActiveIc.value
? "Removing the selected Active IC is disabled"
: "Remove Device";
});
return html`
<sl-tooltip content="${watch(this.objectSignals.prefabName)}">
<sl-tooltip content="${watch(this.prefabName)}">
<img
class="image me-2"
src="img/stationpedia/${watch(this.objectSignals.prefabName)}.png"
src="img/stationpedia/${watch(this.prefabName)}.png"
onerror="this.src = '${VMDeviceCard.transparentImg}'"
/>
</sl-tooltip>
<div class="header-name">
<sl-input
id="vmDeviceCard${watch(this.objectID)}Id"
id="vmDeviceCard${watch(this.objectIDSignal)}Id"
class="device-id me-1"
size="small"
pill
value=${watch(this.objectID)}
value=${watch(this.objectIDSignal)}
@sl-change=${this._handleChangeID}
>
<span slot="prefix">Id</span>
<sl-copy-button
slot="suffix"
.value=${watch(this.objectID)}
.value=${watch(this.objectIDSignal)}
></sl-copy-button>
</sl-input>
<sl-input
id="vmDeviceCard${watch(this.objectID)}Name"
id="vmDeviceCard${watch(this.objectIDSignal)}Name"
class="device-name me-1"
size="small"
pill
placeholder=${watch(this.objectSignals.prefabName)}
value=${watch(this.objectSignals.name)}
placeholder=${watch(this.prefabName)}
value=${watch(this.objectName)}
@sl-change=${this._handleChangeName}
>
<span slot="prefix">Name</span>
<sl-copy-button
slot="suffix"
from="vmDeviceCard${watch(this.objectID)}Name.value"
from="vmDeviceCard${watch(this.objectIDSignal)}Name.value"
></sl-copy-button>
</sl-input>
<sl-input
id="vmDeviceCard${watch(this.objectID)}NameHash"
id="vmDeviceCard${watch(this.objectIDSignal)}NameHash"
size="small"
pill
class="device-name-hash me-1"
value="${watch(this.objectSignals.nameHash)}"
value="${watch(this.objectNameHash)}"
readonly
>
<span slot="prefix">Hash</span>
<sl-copy-button
slot="suffix"
from="vmDeviceCard${this.objectID}NameHash.value"
from="vmDeviceCard${watch(this.objectIDSignal)}NameHash.value"
></sl-copy-button>
</sl-input>
${watch(badgesHtml)}
</div>
<div class="ms-auto mt-auto mb-auto me-2">
<sl-tooltip
content=${thisIsActiveIc
? "Removing the selected Active IC is disabled"
: "Remove Device"}
content=${watch(removeText)}
>
<sl-icon-button
class="remove-button"
name="trash"
label="Remove Device"
?disabled=${thisIsActiveIc}
?disabled=${watch(this.thisIsActiveIc)}
@click=${this._handleDeviceRemoveButton}
></sl-icon-button>
</sl-tooltip>
@@ -238,7 +238,7 @@ export class VMDeviceCard extends VMTemplateDBMixin(
renderFields() {
return this.delayRenderTab(
"fields",
html`<vm-device-fields .deviceID=${this.objectID}></vm-device-fields>`,
html`<vm-device-fields .objectID=${watch(this.objectIDSignal)}></vm-device-fields>`,
);
}
@@ -249,17 +249,24 @@ export class VMDeviceCard extends VMTemplateDBMixin(
static transparentImg =
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" as const;
objectSlotCount = computed(() => {
return this.vm.value?.state.getObjectSlotCount(this.objectIDSignal.value).value;
});
async renderSlots() {
const slotsHtml = computed(() => {
return repeat(range(this.objectSlotCount.value),
(_slot, index) => html`
<vm-object-slot .objectID=${watch(this.objectIDSignal)} .slotIndex=${index} class-"flex flex-row max-w-lg mr-2 mb-2">
</vm-object-slot>
`,
);
});
return this.delayRenderTab(
"slots",
html`
<div class="flex flex-row flex-wrap">
${repeat(Array(this.objectSignals.slotsCount),
(_slot, index) => html`
<vm-device-slot .deviceID=${this.objectID} .slotIndex=${index} class-"flex flex-row max-w-lg mr-2 mb-2">
</vm-device-slot>
`,
)}
${watch(slotsHtml)}
</div>
`,
);
@@ -269,37 +276,86 @@ export class VMDeviceCard extends VMTemplateDBMixin(
return this.delayRenderTab("reagents", html``);
}
renderNetworks() {
const vmNetworks = window.VM.vm.networkIds;
const networks = this.objectSignals.connections.value.map((connection, index, _conns) => {
const conn =
typeof connection === "object" && "CableNetwork" in connection
? connection.CableNetwork
: null;
networkIds = computed(() => {
return this.vm.value?.state.networkIds.value ?? [];
});
numConnections = computed(() => {
return this.vm.value?.state.getObjectConnectionCount(this.objectIDSignal.value).value;
})
private _connectionsSelectRefMap: Map<number, Ref<SlSelect>> = new Map();
getConnectionSelectRef(index: number): Ref<SlSelect> {
if (!this._connectionsSelectRefMap.has(index)) {
this._connectionsSelectRefMap.set(index, createRef());
}
return this._connectionsSelectRefMap.get(index);
}
forceSelectUpdate(...slSelects: Ref<SlSelect>[]) {
for (const slSelect of slSelects) {
if (slSelect.value != null && "handleValueChange" in slSelect.value) {
slSelect.value.handleValueChange();
}
}
}
renderConnections() {
const connectionsHtml = computed(() => range(this.numConnections.value).map(index => {
const conn = computed(() => {
return this.vm.value?.state.getObjectConnection(this.objectIDSignal.value, index).value;
});
const connNet = computed(() => {
const connection: Connection = conn.value ?? "None";
if (typeof connection === "object" && "CableNetwork" in connection) {
return connection.CableNetwork.net;
}
return null;
});
const selectDisabled = computed(() => !isSome(connNet.value));
const selectOptions = computed(() => {
return this.networkIds.value.map(id => html`
<sl-option value=${id}>
Network ${id}
</sl-option>
`);
});
const connTyp = computed(() => {
const connection: Connection = conn.value ?? "None";
return typeof connection === "object" ? Object.keys(connection)[0] : connection;
});
const connectionSelectRef = this.getConnectionSelectRef(index);
selectOptions.subscribe(() => {this.forceSelectUpdate(connectionSelectRef)})
connNet.subscribe((net) => {
if (isSome(connectionSelectRef.value)) {
connectionSelectRef.value.value = net.toString(0)
connectionSelectRef.value.handleValueChange();
}
})
return html`
<sl-select
hoist
placement="top"
clearable
key=${index}
value=${conn?.net}
?disabled=${conn === null}
value=${watch(connNet)}
?disabled=${watch(selectDisabled)}
@sl-change=${this._handleChangeConnection}
${ref(connectionSelectRef)}
>
<span slot="prefix">Connection:${index} </span>
${vmNetworks.value.map(
(net) =>
html`<sl-option value=${net.toString()}
>Network ${net}</sl-option
>`,
)}
<span slot="prefix"> ${conn?.typ} </span>
${watch(selectOptions)}
<span slot="prefix"> ${watch(connTyp)} </span>
</sl-select>
`;
});
}));
return this.delayRenderTab(
"networks",
html`<div class="networks">${networks}</div>`,
html`<div class="networks">${watch(connectionsHtml)}</div>`,
);
}
@@ -307,7 +363,7 @@ export class VMDeviceCard extends VMTemplateDBMixin(
return this.delayRenderTab(
"pins",
html`<div class="pins">
<vm-device-pins .deviceID=${this.objectID}></vm-device-pins>
<vm-device-pins .objectID=${watch(this.objectIDSignal)}></vm-device-pins>
</div>`,
);
}
@@ -319,12 +375,12 @@ export class VMDeviceCard extends VMTemplateDBMixin(
resolver?: (result: HTMLTemplateResult) => void;
};
} = {
fields: {},
slots: {},
reagents: {},
networks: {},
pins: {},
};
fields: {},
slots: {},
reagents: {},
networks: {},
pins: {},
};
delayRenderTab(
name: CardTab,
@@ -351,9 +407,22 @@ export class VMDeviceCard extends VMTemplateDBMixin(
}
}
numPins = computed(() => {
return this.vm.value?.state.getDeviceNumPins(this.objectIDSignal.value)
});
displayName = computed(() => {
const obj = this.vm.value?.state.getObject(this.objectIDSignal.value).value;
return obj?.obj_info.name ?? obj?.obj_info.prefab ?? null;
});
imageName = computed(() => {
const obj = this.vm.value?.state.getObject(this.objectIDSignal.value).value;
return obj?.obj_info.prefab ?? "error";
});
render(): HTMLTemplateResult {
const disablePins = computed(() => {return !this.objectSignals.numPins.value;});
const displayName = computed(() => { return this.objectSignals.name.value ?? this.objectSignals.prefabName.value})
const disablePins = computed(() => { return !this.numPins.value; });
return html`
<ic10-details class="device-card" ?open=${this.open}>
<div class="header" slot="summary">${this.renderHeader()}</div>
@@ -376,7 +445,7 @@ export class VMDeviceCard extends VMTemplateDBMixin(
${until(this.renderReagents(), html`<sl-spinner></sl-spinner>`)}
</sl-tab-panel>
<sl-tab-panel name="networks">
${until(this.renderNetworks(), html`<sl-spinner></sl-spinner>`)}
${until(this.renderConnections(), html`<sl-spinner></sl-spinner>`)}
</sl-tab-panel>
<sl-tab-panel name="pins"
>${until(this.renderPins(), html`<sl-spinner></sl-spinner>`)}
@@ -391,12 +460,12 @@ export class VMDeviceCard extends VMTemplateDBMixin(
<div class="remove-dialog-body">
<img
class="dialog-image mt-auto mb-auto me-2"
src="img/stationpedia/${watch(this.objectSignals.prefabName)}.png"
src="img/stationpedia/${watch(this.imageName)}.png"
onerror="this.src = '${VMDeviceCard.transparentImg}'"
/>
<div class="flex-g">
<p><strong>Are you sure you want to remove this device?</strong></p>
<span>Id ${this.objectID} : ${watch(displayName)}</span>
<span>Id ${watch(this.objectIDSignal)} : ${watch(this.displayName)}</span>
</div>
</div>
<div slot="footer">
@@ -435,7 +504,7 @@ export class VMDeviceCard extends VMTemplateDBMixin(
const val = parseIntWithHexOrBinary(input.value);
if (!isNaN(val)) {
window.VM.get().then((vm) => {
if (!vm.changeObjectID(this.objectID.peek(), val)) {
if (!vm.changeObjectID(this.objectID, val)) {
input.value = this.objectID.toString();
}
});
@@ -448,10 +517,9 @@ 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.peek(), name)) {
input.value = this.objectSignals.name.value;
if (!vm.setObjectName(this.objectID, name)) {
input.value = this.objectName.peek();
}
this.updateObject();
});
}
_handleDeviceRemoveButton(_e: Event) {
@@ -460,7 +528,7 @@ export class VMDeviceCard extends VMTemplateDBMixin(
_removeDialogRemove() {
this.removeDialog.hide();
window.VM.get().then((vm) => vm.removeDevice(this.objectID.peek()));
window.VM.get().then((vm) => vm.removeDevice(this.objectID));
}
_handleChangeConnection(e: CustomEvent) {
@@ -468,8 +536,7 @@ 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.peek(), conn, val),
vm.setDeviceConnection(this.objectID, conn, val),
);
this.updateObject();
}
}

View File

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

View File

@@ -3,23 +3,19 @@ import { customElement, 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 { isSome, structuralEqual } from "utils";
import { repeat } from "lit/directives/repeat.js";
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 { computed, ReadonlySignal, Signal, signal, SignalWatcher, watch } from "@lit-labs/preact-signals";
import { ObjectID } from "ic10emu_wasm";
import { VMObjectMixin } from "virtualMachine/baseDevice";
@customElement("vm-device-list")
export class VMDeviceList extends SignalWatcher(BaseElement) {
devices: Signal<ObjectID[]>;
private _filter: Signal<string> = signal("");
private _filteredDeviceIds: Signal<number[] | undefined>;
export class VMDeviceList extends VMObjectMixin(BaseElement) {
static styles = [
...defaultCss,
css`
@@ -48,34 +44,42 @@ export class VMDeviceList extends SignalWatcher(BaseElement) {
constructor() {
super();
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)
}
devices: ReadonlySignal<ObjectID[]> = (() => {
let last: ObjectID[] = null;
return computed(() => {
const vm = this.vm.value;
const next: ObjectID[] = vm?.state.vm.value?.objects.flatMap((obj): ObjectID[] => {
if (!isSome(obj.obj_info.parent_slot) && !isSome(obj.obj_info.root_parent_human)) {
return [obj.obj_info.id]
}
return [];
})
if (structuralEqual(last, next)) {
return last;
}
deviceIds.sort();
return deviceIds;
last = next;
return next;
});
this._filteredDeviceIds = computed(() => {
})();
private _filter: Signal<string> = signal("");
private _filteredDeviceIds: ReadonlySignal<ObjectID[]> = (() => {
let last: ObjectID[] = null;
return computed(() => {
const vm = this.vm.value;
let next = this.devices.value;
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 name = vm?.state.getObjectName(device_id).value;
const prefab = vm?.state.getObjectPrefabName(device_id).value;
if (name != null) {
datapoints.push([name, device_id]);
}
if (prefab != null) {
datapoints.push([prefab, device_id]);
}
}
const haystack: string[] = datapoints.map((data) => data[0]);
@@ -87,12 +91,15 @@ export class VMDeviceList extends SignalWatcher(BaseElement) {
filtered
?.map((data) => data[1])
?.filter((val, index, arr) => arr.indexOf(val) === index) ?? [];
return deviceIds;
} else {
return Array.from(this.devices.value);
next = deviceIds;
}
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
}
})();
protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
this.renderRoot.querySelector(".device-list").addEventListener(
@@ -102,13 +109,13 @@ export class VMDeviceList extends SignalWatcher(BaseElement) {
}
protected render(): HTMLTemplateResult {
const deviceCards = repeat(
const deviceCards = computed(() => repeat(
this.filteredDeviceIds.value,
(id) => id,
(id) =>
html`<vm-device-card .deviceID=${id} class="device-list-card">
html`<vm-device-card .objectID=${id} class="device-list-card">
</vm-device-card>`,
);
));
const numDevices = computed(() => this.devices.value.length);
const result = html`
<div class="header">
@@ -126,7 +133,7 @@ export class VMDeviceList extends SignalWatcher(BaseElement) {
</sl-input>
<vm-add-device-button class="ms-auto"></vm-add-device-button>
</div>
<div class="device-list">${deviceCards}</div>
<div class="device-list">${watch(deviceCards)}</div>
<vm-slot-add-dialog></vm-slot-add-dialog>
`;
@@ -138,7 +145,7 @@ export class VMDeviceList extends SignalWatcher(BaseElement) {
_showDeviceSlotDialog(
e: CustomEvent<SlotModifyEvent>,
) {
this.slotDialog.show(e.detail.deviceID, e.detail.slotIndex);
this.slotDialog.show(e.detail.objectID, e.detail.slotIndex);
}
get filteredDeviceIds() {

View File

@@ -1,41 +1,36 @@
import { html, css } from "lit";
import { customElement, property } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMTemplateDBMixin, VMObjectMixin } from "virtualMachine/baseDevice";
import { VMObjectMixin } from "virtualMachine/baseDevice";
import { displayNumber, parseNumber } from "utils";
import type { LogicType } from "ic10emu_wasm";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js";
import { computed, Signal, watch } from "@lit-labs/preact-signals";
@customElement("vm-device-fields")
export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(BaseElement)) {
export class VMDeviceSlot extends VMObjectMixin(BaseElement) {
constructor() {
super();
this.setupSignals();
}
setupSignals() {
this.logicFieldNames = computed(() => {
return Array.from(this.objectSignals.logicFields.value.keys());
});
}
logicFieldNames: Signal<LogicType[]>;
logicFieldNames = computed(() => {
return this.vm.value?.state.getObjectFieldNames(this.objectIDSignal.value).value;
});
render() {
const inputIdBase = `vmDeviceCard${this.objectID}Field`;
const fieldsHtml = computed(() => {
return this.logicFieldNames.value.map((name) => {
const field = computed(() => {
return this.objectSignals.logicFields.value.get(name);
return this.vm.value?.state.getObjectField(this.objectIDSignal.value, name).value ?? null;
});
const typ = computed(() => {
return field.value.field_type;
return field.value?.field_type ?? null;
});
const value = computed(() => {
return displayNumber(field.value.value);
return displayNumber(field.value?.value ?? null);
});
return html` <sl-input id="${inputIdBase}${name}" key="${name}" value="${watch(value)}" size="small"
return html` <sl-input id="${inputIdBase}${name}" key="${name}" value="${watch(value)}" size="small"
@sl-change=${this._handleChangeField}>
<span slot="prefix">${name}</span>
<sl-copy-button slot="suffix" from="${inputIdBase}${name}.value"></sl-copy-button>
@@ -53,10 +48,9 @@ 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.peek(), field, val, true)) {
input.value = this.objectSignals.logicFields.value.get(field).value.toString();
if (!vm.setObjectField(this.objectID, field, val, true)) {
input.value = displayNumber(this.vm.value?.state.getObjectField(this.objectIDSignal.value, field).value?.value ?? null);
}
this.updateObject();
});
}
}

View File

@@ -1,61 +1,86 @@
import { html, css } from "lit";
import { customElement, property } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMTemplateDBMixin, VMObjectMixin } from "virtualMachine/baseDevice";
import { html } from "lit";
import { customElement } from "lit/decorators.js";
import { BaseElement } from "components";
import { VMObjectMixin } from "virtualMachine/baseDevice";
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js";
import { ObjectID } from "ic10emu_wasm";
import { effect, watch } from "@lit-labs/preact-signals";
import { SlOption } from "@shoelace-style/shoelace";
import { ObjectID, ObjectTemplate } from "ic10emu_wasm";
import { computed, watch } from "@lit-labs/preact-signals";
import { createRef, ref, Ref } from "lit/directives/ref.js";
import { isSome, range } from "utils";
@customElement("vm-device-pins")
export class VMDevicePins extends VMObjectMixin(VMTemplateDBMixin(BaseElement)) {
constructor() {
super();
// this.subscribe("visible-devices");
export class VMDevicePins extends VMObjectMixin(BaseElement) {
forceSelectUpdate(...slSelects: Ref<SlSelect>[]) {
for (const slSelect of slSelects) {
if (slSelect.value != null && "handleValueChange" in slSelect.value) {
slSelect.value.handleValueChange();
}
}
}
render() {
const pins = new Array(this.objectSignals.numPins.value ?? 0)
.fill(true)
.map((_, index) => this.objectSignals.pins.value.get(index));
const visibleDevices = (this.objectSignals.visibleDevices.value ?? []);
const forceSelectUpdate = () => {
const slSelect = this.renderRoot.querySelector("sl-select") as SlSelect;
if (slSelect != null) {
slSelect.handleValueChange();
}
};
const pinsHtml = pins?.map(
(pin, index) => {
return 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) => {
device.id.subscribe((id: ObjectID) => {
forceSelectUpdate();
});
device.displayName.subscribe((_: string) => {
forceSelectUpdate();
});
return html`
<sl-option value=${watch(device.id)}>
Device ${watch(device.id)} :
${watch(device.displayName)}
</sl-option>
`
}
private _pinSelectRefMap: Map<number, Ref<SlSelect>> = new Map();
)}
</sl-select>`;
}
);
getPinSelectRef(index: number): Ref<SlSelect> {
if (!this._pinSelectRefMap.has(index)) {
this._pinSelectRefMap.set(index, createRef());
}
return this._pinSelectRefMap.get(index);
}
visibleDeviceIds = computed(() => {
const vm = this.vm.value;
const obj = vm?.state.getObject(this.objectIDSignal.value).value
return obj?.obj_info.visible_devices ?? [];
});
numPins = computed(() => {
const vm = this.vm.value;
return vm?.state.getDeviceNumPins(this.objectIDSignal.value).value;
})
deviceOptions = computed(() => {
return this.visibleDeviceIds.value.map(id => {
const deviceDisplayName = this.vm.value?.state.getObjectDisplayName(id);
deviceDisplayName.subscribe(() => {
this.forceSelectUpdate(...this._pinSelectRefMap.values());
});
return html`
<sl-option value=${id}}>
Device ${id} :
${watch(deviceDisplayName)}
</sl-option>
`
});
});
render() {
const pinsHtml = computed(() => {
return range(this.numPins.value).map(
index => {
const selectRef = this.getPinSelectRef(index);
const pin = computed(() => {
const vm = this.vm.value;
return vm?.state.getDevicePin(this.objectIDSignal.value, index).value;
});
return html`
<sl-select
hoist
placement="top"
clearable
key=${index}
value=${watch(pin)}
@sl-change=${this._handleChangePin}
${ref(selectRef)}
>
<span slot="prefix">d${index}</span>
${watch(this.deviceOptions)}
</sl-select>
`;
}
);
});
return pinsHtml;
}
@@ -63,7 +88,6 @@ 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.peek(), pin, val));
this.updateObject();
window.VM.get().then((vm) => vm.setDevicePin(this.objectID, pin, val));
}
}

View File

@@ -1,11 +1,12 @@
import { html, css } from "lit";
import { customElement, property } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMTemplateDBMixin, VMObjectMixin, VmObjectSlotInfo, ComputedObjectSignals } from "virtualMachine/baseDevice";
import { VMObjectMixin, } from "virtualMachine/baseDevice";
import {
clamp,
crc32,
displayNumber,
isSome,
parseNumber,
} from "utils";
import {
@@ -21,34 +22,22 @@ import { when } from "lit/directives/when.js";
import { computed, signal, Signal, SignalWatcher, watch } from "@lit-labs/preact-signals";
export interface SlotModifyEvent {
deviceID: number;
objectID: number;
slotIndex: number;
}
@customElement("vm-device-slot")
export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(BaseElement))) {
private _slotIndex: Signal<number>;
@customElement("vm-object-slot")
export class VMDeviceSlot extends VMObjectMixin(BaseElement) {
slotSignal: Signal<VmObjectSlotInfo>;
get slotIndex() {
return this._slotIndex.value;
}
slotIndexSignal: Signal<number> = signal(0);
@property({ type: Number })
set slotIndex(val: number) {
this._slotIndex.value = val;
get slotIndex() {
return this.slotIndexSignal.peek();
}
constructor() {
super();
this._slotIndex = signal(0);
this.subscribe("active-ic");
this.slotSignal = computed(() => {
const index = this._slotIndex.value;
return this.objectSignals.slots.value[index];
});
this.setupSignals();
set slotIndex(val: number) {
this.slotIndexSignal.value = val;
}
static styles = [
@@ -79,93 +68,86 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(
`,
];
setupSignals() {
this.slotOccupant = computed(() => {
const slot = this.slotSignal.value ?? null;
return slot?.occupant ?? null;
});
this.slotFieldTypes = computed(() => {
return Array.from(this.slotSignal.value?.logicFields.keys() ?? []) ;
});
this.slotOccupantImg = computed(() => {
const slot = this.slotSignal.value ?? null;
if (slot != null && slot.occupant != null) {
const prefabName = slot.occupant.prefabName;
return `img/stationpedia/${watch(prefabName)}.png`;
} else {
return `img/stationpedia/SlotIcon_${slot.typ}.png`;
}
});
this.slotOccupantPrefabName = computed(() => {
const slot = this.slotSignal.value ?? null;
if (slot != null && slot.occupant != null) {
const prefabName = slot.occupant.prefabName.value;
return prefabName;
} else {
return null;
}
});
this.slotOccupantTemplate = computed(() => {
if (this.objectSignals != null && "slots" in this.objectSignals.template.value) {
return this.objectSignals.template.value.slots[this.slotIndex];
} else {
return null;
}
});
}
slotOccupant: Signal<ComputedObjectSignals | null>;
slotFieldTypes: Signal<LogicSlotType[]>;
slotOccupantImg: Signal<string>;
slotOccupantPrefabName: Signal<string | null>;
slotOccupantTemplate: Signal<SlotInfo | null>;
slotInfo = computed(() => {
return this.vm.value?.state.getObjectSlotInfo(this.objectIDSignal.value, this.slotIndexSignal.value).value ?? null;
});
slotOccupantId = computed(() => {
const slot = this.slotInfo.value ?? null;
return slot?.occupant ?? null;
});
slotOccupant = computed(() => {
return this.vm.value?.state.getObject(this.slotOccupantId.value).value;
});
slotFieldTypes = computed(() => {
return this.vm.value?.state.getObjectSlotFieldNames(this.objectIDSignal.value, this.slotIndexSignal.value).value ?? [];
});
slotOccupantImg = computed(() => {
const occupant = this.slotOccupant.value;
if (isSome(occupant)) {
const prefabName = occupant.obj_info.prefab;
return `img/stationpedia/${prefabName}.png`;
} else {
const slot = this.vm.value?.state.getObjectSlotInfo(this.objectIDSignal.value, this.slotIndexSignal.value).value ?? null;
return `img/stationpedia/SlotIcon_${slot?.typ}.png`;
}
});
slotOccupantPrefabName = computed(() => {
const occupant = this.slotOccupant.value;
return occupant?.obj_info.prefab ?? null;
});
slotQuantity = computed(() => {
const slot = this.slotInfo.value ?? null;
return slot?.quantity;
});
slotTyp = computed(() => {
const slot = this.slotInfo.value ?? null;
return slot?.typ;
});
slotName = computed(() => {
const slot = this.slotInfo.value ?? null;
return slot?.name ?? slot?.typ;
});
slotDisplayName = computed(() => {
return this.slotOccupantPrefabName.value ?? this.slotName ?? "";
});
maxQuantity = computed(() => {
const occupant = this.slotOccupant.value;
const template = occupant?.template ?? null;
if (isSome(template) && "item" in template) {
return template.item.max_quantity;
}
return 1;
});
renderHeader() {
const inputIdBase = `vmDeviceSlot${this.objectID}Slot${this.slotIndex}Head`;
// const slot = this.slotSignal.value;
const slotImg = this.slotOccupantImg;
// const inputIdBase = computed(() => `vmDeviceSlot${this.objectIDSignal.value}Slot${this.slotIndexSignal.value}Head`);
const img = html`<img
class="w-10 h-10"
src="${watch(slotImg)}"
src="${watch(this.slotOccupantImg)}"
onerror="this.src = '${VMDeviceCard.transparentImg}'"
/>`;
const template = this.slotOccupantTemplate;
const templateName = computed(() => {
return template.value?.name ?? null;
});
const slotTyp = computed(() => {
return this.slotSignal.value.typ;
})
const enableQuantityInput = false;
const quantity = computed(() => {
const slot = this.slotSignal.value;
return slot.quantity;
});
const maxQuantity = computed(() => {
const slotOccupant = this.slotSignal.value.occupant;
const template = slotOccupant?.template.value ?? null;
if (template != null && "item" in template) {
return template.item.max_quantity;
} else {
return 1;
}
});
const slotDisplayName = computed(() => {
return this.slotOccupantPrefabName.value ?? this.slotSignal.value.typ;
const removeDisabled = computed(() => {
return this.vm.value?.activeIC.value === this.objectIDSignal.value && this.slotTyp.value === "ProgrammableChip"
});
const tooltipContent = computed(() => {
return this.activeICId === this.objectID && slotTyp.value === "ProgrammableChip"
return removeDisabled.value
? "Removing the selected Active IC is disabled"
: "Remove Occupant"
})
const removeDisabled = computed(() => {
return this.activeICId === this.objectID && slotTyp.value === "ProgrammableChip"
});
const quantityContent = computed(() => {
@@ -176,7 +158,7 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(
text-neutral-200/90 font-mono bg-neutral-500/40 rounded pl-1 pr-1"
>
<small>
${watch(quantity)}/${watch(maxQuantity)}
${watch(this.slotQuantity)}/${watch(this.maxQuantity)}
</small>
</div>`
} else {
@@ -185,34 +167,30 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(
});
const slotName = computed(() => {
if(this.slotOccupant.value != null) {
return html` <span> ${watch(this.slotOccupantPrefabName)} </span> `
} else {
html` <span> ${watch(templateName)} </span> `
}
return html` <span> ${watch(this.slotDisplayName)} </span> `
});
const inputContent = computed(() => {
if (this.slotOccupant.value != null) {
if (isSome(this.slotOccupant.value)) {
return html`
<div class="quantity-input ms-auto pl-2 mt-auto mb-auto me-2">
${enableQuantityInput
? html`<sl-input
? html`<sl-input
type="number"
size="small"
.value=${watch(quantity)}
.value=${watch(this.slotQuantity)}
.min=${1}
.max=${watch(maxQuantity)}
.max=${watch(this.maxQuantity)}
@sl-change=${this._handleSlotQuantityChange}
>
<div slot="help-text">
<span>
Max Quantity:
${watch(maxQuantity)}
${watch(this.maxQuantity)}
</span>
</div>
</sl-input>`
: ""}
: ""}
<sl-tooltip
content=${watch(tooltipContent)}
>
@@ -245,7 +223,7 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(
>
<small>${this.slotIndex}</small>
</div>
<sl-tooltip content="${watch(slotDisplayName)}">
<sl-tooltip content="${watch(this.slotDisplayName)}">
${img}
</sl-tooltip>
${watch(quantityContent)}
@@ -258,7 +236,7 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(
<div class="text-neutral-400 text-xs mt-auto flex flex-col mb-1">
<div>
<strong class="mt-auto mb-auto">Type:</strong
><span class="p-1">${watch(slotTyp)}</span>
><span class="p-1">${watch(this.slotTyp)}</span>
</div>
</div>
</div>
@@ -268,7 +246,7 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(
}
_handleSlotOccupantRemove() {
window.VM.vm.removeSlotOccupant(this.objectID.peek(), this.slotIndex);
window.VM.vm.removeSlotOccupant(this.objectID, this.slotIndex);
}
_handleSlotClick(_e: Event) {
@@ -276,64 +254,57 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(
new CustomEvent<SlotModifyEvent>("device-modify-slot", {
bubbles: true,
composed: true,
detail: { deviceID: this.objectID.peek(), slotIndex: this.slotIndex },
detail: { objectID: this.objectID, slotIndex: this.slotIndex },
}),
);
}
_handleSlotQuantityChange(e: Event) {
const input = e.currentTarget as SlInput;
const slot = this.slotSignal.value;
const val = clamp(
input.valueAsNumber,
1,
"item" in slot.occupant.template.value
? slot.occupant.template.value.item.max_quantity
: 1,
this.maxQuantity.peek()
);
if (
!window.VM.vm.setObjectSlotField(
this.objectID.peek(),
this.objectID,
this.slotIndex,
"Quantity",
val,
true,
)
) {
input.value = this.slotSignal.value.quantity.toString();
input.value = this.slotQuantity.value.toString();
}
}
renderFields() {
const inputIdBase = `vmDeviceSlot${this.objectID}Slot${this.slotIndex}Field`;
const inputIdBase = computed(() => `vmDeviceSlot${this.objectIDSignal.value}Slot${this.slotIndexSignal.value}Field`);
const fields = computed(() => {
const slot = this.slotSignal.value;
const _fields =
slot.logicFields??
new Map<LogicSlotType, LogicField>();
return this.slotFieldTypes.value.map(
(name, _index, _types) => {
field => {
const slotField = computed(() => {
return this.slotSignal.value.logicFields.get(name);
return this.vm.value?.state.getObjectSlotField(this.objectIDSignal.value, this.slotIndexSignal.value, field).value ?? null;
});
const fieldValue = computed(() => {
return displayNumber(slotField.value.value);
return displayNumber(slotField.value?.value ?? null);
})
const fieldAccessType = computed(() => {
return slotField.value.field_type;
return slotField.value?.field_type ?? null;
})
return html`
<sl-input
id="${inputIdBase}${name}"
key="${name}"
id="${watch(inputIdBase)}${field}"
key="${field}"
value="${watch(fieldValue)}"
size="small"
@sl-change=${this._handleChangeSlotField}
>
<span slot="prefix">${name}</span>
<span slot="prefix">${field}</span>
<sl-copy-button
slot="suffix"
from="${inputIdBase}${name}.value"
from="${watch(inputIdBase)}${field}.value"
></sl-copy-button>
<span slot="suffix">${watch(fieldAccessType)}</span>
</sl-input>
@@ -354,27 +325,19 @@ export class VMDeviceSlot extends VMObjectMixin(VMTemplateDBMixin(SignalWatcher(
const field = input.getAttribute("key")! as LogicSlotType;
let val = parseNumber(input.value);
if (field === "Quantity") {
const slot = this.slotSignal.value;
const slot = this.slotIndexSignal.value;
val = clamp(
input.valueAsNumber,
1,
"item" in slot.occupant.template.value
? slot.occupant.template.value.item.max_quantity
: 1,
this.maxQuantity.peek(),
);
}
window.VM.get().then((vm) => {
if (
!vm.setObjectSlotField(this.objectID.peek(), this.slotIndex, field, val, true)
!vm.setObjectSlotField(this.objectID, this.slotIndex, field, val, true)
) {
input.value = (
this.slotSignal.value.logicFields ??
new Map<LogicSlotType, LogicField>()
)
.get(field)
.toString();
input.value = (vm.state.getObjectSlotField(this.objectIDSignal.value, this.slotIndexSignal.value, field).value.value ?? null).toString();
}
this.updateObject();
});
}

View File

@@ -1,27 +1,25 @@
import { html, css, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { customElement, query } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { ComputedObjectSignals, globalObjectSignalMap, VMTemplateDBMixin } from "virtualMachine/baseDevice";
import { VMObjectMixin } 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";
import { when } from "lit/directives/when.js";
import uFuzzy from "@leeoniya/ufuzzy";
import {
FrozenObject,
ItemInfo,
LogicField,
LogicSlotType,
ObjectInfo,
ObjectTemplate,
} from "ic10emu_wasm";
import { computed, ReadonlySignal, signal, Signal, watch } from "@lit-labs/preact-signals";
import { repeat } from "lit/directives/repeat.js";
import { isSome, structuralEqual } from "utils";
type SlotableItemTemplate = Extract<ObjectTemplate, { item: ItemInfo }>;
@customElement("vm-slot-add-dialog")
export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
export class VMSlotAddDialog extends VMObjectMixin(BaseElement) {
static styles = [
...defaultCss,
css`
@@ -40,11 +38,6 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
`,
];
private _items: Signal<Record<string, SlotableItemTemplate>> = signal({});
private _filteredItems: ReadonlySignal<SlotableItemTemplate[]>;
private _datapoints: ReadonlySignal<[string, string][]>;
private _haystack: ReadonlySignal<string[]>;
private _filter: Signal<string> = signal("");
get filter() {
return this._filter.peek();
@@ -54,75 +47,104 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
this._filter.value = val;
}
private _searchResults: ReadonlySignal<{
entry: SlotableItemTemplate;
haystackEntry: string;
ranges: number[];
}[]>;
templateDB = computed(() => {
return this.vm.value?.state.templateDB.value ?? null;
});
constructor() {
super();
this.setupSearch();
}
items = (() => {
let last: { [k: string]: SlotableItemTemplate } = null;
return computed(() => {
const next = Object.fromEntries(
Array.from(Object.values(this.templateDB.value ?? {})).flatMap((template) => {
if ("item" in template) {
return [[template.prefab.prefab_name, template]] as [
string,
SlotableItemTemplate,
][];
} else {
return [] as [string, SlotableItemTemplate][];
}
}),
);
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
})();
postDBSetUpdate(): void {
this._items.value = Object.fromEntries(
Array.from(Object.values(this.templateDB)).flatMap((template) => {
if ("item" in template) {
return [[template.prefab.prefab_name, template]] as [
string,
SlotableItemTemplate,
][];
} else {
return [] as [string, SlotableItemTemplate][];
}
}),
);
}
setupSearch() {
const filteredItems = computed(() => {
let filtered = Array.from(Object.values(this._items.value));
const obj = globalObjectSignalMap.get(this.objectID.value ?? null);
if (obj != null) {
filteredItems = (() => {
let last: SlotableItemTemplate[] = null;
return computed(() => {
let filtered = Array.from(Object.values(this.items.value));
const obj = this.vm.value?.state.getObject(this.objectIDSignal.value).value;
if (isSome(obj)) {
const template = obj.template;
const slot = "slots" in template.value ? template.value.slots[this.slotIndex.value] : null;
const slot = "slots" in template ? template.slots[this.slotIndex.value] : null;
const typ = slot.typ;
if (typeof typ === "string" && typ !== "None") {
filtered = Array.from(Object.values(this._items.value)).filter(
filtered = Array.from(Object.values(this.items.value)).filter(
(item) => item.item.slot_class === typ,
);
}
}
if (structuralEqual(last, filtered)) {
return last;
}
last = filtered;
return filtered;
});
this._filteredItems = filteredItems;
})();
const datapoints = computed(() => {
datapoints = (() => {
let last: [string, string][] = null;
return computed(() => {
const datapoints: [string, string][] = [];
for (const entry of this._filteredItems.value) {
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],
);
}
if (structuralEqual(last, datapoints)) {
return last;
}
last = datapoints;
return datapoints;
});
this._datapoints = datapoints;
})();
const haystack: Signal<string[]> = computed(() => {
return datapoints.value.map((data) => data[0]);
haystack = (() => {
let last: string[] = null;
return computed(() => {
const hay = this.datapoints.value.map(data => data[0])
if (structuralEqual(last, hay)) {
return last;
}
last = hay;
return hay
});
this._haystack = haystack;
})();
const searchResults = computed(() => {
searchResults: ReadonlySignal<{
entry: SlotableItemTemplate
haystackEntry: string,
ranges: number[]
}[]> = (() => {
let last: {
entry: SlotableItemTemplate
haystackEntry: string,
ranges: number[]
}[] = null;
return computed(() => {
let results;
if (this._filter.value) {
const uf = new uFuzzy({});
const [_idxs, info, order] = uf.search(
this._haystack.value,
this.haystack.value,
this._filter.value,
0,
1e3,
@@ -130,8 +152,8 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
const filtered =
order?.map((infoIdx) => ({
name: this._datapoints.value[info.idx[infoIdx]][1],
haystackEntry: this._haystack.value[info.idx[infoIdx]],
name: this.datapoints.value[info.idx[infoIdx]][1],
haystackEntry: this.haystack.value[info.idx[infoIdx]],
ranges: info.ranges[infoIdx],
})) ?? [];
@@ -141,22 +163,25 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
});
results = unique.map(({ name, haystackEntry, ranges }) => ({
entry: this._items.value[name]!,
entry: this.items.value[name]!,
haystackEntry,
ranges,
}));
} else {
// return everything
results = [...this._filteredItems.value].map((st) => ({
results = [...this.filteredItems.value].map((st) => ({
entry: st,
haystackEntry: st.prefab.prefab_name,
ranges: [],
}));
}
if (structuralEqual(last, results)) {
return last;
}
last = results
return results;
});
this._searchResults = searchResults;
}
})();
renderSearchResults() {
const enableNone = false;
@@ -170,7 +195,7 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
`;
const resultsHtml = computed(() => {
return repeat(
this._searchResults.value,
this.searchResults.value,
(result) => {
return result.entry.prefab.prefab_hash;
},
@@ -205,15 +230,15 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
}
_handleClickNone() {
window.VM.vm.removeSlotOccupant(this.objectID.peek(), this.slotIndex.peek());
window.VM.vm.removeSlotOccupant(this.objectID, this.slotIndex.peek());
this.hide();
}
_handleClickItem(e: Event) {
const div = e.currentTarget as HTMLDivElement;
const key = parseInt(div.getAttribute("key"));
const entry = this.templateDB.get(key) as SlotableItemTemplate;
const obj = window.VM.vm.objects.get(this.objectID.peek());
const entry = this.templateDB.value.get(key) as SlotableItemTemplate;
const obj = window.VM.vm.state.getObject(this.objectID);
const dbTemplate = obj.peek().template;
console.log("using entry", dbTemplate);
@@ -224,7 +249,7 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
database_template: true,
template: undefined,
};
window.VM.vm.setSlotOccupant(this.objectID.peek(), this.slotIndex.peek(), template, 1);
window.VM.vm.setSlotOccupant(this.objectID, this.slotIndex.peek(), template, 1);
this.hide();
}
@@ -232,14 +257,10 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
@query(".device-search-input") searchInput: SlInput;
render() {
const device = computed(() => {
return globalObjectSignalMap.get(this.objectID.value) ?? null;
const name = computed(() => {
return this.vm.value?.state.getObjectDisplayName(this.objectIDSignal.value) ?? "";
});
const name = computed(() => {
return device.value?.displayName.value ?? nothing;
});
const id = computed(() => this.objectID.value ?? 0);
const id = computed(() => this.objectIDSignal.value ?? 0);
const resultsHtml = html`
<div class="flex flex-row overflow-x-auto">
${this.renderSearchResults()}
@@ -284,11 +305,10 @@ export class VMSlotAddDialog extends VMTemplateDBMixin(BaseElement) {
this.slotIndex = undefined;
}
private objectID: Signal<number> = signal(null);
private slotIndex: Signal<number> = signal(0);
show(objectID: number, slotIndex: number) {
this.objectID.value = objectID;
this.objectIDSignal.value = objectID;
this.slotIndex.value = slotIndex;
this.dialog.show();
this.searchInput.select();

View File

@@ -20,12 +20,12 @@ import { customElement, property, query, state } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { connectionFromConnectionInfo } from "./dbutils";
import { crc32, displayNumber, parseNumber } from "utils";
import { crc32, displayNumber, parseNumber, structuralEqual } 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 { globalObjectSignalMap, VMTemplateDBMixin } from "virtualMachine/baseDevice";
import { computed, Signal, watch } from "@lit-labs/preact-signals";
import { VMObjectMixin } from "virtualMachine/baseDevice";
import { computed, effect, signal, Signal, watch } from "@lit-labs/preact-signals";
import { createRef, ref, Ref } from "lit/directives/ref.js";
export interface SlotTemplate {
@@ -43,7 +43,7 @@ export interface ConnectionCableNetwork {
}
@customElement("vm-device-template")
export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
export class VmObjectTemplate extends VMObjectMixin(BaseElement) {
static styles = [
...defaultCss,
css`
@@ -84,34 +84,40 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
objectName: Signal<string | undefined>;
connections: Signal<Connection[]>;
constructor() {
super();
this.templateDB = window.VM.vm.templateDB;
}
private _prefabName: string;
private _prefabHash: number;
private prefabNameSignal = signal(null);
private prefabHashSignal = computed(() => crc32(this.prefabNameSignal.value));
get prefabName(): string {
return this._prefabName;
return this.prefabNameSignal.peek();
}
get prefabHash(): number {
return this._prefabHash;
return this.prefabHashSignal.peek();
}
@property({ type: String })
set prefabName(val: string) {
this._prefabName = val;
this._prefabHash = crc32(this._prefabName);
this.setupState();
this.prefabNameSignal.value = val;
}
get dbTemplate(): ObjectTemplate {
return this.templateDB.get(this._prefabHash);
dbTemplate = (() => {
let last: ObjectTemplate = null;
return computed(() => {
const next = this.vm.value?.state.templateDB.value.get(this.prefabHashSignal.value) ?? null;
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
})();
constructor() {
super();
this.dbTemplate.subscribe(() => this.setupState())
}
setupState() {
const dbTemplate = this.dbTemplate;
const dbTemplate = this.dbTemplate.value;
this.fields.value = Object.fromEntries(
(
@@ -122,7 +128,7 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
) as [LogicType, MemoryAccess][]
).map(([lt, access]) => {
const value =
lt === "PrefabHash" ? this.dbTemplate.prefab.prefab_hash : 0.0;
lt === "PrefabHash" ? dbTemplate.prefab.prefab_hash : 0.0;
return [lt, value];
}),
) as Record<LogicType, number>;
@@ -187,7 +193,7 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
const val = parseNumber(input.value);
this.fields.value = { ...this.fields.value, [field]: val};
if (field === "ReferenceId" && val !== 0) {
this.objectId.value = val;
this.objectIDSignal.value = val;
}
}
@@ -213,11 +219,16 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
return html``;
}
networkOptions = computed(() => {
const vm = this.vm.value;
return vm?.state.networkIds.value.map(net => html`<sl-option value=${net}>Network ${net}</sl-option>`);
});
renderNetworks() {
const vm = window.VM.vm;
const vmNetworks = computed(() => {
return vm.networkIds.value.map((net) => html`<sl-option value=${net}>Network ${net}</sl-option>`);
});
this.networkOptions.subscribe((_) => {
this.forceSelectUpdate(this.networksSelectRef);
})
const connections = computed(() => {
this.connections.value.map((connection, index, _conns) => {
const conn =
@@ -236,13 +247,12 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
${ref(this.networksSelectRef)}
>
<span slot="prefix">Connection:${index} </span>
${watch(vmNetworks)}
${watch(this.networkOptions)}
<span slot="prefix"> ${conn?.typ} </span>
</sl-select>
`;
});
});
vmNetworks.subscribe((_) => { this.forceSelectUpdate(this.networksSelectRef)})
return html`
<div class="networks">
${watch(connections)}
@@ -272,42 +282,50 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
this.forceSelectUpdate(...this._pinsSelectRefMap.values());
}
renderPins(): HTMLTemplateResult {
const networks = computed(() => {
return this.connections.value.flatMap((connection, index) => {
return typeof connection === "object" && "CableNetwork" in connection
? [connection.CableNetwork.net]
: [];
});
networks = computed(() => {
return this.connections.value.flatMap(connection => {
return typeof connection === "object" && "CableNetwork" in connection
? [connection.CableNetwork.net]
: [];
});
const visibleDeviceIds = computed(() => {
return [
...new Set(
networks.value.flatMap((net) => window.VM.vm.networkDataDevicesSignal(net).value),
),
];
});
visibleDeviceIds = (() => {
let last: ObjectID[] = null;
return computed(() => {
const vm = this.vm.value;
const next = [
...new Set(
this.networks.value.flatMap((net) => vm?.state.getNetwork(net).value.devices ?? []),
),
];
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
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(); });
})();
visibleDeviceOptions = computed(() => {
return this.visibleDeviceIds.value.map(
id => {
const displayName = computed(() => {
this.vm.value?.state.getObjectDisplayName(id).value;
});
displayName.subscribe((_) => { this.forcePinSelectUpdate(); });
return html`
<sl-option value=${id}>
Device ${id} :
${watch(displayName)}
</sl-option>
`
}
)
});
renderPins(): HTMLTemplateResult {
this.visibleDeviceOptions.subscribe((_) => { this.forcePinSelectUpdate(); });
const pinsHtml = computed(() => {
this.pins.value.map(
(pin, index) => {
@@ -322,7 +340,7 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
${ref(pinRef)}
>
<span slot="prefix">d${index}</span>
${watch(visibleDevicesHtml)}
${watch(this.visibleDeviceOptions)}
</sl-select>`
}
);
@@ -341,20 +359,22 @@ export class VmObjectTemplate extends VMTemplateDBMixin(BaseElement) {
render() {
const device = this.dbTemplate;
const prefabName = computed(() => device.value.prefab.prefab_name);
const name = computed(() => device.value.prefab.name);
return html`
<sl-card class="template-card">
<div class="header h-20 w-96" slot="header">
<sl-tooltip content="${device?.prefab.prefab_name}">
<sl-tooltip content="${watch(prefabName)}">
<img
class="image me-2"
src="img/stationpedia/${device?.prefab.prefab_name}.png"
src="img/stationpedia/${watch(prefabName)}.png"
onerror="this.src = '${VMDeviceCard.transparentImg}'"
/>
</sl-tooltip>
<div class="vstack">
<span>${device.prefab.name}</span>
<span><small>${device?.prefab.prefab_name}</small></span>
<span><small>${device?.prefab.prefab_hash}</small></span>
<span>${watch(name)}</span>
<span><small>${watch(prefabName)}</small></span>
<span><small>${watch(prefabName)}</small></span>
</div>
<sl-button
class="ms-auto mt-auto mb-auto"
@@ -390,7 +410,7 @@ 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.value,
id: this.objectIDSignal.value,
name: this.objectName.value,
prefab: this.prefabName,
} as ObjectInfo;

View File

@@ -30,6 +30,8 @@ import {
} from '@lit-labs/preact-signals';
import type { Signal } from '@lit-labs/preact-signals';
import { getJsonContext } from "./jsonErrorUtils";
import { VMState } from "./state";
import { Obj } from "@popperjs/core";
export interface VirtualMachineEventMap {
"vm-template-db-loaded": CustomEvent<TemplateDatabase>;
@@ -50,17 +52,8 @@ const jsonErrorRegex = /((invalid type: .*)|(missing field .*)) at line (?<error
class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
ic10vm: Comlink.Remote<VMRef>;
templateDBPromise: Promise<TemplateDatabase>;
templateDB: TemplateDatabase;
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>;
state: VMState = new VMState();
private vm_worker: Worker;
@@ -69,15 +62,6 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
constructor(app: App) {
super();
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();
}
@@ -90,207 +74,30 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
console.info("VM Worker loaded");
const vm = Comlink.wrap<VMRef>(this.vm_worker);
this.ic10vm = vm;
this._vmState.value = await this.ic10vm.saveVMState();
window.VM.set(this);
this.state.vm.value = await this.ic10vm.saveVMState();
this.templateDBPromise = this.ic10vm.getTemplateDatabase();
this.templateDBPromise.then((db) => this.setupTemplateDatabase(db));
effect(() => {
this.updateObjects(this._vmState.value);
this.updateNetworks(this._vmState.value);
});
this.updateCode();
}
get state() {
return this._vmState;
}
get objects() {
return this._objects;
}
get objectIds(): Signal<ObjectID[]> {
return this._objectIds;
}
get circuitHolders() {
return this._circuitHolders;
}
get circuitHolderIds(): Signal<ObjectID[]> {
return this._circuitHolderIds;
}
get networks() {
return this._networks;
}
get networkIds(): Signal<ObjectID[]> {
return this._networkIds;
}
get defaultNetwork() {
return this._default_network;
window.VM.set(this);
}
get activeIC() {
return this._circuitHolders.get(this.app.session.activeIC);
return computed(() => this.app.session.activeIC.value);
}
async visibleDevices(source: number): Promise<Signal<FrozenObjectFull>[]> {
try {
const visDevices = await this.ic10vm.visibleDevices(source);
const ids = Array.from(visDevices);
ids.sort();
return ids.map((id, _index) => this._objects.get(id)!);
} catch (err) {
this.handleVmError(err);
}
}
async visibleDeviceIds(source: number): Promise<number[]> {
async visibleDeviceIds(source: ObjectID): Promise<ObjectID[]> {
const visDevices = await this.ic10vm.visibleDevices(source);
const ids = Array.from(visDevices);
ids.sort();
return ids;
}
async updateNetworks(state: FrozenVM) {
let updateFlag = false;
const removedNetworks = [];
const networkIds: ObjectID[] = [];
const frozenNetworks: FrozenCableNetwork[] = state.networks;
const updatedNetworks: ObjectID[] = [];
for (const [index, net] of frozenNetworks.entries()) {
const id = net.id;
networkIds.push(id);
if (!this._networks.has(id)) {
this._networks.set(id, signal(net));
updateFlag = true;
updatedNetworks.push(id);
} else {
const mappedNet = this._networks.get(id);
if (!structuralEqual(mappedNet.peek(), net)) {
mappedNet.value = net;
updatedNetworks.push(id);
updateFlag = true;
}
}
}
for (const id of this._networks.keys()) {
if (!networkIds.includes(id)) {
this._networks.delete(id);
updateFlag = true;
removedNetworks.push(id);
}
}
if (updateFlag) {
const ids = Array.from(updatedNetworks);
ids.sort();
this.dispatchCustomEvent("vm-networks-update", ids);
if (removedNetworks.length > 0) {
this.dispatchCustomEvent("vm-networks-removed", removedNetworks);
}
this.app.session.save();
}
networkIds.sort();
this._networkIds.value = networkIds;
}
async updateObjects(state: FrozenVM) {
const removedObjects = [];
const frozenObjects = state.objects;
const objectIds: ObjectID[] = [];
const updatedObjects: ObjectID[] = [];
let updateFlag = false;
for (const [index, obj] of frozenObjects.entries()) {
const id = obj.obj_info.id;
objectIds.push(id);
if (!this._objects.has(id)) {
this._objects.set(id, signal(obj));
updateFlag = true;
updatedObjects.push(id);
} else {
const mappedObject = this._objects.get(id);
if (!structuralEqual(obj, mappedObject.peek())) {
mappedObject.value = obj;
updatedObjects.push(id);
updateFlag = true;
}
}
}
for (const id of this._objects.keys()) {
if (!objectIds.includes(id)) {
this._objects.delete(id);
updateFlag = true;
removedObjects.push(id);
}
}
for (const [id, obj] of this._objects) {
if (typeof obj.peek().obj_info.socketed_ic !== "undefined") {
if (!this._circuitHolders.has(id)) {
this._circuitHolders.set(id, obj);
updateFlag = true;
if (!updatedObjects.includes(id)) {
updatedObjects.push(id);
}
}
} else {
if (this._circuitHolders.has(id)) {
updateFlag = true;
if (!updatedObjects.includes(id)) {
updatedObjects.push(id);
}
this._circuitHolders.delete(id);
}
}
}
for (const id of this._circuitHolders.keys()) {
if (!this._objects.has(id)) {
this._circuitHolders.delete(id);
updateFlag = true;
if (!removedObjects.includes(id)) {
removedObjects.push(id);
}
}
}
if (updateFlag) {
const ids = Array.from(updatedObjects);
ids.sort();
this.dispatchCustomEvent("vm-objects-update", ids);
if (removedObjects.length > 0) {
this.dispatchCustomEvent("vm-objects-removed", removedObjects);
}
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() {
const progs = this.app.session.programs;
const progs = this.app.session.programs.peek();
for (const id of progs.keys()) {
const attempt = Date.now().toString(16);
const circuitHolder = this._circuitHolders.get(id);
const circuitHolder = this.state.getObject(id);
const prog = progs.get(id);
if (
circuitHolder &&
@@ -314,42 +121,43 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
}
async step() {
const ic = this.activeIC;
const ic = this.activeIC.peek();
if (ic) {
try {
await this.ic10vm.stepProgrammable(ic.peek().obj_info.id, false);
await this.ic10vm.stepProgrammable(ic, false);
} catch (err) {
this.handleVmError(err);
}
this.update();
this.dispatchCustomEvent("vm-run-ic", this.activeIC!.peek().obj_info.id);
this.dispatchCustomEvent("vm-run-ic", ic);
}
}
async run() {
const ic = this.activeIC;
const ic = this.activeIC.peek();
if (ic) {
try {
await this.ic10vm.runProgrammable(ic.peek().obj_info.id, false);
await this.ic10vm.runProgrammable(ic, false);
} catch (err) {
this.handleVmError(err);
}
this.update();
this.dispatchCustomEvent("vm-run-ic", this.activeIC!.peek().obj_info.id);
this.dispatchCustomEvent("vm-run-ic", this.activeIC.peek());
}
}
async reset() {
const ic = this.activeIC;
const ic = this.activeIC.peek();
if (ic) {
await this.ic10vm.resetProgrammable(ic.peek().obj_info.id);
await this.ic10vm.resetProgrammable(ic);
await this.update();
}
}
async update(save: boolean = true) {
try {
this._vmState.value = await this.ic10vm.saveVMState();
const newState = await this.ic10vm.saveVMState();
this.state.vm.value = newState;
if (save) this.app.session.save();
} catch (err) {
this.handleVmError(err);
@@ -386,46 +194,30 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
this.dispatchCustomEvent("vm-message", toastMessage);
}
// return the data connected oject ids for a network
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);
if (this.app.session.activeIC === oldID) {
this.app.session.activeIC = newID;
}
await this.update();
this.dispatchCustomEvent("vm-object-id-change", {
old: oldID,
new: newID,
});
this.app.session.changeID(oldID, newID);
return true;
} catch (err) {
this.handleVmError(err);
return false;
}
if (this.app.session.activeIC.peek() === oldID) {
this.app.session.activeIC = newID;
}
await this.update();
this.dispatchCustomEvent("vm-object-id-change", {
old: oldID,
new: newID,
});
this.app.session.changeID(oldID, newID);
return true;
}
async setRegister(index: number, val: number): Promise<boolean> {
const ic = this.activeIC!;
const ic = this.activeIC.peek();
if (ic) {
try {
await this.ic10vm.setRegister(ic.peek().obj_info.id, index, val);
await this.ic10vm.setRegister(ic, index, val);
} catch (err) {
this.handleVmError(err);
return false;
@@ -436,10 +228,10 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
}
async setStack(addr: number, val: number): Promise<boolean> {
const ic = this.activeIC!;
const ic = this.activeIC.peek();
if (ic) {
try {
await this.ic10vm.setMemory(ic.peek().obj_info.id, addr, val);
await this.ic10vm.setMemory(ic, addr, val);
} catch (err) {
this.handleVmError(err);
return false;
@@ -450,18 +242,14 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
}
async setObjectName(id: number, name: string): Promise<boolean> {
const obj = this._objects.get(id);
if (obj) {
try {
await this.ic10vm.setObjectName(obj.peek().obj_info.id, name);
} catch (e) {
this.handleVmError(e);
return false;
}
await this.update();
return true;
try {
await this.ic10vm.setObjectName(id, name);
} catch (e) {
this.handleVmError(e);
return false;
}
return false;
await this.update();
return true;
}
async setObjectField(
@@ -471,18 +259,14 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
force?: boolean,
): Promise<boolean> {
force = force ?? false;
const obj = this._objects.get(id);
if (obj) {
try {
await this.ic10vm.setLogicField(obj.peek().obj_info.id, field, val, force);
} catch (err) {
this.handleVmError(err);
return false;
}
await this.update();
return true;
try {
await this.ic10vm.setLogicField(id, field, val, force);
} catch (err) {
this.handleVmError(err);
return false;
}
return false;
await this.update();
return true;
}
async setObjectSlotField(
@@ -493,24 +277,20 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
force?: boolean,
): Promise<boolean> {
force = force ?? false;
const obj = this._objects.get(id);
if (obj) {
try {
await this.ic10vm.setSlotLogicField(
obj.peek().obj_info.id,
field,
slot,
val,
force,
);
} catch (err) {
this.handleVmError(err);
return false;
}
await this.update();
return true;
try {
await this.ic10vm.setSlotLogicField(
id,
field,
slot,
val,
force,
);
} catch (err) {
this.handleVmError(err);
return false;
}
return false;
await this.update();
return true;
}
async setDeviceConnection(
@@ -518,18 +298,14 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
conn: number,
val: number | undefined,
): Promise<boolean> {
const device = this._objects.get(id);
if (typeof device !== "undefined") {
try {
await this.ic10vm.setDeviceConnection(id, conn, val);
} catch (err) {
this.handleVmError(err);
return false;
}
await this.update();
return true;
try {
await this.ic10vm.setDeviceConnection(id, conn, val);
} catch (err) {
this.handleVmError(err);
return false;
}
return false;
await this.update();
return true;
}
async setDevicePin(
@@ -537,50 +313,48 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
pin: number,
val: number | undefined,
): Promise<boolean> {
const device = this._objects.get(id);
if (typeof device !== "undefined") {
try {
await this.ic10vm.setPin(id, pin, val);
} catch (err) {
this.handleVmError(err);
return false;
}
await this.update();
return true;
try {
await this.ic10vm.setPin(id, pin, val);
} catch (err) {
this.handleVmError(err);
return false;
}
return false;
await this.update();
return true;
}
setupTemplateDatabase(db: TemplateDatabase) {
this.templateDB = db;
console.log("Loaded Template Database", this.templateDB);
this.dispatchCustomEvent("vm-template-db-loaded", this.templateDB);
this.state.templateDB.value = db;
console.log("Loaded Template Database", this.state.templateDB.value);
this.dispatchCustomEvent("vm-template-db-loaded", this.state.templateDB.value);
}
async addObjectFrozen(frozen: FrozenObject): Promise<ObjectID | undefined> {
let id = undefined;
try {
console.log("adding device", frozen);
const id = await this.ic10vm.addObjectFrozen(frozen);
await this.update();
return id;
id = await this.ic10vm.addObjectFrozen(frozen);
} catch (err) {
this.handleVmError(err);
return undefined;
}
await this.update();
return id;
}
async addObjectsFrozen(
frozenObjects: FrozenObject[],
): Promise<ObjectID[] | undefined> {
let ids = undefined;
try {
console.log("adding devices", frozenObjects);
const ids = await this.ic10vm.addObjectsFrozen(frozenObjects);
await this.update();
return Array.from(ids);
ids = await this.ic10vm.addObjectsFrozen(frozenObjects);
} catch (err) {
this.handleVmError(err);
return undefined;
}
await this.update();
return Array.from(ids ?? []);
}
async removeDevice(id: number): Promise<boolean> {
@@ -600,32 +374,26 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
frozen: FrozenObject,
quantity: number,
): Promise<boolean> {
const device = this._objects.get(id);
if (typeof device !== "undefined") {
try {
console.log("setting slot occupant", frozen);
await this.ic10vm.setSlotOccupant(id, index, frozen, quantity);
await this.update();
return true;
} catch (err) {
this.handleVmError(err);
}
try {
console.log("setting slot occupant", frozen);
await this.ic10vm.setSlotOccupant(id, index, frozen, quantity);
} catch (err) {
this.handleVmError(err);
return false;
}
return false;
await this.update();
return true;
}
async removeSlotOccupant(id: number, index: number): Promise<boolean> {
const device = this._objects.get(id);
if (typeof device !== "undefined") {
try {
await this.ic10vm.removeSlotOccupant(id, index);
await this.update();
return true;
} catch (err) {
this.handleVmError(err);
}
try {
await this.ic10vm.removeSlotOccupant(id, index);
} catch (err) {
this.handleVmError(err);
return false;
}
return false;
await this.update();
return true;
}
async saveVMState(): Promise<FrozenVM> {
@@ -636,18 +404,16 @@ class VirtualMachine extends TypedEventTarget<VirtualMachineEventMap>() {
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, {jsonContext: JSON.stringify(state)});
this.handleVmError(e, { jsonContext: JSON.stringify(state) });
return;
}
// TODO: Cleanup old state
await this.update();
}
getPrograms(): [number, string][] {
const programs: [number, string][] = Array.from(
this._circuitHolders.entries(),
).map(([id, ic]) => [id, ic.peek().obj_info.source_code]);
const programs: [number, string][] = this.state.circuitHolderIds.value.map((id) => [id, this.state.getObjectProgramSource(id).value]);
return programs;
}
}

View File

@@ -1,15 +1,14 @@
import { html, css, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMActiveICMixin } from "virtualMachine/baseDevice";
import { VMObjectMixin } 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";
import { displayNumber, parseNumber, range, structuralEqual } from "utils";
import { computed, ReadonlySignal, Signal, watch } from "@lit-labs/preact-signals";
@customElement("vm-ic-registers")
export class VMICRegisters extends VMActiveICMixin(BaseElement) {
export class VMICRegisters extends VMObjectMixin(BaseElement) {
static styles = [
...defaultCss,
css`
@@ -39,38 +38,56 @@ export class VMICRegisters extends VMActiveICMixin(BaseElement) {
["ra", 17],
];
constructor() {
super();
this.subscribe("active-ic")
circuit = computed(() => {
return this.vm.value?.state.getCircuitInfo(this.vm.value?.activeIC.value).value;
});
registerCount = computed(() => {
return this.vm.value?.state.getCircuitRegistersCount(this.vm.value.activeIC.value).value;
})
registerAliases = (() => {
let last: [string, number][] = null;
return computed(() => {
const aliases = this.vm.value?.state.getCircuitAliases(this.vm.value?.activeIC.value).value
const forRegisters = [...(Object.entries(aliases ?? {}) ?? [])].flatMap(([alias, target]): [string, number][] => {
if ("RegisterSpec" in target && target.RegisterSpec.indirection === 0) {
return [[alias, target.RegisterSpec.target]];
}
return [];
}).concat(VMICRegisters.defaultAliases);
if (structuralEqual(last, forRegisters)) {
return last;
}
last = forRegisters;
return forRegisters;
});
})();
aliasesFor(index: number): ReadonlySignal<string[]> {
return computed(() => {
return this.registerAliases.value?.flatMap(([alias, target]): string[] => target === index ? [alias] : [])
});
}
registerAt(index: number): ReadonlySignal<number> {
return computed(() => {
return this.vm.value?.state.getCircuitRegistersAt(this.vm.value?.activeIC.value, index).value
})
}
protected render() {
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][];
} else {
return [] as [string, number][];
}
}
).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 registerHtml = computed(() => range(this.registerCount.value).map(index => {
const aliasesList = computed(() => {
return aliases.value.join(", ");
});
return this.aliasesFor(index).value?.join(", ") ?? nothing;
})
const aliasesText = computed(() => {
return aliasesList.value || "None";
return this.aliasesFor(index).value?.join(", ") ?? "None";
});
const valDisplay = computed(() => {
const val = this.objectSignals.registers.value[index];
const val = this.registerAt(index).value;
return displayNumber(val);
});
return html`
@@ -92,12 +109,12 @@ export class VMICRegisters extends VMActiveICMixin(BaseElement) {
</sl-input>
</sl-tooltip>
`;
}) ?? nothing;
}) ?? nothing);
return html`
<sl-card class="card">
<div class="card-body">
${registerHtml}
${watch(registerHtml)}
</div>
</sl-card>
`;

View File

@@ -1,14 +1,14 @@
import { html, css, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMActiveICMixin } from "virtualMachine/baseDevice";
import { VMObjectMixin } from "virtualMachine/baseDevice";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
import { displayNumber, parseNumber } from "utils";
import { displayNumber, parseNumber, range } from "utils";
import { computed, watch } from "@lit-labs/preact-signals";
@customElement("vm-ic-stack")
export class VMICStack extends VMActiveICMixin(BaseElement) {
export class VMICStack extends VMObjectMixin(BaseElement) {
static styles = [
...defaultCss,
css`
@@ -36,25 +36,38 @@ export class VMICStack extends VMActiveICMixin(BaseElement) {
`,
];
constructor() {
super();
this.subscribe("active-ic")
circuit = computed(() => {
return this.vm.value?.state.getCircuitInfo(this.vm.value?.activeIC.value).value;
});
sp = computed(() => {
return this.circuit.value?.registers[16] ?? 0;
});
socketedIc = computed(() => {
return this.vm.value?.state.getObject(this.vm.value?.activeIC.value).value?.obj_info.socketed_ic ?? null;
})
memorySize = computed(() => {
return this.vm.value?.state.getObjectMemorySize(this.socketedIc.value).value;
});
memoryAt(index: number) {
return computed(() => {
return this.vm.value?.state.getObjectMemoryAt(this.socketedIc.value, index).value
});
}
protected render() {
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 memoryHtml = computed(() => range(this.memorySize.value).map(index => {
const content = computed(() => {
return sp.value === index ? html`<strong>Stack Pointer</strong>` : nothing;
return this.sp.value === index ? html`<strong>Stack Pointer</strong>` : nothing;
});
const pointerClass = computed(() => {
return sp.value === index ? "stack-pointer" : nothing;
return this.sp.value === index ? "stack-pointer" : nothing;
});
const displayVal = computed(() => {
return displayNumber(this.objectSignals.memory.value[index]);
return displayNumber(this.memoryAt(index).value);
});
return html`
@@ -75,12 +88,12 @@ export class VMICStack extends VMActiveICMixin(BaseElement) {
</sl-input>
</sl-tooltip>
`;
}) ?? nothing;
}));
return html`
<sl-card class="card">
<div class="card-body">
${memoryHtml}
${watch(memoryHtml)}
</div>
</sl-card>
`;

View File

@@ -0,0 +1,600 @@
import { computed, ReadonlySignal, signal, Signal } from "@lit-labs/preact-signals";
import { Obj } from "@popperjs/core";
import { Class, Connection, FrozenCableNetwork, FrozenNetworks, FrozenObject, FrozenObjectFull, FrozenVM, ICInfo, LogicField, LogicSlotType, LogicType, ObjectID, Operand, Slot, TemplateDatabase } from "ic10emu_wasm";
import { fromJson, isSome, structuralEqual } from "utils";
export interface ObjectSlotInfo {
parent: ObjectID;
index: number;
name: string;
typ: Class;
quantity: number;
occupant: ObjectID;
}
export class VMState {
vm: Signal<FrozenVM> = signal(null);
templateDB: Signal<TemplateDatabase> = signal(null);
objectIds: ReadonlySignal<ObjectID[]> = computed(() => this.vm.value?.objects.map((obj) => obj.obj_info.id) ?? []);
circuitHolderIds: ReadonlySignal<ObjectID[]> = computed(() => this.vm.value?.circuit_holders ?? []);
programHolderIds: ReadonlySignal<ObjectID[]> = computed(() => this.vm.value?.program_holders ?? []);
networkIds: ReadonlySignal<ObjectID[]> = computed(() => this.vm.value?.networks.map((net) => net.id) ?? []);
wirelessTransmitterIds: ReadonlySignal<ObjectID[]> = computed(() => this.vm.value?.wireless_transmitters ?? []);
wirelessReceivers: ReadonlySignal<ObjectID[]> = computed(() => this.vm.value?.wireless_receivers ?? []);
defaultNetworkId: ReadonlySignal<ObjectID> = computed(() => this.vm.value?.default_network_key ?? null);
private _signalCache: Map<string, WeakRef<ReadonlySignal<any>>> = new Map();
private _signalRegistry = new FinalizationRegistry((key: string) => {
const s = this._signalCache.get(key);
if (s && !s.deref()) this._signalCache.delete(key);
});
signalCacheHas(key: string): boolean {
return this._signalCache.has(key) && typeof this._signalCache.get(key).deref() !== undefined
}
signalCacheGet<T>(key: string): any {
return this._signalCache.get(key).deref();
}
signalCacheSet<T>(key: string, s: ReadonlySignal<T>) {
this._signalCache.set(key, new WeakRef(s));
this._signalRegistry.register(s, key);
}
getObject(id: ObjectID): ReadonlySignal<FrozenObject> {
const key = `obj:${id}`;
if (!this.signalCacheHas(key)) {
let last: FrozenObject = null;
const s = computed(() => {
const obj = this.vm.value?.objects.find((o) => o.obj_info.id === id) ?? null;
if (obj?.database_template ?? false) {
return { ...obj, template: this.templateDB.value?.get(obj.obj_info.prefab_hash) }
}
if (structuralEqual(last, obj)) {
return last;
}
last = obj;
return obj;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectName(id: ObjectID): ReadonlySignal<string> {
const key = `obj:${id},name`;
if (!this.signalCacheHas(key)) {
const s = computed(() => {
const obj = this.getObject(id);
return obj.value?.obj_info.name ?? "";
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectPrefabName(id: ObjectID): ReadonlySignal<string> {
const key = `obj:${id},prefabName`;
if (!this.signalCacheHas(key)) {
const s = computed(() => {
const obj = this.getObject(id);
return obj.value?.obj_info.prefab ?? "";
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectDisplayName(id: ObjectID): ReadonlySignal<string> {
const key = `obj:${id},DisplayName`;
if (!this.signalCacheHas(key)) {
const s = computed(() => {
const obj = this.getObject(id).value;
return obj?.obj_info.name ?? obj?.obj_info.prefab ?? "";
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getNetwork(id: ObjectID): Signal<FrozenCableNetwork> {
const key = `network:${id}`;
if (!this.signalCacheHas(key)) {
let last: FrozenCableNetwork = null
const s = computed(() => {
const next = this.vm.value?.networks.find((n) => n.id === id) ?? null
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectFieldNames(id: ObjectID): ReadonlySignal<LogicType[]> {
const key = `obj:${id},fieldNames`;
if (!this.signalCacheHas(key)) {
let last: LogicType[] = null;
const s = computed((): LogicType[] => {
const obj = this.getObject(id).value;
const template = obj?.template;
const logicAccess = isSome(template) && "logic" in template ? template.logic.logic_types : null;
const next = Array.from(logicAccess?.keys() ?? [])
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectField(id: ObjectID, field: LogicType): ReadonlySignal<LogicField> {
const key = `obj:${id},field:${field}`;
if (!this.signalCacheHas(key)) {
const s = computed((): LogicField => {
const obj = this.getObject(id).value;
const template = obj?.template;
const logicAccess = isSome(template) && "logic" in template ? template.logic.logic_types.get(field) : null;
const logicValue = obj?.obj_info.logic_values.get(field) ?? null;
return isSome(logicAccess) || isSome(logicValue) ? {
field_type: logicAccess,
value: logicValue,
} : null;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectSlotCount(id: ObjectID): ReadonlySignal<number> {
const key = `obj:${id},slotsCount`;
if (!this.signalCacheHas(key)) {
const s = computed((): number => {
const obj = this.getObject(id).value;
const template = obj?.template;
return isSome(template) && "slots" in template ? template.slots.length : 0
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectSlotInfo(id: ObjectID, index: number): ReadonlySignal<ObjectSlotInfo> {
const key = `obj:${id},slot${index}`;
if (!this.signalCacheHas(key)) {
let last: ObjectSlotInfo = null;
const s = computed((): ObjectSlotInfo => {
const obj = this.getObject(id).value;
const info = obj?.obj_info.slots.get(index);
const template = obj?.template;
const slotTemplate = isSome(template) && "slots" in template ? template.slots[index] : null;
if (isSome(obj)) {
const next = {
parent: obj?.obj_info.id,
index,
name: slotTemplate?.name,
typ: slotTemplate?.typ,
quantity: info?.quantity,
occupant: info?.id
}
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
}
return null;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectSlotFieldNames(id: ObjectID, index: number): ReadonlySignal<LogicSlotType[]> {
const key = `obj:${id},slot:${index},fieldNames`;
if (!this.signalCacheHas(key)) {
let last: LogicSlotType[] = null;
const s = computed((): LogicSlotType[] => {
const obj = this.getObject(id).value;
const template = obj?.template;
let logicTemplate = null;
if (isSome(template) && ("logic" in template)) {
logicTemplate = template.logic.logic_slot_types.get(index.toString());
}
const next = Array.from(logicTemplate?.keys() ?? []);
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectSlotField(id: ObjectID, index: number, field: LogicSlotType): ReadonlySignal<LogicField> {
const key = `obj:${id},slot:${index},field:${field}`;
if (!this.signalCacheHas(key)) {
let last: LogicField = null
const s = computed((): LogicField => {
const obj = this.getObject(id).value;
const template = obj?.template;
const logicTemplate = isSome(template) && "logic" in template ? template.logic.logic_slot_types.get(index.toString()) : null;
const slotFieldValue = obj?.obj_info.slot_logic_values?.get(index)?.get(field) ?? null;
const slotFieldAccess = logicTemplate?.get(field)
const next = isSome(slotFieldValue) || isSome(slotFieldAccess) ? {
field_type: slotFieldAccess,
value: slotFieldValue
} : null
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectSlotOccupantId(id: ObjectID, index: number): ReadonlySignal<ObjectID> {
const key = `obj:${id},slot:${index},occupant`
if (!this.signalCacheHas(key)) {
const s = computed((): ObjectID => {
const obj = this.getObject(id).value;
const info = obj?.obj_info.slots.get(index);
return info?.id ?? null;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectSocketedIcId(id: ObjectID): ReadonlySignal<ObjectID> {
const key = `obj:${id},socketedIc`
if (!this.signalCacheHas(key)) {
const s = computed((): ObjectID => {
const obj = this.getObject(id).value;
return obj?.obj_info.socketed_ic ?? null;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectConnectionCount(id: ObjectID): ReadonlySignal<number> {
const key = `obj:${id},connectionCount`;
if (!this.signalCacheHas(key)) {
const s = computed((): number => {
const obj = this.getObject(id).value;
const template = obj?.template;
const connectionList =
isSome(template) && "device" in template
? template.device.connection_list
: [];
return connectionList.length;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectConnections(id: ObjectID): ReadonlySignal<Connection[]> {
const key = `obj:${id},connections`
if (!this.signalCacheHas(key)) {
let last: Connection[] = null;
const s = computed((): Connection[] => {
const obj = this.getObject(id).value;
const template = obj?.template;
const connectionsMap = obj?.obj_info.connections ?? null;
const connectionList =
isSome(template) && "device" in template
? template.device.connection_list
: [];
const connections = connectionList.map((conn, index): Connection => {
if (conn.typ === "Data") {
return {
CableNetwork: {
typ: "Data",
role: conn.role,
net: connectionsMap.get(index),
},
};
} else if (conn.typ === "Power") {
return {
CableNetwork: {
typ: "Power",
role: conn.role,
net: connectionsMap.get(index),
},
};
} else if (conn.typ === "PowerAndData") {
return {
CableNetwork: {
typ: "Data",
role: conn.role,
net: connectionsMap.get(index),
},
};
} else if (conn.typ === "Pipe") {
return { Pipe: { role: conn.role } };
} else if (conn.typ === "Chute") {
return { Chute: { role: conn.role } };
} else if (conn.typ === "Elevator") {
return { Elevator: { role: conn.role } };
} else if (conn.typ === "LaunchPad") {
return { LaunchPad: { role: conn.role } };
} else if (conn.typ === "LandingPad") {
return { LandingPad: { role: conn.role } };
} else if (conn.typ === "PipeLiquid") {
return { PipeLiquid: { role: conn.role } };
} else if (conn.typ === "RoboticArmRail") {
return { RoboticArmRail: { role: conn.role } }
}
return "None";
});
if (structuralEqual(last, connections)) {
return last;
}
last = connections;
return connections;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectConnection(id: ObjectID, index: number): ReadonlySignal<Connection> {
const key = `obj:${id},connection:${index}`;
if (!this.signalCacheHas(key)) {
let last: Connection = null;
const s = computed((): Connection => {
const connections = this.getObjectConnections(id).value ?? [];
const next = connections[index] ?? null;
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
})
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key)
}
getCircuitInfo(id: ObjectID): ReadonlySignal<ICInfo> {
const key = `obj:${id},socketedIc`
if (!this.signalCacheHas(key)) {
let last: ICInfo = null;
const s = computed((): ICInfo => {
const obj = this.getObject(id).value;
if (!isSome(obj)) {
return null;
}
let circuitInfo: ICInfo = obj.obj_info.circuit;
if (!isSome(circuitInfo)) {
const icObj = this.getObject(obj.obj_info.socketed_ic).value;
if (!isSome(icObj)) {
return null;
}
circuitInfo = icObj.obj_info.circuit;
}
if (structuralEqual(last, circuitInfo)) {
return last;
}
last = circuitInfo;
return circuitInfo;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectMemorySize(id: ObjectID): ReadonlySignal<number> {
const key = `obj:${id},memorySize`;
if (!this.signalCacheHas(key)) {
const s = computed((): number => {
return this.getObject(id).value?.obj_info.memory?.length ?? null;
})
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectMemory(id: ObjectID): ReadonlySignal<number[]> {
const key = `obj:${id},memory`;
if (!this.signalCacheHas(key)) {
let last: number[] = null;
const s = computed((): number[] => {
const next = this.getObject(id).value?.obj_info.memory ?? null;
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
})
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getObjectMemoryAt(id: ObjectID, index: number): ReadonlySignal<number> {
const key = `obj:${id},memory:${index}`;
if (!this.signalCacheHas(key)) {
const s = computed((): number => {
return (this.getObject(id).value?.obj_info.memory ?? [])[index] ?? null;
})
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getCircuitRegistersCount(id: ObjectID): ReadonlySignal<number> {
const key = `obj:${id},registersCount`;
if (!this.signalCacheHas(key)) {
const s = computed((): number => {
return this.getCircuitInfo(id).value?.registers.length ?? null;
})
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getCircuitRegisters(id: ObjectID): ReadonlySignal<number[]> {
const key = `obj:${id},registers`;
if (!this.signalCacheHas(key)) {
let last: number[] = null;
const s = computed((): number[] => {
const next = this.getCircuitInfo(id).value?.registers ?? null;
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
})
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getCircuitRegistersAt(id: ObjectID, index: number): ReadonlySignal<number> {
const key = `obj:${id},register:${index}`;
if (!this.signalCacheHas(key)) {
const s = computed((): number => {
return (this.getCircuitInfo(id).value?.registers ?? [])[index] ?? null;
})
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getCircuitAliases(id: ObjectID): ReadonlySignal<Record<string, Operand>> {
const key = `obj:${id},circuitAliases`;
if (!this.signalCacheHas(key)) {
let last: Record<string, Operand> = null;
const s = computed(() => {
const circuit = this.getCircuitInfo(id).value;
const aliases = circuit?.aliases;
const next = Object.fromEntries(aliases?.entries() ?? [])
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key)
}
getDeviceNumPins(id: ObjectID): ReadonlySignal<number> {
const key = `obj:${id},numPins`;
if (!this.signalCacheHas(key)) {
const s = computed((): number => {
const obj = this.getObject(id).value;
return [...obj?.obj_info.device_pins?.keys() ?? []].length;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getDevicePins(id: ObjectID): ReadonlySignal<[number, ObjectID][]> {
const key = `obj:${id},pins`;
if (!this.signalCacheHas(key)) {
let last: [number, ObjectID][] = null;
const s = computed((): [number, ObjectID][] => {
const obj = this.getObject(id).value;
const next = [...obj?.obj_info.device_pins?.entries() ?? []];
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getDevicePin(id: ObjectID, pin: number): ReadonlySignal<ObjectID> {
const key = `obj:${id},pin:${id}`;
if (!this.signalCacheHas(key)) {
const s = computed((): ObjectID => {
const obj = this.getObject(id).value;
return obj?.obj_info.device_pins?.get(pin);
});
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key);
}
getNetworkDevices(id: ObjectID): ReadonlySignal<ObjectID[]> {
const key = `network:${id},devices`;
if (!this.signalCacheHas(key)) {
let last: ObjectID[] = null;
const s = computed(() => {
const next = this.vm.value.networks.find((net) => net.id === id)?.devices ?? null;
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
})
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key)
}
getObjectProgramSource(id: ObjectID): ReadonlySignal<string> {
const key = `obj:${id},source`;
if (!this.signalCacheHas(key)) {
const s = computed(() => {
return this.getObject(id).value?.obj_info.source_code ?? null;
})
this.signalCacheSet(key, s);
return s;
}
return this.signalCacheGet(key)
}
}