diff --git a/ic10emu/src/interpreter.rs b/ic10emu/src/interpreter.rs index 77677c3..8e9a6c0 100644 --- a/ic10emu/src/interpreter.rs +++ b/ic10emu/src/interpreter.rs @@ -46,6 +46,10 @@ pub enum ICError { StackIndexOutOfRange(f64), #[error("slot index out of range: '{0}'")] SlotIndexOutOfRange(f64), + #[error("pin index {0} out of range 0-6")] + PinIndexOutOfRange(usize), + #[error("Connection index {0} out of range {1}")] + ConnectionIndexOutOfRange(usize, usize), #[error("Unknown device ID '{0}'")] UnknownDeviceID(f64), #[error("Too few operands!: provide: '{provided}', desired: '{desired}'")] @@ -98,14 +102,12 @@ pub enum ICError { TypeValueNotKnown, #[error("Empty Device List")] EmptyDeviceList, - #[error("Connection index out of range: '{0}'")] - ConnectionIndexOutOFRange(u32), #[error("Connection specifier missing")] MissingConnectionSpecifier, #[error("No data network on connection '{0}'")] - NotDataConnection(u32), + NotDataConnection(usize), #[error("Network not connected on connection '{0}'")] - NetworkNotConnected(u32), + NetworkNotConnected(usize), #[error("Bad Network Id '{0}'")] BadNetworkId(u32), } diff --git a/ic10emu/src/lib.rs b/ic10emu/src/lib.rs index e45ecec..eb28d97 100644 --- a/ic10emu/src/lib.rs +++ b/ic10emu/src/lib.rs @@ -30,6 +30,8 @@ pub enum VMError { LineError(#[from] LineError), #[error("Invalid network ID {0}")] InvalidNetwork(u16), + #[error("Device {0} not visible to device {1} (not on the same networks)")] + DeviceNotVisible(u16, u16), } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -169,7 +171,7 @@ pub struct Device { pub slots: Vec, pub reagents: HashMap>, pub ic: Option, - pub connections: [Connection; 8], + pub connections: Vec, } #[derive(Debug, Serialize, Deserialize)] @@ -264,7 +266,7 @@ impl Device { slots: Vec::new(), reagents: HashMap::new(), ic: None, - connections: [Connection::default(); 8], + connections: vec![Connection::default()], }; device.connections[0] = Connection::CableNetwork(None); device @@ -300,16 +302,19 @@ impl Device { } pub fn get_network_id(&self, connection: usize) -> Result { - if connection >= 8 { - Err(ICError::ConnectionIndexOutOFRange(connection as u32)) + if connection >= self.connections.len() { + Err(ICError::ConnectionIndexOutOfRange( + connection, + self.connections.len(), + )) } else if let Connection::CableNetwork(network_id) = self.connections[connection] { if let Some(network_id) = network_id { Ok(network_id) } else { - Err(ICError::NetworkNotConnected(connection as u32)) + Err(ICError::NetworkNotConnected(connection)) } } else { - Err(ICError::NotDataConnection(connection as u32)) + Err(ICError::NotDataConnection(connection)) } } @@ -388,6 +393,11 @@ impl Device { } 0.0 } + + pub fn set_name(&mut self, name: &str) { + self.name_hash = Some((const_crc32::crc32(name.as_bytes()) as i32).into()); + self.name = Some(name.to_owned()); + } } impl Default for VM { @@ -451,15 +461,28 @@ impl VM { }); } let id = device.id; + + let first_data_network = + device + .connections + .iter() + .enumerate() + .find_map(|(index, conn)| match conn { + &Connection::CableNetwork(_) => Some(index), + &Connection::Other => None, + }); self.devices.insert(id, Rc::new(RefCell::new(device))); - let _ = self.add_device_to_network( - id, - if let Some(network) = network { - network - } else { - self.default_network - }, - ); + if let Some(first_data_network) = first_data_network { + let _ = self.add_device_to_network( + id, + if let Some(network) = network { + network + } else { + self.default_network + }, + first_data_network, + ); + } Ok(id) } @@ -485,16 +508,28 @@ impl VM { } let id = device.id; let ic_id = ic.id; + let first_data_network = + device + .connections + .iter() + .enumerate() + .find_map(|(index, conn)| match conn { + &Connection::CableNetwork(_) => Some(index), + &Connection::Other => None, + }); self.devices.insert(id, Rc::new(RefCell::new(device))); self.ics.insert(ic_id, Rc::new(RefCell::new(ic))); - let _ = self.add_device_to_network( - id, - if let Some(network) = network { - network - } else { - self.default_network - }, - ); + if let Some(first_data_network) = first_data_network { + let _ = self.add_device_to_network( + id, + if let Some(network) = network { + network + } else { + self.default_network + }, + first_data_network, + ); + } Ok(id) } @@ -682,12 +717,71 @@ impl VM { false } - fn add_device_to_network(&self, id: u16, network_id: u16) -> Result { - self.set_modified(id); - if !self.devices.contains_key(&id) { + /// return a vecter with the device ids the source id can see via it's connected netowrks + pub fn visible_devices(&self, source: u16) -> Vec { + self.networks + .values() + .filter_map(|net| { + if net.borrow().contains(&[source]) { + Some( + net.borrow() + .devices + .iter() + .filter(|id| id != &&source) + .copied() + .collect_vec(), + ) + } else { + None + } + }) + .concat() + } + + pub fn set_pin(&self, id: u16, pin: usize, val: Option) -> Result { + let Some(device) = self.devices.get(&id) else { return Err(VMError::UnknownId(id)); }; + if let Some(other_device) = val { + if !self.devices.contains_key(&other_device) { + return Err(VMError::UnknownId(other_device)); + } + if !self.devices_on_same_network(&[id, other_device]) { + return Err(VMError::DeviceNotVisible(other_device, id)); + } + } + if !(0..7).contains(&pin) { + Err(ICError::PinIndexOutOfRange(pin).into()) + } else { + let Some(ic_id) = device.borrow().ic else { + return Err(VMError::NoIC(id)); + }; + self.ics.get(&ic_id).unwrap().borrow_mut().pins[pin] = val; + Ok(true) + } + } + + pub fn add_device_to_network( + &self, + id: u16, + network_id: u16, + connection: usize, + ) -> Result { if let Some(network) = self.networks.get(&network_id) { + let Some(device) = self.devices.get(&id) else { + return Err(VMError::UnknownId(id)); + }; + if connection >= device.borrow().connections.len() { + let conn_len = device.borrow().connections.len(); + return Err(ICError::ConnectionIndexOutOfRange(connection, conn_len).into()); + } + let Connection::CableNetwork(ref mut conn) = + device.borrow_mut().connections[connection] + else { + return Err(ICError::NotDataConnection(connection).into()); + }; + *conn = Some(network_id); + network.borrow_mut().add(id); Ok(true) } else { @@ -695,6 +789,27 @@ impl VM { } } + pub fn remove_device_from_network(&self, id: u16, network_id: u16) -> Result { + if let Some(network) = self.networks.get(&network_id) { + let Some(device) = self.devices.get(&id) else { + return Err(VMError::UnknownId(id)); + }; + let mut device_ref = device.borrow_mut(); + + for conn in device_ref.connections.iter_mut() { + if let Connection::CableNetwork(conn) = conn { + if Some(network_id) == *conn { + *conn = None; + } + } + } + network.borrow_mut().remove(id); + Ok(true) + } else { + Err(VMError::InvalidNetwork(network_id)) + } + } + pub fn set_batch_device_field( &self, source: u16, diff --git a/ic10emu_wasm/src/lib.rs b/ic10emu_wasm/src/lib.rs index 3e30511..490135a 100644 --- a/ic10emu_wasm/src/lib.rs +++ b/ic10emu_wasm/src/lib.rs @@ -2,15 +2,19 @@ mod utils; mod types; -use types::{Stack, Registers}; +use ic10emu::{ + grammar::{LogicType, SlotLogicType}, + Connection, +}; +use serde::{Deserialize, Serialize}; +use types::{Registers, Stack}; -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, rc::Rc, str::FromStr}; use itertools::Itertools; // use itertools::Itertools; use wasm_bindgen::prelude::*; - #[wasm_bindgen] extern "C" { fn alert(s: &str); @@ -22,6 +26,16 @@ pub struct DeviceRef { vm: Rc>, } +use thiserror::Error; + +#[derive(Error, Debug, Serialize, Deserialize)] +pub enum BindingError { + #[error("{0} is not a valid variant")] + InvalidEnumVariant(String), + #[error("Index {0} is out of range {1}")] + OutOfBounds(usize, usize), +} + #[wasm_bindgen] impl DeviceRef { fn from_device(device: Rc>, vm: Rc>) -> Self { @@ -75,118 +89,81 @@ impl DeviceRef { #[wasm_bindgen(getter, js_name = "ip")] pub fn ic_ip(&self) -> Option { - self.device - .borrow() - .ic - .as_ref() - .and_then(|ic| { - self.vm - .borrow() - .ics - .get(ic) - .map(|ic| ic.as_ref().borrow().ip) - }) + self.device.borrow().ic.as_ref().and_then(|ic| { + self.vm + .borrow() + .ics + .get(ic) + .map(|ic| ic.as_ref().borrow().ip) + }) } #[wasm_bindgen(getter, js_name = "instructionCount")] pub fn ic_instruction_count(&self) -> Option { - self.device - .borrow() - .ic - .as_ref() - .and_then(|ic| { - self.vm - .borrow() - .ics - .get(ic) - .map(|ic| ic.as_ref().borrow().ic) - }) + self.device.borrow().ic.as_ref().and_then(|ic| { + self.vm + .borrow() + .ics + .get(ic) + .map(|ic| ic.as_ref().borrow().ic) + }) } #[wasm_bindgen(getter, js_name = "stack")] pub fn ic_stack(&self) -> Option { - self.device - .borrow() - .ic - .as_ref() - .and_then(|ic| { - self.vm - .borrow() - .ics - .get(ic) - .map(|ic| Stack(ic.as_ref().borrow().stack)) - }) + self.device.borrow().ic.as_ref().and_then(|ic| { + self.vm + .borrow() + .ics + .get(ic) + .map(|ic| Stack(ic.as_ref().borrow().stack)) + }) } #[wasm_bindgen(getter, js_name = "registers")] pub fn ic_registers(&self) -> Option { - self.device - .borrow() - .ic - .as_ref() - .and_then(|ic| { - self.vm - .borrow() - .ics - .get(ic) - .map(|ic| Registers(ic.as_ref().borrow().registers)) - }) + self.device.borrow().ic.as_ref().and_then(|ic| { + self.vm + .borrow() + .ics + .get(ic) + .map(|ic| Registers(ic.as_ref().borrow().registers)) + }) } #[wasm_bindgen(getter, js_name = "aliases", skip_typescript)] pub fn ic_aliases(&self) -> JsValue { - serde_wasm_bindgen::to_value( - &self - .device + serde_wasm_bindgen::to_value(&self.device.borrow().ic.as_ref().and_then(|ic| { + self.vm .borrow() - .ic - .as_ref() - .and_then(|ic| { - self.vm - .borrow() - .ics - .get(ic) - .map(|ic| ic.as_ref().borrow().aliases.clone()) - }), - ) + .ics + .get(ic) + .map(|ic| ic.as_ref().borrow().aliases.clone()) + })) .unwrap() } #[wasm_bindgen(getter, js_name = "defines", skip_typescript)] pub fn ic_defines(&self) -> JsValue { - serde_wasm_bindgen::to_value( - &self - .device + serde_wasm_bindgen::to_value(&self.device.borrow().ic.as_ref().and_then(|ic| { + self.vm .borrow() - .ic - .as_ref() - .and_then(|ic| { - self.vm - .borrow() - .ics - .get(ic) - .map(|ic| ic.as_ref().borrow().defines.clone()) - }), - ) + .ics + .get(ic) + .map(|ic| ic.as_ref().borrow().defines.clone()) + })) .unwrap() } #[wasm_bindgen(getter, js_name = "pins", skip_typescript)] pub fn ic_pins(&self) -> JsValue { - serde_wasm_bindgen::to_value( - &self - .device + serde_wasm_bindgen::to_value(&self.device.borrow().ic.as_ref().and_then(|ic| { + self.vm .borrow() - .ic - .as_ref() - .and_then(|ic| { - self.vm - .borrow() - .ics - .get(ic) - .map(|ic| ic.as_ref().borrow().pins) - }), - ) + .ics + .get(ic) + .map(|ic| ic.as_ref().borrow().pins) + })) .unwrap() } @@ -208,20 +185,13 @@ impl DeviceRef { #[wasm_bindgen(getter, js_name = "program", skip_typescript)] pub fn ic_program(&self) -> JsValue { - serde_wasm_bindgen::to_value( - &self - .device + serde_wasm_bindgen::to_value(&self.device.borrow().ic.as_ref().and_then(|ic| { + self.vm .borrow() - .ic - .as_ref() - .and_then(|ic| { - self.vm - .borrow() - .ics - .get(ic) - .map(|ic| ic.borrow().program.clone()) - }), - ) + .ics + .get(ic) + .map(|ic| ic.borrow().program.clone()) + })) .unwrap() } @@ -275,7 +245,7 @@ impl DeviceRef { } #[wasm_bindgen(js_name = "setStack")] - pub fn ic_set_stack(&mut self, address: f64, val: f64) -> Result { + pub fn ic_set_stack(&self, address: f64, val: f64) -> Result { let ic_id = *self .device .borrow() @@ -290,6 +260,77 @@ impl DeviceRef { let result = ic.borrow_mut().poke(address, val)?; Ok(result) } + + #[wasm_bindgen(js_name = "setName")] + pub fn set_name(&self, name: &str) { + self.device.borrow_mut().set_name(name) + } + + #[wasm_bindgen(js_name = "setField")] + pub fn set_field(&self, field: &str, value: f64) -> Result { + let logic_typ = LogicType::from_str(field)?; + let mut device_ref = self.device.borrow_mut(); + let logic_field = device_ref + .fields + .get_mut(&logic_typ) + .ok_or_else(|| BindingError::InvalidEnumVariant(field.to_owned()))?; + let last = logic_field.value; + logic_field.value = value; + Ok(last) + } + + #[wasm_bindgen(js_name = "setSlotField")] + pub fn set_slot_field(&self, slot: usize, field: &str, value: f64) -> Result { + let logic_typ = SlotLogicType::from_str(field)?; + let mut device_ref = self.device.borrow_mut(); + let slots_len = device_ref.slots.len(); + let slot = device_ref + .slots + .get_mut(slot) + .ok_or(BindingError::OutOfBounds(slot, slots_len))?; + let logic_field = slot + .fields + .get_mut(&logic_typ) + .ok_or_else(|| BindingError::InvalidEnumVariant(field.to_owned()))?; + let last = logic_field.value; + logic_field.value = value; + Ok(last) + } + + #[wasm_bindgen(js_name = "setConnection")] + pub fn set_connection(&self, conn: usize, net: Option) -> Result<(), JsError> { + let mut device_ref = self.device.borrow_mut(); + let conn_len = device_ref.connections.len(); + let conn_ref = device_ref + .connections + .get_mut(conn) + .ok_or(BindingError::OutOfBounds(conn, conn_len))?; + match conn_ref { + &mut Connection::CableNetwork(ref mut net_ref) => *net_ref = net, + _ => { + *conn_ref = Connection::CableNetwork(net); + } + } + Ok(()) + } + + #[wasm_bindgen(js_name = "addDeviceToNetwork")] + pub fn add_device_to_network(&self, network_id: u16, connection: usize) -> Result { + let id = self.device.borrow().id; + Ok(self.vm.borrow().add_device_to_network(id, network_id, connection)?) + } + + #[wasm_bindgen(js_name = "removeDeviceFromNetwork")] + pub fn remove_device_from_network(&self, network_id: u16) -> Result { + let id = self.device.borrow().id; + Ok(self.vm.borrow().remove_device_from_network(id, network_id)?) + } + + #[wasm_bindgen(js_name = "setPin")] + pub fn set_pin(&self, pin: usize, val: Option) -> Result { + let id = self.device.borrow().id; + Ok(self.vm.borrow().set_pin(id, pin, val)?) + } } #[wasm_bindgen] @@ -370,6 +411,27 @@ impl VM { self.vm.borrow().last_operation_modified() } + #[wasm_bindgen(js_name = "visibleDevices")] + pub fn visible_devices(&self, source: u16) -> Vec { + self.vm.borrow().visible_devices(source) + } + + #[wasm_bindgen(js_name = "addDeviceToNetwork")] + pub fn add_device_to_network(&self, id: u16, network_id: u16, connection: usize) -> Result { + Ok(self.vm.borrow().add_device_to_network(id, network_id, connection)?) + } + + #[wasm_bindgen(js_name = "removeDeviceFromNetwork")] + pub fn remove_device_from_network(&self, id: u16, network_id: u16) -> Result { + Ok(self.vm.borrow().remove_device_from_network(id, network_id)?) + } + + #[wasm_bindgen(js_name = "setPin")] + pub fn set_pin(&self, id: u16, pin: usize, val: Option) -> Result { + Ok(self.vm.borrow().set_pin(id, pin, val)?) + } + + } impl Default for VM { diff --git a/www/src/ts/components/base.ts b/www/src/ts/components/base.ts index e6b8963..6f60273 100644 --- a/www/src/ts/components/base.ts +++ b/www/src/ts/components/base.ts @@ -1,7 +1,6 @@ import { CSSResultGroup, LitElement, css, unsafeCSS } from "lit"; import shoelaceDark from "@shoelace-style/shoelace/dist/themes/dark.styles.js"; - export const defaultCss = [ shoelaceDark, css` diff --git a/www/src/ts/utils.ts b/www/src/ts/utils.ts index 8beb357..aec801b 100644 --- a/www/src/ts/utils.ts +++ b/www/src/ts/utils.ts @@ -157,3 +157,23 @@ export async function openFile(editor: Ace.Editor) { input.click(); } } + +export function parseNumber(s: string): number { + switch (s.toLowerCase()) { + case 'nan': + return Number.NaN; + case 'pinf': + return Number.POSITIVE_INFINITY; + case 'ninf': + return Number.NEGATIVE_INFINITY; + case 'pi': + return 3.141592653589793; + case 'deg2rad': + return 0.0174532923847437; + case 'rad2deg': + return 57.2957801818848; + case 'epsilon': + return Number.EPSILON; + } + return parseFloat(s); +} diff --git a/www/src/ts/virtual_machine/base_device.ts b/www/src/ts/virtual_machine/base_device.ts index 8dbd710..d4a931a 100644 --- a/www/src/ts/virtual_machine/base_device.ts +++ b/www/src/ts/virtual_machine/base_device.ts @@ -149,7 +149,6 @@ export class VMActiveIC extends VMBaseDevice { "session-active-ic", this._handleActiveIC.bind(this), ); - this.updateIC(); return root; } diff --git a/www/src/ts/virtual_machine/controls.ts b/www/src/ts/virtual_machine/controls.ts index 265a68b..29806e9 100644 --- a/www/src/ts/virtual_machine/controls.ts +++ b/www/src/ts/virtual_machine/controls.ts @@ -10,6 +10,10 @@ import "@shoelace-style/shoelace/dist/components/button/button.js"; import "@shoelace-style/shoelace/dist/components/icon/icon.js"; import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js"; import "@shoelace-style/shoelace/dist/components/divider/divider.js"; +import "@shoelace-style/shoelace/dist/components/select/select.js"; +import "@shoelace-style/shoelace/dist/components/badge/badge.js"; +import "@shoelace-style/shoelace/dist/components/option/option.js"; +import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.js"; @customElement("vm-ic-controls") export class VMICControls extends VMActiveIC { @@ -41,6 +45,19 @@ export class VMICControls extends VMActiveIC { sl-divider { --spacing: 0.25rem; } + + sl-button[variant="success"] { + /* Changes the success theme color to purple using primitives */ + --sl-color-success-600: var(--sl-color-purple-700); + } + sl-button[variant="primary"] { + /* Changes the success theme color to purple using primitives */ + --sl-color-primary-600: var(--sl-color-cyan-600); + } + sl-button[variant="warning"] { + /* Changes the success theme color to purple using primitives */ + --sl-color-warning-600: var(--sl-color-amber-600); + } `, ]; @@ -50,6 +67,7 @@ export class VMICControls extends VMActiveIC { } protected render() { + const ics = Array.from(window.VM!.ics); return html`
@@ -96,10 +114,19 @@ export class VMICControls extends VMActiveIC {
- Device: - ${this.deviceID}${this.name ?? this.prefabName - ? ` : ${this.name ?? this.prefabName}` - : ""} + + ${ics.map( + ([id, device], _index) => + html` + Device:${id} ${device.name ?? device.prefabName} + `, + )} +
@@ -121,15 +148,15 @@ export class VMICControls extends VMActiveIC {
Errors ${this.errors.map( - (err) => - html`
+ (err) => + html`
Line: ${err.ParseError.line} - ${err.ParseError.start}:${err.ParseError.end} ${err.ParseError.msg}
`, - )} + )}
@@ -145,4 +172,10 @@ export class VMICControls extends VMActiveIC { _handleResetClick() { window.VM?.reset(); } + + _handleChangeActiveIC(e: CustomEvent) { + const select = e.target as SlSelect; + const icId = parseInt(select.value as string); + window.App!.session.activeIC = icId; + } } diff --git a/www/src/ts/virtual_machine/device.ts b/www/src/ts/virtual_machine/device.ts index 9f16dd0..bb8231c 100644 --- a/www/src/ts/virtual_machine/device.ts +++ b/www/src/ts/virtual_machine/device.ts @@ -1,398 +1,359 @@ -import { Offcanvas } from "bootstrap"; -import { VirtualMachine, VirtualMachineUI } from "."; -import { DeviceRef, VM } from "ic10emu_wasm"; +import { Slot } from "ic10emu_wasm"; +import { html, css, HTMLTemplateResult } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { BaseElement, defaultCss } from "../components"; +import { VMBaseDevice } from "./base_device"; +import "@shoelace-style/shoelace/dist/components/card/card.js"; +import "@shoelace-style/shoelace/dist/components/icon/icon.js"; +import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js"; +import "@shoelace-style/shoelace/dist/components/input/input.js"; +import "@shoelace-style/shoelace/dist/components/details/details.js"; +import "@shoelace-style/shoelace/dist/components/tab/tab.js"; +import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js"; +import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js"; +import "@shoelace-style/shoelace/dist/components/copy-button/copy-button.js"; +import "@shoelace-style/shoelace/dist/components/select/select.js"; +import "@shoelace-style/shoelace/dist/components/badge/badge.js"; +import "@shoelace-style/shoelace/dist/components/option/option.js"; +import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js"; +import { parseNumber, structuralEqual } from "../utils"; +import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.js"; - - -class VMDeviceUI { - ui: VirtualMachineUI; - summary: HTMLDivElement; - canvasEl: HTMLDivElement; - deviceCountEl: HTMLElement; - canvas: Offcanvas; - private _deviceSummaryCards: Map; - private _offCanvaseCards: Map< - number, - { col: HTMLElement; card: VMDeviceCard } - >; - - constructor(ui: VirtualMachineUI) { - const that = this; - that.ui = ui; - this.summary = document.getElementById("vmDeviceSummary") as HTMLDivElement; - this.canvasEl = document.getElementById( - "vmDevicesOCBody", - ) as HTMLDivElement; - this.deviceCountEl = document.getElementById("vmViewDeviceCount"); - this.canvas = new Offcanvas(this.canvasEl); - this._deviceSummaryCards = new Map(); - this._offCanvaseCards = new Map(); - } - - update(active_ic: DeviceRef) { - const devices = window.VM.devices; - this.deviceCountEl.textContent = `(${devices.size})`; - for (const [id, device] of devices) { - if (!this._deviceSummaryCards.has(id)) { - this._deviceSummaryCards.set(id, new VMDeviceSummaryCard(this, device)); - } - if (!this._offCanvaseCards.has(id)) { - const col = document.createElement("div"); - col.classList.add("col"); - col.id = `${this.canvasEl.id}_col${id}` - this.canvasEl.appendChild(col); - this._offCanvaseCards.set(id, { - col, - card: new VMDeviceCard(this, col, device), - }); - } - } - this._deviceSummaryCards.forEach((card, id, cards) => { - if (!devices.has(id)) { - card.destroy(); - cards.delete(id); - } else { - card.update(active_ic); - } - }, this); - this._offCanvaseCards.forEach((card, id, cards) => { - if (!devices.has(id)) { - card.card.destroy(); - card.col.remove(); - cards.delete(id); - } else { - card.card.update(active_ic); - } - }, this); - } -} - -class VMDeviceSummaryCard { - root: HTMLDivElement; - viewBtn: HTMLButtonElement; - deviceUI: VMDeviceUI; - device: DeviceRef; - badges: HTMLSpanElement[]; - constructor(deviceUI: VMDeviceUI, device: DeviceRef) { - // const that = this; - this.deviceUI = deviceUI; - this.device = device; - this.root = document.createElement("div"); - this.root.classList.add( - "hstack", - "gap-2", - "bg-light-subtle", - "border", - "border-secondary-subtle", - "rounded", - ); - this.viewBtn = document.createElement("button"); - this.viewBtn.type = "button"; - this.viewBtn.classList.add("btn", "btn-success"); - this.root.appendChild(this.viewBtn); - this.deviceUI.summary.appendChild(this.root); - this.badges = []; - - this.update(window.VM.activeIC); - } - - update(active_ic: DeviceRef) { - const that = this; - // clear previous badges - this.badges.forEach((badge) => badge.remove()); - this.badges = []; - - //update name - var deviceName = this.device.name ?? this.device.prefabName ?? ""; - if (deviceName) { - deviceName = `: ${deviceName}`; - } - const btnTxt = `Device ${this.device.id}${deviceName}`; - this.viewBtn.textContent = btnTxt; - - // regenerate badges - this.device.connections.forEach((conn, index) => { - if (typeof conn === "object") { - var badge = document.createElement("span"); - badge.classList.add("badge", "text-bg-light"); - badge.textContent = `Net ${index}:${conn.CableNetwork}`; - that.badges.push(badge); - that.root.appendChild(badge); - } - }); - - if (this.device.id === active_ic.id) { - var badge = document.createElement("span"); - badge.classList.add("badge", "text-bg-success"); - badge.textContent = "db"; - that.badges.push(badge); - that.root.appendChild(badge); - } - - active_ic.pins?.forEach((id, index) => { - if (that.device.id === id) { - var badge = document.createElement("span"); - badge.classList.add("badge", "text-bg-success"); - badge.textContent = `d${index}`; - that.badges.push(badge); - that.root.appendChild(badge); - } - }); - } - - destroy() { - this.root.remove(); - } -} - -class VMDeviceCard { - ui: VMDeviceUI; - container: HTMLElement; - device: DeviceRef; - root: HTMLDivElement; - nav: HTMLUListElement; - - header: HTMLDivElement; - nameInput: HTMLInputElement; - nameHash: HTMLSpanElement; - body: HTMLDivElement; - badges: HTMLSpanElement[]; - fieldsContainer: HTMLDivElement; - slotsContainer: HTMLDivElement; - pinsContainer: HTMLDivElement; - networksContainer: HTMLDivElement; - reagentsContainer: HTMLDivElement; - nav_id: string; - navTabs: { [key: string]: { li: HTMLLIElement; button: HTMLButtonElement } }; - paneContainer: HTMLDivElement; - tabPanes: { [key: string]: HTMLElement }; - image: HTMLImageElement; +@customElement("vm-device-card") +export class VMDeviceCard extends VMBaseDevice { image_err: boolean; - title: HTMLHeadingElement; - fieldEls: Map; - constructor(ui: VMDeviceUI, container: HTMLElement, device: DeviceRef) { - this.ui = ui; - this.container = container; - this.device = device; - this.nav_id = `${this.container.id}_vmDeviceCard${this.device.id}`; - - this.root = document.createElement("div"); - this.root.classList.add("card"); - - this.header = document.createElement("div"); - this.header.classList.add("card-header", "hstack"); - this.image = document.createElement("img"); - this.image_err = false; - this.image.src = `/img/stationpedia/${this.device.prefabName}.png`; - this.image.onerror = this.onImageErr; - this.image.width = 48; - this.image.classList.add("me-2"); - this.header.appendChild(this.image); - - this.title = document.createElement("h5"); - this.title.textContent = `Device ${this.device.id} : ${this.device.prefabName ?? ""}`; - this.header.appendChild(this.title); - - this.nameInput = document.createElement("input"); - this.nameHash = document.createElement("span"); - - this.root.appendChild(this.header); - - this.body = document.createElement("div"); - this.body.classList.add("card-body"); - this.root.appendChild(this.body); - - this.nav = document.createElement("ul"); - this.nav.classList.add("nav", "nav-tabs"); - this.nav.role = "tablist"; - this.nav.id = this.nav_id; - this.navTabs = {}; - this.tabPanes = {}; - - this.body.appendChild(this.nav); - - this.paneContainer = document.createElement("div"); - this.paneContainer.id = `${this.nav_id}_tabs`; - - this.body.appendChild(this.paneContainer); - - this.badges = []; - this.fieldsContainer = document.createElement("div"); - this.fieldsContainer.id = `${this.nav_id}_fields`; - this.fieldsContainer.classList.add("vstack"); - this.fieldEls = new Map(); - this.slotsContainer = document.createElement("div"); - this.slotsContainer.id = `${this.nav_id}_slots`; - this.slotsContainer.classList.add("vstack"); - this.reagentsContainer = document.createElement("div"); - this.reagentsContainer.id = `${this.nav_id}_reagents`; - this.reagentsContainer.classList.add("vstack"); - this.networksContainer = document.createElement("div"); - this.networksContainer.id = `${this.nav_id}_networks`; - this.networksContainer.classList.add("vstack"); - this.pinsContainer = document.createElement("div"); - this.pinsContainer.id = `${this.nav_id}_pins`; - this.pinsContainer.classList.add("vstack"); - - this.addTab("Fields", this.fieldsContainer); - this.addTab("Slots", this.slotsContainer); - this.addTab("Networks", this.networksContainer); - - this.update(window.VM.activeIC); - - // do last to minimise reflows - this.container.appendChild(this.root); - } + static styles = [ + ...defaultCss, + css` + :host { + display: block; + box-sizing: border-box; + } + .card { + width: 100%; + box-sizing: border-box; + } + .image { + width: 4rem; + height: 4rem; + } + .header { + display: flex; + flex-direction: row; + } + .header-name { + display: flex; + flex-direction: row; + width: 100%; + flex-grow: 1; + align-items: center; + flex-wrap: wrap; + } + // .device-name { + // box-sizing: border-box; + // width: 8rem; + // } + // .device-name-hash { + // box-sizing: border-box; + // width: 5rem; + // } + sl-divider { + --spacing: 0.25rem; + } + sl-button[variant="success"] { + /* Changes the success theme color to purple using primitives */ + --sl-color-success-600: var(--sl-color-purple-700); + } + sl-button[variant="primary"] { + /* Changes the success theme color to purple using primitives */ + --sl-color-primary-600: var(--sl-color-cyan-600); + } + sl-button[variant="warning"] { + /* Changes the success theme color to purple using primitives */ + --sl-color-warning-600: var(--sl-color-amber-600); + } + sl-tab-group { + margin-left: 1rem; + margin-right: 1rem; + --indicator-color: var(--sl-color-purple-600); + --sl-color-primary-600: var(--sl-color-purple-600); + } + `, + ]; onImageErr(e: Event) { this.image_err = true; console.log("Image load error", e); } - - addNav(name: string, target: string) { - if (!(name in this.navTabs)) { - var li = document.createElement("li"); - li.classList.add("nav-item"); - li.role = "presentation"; - var button = document.createElement("button"); - button.classList.add("nav-link"); - if (!(Object.keys(this.navTabs).length > 0)) { - button.classList.add("active"); - button.tabIndex = 0; - } else { - button.tabIndex = -1; - } - button.id = `${this.nav_id}_tab_${name}`; - button.setAttribute("data-bs-toggle", "tab"); - button.setAttribute("data-bs-target", `#${target}`); - button.type = "button"; - button.role = "tab"; - button.setAttribute("aria-controls", target); - button.setAttribute( - "aria-selected", - Object.keys(this.navTabs).length > 0 ? "false" : "true", - ); - button.textContent = name; - li.appendChild(button); - this.nav.appendChild(li); - this.navTabs[name] = { li, button }; - return true; + renderHeader(): HTMLTemplateResult { + const activeIc = window.VM?.activeIC; + const badges: HTMLTemplateResult[] = []; + if (this.deviceID == activeIc?.id) { + badges.push(html`db`); } - return false; - } - - removeNav(name: string) { - if (name in this.navTabs) { - this.navTabs[name].li.remove(); - delete this.navTabs[name]; - return true; - } - return false; - } - - addTab(name: string, tab: HTMLElement) { - const paneName = `${this.nav_id}_pane_${name}`; - if (this.addNav(name, paneName)) { - if (name in this.tabPanes) { - this.tabPanes[name].remove(); - } - const pane = document.createElement("div"); - pane.classList.add("tap-pane", "fade"); - if (!(Object.keys(this.tabPanes).length > 0)) { - pane.classList.add("show", "active"); - } - pane.id = paneName; - pane.role = "tabpanel"; - pane.setAttribute("aria-labelledby", `${this.nav_id}_tab_${name}`); - pane.tabIndex = 0; - - this.paneContainer.appendChild(pane); - pane.appendChild(tab); - this.tabPanes[name] = tab; - } - } - - removeTab(name: string) { - let result = this.removeNav(name); - if (name in this.tabPanes) { - this.tabPanes[name].remove(); - delete this.tabPanes[name]; - return true; - } - return result; - } - - update(active_ic: DeviceRef) { - if (this.device.pins) { - this.addTab("Pins", this.pinsContainer); - } else { - this.removeTab("Pins"); - } - - // fields - for (const [name, _field] of this.device.fields) { - if (!this.fieldEls.has(name)) { - const field = new VMDeviceField(this.device, name, this, this.fieldsContainer); - this.fieldEls.set(name, field); - } - } - this.fieldEls.forEach((field, name, map) => { - if(!this.device.fields.has(name)) { - field.destroy(); - map.delete(name); - } else { - field.update(active_ic); + activeIc?.pins?.forEach((id, index) => { + if (this.deviceID == id) { + badges.push(html``); } }, this); - - - // TODO Reagents + return html` + +
+ + Device ${this.deviceID} + + + + Name Hash + + + ${badges.map((badge) => badge)} +
+ `; } - destroy() { - this.root.remove(); + renderFields(): HTMLTemplateResult { + const fields = Array.from(this.fields); + return html` + ${fields.map(([name, field], _index, _fields) => { + return html` + ${name} + ${field.field_type} + `; + })} + `; + } + + renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult { + const fields = Array.from(slot.fields); + return html` + + ${slotIndex} : ${slot.typ} +
+ ${fields.map( + ([name, field], _index, _fields) => html` + + ${name} + ${field.field_type} + + `, + )} +
+
+ `; + } + renderSlots(): HTMLTemplateResult { + return html` +
+ ${this.slots.map((slot, index, _slots) => this.renderSlot(slot, index))} +
+ `; + } + renderReagents(): HTMLTemplateResult { + return html``; + } + renderNetworks(): HTMLTemplateResult { + const vmNetworks = window.VM!.networks; + return html` +
+ ${this.connections.map((connection, index, _conns) => { + const conn = + typeof connection === "object" ? connection.CableNetwork : null; + return html` + + Connection:${index} + ${vmNetworks.map( + (net) => + html`Network ${net}`, + )} + + `; + })} +
+ `; + } + renderPins(): HTMLTemplateResult { + const pins = this.pins; + const visibleDevices = window.VM!.visibleDevices(this.deviceID); + return html` +
+ ${pins?.map( + (pin, index) => + html` + d${index} + ${visibleDevices.map( + (device, _index) => + html` + Device ${device.id} : ${device.name ?? device.prefabName} + `, + )} + `, + )} +
+ `; + } + protected render(): HTMLTemplateResult { + return html` + +
${this.renderHeader()}
+ + Fields + Slots + Reagents + Networks + Pins + + ${this.renderFields()} + ${this.renderSlots()} + ${this.renderReagents()} + ${this.renderNetworks()} + ${this.renderPins()} + +
+ `; + } + + _handleChangeName(e: CustomEvent) { + const input = e.target as SlInput; + this.device.setName(input.value); + this.updateDevice(); + } + + _handleChangeField(e: CustomEvent) { + const input = e.target as SlInput; + const field = input.getAttribute("key")!; + const val = parseNumber(input.value); + this.device.setField(field, val); + this.updateDevice(); + } + + _handleChangeSlotField(e: CustomEvent) { + const input = e.target as SlInput; + const slot = parseInt(input.getAttribute("slotIndex")!); + const field = input.getAttribute("key")!; + const val = parseNumber(input.value); + this.device.setSlotField(slot, field, val); + this.updateDevice(); + } + + _handleChangeConnection(e: CustomEvent) { + const select = e.target as SlSelect; + const conn = parseInt(select.getAttribute("key")!); + const last = this.device.connections[conn]; + const val = select.value ? parseInt(select.value as string) : undefined; + if (typeof last === "object" && typeof last.CableNetwork === "number") { + // is there no other connection to the previous network? + if ( + !this.device.connections.some((other_conn, index) => { + structuralEqual(last, other_conn) && index !== conn; + }) + ) { + this.device.removeDeviceFromNetwork(last.CableNetwork); + } + } + if (typeof val !== "undefined") { + this.device.addDeviceToNetwork(conn, val); + } else { + this.device.setConnection(conn, val); + } + + this.updateDevice(); + } + + _handleChangePin(e: CustomEvent) { + const select = e.target as SlSelect; + const pin = parseInt(select.getAttribute("key")!); + const val = select.value ? parseInt(select.value as string) : undefined; + this.device.setPin(pin, val); + this.updateDevice(); } } -class VMDeviceField { - container: HTMLElement; - card: VMDeviceCard; - device: DeviceRef; - field: string; - root: HTMLDivElement; - name: HTMLSpanElement; - fieldType: HTMLSpanElement; - input: HTMLInputElement; - constructor(device: DeviceRef, field: string, card: VMDeviceCard, container: HTMLElement) { - this.device = device; - this.field = field; - this.card = card; - this.container = container; - this.root = document.createElement('div'); - this.root.classList.add("input-group", "input-group-sm"); - this.name = document.createElement('span'); - this.name.classList.add("input-group-text", "field_name"); - this.name.textContent = this.field; - this.root.appendChild(this.name); - this.fieldType = document.createElement('span'); - this.fieldType.classList.add("input-group-text", "field_type"); - this.fieldType.textContent = device.fields.get(this.field)?.field_type; - this.root.appendChild(this.fieldType); - this.input = document.createElement('input'); - this.input.type = "text"; - this.input.value = this.device.fields.get(this.field)?.value.toString(); - this.root.appendChild(this.input); +@customElement("vm-device-list") +export class VMDeviceList extends BaseElement { + @state() accessor devices: number[]; - this.container.appendChild(this.root); + constructor() { + super(); + this.devices = window.VM!.deviceIds; } - destroy () { - this.root.remove(); + + connectedCallback(): void { + const root = super.connectedCallback(); + window.VM?.addEventListener( + "vm-devices-update", + this._handleDevicesUpdate.bind(this), + ); + return root; } - update(_active_ic: DeviceRef) { - this.input.value = this.device.fields.get(this.field)?.value.toString(); + + _handleDevicesUpdate(e: CustomEvent) { + const ids = e.detail; + if (!structuralEqual(this.devices, ids)) { + this.devices = ids; + } + } + + protected render(): HTMLTemplateResult { + return html` +
+ ${this.devices.map( + (id, _index, _ids) => + html``, + )} +
+ `; } } - -export { VMDeviceUI }; diff --git a/www/src/ts/virtual_machine/index.ts b/www/src/ts/virtual_machine/index.ts index 7ee07df..c709e37 100644 --- a/www/src/ts/virtual_machine/index.ts +++ b/www/src/ts/virtual_machine/index.ts @@ -51,25 +51,49 @@ class VirtualMachine extends EventTarget { return this._devices; } + get deviceIds() { + return Array.from(this.ic10vm.devices); + } + get ics() { return this._ics; } + get icIds() { + return Array.from(this.ic10vm.ics); + } + + get networks() { + return Array.from(this.ic10vm.networks); + } + + get defaultNetwork() { + return this.ic10vm.defaultNetwork; + } + get activeIC() { return this._ics.get(window.App!.session.activeIC); } + visibleDevices(source: number) { + const ids = Array.from(this.ic10vm.visibleDevices(source)); + return ids.map((id, _index) => this._devices.get(id)!); + } + updateDevices() { + var update_flag = false; const device_ids = this.ic10vm.devices; for (const id of device_ids) { if (!this._devices.has(id)) { this._devices.set(id, this.ic10vm.getDevice(id)!); + update_flag = true; } } for (const id of this._devices.keys()) { if (!device_ids.includes(id)) { this._devices.get(id)!.free(); this._devices.delete(id); + update_flag = true; } } @@ -77,14 +101,21 @@ class VirtualMachine extends EventTarget { for (const id of ics) { if (!this._ics.has(id)) { this._ics.set(id, this._devices.get(id)!); + update_flag = true; } } for (const id of this._ics.keys()) { if (!ics.includes(id)) { this._ics.get(id)!.free(); this._ics.delete(id); + update_flag = true; } } + if (update_flag) { + this.dispatchEvent( + new CustomEvent("vm-devices-update", { detail: device_ids }), + ); + } } updateCode() { @@ -190,6 +221,7 @@ class VirtualMachine extends EventTarget { this.db = db; console.log("Loaded Device Database", this.db); } + } class VirtualMachineUI { diff --git a/www/src/ts/virtual_machine/registers.ts b/www/src/ts/virtual_machine/registers.ts index 6c4f598..3752cbf 100644 --- a/www/src/ts/virtual_machine/registers.ts +++ b/www/src/ts/virtual_machine/registers.ts @@ -57,7 +57,7 @@ export class VMICRegisters extends VMActiveIC { } }; const validation = - "[-+]?(([0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?)|((\\.[0-9]+)([eE][+-]?[0-9]+)?)|([iI][nN][fF][iI][nN][iI][tT][yY]))"; + "[\\-+]?(([0-9]+(\\.[0-9]+)?([eE][\\-+]?[0-9]+)?)|((\\.[0-9]+)([eE][\\-+]?[0-9]+)?)|([iI][nN][fF][iI][nN][iI][tT][yY]))"; const registerAliases: [string, number][] = ( ( [...(this.aliases ?? [])].filter( @@ -73,10 +73,10 @@ export class VMICRegisters extends VMActiveIC {
${this.registers?.map((val, index) => { - const aliases = registerAliases - .filter(([_alias, target]) => index === target) - .map(([alias, _target]) => alias); - return html` + const aliases = registerAliases + .filter(([_alias, target]) => index === target) + .map(([alias, _target]) => alias); + return html`
Regster r${index} Aliases: @@ -96,7 +96,7 @@ export class VMICRegisters extends VMActiveIC { `; - })} + })}
`; diff --git a/www/src/ts/virtual_machine/stack.ts b/www/src/ts/virtual_machine/stack.ts index fb4725f..8188dcc 100644 --- a/www/src/ts/virtual_machine/stack.ts +++ b/www/src/ts/virtual_machine/stack.ts @@ -7,7 +7,6 @@ import "@shoelace-style/shoelace/dist/components/card/card.js"; import "@shoelace-style/shoelace/dist/components/icon/icon.js"; import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js"; import "@shoelace-style/shoelace/dist/components/input/input.js"; -import { RegisterSpec } from "ic10emu_wasm"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js"; @customElement("vm-ic-stack") @@ -54,7 +53,7 @@ export class VMICStack extends VMActiveIC { } }; const validation = - "[-+]?(([0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?)|((\\.[0-9]+)([eE][+-]?[0-9]+)?)|([iI][nN][fF][iI][nN][iI][tT][yY]))"; + "[\\-+]?(([0-9]+(\\.[0-9]+)?([eE][\\-+]?[0-9]+)?)|((\\.[0-9]+)([eE][\\-+]?[0-9]+)?)|([iI][nN][fF][iI][nN][iI][tT][yY]))"; const sp = this.registers![16]; return html` diff --git a/www/src/ts/virtual_machine/ui.ts b/www/src/ts/virtual_machine/ui.ts index d8eb189..be82639 100644 --- a/www/src/ts/virtual_machine/ui.ts +++ b/www/src/ts/virtual_machine/ui.ts @@ -2,19 +2,25 @@ import { HTMLTemplateResult, html, css } from "lit"; import { customElement, property, query } from "lit/decorators.js"; import { BaseElement, defaultCss } from "../components"; import "@shoelace-style/shoelace/dist/components/details/details.js"; +import "@shoelace-style/shoelace/dist/components/tab/tab.js"; +import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js"; +import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js"; import "./controls"; import "./registers"; import "./stack"; +import "./device"; @customElement("vm-ui") export class VMUI extends BaseElement { static styles = [ ...defaultCss, css` - sl-details { + sl-tab-group { margin-left: 1rem; margin-right: 1rem; + --indicator-color: var(--sl-color-purple-600); + --sl-color-primary-600: var(--sl-color-purple-600); } sl-details::part(header) { padding: 0.3rem; @@ -42,12 +48,21 @@ export class VMUI extends BaseElement { return html`
- - - - - - + + Active IC + Devices + + + + + + + + + + + +
`; }