308 lines
7.4 KiB
TypeScript
308 lines
7.4 KiB
TypeScript
import { range } from "@davidsouther/jiffies/lib/esm/range.js";
|
|
import {
|
|
Pin as ChipPin,
|
|
Pins,
|
|
Voltage,
|
|
} from "@nand2tetris/simulator/chip/chip.js";
|
|
import { createContext, useContext, useEffect, useState } from "react";
|
|
import { ChipDisplayInfo, getDisplayInfo } from "./pin_display.js";
|
|
import "./public/pin.css";
|
|
import { ChipSim } from "./stores/chip.store.js";
|
|
|
|
export const PinContext = createContext({});
|
|
|
|
export interface ImmPin {
|
|
bits: [number, Voltage][];
|
|
pin: ChipPin;
|
|
}
|
|
|
|
export function reducePin(pin: ChipPin) {
|
|
return {
|
|
pin,
|
|
bits: range(0, pin.width)
|
|
.map((i) => [i, pin.voltage(i)] as [number, Voltage])
|
|
.reverse(),
|
|
};
|
|
}
|
|
|
|
export function reducePins(pins: Pins): ImmPin[] {
|
|
return [...pins.entries()].map(reducePin);
|
|
}
|
|
|
|
export interface PinoutPins {
|
|
pins: ImmPin[];
|
|
toggle?: (pin: ChipPin, bit?: number) => void;
|
|
}
|
|
|
|
export const FullPinout = (props: {
|
|
sim: ChipSim;
|
|
toggle: (pin: ChipPin, i: number | undefined) => void;
|
|
setInputValid: (pending: boolean) => void;
|
|
hideInternal?: boolean;
|
|
}) => {
|
|
const { inPins, outPins, internalPins } = props.sim;
|
|
const displayInfo = getDisplayInfo(props.sim.chip[0].name ?? "");
|
|
return (
|
|
<>
|
|
<style>{`
|
|
table.pinout th {
|
|
font-weight: bold;
|
|
}
|
|
|
|
table.pinout tbody td:first-child {
|
|
text-align: right;
|
|
--font-size: 1rem;
|
|
width: 0;
|
|
white-space: nowrap;
|
|
border-right: var(--border-width) solid var(--table-border-color);
|
|
}
|
|
|
|
table.pinout tbody button {
|
|
--font-size: 0.875em;
|
|
font-family: var(--font-family-monospace);
|
|
max-width: 2em;
|
|
}
|
|
`}</style>
|
|
<table className="pinout">
|
|
<tbody>
|
|
<PinoutBlock
|
|
pins={inPins}
|
|
header="Input pins"
|
|
toggle={props.toggle}
|
|
setInputValid={props.setInputValid}
|
|
displayInfo={displayInfo}
|
|
/>
|
|
<PinoutBlock
|
|
pins={outPins}
|
|
header="Output pins"
|
|
disabled={props.sim.pending}
|
|
enableEdit={false}
|
|
displayInfo={displayInfo}
|
|
/>
|
|
{!props.hideInternal && (
|
|
<PinoutBlock
|
|
pins={internalPins}
|
|
header="Internal pins"
|
|
disabled={props.sim.pending}
|
|
enableEdit={false}
|
|
displayInfo={displayInfo}
|
|
/>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const PinoutBlock = (
|
|
props: PinoutPins & {
|
|
header: string;
|
|
disabled?: boolean;
|
|
enableEdit?: boolean;
|
|
setInputValid?: (valid: boolean) => void;
|
|
displayInfo: ChipDisplayInfo;
|
|
},
|
|
) => (
|
|
<>
|
|
{props.pins.length > 0 && (
|
|
<tr>
|
|
<th colSpan={2}>{props.header}</th>
|
|
</tr>
|
|
)}
|
|
{[...props.pins].map((immPin) => (
|
|
<tr key={immPin.pin.name}>
|
|
<td>{immPin.pin.name}</td>
|
|
<td>
|
|
<Pin
|
|
pin={immPin}
|
|
toggle={props.toggle}
|
|
disabled={props.disabled}
|
|
enableEdit={props.enableEdit}
|
|
signed={props.displayInfo.isSigned(immPin.pin.name)}
|
|
setInputValid={props.setInputValid}
|
|
internal={props.header === "Internal pins" ? true : false}
|
|
/>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</>
|
|
);
|
|
|
|
export const Pinout = ({
|
|
pins,
|
|
toggle,
|
|
}: {
|
|
pins: ImmPin[];
|
|
toggle?: (pin: ChipPin, bit?: number) => void;
|
|
}) => {
|
|
if (pins.length === 0) {
|
|
return <>None</>;
|
|
}
|
|
return (
|
|
<table className="pinout">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Value</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{[...pins].map((immPin) => (
|
|
<tr key={immPin.pin.name}>
|
|
<td>{immPin.pin.name}</td>
|
|
<td>
|
|
<Pin pin={immPin} toggle={toggle} internal />
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
);
|
|
};
|
|
|
|
const Pin = ({
|
|
pin,
|
|
toggle,
|
|
disabled = false,
|
|
enableEdit = true,
|
|
signed = true,
|
|
setInputValid,
|
|
internal = false,
|
|
}: {
|
|
pin: ImmPin;
|
|
toggle: ((pin: ChipPin, bit?: number) => void) | undefined;
|
|
disabled?: boolean;
|
|
enableEdit?: boolean;
|
|
signed?: boolean;
|
|
setInputValid?: (valid: boolean) => void;
|
|
internal: boolean;
|
|
}) => {
|
|
const [isBin, setIsBin] = useState(true);
|
|
let inputValid = true;
|
|
const [decimal, setDecimal] = useState("");
|
|
|
|
const toggleBin = () => {
|
|
setIsBin(!isBin);
|
|
};
|
|
|
|
const resetDispatcher = useContext(PinContext);
|
|
if (resetDispatcher instanceof PinResetDispatcher) {
|
|
resetDispatcher.registerCallback(() => {
|
|
setIsBin(true);
|
|
});
|
|
}
|
|
|
|
const setInputValidity = (valid: boolean) => {
|
|
inputValid = valid;
|
|
setInputValid?.(valid);
|
|
};
|
|
|
|
const handleDecimalChange = (value: string) => {
|
|
const positive = value.replace(/[^\d]/g, "");
|
|
const numeric = signed && value[0] === "-" ? `-${positive}` : positive;
|
|
|
|
setDecimal(numeric);
|
|
if (isNaN(parseInt(numeric))) {
|
|
setInputValidity(false);
|
|
} else {
|
|
const newValue = parseInt(numeric);
|
|
if (
|
|
(!signed && newValue >= Math.pow(2, pin.bits.length)) ||
|
|
(signed &&
|
|
(newValue >= Math.pow(2, pin.bits.length - 1) ||
|
|
newValue < -Math.pow(2, pin.bits.length - 1)))
|
|
) {
|
|
setInputValidity(false);
|
|
} else {
|
|
updatePins(newValue);
|
|
setInputValidity(true);
|
|
}
|
|
}
|
|
};
|
|
|
|
const updatePins = (n: number) => {
|
|
for (let i = 0; i < pin.bits.length; i++) {
|
|
if (pin.bits[pin.bits.length - i - 1][1] !== ((n >> i) & 1)) {
|
|
toggle?.(pin.pin, i);
|
|
}
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!isBin && inputValid) {
|
|
let value = 0;
|
|
if (signed && pin.bits[0][1]) {
|
|
// negative
|
|
for (const [i, v] of pin.bits) {
|
|
if (i < pin.bits.length - 1 && !v) {
|
|
value += 2 ** i;
|
|
}
|
|
}
|
|
value = -value - 1;
|
|
} else {
|
|
// positive
|
|
const limit = signed ? pin.bits.length - 1 : pin.bits.length;
|
|
for (const [i, v] of pin.bits) {
|
|
if (i < limit && v) {
|
|
value += 2 ** i;
|
|
}
|
|
}
|
|
}
|
|
setDecimal(value.toString());
|
|
}
|
|
}, [pin, isBin]);
|
|
|
|
return (
|
|
<div
|
|
style={{ display: "flex", flexDirection: "row", alignItems: "center" }}
|
|
>
|
|
<fieldset role="group" style={{ width: `${pin.bits.length}rem` }}>
|
|
{isBin ? (
|
|
pin.bits.map(([i, v]) => (
|
|
<button
|
|
key={i}
|
|
disabled={disabled}
|
|
style={internal ? { backgroundColor: "grey" } : {}}
|
|
onClick={() => toggle?.(pin.pin, i)}
|
|
data-testid={`pin-${i}`}
|
|
>
|
|
{v}
|
|
</button>
|
|
))
|
|
) : (
|
|
<input
|
|
className="colored"
|
|
value={decimal}
|
|
onChange={(e) => {
|
|
handleDecimalChange(e.target.value);
|
|
}}
|
|
disabled={!enableEdit}
|
|
/>
|
|
)}
|
|
</fieldset>
|
|
{pin.bits.length > 1 && (
|
|
<>
|
|
<div style={{ width: "1em" }} />
|
|
<button className="pin-control" onClick={() => toggleBin()}>
|
|
{isBin ? "dec" : "bin"}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export class PinResetDispatcher {
|
|
private callbacks: (() => void)[] = [];
|
|
|
|
registerCallback(callback: () => void) {
|
|
this.callbacks.push(callback);
|
|
}
|
|
|
|
reset() {
|
|
for (const callback of this.callbacks) {
|
|
callback();
|
|
}
|
|
}
|
|
}
|