refactor(frontend): fix signal graph
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
"trunc",
|
||||
"ufuzzy",
|
||||
"VMIC",
|
||||
"VMUI",
|
||||
"vstack",
|
||||
"whos"
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
600
www/src/ts/virtualMachine/state.ts
Normal file
600
www/src/ts/virtualMachine/state.ts
Normal 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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user