Files
nand2tetris/web-ide-main/components/src/pinout.tsx
T
2026-04-09 14:14:56 +02:00

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