Device Cards

brings the rework inline were last efforts left off

Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
Rachel Powers
2024-04-09 21:01:12 -07:00
parent 362695af4b
commit 3cdcc742b9
12 changed files with 766 additions and 529 deletions

View File

@@ -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),
}

View File

@@ -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<Slot>,
pub reagents: HashMap<ReagentMode, HashMap<i32, f64>>,
pub ic: Option<u16>,
pub connections: [Connection; 8],
pub connections: Vec<Connection>,
}
#[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<u16, ICError> {
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<bool, VMError> {
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<u16> {
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<u16>) -> Result<bool, VMError> {
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<bool, VMError> {
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<bool, VMError> {
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,

View File

@@ -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<RefCell<ic10emu::VM>>,
}
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<RefCell<ic10emu::Device>>, vm: Rc<RefCell<ic10emu::VM>>) -> Self {
@@ -75,118 +89,81 @@ impl DeviceRef {
#[wasm_bindgen(getter, js_name = "ip")]
pub fn ic_ip(&self) -> Option<u32> {
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<u16> {
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<Stack> {
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<Registers> {
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<f64, JsError> {
pub fn ic_set_stack(&self, address: f64, val: f64) -> Result<f64, JsError> {
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<f64, JsError> {
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<f64, JsError> {
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<u16>) -> 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<bool, JsError> {
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<bool, JsError> {
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<u16>) -> Result<bool, JsError> {
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<u16> {
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<bool, JsError> {
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<bool, JsError> {
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<u16>) -> Result<bool, JsError> {
Ok(self.vm.borrow().set_pin(id, pin, val)?)
}
}
impl Default for VM {

View File

@@ -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`

View File

@@ -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);
}

View File

@@ -149,7 +149,6 @@ export class VMActiveIC extends VMBaseDevice {
"session-active-ic",
this._handleActiveIC.bind(this),
);
this.updateIC();
return root;
}

View File

@@ -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`
<sl-card class="card">
<div class="controls" slot="header">
@@ -96,10 +114,19 @@ export class VMICControls extends VMActiveIC {
</sl-tooltip>
</sl-button-group>
<div class="device-id">
Device:
${this.deviceID}${this.name ?? this.prefabName
? ` : ${this.name ?? this.prefabName}`
: ""}
<sl-select
hoist
placement="bottom"
value="${this.deviceID}"
@sl-change=${this._handleChangeActiveIC}
>
${ics.map(
([id, device], _index) =>
html`<sl-option value=${id}>
Device:${id} ${device.name ?? device.prefabName}
</sl-option>`,
)}
</sl-select>
</div>
</div>
<div class="stats">
@@ -121,15 +148,15 @@ export class VMICControls extends VMActiveIC {
<div class="vstack">
<span>Errors</span>
${this.errors.map(
(err) =>
html`<div class="hstack">
(err) =>
html`<div class="hstack">
<span>
Line: ${err.ParseError.line} -
${err.ParseError.start}:${err.ParseError.end}
</span>
<span class="ms-auto">${err.ParseError.msg}</span>
</div>`,
)}
)}
</div>
</div>
</sl-card>
@@ -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;
}
}

View File

@@ -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<number, VMDeviceSummaryCard>;
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<string, VMDeviceField>;
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`<sl-badge variant="primary" pill pulse>db</sl-badge>`);
}
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`<sl-badge variant="success" pill></sl-badge>`);
}
}, this);
// TODO Reagents
return html`
<img
class="image"
src="img/stationpedia/${this.prefabName}.png"
@onerr=${this.onImageErr}
/>
<div class="header-name">
<sl-input
id="vmDeviceCard${this.deviceID}Name"
class="device-name"
size="small"
pill
placeholder="${this.prefabName}"
@sl-change=${this._handleChangeName}
>
<span slot="prefix">Device ${this.deviceID}</span>
<sl-copy-button
slot="suffix"
from="vmDeviceCard${this.deviceID}Name.value"
></sl-copy-button>
</sl-input>
<sl-input
id="vmDeviceCard${this.deviceID}NameHash"
size="small"
pill
class="device-name-hash"
value="${this.nameHash}"
disabled
>
<span slot="prefix">Name Hash</span>
<sl-copy-button
slot="suffix"
from="vmDeviceCard${this.deviceID}NameHash.value"
></sl-copy-button>
</sl-input>
${badges.map((badge) => badge)}
</div>
`;
}
destroy() {
this.root.remove();
renderFields(): HTMLTemplateResult {
const fields = Array.from(this.fields);
return html`
${fields.map(([name, field], _index, _fields) => {
return html` <sl-input
key="${name}"
value="${field.value}"
@sl-change=${this._handleChangeField}
>
<span slot="prefix">${name}</span>
<span slot="suffix">${field.field_type}</span>
</sl-input>`;
})}
`;
}
renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult {
const fields = Array.from(slot.fields);
return html`
<sl-card class="slot-card">
<span slot="header" class="slot-header"
>${slotIndex} : ${slot.typ}</span
>
<div class="slot-fields">
${fields.map(
([name, field], _index, _fields) => html`
<sl-input
slotIndex=${slotIndex}
key="${name}"
value="${field.value}"
@sl-change=${this._handleChangeSlotField}
>
<span slot="prefix">${name}</span>
<span slot="suffix">${field.field_type}</span>
</sl-input>
`,
)}
</div>
</sl-card>
`;
}
renderSlots(): HTMLTemplateResult {
return html`
<div clas="slots">
${this.slots.map((slot, index, _slots) => this.renderSlot(slot, index))}
</div>
`;
}
renderReagents(): HTMLTemplateResult {
return html``;
}
renderNetworks(): HTMLTemplateResult {
const vmNetworks = window.VM!.networks;
return html`
<div class="networks">
${this.connections.map((connection, index, _conns) => {
const conn =
typeof connection === "object" ? connection.CableNetwork : null;
return html`
<sl-select
hoist
placement="top"
clearable
key=${index}
value=${conn}
?disabled=${conn === null}
@sl-change=${this._handleChangeConnection}
>
<span slot="prefix">Connection:${index}</span>
${vmNetworks.map(
(net) =>
html`<sl-option value=${net}>Network ${net}</sl-option>`,
)}
</sl-select>
`;
})}
</div>
`;
}
renderPins(): HTMLTemplateResult {
const pins = this.pins;
const visibleDevices = window.VM!.visibleDevices(this.deviceID);
return html`
<div class="pins">
${pins?.map(
(pin, index) =>
html`<sl-select
hoist
placement="top"
clearable
key=${index}
value=${pin}
@sl-change=${this._handleChangePin}
>
<span slot="prefix">d${index}</span>
${visibleDevices.map(
(device, _index) =>
html`<sl-option value=${device.id}>
Device ${device.id} : ${device.name ?? device.prefabName}
</sl-option>`,
)}
</sl-select>`,
)}
</div>
`;
}
protected render(): HTMLTemplateResult {
return html`
<sl-card class="card">
<div class="header" slot="header">${this.renderHeader()}</div>
<sl-tab-group>
<sl-tab slot="nav" panel="fields">Fields</sl-tab>
<sl-tab slot="nav" panel="slots">Slots</sl-tab>
<sl-tab slot="nav" panel="reagents" disabled>Reagents</sl-tab>
<sl-tab slot="nav" panel="networks">Networks</sl-tab>
<sl-tab slot="nav" panel="pins" ?disabled=${!this.pins}>Pins</sl-tab>
<sl-tab-panel name="fields">${this.renderFields()}</sl-tab-panel>
<sl-tab-panel name="slots">${this.renderSlots()}</sl-tab-panel>
<sl-tab-panel name="reagents">${this.renderReagents()}</sl-tab-panel>
<sl-tab-panel name="networks">${this.renderNetworks()}</sl-tab-panel>
<sl-tab-panel name="pins">${this.renderPins()}</sl-tab-panel>
</sl-tab-group>
</sl-card>
`;
}
_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`
<div class="device-list">
${this.devices.map(
(id, _index, _ids) =>
html`<vm-device-card .deviceID=${id}></vm-device-card>`,
)}
</div>
`;
}
}
export { VMDeviceUI };

View File

@@ -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 {

View File

@@ -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 {
<sl-card class="card">
<div class="card-body">
${this.registers?.map((val, index) => {
const aliases = registerAliases
.filter(([_alias, target]) => index === target)
.map(([alias, _target]) => alias);
return html`
const aliases = registerAliases
.filter(([_alias, target]) => index === target)
.map(([alias, _target]) => alias);
return html`
<sl-tooltip placement="left" class="tooltip">
<div slot="content">
<strong>Regster r${index}</strong> Aliases:
@@ -96,7 +96,7 @@ export class VMICRegisters extends VMActiveIC {
</sl-input>
</sl-tooltip>
`;
})}
})}
</div>
</sl-card>
`;

View File

@@ -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`

View File

@@ -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`
<div class="side-container">
<vm-ic-controls></vm-ic-controls>
<sl-details summary="Registers" open>
<vm-ic-registers></vm-ic-registers>
</sl-details>
<sl-details summary="Stack">
<vm-ic-stack></vm-ic-stack>
</sl-details>
<sl-tab-group>
<sl-tab slot="nav" panel="active-ic">Active IC</sl-tab>
<sl-tab slot="nav" panel="devices">Devices</sl-tab>
<sl-tab-panel name="active-ic">
<sl-details summary="Registers" open>
<vm-ic-registers></vm-ic-registers>
</sl-details>
<sl-details summary="Stack">
<vm-ic-stack></vm-ic-stack>
</sl-details>
</sl-tab-panel>
<sl-tab-panel name="devices">
<vm-device-list></vm-device-list>
</sl-tab-panel>
</sl-tab-group>
</div>
`;
}