feat(editor): F4.3 the editor's frame and surface; a switch's cloud read and written
packages/editor (React 18, Vite) draws a document as Java FILIUS does: Java's default hardware icons, the device button of JSidebarButton, cables as JCablePanel's quadratic curve between the buttons' middles, wireless cables dashed, documentation items below. The surface moves with mouse and finger (the pointer captured only once it moved) and zooms with Ctrl and the wheel, a trackpad pinch, two fingers and the zoom bar; the first view shows all. German and English catalogs with Java's device names, the dashboard's colour tokens, the bridge with its origin check. packages/doc gets the drawing's light view of a document. Java's Switch.cloud (the Internet in the examples) was lost on reading; it is now read, kept in the model and written, with FlsOracle and the corpus, written and exported checks renewed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -96,6 +96,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `src/projectSnapshot.ts` | Swift `TopologyNetworkRuntimeTopologySnapshot(editorState:)` | neu (Abnahme F2): das importierte Projekt als Schnappschuss des Kerns; nur die Kabel verbinden (Swifts Verbindung über die SSID nicht übernommen), nur eingeschaltete DNS-Server laufen; seit F3.1 jeder installierte Echo-Server (ein oder aus, er lässt sich zur Laufzeit schalten) und einfache Client; seit F3.2a jeder Webserver und Browser und die Dateisysteme; seit F3.2c der Rückweg `projectWithSimulationChanges`: die Firewalls aus dem Schnappschuss des Laufs ins Projekt, wie Java sie beim Speichern hat; seit F3.3a die installierten E-Mail-Server (ein oder aus) und auf dem Rückweg ihre Postfächer; seit F3.3b die installierten E-Mail-Programme und auf dem Rückweg ihre Ordner; seit F3.4a die Knoten mit der Befehlszeile (ihre Dateien sind die des Projekts) |
|
| `src/projectSnapshot.ts` | Swift `TopologyNetworkRuntimeTopologySnapshot(editorState:)` | neu (Abnahme F2): das importierte Projekt als Schnappschuss des Kerns; nur die Kabel verbinden (Swifts Verbindung über die SSID nicht übernommen), nur eingeschaltete DNS-Server laufen; seit F3.1 jeder installierte Echo-Server (ein oder aus, er lässt sich zur Laufzeit schalten) und einfache Client; seit F3.2a jeder Webserver und Browser und die Dateisysteme; seit F3.2c der Rückweg `projectWithSimulationChanges`: die Firewalls aus dem Schnappschuss des Laufs ins Projekt, wie Java sie beim Speichern hat; seit F3.3a die installierten E-Mail-Server (ein oder aus) und auf dem Rückweg ihre Postfächer; seit F3.3b die installierten E-Mail-Programme und auf dem Rückweg ihre Ordner; seit F3.4a die Knoten mit der Befehlszeile (ihre Dateien sind die des Projekts) |
|
||||||
| `test/examplesJava.test.ts`, `test/examples/*.java.json` | — | neu (Abnahme F2): fünf Beispiele von Filius (`internetwork_komplett`, `einfaches_rechnernetz_komplett`, `dns_server`, `nat_with_mapped_port`, `Internet_Komplett_…_DE`) mit dem eigenen Leser eingelesen und im Kern gespielt, gegen Java-Filius (`ExampleRun`): je Karte Frames, Zeiten und Tabellenzeilen, die Aufrufe, die Adressen am Ende; gleichzeitig startende DHCP-Clients je Client; zwei benannte Java-Fehler (`JAVA_BUGS`: Ping ohne Gateway wirft eine `NullPointerException`; NAT-Eintrag nur für die erste Gegenstelle) |
|
| `test/examplesJava.test.ts`, `test/examples/*.java.json` | — | neu (Abnahme F2): fünf Beispiele von Filius (`internetwork_komplett`, `einfaches_rechnernetz_komplett`, `dns_server`, `nat_with_mapped_port`, `Internet_Komplett_…_DE`) mit dem eigenen Leser eingelesen und im Kern gespielt, gegen Java-Filius (`ExampleRun`): je Karte Frames, Zeiten und Tabellenzeilen, die Aufrufe, die Adressen am Ende; gleichzeitig startende DHCP-Clients je Client; zwei benannte Java-Fehler (`JAVA_BUGS`: Ping ohne Gateway wirft eine `NullPointerException`; NAT-Eintrag nur für die erste Gegenstelle) |
|
||||||
| `test/classicsJava.test.ts`, `test/exampleRun.ts`, `test/examples/{web,email,gnutella}.java.json` | — | neu (Abnahme F3): die Unterrichtsklassiker auf Beispielen von Filius gegen Java-Filius (`ExampleRun`): Webseite über ihren DNS-Namen (`internetwork_komplett`), E-Mail zwischen zwei Netzen durch NAT und Weitergabe von Server zu Server (`Internet_Komplett_…_DE`), Datei per Gnutella über einen Router finden und holen, Ping, Pong und Query weitergereicht (`zwei_rechnernetze_komplett`, Programme vom Test eingesetzt); je Karte Frames, Zeiten und Tabellenzeilen (ohne RIP), die Zeilen jedes Programms, Mails, Teilnehmer, Ergebnisse und Dateien am Ende; Javas Fehler aus F3.6b und `closedPort` benannt (`JAVA_BUGS`); gleich lange RIP-Wege nach Javas Wahl (`equalRipTies`). `exampleRun.ts`: was beide Beispiel-Tests teilen (Datei öffnen, Javas Namen, Karten nach MAC) |
|
| `test/classicsJava.test.ts`, `test/exampleRun.ts`, `test/examples/{web,email,gnutella}.java.json` | — | neu (Abnahme F3): die Unterrichtsklassiker auf Beispielen von Filius gegen Java-Filius (`ExampleRun`): Webseite über ihren DNS-Namen (`internetwork_komplett`), E-Mail zwischen zwei Netzen durch NAT und Weitergabe von Server zu Server (`Internet_Komplett_…_DE`), Datei per Gnutella über einen Router finden und holen, Ping, Pong und Query weitergereicht (`zwei_rechnernetze_komplett`, Programme vom Test eingesetzt); je Karte Frames, Zeiten und Tabellenzeilen (ohne RIP), die Zeilen jedes Programms, Mails, Teilnehmer, Ergebnisse und Dateien am Ende; Javas Fehler aus F3.6b und `closedPort` benannt (`JAVA_BUGS`); gleich lange RIP-Wege nach Javas Wahl (`equalRipTies`). `exampleRun.ts`: was beide Beispiel-Tests teilen (Datei öffnen, Javas Namen, Karten nach MAC) |
|
||||||
|
| `src/topology.ts`, `src/importProject.ts`, `src/exportProject.ts`, `src/topologyWriter.ts` (Wolke) | Java `Switch.cloud`, `JSwitchKonfiguration` | nachgetragen (F4.3): ein Switch, als Wolke gezeichnet, wird gelesen, im Modell gehalten (`SwitchConfiguration.showsAsCloud`) und wieder geschrieben — vorher ging die Eigenschaft beim Lesen verloren (`nat.fls`, `man-in-the-middle.fls`); `FlsOracle` schreibt sie, Korpus und Schreib-/Exportproben neu geprüft |
|
||||||
| `src/topologyWriter.ts` | Java `SzenarioVerwaltung.netzwerkSpeichern`, Konstruktoren von `NetzwerkInterface`, `Port`, `GUIKabelItem` und der Systemsoftware | neu nach Java: `konfiguration.xml` aus Javas Sicht — Geräte, Schnittstellen (vom Konstruktor angelegte über Getter, weitere per `add`), Ports über Getter-Verweise, Kabel samt Zeichen-Enden (`kabelpanel`), Doku-Elemente; Systemsoftware über ihre Setter: SSID, Lernzeit, RIP, manuelle Routen, DHCP-Client und -Server, statische NAT, Modem, Router-Firewall (über das `konfig`-Plug-in des Webservers, wie XMLEncoder sie erreicht); eigene Anwendungen eines Projekts, wie die Datei sie hatte; Dateisysteme (an die Wurzel des Konstruktors angehängt wie bei XMLEncoder, beim Router hinter dessen Vorlagen); installierte Programme nach den Dateien (Webserver, Tauschbörse und E-Mail-Server legen ihre Ordner nur an, wo sie fehlen; der DNS-Server liest `/dns/hosts` beim Installieren), je unter ihrem Schlüssel mit der Systemsoftware wie `installApp`, samt Einstellungen und persönlicher Firewall; E-Mail: Domain und Bean-Konten des Servers samt Postfächern, Ordner und Adressbuch des Clients (sein Konto steht in `/konten.txt`, einer Datei); Javas Fehler nicht übernommen: ein Empfänger, den eine ältere Datei als Zeichenkette hält, wird als der `AddressEntry` geschrieben, den Javas `AddressEntry(String)` daraus macht (Java legt die Zeichenkette ungeprüft in die typisierte Liste, sein E-Mail-Fenster scheitert daran), ein `null`-Empfänger fällt weg; Kopf „Filius version: 2.14.0 (mdg-filius)“; seit der Abnahme F2 `ipForwardingEnabled`, wo es vom Standard der Klasse abweicht |
|
| `src/topologyWriter.ts` | Java `SzenarioVerwaltung.netzwerkSpeichern`, Konstruktoren von `NetzwerkInterface`, `Port`, `GUIKabelItem` und der Systemsoftware | neu nach Java: `konfiguration.xml` aus Javas Sicht — Geräte, Schnittstellen (vom Konstruktor angelegte über Getter, weitere per `add`), Ports über Getter-Verweise, Kabel samt Zeichen-Enden (`kabelpanel`), Doku-Elemente; Systemsoftware über ihre Setter: SSID, Lernzeit, RIP, manuelle Routen, DHCP-Client und -Server, statische NAT, Modem, Router-Firewall (über das `konfig`-Plug-in des Webservers, wie XMLEncoder sie erreicht); eigene Anwendungen eines Projekts, wie die Datei sie hatte; Dateisysteme (an die Wurzel des Konstruktors angehängt wie bei XMLEncoder, beim Router hinter dessen Vorlagen); installierte Programme nach den Dateien (Webserver, Tauschbörse und E-Mail-Server legen ihre Ordner nur an, wo sie fehlen; der DNS-Server liest `/dns/hosts` beim Installieren), je unter ihrem Schlüssel mit der Systemsoftware wie `installApp`, samt Einstellungen und persönlicher Firewall; E-Mail: Domain und Bean-Konten des Servers samt Postfächern, Ordner und Adressbuch des Clients (sein Konto steht in `/konten.txt`, einer Datei); Javas Fehler nicht übernommen: ein Empfänger, den eine ältere Datei als Zeichenkette hält, wird als der `AddressEntry` geschrieben, den Javas `AddressEntry(String)` daraus macht (Java legt die Zeichenkette ungeprüft in die typisierte Liste, sein E-Mail-Fenster scheitert daran), ein `null`-Empfänger fällt weg; Kopf „Filius version: 2.14.0 (mdg-filius)“; seit der Abnahme F2 `ipForwardingEnabled`, wo es vom Standard der Klasse abweicht |
|
||||||
| `test/topologyWriter.test.ts`, `test/written.java.json` | — | neu: alle 23 Korpus-Dateien gelesen und neu geschrieben, von Java geladen — Topologie, Systemsoftware, Dateisysteme und Programme samt E-Mail wie beim Original (bis auf die benannte Abweichung bei Empfängern als Zeichenkette, nur in `man-in-the-middle.fls`) (Java gegen Java); dazu eine künstliche Topologie mit dem, was der Korpus nicht hat (u. a. eingeschaltete Router-Firewall mit Regeln jedes Protokolls, alle Firewall-Schalter abseits der Vorgabe, Modem mit eigenem Port und Ziel, verschachtelter Dateibaum mit umbenannter Wurzel, namenlosem Ordner, Datei ohne Typ und Inhalt mit CR/CRLF und Zeichen jenseits der BMP, Heimrouter mit eigener Wurzel, jedes Filius-Programm mit Einstellungen abseits der Vorgabe samt persönlicher Firewall, von Java angelegte Ordner `/peer2peer` und `/mailserver`, E-Mail-Server und -Client mit Konto, Postfach, drei Ordnern, Adressbuch und Empfängern aller Formen) |
|
| `test/topologyWriter.test.ts`, `test/written.java.json` | — | neu: alle 23 Korpus-Dateien gelesen und neu geschrieben, von Java geladen — Topologie, Systemsoftware, Dateisysteme und Programme samt E-Mail wie beim Original (bis auf die benannte Abweichung bei Empfängern als Zeichenkette, nur in `man-in-the-middle.fls`) (Java gegen Java); dazu eine künstliche Topologie mit dem, was der Korpus nicht hat (u. a. eingeschaltete Router-Firewall mit Regeln jedes Protokolls, alle Firewall-Schalter abseits der Vorgabe, Modem mit eigenem Port und Ziel, verschachtelter Dateibaum mit umbenannter Wurzel, namenlosem Ordner, Datei ohne Typ und Inhalt mit CR/CRLF und Zeichen jenseits der BMP, Heimrouter mit eigener Wurzel, jedes Filius-Programm mit Einstellungen abseits der Vorgabe samt persönlicher Firewall, von Java angelegte Ordner `/peer2peer` und `/mailserver`, E-Mail-Server und -Client mit Konto, Postfach, drei Ordnern, Adressbuch und Empfängern aller Formen) |
|
||||||
| `src/exportProject.ts` | Gegenstück zu `importProject.ts`; `saveProjectArchive` schreibt eine ganze `.fls` samt der übrigen Archiveinträge (Gegenstück: `openProjectArchive` in `importProject.ts`, das sie in `archiveEntries` des Modells legt); Java `JHostKonfiguration.updateWifiConnection`, `InternetKnotenBetriebssystem.getStandardGateway`/`setStandardGateway` | neu: Editor-Modell → Javas Sicht für den Schreiber, Schritt X1 Topologie — Geräte, Schnittstellen (MAC wie in der Simulation: aus der Datei oder aus der Port-ID abgeleitet), Kabel samt Funk (Karte drahtlos und Switch, wie Java verbindet), Gateway und DNS an allen Schnittstellen wie Javas eigene Setter, Doku-Elemente; X2 Systemsoftware — SSIDs, Lernzeit, Routen, RIP, DHCP-Client und -Server, Portweiterleitung, Firewall (Namen des Kerns als Javas Zahlen), Modems aus den Paaren des Modells (Port aus dem Paarnamen, der erste eines Paars dient, Ziel `localhost`); X3 Dateien von Rechner und Notebook — Typ aus der Inhaltsart (Text nach Endung, sonst `text`; Bild und Binärdaten nach Medientyp), Bilder und Binärdaten in Base64 wie Filius' `Base64.encodeBytes` (Umbruch nach 76 Zeichen, an 19 Längen mit Java gemessen); X4a Programme — je Programm die Filius-Klasse unter ihrem Namen als Schlüssel, eingeschaltete Server, Ports, virtuelle Hosts, DNS-Rekursion, Ziel des Clients (leer als `null` wie Javas Konstruktor), Teilnehmerzahl, persönliche Firewall; X4b E-Mail — Beans des Servers (Domain, Konten, Postfächer) und des Clients (drei Ordner, Adressbuch), `/konten.txt` und `/mailserver/konten.txt` aus den Konten des Modells neu geschrieben im Format von Filius 2.14 (`EmailKonto.toString`, `listeBenutzerkontenZuString`), ohne Javas Fehler (leere Felder statt "null"); ein Zeichen, das ein Feld im Format nicht tragen kann, wird dort zum Leerzeichen, mit Warnung; seit der Abnahme F2 die IP-Weiterleitung aus dem Modell |
|
| `src/exportProject.ts` | Gegenstück zu `importProject.ts`; `saveProjectArchive` schreibt eine ganze `.fls` samt der übrigen Archiveinträge (Gegenstück: `openProjectArchive` in `importProject.ts`, das sie in `archiveEntries` des Modells legt); Java `JHostKonfiguration.updateWifiConnection`, `InternetKnotenBetriebssystem.getStandardGateway`/`setStandardGateway` | neu: Editor-Modell → Javas Sicht für den Schreiber, Schritt X1 Topologie — Geräte, Schnittstellen (MAC wie in der Simulation: aus der Datei oder aus der Port-ID abgeleitet), Kabel samt Funk (Karte drahtlos und Switch, wie Java verbindet), Gateway und DNS an allen Schnittstellen wie Javas eigene Setter, Doku-Elemente; X2 Systemsoftware — SSIDs, Lernzeit, Routen, RIP, DHCP-Client und -Server, Portweiterleitung, Firewall (Namen des Kerns als Javas Zahlen), Modems aus den Paaren des Modells (Port aus dem Paarnamen, der erste eines Paars dient, Ziel `localhost`); X3 Dateien von Rechner und Notebook — Typ aus der Inhaltsart (Text nach Endung, sonst `text`; Bild und Binärdaten nach Medientyp), Bilder und Binärdaten in Base64 wie Filius' `Base64.encodeBytes` (Umbruch nach 76 Zeichen, an 19 Längen mit Java gemessen); X4a Programme — je Programm die Filius-Klasse unter ihrem Namen als Schlüssel, eingeschaltete Server, Ports, virtuelle Hosts, DNS-Rekursion, Ziel des Clients (leer als `null` wie Javas Konstruktor), Teilnehmerzahl, persönliche Firewall; X4b E-Mail — Beans des Servers (Domain, Konten, Postfächer) und des Clients (drei Ordner, Adressbuch), `/konten.txt` und `/mailserver/konten.txt` aus den Konten des Modells neu geschrieben im Format von Filius 2.14 (`EmailKonto.toString`, `listeBenutzerkontenZuString`), ohne Javas Fehler (leere Felder statt "null"); ein Zeichen, das ein Feld im Format nicht tragen kann, wird dort zum Leerzeichen, mit Warnung; seit der Abnahme F2 die IP-Weiterleitung aus dem Modell |
|
||||||
@@ -117,6 +118,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `src/values.ts` | — | neu (F4.1): Einstellungen als `Y.Map` je Feld und Listen als `Y.Array`, damit gleichzeitige Änderungen verschiedener Felder und Zeilen beide bleiben |
|
| `src/values.ts` | — | neu (F4.1): Einstellungen als `Y.Map` je Feld und Listen als `Y.Array`, damit gleichzeitige Änderungen verschiedener Felder und Zeilen beide bleiben |
|
||||||
| `src/janitor.ts` | Plan §5.3 | neu (F4.1): der Hausmeister, I1 (Kabel ins Leere), I2 (zwei Kabel an einem Port: kleinere Kennung bleibt), I4 (fehlender Ordner, ohne Rücksicht auf Groß-/Kleinschreibung); deterministisch, idempotent, Ursprung `janitor` |
|
| `src/janitor.ts` | Plan §5.3 | neu (F4.1): der Hausmeister, I1 (Kabel ins Leere), I2 (zwei Kabel an einem Port: kleinere Kennung bleibt), I4 (fehlender Ordner, ohne Rücksicht auf Groß-/Kleinschreibung); deterministisch, idempotent, Ursprung `janitor` |
|
||||||
| `src/snapshot.ts` | — | neu (F4.1): Schnappschuss des Kerns und `.fls`-Export eines Dokuments, beide über das Projekt (`projectSnapshot`, `saveProjectArchive`) |
|
| `src/snapshot.ts` | — | neu (F4.1): Schnappschuss des Kerns und `.fls`-Export eines Dokuments, beide über das Projekt (`projectSnapshot`, `saveProjectArchive`) |
|
||||||
|
| `src/view.ts` | Java `Host.holeAnzeigeName`; `exportProject.joinsWirelessly` | neu (F4.3): die Zeichensicht ohne Dateisysteme — Geräte mit Javas Beschriftung und Wolke, Kabel mit Funk-Kennung, Doku-Elemente in der Zeichenfolge des Modells |
|
||||||
| `test/projectDoc.test.ts` | — | neu (F4.1): alle 23 Korpusdateien → Dokument → Yjs-Update → Projekt: dasselbe Projekt, derselbe Schnappschuss, dieselbe `.fls`, dieselbe Reihenfolge je Ordner; Kennungen absichtlich gegen die Dateireihenfolge |
|
| `test/projectDoc.test.ts` | — | neu (F4.1): alle 23 Korpusdateien → Dokument → Yjs-Update → Projekt: dasselbe Projekt, derselbe Schnappschuss, dieselbe `.fls`, dieselbe Reihenfolge je Ordner; Kennungen absichtlich gegen die Dateireihenfolge |
|
||||||
| `test/collaboration.test.ts`, `test/values.test.ts` | — | neu (F4.1): zwei Replikate — zwei Felder eines Geräts, zwei neue Zeilen, gemeinsames Tippen; der Hausmeister nach einem Zusammenführen (I1, I2 auf beiden Replikaten gleich, I4 auch in anderer Schreibweise), idempotent ohne Aktualisierung; `long` einer eigenen Anwendung, Binärdateien |
|
| `test/collaboration.test.ts`, `test/values.test.ts` | — | neu (F4.1): zwei Replikate — zwei Felder eines Geräts, zwei neue Zeilen, gemeinsames Tippen; der Hausmeister nach einem Zusammenführen (I1, I2 auf beiden Replikaten gleich, I4 auch in anderer Schreibweise), idempotent ohne Aktualisierung; `long` einer eigenen Anwendung, Binärdateien |
|
||||||
|
|
||||||
@@ -136,6 +138,19 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `test/room.test.ts`, `test/localSimHost.test.ts` | — | neu (F4.2): der Raum gegen die von Hand gesteuerte Engine (dieselben Rahmen, dieselben Zeilen), Abonnieren, Geschwindigkeit, Nachholen, Fehler; Host und Worker-Seite über einen `MessageChannel` |
|
| `test/room.test.ts`, `test/localSimHost.test.ts` | — | neu (F4.2): der Raum gegen die von Hand gesteuerte Engine (dieselben Rahmen, dieselben Zeilen), Abonnieren, Geschwindigkeit, Nachholen, Fehler; Host und Worker-Seite über einen `MessageChannel` |
|
||||||
| `browser/m9.spec.ts`, `browser/traceDigest.ts`, `browser/*Entry.ts`, `/playwright.config.ts` | Plan §13 M9 | neu (F4.2): M9 in Chromium — 17 Beispiele, je 30 s mit Eingaben, ergeben Zeichen für Zeichen denselben Mitschnitt wie in Node; der Raum in einem echten Worker |
|
| `browser/m9.spec.ts`, `browser/traceDigest.ts`, `browser/*Entry.ts`, `/playwright.config.ts` | Plan §13 M9 | neu (F4.2): M9 in Chromium — 17 Beispiele, je 30 s mit Eingaben, ergeben Zeichen für Zeichen denselben Mitschnitt wie in Node; der Raum in einem echten Worker |
|
||||||
|
|
||||||
|
## packages/editor
|
||||||
|
|
||||||
|
| Datei | Quelle | Art der Übertragung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `src/assets/hardware/*.png` | Java-Filius `src/main/resources/gfx/hardware/` (GPL) | unverändert kopiert: Javas Standardsymbole (`IconMode.DEFAULT`), damit Arbeitsblätter passen |
|
||||||
|
| `src/canvas/viewport.ts` | Dashboard `frontend/src/apps/kanban/zoom.js` (Rechte bei uns, Plan §7.1) | kopiert und umgebaut (F4.3): Grenzen, Stufen, Einpassen, Fingerrechnung; statt Rollposition ein Ausschnitt aus Maßstab und Versatz |
|
||||||
|
| `src/canvas/layout.ts` | Java `JSidebarButton.getWidth/getHeight`, `JCablePanel.updateBounds/paintComponent`, `IconMode`, `Host.holeAnzeigeName` | neu nach Java (F4.3): Knopfgröße, Symbol oben mittig, Kabel als Javas quadratische Kurve zwischen den Knopfmitten (ganzzahlig wie Java); Beschriftungsschrift nach den Maßen der Dateien ab Java 2.5 |
|
||||||
|
| `src/canvas/Canvas.tsx`, `src/canvas/ZoomBar.tsx`, `src/canvas/icons.ts`, `src/Editor.tsx`, `src/main.tsx` | Plan §7.1, §7.2 | neu (F4.3): die Fläche (SVG) mit Geräten, Kabeln (Funk gestrichelt), Doku-Elementen; Verschieben (Zeiger erst bei Bewegung gefangen), Strg+Rad und Trackpad-Pinch, zwei Finger, Zoomleiste mit 44-px-Zielen; Entwicklungsseite mit Beispielen |
|
||||||
|
| `src/i18n/*` | Java `MessagesBundle_de_DE`, `_en_GB` | neu (F4.3): Kataloge de/en mit englischen Schlüsseln, Javas Wörter für Geräte (M11) |
|
||||||
|
| `src/theme/tokens.css` | Dashboard `frontend/src/index.css` (semantische Token) | neu (F4.3): dieselben Namen und Werte, hell und dunkel; dazu die Farben der Fläche nach Java (`JCablePanel`) |
|
||||||
|
| `src/bridge/dashboardBridge.ts` | Dashboard `frontend/src/apps/scratch/lib/bridge.js` (Muster) | neu (F4.3): Version, Namen, Herkunftsprüfung; `setLocale`, `setTheme`, `ready` |
|
||||||
|
| `test/*.test.ts`, `browser/canvas.spec.ts` | — | neu (F4.3): Rechnung von Ausschnitt und Java-Anordnung, Kataloge gegen Javas Bündel, Brücke; im Browser jedes Beispiel gegen die in der Datei gespeicherten Knopfmaße (ab Java 2.5), Gesten mit Maus, Rad und zwei Fingern (CDP), Sprache und Dunkel |
|
||||||
|
|
||||||
## tools/swift-oracle
|
## tools/swift-oracle
|
||||||
|
|
||||||
Baut den unveränderten Swift-Kern (Stand `3b5ded0`) unter Linux, nur zwei
|
Baut den unveränderten Swift-Kern (Stand `3b5ded0`) unter Linux, nur zwei
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ die Herkunft je Paket. Lizenz dieses Repos: **GPL-3.0** (`LICENSE`).
|
|||||||
| `packages/doc` | gemeinsames Dokument (Yjs): Projekt ⇄ Dokument, Schnappschuss des Kerns, Hausmeister |
|
| `packages/doc` | gemeinsames Dokument (Yjs): Projekt ⇄ Dokument, Schnappschuss des Kerns, Hausmeister |
|
||||||
| `packages/protocol` | die eine Nachrichtenliste zwischen Editor und Simulationsraum (`/sim`) |
|
| `packages/protocol` | die eine Nachrichtenliste zwischen Editor und Simulationsraum (`/sim`) |
|
||||||
| `packages/sim` | Simulationsraum (für Server F und den Web Worker) und `LocalSimHost` |
|
| `packages/sim` | Simulationsraum (für Server F und den Web Worker) und `LocalSimHost` |
|
||||||
| `packages/editor` | Oberfläche im Iframe des Dashboards *(folgt)* |
|
| `packages/editor` | Oberfläche im Iframe des Dashboards (React, Vite): bisher die Fläche; `npm run dev -w @mdg-filius/editor` öffnet die Entwicklungsseite |
|
||||||
| `server` | Filius-Dienst auf Server F *(folgt)* |
|
| `server` | Filius-Dienst auf Server F *(folgt)* |
|
||||||
|
|
||||||
Plan und Entscheidungen: `knowledge/plans/FILIUS_WEB_PLAN.md` im Dashboard.
|
Plan und Entscheidungen: `knowledge/plans/FILIUS_WEB_PLAN.md` im Dashboard.
|
||||||
|
|||||||
@@ -417,6 +417,7 @@
|
|||||||
99,
|
99,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -813,6 +814,7 @@
|
|||||||
68,
|
68,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -1109,6 +1111,7 @@
|
|||||||
68,
|
68,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -1407,6 +1410,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -1448,6 +1452,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -2000,6 +2005,7 @@
|
|||||||
68,
|
68,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -3527,6 +3533,7 @@
|
|||||||
110,
|
110,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -3831,6 +3838,7 @@
|
|||||||
143,
|
143,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -4741,6 +4749,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -6322,6 +6331,7 @@
|
|||||||
110,
|
110,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -6620,6 +6630,7 @@
|
|||||||
143,
|
143,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -7749,6 +7760,7 @@
|
|||||||
99,
|
99,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -8247,6 +8259,7 @@
|
|||||||
68,
|
68,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -8527,6 +8540,7 @@
|
|||||||
121,
|
121,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -8823,6 +8837,7 @@
|
|||||||
68,
|
68,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -9119,6 +9134,7 @@
|
|||||||
68,
|
68,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -9733,6 +9749,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -9946,6 +9963,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -9987,6 +10005,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -10607,6 +10626,7 @@
|
|||||||
68,
|
68,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -11089,6 +11109,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -11130,6 +11151,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -11420,6 +11442,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -11820,6 +11843,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -12697,6 +12721,7 @@
|
|||||||
79,
|
79,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": true,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -13655,6 +13680,7 @@
|
|||||||
121,
|
121,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -13967,6 +13993,7 @@
|
|||||||
79,
|
79,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": true,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -14067,6 +14094,7 @@
|
|||||||
68,
|
68,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -14108,6 +14136,7 @@
|
|||||||
68,
|
68,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -14779,6 +14808,7 @@
|
|||||||
68,
|
68,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -15035,6 +15065,7 @@
|
|||||||
68,
|
68,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -15500,6 +15531,7 @@
|
|||||||
121,
|
121,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -15748,6 +15780,7 @@
|
|||||||
68,
|
68,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -16114,6 +16147,7 @@
|
|||||||
155,
|
155,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -16359,6 +16393,7 @@
|
|||||||
68,
|
68,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -17330,6 +17365,7 @@
|
|||||||
68,
|
68,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -17413,6 +17449,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -17454,6 +17491,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -18239,6 +18277,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -18452,6 +18491,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -18493,6 +18533,7 @@
|
|||||||
80,
|
80,
|
||||||
19
|
19
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
|
|||||||
@@ -20,6 +20,295 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/code-frame": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-validator-identifier": "^7.29.7",
|
||||||
|
"js-tokens": "^4.0.0",
|
||||||
|
"picocolors": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/code-frame/node_modules/js-tokens": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@babel/compat-data": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/core": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.29.7",
|
||||||
|
"@babel/generator": "^7.29.7",
|
||||||
|
"@babel/helper-compilation-targets": "^7.29.7",
|
||||||
|
"@babel/helper-module-transforms": "^7.29.7",
|
||||||
|
"@babel/helpers": "^7.29.7",
|
||||||
|
"@babel/parser": "^7.29.7",
|
||||||
|
"@babel/template": "^7.29.7",
|
||||||
|
"@babel/traverse": "^7.29.7",
|
||||||
|
"@babel/types": "^7.29.7",
|
||||||
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
|
"convert-source-map": "^2.0.0",
|
||||||
|
"debug": "^4.1.0",
|
||||||
|
"gensync": "^1.0.0-beta.2",
|
||||||
|
"json5": "^2.2.3",
|
||||||
|
"semver": "^6.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/babel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/generator": {
|
||||||
|
"version": "7.29.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
|
||||||
|
"integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.29.8",
|
||||||
|
"@babel/types": "^7.29.8",
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.12",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.28",
|
||||||
|
"jsesc": "^3.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-compilation-targets": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/compat-data": "^7.29.7",
|
||||||
|
"@babel/helper-validator-option": "^7.29.7",
|
||||||
|
"browserslist": "^4.24.0",
|
||||||
|
"lru-cache": "^5.1.1",
|
||||||
|
"semver": "^6.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-globals": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-module-imports": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/traverse": "^7.29.7",
|
||||||
|
"@babel/types": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-module-transforms": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-module-imports": "^7.29.7",
|
||||||
|
"@babel/helper-validator-identifier": "^7.29.7",
|
||||||
|
"@babel/traverse": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-plugin-utils": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-string-parser": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-validator-option": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helpers": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/template": "^7.29.7",
|
||||||
|
"@babel/types": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/parser": {
|
||||||
|
"version": "7.29.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz",
|
||||||
|
"integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.29.8"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"parser": "bin/babel-parser.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-plugin-utils": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-plugin-utils": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/template": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.29.7",
|
||||||
|
"@babel/parser": "^7.29.7",
|
||||||
|
"@babel/types": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/traverse": {
|
||||||
|
"version": "7.29.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
|
||||||
|
"integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.29.7",
|
||||||
|
"@babel/generator": "^7.29.8",
|
||||||
|
"@babel/helper-globals": "^7.29.7",
|
||||||
|
"@babel/parser": "^7.29.8",
|
||||||
|
"@babel/template": "^7.29.7",
|
||||||
|
"@babel/types": "^7.29.8",
|
||||||
|
"debug": "^4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/types": {
|
||||||
|
"version": "7.29.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
|
||||||
|
"integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-string-parser": "^7.29.7",
|
||||||
|
"@babel/helper-validator-identifier": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.28.2",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
|
||||||
@@ -462,6 +751,38 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
|
"version": "0.3.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
|
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/remapping": {
|
||||||
|
"version": "2.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||||
|
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
|
||||||
@@ -469,6 +790,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
|
"version": "0.3.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||||
|
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mdg-filius/core": {
|
"node_modules/@mdg-filius/core": {
|
||||||
"resolved": "packages/core",
|
"resolved": "packages/core",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -477,6 +809,10 @@
|
|||||||
"resolved": "packages/doc",
|
"resolved": "packages/doc",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@mdg-filius/editor": {
|
||||||
|
"resolved": "packages/editor",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@mdg-filius/fls": {
|
"node_modules/@mdg-filius/fls": {
|
||||||
"resolved": "packages/fls",
|
"resolved": "packages/fls",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -522,6 +858,13 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@rolldown/pluginutils": {
|
||||||
|
"version": "1.0.0-rc.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||||
|
"integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.63.5",
|
"version": "4.63.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.5.tgz",
|
||||||
@@ -872,6 +1215,51 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/babel__core": {
|
||||||
|
"version": "7.20.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
|
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.20.7",
|
||||||
|
"@babel/types": "^7.20.7",
|
||||||
|
"@types/babel__generator": "*",
|
||||||
|
"@types/babel__template": "*",
|
||||||
|
"@types/babel__traverse": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__generator": {
|
||||||
|
"version": "7.27.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
||||||
|
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__template": {
|
||||||
|
"version": "7.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
||||||
|
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.1.0",
|
||||||
|
"@babel/types": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__traverse": {
|
||||||
|
"version": "7.28.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
||||||
|
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.28.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/chai": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
@@ -907,6 +1295,55 @@
|
|||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/prop-types": {
|
||||||
|
"version": "15.7.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
|
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "18.3.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
|
||||||
|
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/prop-types": "*",
|
||||||
|
"csstype": "^3.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/react-dom": {
|
||||||
|
"version": "18.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||||
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitejs/plugin-react": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.29.0",
|
||||||
|
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
||||||
|
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
||||||
|
"@rolldown/pluginutils": "1.0.0-rc.3",
|
||||||
|
"@types/babel__core": "^7.20.5",
|
||||||
|
"react-refresh": "^0.18.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "3.2.7",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
|
||||||
@@ -1032,6 +1469,53 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/baseline-browser-mapping": {
|
||||||
|
"version": "2.11.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz",
|
||||||
|
"integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"baseline-browser-mapping": "dist/cli.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/browserslist": {
|
||||||
|
"version": "4.29.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz",
|
||||||
|
"integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/browserslist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"baseline-browser-mapping": "^2.11.26",
|
||||||
|
"caniuse-lite": "^1.0.30001813",
|
||||||
|
"electron-to-chromium": "^1.5.439",
|
||||||
|
"node-releases": "^2.0.57",
|
||||||
|
"update-browserslist-db": "^1.3.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"browserslist": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cac": {
|
"node_modules/cac": {
|
||||||
"version": "6.7.14",
|
"version": "6.7.14",
|
||||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||||
@@ -1042,6 +1526,27 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/caniuse-lite": {
|
||||||
|
"version": "1.0.30001814",
|
||||||
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz",
|
||||||
|
"integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/browserslist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "CC-BY-4.0"
|
||||||
|
},
|
||||||
"node_modules/chai": {
|
"node_modules/chai": {
|
||||||
"version": "5.3.3",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
|
||||||
@@ -1069,6 +1574,20 @@
|
|||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/convert-source-map": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -1097,6 +1616,13 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-to-chromium": {
|
||||||
|
"version": "1.5.444",
|
||||||
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.444.tgz",
|
||||||
|
"integrity": "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/es-module-lexer": {
|
"node_modules/es-module-lexer": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||||
@@ -1146,6 +1672,16 @@
|
|||||||
"@esbuild/win32-x64": "0.28.2"
|
"@esbuild/win32-x64": "0.28.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escalade": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
@@ -1205,6 +1741,16 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gensync": {
|
||||||
|
"version": "1.0.0-beta.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
|
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/isomorphic.js": {
|
"node_modules/isomorphic.js": {
|
||||||
"version": "0.2.5",
|
"version": "0.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
|
||||||
@@ -1222,6 +1768,32 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jsesc": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"jsesc": "bin/jsesc"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/json5": {
|
||||||
|
"version": "2.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
|
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"json5": "lib/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lib0": {
|
"node_modules/lib0": {
|
||||||
"version": "0.2.119",
|
"version": "0.2.119",
|
||||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.119.tgz",
|
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.119.tgz",
|
||||||
@@ -1243,6 +1815,24 @@
|
|||||||
"url": "https://github.com/sponsors/dmonad"
|
"url": "https://github.com/sponsors/dmonad"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"loose-envify": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/loose-envify/node_modules/js-tokens": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/loupe": {
|
"node_modules/loupe": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
|
||||||
@@ -1250,6 +1840,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -1286,6 +1886,16 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-releases": {
|
||||||
|
"version": "2.0.57",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz",
|
||||||
|
"integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pathe": {
|
"node_modules/pathe": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
@@ -1399,6 +2009,41 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-dom": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"scheduler": "^0.23.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-refresh": {
|
||||||
|
"version": "0.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
||||||
|
"integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.63.5",
|
"version": "4.63.5",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz",
|
||||||
@@ -1457,6 +2102,25 @@
|
|||||||
"node": ">=v12.22.7"
|
"node": ">=v12.22.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/scheduler": {
|
||||||
|
"version": "0.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||||
|
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/siginfo": {
|
"node_modules/siginfo": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||||
@@ -1583,6 +2247,37 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/update-browserslist-db": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/browserslist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"escalade": "^3.2.0",
|
||||||
|
"picocolors": "^1.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"update-browserslist-db": "cli.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"browserslist": ">= 4.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.3.6",
|
"version": "7.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
|
||||||
@@ -1777,6 +2472,13 @@
|
|||||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/yallist": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/yjs": {
|
"node_modules/yjs": {
|
||||||
"version": "13.6.33",
|
"version": "13.6.33",
|
||||||
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.33.tgz",
|
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.33.tgz",
|
||||||
@@ -1809,6 +2511,27 @@
|
|||||||
"yjs": "^13.6.33"
|
"yjs": "^13.6.33"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/editor": {
|
||||||
|
"name": "@mdg-filius/editor",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"license": "GPL-3.0-only",
|
||||||
|
"dependencies": {
|
||||||
|
"@mdg-filius/core": "0.0.0",
|
||||||
|
"@mdg-filius/doc": "0.0.0",
|
||||||
|
"@mdg-filius/fls": "0.0.0",
|
||||||
|
"@mdg-filius/protocol": "0.0.0",
|
||||||
|
"@mdg-filius/sim": "0.0.0",
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"yjs": "^13.6.33"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.31",
|
||||||
|
"@types/react-dom": "^18.3.7",
|
||||||
|
"@vitejs/plugin-react": "^5.2.0",
|
||||||
|
"vite": "^7.3.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/fls": {
|
"packages/fls": {
|
||||||
"name": "@mdg-filius/fls",
|
"name": "@mdg-filius/fls",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"typecheck": "tsc -b && tsc -p packages/core/test && tsc -p packages/fls/test && tsc -p packages/doc/test && tsc -p packages/protocol/test && tsc -p packages/sim/test && tsc -p packages/sim/browser",
|
"typecheck": "tsc -b && tsc -p packages/core/test && tsc -p packages/fls/test && tsc -p packages/doc/test && tsc -p packages/protocol/test && tsc -p packages/sim/test && tsc -p packages/sim/browser && tsc -p packages/editor && tsc -p packages/editor/test",
|
||||||
"test:browser": "playwright test",
|
"test:browser": "playwright test",
|
||||||
"check": "npm run typecheck && npm run test",
|
"check": "npm run typecheck && npm run test",
|
||||||
"hooks": "git config core.hooksPath scripts/hooks",
|
"hooks": "git config core.hooksPath scripts/hooks",
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ export function bestMatchingManualRoute(
|
|||||||
export interface SwitchConfiguration {
|
export interface SwitchConfiguration {
|
||||||
readonly ssid: string;
|
readonly ssid: string;
|
||||||
readonly retentionTimeMilliseconds: number;
|
readonly retentionTimeMilliseconds: number;
|
||||||
|
/** Drawn as a cloud (Java's Switch.cloud, the Internet in the examples); only set when true. The simulation ignores it. */
|
||||||
|
readonly showsAsCloud?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Java SwitchFirmware keeps a learned MAC address for five minutes. */
|
/** Java SwitchFirmware keeps a learned MAC address for five minutes. */
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export * from './janitor.js';
|
|||||||
export * from './projectDoc.js';
|
export * from './projectDoc.js';
|
||||||
export * from './snapshot.js';
|
export * from './snapshot.js';
|
||||||
export * from './values.js';
|
export * from './values.js';
|
||||||
|
export * from './view.js';
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
// What the editor draws, read straight from the document: the devices with
|
||||||
|
// their labels, the cables, the documentation items. Lighter than
|
||||||
|
// readProject (no file systems, no settings beyond the few a drawing needs),
|
||||||
|
// 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 { inOrder, ROOTS } from './projectDoc.js';
|
||||||
|
|
||||||
|
export interface NodeView {
|
||||||
|
readonly id: string;
|
||||||
|
readonly kind: NodeKind;
|
||||||
|
readonly name: string;
|
||||||
|
/** What Java writes under the icon (Host.holeAnzeigeName): the name, or the first card's address. */
|
||||||
|
readonly label: string;
|
||||||
|
/** Java's position of the device's button: its top left corner. */
|
||||||
|
readonly x: number;
|
||||||
|
readonly y: number;
|
||||||
|
/** A switch drawn as a cloud. */
|
||||||
|
readonly cloud: boolean;
|
||||||
|
readonly ports: readonly { readonly id: string; readonly label: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkView {
|
||||||
|
readonly id: string;
|
||||||
|
readonly sourceNode: string;
|
||||||
|
readonly sourcePort: string;
|
||||||
|
readonly targetNode: string;
|
||||||
|
readonly targetPort: string;
|
||||||
|
/** A host's wireless card joining a switch: Java draws it dashed and grey. */
|
||||||
|
readonly wireless: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentationView {
|
||||||
|
readonly id: string;
|
||||||
|
readonly kind: DocumentationItemKind;
|
||||||
|
readonly x: number;
|
||||||
|
readonly y: number;
|
||||||
|
readonly width: number;
|
||||||
|
readonly height: number;
|
||||||
|
readonly text: string;
|
||||||
|
/** Components 0 to 1. */
|
||||||
|
readonly color: { readonly red: number; readonly green: number; readonly blue: number; readonly alpha: number };
|
||||||
|
readonly font: string;
|
||||||
|
readonly size: number;
|
||||||
|
readonly bold: boolean;
|
||||||
|
readonly order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TopologyView {
|
||||||
|
readonly nodes: readonly NodeView[];
|
||||||
|
readonly links: readonly LinkView[];
|
||||||
|
readonly docs: readonly DocumentationView[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsOf = (node: Y.Map<unknown>) => node.get('settings') as Y.Map<Y.Map<unknown>> | undefined;
|
||||||
|
|
||||||
|
/** Host.holeAnzeigeName: a PC's or notebook's address and MAC address, as its label policy asks. */
|
||||||
|
function labelOf(node: Y.Map<unknown>, kind: NodeKind, name: string): string {
|
||||||
|
const policy = node.get('labelPolicy') as HostLabelPolicy;
|
||||||
|
if ((kind !== 'pc' && kind !== 'notebook') || policy === 'manual') return name;
|
||||||
|
const ip = (settingsOf(node)?.get('device')?.get('ipAddress') as string | undefined) ?? '';
|
||||||
|
const port = (node.get('ports') as Y.Array<Y.Map<unknown>>).get(0);
|
||||||
|
const mac = port === undefined ? '' : ((port.get('mac') as string | undefined) ?? stableGeneratedMacAddress(port.get('id') as string));
|
||||||
|
if (policy === 'ipAddress') return ip;
|
||||||
|
if (policy === 'macAddress') return mac;
|
||||||
|
return `${ip} (${mac})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The drawing of the document, in the order of its devices and cables. */
|
||||||
|
export function topologyView(doc: Y.Doc): TopologyView {
|
||||||
|
const kinds = new Map<string, NodeKind>();
|
||||||
|
const wirelessHosts = new Set<string>();
|
||||||
|
const nodes = inOrder(doc.getMap<Y.Map<unknown>>(ROOTS.nodes)).map(([id, node]): NodeView => {
|
||||||
|
const kind = node.get('kind') as NodeKind;
|
||||||
|
const name = node.get('name') as string;
|
||||||
|
kinds.set(id, kind);
|
||||||
|
if (settingsOf(node)?.get('wireless')?.get('isEnabled') === true && (kind === 'pc' || kind === 'notebook')) wirelessHosts.add(id);
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
name,
|
||||||
|
label: labelOf(node, kind, name),
|
||||||
|
x: node.get('x') as number,
|
||||||
|
y: node.get('y') as number,
|
||||||
|
cloud: kind === 'networkSwitch' && settingsOf(node)?.get('switch')?.get('showsAsCloud') === true,
|
||||||
|
ports: (node.get('ports') as Y.Array<Y.Map<unknown>>).toArray().map((port) => ({ id: port.get('id') as string, label: port.get('label') as string })),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// exportProject's joinsWirelessly: a host with a wireless card and a switch.
|
||||||
|
const wireless = (first: string, second: string) => (wirelessHosts.has(first) && kinds.get(second) === 'networkSwitch') || (wirelessHosts.has(second) && kinds.get(first) === 'networkSwitch');
|
||||||
|
const links = inOrder(doc.getMap<Y.Map<unknown>>(ROOTS.links)).map(([id, link]): LinkView => {
|
||||||
|
const sourceNode = link.get('sourceNode') as string;
|
||||||
|
const targetNode = link.get('targetNode') as string;
|
||||||
|
return { id, sourceNode, sourcePort: link.get('sourcePort') as string, targetNode, targetPort: link.get('targetPort') as string, wireless: wireless(sourceNode, targetNode) };
|
||||||
|
});
|
||||||
|
const docs = [...doc.getMap<Y.Map<unknown>>(ROOTS.docs).entries()].map(
|
||||||
|
([id, item]): DocumentationView => ({
|
||||||
|
id,
|
||||||
|
kind: item.get('kind') as DocumentationItemKind,
|
||||||
|
x: item.get('x') as number,
|
||||||
|
y: item.get('y') as number,
|
||||||
|
width: item.get('width') as number,
|
||||||
|
height: item.get('height') as number,
|
||||||
|
text: (item.get('text') as Y.Text).toString(),
|
||||||
|
color: { red: item.get('red') as number, green: item.get('green') as number, blue: item.get('blue') as number, alpha: item.get('alpha') as number },
|
||||||
|
font: item.get('font') as string,
|
||||||
|
size: item.get('size') as number,
|
||||||
|
bold: item.get('bold') as boolean,
|
||||||
|
order: item.get('order') as number,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// The model's render order (inDeterministicRenderOrder): rectangles below texts, then by order, then by id.
|
||||||
|
const layer = (item: DocumentationView) => (item.kind === 'rectangle' ? 0 : 1);
|
||||||
|
docs.sort((left, right) => layer(left) - layer(right) || left.order - right.order || compareIds(left.id, right.id));
|
||||||
|
return { nodes, links, docs };
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// The drawing of a document against the project it holds, for every file of
|
||||||
|
// the corpus: the same devices in the same order at the same places, Java's
|
||||||
|
// labels, the cables as the export draws them (wireless or not), the
|
||||||
|
// documentation items in the model's render order.
|
||||||
|
|
||||||
|
import { readdirSync, readFileSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
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';
|
||||||
|
|
||||||
|
const corpus = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus');
|
||||||
|
const files = ['java-examples', 'java-tests', 'ipad-fixtures'].flatMap((folder) =>
|
||||||
|
readdirSync(join(corpus, folder))
|
||||||
|
.filter((name) => name.endsWith('.fls'))
|
||||||
|
.map((name) => join(folder, name)),
|
||||||
|
);
|
||||||
|
|
||||||
|
function docOf(file: string, change: (project: ImportedProject) => void = () => {}): Y.Doc {
|
||||||
|
let counter = 0;
|
||||||
|
const project = openProjectArchive(new Uint8Array(readFileSync(join(corpus, file))), () => `00000000-0000-0000-0000-${(0xffff_ffff - ++counter).toString(16).padStart(12, '0')}`);
|
||||||
|
change(project);
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
writeProject(doc, project);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('the drawing of a document', () => {
|
||||||
|
for (const file of files) {
|
||||||
|
it(file, () => {
|
||||||
|
const doc = docOf(file);
|
||||||
|
const project = readProject(doc);
|
||||||
|
const view = topologyView(doc);
|
||||||
|
expect(view.nodes.map((node) => [node.id, node.kind, node.name, node.x, node.y])).toEqual(
|
||||||
|
project.graph.nodes.map((node) => [node.id, node.kind, node.displayName, node.position.x, node.position.y]),
|
||||||
|
);
|
||||||
|
expect(view.nodes.map((node) => node.ports)).toEqual(project.graph.nodes.map((node) => node.ports.map((port) => ({ id: port.id, label: port.label }))));
|
||||||
|
expect(view.links.map((link) => [link.id, link.sourceNode, link.sourcePort, link.targetNode, link.targetPort])).toEqual(
|
||||||
|
project.graph.links.map((link) => [link.id, link.sourceNodeId, link.sourcePortId, link.targetNodeId, link.targetPortId]),
|
||||||
|
);
|
||||||
|
expect(view.links.map((link) => link.wireless)).toEqual(exportProject(project).topology.links.map((link) => link.wireless));
|
||||||
|
expect(view.docs.map(({ id, kind, x, y, width, height, text, color, font, size, bold, order }) => ({ id, kind, frame: { x, y, width, height }, text, color, fontName: font, fontSize: size, isBold: bold, order }))).toEqual(project.documentationItems);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('draws the Internet of the NAT example as a cloud', () => {
|
||||||
|
const view = topologyView(docOf('java-examples/nat.fls'));
|
||||||
|
expect(view.nodes.filter((node) => node.cloud).map((node) => node.name)).toEqual(['Internet']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes a host's label as Java does: its name, address, MAC address or both", () => {
|
||||||
|
const labels = (policy: 'manual' | 'ipAddress' | 'macAddress' | 'ipAndMAC') => {
|
||||||
|
const doc = docOf('java-examples/zwei_rechnernetze_komplett.fls', (project) => {
|
||||||
|
for (const node of project.graph.nodes) node.hostLabelPolicy = policy;
|
||||||
|
});
|
||||||
|
const project = readProject(doc);
|
||||||
|
const notebook = project.graph.nodes.find((node) => node.displayName === 'Notebook 1.10')!;
|
||||||
|
const view = topologyView(doc);
|
||||||
|
return { label: view.nodes.find((node) => node.id === notebook.id)!.label, mac: notebook.ports[0]!.importedMacAddress ?? stableGeneratedMacAddress(notebook.ports[0]!.id), switchLabel: view.nodes.find((node) => node.kind === 'networkSwitch')!.label };
|
||||||
|
};
|
||||||
|
expect(labels('manual').label).toBe('Notebook 1.10');
|
||||||
|
expect(labels('ipAddress').label).toBe('141.99.1.10');
|
||||||
|
const { label, mac, switchLabel } = labels('ipAndMAC');
|
||||||
|
expect(label).toBe(`141.99.1.10 (${mac})`);
|
||||||
|
expect(labels('macAddress').label).toBe(mac);
|
||||||
|
// Only hosts show an address (Host.holeAnzeigeName).
|
||||||
|
expect(switchLabel).toBe('Switch 1');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
// The editor's surface in Chromium (Playwright), on its built page.
|
||||||
|
//
|
||||||
|
// - Every FILIUS example drawn: its devices and cables, each device as large
|
||||||
|
// as Java made its button when it saved the file — the bounds in the files
|
||||||
|
// saved by Java 2.5 and later (before, the button's width left the label
|
||||||
|
// out, and older ones had other borders); the height within the font
|
||||||
|
// heights of the saving machines (15 to 17), the width where the label is
|
||||||
|
// wider than the icon within 16 % (the machines' fonts differ).
|
||||||
|
// - The gestures: the wheel with Ctrl zooms around the pointer, a drag moves
|
||||||
|
// the surface, a press without moving does nothing, the buttons step and fit.
|
||||||
|
// - English and dark.
|
||||||
|
|
||||||
|
import { readdirSync, readFileSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { expect, test, type Page } from '@playwright/test';
|
||||||
|
import { build, preview, type PreviewServer } from 'vite';
|
||||||
|
import { decodeJavaBeans, isJavaObject, listItems, readFlsArchive } from '@mdg-filius/fls';
|
||||||
|
import { ICONS, iconOf } from '../src/canvas/layout.js';
|
||||||
|
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const config = join(here, '../vite.config.ts');
|
||||||
|
const examples = join(here, '../../../corpus/java-examples');
|
||||||
|
|
||||||
|
let server: PreviewServer;
|
||||||
|
let base: string;
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
await build({ configFile: config, logLevel: 'silent' });
|
||||||
|
server = await preview({ configFile: config, preview: { port: 0 }, logLevel: 'silent' });
|
||||||
|
base = server.resolvedUrls!.local[0]!;
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await new Promise<void>((resolve) => server.httpServer.close(() => resolve()));
|
||||||
|
});
|
||||||
|
|
||||||
|
/** The width and height Java gave each device's button when it saved the file, in the file's order. */
|
||||||
|
function savedBounds(file: string): { width: number; height: number; javaClass: string; cloud: boolean }[] {
|
||||||
|
const values = decodeJavaBeans(readFlsArchive(new Uint8Array(readFileSync(join(examples, file)))).configurationXml);
|
||||||
|
const nodes = listItems(values[typeof values[0] === 'string' ? 1 : 0]);
|
||||||
|
return nodes.flatMap((item) => {
|
||||||
|
if (!isJavaObject(item)) return [];
|
||||||
|
const label = item.properties.get('imageLabel');
|
||||||
|
const device = item.properties.get('knoten');
|
||||||
|
const bounds = isJavaObject(label) ? label.properties.get('bounds') : undefined;
|
||||||
|
if (!isJavaObject(bounds) || !isJavaObject(device)) return [];
|
||||||
|
const field = (name: string, index: number) => (bounds.properties.get(name) ?? bounds.args[index]) as number;
|
||||||
|
return [{ width: field('width', 2), height: field('height', 3), javaClass: device.className.slice(device.className.lastIndexOf('.') + 1), cloud: device.properties.get('cloud') === true }];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The version line Java wrote at the file's start. */
|
||||||
|
function savedVersion(file: string): string | undefined {
|
||||||
|
const values = decodeJavaBeans(readFlsArchive(new Uint8Array(readFileSync(join(examples, file)))).configurationXml);
|
||||||
|
return typeof values[0] === 'string' ? values[0] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function open(page: Page, example: string, query = '') {
|
||||||
|
await page.goto(`${base}?example=${example}${query}`);
|
||||||
|
await page.waitForSelector('g.device');
|
||||||
|
}
|
||||||
|
|
||||||
|
const KIND_OF_CLASS: Record<string, 'pc' | 'notebook' | 'networkSwitch' | 'router' | 'gateway' | 'remoteLink'> = {
|
||||||
|
Rechner: 'pc',
|
||||||
|
Notebook: 'notebook',
|
||||||
|
Switch: 'networkSwitch',
|
||||||
|
Vermittlungsrechner: 'router',
|
||||||
|
Gateway: 'gateway',
|
||||||
|
Modem: 'remoteLink',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('draws every example with devices as large as Java made them', async ({ page }) => {
|
||||||
|
const deviations: string[] = [];
|
||||||
|
let compared = 0;
|
||||||
|
for (const file of readdirSync(examples).filter((name) => name.endsWith('.fls'))) {
|
||||||
|
await open(page, file);
|
||||||
|
const saved = savedBounds(file);
|
||||||
|
const version = /Filius version: (\d+)\.(\d+)/.exec(savedVersion(file) ?? '');
|
||||||
|
const comparable = version !== null && (Number(version[1]) > 2 || (Number(version[1]) === 2 && Number(version[2]) >= 5));
|
||||||
|
const devices = await page.locator('g.device').evaluateAll((elements) =>
|
||||||
|
elements.map((element) => ({ box: element.getAttribute('data-box')!.split(' ').map(Number), label: element.getAttribute('aria-label') })),
|
||||||
|
);
|
||||||
|
expect(devices.length, file).toBe(saved.length);
|
||||||
|
expect(await page.locator('path[data-link]').count(), file).toBeGreaterThan(0);
|
||||||
|
for (const [index, { box, label }] of devices.entries()) {
|
||||||
|
const java = saved[index]!;
|
||||||
|
if (!comparable) continue;
|
||||||
|
compared += 1;
|
||||||
|
const [, , width, height] = box as [number, number, number, number];
|
||||||
|
const icon = ICONS[iconOf(KIND_OF_CLASS[java.javaClass]!, java.cloud)];
|
||||||
|
// The height: icon plus 5 plus Java's font height on the machine that saved it (15 to 17).
|
||||||
|
const heightOk = height - java.height >= 0 && height - java.height <= 2;
|
||||||
|
// The width: exact where the icon is wider than the label; else the label's width, which depends on
|
||||||
|
// the saving machine's font (Java takes the system's look and feel): within 16 %.
|
||||||
|
const tolerance = java.width === icon.width + 21 && width === icon.width + 21 ? 0 : Math.max(4, java.width * 0.16);
|
||||||
|
if (!heightOk || Math.abs(width - java.width) > tolerance) deviations.push(`${file} #${index + 1} ${label}: Java ${java.width}×${java.height}, here ${width}×${height}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(compared).toBeGreaterThan(80);
|
||||||
|
expect(deviations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zooms around the pointer with Ctrl and the wheel, and moves with a drag', async ({ page }) => {
|
||||||
|
await open(page, 'internetwork_komplett.fls');
|
||||||
|
const zoom = () => page.locator('[data-zoom]').getAttribute('data-zoom').then(Number);
|
||||||
|
const device = page.locator('g.device').first();
|
||||||
|
const before = (await device.boundingBox())!;
|
||||||
|
const start = await zoom();
|
||||||
|
// Zoom in at the device's middle: it stays where it is, and grows.
|
||||||
|
const point = { x: before.x + before.width / 2, y: before.y + before.height / 2 };
|
||||||
|
await page.mouse.move(point.x, point.y);
|
||||||
|
await page.keyboard.down('Control');
|
||||||
|
await page.mouse.wheel(0, -100);
|
||||||
|
await page.keyboard.up('Control');
|
||||||
|
await expect.poll(zoom).toBeGreaterThan(start);
|
||||||
|
const after = (await device.boundingBox())!;
|
||||||
|
expect(Math.abs(after.x + after.width / 2 - point.x)).toBeLessThan(1.5);
|
||||||
|
expect(Math.abs(after.y + after.height / 2 - point.y)).toBeLessThan(1.5);
|
||||||
|
expect(after.width).toBeGreaterThan(before.width);
|
||||||
|
|
||||||
|
// A press without moving leaves the view; a drag moves it as far as the pointer went.
|
||||||
|
const surface = (await page.locator('svg.filius-canvas').boundingBox())!;
|
||||||
|
const empty = { x: surface.x + 20, y: surface.y + surface.height - 20 };
|
||||||
|
await page.mouse.move(empty.x, empty.y);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.up();
|
||||||
|
expect((await device.boundingBox())!.x).toBeCloseTo(after.x, 1);
|
||||||
|
await page.mouse.move(empty.x, empty.y);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(empty.x + 60, empty.y - 40, { steps: 6 });
|
||||||
|
await page.mouse.up();
|
||||||
|
const moved = (await device.boundingBox())!;
|
||||||
|
expect(moved.x - after.x).toBeCloseTo(60, 0);
|
||||||
|
expect(moved.y - after.y).toBeCloseTo(-40, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("steps with the buttons, shows all, and keeps to Java's limits", async ({ page }) => {
|
||||||
|
await open(page, 'internetwork_komplett.fls');
|
||||||
|
const zoom = () => page.locator('[data-zoom]').getAttribute('data-zoom').then(Number);
|
||||||
|
await page.getByRole('button', { name: 'Vergrößern' }).click();
|
||||||
|
expect(await zoom()).toBe(1.25);
|
||||||
|
for (let press = 0; press < 6; press += 1) await page.getByRole('button', { name: 'Vergrößern' }).click({ force: true });
|
||||||
|
expect(await zoom()).toBe(2);
|
||||||
|
await expect(page.getByRole('button', { name: 'Vergrößern' })).toBeDisabled();
|
||||||
|
await page.getByRole('button', { name: 'Alles zeigen' }).click();
|
||||||
|
const surface = (await page.locator('svg.filius-canvas').boundingBox())!;
|
||||||
|
for (const box of await page.locator('g.device').evaluateAll((elements) => elements.map((element) => element.getBoundingClientRect().toJSON() as DOMRect))) {
|
||||||
|
expect(box.left).toBeGreaterThanOrEqual(surface.x);
|
||||||
|
expect(box.right).toBeLessThanOrEqual(surface.x + surface.width);
|
||||||
|
expect(box.top).toBeGreaterThanOrEqual(surface.y);
|
||||||
|
expect(box.bottom).toBeLessThanOrEqual(surface.y + surface.height);
|
||||||
|
}
|
||||||
|
for (let press = 0; press < 8; press += 1) await page.getByRole('button', { name: 'Verkleinern' }).click({ force: true });
|
||||||
|
expect(await zoom()).toBe(0.25);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('speaks English and goes dark when asked', async ({ page }) => {
|
||||||
|
await open(page, 'nat.fls', '&lang=en&theme=dark');
|
||||||
|
await expect(page.getByRole('img', { name: 'Home Router: Heimrouter' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: 'Zoom in' })).toBeVisible();
|
||||||
|
expect(await page.evaluate(() => document.documentElement.dataset.theme)).toBe('dark');
|
||||||
|
await expect(page.getByRole('img', { name: 'Switch / Wi-Fi: Internet' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zooms with two fingers around their middle, and moves with one', async ({ browser }) => {
|
||||||
|
const context = await browser.newContext({ hasTouch: true, viewport: { width: 1024, height: 768 } });
|
||||||
|
const page = await context.newPage();
|
||||||
|
await open(page, 'internetwork_komplett.fls');
|
||||||
|
const zoom = () => page.locator('[data-zoom]').getAttribute('data-zoom').then(Number);
|
||||||
|
const cdp = await context.newCDPSession(page);
|
||||||
|
const touch = (type: 'touchStart' | 'touchMove' | 'touchEnd', points: readonly [number, number][]) =>
|
||||||
|
cdp.send('Input.dispatchTouchEvent', { type, touchPoints: points.map(([x, y], id) => ({ x, y, id })) });
|
||||||
|
const device = page.locator('g.device').nth(4);
|
||||||
|
const before = (await device.boundingBox())!;
|
||||||
|
const centre = { x: before.x + before.width / 2, y: before.y + before.height / 2 };
|
||||||
|
const start = await zoom();
|
||||||
|
// Two fingers 40 px either side of the device, spread to 80.
|
||||||
|
await touch('touchStart', [
|
||||||
|
[centre.x - 40, centre.y],
|
||||||
|
[centre.x + 40, centre.y],
|
||||||
|
]);
|
||||||
|
for (const distance of [50, 60, 70, 80]) {
|
||||||
|
await touch('touchMove', [
|
||||||
|
[centre.x - distance, centre.y],
|
||||||
|
[centre.x + distance, centre.y],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await touch('touchEnd', []);
|
||||||
|
expect(await zoom()).toBeCloseTo(start * 2 > 2 ? 2 : start * 2, 1);
|
||||||
|
const after = (await device.boundingBox())!;
|
||||||
|
expect(Math.abs(after.x + after.width / 2 - centre.x)).toBeLessThan(2);
|
||||||
|
expect(Math.abs(after.y + after.height / 2 - centre.y)).toBeLessThan(2);
|
||||||
|
|
||||||
|
// One finger moves the surface.
|
||||||
|
await touch('touchStart', [[500, 700]]);
|
||||||
|
for (const step of [10, 20, 30, 40, 50]) await touch('touchMove', [[500 + step, 700]]);
|
||||||
|
await touch('touchEnd', []);
|
||||||
|
expect((await device.boundingBox())!.x - after.x).toBeCloseTo(50, 0);
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('moves with the wheel alone, and takes a press that hardly moved for no drag', async ({ page }) => {
|
||||||
|
await open(page, 'internetwork_komplett.fls');
|
||||||
|
const zoom = () => page.locator('[data-zoom]').getAttribute('data-zoom').then(Number);
|
||||||
|
const device = page.locator('g.device').first();
|
||||||
|
const before = (await device.boundingBox())!;
|
||||||
|
const start = await zoom();
|
||||||
|
await page.mouse.move(before.x, before.y);
|
||||||
|
await page.mouse.wheel(30, 50);
|
||||||
|
await expect.poll(async () => (await device.boundingBox())!.y).toBeCloseTo(before.y - 50, 0);
|
||||||
|
expect((await device.boundingBox())!.x).toBeCloseTo(before.x - 30, 0);
|
||||||
|
expect(await zoom()).toBe(start);
|
||||||
|
|
||||||
|
// Two pixels are a press, not a drag (DRAG_THRESHOLD): the surface stays.
|
||||||
|
const placed = (await device.boundingBox())!;
|
||||||
|
const surface = (await page.locator('svg.filius-canvas').boundingBox())!;
|
||||||
|
await page.mouse.move(surface.x + 20, surface.y + surface.height - 20);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(surface.x + 22, surface.y + surface.height - 20);
|
||||||
|
await page.mouse.up();
|
||||||
|
expect((await device.boundingBox())!.x).toBe(placed.x);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows all of a large example at first', async ({ page }) => {
|
||||||
|
await open(page, 'Internet_Komplett_mit_eMail_Webserver_Intranet_Portforwarding_Firewall_DHCP_DE.fls');
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
const zoom = Number(await page.locator('[data-zoom]').getAttribute('data-zoom'));
|
||||||
|
expect(zoom).toBeLessThan(1);
|
||||||
|
const surface = (await page.locator('svg.filius-canvas').boundingBox())!;
|
||||||
|
for (const box of await page.locator('g.device').evaluateAll((elements) => elements.map((element) => element.getBoundingClientRect().toJSON() as DOMRect))) {
|
||||||
|
expect(box.left).toBeGreaterThanOrEqual(surface.x);
|
||||||
|
expect(box.right).toBeLessThanOrEqual(surface.x + surface.width);
|
||||||
|
expect(box.top).toBeGreaterThanOrEqual(surface.y);
|
||||||
|
expect(box.bottom).toBeLessThanOrEqual(surface.y + surface.height);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||||
|
<title>Filius</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@mdg-filius/editor",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"license": "GPL-3.0-only",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mdg-filius/core": "0.0.0",
|
||||||
|
"@mdg-filius/doc": "0.0.0",
|
||||||
|
"@mdg-filius/fls": "0.0.0",
|
||||||
|
"@mdg-filius/protocol": "0.0.0",
|
||||||
|
"@mdg-filius/sim": "0.0.0",
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"yjs": "^13.6.33"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.31",
|
||||||
|
"@types/react-dom": "^18.3.7",
|
||||||
|
"@vitejs/plugin-react": "^5.2.0",
|
||||||
|
"vite": "^7.3.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// The editor on a document: for now the surface that draws it (F4.3); the
|
||||||
|
// tools, the device sheet and the simulation follow (F4.4–F4.8).
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import type * as Y from 'yjs';
|
||||||
|
import { topologyView, type TopologyView } from '@mdg-filius/doc';
|
||||||
|
import { Canvas } from './canvas/Canvas.js';
|
||||||
|
|
||||||
|
/** The document's drawing, read anew after every change. */
|
||||||
|
export function useTopologyView(doc: Y.Doc): TopologyView {
|
||||||
|
const [view, setView] = useState(() => topologyView(doc));
|
||||||
|
useEffect(() => {
|
||||||
|
setView(topologyView(doc));
|
||||||
|
const update = () => setView(topologyView(doc));
|
||||||
|
doc.on('update', update);
|
||||||
|
return () => doc.off('update', update);
|
||||||
|
}, [doc]);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Editor({ doc }: { readonly doc: Y.Doc }) {
|
||||||
|
const view = useTopologyView(doc);
|
||||||
|
return <Canvas view={view} />;
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,50 @@
|
|||||||
|
// The bridge to the dashboard (plan §9; the pattern of Scratch's
|
||||||
|
// lib/bridge.js and its twin dashboard-bridge.js): the editor runs in an
|
||||||
|
// iframe on another host, and between the two there is only postMessage.
|
||||||
|
// This file holds names, a version and the origin check, nothing that could
|
||||||
|
// drift apart from its twin in the dashboard (lib/bridge.js, F6). Every
|
||||||
|
// message is checked against the expected origin, both ways.
|
||||||
|
|
||||||
|
export const BRIDGE_VERSION = 1;
|
||||||
|
|
||||||
|
/** Dashboard → editor. */
|
||||||
|
export const COMMANDS = {
|
||||||
|
/** `{ locale: 'de' | 'en' }` */
|
||||||
|
SET_LOCALE: 'setLocale',
|
||||||
|
/** `{ theme: 'light' | 'dark', tokens?: { [name]: value } }`: the dashboard's resolved colours. */
|
||||||
|
SET_THEME: 'setTheme',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Editor → dashboard. */
|
||||||
|
export const EVENTS = {
|
||||||
|
/** The editor listens. */
|
||||||
|
READY: 'ready',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface BridgeHandlers {
|
||||||
|
readonly onLocale: (locale: string) => void;
|
||||||
|
readonly onTheme: (theme: 'light' | 'dark', tokens: Readonly<Record<string, string>>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
readonly parent: { postMessage(message: unknown, targetOrigin: string): void };
|
||||||
|
addEventListener(type: 'message', listener: (event: { readonly origin: string; readonly data: unknown }) => void): void;
|
||||||
|
removeEventListener(type: 'message', listener: (event: { readonly origin: string; readonly data: unknown }) => void): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Listens to the dashboard at `parentOrigin` and tells it the editor is ready; returns how to stop. */
|
||||||
|
export function startBridge(window: Window, parentOrigin: string, handlers: BridgeHandlers): () => void {
|
||||||
|
const listener = (event: { readonly origin: string; readonly data: unknown }) => {
|
||||||
|
if (event.origin !== parentOrigin) return;
|
||||||
|
const data = event.data as { v?: unknown; type?: unknown; locale?: unknown; theme?: unknown; tokens?: unknown } | null;
|
||||||
|
if (data === null || typeof data !== 'object' || data.v !== BRIDGE_VERSION) return;
|
||||||
|
if (data.type === COMMANDS.SET_LOCALE && typeof data.locale === 'string') handlers.onLocale(data.locale);
|
||||||
|
if (data.type === COMMANDS.SET_THEME && (data.theme === 'light' || data.theme === 'dark')) {
|
||||||
|
const tokens = data.tokens !== null && typeof data.tokens === 'object' ? (data.tokens as Record<string, unknown>) : {};
|
||||||
|
handlers.onTheme(data.theme, Object.fromEntries(Object.entries(tokens).filter((entry): entry is [string, string] => entry[0].startsWith('--') && typeof entry[1] === 'string')));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('message', listener);
|
||||||
|
window.parent.postMessage({ v: BRIDGE_VERSION, type: EVENTS.READY }, parentOrigin);
|
||||||
|
return () => window.removeEventListener('message', listener);
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
// The surface (plan §7.1): the devices with Java's icons and labels, the
|
||||||
|
// cables, the documentation items, in an SVG that one moves with a finger or
|
||||||
|
// the mouse and zooms with two fingers, a pinch on the trackpad, Ctrl and the
|
||||||
|
// wheel or the buttons.
|
||||||
|
//
|
||||||
|
// The pointer is captured only once it has moved: captured at once, it would
|
||||||
|
// take the click from whatever lies under it (pointer-capture trap; jsdom
|
||||||
|
// never shows it, Playwright does). The surface sets touch-action: none, so
|
||||||
|
// that the browser's own zoom and scroll stay out of a gesture on it (M7).
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
||||||
|
import type { DocumentationView, NodeView, TopologyView } from '@mdg-filius/doc';
|
||||||
|
import { useTranslate } from '../i18n/i18n.js';
|
||||||
|
import { ICON_URLS } from './icons.js';
|
||||||
|
import { bounds, cablePath, deviceBox, JAVA_LABEL_FONT, type DeviceBox } from './layout.js';
|
||||||
|
import { fitted, middle, shifted, spread, zoomedAt, zoomIn, zoomOut, type Point, type Size, type Viewport } from './viewport.js';
|
||||||
|
import { ZoomBar } from './ZoomBar.js';
|
||||||
|
|
||||||
|
/** How far a pointer moves before a press becomes a drag. */
|
||||||
|
const DRAG_THRESHOLD = 4;
|
||||||
|
|
||||||
|
/** The label's width in Java's font, measured once per text. */
|
||||||
|
function useTextWidth(): (text: string) => number {
|
||||||
|
const cache = useRef(new Map<string, number>());
|
||||||
|
const context = useMemo(() => document.createElement('canvas').getContext('2d'), []);
|
||||||
|
return useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
const known = cache.current.get(text);
|
||||||
|
if (known !== undefined) return known;
|
||||||
|
if (context === null) return text.length * 7;
|
||||||
|
context.font = JAVA_LABEL_FONT;
|
||||||
|
const width = context.measureText(text).width;
|
||||||
|
cache.current.set(text, width);
|
||||||
|
return width;
|
||||||
|
},
|
||||||
|
[context],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rgba = (color: DocumentationView['color']) =>
|
||||||
|
`rgba(${Math.round(color.red * 255)}, ${Math.round(color.green * 255)}, ${Math.round(color.blue * 255)}, ${color.alpha})`;
|
||||||
|
|
||||||
|
function Documentation({ item }: { readonly item: DocumentationView }) {
|
||||||
|
if (item.kind === 'rectangle') {
|
||||||
|
return <rect className="doc-rectangle" x={item.x} y={item.y} width={item.width} height={item.height} fill={rgba(item.color)} data-doc={item.id} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<foreignObject x={item.x} y={item.y} width={item.width} height={item.height} data-doc={item.id}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: rgba(item.color),
|
||||||
|
font: `${item.bold ? 'bold ' : ''}${item.size}px "${item.font}", Dialog, sans-serif`,
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.text}
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Device({ node, box, kindName }: { readonly node: NodeView; readonly box: DeviceBox; readonly kindName: string }) {
|
||||||
|
return (
|
||||||
|
<g className="device" data-node={node.id} data-box={`${box.x} ${box.y} ${box.width} ${box.height}`} role="img" aria-label={`${kindName}: ${node.label}`}>
|
||||||
|
<image href={ICON_URLS[box.icon]} x={box.iconRect.x} y={box.iconRect.y} width={box.iconRect.width} height={box.iconRect.height} />
|
||||||
|
<text x={box.label.x} y={box.label.y} textAnchor="middle" style={{ font: JAVA_LABEL_FONT, fill: 'var(--canvas-label)' }}>
|
||||||
|
{node.label}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CanvasProps {
|
||||||
|
readonly view: TopologyView;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Canvas({ view }: CanvasProps) {
|
||||||
|
const t = useTranslate();
|
||||||
|
const surface = useRef<SVGSVGElement>(null);
|
||||||
|
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
|
||||||
|
const [viewport, setViewport] = useState<Viewport | null>(null);
|
||||||
|
const textWidth = useTextWidth();
|
||||||
|
|
||||||
|
const boxes = useMemo(() => new Map(view.nodes.map((node) => [node.id, deviceBox(node, textWidth(node.label))])), [view.nodes, textWidth]);
|
||||||
|
const content = useMemo(() => bounds([...boxes.values(), ...view.docs.map((item) => ({ x: item.x, y: item.y, width: item.width, height: item.height }))]), [boxes, view.docs]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const element = surface.current;
|
||||||
|
if (element === null) return undefined;
|
||||||
|
const observer = new ResizeObserver(([entry]) => setSize({ width: entry!.contentRect.width, height: entry!.contentRect.height }));
|
||||||
|
observer.observe(element);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// The first view shows all; after that the view is the user's.
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewport === null && size.width > 0 && size.height > 0) setViewport(fitted(content, size));
|
||||||
|
}, [viewport, size, content]);
|
||||||
|
|
||||||
|
const current = viewport ?? { scale: 1, x: 0, y: 0 };
|
||||||
|
const local = useCallback((clientX: number, clientY: number): Point => {
|
||||||
|
const rect = surface.current!.getBoundingClientRect();
|
||||||
|
return { x: clientX - rect.left, y: clientY - rect.top };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Pointers down on the surface, and what they started.
|
||||||
|
const pointers = useRef(new Map<number, Point>());
|
||||||
|
const gesture = useRef<{ kind: 'press'; start: Point } | { kind: 'pan'; last: Point } | { kind: 'pinch'; spread: number; middle: Point } | null>(null);
|
||||||
|
|
||||||
|
const onPointerDown = (event: React.PointerEvent<SVGSVGElement>) => {
|
||||||
|
const point = local(event.clientX, event.clientY);
|
||||||
|
pointers.current.set(event.pointerId, point);
|
||||||
|
if (pointers.current.size === 2) {
|
||||||
|
const [first, second] = [...pointers.current.values()] as [Point, Point];
|
||||||
|
gesture.current = { kind: 'pinch', spread: spread(first, second), middle: middle(first, second) };
|
||||||
|
for (const id of pointers.current.keys()) if (!surface.current!.hasPointerCapture(id)) surface.current!.setPointerCapture(id);
|
||||||
|
} else if (pointers.current.size === 1) {
|
||||||
|
gesture.current = { kind: 'press', start: point };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerMove = (event: React.PointerEvent<SVGSVGElement>) => {
|
||||||
|
if (!pointers.current.has(event.pointerId)) return;
|
||||||
|
const point = local(event.clientX, event.clientY);
|
||||||
|
pointers.current.set(event.pointerId, point);
|
||||||
|
const now = gesture.current;
|
||||||
|
if (now === null) return;
|
||||||
|
if (now.kind === 'press') {
|
||||||
|
if (Math.hypot(point.x - now.start.x, point.y - now.start.y) < DRAG_THRESHOLD) return;
|
||||||
|
// Captured only now that it is a drag.
|
||||||
|
surface.current!.setPointerCapture(event.pointerId);
|
||||||
|
gesture.current = { kind: 'pan', last: now.start };
|
||||||
|
}
|
||||||
|
const active = gesture.current!;
|
||||||
|
if (active.kind === 'pan') {
|
||||||
|
setViewport((before) => shifted(before ?? current, { x: point.x - active.last.x, y: point.y - active.last.y }));
|
||||||
|
gesture.current = { kind: 'pan', last: point };
|
||||||
|
} else if (active.kind === 'pinch' && pointers.current.size === 2) {
|
||||||
|
const [first, second] = [...pointers.current.values()] as [Point, Point];
|
||||||
|
const nextSpread = spread(first, second);
|
||||||
|
const nextMiddle = middle(first, second);
|
||||||
|
setViewport((before) => {
|
||||||
|
const base = before ?? current;
|
||||||
|
const zoomed = zoomedAt(base, base.scale * (nextSpread / Math.max(1, active.spread)), active.middle);
|
||||||
|
return shifted(zoomed, { x: nextMiddle.x - active.middle.x, y: nextMiddle.y - active.middle.y });
|
||||||
|
});
|
||||||
|
gesture.current = { kind: 'pinch', spread: nextSpread, middle: nextMiddle };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = (event: React.PointerEvent<SVGSVGElement>) => {
|
||||||
|
pointers.current.delete(event.pointerId);
|
||||||
|
if (surface.current?.hasPointerCapture(event.pointerId) === true) surface.current.releasePointerCapture(event.pointerId);
|
||||||
|
// A pinch that loses a finger goes on as a pan with the other.
|
||||||
|
const [remaining] = [...pointers.current.values()];
|
||||||
|
gesture.current = remaining === undefined ? null : { kind: 'pan', last: remaining };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The wheel: with Ctrl (and a trackpad's pinch, which comes as Ctrl and the wheel) it zooms, else it moves.
|
||||||
|
// Registered by hand: React's wheel listener is passive and could not keep the page from zooming.
|
||||||
|
useEffect(() => {
|
||||||
|
const element = surface.current;
|
||||||
|
if (element === null) return undefined;
|
||||||
|
const onWheel = (event: WheelEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const point = local(event.clientX, event.clientY);
|
||||||
|
setViewport((before) => {
|
||||||
|
const base = before ?? { scale: 1, x: 0, y: 0 };
|
||||||
|
if (event.ctrlKey || event.metaKey) return zoomedAt(base, base.scale * Math.exp(-event.deltaY * 0.01), point);
|
||||||
|
return shifted(base, { x: -event.deltaX, y: -event.deltaY });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
element.addEventListener('wheel', onWheel, { passive: false });
|
||||||
|
return () => element.removeEventListener('wheel', onWheel);
|
||||||
|
}, [local]);
|
||||||
|
|
||||||
|
const centre = { x: size.width / 2, y: size.height / 2 };
|
||||||
|
const style: CSSProperties = { width: '100%', height: '100%', display: 'block', background: 'var(--canvas-bg)', touchAction: 'none', userSelect: 'none' };
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||||
|
<svg
|
||||||
|
ref={surface}
|
||||||
|
className="filius-canvas"
|
||||||
|
role="application"
|
||||||
|
aria-label={t('canvas.label')}
|
||||||
|
style={style}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
onPointerCancel={onPointerUp}
|
||||||
|
>
|
||||||
|
<g className="surface" transform={`translate(${current.x} ${current.y}) scale(${current.scale})`}>
|
||||||
|
<g className="docs">
|
||||||
|
{view.docs.map((item) => (
|
||||||
|
<Documentation key={item.id} item={item} />
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
<g className="cables" fill="none" strokeWidth={2}>
|
||||||
|
{view.links.map((link) => {
|
||||||
|
const [source, target] = [boxes.get(link.sourceNode), boxes.get(link.targetNode)];
|
||||||
|
// Drawn tolerantly: a cable whose device is gone (before the janitor) is left out.
|
||||||
|
if (source === undefined || target === undefined) return null;
|
||||||
|
return (
|
||||||
|
<path
|
||||||
|
key={link.id}
|
||||||
|
data-link={link.id}
|
||||||
|
d={cablePath(source.center, target.center)}
|
||||||
|
stroke={link.wireless ? 'var(--canvas-cable-wireless)' : 'var(--canvas-cable)'}
|
||||||
|
strokeDasharray={link.wireless ? '10' : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
<g className="devices">
|
||||||
|
{view.nodes.map((node) => (
|
||||||
|
<Device key={node.id} node={node} box={boxes.get(node.id)!} kindName={t(`device.${node.kind}`)} />
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<ZoomBar
|
||||||
|
scale={current.scale}
|
||||||
|
onZoomIn={() => setViewport(zoomedAt(current, zoomIn(current.scale), centre))}
|
||||||
|
onZoomOut={() => setViewport(zoomedAt(current, zoomOut(current.scale), centre))}
|
||||||
|
onFit={() => setViewport(fitted(content, size))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// The zoom's buttons: out, the scale, in, show all. Each a target of at least
|
||||||
|
// 44 px for a finger (plan §7.2).
|
||||||
|
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
import { useTranslate } from '../i18n/i18n.js';
|
||||||
|
import { MAX_ZOOM, MIN_ZOOM } from './viewport.js';
|
||||||
|
|
||||||
|
const bar: CSSProperties = {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 12,
|
||||||
|
bottom: 12,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 2,
|
||||||
|
padding: 2,
|
||||||
|
borderRadius: 8,
|
||||||
|
background: 'var(--surface-raised)',
|
||||||
|
border: '1px solid var(--border-default)',
|
||||||
|
boxShadow: '0 1px 3px color-mix(in srgb, var(--shadow-color) 20%, transparent)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const button: CSSProperties = {
|
||||||
|
minWidth: 44,
|
||||||
|
height: 44,
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 6,
|
||||||
|
background: 'transparent',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
font: 'inherit',
|
||||||
|
fontSize: 18,
|
||||||
|
cursor: 'pointer',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ZoomBar({ scale, onZoomIn, onZoomOut, onFit }: { readonly scale: number; readonly onZoomIn: () => void; readonly onZoomOut: () => void; readonly onFit: () => void }) {
|
||||||
|
const t = useTranslate();
|
||||||
|
return (
|
||||||
|
<div style={bar} role="toolbar">
|
||||||
|
<button type="button" style={button} onClick={onZoomOut} disabled={scale <= MIN_ZOOM + 0.001} aria-label={t('zoom.out')} title={t('zoom.out')}>
|
||||||
|
−
|
||||||
|
</button>
|
||||||
|
<span style={{ minWidth: 56, textAlign: 'center', fontSize: 14, color: 'var(--text-secondary)' }} data-zoom={scale}>
|
||||||
|
{t('zoom.level', { percent: Math.round(scale * 100) })}
|
||||||
|
</span>
|
||||||
|
<button type="button" style={button} onClick={onZoomIn} disabled={scale >= MAX_ZOOM - 0.001} aria-label={t('zoom.in')} title={t('zoom.in')}>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
<button type="button" style={{ ...button, fontSize: 16 }} onClick={onFit} aria-label={t('zoom.fit')} title={t('zoom.fit')}>
|
||||||
|
⤢
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Java FILIUS' own hardware icons (GPL, src/main/resources/gfx/hardware),
|
||||||
|
// so that teachers recognise their worksheets (plan §7.1).
|
||||||
|
|
||||||
|
import cloud from '../assets/hardware/cloud.png';
|
||||||
|
import gateway from '../assets/hardware/gateway.png';
|
||||||
|
import laptop from '../assets/hardware/laptop.png';
|
||||||
|
import modem from '../assets/hardware/modem.png';
|
||||||
|
import router from '../assets/hardware/router.png';
|
||||||
|
import server from '../assets/hardware/server.png';
|
||||||
|
import networkSwitch from '../assets/hardware/switch.png';
|
||||||
|
import type { IconName } from './layout.js';
|
||||||
|
|
||||||
|
export const ICON_URLS: Readonly<Record<IconName, string>> = { cloud, gateway, laptop, modem, router, server, switch: networkSwitch };
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
// Where Java draws a device and a cable (filius.gui.netzwerksicht), so that a
|
||||||
|
// project looks as on the school's PCs and worksheets fit.
|
||||||
|
//
|
||||||
|
// A device is a JSidebarButton: its icon above, its label below, centred. The
|
||||||
|
// position a file stores is the button's top left corner. Its width is the
|
||||||
|
// wider of label and icon plus 5 and twice the border of 8, its height the
|
||||||
|
// font's height plus the icon's plus 5 (JSidebarButton.getWidth/getHeight);
|
||||||
|
// the icon sits at the top. A cable runs between the middles of the two
|
||||||
|
// buttons — a little below the icons' middles — as a quadratic curve whose
|
||||||
|
// control point lies a quarter, or for a falling line three quarters, across
|
||||||
|
// the rectangle the two ends span (JCablePanel.paintComponent, Java's
|
||||||
|
// default icons; integer arithmetic as Java's).
|
||||||
|
|
||||||
|
import type { NodeKind } from '@mdg-filius/core';
|
||||||
|
import type { Point, Rect } from './viewport.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The label's font. Java takes the system's look and feel (Main: the system
|
||||||
|
* look and feel), so its labels are as large as the saving machine's: in the
|
||||||
|
* bounds of the files Java 2.5 and later saved, mostly a font 17 high whose
|
||||||
|
* widths are Arial's at 14 px ("Vermittlungsrechner" 126 wide in Java, 125
|
||||||
|
* here). Arial is what most of them ran on (Windows) and what the editor
|
||||||
|
* mostly runs on (Helvetica on an iPad has its widths).
|
||||||
|
*/
|
||||||
|
export const JAVA_LABEL_FONT = '14px Arial, Helvetica, "Liberation Sans", "DejaVu Sans", sans-serif';
|
||||||
|
/** Its FontMetrics.getHeight, as most of the files' bounds have it. */
|
||||||
|
export const JAVA_LABEL_FONT_HEIGHT = 17;
|
||||||
|
const JAVA_LABEL_ASCENT = 14;
|
||||||
|
const BORDER = 8;
|
||||||
|
|
||||||
|
/** Java's default icons (IconMode.DEFAULT) and their sizes. */
|
||||||
|
export const ICONS = {
|
||||||
|
server: { width: 64, height: 94 },
|
||||||
|
laptop: { width: 84, height: 80 },
|
||||||
|
switch: { width: 70, height: 52 },
|
||||||
|
cloud: { width: 70, height: 52 },
|
||||||
|
router: { width: 77, height: 58 },
|
||||||
|
gateway: { width: 77, height: 58 },
|
||||||
|
modem: { width: 80, height: 56 },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type IconName = keyof typeof ICONS;
|
||||||
|
|
||||||
|
export function iconOf(kind: NodeKind, cloud: boolean): IconName {
|
||||||
|
switch (kind) {
|
||||||
|
case 'pc':
|
||||||
|
return 'server';
|
||||||
|
case 'notebook':
|
||||||
|
return 'laptop';
|
||||||
|
case 'networkSwitch':
|
||||||
|
return cloud ? 'cloud' : 'switch';
|
||||||
|
case 'router':
|
||||||
|
return 'router';
|
||||||
|
case 'gateway':
|
||||||
|
return 'gateway';
|
||||||
|
case 'remoteLink':
|
||||||
|
return 'modem';
|
||||||
|
case 'unsupported':
|
||||||
|
// As the iPad app draws a device it does not know.
|
||||||
|
return 'cloud';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceBox extends Rect {
|
||||||
|
readonly icon: IconName;
|
||||||
|
readonly iconRect: Rect;
|
||||||
|
/** The label's baseline, centred at x. */
|
||||||
|
readonly label: Point;
|
||||||
|
/** Where cables meet the device. */
|
||||||
|
readonly center: Point;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A device's button, as JSidebarButton lays it out, with the label's width as the browser measures it. */
|
||||||
|
export function deviceBox(node: { readonly kind: NodeKind; readonly cloud: boolean; readonly x: number; readonly y: number }, labelWidth: number): DeviceBox {
|
||||||
|
const icon = iconOf(node.kind, node.cloud);
|
||||||
|
const size = ICONS[icon];
|
||||||
|
const width = Math.max(Math.round(labelWidth), size.width) + 5 + 2 * BORDER;
|
||||||
|
const height = JAVA_LABEL_FONT_HEIGHT + size.height + 5;
|
||||||
|
return {
|
||||||
|
x: node.x,
|
||||||
|
y: node.y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
icon,
|
||||||
|
iconRect: { x: node.x + (width - size.width) / 2, y: node.y, width: size.width, height: size.height },
|
||||||
|
label: { x: node.x + width / 2, y: node.y + size.height + JAVA_LABEL_ASCENT },
|
||||||
|
center: { x: Math.trunc(node.x + 0.5 * width), y: Math.trunc(node.y + 0.5 * height) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JCablePanel's curve between two devices' middles, as an SVG path. */
|
||||||
|
export function cablePath(first: Point, second: Point): string {
|
||||||
|
const [x1, y1, x2, y2] = [first.x, first.y, second.x, second.y];
|
||||||
|
// The panel spans the two ends with 2 to spare (updateBounds); the control point is relative to it.
|
||||||
|
const left = Math.min(x1, x2) - 2;
|
||||||
|
const top = Math.min(y1, y2) - 2;
|
||||||
|
let controlX = Math.trunc((x1 - left + x2 - left) / 4);
|
||||||
|
if ((x1 > x2 && y1 > y2) || (x1 < x2 && y1 < y2)) controlX = 3 * controlX;
|
||||||
|
const controlY = Math.trunc((y1 - top + y2 - top) / 4);
|
||||||
|
return `M ${x1} ${y1} Q ${left + controlX} ${top + controlY} ${x2} ${y2}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The rectangle around all of them. */
|
||||||
|
export function bounds(rects: readonly Rect[]): Rect {
|
||||||
|
if (rects.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
|
||||||
|
const left = Math.min(...rects.map((rect) => rect.x));
|
||||||
|
const top = Math.min(...rects.map((rect) => rect.y));
|
||||||
|
const right = Math.max(...rects.map((rect) => rect.x + rect.width));
|
||||||
|
const bottom = Math.max(...rects.map((rect) => rect.y + rect.height));
|
||||||
|
return { x: left, y: top, width: right - left, height: bottom - top };
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// The view onto the surface: how far it is zoomed and shifted — the
|
||||||
|
// arithmetic behind it, without a browser.
|
||||||
|
//
|
||||||
|
// Copied from the dashboard's Kanban pinboard (frontend/src/apps/kanban/
|
||||||
|
// zoom.js; the rights are ours, plan §7.1) and changed from a scrolled
|
||||||
|
// container to a transform, as an SVG surface wants it:
|
||||||
|
//
|
||||||
|
// screen pixel = surface pixel × scale + shift
|
||||||
|
//
|
||||||
|
// Surface pixels are Java's: what the document stores and the .fls file has.
|
||||||
|
// Screen pixels are what the browser reports for a pointer.
|
||||||
|
|
||||||
|
export const MIN_ZOOM = 0.25;
|
||||||
|
export const MAX_ZOOM = 2;
|
||||||
|
|
||||||
|
/** The steps of the two buttons: a fixed ladder, so that one gets back to 100 % without searching. */
|
||||||
|
export const ZOOM_STEPS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2] as const;
|
||||||
|
|
||||||
|
export interface Point {
|
||||||
|
readonly x: number;
|
||||||
|
readonly y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Size {
|
||||||
|
readonly width: number;
|
||||||
|
readonly height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rect extends Point, Size {}
|
||||||
|
|
||||||
|
export interface Viewport {
|
||||||
|
readonly scale: number;
|
||||||
|
/** Where the surface's origin is on the screen. */
|
||||||
|
readonly x: number;
|
||||||
|
readonly y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clampZoom = (zoom: number): number => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
|
||||||
|
|
||||||
|
/** The next step up, from any value in between. */
|
||||||
|
export function zoomIn(zoom: number): number {
|
||||||
|
return ZOOM_STEPS.find((step) => step > zoom + 0.001) ?? MAX_ZOOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The next step down. */
|
||||||
|
export function zoomOut(zoom: number): number {
|
||||||
|
const smaller = ZOOM_STEPS.filter((step) => step < zoom - 0.001);
|
||||||
|
return smaller.at(-1) ?? MIN_ZOOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toSurface(viewport: Viewport, point: Point): Point {
|
||||||
|
return { x: (point.x - viewport.x) / viewport.scale, y: (point.y - viewport.y) / viewport.scale };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toScreen(viewport: Viewport, point: Point): Point {
|
||||||
|
return { x: point.x * viewport.scale + viewport.x, y: point.y * viewport.scale + viewport.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zoomed to a scale so that a point stays put — the pointer for the wheel,
|
||||||
|
* the middle between two fingers, the middle of the view for the buttons.
|
||||||
|
* Without it the content jumps away from where one zooms.
|
||||||
|
*/
|
||||||
|
export function zoomedAt(viewport: Viewport, scale: number, point: Point): Viewport {
|
||||||
|
const to = clampZoom(scale);
|
||||||
|
const surface = toSurface(viewport, point);
|
||||||
|
return { scale: to, x: point.x - surface.x * to, y: point.y - surface.y * to };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shifted(viewport: Viewport, by: Point): Viewport {
|
||||||
|
return { ...viewport, x: viewport.x + by.x, y: viewport.y + by.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view in which the content fits, in the middle, with a margin. Never
|
||||||
|
* above 100 %: fitting means zooming out until all is in view; three devices
|
||||||
|
* on a large screen at 200 % is not what anyone means by it.
|
||||||
|
*/
|
||||||
|
export function fitted(content: Rect, view: Size, margin = 24): Viewport {
|
||||||
|
if (!(content.width > 0) || !(content.height > 0) || !(view.width > 2 * margin) || !(view.height > 2 * margin)) {
|
||||||
|
return { scale: 1, x: margin - (content.width > 0 ? content.x : 0), y: margin - (content.height > 0 ? content.y : 0) };
|
||||||
|
}
|
||||||
|
const scale = clampZoom(Math.min(1, (view.width - 2 * margin) / content.width, (view.height - 2 * margin) / content.height));
|
||||||
|
return {
|
||||||
|
scale,
|
||||||
|
x: (view.width - content.width * scale) / 2 - content.x * scale,
|
||||||
|
y: (view.height - content.height * scale) / 2 - content.y * scale,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The distance between two touches: the measure of the pinch. */
|
||||||
|
export function spread(first: Point, second: Point): number {
|
||||||
|
return Math.hypot(second.x - first.x, second.y - first.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The middle between two touches. */
|
||||||
|
export function middle(first: Point, second: Point): Point {
|
||||||
|
return { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"canvas.label": "Netzwerk",
|
||||||
|
"device.cable": "Verbindung",
|
||||||
|
"device.gateway": "Heimrouter",
|
||||||
|
"device.networkSwitch": "Switch / WLAN",
|
||||||
|
"device.notebook": "Notebook",
|
||||||
|
"device.pc": "Rechner",
|
||||||
|
"device.remoteLink": "Modem",
|
||||||
|
"device.router": "Vermittlungsrechner",
|
||||||
|
"device.unsupported": "Unbekanntes Gerät",
|
||||||
|
"dev.example": "Beispiel",
|
||||||
|
"dev.openFile": "Datei öffnen …",
|
||||||
|
"dev.title": "Filius-Editor (Entwicklung)",
|
||||||
|
"zoom.fit": "Alles zeigen",
|
||||||
|
"zoom.in": "Vergrößern",
|
||||||
|
"zoom.level": "{percent} %",
|
||||||
|
"zoom.out": "Verkleinern"
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"canvas.label": "Network",
|
||||||
|
"device.cable": "Connection",
|
||||||
|
"device.gateway": "Home Router",
|
||||||
|
"device.networkSwitch": "Switch / Wi-Fi",
|
||||||
|
"device.notebook": "Notebook",
|
||||||
|
"device.pc": "Computer",
|
||||||
|
"device.remoteLink": "Modem",
|
||||||
|
"device.router": "Router",
|
||||||
|
"device.unsupported": "Unknown device",
|
||||||
|
"dev.example": "Example",
|
||||||
|
"dev.openFile": "Open file …",
|
||||||
|
"dev.title": "Filius editor (development)",
|
||||||
|
"zoom.fit": "Show all",
|
||||||
|
"zoom.in": "Zoom in",
|
||||||
|
"zoom.level": "{percent} %",
|
||||||
|
"zoom.out": "Zoom out"
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// The editor's visible texts, German and English (plan §7.5): English keys,
|
||||||
|
// the words in i18n/{de,en}.json — where Java FILIUS has a word for a thing,
|
||||||
|
// Java's (MessagesBundle), so that the terms on worksheets fit. The language
|
||||||
|
// comes from the dashboard over the bridge.
|
||||||
|
|
||||||
|
import { createContext, useContext, type ReactNode } from 'react';
|
||||||
|
import de from './de.json';
|
||||||
|
import en from './en.json';
|
||||||
|
|
||||||
|
export type Language = 'de' | 'en';
|
||||||
|
export const CATALOGS: Readonly<Record<Language, Readonly<Record<string, string>>>> = { de, en };
|
||||||
|
|
||||||
|
export type Translate = (key: string, values?: Readonly<Record<string, string | number>>) => string;
|
||||||
|
|
||||||
|
/** The text of a key in a language, its {placeholders} filled; the key itself where the catalog has none. */
|
||||||
|
export function translate(language: Language, key: string, values: Readonly<Record<string, string | number>> = {}): string {
|
||||||
|
const text = CATALOGS[language][key] ?? key;
|
||||||
|
return text.replace(/\{(\w+)\}/g, (whole, name: string) => (name in values ? String(values[name]) : whole));
|
||||||
|
}
|
||||||
|
|
||||||
|
const LanguageContext = createContext<Language>('de');
|
||||||
|
|
||||||
|
export function LanguageProvider({ language, children }: { readonly language: Language; readonly children: ReactNode }) {
|
||||||
|
return <LanguageContext.Provider value={language}>{children}</LanguageContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTranslate(): Translate {
|
||||||
|
const language = useContext(LanguageContext);
|
||||||
|
return (key, values) => translate(language, key, values);
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// The editor's page. Embedded in the dashboard (F6) it gets its document and
|
||||||
|
// language over the bridge; on its own — for development and the browser
|
||||||
|
// tests — it opens one of FILIUS' examples or a file (`?example=<name>`
|
||||||
|
// opens that example at once, `?lang=en` and `?theme=dark` set the rest).
|
||||||
|
|
||||||
|
import { StrictMode, useEffect, useState } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import { writeProject } from '@mdg-filius/doc';
|
||||||
|
import { openProjectArchive } from '@mdg-filius/fls';
|
||||||
|
import { startBridge } from './bridge/dashboardBridge.js';
|
||||||
|
import { Editor } from './Editor.js';
|
||||||
|
import { LanguageProvider, translate, type Language } from './i18n/i18n.js';
|
||||||
|
import './theme/tokens.css';
|
||||||
|
|
||||||
|
const EXAMPLES = import.meta.glob('../../../corpus/java-examples/*.fls', { query: '?url', import: 'default', eager: true }) as Record<string, string>;
|
||||||
|
const exampleUrl = (name: string) => Object.entries(EXAMPLES).find(([path]) => path.endsWith(`/${name}`))?.[1];
|
||||||
|
|
||||||
|
function docOf(bytes: Uint8Array): Y.Doc {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
writeProject(doc, openProjectArchive(bytes, () => crypto.randomUUID()));
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const [language, setLanguage] = useState<Language>(params.get('lang') === 'en' ? 'en' : 'de');
|
||||||
|
const [doc, setDoc] = useState<Y.Doc | null>(null);
|
||||||
|
const [example, setExample] = useState(params.get('example') ?? '');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (params.get('theme') === 'dark') document.documentElement.dataset.theme = 'dark';
|
||||||
|
const parent = params.get('parent');
|
||||||
|
if (parent === null) return undefined;
|
||||||
|
return startBridge(window, parent, {
|
||||||
|
onLocale: (locale) => setLanguage(locale.startsWith('en') ? 'en' : 'de'),
|
||||||
|
onTheme: (theme, tokens) => {
|
||||||
|
document.documentElement.dataset.theme = theme;
|
||||||
|
for (const [name, value] of Object.entries(tokens)) document.documentElement.style.setProperty(name, value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// The page's parameters are read once.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const url = exampleUrl(example);
|
||||||
|
if (url === undefined) return;
|
||||||
|
void fetch(url)
|
||||||
|
.then((response) => response.arrayBuffer())
|
||||||
|
.then((buffer) => setDoc(docOf(new Uint8Array(buffer))));
|
||||||
|
}, [example]);
|
||||||
|
|
||||||
|
const t = (key: string) => translate(language, key);
|
||||||
|
return (
|
||||||
|
<LanguageProvider language={language}>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
|
<header style={{ display: 'flex', gap: 12, alignItems: 'center', padding: '8px 12px', borderBottom: '1px solid var(--border-default)', background: 'var(--surface-card)' }}>
|
||||||
|
<strong>{t('dev.title')}</strong>
|
||||||
|
<label>
|
||||||
|
{t('dev.example')}{' '}
|
||||||
|
<select value={example} onChange={(event) => setExample(event.target.value)}>
|
||||||
|
<option value="" />
|
||||||
|
{Object.keys(EXAMPLES)
|
||||||
|
.map((path) => path.slice(path.lastIndexOf('/') + 1))
|
||||||
|
.sort()
|
||||||
|
.map((name) => (
|
||||||
|
<option key={name} value={name}>
|
||||||
|
{name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
{t('dev.openFile')}{' '}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".fls"
|
||||||
|
onChange={(event) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file !== undefined) void file.arrayBuffer().then((buffer) => setDoc(docOf(new Uint8Array(buffer))));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</header>
|
||||||
|
<main style={{ flex: 1, minHeight: 0 }}>
|
||||||
|
{doc === null ? null : <Editor key={doc.guid} doc={doc} />}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</LanguageProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/* The editor's colours: the dashboard's semantic tokens under the same names
|
||||||
|
* (frontend/src/index.css), light by default and dark when the dashboard says
|
||||||
|
* so over the bridge (data-theme on <html>) — never by the system's setting,
|
||||||
|
* which the dashboard has already resolved. The bridge may set any of them
|
||||||
|
* to the dashboard's own values. */
|
||||||
|
:root {
|
||||||
|
--surface-page: #f5f6f8;
|
||||||
|
--surface-card: #ffffff;
|
||||||
|
--surface-raised: #ffffff;
|
||||||
|
--surface-sunken: #f1f3f5;
|
||||||
|
--surface-hover: #eff1f4;
|
||||||
|
--surface-selected: #e8f0f6;
|
||||||
|
--text-primary: #1d2939;
|
||||||
|
--text-secondary: #667085;
|
||||||
|
--text-muted: #78839a;
|
||||||
|
--border-default: #dee2e6;
|
||||||
|
--border-subtle: #e9ebef;
|
||||||
|
--border-strong: #c7ced8;
|
||||||
|
--border-focus: #2c9c32;
|
||||||
|
--accent: #1a5984;
|
||||||
|
--accent-hover: #2980b9;
|
||||||
|
--accent-soft: #e8f0f6;
|
||||||
|
--accent-on-soft: #1a5984;
|
||||||
|
--text-on-accent: #ffffff;
|
||||||
|
--shadow-color: #101828;
|
||||||
|
--danger: #e74c3c;
|
||||||
|
--danger-soft: #f8d7da;
|
||||||
|
--danger-on-soft: #721c24;
|
||||||
|
/* The surface itself: Java's white design area and its dark grey cables (JCablePanel). */
|
||||||
|
--canvas-bg: #ffffff;
|
||||||
|
--canvas-cable: rgb(64 64 64);
|
||||||
|
--canvas-cable-wireless: rgb(192 192 192);
|
||||||
|
--canvas-label: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme='dark'] {
|
||||||
|
--surface-page: #14171c;
|
||||||
|
--surface-card: #1b1f26;
|
||||||
|
--surface-raised: #232830;
|
||||||
|
--surface-sunken: #101318;
|
||||||
|
--surface-hover: #262c35;
|
||||||
|
--surface-selected: #1e3448;
|
||||||
|
--text-primary: #e6e9ef;
|
||||||
|
--text-secondary: #a7b0bf;
|
||||||
|
--text-muted: #7d8797;
|
||||||
|
--border-default: #333a45;
|
||||||
|
--border-subtle: #272d36;
|
||||||
|
--border-strong: #4a5361;
|
||||||
|
--border-focus: #3ecf4a;
|
||||||
|
--accent: #4a9fd5;
|
||||||
|
--accent-hover: #6cb6e4;
|
||||||
|
--accent-soft: #16344b;
|
||||||
|
--accent-on-soft: #8ec5e8;
|
||||||
|
--text-on-accent: #0d1b26;
|
||||||
|
--shadow-color: #000000;
|
||||||
|
--danger: #f2695a;
|
||||||
|
--danger-soft: #3a1a17;
|
||||||
|
--danger-on-soft: #ffa79b;
|
||||||
|
/* Java's icons are drawn for a light ground: the surface stays light, a shade darker. */
|
||||||
|
--canvas-bg: #e9ecf1;
|
||||||
|
--canvas-cable: rgb(64 64 64);
|
||||||
|
--canvas-cable-wireless: rgb(150 150 150);
|
||||||
|
--canvas-label: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--surface-page);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// The bridge to the dashboard: only the expected origin, only this version,
|
||||||
|
// and the editor says when it listens.
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { BRIDGE_VERSION, startBridge } from '../src/bridge/dashboardBridge.js';
|
||||||
|
|
||||||
|
function fakeWindow() {
|
||||||
|
const posted: [unknown, string][] = [];
|
||||||
|
const listeners = new Set<(event: { origin: string; data: unknown }) => void>();
|
||||||
|
return {
|
||||||
|
posted,
|
||||||
|
window: {
|
||||||
|
parent: { postMessage: (message: unknown, origin: string) => posted.push([message, origin]) },
|
||||||
|
addEventListener: (_: 'message', listener: (event: { origin: string; data: unknown }) => void) => listeners.add(listener),
|
||||||
|
removeEventListener: (_: 'message', listener: (event: { origin: string; data: unknown }) => void) => listeners.delete(listener),
|
||||||
|
},
|
||||||
|
send: (origin: string, data: unknown) => {
|
||||||
|
for (const listener of listeners) listener({ origin, data });
|
||||||
|
},
|
||||||
|
listening: () => listeners.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('the bridge to the dashboard', () => {
|
||||||
|
it('says it is ready, to the dashboard only', () => {
|
||||||
|
const fake = fakeWindow();
|
||||||
|
startBridge(fake.window, 'https://dashboard.example', { onLocale: () => {}, onTheme: () => {} });
|
||||||
|
expect(fake.posted).toEqual([[{ v: BRIDGE_VERSION, type: 'ready' }, 'https://dashboard.example']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes language and colours from the dashboard, and nothing from elsewhere', () => {
|
||||||
|
const fake = fakeWindow();
|
||||||
|
const got: unknown[] = [];
|
||||||
|
const stop = startBridge(fake.window, 'https://dashboard.example', {
|
||||||
|
onLocale: (locale) => got.push(['locale', locale]),
|
||||||
|
onTheme: (theme, tokens) => got.push(['theme', theme, tokens]),
|
||||||
|
});
|
||||||
|
fake.send('https://evil.example', { v: 1, type: 'setLocale', locale: 'en' });
|
||||||
|
fake.send('https://dashboard.example', { v: 2, type: 'setLocale', locale: 'en' });
|
||||||
|
fake.send('https://dashboard.example', { v: 1, type: 'setLocale', locale: 7 });
|
||||||
|
fake.send('https://dashboard.example', { v: 1, type: 'setTheme', theme: 'pink' });
|
||||||
|
fake.send('https://dashboard.example', null);
|
||||||
|
expect(got).toEqual([]);
|
||||||
|
fake.send('https://dashboard.example', { v: 1, type: 'setLocale', locale: 'en-GB' });
|
||||||
|
fake.send('https://dashboard.example', { v: 1, type: 'setTheme', theme: 'dark', tokens: { '--accent': '#123456', color: 'red', '--bad': 3 } });
|
||||||
|
expect(got).toEqual([
|
||||||
|
['locale', 'en-GB'],
|
||||||
|
['theme', 'dark', { '--accent': '#123456' }],
|
||||||
|
]);
|
||||||
|
stop();
|
||||||
|
expect(fake.listening()).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// The catalogs: the same keys in both languages, placeholders filled, and —
|
||||||
|
// where Java FILIUS names a thing — Java's word in its bundle (M11).
|
||||||
|
|
||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { CATALOGS, translate } from '../src/i18n/i18n.js';
|
||||||
|
|
||||||
|
const bundles = join(dirname(fileURLToPath(import.meta.url)), '../../../tools/java-oracle/.upstream/src/main/resources/filius/messages');
|
||||||
|
|
||||||
|
/** A Java .properties file: ISO-8859-1, \uXXXX escapes, `key = value`. */
|
||||||
|
function properties(file: string): Map<string, string> {
|
||||||
|
const text = new TextDecoder('latin1').decode(readFileSync(join(bundles, file)));
|
||||||
|
const entries = new Map<string, string>();
|
||||||
|
for (const line of text.split(/\r?\n/)) {
|
||||||
|
const match = /^([\w.]+)\s*=\s*(.*)$/.exec(line);
|
||||||
|
if (match !== null) entries.set(match[1]!, match[2]!.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16))).trim());
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The editor's keys whose words are Java's, and the key of each in Java's bundle. */
|
||||||
|
const FROM_JAVA: Readonly<Record<string, string>> = {
|
||||||
|
'device.pc': 'hw_rechner_msg1',
|
||||||
|
'device.notebook': 'hw_notebook_msg1',
|
||||||
|
'device.networkSwitch': 'hw_switch_msg1',
|
||||||
|
'device.router': 'hw_vermittlungsrechner_msg1',
|
||||||
|
'device.gateway': 'hw_gateway_msg1',
|
||||||
|
'device.cable': 'hw_kabel_msg1',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('the catalogs', () => {
|
||||||
|
it('have the same keys in German and English, each with a text', () => {
|
||||||
|
expect(Object.keys(CATALOGS.en).sort()).toEqual(Object.keys(CATALOGS.de).sort());
|
||||||
|
for (const catalog of Object.values(CATALOGS)) for (const text of Object.values(catalog)) expect(text.trim()).not.toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fill placeholders, and show a key they lack', () => {
|
||||||
|
expect(translate('de', 'zoom.level', { percent: 75 })).toBe('75 %');
|
||||||
|
expect(translate('en', 'zoom.level')).toBe('{percent} %');
|
||||||
|
expect(translate('de', 'no.such.key')).toBe('no.such.key');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('name every kind of device', () => {
|
||||||
|
for (const kind of ['pc', 'notebook', 'networkSwitch', 'router', 'gateway', 'remoteLink', 'unsupported']) expect(CATALOGS.de[`device.${kind}`]).toBeDefined();
|
||||||
|
// Java's Modem.TYPE, the same in every language.
|
||||||
|
expect([CATALOGS.de['device.remoteLink'], CATALOGS.en['device.remoteLink']]).toEqual(['Modem', 'Modem']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skipIf(!existsSync(bundles))("use Java's words where Java has them (M11)", () => {
|
||||||
|
const [de, en] = [properties('MessagesBundle_de_DE.properties'), properties('MessagesBundle_en_GB.properties')];
|
||||||
|
for (const [key, javaKey] of Object.entries(FROM_JAVA)) {
|
||||||
|
expect(CATALOGS.de[key], key).toBe(de.get(javaKey));
|
||||||
|
expect(CATALOGS.en[key], key).toBe(en.get(javaKey));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// Java's drawing of a device and a cable (JSidebarButton, JCablePanel).
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { bounds, cablePath, deviceBox, iconOf, ICONS, JAVA_LABEL_FONT_HEIGHT } from '../src/canvas/layout.js';
|
||||||
|
|
||||||
|
describe("Java's drawing", () => {
|
||||||
|
it('picks the icon of each kind, the cloud for a switch drawn as one', () => {
|
||||||
|
expect((['pc', 'notebook', 'networkSwitch', 'router', 'gateway', 'remoteLink', 'unsupported'] as const).map((kind) => iconOf(kind, false))).toEqual([
|
||||||
|
'server',
|
||||||
|
'laptop',
|
||||||
|
'switch',
|
||||||
|
'router',
|
||||||
|
'gateway',
|
||||||
|
'modem',
|
||||||
|
'cloud',
|
||||||
|
]);
|
||||||
|
expect(iconOf('networkSwitch', true)).toBe('cloud');
|
||||||
|
expect(iconOf('router', true)).toBe('router');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('makes a button as wide as its icon or its label, plus 5 and twice the border, and as high as both plus 5', () => {
|
||||||
|
const short = deviceBox({ kind: 'pc', cloud: false, x: 100, y: 200 }, 30);
|
||||||
|
expect(short).toMatchObject({ x: 100, y: 200, width: 64 + 21, height: JAVA_LABEL_FONT_HEIGHT + 94 + 5, icon: 'server' });
|
||||||
|
// The icon at the top, in the middle; the label's baseline under it, in the middle.
|
||||||
|
expect(short.iconRect).toEqual({ x: 100 + (85 - 64) / 2, y: 200, width: 64, height: 94 });
|
||||||
|
expect(short.label).toEqual({ x: 100 + 85 / 2, y: 200 + 94 + 14 });
|
||||||
|
const long = deviceBox({ kind: 'router', cloud: false, x: 0, y: 0 }, 124.6);
|
||||||
|
expect(long.width).toBe(125 + 21);
|
||||||
|
expect(long.iconRect.x).toBe((146 - 77) / 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('meets cables in the middle of the button, in whole pixels as Java does', () => {
|
||||||
|
const box = deviceBox({ kind: 'notebook', cloud: false, x: 11, y: 7 }, 10);
|
||||||
|
// 84 + 21 = 105 wide, 17 + 80 + 5 = 102 high.
|
||||||
|
expect(box.center).toEqual({ x: Math.trunc(11 + 52.5), y: Math.trunc(7 + 51) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("draws a cable as JCablePanel's curve", () => {
|
||||||
|
// Rising to the right (y falls): the control point a quarter across the panel.
|
||||||
|
// Panel from (98, 48); control (98 + trunc((2 + 302) / 4), 48 + trunc((52 + 2) / 4)) = (174, 61).
|
||||||
|
expect(cablePath({ x: 100, y: 100 }, { x: 400, y: 50 })).toBe('M 100 100 Q 174 61 400 50');
|
||||||
|
// Falling to the right: three quarters across.
|
||||||
|
expect(cablePath({ x: 100, y: 50 }, { x: 400, y: 100 })).toBe(`M 100 50 Q ${98 + 3 * Math.trunc(304 / 4)} 61 400 100`);
|
||||||
|
// The same either way round.
|
||||||
|
expect(cablePath({ x: 400, y: 100 }, { x: 100, y: 50 })).toBe(`M 400 100 Q ${98 + 3 * Math.trunc(304 / 4)} 61 100 50`);
|
||||||
|
// Straight down: not falling (x does not change); the panel from (8, 8), control (8 + trunc(4 / 4), 8 + trunc(84 / 4)).
|
||||||
|
expect(cablePath({ x: 10, y: 10 }, { x: 10, y: 90 })).toBe('M 10 10 Q 9 29 10 90');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('knows every icon it draws', () => {
|
||||||
|
expect(Object.keys(ICONS).sort()).toEqual(['cloud', 'gateway', 'laptop', 'modem', 'router', 'server', 'switch']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('spans the rectangles around everything', () => {
|
||||||
|
expect(bounds([])).toEqual({ x: 0, y: 0, width: 0, height: 0 });
|
||||||
|
expect(bounds([{ x: 10, y: 20, width: 5, height: 5 }, { x: -5, y: 30, width: 10, height: 10 }])).toEqual({ x: -5, y: 20, width: 20, height: 20 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"composite": false,
|
||||||
|
"declaration": false,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["node", "vite/client"],
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": [".", "../browser", "../src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// The view onto the surface: zoom steps and limits, zooming around a point,
|
||||||
|
// fitting the content in.
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { clampZoom, fitted, MAX_ZOOM, middle, MIN_ZOOM, shifted, spread, toScreen, toSurface, zoomedAt, zoomIn, zoomOut, ZOOM_STEPS } from '../src/canvas/viewport.js';
|
||||||
|
|
||||||
|
describe('the view onto the surface', () => {
|
||||||
|
it('keeps the zoom between a quarter and twice', () => {
|
||||||
|
expect([clampZoom(0.1), clampZoom(1.3), clampZoom(9)]).toEqual([MIN_ZOOM, 1.3, MAX_ZOOM]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('steps along the ladder from any value, and back to where it was', () => {
|
||||||
|
expect(ZOOM_STEPS.map(zoomIn)).toEqual([0.5, 0.75, 1, 1.25, 1.5, 2, 2]);
|
||||||
|
expect(ZOOM_STEPS.map(zoomOut)).toEqual([0.25, 0.25, 0.5, 0.75, 1, 1.25, 1.5]);
|
||||||
|
expect([zoomIn(0.8), zoomOut(0.8), zoomIn(1.0005)]).toEqual([1, 0.75, 1.25]);
|
||||||
|
for (const step of ZOOM_STEPS.slice(1, -1)) expect(zoomOut(zoomIn(step))).toBe(step);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('turns screen and surface pixels into each other', () => {
|
||||||
|
const view = { scale: 0.5, x: 100, y: -20 };
|
||||||
|
expect(toScreen(view, { x: 40, y: 80 })).toEqual({ x: 120, y: 20 });
|
||||||
|
expect(toSurface(view, toScreen(view, { x: 40, y: 80 }))).toEqual({ x: 40, y: 80 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('zooms so that the point zoomed at stays put, within the limits', () => {
|
||||||
|
const view = { scale: 1, x: 30, y: 40 };
|
||||||
|
const point = { x: 200, y: 150 };
|
||||||
|
const zoomed = zoomedAt(view, 1.5, point);
|
||||||
|
expect(zoomed.scale).toBe(1.5);
|
||||||
|
expect(toScreen(zoomed, toSurface(view, point))).toEqual(point);
|
||||||
|
expect(zoomedAt(view, 10, point).scale).toBe(MAX_ZOOM);
|
||||||
|
expect(toScreen(zoomedAt(view, 10, point), toSurface(view, point))).toEqual(point);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves by what the pointer moved', () => {
|
||||||
|
expect(shifted({ scale: 2, x: 1, y: 2 }, { x: 10, y: -5 })).toEqual({ scale: 2, x: 11, y: -3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fits the content in the middle with a margin, never above 100 %', () => {
|
||||||
|
const content = { x: 100, y: 50, width: 800, height: 400 };
|
||||||
|
const view = fitted(content, { width: 448, height: 448 });
|
||||||
|
expect(view.scale).toBe(0.5);
|
||||||
|
// Left and right margin alike, the content in the middle vertically.
|
||||||
|
expect(toScreen(view, { x: 100, y: 50 })).toEqual({ x: 24, y: 124 });
|
||||||
|
expect(toScreen(view, { x: 900, y: 450 })).toEqual({ x: 424, y: 324 });
|
||||||
|
expect(fitted({ x: 0, y: 0, width: 100, height: 100 }, { width: 1000, height: 1000 }).scale).toBe(1);
|
||||||
|
expect(fitted({ x: 0, y: 0, width: 100_000, height: 10 }, { width: 500, height: 500 }).scale).toBe(MIN_ZOOM);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows an empty surface at 100 % with its origin at the margin', () => {
|
||||||
|
expect(fitted({ x: 0, y: 0, width: 0, height: 0 }, { width: 500, height: 500 })).toEqual({ scale: 1, x: 24, y: 24 });
|
||||||
|
expect(fitted({ x: 10, y: 10, width: 50, height: 50 }, { width: 0, height: 0 })).toEqual({ scale: 1, x: 14, y: 14 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('measures two fingers', () => {
|
||||||
|
expect(spread({ x: 0, y: 0 }, { x: 3, y: 4 })).toBe(5);
|
||||||
|
expect(middle({ x: 0, y: 10 }, { x: 4, y: 20 })).toEqual({ x: 2, y: 15 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"composite": false,
|
||||||
|
"declaration": false,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src", "vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
// The editor's page. The corpus (FILIUS' examples, for the development page
|
||||||
|
// and the browser tests) lies outside the package.
|
||||||
|
export default defineConfig({
|
||||||
|
root: fileURLToPath(new URL('.', import.meta.url)),
|
||||||
|
base: './',
|
||||||
|
plugins: [react()],
|
||||||
|
server: { fs: { allow: [fileURLToPath(new URL('../..', import.meta.url))] } },
|
||||||
|
build: { outDir: 'dist', emptyOutDir: true },
|
||||||
|
});
|
||||||
@@ -226,6 +226,7 @@ function exportNode(node: TopologyNode, project: ProjectToExport, servingModems:
|
|||||||
useMacAsName: showsMac(node.hostLabelPolicy),
|
useMacAsName: showsMac(node.hostLabelPolicy),
|
||||||
interfaces,
|
interfaces,
|
||||||
portCount: javaClass === 'Switch' || javaClass === 'Modem' ? node.ports.length : interfaces.length,
|
portCount: javaClass === 'Switch' || javaClass === 'Modem' ? node.ports.length : interfaces.length,
|
||||||
|
...(node.kind === 'networkSwitch' && project.switchConfigurations.get(node.id)?.showsAsCloud === true ? { cloud: true } : {}),
|
||||||
system: exportSystem(node, project, servingModems, (message) => warnings.push(`node '${node.displayName}': ${message}`)),
|
system: exportSystem(node, project, servingModems, (message) => warnings.push(`node '${node.displayName}': ${message}`)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -602,6 +602,7 @@ function registerSystemConfigurations(
|
|||||||
system.switchConfigurations.set(node.id, {
|
system.switchConfigurations.set(node.id, {
|
||||||
ssid: ssid === '' ? defaultSwitchConfiguration(node.id).ssid : ssid,
|
ssid: ssid === '' ? defaultSwitchConfiguration(node.id).ssid : ssid,
|
||||||
retentionTimeMilliseconds: source.system.retentionTime ?? 300_000,
|
retentionTimeMilliseconds: source.system.retentionTime ?? 300_000,
|
||||||
|
...(source.cloud ? { showsAsCloud: true } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Java joins a wireless network when the card is wireless and an SSID is set
|
// Java joins a wireless network when the card is wireless and an SSID is set
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export interface FlsNode {
|
|||||||
/** Hosts only (Rechner, Notebook); false for the others. */
|
/** Hosts only (Rechner, Notebook); false for the others. */
|
||||||
useIpAsName: boolean;
|
useIpAsName: boolean;
|
||||||
useMacAsName: boolean;
|
useMacAsName: boolean;
|
||||||
|
/** A Switch drawn as a cloud (Switch.cloud, the Internet in the examples); false for the others. */
|
||||||
|
cloud: boolean;
|
||||||
/** The interfaces of a Rechner, Notebook, Vermittlungsrechner or Gateway; empty for the others. */
|
/** The interfaces of a Rechner, Notebook, Vermittlungsrechner or Gateway; empty for the others. */
|
||||||
interfaces: FlsInterface[];
|
interfaces: FlsInterface[];
|
||||||
/** Interfaces, or the ports of a Switch or Modem. */
|
/** Interfaces, or the ports of a Switch or Modem. */
|
||||||
@@ -204,6 +206,7 @@ class Reader {
|
|||||||
y,
|
y,
|
||||||
useIpAsName: info.host && this.booleanProperty(device, 'useIPAsName', `node ${itemIndex + 1}`),
|
useIpAsName: info.host && this.booleanProperty(device, 'useIPAsName', `node ${itemIndex + 1}`),
|
||||||
useMacAsName: info.host && this.booleanProperty(device, 'useMACAsName', `node ${itemIndex + 1}`),
|
useMacAsName: info.host && this.booleanProperty(device, 'useMACAsName', `node ${itemIndex + 1}`),
|
||||||
|
cloud: simpleName === 'Switch' && this.booleanProperty(device, 'cloud', `node ${itemIndex + 1}`),
|
||||||
interfaces: [],
|
interfaces: [],
|
||||||
portCount: 0,
|
portCount: 0,
|
||||||
system: readSystemSoftware(device, simpleName, (message) =>
|
system: readSystemSoftware(device, simpleName, (message) =>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export type SystemToWrite = Omit<FlsSystemSoftware, 'applications'> & {
|
|||||||
export type NodeToWrite = Pick<
|
export type NodeToWrite = Pick<
|
||||||
FlsNode,
|
FlsNode,
|
||||||
'javaClass' | 'name' | 'x' | 'y' | 'useIpAsName' | 'useMacAsName' | 'interfaces' | 'portCount'
|
'javaClass' | 'name' | 'x' | 'y' | 'useIpAsName' | 'useMacAsName' | 'interfaces' | 'portCount'
|
||||||
> & { readonly system: SystemToWrite };
|
> & { readonly system: SystemToWrite; readonly cloud?: boolean };
|
||||||
|
|
||||||
export interface TopologyToWrite {
|
export interface TopologyToWrite {
|
||||||
readonly nodes: readonly NodeToWrite[];
|
readonly nodes: readonly NodeToWrite[];
|
||||||
@@ -133,7 +133,8 @@ function nodeItem(node: NodeToWrite, ports: BeanValue[]): BeanObject {
|
|||||||
|
|
||||||
function device(node: NodeToWrite, ports: BeanValue[]): BeanObject {
|
function device(node: NodeToWrite, ports: BeanValue[]): BeanObject {
|
||||||
const constructed = CONSTRUCTED[node.javaClass];
|
const constructed = CONSTRUCTED[node.javaClass];
|
||||||
const statements: BeanStatement[] = [{ set: 'name', value: node.name }];
|
// A switch drawn as a cloud: written only when true, before the name, as XMLEncoder orders Switch's properties.
|
||||||
|
const statements: BeanStatement[] = [...(node.cloud === true ? [{ set: 'cloud', value: true }] : []), { set: 'name', value: node.name }];
|
||||||
if (constructed.host) {
|
if (constructed.host) {
|
||||||
statements.push({ set: 'useIPAsName', value: node.useIpAsName }, { set: 'useMACAsName', value: node.useMacAsName });
|
statements.push({ set: 'useIPAsName', value: node.useIpAsName }, { set: 'useMACAsName', value: node.useMacAsName });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -417,6 +417,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -822,6 +823,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -1118,6 +1120,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -1417,6 +1420,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -1458,6 +1462,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -1999,6 +2004,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -3502,6 +3508,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -3806,6 +3813,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -4737,6 +4745,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -6294,6 +6303,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -6592,6 +6602,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -7742,6 +7753,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -8249,6 +8261,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -8530,6 +8543,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -8827,6 +8841,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -9123,6 +9138,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -9738,6 +9754,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -9951,6 +9968,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -9992,6 +10010,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -10613,6 +10632,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -11096,6 +11116,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -11137,6 +11158,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -11427,6 +11449,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -11827,6 +11850,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -12669,6 +12693,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": true,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -13201,7 +13226,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sha256": "5f474b24ec3cd2a206ac86bccd0adb63e3b3c24bc22060643fa434e8edbd0c0c"
|
"sha256": "222f57095cb88eb285a82b85212bf1a970484c088a5993333705939406085a77"
|
||||||
},
|
},
|
||||||
"java-examples/modem_beispiel.fls": {
|
"java-examples/modem_beispiel.fls": {
|
||||||
"success": true,
|
"success": true,
|
||||||
@@ -13633,6 +13658,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -13946,6 +13972,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": true,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -14032,7 +14059,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": [],
|
"documentation": [],
|
||||||
"sha256": "353c4d3571e9dbc9dda20f6ef41cc1d56b5d96375e35679ae65d1bf1d279e69e"
|
"sha256": "6f878280aef6f517d1eb4bf5eb056298cce51e3aaeea8ebf872ee98bfe5fc6fd"
|
||||||
},
|
},
|
||||||
"java-examples/nat_with_mapped_port.fls": {
|
"java-examples/nat_with_mapped_port.fls": {
|
||||||
"success": true,
|
"success": true,
|
||||||
@@ -14047,6 +14074,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -14088,6 +14116,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -14759,6 +14788,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -15015,6 +15045,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -15493,6 +15524,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -15741,6 +15773,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -16116,6 +16149,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -16361,6 +16395,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -17349,6 +17384,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -17433,6 +17469,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -17474,6 +17511,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -18248,6 +18286,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -18461,6 +18500,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -18502,6 +18542,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -19182,6 +19223,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
|
|||||||
@@ -536,6 +536,7 @@ function flsNode(javaClass: FlsNode['javaClass'], fields: Partial<FlsNode> = {})
|
|||||||
y: 0,
|
y: 0,
|
||||||
useIpAsName: false,
|
useIpAsName: false,
|
||||||
useMacAsName: false,
|
useMacAsName: false,
|
||||||
|
cloud: false,
|
||||||
interfaces: [],
|
interfaces: [],
|
||||||
portCount: 0,
|
portCount: 0,
|
||||||
system: {},
|
system: {},
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ function asJavaView(topology: FlsTopology, javaBounds: (number[] | null)[]) {
|
|||||||
...(node.javaClass === 'Rechner' || node.javaClass === 'Notebook'
|
...(node.javaClass === 'Rechner' || node.javaClass === 'Notebook'
|
||||||
? { useIpAsName: node.useIpAsName, useMacAsName: node.useMacAsName }
|
? { useIpAsName: node.useIpAsName, useMacAsName: node.useMacAsName }
|
||||||
: {}),
|
: {}),
|
||||||
|
...(node.javaClass === 'Switch' ? { cloud: node.cloud } : {}),
|
||||||
ports:
|
ports:
|
||||||
node.javaClass === 'Switch' || node.javaClass === 'Modem'
|
node.javaClass === 'Switch' || node.javaClass === 'Modem'
|
||||||
? Array.from({ length: node.portCount }, () => ({}))
|
? Array.from({ length: node.portCount }, () => ({}))
|
||||||
|
|||||||
@@ -417,6 +417,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -814,6 +815,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -1110,6 +1112,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -1409,6 +1412,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -1450,6 +1454,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -2003,6 +2008,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -3530,6 +3536,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -3834,6 +3841,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -4745,6 +4753,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -6326,6 +6335,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -6624,6 +6634,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -7754,6 +7765,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -8253,6 +8265,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -8534,6 +8547,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -8831,6 +8845,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -9127,6 +9142,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -9742,6 +9758,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -9955,6 +9972,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -9996,6 +10014,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -10617,6 +10636,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -11100,6 +11120,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -11141,6 +11162,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -11431,6 +11453,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -11831,6 +11854,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -12673,6 +12697,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": true,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -13201,7 +13226,7 @@
|
|||||||
"font": null
|
"font": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sha256": "f34f202638d8723ab709a39933495ef5028c2ba1b7dd41a7952c7af3c68f1102"
|
"sha256": "307f7d281eff71dbed8f9ab29c12fac9fba3113765655cbbee167481f9cdad52"
|
||||||
},
|
},
|
||||||
"java-examples/modem_beispiel.fls": {
|
"java-examples/modem_beispiel.fls": {
|
||||||
"success": true,
|
"success": true,
|
||||||
@@ -13633,6 +13658,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -13946,6 +13972,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": true,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -14032,7 +14059,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": [],
|
"documentation": [],
|
||||||
"sha256": "181fd85daa8b7828e6619fb7733295c442cbe862f44435a99984b557607c4a5d"
|
"sha256": "9ab5152668178899b1366703c34ffecba77d3bff0a9a01e1a0cfb90e2b9f0454"
|
||||||
},
|
},
|
||||||
"java-examples/nat_with_mapped_port.fls": {
|
"java-examples/nat_with_mapped_port.fls": {
|
||||||
"success": true,
|
"success": true,
|
||||||
@@ -14047,6 +14074,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -14088,6 +14116,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -14759,6 +14788,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -15015,6 +15045,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -15481,6 +15512,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -15729,6 +15761,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -16096,6 +16129,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -16341,6 +16375,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -17313,6 +17348,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -17397,6 +17433,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -17438,6 +17475,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -18224,6 +18262,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -18437,6 +18476,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -18478,6 +18518,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -19087,6 +19128,7 @@
|
|||||||
21,
|
21,
|
||||||
20
|
20
|
||||||
],
|
],
|
||||||
|
"cloud": false,
|
||||||
"ports": [
|
"ports": [
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
|
|||||||
@@ -95,3 +95,5 @@ packages/core/src/runtime/apps/gnutella.ts::hallo
|
|||||||
packages/core/src/runtime/apps/gnutella.ts::welt
|
packages/core/src/runtime/apps/gnutella.ts::welt
|
||||||
# GMX: the mail provider of FILIUS' example Internet_Komplett_…_DE (mail.gmx.de, herbert@gmx.de), a proper name.
|
# GMX: the mail provider of FILIUS' example Internet_Komplett_…_DE (mail.gmx.de, herbert@gmx.de), a proper name.
|
||||||
packages/fls/test/classicsJava.test.ts::gmx
|
packages/fls/test/classicsJava.test.ts::gmx
|
||||||
|
# Rechner, Vermittlungsrechner …: Java FILIUS' device classes, by which the browser test reads the bounds a file saved.
|
||||||
|
packages/editor/browser/canvas.spec.ts::rechner
|
||||||
|
|||||||
@@ -201,6 +201,8 @@ public class FlsOracle {
|
|||||||
json.put("useIpAsName", host.isUseIPAsName());
|
json.put("useIpAsName", host.isUseIPAsName());
|
||||||
json.put("useMacAsName", host.isUseMACAsName());
|
json.put("useMacAsName", host.isUseMACAsName());
|
||||||
}
|
}
|
||||||
|
// Drawn as a cloud (the Internet in the examples): JSwitchKonfiguration's check box.
|
||||||
|
if (node instanceof filius.hardware.knoten.Switch networkSwitch) json.put("cloud", networkSwitch.isCloud());
|
||||||
List<Object> ports = new ArrayList<>();
|
List<Object> ports = new ArrayList<>();
|
||||||
if (node instanceof InternetKnoten internet) {
|
if (node instanceof InternetKnoten internet) {
|
||||||
List<NetzwerkInterface> nics = internet.getNetzwerkInterfaces();
|
List<NetzwerkInterface> nics = internet.getNetzwerkInterfaces();
|
||||||
|
|||||||