feat(editor): a modem offers a code or joins one, and says under it how it stands (K4, K5)
The modem's sheet chooses how it finds its partner: the other modem on its port in this project, as FILIUS' file pairs them; another project's, by offering this modem's code (shown large, with a copy button) or by joining a code typed in as one reads it off the board. In the action mode each modem says under it how it stands — connected, waiting, taken, no second modem on the port, cable missing, on the server only. Accepted in three browsers on a real F, each in a copy of FILIUS' modem example: Ben's notebook pings the computer behind Mia's access point; Rita joins the same code, reads that it is taken, and gets it once Ben stops. FILIUS' modem example runs in the page's own room as well. The .fls export writes an offering modem as Java's server and a joining one as its client; the code has no place there and is named as lost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
// (moving.ts).
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
||||
import type { ModemStatus } from '@mdg-filius/protocol';
|
||||
import type * as Y from 'yjs';
|
||||
import {
|
||||
addDevice,
|
||||
@@ -51,7 +52,7 @@ import {
|
||||
type TopologyView,
|
||||
} from '@mdg-filius/doc';
|
||||
import { DOCUMENTATION_NEW_SIZE, type DocumentationItemKind } from '@mdg-filius/core';
|
||||
import { Canvas, type CanvasHandle, type Selection, type Side } from './canvas/Canvas.js';
|
||||
import { Canvas, type CanvasHandle, type DeviceNote, type Selection, type Side } from './canvas/Canvas.js';
|
||||
import type { Point } from './canvas/viewport.js';
|
||||
import { ContextMenu, type MenuItem } from './design/ContextMenu.js';
|
||||
import { DocumentationMenu } from './design/DocumentationMenu.js';
|
||||
@@ -182,14 +183,32 @@ export interface EditorProps {
|
||||
readonly frozen?: boolean;
|
||||
/** The others in the project and how to tell them this editor's state (on F). */
|
||||
readonly presence?: Presence;
|
||||
/** The project on F, whose modems offer codes to other projects; none on the development page. */
|
||||
readonly project?: string;
|
||||
}
|
||||
|
||||
export function Editor({ doc, createSimHost, projectName, readOnly = false, frozen = false, presence }: EditorProps) {
|
||||
/** How a modem's standing reads under it: good news, a wait, or trouble. */
|
||||
const MODEM_TONES: Readonly<Record<ModemStatus, DeviceNote['tone']>> = {
|
||||
connected: 'good',
|
||||
waiting: 'waiting',
|
||||
taken: 'trouble',
|
||||
unpaired: 'trouble',
|
||||
ambiguous: 'trouble',
|
||||
detached: 'trouble',
|
||||
unavailable: 'trouble',
|
||||
};
|
||||
|
||||
export function Editor({ doc, createSimHost, projectName, readOnly = false, frozen = false, presence, project }: EditorProps) {
|
||||
const t = useTranslate();
|
||||
const language = useLanguage();
|
||||
const view = useTopologyView(doc);
|
||||
const simulation = useSimulation(doc, createSimHost);
|
||||
const simulating = simulation.mode === 'simulation';
|
||||
// How each modem stands, under its label, while the simulation runs.
|
||||
const modemNotes = useMemo(
|
||||
() => new Map([...simulation.modems].map(([id, status]) => [id, { text: t(`modem.status.${status}`), tone: MODEM_TONES[status] }] as const)),
|
||||
[simulation.modems, t],
|
||||
);
|
||||
const [documenting, setDocumenting] = useState(false);
|
||||
// The simulation comes first: started, it ends the documentation mode (changeMode).
|
||||
const editorMode: EditorMode = simulating ? 'simulation' : documenting ? 'documentation' : 'design';
|
||||
@@ -516,6 +535,7 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz
|
||||
);
|
||||
}}
|
||||
activeLinks={simulation.activeLinks}
|
||||
notes={modemNotes}
|
||||
onMenu={(target, at) => {
|
||||
if (readOnly && !simulating) return;
|
||||
if (editorMode === 'documentation') {
|
||||
@@ -683,6 +703,7 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz
|
||||
onHighlight={setHighlightedLink}
|
||||
onClose={() => setSelection(null)}
|
||||
readOnly={readOnly}
|
||||
{...(project === undefined ? {} : { project })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -107,9 +107,22 @@ export interface CanvasProps {
|
||||
readonly renderDocText?: (item: DocumentationView, frame: ScreenFrame) => React.ReactNode;
|
||||
/** The cables that carry a frame now, drawn as Java draws them then (JCablePanel.farbeBlinken). */
|
||||
readonly activeLinks?: ReadonlySet<string>;
|
||||
/** A line under a device's label, by node (how a modem stands while the simulation runs). */
|
||||
readonly notes?: ReadonlyMap<string, DeviceNote>;
|
||||
}
|
||||
|
||||
/** A line under a device's label: its text, and whether it is good news, a wait or trouble. */
|
||||
export interface DeviceNote {
|
||||
readonly text: string;
|
||||
readonly tone: 'good' | 'waiting' | 'trouble';
|
||||
}
|
||||
|
||||
const NOTE_COLORS: Readonly<Record<DeviceNote['tone'], string>> = { good: 'var(--accent)', waiting: 'var(--text-secondary)', trouble: 'var(--danger)' };
|
||||
/** How far the note sits below the label's baseline. */
|
||||
const NOTE_LINE = 14;
|
||||
|
||||
const NO_LINKS: ReadonlySet<string> = new Set();
|
||||
const NO_NOTES: ReadonlyMap<string, DeviceNote> = new Map();
|
||||
const NO_CHOICES: ReadonlyMap<string, readonly string[]> = new Map();
|
||||
const NO_COLORS: readonly string[] = [];
|
||||
|
||||
@@ -195,6 +208,7 @@ function Device({
|
||||
selected,
|
||||
pending,
|
||||
chosenBy,
|
||||
note,
|
||||
}: {
|
||||
readonly node: NodeView;
|
||||
readonly box: DeviceBox;
|
||||
@@ -203,6 +217,7 @@ function Device({
|
||||
readonly pending: boolean;
|
||||
/** The colours of the others who have chosen it, each a frame further out. */
|
||||
readonly chosenBy: readonly string[];
|
||||
readonly note: DeviceNote | undefined;
|
||||
}) {
|
||||
return (
|
||||
<g
|
||||
@@ -211,7 +226,7 @@ function Device({
|
||||
data-box={`${box.x} ${box.y} ${box.width} ${box.height}`}
|
||||
data-selected={selected ? 'true' : undefined}
|
||||
role="img"
|
||||
aria-label={`${kindName}: ${node.label}`}
|
||||
aria-label={note === undefined ? `${kindName}: ${node.label}` : `${kindName}: ${node.label} – ${note.text}`}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{/* The whole button takes the pointer, as Java's label does. */}
|
||||
@@ -249,6 +264,11 @@ function Device({
|
||||
<text x={box.label.x} y={box.label.y} textAnchor="middle" style={{ font: JAVA_LABEL_FONT, fill: 'var(--canvas-label)' }}>
|
||||
{node.label}
|
||||
</text>
|
||||
{note === undefined ? null : (
|
||||
<text data-note={note.tone} x={box.label.x} y={box.label.y + NOTE_LINE} textAnchor="middle" style={{ font: JAVA_LABEL_FONT, fill: NOTE_COLORS[note.tone] }}>
|
||||
{note.text}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
@@ -288,6 +308,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
locked = false,
|
||||
chosenBy = NO_CHOICES,
|
||||
activeLinks = NO_LINKS,
|
||||
notes = NO_NOTES,
|
||||
onDocFrame,
|
||||
renderDocText,
|
||||
},
|
||||
@@ -741,6 +762,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
selected={selection?.kind === 'device' && selection.id === node.id}
|
||||
chosenBy={chosenBy.get(node.id) ?? NO_COLORS}
|
||||
pending={cableFrom?.id === node.id}
|
||||
note={notes.get(node.id)}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
|
||||
@@ -331,6 +331,25 @@
|
||||
"mode.documentation": "Wechselt in den Dokumentationsmodus",
|
||||
"mode.simulation": "Wechselt in den Aktionsmodus",
|
||||
"mode.speed": "Simulationsgeschwindigkeit anpassen",
|
||||
"modem.code": "Code dieses Modems",
|
||||
"modem.connection": "Verbindung",
|
||||
"modem.connection.inProject": "Mit einem Modem in diesem Projekt (gleicher Port)",
|
||||
"modem.connection.join": "Mit einem anderen Projekt: mit Code verbinden",
|
||||
"modem.connection.offer": "Mit einem anderen Projekt: Verbindung anbieten",
|
||||
"modem.copied": "Kopiert ✓",
|
||||
"modem.copy": "Code kopieren",
|
||||
"modem.copyFailed": "Der Code ließ sich nicht kopieren.",
|
||||
"modem.joinHint": "Den Code zeigt das Modem im anderen Projekt. Die Verbindung steht, sobald beide Simulationen laufen.",
|
||||
"modem.offerHint": "Geben Sie den Code an die andere Gruppe weiter. Ihr Modem verbindet sich, sobald beide Simulationen laufen.",
|
||||
"modem.partnerCode": "Code des anderen Modems",
|
||||
"modem.serverOnly": "Verbindungen zu anderen Projekten gibt es nur in Projekten auf dem Server.",
|
||||
"modem.status.ambiguous": "mehr als zwei Modems mit diesem Port",
|
||||
"modem.status.connected": "verbunden",
|
||||
"modem.status.detached": "Kabel fehlt",
|
||||
"modem.status.taken": "Code schon belegt",
|
||||
"modem.status.unavailable": "nur auf dem Server",
|
||||
"modem.status.unpaired": "kein zweites Modem mit diesem Port",
|
||||
"modem.status.waiting": "wartet auf Gegenstelle",
|
||||
"network.dnsServer": "DNS-Server:",
|
||||
"network.gateway": "Gateway:",
|
||||
"network.ipAddress": "IP-Adresse:",
|
||||
|
||||
@@ -331,6 +331,25 @@
|
||||
"mode.documentation": "Change to Documentation Mode",
|
||||
"mode.simulation": "Change to Simulation Mode",
|
||||
"mode.speed": "Adjust Simulation Velocity",
|
||||
"modem.code": "Code of this modem",
|
||||
"modem.connection": "Connection",
|
||||
"modem.connection.inProject": "With a modem in this project (same port)",
|
||||
"modem.connection.join": "With another project: connect with a code",
|
||||
"modem.connection.offer": "With another project: offer a connection",
|
||||
"modem.copied": "Copied ✓",
|
||||
"modem.copy": "Copy code",
|
||||
"modem.copyFailed": "The code could not be copied.",
|
||||
"modem.joinHint": "The modem in the other project shows the code. The connection stands as soon as both simulations run.",
|
||||
"modem.offerHint": "Pass the code on to the other group. Your modem connects as soon as both simulations run.",
|
||||
"modem.partnerCode": "Code of the other modem",
|
||||
"modem.serverOnly": "Connections to other projects exist only in projects on the server.",
|
||||
"modem.status.ambiguous": "more than two modems with this port",
|
||||
"modem.status.connected": "connected",
|
||||
"modem.status.detached": "cable missing",
|
||||
"modem.status.taken": "code already taken",
|
||||
"modem.status.unavailable": "on the server only",
|
||||
"modem.status.unpaired": "no second modem with this port",
|
||||
"modem.status.waiting": "waiting for the other side",
|
||||
"network.dnsServer": "DNS server:",
|
||||
"network.gateway": "Gateway:",
|
||||
"network.ipAddress": "IP address:",
|
||||
|
||||
@@ -26,11 +26,13 @@ type Ready = Extract<SharedDocState, { kind: 'ready' }>;
|
||||
|
||||
/** The editor on the ready document, with the others in the project. */
|
||||
function ReadyEditor({
|
||||
project,
|
||||
state,
|
||||
createSimHost,
|
||||
onPeople,
|
||||
blobs,
|
||||
}: {
|
||||
readonly project: string;
|
||||
readonly state: Ready;
|
||||
readonly createSimHost: SimHostFactory;
|
||||
readonly onPeople: ((people: readonly PresenceUser[]) => void) | undefined;
|
||||
@@ -52,7 +54,7 @@ function ReadyEditor({
|
||||
const reach = useMemo(() => (files === null || writing ? files : { saveFile: files.saveFile }), [files, writing]);
|
||||
return (
|
||||
<DashboardFilesContext.Provider value={reach}>
|
||||
<Editor doc={state.doc} createSimHost={createSimHost} readOnly={state.readOnly} frozen={state.frozen} presence={presence} />
|
||||
<Editor doc={state.doc} project={project} createSimHost={createSimHost} readOnly={state.readOnly} frozen={state.frozen} presence={presence} />
|
||||
</DashboardFilesContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -104,7 +106,7 @@ export function SharedProject({ server, project, onPeople }: SharedProjectProps)
|
||||
{t('remote.uploadFailed')}
|
||||
</p>
|
||||
) : null}
|
||||
<ReadyEditor key={state.doc.guid} state={state} createSimHost={createSimHost} onPeople={onPeople} blobs={blobs} />
|
||||
<ReadyEditor key={state.doc.guid} project={project} state={state} createSimHost={createSimHost} onPeople={onPeople} blobs={blobs} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import type * as Y from 'yjs';
|
||||
import { modemCode, modemCodeText } from '@mdg-filius/core';
|
||||
import {
|
||||
deviceSheet,
|
||||
setDhcpClient,
|
||||
@@ -14,6 +15,8 @@ import {
|
||||
setHostLabels,
|
||||
setHostWireless,
|
||||
setIpForwarding,
|
||||
setModemCode,
|
||||
setModemConnection,
|
||||
setModemPort,
|
||||
setSwitchCloud,
|
||||
setSwitchRetention,
|
||||
@@ -21,6 +24,7 @@ import {
|
||||
type CheckedField,
|
||||
type DeviceSheet as Sheet,
|
||||
type HostSheet,
|
||||
type ModemConnection,
|
||||
type ModemSheet,
|
||||
type SwitchSheet,
|
||||
type WirelessError,
|
||||
@@ -63,6 +67,8 @@ export interface DeviceSheetProps {
|
||||
readonly onHighlight: (link: string | null) => void;
|
||||
/** Who only reads (on F): everything shown, nothing changed (SheetLock). */
|
||||
readonly readOnly?: boolean;
|
||||
/** The project on F (a modem's code is derived from it); none on the development page. */
|
||||
readonly project?: string;
|
||||
}
|
||||
|
||||
/** A page of the sheet that Java opens as a dialog. */
|
||||
@@ -70,7 +76,7 @@ export type SheetPage = 'dhcp' | 'forwarding' | 'firewall' | 'connections';
|
||||
|
||||
const PAGE_TITLES: Readonly<Record<SheetPage, string>> = { dhcp: 'sheet.dhcpServer', forwarding: 'sheet.portForwarding', firewall: 'sheet.firewall', connections: 'sheet.connections' };
|
||||
|
||||
export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onClose, onHighlight, readOnly = false }: DeviceSheetProps) {
|
||||
export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onClose, onHighlight, readOnly = false, project }: DeviceSheetProps) {
|
||||
const t = useTranslate();
|
||||
const sheet = useDeviceSheet(doc, id);
|
||||
// The page open, if any; the editor makes a sheet per device, so another device starts at its fields.
|
||||
@@ -116,7 +122,7 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
|
||||
</SheetLock>
|
||||
) : sheet.kind === 'remoteLink' ? (
|
||||
<SheetLock>
|
||||
<ModemFields sheet={sheet} change={change} />
|
||||
<ModemFields sheet={sheet} change={change} project={project} onMessage={onMessage} />
|
||||
</SheetLock>
|
||||
) : sheet.kind === 'router' ? (
|
||||
<RouterFields sheet={sheet} change={change} onHighlight={onHighlight} onOpen={onOpen} />
|
||||
@@ -264,19 +270,103 @@ function SwitchFields({ sheet, change }: { readonly sheet: SwitchSheet; readonly
|
||||
);
|
||||
}
|
||||
|
||||
function ModemFields({ sheet, change }: { readonly sheet: ModemSheet; readonly change: Change }) {
|
||||
const MODEM_CONNECTIONS: readonly ModemConnection[] = ['inProject', 'offer', 'join'];
|
||||
/** How long the copy button says the code is copied. */
|
||||
const COPIED_MILLISECONDS = 2_000;
|
||||
|
||||
/**
|
||||
* A modem (JModemKonfiguration): its name, and how it finds its partner —
|
||||
* the other modem on its port in this project, as FILIUS' file pairs them;
|
||||
* or a modem of another project, which takes the place of Java's IP address
|
||||
* and port: this one offers a code to pass on, or joins the modem whose code
|
||||
* is typed in (FILIUS_MODEM_PLAN). The code needs server F; on the
|
||||
* development page there is none.
|
||||
*/
|
||||
function ModemFields({
|
||||
sheet,
|
||||
change,
|
||||
project,
|
||||
onMessage,
|
||||
}: {
|
||||
readonly sheet: ModemSheet;
|
||||
readonly change: Change;
|
||||
readonly project: string | undefined;
|
||||
readonly onMessage: (message: string) => void;
|
||||
}) {
|
||||
const t = useTranslate();
|
||||
const code = project === undefined ? undefined : modemCodeText(modemCode(project, change.id));
|
||||
// The button says so for a moment once the code is copied; a failure goes to the editor's message.
|
||||
const [copied, setCopied] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!copied) return;
|
||||
const timer = setTimeout(() => setCopied(false), COPIED_MILLISECONDS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [copied]);
|
||||
const copy = () => {
|
||||
if (code === undefined) return;
|
||||
(navigator.clipboard?.writeText(code) ?? Promise.reject(new Error('no clipboard'))).then(
|
||||
() => setCopied(true),
|
||||
() => onMessage(t('modem.copyFailed')),
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<NameField name={sheet.name} change={change} />
|
||||
<SheetField
|
||||
name="port"
|
||||
label={t('sheet.port')}
|
||||
value={String(sheet.port)}
|
||||
rule="port"
|
||||
inputMode="numeric"
|
||||
onCommit={(text) => change.run(() => setModemPort(change.doc, change.id, text, change.origin))}
|
||||
/>
|
||||
<fieldset className="sheet-group" role="radiogroup" aria-label={t('modem.connection')}>
|
||||
{MODEM_CONNECTIONS.map((connection) => (
|
||||
<label key={connection} className="sheet-check">
|
||||
<input
|
||||
type="radio"
|
||||
name="modemConnection"
|
||||
value={connection}
|
||||
checked={sheet.connection === connection}
|
||||
onChange={() => change.run(() => setModemConnection(change.doc, change.id, connection, change.origin))}
|
||||
/>
|
||||
{t(`modem.connection.${connection}`)}
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
{sheet.connection === 'inProject' ? (
|
||||
<SheetField
|
||||
name="port"
|
||||
label={t('sheet.port')}
|
||||
value={String(sheet.port)}
|
||||
rule="port"
|
||||
inputMode="numeric"
|
||||
onCommit={(text) => change.run(() => setModemPort(change.doc, change.id, text, change.origin))}
|
||||
/>
|
||||
) : sheet.connection === 'offer' ? (
|
||||
code === undefined ? (
|
||||
<p className="sheet-note">{t('modem.serverOnly')}</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="sheet-field">
|
||||
<span>{t('modem.code')}</span>
|
||||
<output className="modem-code" aria-label={t('modem.code')}>
|
||||
{code}
|
||||
</output>
|
||||
</div>
|
||||
<div className="sheet-buttons">
|
||||
<button type="button" onClick={copy} aria-live="polite">
|
||||
{t(copied ? 'modem.copied' : 'modem.copy')}
|
||||
</button>
|
||||
</div>
|
||||
<p className="sheet-note">{t('modem.offerHint')}</p>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<SheetField
|
||||
name="modemCode"
|
||||
label={t('modem.partnerCode')}
|
||||
value={sheet.code === '' ? '' : modemCodeText(sheet.code)}
|
||||
rule="modemCode"
|
||||
unfilled
|
||||
onCommit={(text) => change.run(() => setModemCode(change.doc, change.id, text, change.origin))}
|
||||
/>
|
||||
<p className="sheet-note">{t(project === undefined ? 'modem.serverOnly' : 'modem.joinHint')}</p>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -427,6 +427,15 @@
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
/* A modem's code to pass on: read aloud or copied off the board, so large and spaced. */
|
||||
.modem-code {
|
||||
font-family: ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.sheet-connected,
|
||||
.sheet-note {
|
||||
margin: 8px 0;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// The editor's side of the simulation room (plan §6): its mode and speed,
|
||||
// the cables that carry frames, a computer's network as it runs, the lines
|
||||
// and views of the programs' windows and the actions taken in them, the
|
||||
// cards' frames for the data exchange window. The room
|
||||
// cards' frames for the data exchange window, how each modem stands. The room
|
||||
// runs behind a SimHost — on F for everyone in the project (RemoteSimHost),
|
||||
// or in a worker of this page (LocalSimHost) on the development page; the
|
||||
// editor keeps no simulation of its own.
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type * as Y from 'yjs';
|
||||
import type { AppKind, DroppedUnit, NodeSettings, SimHost, SimMode, Topic, TraceFrame } from '@mdg-filius/protocol';
|
||||
import type { AppKind, DroppedUnit, ModemStatus, NodeSettings, SimHost, SimMode, Topic, TraceFrame } from '@mdg-filius/protocol';
|
||||
import { LocalSimHost } from '@mdg-filius/sim';
|
||||
import SimWorker from '@mdg-filius/sim/worker?worker';
|
||||
import type { Language } from '../i18n/i18n.js';
|
||||
@@ -52,6 +52,8 @@ export interface Simulation {
|
||||
readonly dropped: readonly DroppedUnit[];
|
||||
/** Each watched card's frames as the data exchange window shows them, by its port. */
|
||||
readonly traces: ReadonlyMap<string, readonly TraceFrame[]>;
|
||||
/** How each modem stands while the simulation runs, by node; empty in the design. */
|
||||
readonly modems: ReadonlyMap<string, ModemStatus>;
|
||||
/** Starts in the language the person runs FILIUS in: the routers' pages come in it, as Java's in its locale. */
|
||||
start(language?: Language): void;
|
||||
stop(): void;
|
||||
@@ -71,6 +73,7 @@ export interface Simulation {
|
||||
}
|
||||
|
||||
const NO_LINKS: ReadonlySet<string> = new Set();
|
||||
const NO_MODEMS: ReadonlyMap<string, ModemStatus> = new Map();
|
||||
|
||||
export function useSimulation(doc: Y.Doc, createHost: SimHostFactory = localSimHost): Simulation {
|
||||
const host = useRef<SimHost | null>(null);
|
||||
@@ -89,6 +92,7 @@ export function useSimulation(doc: Y.Doc, createHost: SimHostFactory = localSimH
|
||||
const [clock, setClock] = useState(0);
|
||||
const [dropped, setDropped] = useState<readonly DroppedUnit[]>([]);
|
||||
const [traces, setTraces] = useState<ReadonlyMap<string, readonly TraceFrame[]>>(new Map());
|
||||
const [modems, setModems] = useState<ReadonlyMap<string, ModemStatus>>(NO_MODEMS);
|
||||
|
||||
useEffect(() => {
|
||||
const made = createHost(doc);
|
||||
@@ -133,6 +137,9 @@ export function useSimulation(doc: Y.Doc, createHost: SimHostFactory = localSimH
|
||||
case 'view':
|
||||
setViews((before) => new Map(before).set(`app:${message.node}/${message.app}`, message.view));
|
||||
return;
|
||||
case 'modems':
|
||||
setModems(new Map(Object.entries(message.modems)));
|
||||
return;
|
||||
case 'error':
|
||||
if (message.code === 'engineFailed') setFailed(true);
|
||||
if (message.code === 'readOnly' || message.code === 'frozen') {
|
||||
@@ -217,5 +224,5 @@ export function useSimulation(doc: Y.Doc, createHost: SimHostFactory = localSimH
|
||||
host.current?.send({ v: 1, kind: 'input', node, app, action, args, reqId: crypto.randomUUID() });
|
||||
}, []);
|
||||
|
||||
return { mode, speed, activeLinks, nodes, apps, views, failed, startedBy, refusal, clock, dropped, traces, start, stop, restart, setSpeed, drop, clearCapture, watch, captures, act };
|
||||
return { mode, speed, activeLinks, nodes, apps, views, failed, startedBy, refusal, clock, dropped, traces, modems, start, stop, restart, setSpeed, drop, clearCapture, watch, captures, act };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user