feat(editor): F4.6a simulation mode and a computer's desktop

Java's mode buttons start and stop the room, now in a worker of the
editor's page (LocalSimHost), with Java's speed slider. While the
simulation runs the design is locked as in Java: no palette, no sheet,
nothing moves, no undo. Cables that carry a frame light up in Java's
green, and a tap on a computer opens its desktop: Java's title, the
software installation first, then the installed programs in the order
of Java's list for the language, a taskbar with the network icon and
the network as the simulation runs it. The room sends that over a new
topic, node:<id>. Desktops stay over a return to the design and show
again at the next start. The programs' windows follow from F4.6b.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 13:41:41 +00:00
co-authored by Claude Opus 5.5
parent 6e97e3565d
commit dfd5ec03ce
44 changed files with 1190 additions and 41 deletions
+26 -1
View File
@@ -4,7 +4,7 @@
// so that it can run on every change while someone drags a device.
import * as Y from 'yjs';
import { compareIds, stableGeneratedMacAddress, type DocumentationItemKind, type HostLabelPolicy, type NodeKind } from '@mdg-filius/core';
import { compareIds, stableGeneratedMacAddress, type DocumentationItemKind, type HostLabelPolicy, type InstallableProgram, type NodeKind } from '@mdg-filius/core';
import { inOrder, ROOTS } from './projectDoc.js';
export interface NodeView {
@@ -115,3 +115,28 @@ export function topologyView(doc: Y.Doc): TopologyView {
docs.sort((left, right) => layer(left) - layer(right) || left.order - right.order || compareIds(left.id, right.id));
return { nodes, links, docs };
}
/** What a computer's desktop needs of the document (Java's GUIDesktopWindow, GUIDesktopPanel). */
export interface DesktopView {
readonly kind: 'pc' | 'notebook';
/** Its label (Host.holeAnzeigeName). */
readonly label: string;
/** Whether the label is its address: Java's title then leaves the address out (isUseIPAsName). */
readonly addressAsName: boolean;
readonly programs: readonly InstallableProgram[];
}
/** A PC's or notebook's desktop; undefined for any other device. */
export function desktopView(doc: Y.Doc, id: string): DesktopView | undefined {
const node = doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get(id);
const kind = node?.get('kind') as NodeKind | undefined;
if (node === undefined || (kind !== 'pc' && kind !== 'notebook')) return undefined;
const policy = node.get('labelPolicy') as HostLabelPolicy;
return {
kind,
label: nodeLabel(node),
addressAsName: policy === 'ipAddress' || policy === 'ipAndMAC',
programs: [...(node.get('programs') as Y.Map<unknown>).keys()] as InstallableProgram[],
};
}
+36 -1
View File
@@ -10,7 +10,7 @@ import { describe, expect, it } from 'vitest';
import * as Y from 'yjs';
import { stableGeneratedMacAddress } from '@mdg-filius/core';
import { exportProject, openProjectArchive, type ImportedProject } from '@mdg-filius/fls';
import { readProject, topologyView, writeProject } from '../src/index.js';
import { addDevice, desktopView, readProject, setHostLabels, topologyView, writeProject } from '../src/index.js';
const corpus = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus');
const files = ['java-examples', 'java-tests', 'ipad-fixtures'].flatMap((folder) =>
@@ -70,3 +70,38 @@ describe('the drawing of a document', () => {
expect(switchLabel).toBe('Switch 1');
});
});
describe("a computer's desktop", () => {
it('names its programs and its label, and whether the label is its address, for computers only', () => {
for (const file of files) {
const doc = docOf(file);
const project = readProject(doc);
for (const node of project.graph.nodes) {
const desktop = desktopView(doc, node.id);
if (node.kind !== 'pc' && node.kind !== 'notebook') {
expect(desktop, `${file} ${node.displayName}`).toBeUndefined();
continue;
}
expect(new Set(desktop!.programs), `${file} ${node.displayName}`).toEqual(project.installedPrograms.get(node.id) ?? new Set());
expect(desktop!.kind).toBe(node.kind);
expect(desktop!.label).toBe(topologyView(doc).nodes.find((each) => each.id === node.id)!.label);
expect(desktop!.addressAsName).toBe(node.hostLabelPolicy === 'ipAddress' || node.hostLabelPolicy === 'ipAndMAC');
}
}
expect(desktopView(docOf(files[0]!), 'nobody')).toBeUndefined();
});
it("takes a label of address and MAC address for the address, as Java's title does (isUseIPAsName), the MAC address alone not", () => {
const doc = new Y.Doc();
let counter = 0;
addDevice(doc, { kind: 'pc', id: 'pc', name: 'PC', x: 0, y: 0, newId: () => `port-${++counter}` }, 'me');
expect(desktopView(doc, 'pc')).toMatchObject({ kind: 'pc', label: 'PC', addressAsName: false, programs: [] });
setHostLabels(doc, 'pc', { ipAddress: true, macAddress: true }, 'me');
expect(desktopView(doc, 'pc')!.addressAsName).toBe(true);
setHostLabels(doc, 'pc', { ipAddress: false, macAddress: true }, 'me');
expect(desktopView(doc, 'pc')!.addressAsName).toBe(false);
setHostLabels(doc, 'pc', { ipAddress: true, macAddress: false }, 'me');
expect(desktopView(doc, 'pc')).toMatchObject({ label: '192.168.0.10', addressAsName: true });
});
});