feat(fls): export the editor model's topology to Java's view
exportProject.ts is the counterpart of importProject.ts: from the editor model to the Java view topologyWriter.ts writes. Step X1, the topology: each node becomes the Java device of its kind with name, position and label flags; its ports become interfaces or ports; each link a cable drawn between its nodes; each documentation item a GUIDocuItem. A port's MAC is the one the simulation uses (the file's, or one derived from the port id); a router's gateway and DNS server go to every interface, as Java's own setters do (Java reads the first interface's only). A host with a wireless card joins a switch by a wireless cable, the only wireless connection Java makes. The import now enables a host's wireless by Java's card flag, also without an SSID, where Swift enables it for every SSID. Checked by Java: every corpus file, imported, exported and written, loads in Java FILIUS to the topology of the original up to named departures of the model (a derived MAC where Java rolls one at each load; documentation in render order, with the model's empty text, Dialog 12 and minimum size where Java has none or less). A synthetic model adds a label with IP and MAC, positions between pixels, a five-port switch, a wireless notebook, a three-interface router and a half-transparent bold text. 17 mutation probes, each loaded by Java, were all caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
// From the editor model to Java's view of a .fls file, which
|
||||
// topologyWriter.ts writes — the counterpart of importProject.ts.
|
||||
//
|
||||
// The topology: each node becomes the Java device of its kind, with its name,
|
||||
// position and label policy; its ports become interfaces (PC, notebook,
|
||||
// router, home router) or ports (switch, modem) of Java's device; each link
|
||||
// becomes a cable between those, drawn between its two nodes; each
|
||||
// documentation item a GUIDocuItem. A host whose wireless is enabled has a
|
||||
// wireless card, and its link to a switch is a wireless cable — the only
|
||||
// wireless connection Java makes (JHostKonfiguration.updateWifiConnection). System software, files and programs
|
||||
// follow in later steps.
|
||||
//
|
||||
// What the model keeps is written as it keeps it, and so differs from the
|
||||
// Java file it may have been imported from where the import decides (named
|
||||
// in exportProject.test.ts):
|
||||
// - a port's MAC address is the one the simulation uses: the file's, or
|
||||
// one derived from the port's id (effectiveMacAddress) — stable across
|
||||
// saves, where Java rolls a new one at each load;
|
||||
// - a router's or home router's gateway and DNS server are written to every
|
||||
// interface, as Java's own setters (setStandardGateway, setDNSServer) do:
|
||||
// Java reads them from the first interface only (getStandardGateway,
|
||||
// getDNSServer), and the model keeps no others;
|
||||
// - a documentation item always has a text and a font, bold or plain: where
|
||||
// Java has none, the empty text and Dialog 12 the model gives it.
|
||||
//
|
||||
// Checked by Java: every corpus file, imported, exported and written, loads in
|
||||
// Java FILIUS to the topology of the original up to those departures.
|
||||
|
||||
import {
|
||||
effectiveMacAddress,
|
||||
interfaceKey,
|
||||
type DeviceConfiguration,
|
||||
type DocumentationItem,
|
||||
type HostLabelPolicy,
|
||||
type HostWirelessConfiguration,
|
||||
type Id,
|
||||
type InterfaceConfiguration,
|
||||
type InterfaceKey,
|
||||
type NodeKind,
|
||||
type TopologyGraph,
|
||||
type TopologyNode,
|
||||
} from '@mdg-filius/core';
|
||||
import type { FlsDocumentationItem, FlsInterface, FlsLink, FlsNodeClass } from './topology.js';
|
||||
import type { NodeToWrite, TopologyToWrite } from './topologyWriter.js';
|
||||
|
||||
/** The parts of the editor model the topology is made of. */
|
||||
export interface ProjectToExport {
|
||||
readonly graph: TopologyGraph;
|
||||
readonly deviceConfigurations: ReadonlyMap<Id, DeviceConfiguration>;
|
||||
readonly interfaceConfigurations: ReadonlyMap<InterfaceKey, InterfaceConfiguration>;
|
||||
readonly hostWirelessConfigurations: ReadonlyMap<Id, HostWirelessConfiguration>;
|
||||
readonly documentationItems: readonly DocumentationItem[];
|
||||
}
|
||||
|
||||
export interface ExportedTopology {
|
||||
readonly topology: TopologyToWrite;
|
||||
readonly warnings: string[];
|
||||
}
|
||||
|
||||
const CLASS_OF_KIND: Readonly<Record<Exclude<NodeKind, 'unsupported'>, FlsNodeClass>> = {
|
||||
pc: 'Rechner',
|
||||
notebook: 'Notebook',
|
||||
networkSwitch: 'Switch',
|
||||
router: 'Vermittlungsrechner',
|
||||
gateway: 'Gateway',
|
||||
remoteLink: 'Modem',
|
||||
};
|
||||
|
||||
export function exportProject(project: ProjectToExport): ExportedTopology {
|
||||
const warnings: string[] = [];
|
||||
const exported = project.graph.nodes.filter((node) => {
|
||||
if (node.kind !== 'unsupported') return true;
|
||||
warnings.push(`node '${node.displayName}': a kind Java FILIUS does not have, left out`);
|
||||
return false;
|
||||
});
|
||||
const indexOfNode = new Map(exported.map((node, index) => [node.id, index]));
|
||||
const nodes = exported.map((node) => exportNode(node, project));
|
||||
|
||||
const links: FlsLink[] = [];
|
||||
project.graph.links.forEach((link, index) => {
|
||||
const source = indexOfNode.get(link.sourceNodeId);
|
||||
const target = indexOfNode.get(link.targetNodeId);
|
||||
const sourcePort = source === undefined ? -1 : exported[source]!.ports.findIndex((port) => port.id === link.sourcePortId);
|
||||
const targetPort = target === undefined ? -1 : exported[target]!.ports.findIndex((port) => port.id === link.targetPortId);
|
||||
if (source === undefined || target === undefined || sourcePort < 0 || targetPort < 0) {
|
||||
warnings.push(`link ${index + 1}: an end is not a port of an exported node, left out`);
|
||||
return;
|
||||
}
|
||||
links.push({
|
||||
ends: [
|
||||
{ node: source, port: sourcePort },
|
||||
{ node: target, port: targetPort },
|
||||
],
|
||||
wireless: joinsWirelessly(exported[source]!, exported[target]!, project),
|
||||
panel: [source, target],
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
topology: { nodes, links, documentation: project.documentationItems.map(exportDocumentationItem) },
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
function exportNode(node: TopologyNode, project: ProjectToExport): NodeToWrite {
|
||||
const javaClass = CLASS_OF_KIND[node.kind as Exclude<NodeKind, 'unsupported'>];
|
||||
const interfaces = exportInterfaces(node, project);
|
||||
return {
|
||||
javaClass,
|
||||
name: node.displayName,
|
||||
x: Math.round(node.position.x),
|
||||
y: Math.round(node.position.y),
|
||||
useIpAsName: showsIp(node.hostLabelPolicy),
|
||||
useMacAsName: showsMac(node.hostLabelPolicy),
|
||||
interfaces,
|
||||
portCount: javaClass === 'Switch' || javaClass === 'Modem' ? node.ports.length : interfaces.length,
|
||||
system: {},
|
||||
};
|
||||
}
|
||||
|
||||
function wirelessCard(node: TopologyNode, project: ProjectToExport): boolean {
|
||||
return (node.kind === 'pc' || node.kind === 'notebook') && project.hostWirelessConfigurations.get(node.id)?.isEnabled === true;
|
||||
}
|
||||
|
||||
/** A host with a wireless card and a switch. */
|
||||
function joinsWirelessly(first: TopologyNode, second: TopologyNode, project: ProjectToExport): boolean {
|
||||
return (
|
||||
(wirelessCard(first, project) && second.kind === 'networkSwitch') ||
|
||||
(wirelessCard(second, project) && first.kind === 'networkSwitch')
|
||||
);
|
||||
}
|
||||
|
||||
function showsIp(policy: HostLabelPolicy): boolean {
|
||||
return policy === 'ipAddress' || policy === 'ipAndMAC';
|
||||
}
|
||||
|
||||
function showsMac(policy: HostLabelPolicy): boolean {
|
||||
return policy === 'macAddress' || policy === 'ipAndMAC';
|
||||
}
|
||||
|
||||
/**
|
||||
* The interfaces of a PC, notebook, router or home router. An address the
|
||||
* model has none for is left to Java's constructor (null).
|
||||
*/
|
||||
function exportInterfaces(node: TopologyNode, project: ProjectToExport): FlsInterface[] {
|
||||
const device = project.deviceConfigurations.get(node.id);
|
||||
switch (node.kind) {
|
||||
case 'pc':
|
||||
case 'notebook':
|
||||
return node.ports.slice(0, 1).map((port) => ({
|
||||
ip: device?.ipAddress ?? null,
|
||||
mask: device?.subnetMask ?? null,
|
||||
mac: effectiveMacAddress(port),
|
||||
gateway: device?.defaultGateway ?? '',
|
||||
dns: device?.dnsServer ?? '',
|
||||
wireless: wirelessCard(node, project),
|
||||
}));
|
||||
case 'router':
|
||||
case 'gateway':
|
||||
return node.ports.map((port) => {
|
||||
const configuration = project.interfaceConfigurations.get(interfaceKey(node.id, port.id));
|
||||
return {
|
||||
ip: configuration?.ipAddress ?? null,
|
||||
mask: configuration?.subnetMask ?? null,
|
||||
mac: effectiveMacAddress(port),
|
||||
gateway: device?.defaultGateway ?? '',
|
||||
dns: device?.dnsServer ?? '',
|
||||
wireless: false,
|
||||
};
|
||||
});
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Components 0…1 as Java's 0…255. */
|
||||
function channel(value: number): number {
|
||||
return Math.round(value * 255);
|
||||
}
|
||||
|
||||
function exportDocumentationItem(item: DocumentationItem): FlsDocumentationItem {
|
||||
return {
|
||||
type: item.kind === 'rectangle' ? 1 : 2,
|
||||
text: item.text,
|
||||
x: Math.round(item.frame.x),
|
||||
y: Math.round(item.frame.y),
|
||||
width: Math.round(item.frame.width),
|
||||
height: Math.round(item.frame.height),
|
||||
color: [channel(item.color.red), channel(item.color.green), channel(item.color.blue), channel(item.color.alpha)],
|
||||
font: { name: item.fontName, style: item.isBold ? 1 : 0, size: item.fontSize },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user