Web-Ide mit aufgenommen
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
COMMANDS_ALU,
|
||||
COMMANDS_OP,
|
||||
Flags,
|
||||
} from "@nand2tetris/simulator/cpu/alu.js";
|
||||
|
||||
export const ALUComponent = ({
|
||||
A,
|
||||
op,
|
||||
D,
|
||||
out,
|
||||
flag,
|
||||
}: {
|
||||
A: number;
|
||||
op: COMMANDS_OP;
|
||||
D: number;
|
||||
out: number;
|
||||
flag: keyof typeof Flags;
|
||||
}) => (
|
||||
<div className="alu">
|
||||
<span>ALU</span>
|
||||
<svg width="250" height="250" version="1.1">
|
||||
<defs>
|
||||
<rect
|
||||
x="34.442518"
|
||||
y="54.335354"
|
||||
width="0.91770717"
|
||||
height="20.780869"
|
||||
/>
|
||||
</defs>
|
||||
<g>
|
||||
<polygon
|
||||
points="70,10 180,85 180,165 70,240 70,135 90,125 70,115"
|
||||
stroke="#000"
|
||||
fill="#6D97AB"
|
||||
/>
|
||||
<text
|
||||
xmlSpace="preserve"
|
||||
textAnchor="middle"
|
||||
y="61"
|
||||
x="35"
|
||||
// fill="#000000" // use style from chip.scss
|
||||
>
|
||||
{A}
|
||||
</text>
|
||||
<text
|
||||
xmlSpace="preserve"
|
||||
textAnchor="middle"
|
||||
y="176"
|
||||
x="35"
|
||||
// fill="#000000" // use style from chip.scss
|
||||
>
|
||||
{D}
|
||||
</text>
|
||||
<text
|
||||
xmlSpace="preserve"
|
||||
textAnchor="middle"
|
||||
y="121"
|
||||
x="207"
|
||||
// fill="#000000" // use style from chip.scss
|
||||
>
|
||||
{out}
|
||||
</text>
|
||||
<text
|
||||
xmlSpace="preserve"
|
||||
y="130.50002"
|
||||
x="110.393929"
|
||||
fill="#ffffff"
|
||||
fontSize={24}
|
||||
>
|
||||
{COMMANDS_ALU.op[op] ?? "(??)"}
|
||||
</text>
|
||||
<g>
|
||||
<path /*stroke="black"*/ d="M 6,67.52217 H 68.675994" />
|
||||
<path
|
||||
/*stroke="black"*/ d="M 68.479388,67.746136 60.290279,61.90711"
|
||||
/>
|
||||
<path
|
||||
/*stroke="black"*/ d="m 68.479388,67.40711 -8.189109,5.839026"
|
||||
/>
|
||||
</g>
|
||||
<g transform="translate(0,115.5)">
|
||||
<path /*stroke="black"*/ d="M 6,67.52217 H 68.675994" />
|
||||
<path
|
||||
d="M 68.479388,67.746136 60.290279,61.90711" /*stroke="black"*/
|
||||
/>
|
||||
<path
|
||||
/*stroke="black"*/ d="m 68.479388,67.40711 -8.189109,5.839026"
|
||||
/>
|
||||
</g>
|
||||
<g transform="translate(176,57.5)">
|
||||
<path /*stroke="black"*/ d="M 6,67.52217 H 68.675994" />
|
||||
<path
|
||||
/*stroke="black"*/ d="M 68.479388,67.746136 60.290279,61.90711"
|
||||
/>
|
||||
<path
|
||||
/*stroke="black"*/ d="m 68.479388,67.40711 -8.189109,5.839026"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,155 @@
|
||||
import { KeyboardAdapter } from "@nand2tetris/simulator/cpu/memory.js";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { RegisterComponent } from "./register.js";
|
||||
|
||||
const KeyMap: Record<string, number | undefined> = {
|
||||
// Delete: 127,
|
||||
Enter: 128,
|
||||
Backspace: 129,
|
||||
ArrowLeft: 130,
|
||||
ArrowUp: 131,
|
||||
ArrowRight: 132,
|
||||
ArrowDown: 133,
|
||||
Home: 134,
|
||||
End: 135,
|
||||
PageUp: 136,
|
||||
PageDown: 137,
|
||||
Insert: 138,
|
||||
Delete: 139,
|
||||
Escape: 140,
|
||||
F1: 141,
|
||||
F2: 142,
|
||||
F3: 143,
|
||||
F4: 144,
|
||||
F5: 145,
|
||||
F6: 146,
|
||||
F7: 147,
|
||||
F8: 148,
|
||||
F9: 149,
|
||||
F10: 150,
|
||||
F11: 151,
|
||||
F12: 152,
|
||||
};
|
||||
|
||||
const keyDisplays: Record<string, string> = {
|
||||
ArrowLeft: "L-arrow",
|
||||
ArrowUp: "U-arrow",
|
||||
ArrowRight: "R-arrow",
|
||||
ArrowDown: "D-arrow",
|
||||
};
|
||||
|
||||
function getKeyDisplay(key: string) {
|
||||
return keyDisplays[key] ?? key;
|
||||
}
|
||||
|
||||
function keyPressToHackCharacter(keypress: KeyboardEvent): number {
|
||||
const mapping = KeyMap[keypress.key];
|
||||
if (mapping !== undefined) {
|
||||
return mapping;
|
||||
}
|
||||
if (keypress.key.length === 1) {
|
||||
const code = keypress.key.charCodeAt(0);
|
||||
if (code >= 32 && code <= 126) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export const Keyboard = ({
|
||||
keyboard,
|
||||
update,
|
||||
}: {
|
||||
keyboard: KeyboardAdapter;
|
||||
update?: () => void;
|
||||
}) => {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [character, setCharacter] = useState("");
|
||||
const [bits, setBits] = useState(keyboard.getKey());
|
||||
let currentKey = 0;
|
||||
|
||||
const toggleRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const toggleEnabled = () => {
|
||||
setEnabled(!enabled);
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCharacter(getKeyDisplay(event.key));
|
||||
toggleRef.current?.blur();
|
||||
const key = keyPressToHackCharacter(event);
|
||||
if (key) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (key === currentKey) {
|
||||
return;
|
||||
}
|
||||
setKey(key);
|
||||
update?.();
|
||||
};
|
||||
|
||||
const onKeyUp = (event: KeyboardEvent) => {
|
||||
toggleRef.current?.blur();
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyboard.getKey()) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
currentKey = 0;
|
||||
keyboard.clearKey();
|
||||
update?.();
|
||||
setBits(keyboard.getKey());
|
||||
setCharacter("");
|
||||
};
|
||||
|
||||
// note on setCharacter vs setKey:
|
||||
// setCharacter sets the string value that will be displayed in the component,
|
||||
// while setKey actually sets and tracks the value that will be stored in the keyboard memory
|
||||
|
||||
const setKey = (key: number) => {
|
||||
if (key === 0) {
|
||||
return;
|
||||
}
|
||||
keyboard.setKey(key);
|
||||
setBits(keyboard.getKey());
|
||||
currentKey = key;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("keyup", onKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
window.removeEventListener("keyup", onKeyUp);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<article className="panel">
|
||||
<div className="flex row align-baseline">
|
||||
<button
|
||||
onClick={toggleEnabled}
|
||||
ref={toggleRef}
|
||||
className="flex-0"
|
||||
style={{ whiteSpace: "pre" }}
|
||||
>
|
||||
{`${enabled ? "Disable" : "Enable"} Keyboard`}
|
||||
</button>
|
||||
<div className="flex-1"></div> {/* padding */}
|
||||
<div className="flex-4">Key: {character}</div>
|
||||
<div className="flex-4">
|
||||
<RegisterComponent name="Char code" bits={bits} />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Memory as MemoryChip } from "@nand2tetris/simulator/cpu/memory.js";
|
||||
import { range } from "@davidsouther/jiffies/lib/esm/range.js";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { MemoryBlock, MemoryCell } from "./memory.js";
|
||||
|
||||
describe("<Memory />", () => {
|
||||
describe("<MemoryCell />", () => {
|
||||
it("renders a read-only cell", () => {
|
||||
render(<MemoryCell index={16} value={"34"} />);
|
||||
|
||||
const addr = screen.getByText("16");
|
||||
expect(addr).toBeVisible();
|
||||
|
||||
const cell = screen.getByText("34");
|
||||
expect(cell).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("<MemoryBlock />", () => {
|
||||
it.skip("renders a small amount of memory", () => {
|
||||
const memory = new MemoryChip(
|
||||
new Int16Array(
|
||||
range(0, 16).map((i) => (Math.pow(i, 12) ^ 0x9753) & 0xffff)
|
||||
).buffer
|
||||
);
|
||||
render(<MemoryBlock memory={memory} />);
|
||||
|
||||
const zero = screen.getByText("0x0000");
|
||||
expect(zero).toBeVisible();
|
||||
|
||||
// const indexes = document.querySelectorAll("code:nth-of-type(even)");
|
||||
// expect(indexes.length).toBe(16);
|
||||
|
||||
// const cells = document.querySelectorAll("code:nth-of-type(even)");
|
||||
// expect(cells.length).toBe(16);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,396 @@
|
||||
import { rounded } from "@davidsouther/jiffies/lib/esm/dom/css/border.js";
|
||||
import {
|
||||
forwardRef,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import {
|
||||
Format,
|
||||
FORMATS,
|
||||
MemoryAdapter,
|
||||
} from "@nand2tetris/simulator/cpu/memory.js";
|
||||
import { loadAsm, loadBlob, loadHack } from "@nand2tetris/simulator/loader.js";
|
||||
import { asm } from "@nand2tetris/simulator/util/asm.js";
|
||||
import { bin, dec, hex } from "@nand2tetris/simulator/util/twos.js";
|
||||
|
||||
import { useClockReset } from "../clockface.js";
|
||||
import InlineEdit from "../inline_edit.js";
|
||||
import { LOADING } from "../messages.js";
|
||||
import { useStateInitializer } from "../react.js";
|
||||
import { BaseContext } from "../stores/base.context.js";
|
||||
import VirtualScroll, { VirtualScrollSettings } from "../virtual_scroll.js";
|
||||
|
||||
const ITEM_HEIGHT = 34;
|
||||
|
||||
export const MemoryBlock = ({
|
||||
memory,
|
||||
jmp = { value: 0 },
|
||||
highlight = -1,
|
||||
editable = false,
|
||||
justifyLeft = false, // TODO: handle this in css in the future
|
||||
count,
|
||||
maxSize,
|
||||
offset = 0,
|
||||
cellLabels,
|
||||
format = dec,
|
||||
onChange = () => undefined,
|
||||
onFocus = () => undefined,
|
||||
}: {
|
||||
jmp?: { value: number };
|
||||
memory: MemoryAdapter;
|
||||
highlight?: number;
|
||||
editable?: boolean;
|
||||
justifyLeft?: boolean;
|
||||
count?: number;
|
||||
offset?: number;
|
||||
maxSize?: number;
|
||||
cellLabels?: string[];
|
||||
format?: (v: number) => string;
|
||||
onChange?: (i: number, value: string, previous: number) => void;
|
||||
onFocus?: (i: number) => void;
|
||||
}) => {
|
||||
const settings = useMemo<Partial<VirtualScrollSettings>>(
|
||||
() => ({
|
||||
count: Math.min(memory.size, count ?? 25),
|
||||
maxIndex: maxSize ?? memory.size,
|
||||
itemHeight: ITEM_HEIGHT,
|
||||
startIndex: jmp.value,
|
||||
}),
|
||||
[memory.size, jmp],
|
||||
);
|
||||
const get = useCallback(
|
||||
(pos: number, count: number): [number, number][] =>
|
||||
memory
|
||||
.range(pos + offset, pos + offset + count)
|
||||
.map((v, i) => [i + pos + offset, v]),
|
||||
[memory],
|
||||
);
|
||||
|
||||
const row = useCallback(
|
||||
([i, v]: [number, number]) => (
|
||||
<MemoryCell
|
||||
index={i}
|
||||
value={format(v)}
|
||||
label={(cellLabels?.[i] ?? "").padStart(
|
||||
cellLabels ? Math.max(...cellLabels.map((label) => label.length)) : 0,
|
||||
)}
|
||||
showLabel={cellLabels != undefined}
|
||||
size={memory.size}
|
||||
editable={editable}
|
||||
justifyLeft={justifyLeft}
|
||||
highlight={i === highlight}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
),
|
||||
[format, editable, highlight, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<VirtualScroll<[number, number], ReactNode>
|
||||
settings={settings}
|
||||
get={get}
|
||||
row={row}
|
||||
rowKey={([i]) => i}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const MemoryCell = ({
|
||||
index,
|
||||
value,
|
||||
label,
|
||||
showLabel = false,
|
||||
size,
|
||||
highlight = false,
|
||||
editable = false,
|
||||
justifyLeft = false,
|
||||
onChange = () => undefined,
|
||||
onFocus = () => undefined,
|
||||
}: {
|
||||
index: number;
|
||||
value: string;
|
||||
label?: string;
|
||||
showLabel?: boolean;
|
||||
size?: number;
|
||||
highlight?: boolean;
|
||||
editable?: boolean;
|
||||
justifyLeft?: boolean;
|
||||
onChange?: (i: number, value: string, previous: number) => void;
|
||||
onFocus?: (i: number) => void;
|
||||
}) => (
|
||||
<div style={{ display: "flex", height: "100%" }}>
|
||||
{showLabel && (
|
||||
<code
|
||||
style={{
|
||||
...rounded("none"),
|
||||
...(highlight ? { background: "var(--mark-background-color)" } : {}),
|
||||
whiteSpace: "pre",
|
||||
}}
|
||||
>
|
||||
{label ?? ""}
|
||||
</code>
|
||||
)}
|
||||
<code
|
||||
style={{
|
||||
...rounded("none"),
|
||||
...(highlight ? { background: "var(--mark-background-color)" } : {}),
|
||||
whiteSpace: "pre",
|
||||
}}
|
||||
>
|
||||
{size
|
||||
? dec(index).padStart(Math.ceil(Math.log10(size)), " ")
|
||||
: dec(index)}
|
||||
</code>
|
||||
<code
|
||||
style={{
|
||||
flex: "1",
|
||||
textAlign: justifyLeft ? "left" : "right",
|
||||
color: "var(--text-color)",
|
||||
...rounded("none"),
|
||||
...(highlight ? { background: "var(--mark-background-color)" } : {}),
|
||||
}}
|
||||
>
|
||||
{editable ? (
|
||||
<InlineEdit
|
||||
value={value}
|
||||
highlight={highlight}
|
||||
onChange={(newValue: string) =>
|
||||
onChange(index, newValue, Number(value))
|
||||
}
|
||||
onFocus={() => onFocus(index)}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ color: "var(--text-color)" }}>{value}</span>
|
||||
)}
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Memory = forwardRef(
|
||||
(
|
||||
{
|
||||
name = "Memory",
|
||||
className,
|
||||
displayEnabled = true,
|
||||
highlight = -1,
|
||||
editable = true,
|
||||
memory,
|
||||
format = "dec",
|
||||
onSetFormat,
|
||||
excludedFormats = [],
|
||||
count,
|
||||
maxSize,
|
||||
offset,
|
||||
initialAddr,
|
||||
cellLabels,
|
||||
fileSelect,
|
||||
showClear = true,
|
||||
onChange = undefined,
|
||||
onClear = undefined,
|
||||
loadTooltip = undefined,
|
||||
}: {
|
||||
name?: string;
|
||||
className?: string;
|
||||
displayEnabled?: boolean;
|
||||
editable?: boolean;
|
||||
highlight?: number;
|
||||
memory: MemoryAdapter;
|
||||
count?: number;
|
||||
maxSize?: number;
|
||||
offset?: number;
|
||||
initialAddr?: number;
|
||||
format: Format;
|
||||
onSetFormat?: (format: Format) => void;
|
||||
excludedFormats?: Format[];
|
||||
cellLabels?: string[];
|
||||
fileSelect?: () => Promise<{ name: string; content: string }>;
|
||||
showClear?: boolean;
|
||||
onChange?: () => void;
|
||||
onClear?: () => void;
|
||||
loadTooltip?: { value: string; placement: string };
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [fmt, setFormat] = useStateInitializer(format);
|
||||
const [jmp, setJmp] = useState("");
|
||||
const [goto, setGoto] = useState({ value: initialAddr ?? 0 });
|
||||
const [highlighted, setHighlighted] = useStateInitializer(highlight);
|
||||
const [renderKey, setRenderKey] = useState(0);
|
||||
|
||||
const jumpTo = () => {
|
||||
const value =
|
||||
!isNaN(parseInt(jmp)) && isFinite(parseInt(jmp)) ? Number(jmp) : 0;
|
||||
setHighlighted(value);
|
||||
setGoto({
|
||||
value: value,
|
||||
});
|
||||
rerenderMemoryBlock();
|
||||
};
|
||||
|
||||
const doLoad = async () => {
|
||||
onChange?.();
|
||||
if (fileSelect) {
|
||||
const { name, content } = await fileSelect();
|
||||
setStatus(LOADING);
|
||||
requestAnimationFrame(async () => {
|
||||
const loader = name.endsWith("hack")
|
||||
? loadHack
|
||||
: name.endsWith("asm")
|
||||
? loadAsm
|
||||
: loadBlob;
|
||||
requestAnimationFrame(async () => {
|
||||
try {
|
||||
const bytes = await loader(content);
|
||||
memory.loadBytes(bytes);
|
||||
setStatus("");
|
||||
setFormat(
|
||||
name.endsWith("hack")
|
||||
? "bin"
|
||||
: name.endsWith("asm")
|
||||
? "asm"
|
||||
: fmt,
|
||||
);
|
||||
jumpTo();
|
||||
} catch (e) {
|
||||
setStatus({
|
||||
message: `Error loading memory: ${(e as Error).message}`,
|
||||
severity: "ERROR",
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { setStatus } = useContext(BaseContext);
|
||||
|
||||
const rerenderMemoryBlock = () => {
|
||||
setRenderKey(renderKey + 1);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
rerender: rerenderMemoryBlock,
|
||||
}));
|
||||
|
||||
const clear = () => {
|
||||
memory.reset();
|
||||
onChange?.();
|
||||
onClear?.();
|
||||
rerenderMemoryBlock();
|
||||
};
|
||||
|
||||
const doUpdate = (i: number, v: string) => {
|
||||
memory.update(i, v, fmt ?? "dec");
|
||||
onChange?.();
|
||||
rerenderMemoryBlock();
|
||||
};
|
||||
|
||||
useClockReset(() => {
|
||||
setJmp("");
|
||||
setGoto({ value: 0 });
|
||||
});
|
||||
|
||||
const doSetFormat = (format: Format) => {
|
||||
setFormat(format);
|
||||
onSetFormat?.(format);
|
||||
};
|
||||
|
||||
return (
|
||||
<article className={`panel memory ${className ?? name}`}>
|
||||
<header>
|
||||
<div style={{ whiteSpace: "nowrap" }}>{name}</div>
|
||||
<fieldset role="group">
|
||||
{fileSelect && (
|
||||
<button
|
||||
onClick={doLoad}
|
||||
className="flex-0"
|
||||
data-tooltip={loadTooltip?.value ?? "Load file"}
|
||||
data-placement={loadTooltip?.placement ?? "bottom"}
|
||||
>
|
||||
{/* <Icon name="upload_file" /> */}
|
||||
📂
|
||||
</button>
|
||||
)}
|
||||
{showClear && (
|
||||
<button
|
||||
onClick={clear}
|
||||
className="flex-0"
|
||||
data-tooltip={"Clear"}
|
||||
data-placement="bottom"
|
||||
>
|
||||
{/* <Icon name="upload_file" /> */}
|
||||
🆑
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
style={{ width: "4em", height: "100%" }}
|
||||
placeholder="Addr"
|
||||
value={jmp}
|
||||
onKeyDown={({ key }) => key === "Enter" && jumpTo()}
|
||||
onChange={({ target: { value } }) => setJmp(value)}
|
||||
/>
|
||||
<button
|
||||
onClick={jumpTo}
|
||||
className="flex-0"
|
||||
data-tooltip={"Scroll to address"}
|
||||
data-placement="bottom"
|
||||
>
|
||||
{/* <Icon name="move_down" /> */}
|
||||
⤵️
|
||||
</button>
|
||||
<select value={fmt} onChange={(e) => doSetFormat(e.target.value)}>
|
||||
{FORMATS.filter(
|
||||
(option) => !excludedFormats.includes(option),
|
||||
).map((option) => (
|
||||
<option key={option}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
</fieldset>
|
||||
</header>
|
||||
{displayEnabled ? (
|
||||
<MemoryBlock
|
||||
key={renderKey}
|
||||
jmp={goto}
|
||||
memory={memory}
|
||||
highlight={highlighted}
|
||||
editable={editable}
|
||||
justifyLeft={fmt == "asm"}
|
||||
count={count}
|
||||
format={(v: number) => doFormat(fmt, v)}
|
||||
cellLabels={cellLabels}
|
||||
maxSize={maxSize}
|
||||
offset={offset}
|
||||
onChange={doUpdate}
|
||||
onFocus={(i) => setHighlighted(i)}
|
||||
/>
|
||||
) : (
|
||||
"Memory display is disabled"
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
},
|
||||
);
|
||||
Memory.displayName = "Memory";
|
||||
|
||||
export default Memory;
|
||||
|
||||
function doFormat(format: Format, v: number): string {
|
||||
switch (format) {
|
||||
case "bin":
|
||||
return bin(v);
|
||||
case "hex":
|
||||
return hex(v);
|
||||
case "asm":
|
||||
return asm(v);
|
||||
case "dec":
|
||||
default:
|
||||
return dec(v);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { dec } from "@nand2tetris/simulator/util/twos.js";
|
||||
|
||||
export const RegisterComponent = ({
|
||||
name,
|
||||
bits,
|
||||
}: {
|
||||
name: string;
|
||||
bits: number;
|
||||
}) => (
|
||||
<div>
|
||||
{name}: {dec(bits)}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,153 @@
|
||||
import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { Memory } from "@nand2tetris/simulator/cpu/memory.js";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useClockFrame, useClockReset } from "../clockface.js";
|
||||
|
||||
const WHITE = "white";
|
||||
const BLACK = "black";
|
||||
type COLOR = typeof WHITE | typeof BLACK;
|
||||
|
||||
export interface ScreenMemory {
|
||||
get(idx: number): number;
|
||||
}
|
||||
|
||||
export function reduceScreen(memory: Memory, offset = 0): ScreenMemory {
|
||||
return {
|
||||
get(idx: number): number {
|
||||
return memory.get(offset + idx);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function get(mem: ScreenMemory, x: number, y: number): COLOR {
|
||||
const byte = mem.get(32 * y + ((x / 16) | 0));
|
||||
const bit = byte & (1 << x % 16);
|
||||
return bit === 0 ? WHITE : BLACK;
|
||||
}
|
||||
|
||||
function set(data: Uint8ClampedArray, x: number, y: number, value: COLOR) {
|
||||
const pixel = (y * 512 + x) * 4;
|
||||
const color = value === WHITE ? 255 : 0;
|
||||
data[pixel] = color;
|
||||
data[pixel + 1] = color;
|
||||
data[pixel + 2] = color;
|
||||
data[pixel + 3] = 255;
|
||||
}
|
||||
|
||||
function drawImage(ctx: CanvasRenderingContext2D, memory: ScreenMemory) {
|
||||
const image = assertExists(
|
||||
ctx.getImageData(0, 0, 512, 256),
|
||||
"Failed to create Context2d",
|
||||
);
|
||||
for (let col = 0; col < 512; col++) {
|
||||
for (let row = 0; row < 256; row++) {
|
||||
const color = get(memory, col, row);
|
||||
set(image.data, col, row, color);
|
||||
}
|
||||
}
|
||||
ctx.putImageData(image, 0, 0);
|
||||
}
|
||||
|
||||
export type ScreenScales = 0 | 1 | 2;
|
||||
|
||||
export const Screen = ({
|
||||
memory,
|
||||
showScaleControls = false,
|
||||
scale = 1,
|
||||
onScale,
|
||||
}: {
|
||||
memory: ScreenMemory;
|
||||
showScaleControls?: boolean;
|
||||
scale?: ScreenScales;
|
||||
onScale?: (scale: ScreenScales) => void;
|
||||
}) => {
|
||||
const canvas = useRef<HTMLCanvasElement>();
|
||||
const [screenScale, setScreenScale] = useState<ScreenScales>(scale);
|
||||
|
||||
const onScaleCB = (scale: ScreenScales) => {
|
||||
onScale?.(scale);
|
||||
setScreenScale(scale);
|
||||
};
|
||||
|
||||
const draw = useCallback(() => {
|
||||
const ctx =
|
||||
canvas.current?.getContext("2d", { willReadFrequently: true }) ??
|
||||
undefined;
|
||||
|
||||
if (ctx) {
|
||||
drawImage(ctx, memory);
|
||||
}
|
||||
}, [memory]);
|
||||
|
||||
const ctxRef = useCallback(
|
||||
(ref: HTMLCanvasElement | null) => {
|
||||
canvas.current = ref ?? undefined;
|
||||
draw();
|
||||
},
|
||||
[canvas, draw],
|
||||
);
|
||||
|
||||
useClockFrame(draw);
|
||||
useClockReset(() => {
|
||||
canvas.current
|
||||
?.getContext("2d")
|
||||
?.clearRect(0, 0, canvas.current.width, canvas.current.height);
|
||||
});
|
||||
|
||||
return (
|
||||
<article className="panel">
|
||||
<header>
|
||||
<div>Screen</div>
|
||||
{showScaleControls && (
|
||||
<fieldset role="group">
|
||||
<button
|
||||
aria-current={screenScale === 0}
|
||||
onClick={() => onScaleCB(0)}
|
||||
>
|
||||
x0
|
||||
</button>
|
||||
<button
|
||||
aria-current={screenScale === 1}
|
||||
onClick={() => onScaleCB(1)}
|
||||
>
|
||||
x1
|
||||
</button>
|
||||
<button
|
||||
aria-current={screenScale === 2}
|
||||
onClick={() => onScaleCB(2)}
|
||||
>
|
||||
x2
|
||||
</button>
|
||||
</fieldset>
|
||||
)}
|
||||
</header>
|
||||
{screenScale > 0 && (
|
||||
<main style={{ backgroundColor: "var(--code-background-color)" }}>
|
||||
<figure
|
||||
style={{
|
||||
width: `${512 * screenScale}px`,
|
||||
height: `${256 * screenScale}px`,
|
||||
boxSizing: "content-box",
|
||||
marginInline: "auto",
|
||||
margin: "auto",
|
||||
borderTop: "2px solid gray",
|
||||
borderLeft: "2px solid gray",
|
||||
borderBottom: "2px solid lightgray",
|
||||
borderRight: "2px solid lightgray",
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
ref={ctxRef}
|
||||
width={512}
|
||||
height={256}
|
||||
style={{
|
||||
transform: `translate(-50%, -50%) scale(${screenScale}) translate(50%, 50%)`,
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
></canvas>
|
||||
</figure>
|
||||
</main>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ALU } from "@nand2tetris/simulator/chip/builtins/index.js";
|
||||
import { Chip } from "@nand2tetris/simulator/chip/chip.js";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { makeVisualization, makeVisualizationsWithId } from "./visualizations";
|
||||
|
||||
describe("visualizations", () => {
|
||||
it("returns empty for chips with no parts", () => {
|
||||
const chip = new Chip([], [], "test");
|
||||
|
||||
expect(makeVisualization(chip)).toBeUndefined();
|
||||
expect(makeVisualizationsWithId({ parts: [chip] })).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns vis for builtins", async () => {
|
||||
const alu = new ALU();
|
||||
|
||||
const vis = makeVisualizationsWithId({ parts: [alu] });
|
||||
expect(vis.length).toBe(1);
|
||||
render(
|
||||
<>
|
||||
{vis.map(([k, v]) => (
|
||||
<div key={k}>{v}</div>
|
||||
))}
|
||||
</>,
|
||||
);
|
||||
const rendered = await screen.findAllByText(/ALU/);
|
||||
expect(rendered).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import {
|
||||
CPU,
|
||||
Computer,
|
||||
Keyboard,
|
||||
ROM32K,
|
||||
Screen,
|
||||
} from "@nand2tetris/simulator/chip/builtins/computer/computer.js";
|
||||
import { ALU } from "@nand2tetris/simulator/chip/builtins/index.js";
|
||||
import {
|
||||
PC,
|
||||
Register,
|
||||
} from "@nand2tetris/simulator/chip/builtins/sequential/bit.js";
|
||||
import {
|
||||
RAM,
|
||||
RAM8,
|
||||
} from "@nand2tetris/simulator/chip/builtins/sequential/ram.js";
|
||||
import { Chip, HIGH } from "@nand2tetris/simulator/chip/chip.js";
|
||||
import { Flags } from "@nand2tetris/simulator/cpu/alu.js";
|
||||
import { decode } from "@nand2tetris/simulator/cpu/cpu.js";
|
||||
import { ReactElement } from "react";
|
||||
import { NO_SCREEN } from "../stores/chip.store.js";
|
||||
import { ALUComponent } from "./alu.js";
|
||||
import { Keyboard as KeyboardComponent } from "./keyboard.js";
|
||||
import { Memory as MemoryComponent } from "./memory.js";
|
||||
import { RegisterComponent } from "./register.js";
|
||||
import { Screen as ScreenComponent } from "./screen.js";
|
||||
|
||||
export function getBuiltinVisualization(part: Chip): ReactElement | undefined {
|
||||
switch (part.name) {
|
||||
case "Register":
|
||||
case "ARegister":
|
||||
case "DRegister":
|
||||
case "PC":
|
||||
case "KEYBOARD":
|
||||
case "RAM8":
|
||||
case "RAM64":
|
||||
case "RAM512":
|
||||
case "RAM4K":
|
||||
case "RAM16K":
|
||||
case "ROM32K":
|
||||
case "Screen":
|
||||
case "Memory":
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function makeMemoryVisualization(chip: RAM) {
|
||||
return (
|
||||
<MemoryComponent
|
||||
name={chip.name}
|
||||
memory={chip.memory}
|
||||
format={chip instanceof ROM32K ? "asm" : "dec"}
|
||||
highlight={chip.address}
|
||||
count={5}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function makeVisualization(
|
||||
chip: Chip,
|
||||
updateAction?: () => void,
|
||||
parameters?: Set<string>,
|
||||
): ReactElement | undefined {
|
||||
if (chip instanceof ALU) {
|
||||
return (
|
||||
<ALUComponent
|
||||
A={chip.in("x").busVoltage}
|
||||
op={chip.op()}
|
||||
D={chip.in("y").busVoltage}
|
||||
out={chip.out().busVoltage}
|
||||
flag={
|
||||
(chip.out("zr").voltage() === HIGH
|
||||
? Flags.Zero
|
||||
: chip.out("ng").voltage() === HIGH
|
||||
? Flags.Negative
|
||||
: Flags.Positive) as keyof typeof Flags
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (chip instanceof Register) {
|
||||
return (
|
||||
<RegisterComponent
|
||||
name={chip.name ?? `Chip ${chip.id}`}
|
||||
bits={chip.bits}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (chip instanceof PC) {
|
||||
return <RegisterComponent name="PC" bits={chip.bits} />;
|
||||
}
|
||||
if (chip instanceof Keyboard) {
|
||||
return <KeyboardComponent keyboard={chip} update={updateAction} />;
|
||||
}
|
||||
if (chip instanceof Screen) {
|
||||
return <ScreenComponent memory={chip.memory} />;
|
||||
}
|
||||
if (chip instanceof RAM) {
|
||||
return makeMemoryVisualization(chip);
|
||||
}
|
||||
if (chip instanceof RAM8) {
|
||||
return <span>RAM {chip.width}</span>;
|
||||
}
|
||||
if (chip instanceof CPU) {
|
||||
const bits = decode(chip.in("instruction").busVoltage);
|
||||
return (
|
||||
<>
|
||||
<RegisterComponent name={"A"} bits={chip.state.A} />
|
||||
<RegisterComponent name={"D"} bits={chip.state.D} />
|
||||
<RegisterComponent name={"PC"} bits={chip.state.PC} />
|
||||
<ALUComponent
|
||||
A={bits.am ? chip.in("inM").busVoltage : chip.state.A}
|
||||
D={chip.state.D}
|
||||
out={chip.state.ALU}
|
||||
op={bits.op}
|
||||
flag={chip.state.flag as keyof typeof Flags}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (chip instanceof Computer) {
|
||||
return (
|
||||
<>
|
||||
<RegisterComponent name={"A"} bits={chip.cpu.state.A} />
|
||||
<RegisterComponent name={"D"} bits={chip.cpu.state.D} />
|
||||
<RegisterComponent name={"PC"} bits={chip.cpu.state.PC} />
|
||||
{!parameters?.has(NO_SCREEN) && (
|
||||
<ScreenComponent memory={chip.ram.screen.memory} />
|
||||
)}
|
||||
{makeMemoryVisualization(chip.rom)}
|
||||
{makeMemoryVisualization(chip.ram.ram)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const vis = [...chip.parts]
|
||||
.map((chip) => makeVisualization(chip, updateAction))
|
||||
.filter((v) => v !== undefined);
|
||||
return vis.length > 0 ? <>{vis}</> : undefined;
|
||||
}
|
||||
|
||||
export function makeVisualizationsWithId(
|
||||
chip: {
|
||||
parts: Chip[];
|
||||
},
|
||||
updateAction?: () => void,
|
||||
parameters?: Set<string>,
|
||||
): [string, ReactElement][] {
|
||||
return [...chip.parts]
|
||||
.map((part, i): [string, ReactElement | undefined] => [
|
||||
`${part.id}_${i}`,
|
||||
makeVisualization(part, updateAction, parameters),
|
||||
])
|
||||
.filter(([_, v]) => v !== undefined) as [string, ReactElement][];
|
||||
}
|
||||
Reference in New Issue
Block a user