feat: F4.8a the documentation items in model and file, measured against Java's
New oracle DocuRun on Java's JDocuElement. Italic is kept (the model knew only bold), an item keeps its file's size and font size (Swift's 32 x 24 and 8-72 changed Java's files), an item without a colour is Java's light grey. Java's new items, colour lists, font sizes and styles, the drag's smallest sizes and Java's stacking; document commands for the editor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,15 @@
|
||||
// ios/FiliusPad/TopologyEditor/Model/TopologyDocumentation.swift
|
||||
// Java counterpart: filius.gui.netzwerksicht.GUIDocuItem (RECT = 1, TEXT = 2).
|
||||
// Rectangles and text boxes drawn over the network.
|
||||
//
|
||||
// Java's rules where they differ from Swift's (measured with
|
||||
// tools/java-oracle's DocuRun, F4.8a): a new item is 100 × 50, a text black
|
||||
// in Dialog 12, a rectangle grey (204, 204, 204); an item keeps the size and
|
||||
// font size its file gives it (Swift enlarges to 32 × 24 and keeps sizes
|
||||
// within 8…72); a text may be italic (Java's style 2) as well as bold; an
|
||||
// item without a colour shows light grey (Color.LIGHT_GRAY); a drag leaves
|
||||
// a rectangle at least 20 × 20 and a text 88 × 20; texts lie over
|
||||
// rectangles, and of one kind the earlier over the later.
|
||||
|
||||
import { compareIds, type Id } from '../ids.js';
|
||||
|
||||
@@ -31,12 +40,21 @@ export interface DocumentationItem {
|
||||
fontName: string;
|
||||
fontSize: number;
|
||||
isBold: boolean;
|
||||
isItalic: boolean;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export const DOCUMENTATION_DEFAULT_TEXT_SIZE = { width: 220, height: 96 } as const;
|
||||
export const DOCUMENTATION_DEFAULT_RECTANGLE_SIZE = { width: 240, height: 140 } as const;
|
||||
export const DOCUMENTATION_MINIMUM_SIZE = { width: 32, height: 24 } as const;
|
||||
/** A new item's size, either kind (JDocuElement's constructor). */
|
||||
export const DOCUMENTATION_NEW_SIZE = { width: 100, height: 50 } as const;
|
||||
|
||||
/**
|
||||
* The smallest size a drag leaves (JDocuElement: RECT_MIN_WIDTH/HEIGHT; a
|
||||
* text's TEXT_MIN_WIDTH/HEIGHT hold for its frame enlarged by the focus,
|
||||
* which is 12 wider and 28 higher than the text).
|
||||
*/
|
||||
export function documentationMinimumSize(kind: DocumentationItemKind): { readonly width: number; readonly height: number } {
|
||||
return kind === 'rectangle' ? { width: 20, height: 20 } : { width: 88, height: 20 };
|
||||
}
|
||||
/** The editor's coordinate range; keeps Java `int` export and rendering safe. */
|
||||
export const DOCUMENTATION_WORLD_BOUNDS: Rect = { x: -10_000, y: -10_000, width: 20_000, height: 20_000 };
|
||||
|
||||
@@ -49,8 +67,31 @@ export function documentationColor(red: number, green: number, blue: number, alp
|
||||
return { red: clampUnit(red), green: clampUnit(green), blue: clampUnit(blue), alpha: clampUnit(alpha) };
|
||||
}
|
||||
|
||||
export const DOCUMENTATION_BLACK = documentationColor(0, 0, 0);
|
||||
export const DOCUMENTATION_PALE_YELLOW = documentationColor(255 / 255, 255 / 255, 227 / 255, 0.82);
|
||||
const byte = (red: number, green: number, blue: number) => documentationColor(red / 255, green / 255, blue / 255);
|
||||
|
||||
export const DOCUMENTATION_BLACK = byte(0, 0, 0);
|
||||
/** A new rectangle's colour (JDocuElement.panelColor[1]). */
|
||||
export const DOCUMENTATION_GREY = byte(204, 204, 204);
|
||||
/** What Java shows for an item its file gives no colour (JDocuElement.setColor(null)). */
|
||||
export const DOCUMENTATION_LIGHT_GRAY = byte(192, 192, 192);
|
||||
|
||||
/** The colours Java's menu offers a text, in its order, three rows (JDocuElement.fontColor). */
|
||||
export const DOCUMENTATION_TEXT_COLORS: readonly DocumentationColor[] = [
|
||||
[0, 0, 0], [102, 102, 102], [153, 153, 153], [179, 179, 179], [255, 255, 255], [255, 0, 0], [179, 163, 199], [79, 130, 189],
|
||||
[156, 186, 89], [148, 138, 84], [148, 56, 54], [94, 74, 122], [31, 74, 125], [110, 135, 56], [89, 84, 48],
|
||||
].map(([red, green, blue]) => byte(red!, green!, blue!));
|
||||
|
||||
/** The colours Java's menu offers a rectangle, in its order, three rows (JDocuElement.panelColor). */
|
||||
export const DOCUMENTATION_RECTANGLE_COLORS: readonly DocumentationColor[] = [
|
||||
[0, 0, 0], [204, 204, 204], [235, 235, 235], [255, 255, 255], [255, 201, 201], [252, 201, 163], [255, 255, 227], [212, 207, 181],
|
||||
[222, 212, 255], [209, 232, 255], [191, 255, 219], [204, 222, 173],
|
||||
].map(([red, green, blue]) => byte(red!, green!, blue!));
|
||||
|
||||
/** The font sizes Java's menu offers: „Normal“, „Groß“, „Sehr Groß“. */
|
||||
export const DOCUMENTATION_FONT_SIZES = [12, 18, 24] as const;
|
||||
|
||||
/** The font styles Java's menu offers, one at a time: „Normal“, „Kursiv“, „Fett“. */
|
||||
export type DocumentationFontStyle = 'plain' | 'italic' | 'bold';
|
||||
|
||||
/** CGRect.standardized: a negative width or height moves the origin instead. */
|
||||
function standardized(frame: Rect): Rect {
|
||||
@@ -59,14 +100,14 @@ function standardized(frame: Rect): Rect {
|
||||
return { x, y, width: Math.abs(frame.width), height: Math.abs(frame.height) };
|
||||
}
|
||||
|
||||
/** At least the minimum size, at most the world, moved inside the world. */
|
||||
/** Its own size (at least one, at most the world), moved inside the world. */
|
||||
export function normalizedFrame(frame: Rect): Rect {
|
||||
const world = DOCUMENTATION_WORLD_BOUNDS;
|
||||
const fallback = { x: world.x, y: world.y, ...DOCUMENTATION_MINIMUM_SIZE };
|
||||
const fallback = { x: world.x, y: world.y, ...DOCUMENTATION_NEW_SIZE };
|
||||
if (![frame.x, frame.y, frame.width, frame.height].every(Number.isFinite)) return fallback;
|
||||
const rect = standardized(frame);
|
||||
const width = Math.min(Math.max(rect.width, DOCUMENTATION_MINIMUM_SIZE.width), world.width);
|
||||
const height = Math.min(Math.max(rect.height, DOCUMENTATION_MINIMUM_SIZE.height), world.height);
|
||||
const width = Math.min(Math.max(rect.width, 1), world.width);
|
||||
const height = Math.min(Math.max(rect.height, 1), world.height);
|
||||
const x = Math.min(Math.max(rect.x, world.x), world.x + world.width - width);
|
||||
const y = Math.min(Math.max(rect.y, world.y), world.y + world.height - height);
|
||||
return { x, y, width, height };
|
||||
@@ -85,12 +126,12 @@ export function isSafeFrame(frame: Rect): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** A font size as Java keeps it: any positive one; else Java's 12. */
|
||||
export function normalizedFontSize(value: number): number {
|
||||
if (!Number.isFinite(value)) return 14;
|
||||
return Math.min(Math.max(value, 8), 72);
|
||||
return Number.isFinite(value) && value > 0 ? value : 12;
|
||||
}
|
||||
|
||||
/** A documentation item as Swift's initializer normalizes it. */
|
||||
/** A documentation item as Swift's initializer normalizes it, with Java's sizes and font. */
|
||||
export function documentationItem(fields: {
|
||||
id: Id;
|
||||
kind: DocumentationItemKind;
|
||||
@@ -100,23 +141,40 @@ export function documentationItem(fields: {
|
||||
fontName?: string;
|
||||
fontSize?: number;
|
||||
isBold?: boolean;
|
||||
isItalic?: boolean;
|
||||
order: number;
|
||||
}): DocumentationItem {
|
||||
const fontName = (fields.fontName ?? 'System').trim();
|
||||
const fontName = (fields.fontName ?? 'Dialog').trim();
|
||||
return {
|
||||
id: fields.id,
|
||||
kind: fields.kind,
|
||||
frame: normalizedFrame(fields.frame),
|
||||
text: fields.kind === 'text' ? (fields.text ?? '') : '',
|
||||
color: fields.color,
|
||||
fontName: fontName === '' ? 'System' : fontName,
|
||||
fontSize: normalizedFontSize(fields.fontSize ?? 14),
|
||||
fontName: fontName === '' ? 'Dialog' : fontName,
|
||||
fontSize: normalizedFontSize(fields.fontSize ?? 12),
|
||||
isBold: fields.isBold ?? false,
|
||||
isItalic: fields.isItalic ?? false,
|
||||
order: Math.max(0, fields.order),
|
||||
};
|
||||
}
|
||||
|
||||
/** Rectangles below text, then by order, then by id. */
|
||||
/** Java's style of a font (Font.PLAIN 0, BOLD 1, ITALIC 2, both 3). */
|
||||
export function documentationFontStyle(item: Pick<DocumentationItem, 'isBold' | 'isItalic'>): number {
|
||||
return (item.isBold ? 1 : 0) | (item.isItalic ? 2 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The order to draw them in, the last on top: as Java's panel stacks them
|
||||
* (GUIDocumentationPanel.updateViewport, the first component on top) —
|
||||
* texts over rectangles, of one kind the earlier over the later.
|
||||
*/
|
||||
export function inJavaDrawingOrder<T extends Pick<DocumentationItem, 'id' | 'kind' | 'order'>>(items: readonly T[]): T[] {
|
||||
const layer = (item: T): number => (item.kind === 'rectangle' ? 0 : 1);
|
||||
return [...items].sort((lhs, rhs) => layer(lhs) - layer(rhs) || rhs.order - lhs.order || compareIds(rhs.id, lhs.id));
|
||||
}
|
||||
|
||||
/** Rectangles first, then text, each by order, then by id: the list of the file. */
|
||||
export function inDeterministicRenderOrder(items: readonly DocumentationItem[]): DocumentationItem[] {
|
||||
const layer = (item: DocumentationItem): number => (item.kind === 'rectangle' ? 0 : 1);
|
||||
return [...items].sort(
|
||||
@@ -148,3 +206,36 @@ export function deterministicDocumentationUuid(seed: string): Id {
|
||||
const hex = high.toString(16).padStart(16, '0') + low.toString(16).padStart(16, '0');
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`.toUpperCase();
|
||||
}
|
||||
|
||||
/** The edges a drag on an item's border moves (JDocuElement: LEFT_SIZING, RIGHT_SIZING, UPPER_SIZING, LOWER_SIZING). */
|
||||
export interface DocumentationEdges {
|
||||
readonly left?: boolean;
|
||||
readonly right?: boolean;
|
||||
readonly top?: boolean;
|
||||
readonly bottom?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* JDocuElement's drag on its border: a moved edge follows the pointer, the
|
||||
* opposite one stays; below the kind's smallest size the moved edge stops
|
||||
* there. `pointer` is where the pointer is, in the drawing's coordinates.
|
||||
*/
|
||||
export function resizedFrame(frame: Rect, kind: DocumentationItemKind, edges: DocumentationEdges, pointer: { readonly x: number; readonly y: number }): Rect {
|
||||
const minimum = documentationMinimumSize(kind);
|
||||
let { x, y, width, height } = frame;
|
||||
if (edges.left === true) {
|
||||
const right = frame.x + frame.width;
|
||||
width = Math.max(right - pointer.x, minimum.width);
|
||||
x = right - width;
|
||||
} else if (edges.right === true) {
|
||||
width = Math.max(pointer.x - frame.x, minimum.width);
|
||||
}
|
||||
if (edges.top === true) {
|
||||
const bottom = frame.y + frame.height;
|
||||
height = Math.max(bottom - pointer.y, minimum.height);
|
||||
y = bottom - height;
|
||||
} else if (edges.bottom === true) {
|
||||
height = Math.max(pointer.y - frame.y, minimum.height);
|
||||
}
|
||||
return { x, y, width, height };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// The documentation items of a document, changed as Java's documentation mode
|
||||
// changes them (JDocuElement, measured with tools/java-oracle's DocuRun): a
|
||||
// new text or rectangle where it is dropped, moved and resized in whole
|
||||
// pixels, its colour, its font size and style, removed. A text's words are
|
||||
// its Y.Text, typed into by everyone at once.
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import {
|
||||
DOCUMENTATION_BLACK,
|
||||
DOCUMENTATION_GREY,
|
||||
DOCUMENTATION_NEW_SIZE,
|
||||
normalizedFrame,
|
||||
type DocumentationColor,
|
||||
type DocumentationFontStyle,
|
||||
type DocumentationItemKind,
|
||||
type Id,
|
||||
type Rect,
|
||||
} from '@mdg-filius/core';
|
||||
import { ROOTS } from './projectDoc.js';
|
||||
|
||||
const docs = (doc: Y.Doc) => doc.getMap<Y.Map<unknown>>(ROOTS.docs);
|
||||
|
||||
/**
|
||||
* A new item, its top left corner at (x, y): 100 × 50, a text empty and black
|
||||
* in Dialog 12, a rectangle grey; after all others in the list, so of its kind
|
||||
* the lowest, as Java adds it.
|
||||
*/
|
||||
export function addDocumentation(doc: Y.Doc, item: { readonly id: Id; readonly kind: DocumentationItemKind; readonly x: number; readonly y: number }, origin: unknown): void {
|
||||
doc.transact(() => {
|
||||
const order = Math.max(-1, ...[...docs(doc).values()].map((each) => each.get('order') as number)) + 1;
|
||||
const color = item.kind === 'text' ? DOCUMENTATION_BLACK : DOCUMENTATION_GREY;
|
||||
const map = new Y.Map<unknown>();
|
||||
map.set('kind', item.kind);
|
||||
map.set('x', Math.round(item.x));
|
||||
map.set('y', Math.round(item.y));
|
||||
map.set('width', DOCUMENTATION_NEW_SIZE.width);
|
||||
map.set('height', DOCUMENTATION_NEW_SIZE.height);
|
||||
map.set('text', new Y.Text(''));
|
||||
map.set('red', color.red);
|
||||
map.set('green', color.green);
|
||||
map.set('blue', color.blue);
|
||||
map.set('alpha', color.alpha);
|
||||
map.set('font', 'Dialog');
|
||||
map.set('size', 12);
|
||||
map.set('bold', false);
|
||||
map.set('italic', false);
|
||||
map.set('order', order);
|
||||
docs(doc).set(item.id, map);
|
||||
}, origin);
|
||||
}
|
||||
|
||||
/** Its place and size, in whole pixels (Java's bounds are ints), inside the editor's world. */
|
||||
export function setDocumentationFrame(doc: Y.Doc, id: Id, frame: Rect, origin: unknown): void {
|
||||
const map = docs(doc).get(id);
|
||||
if (map === undefined) return;
|
||||
const normalized = normalizedFrame({ x: Math.round(frame.x), y: Math.round(frame.y), width: Math.round(frame.width), height: Math.round(frame.height) });
|
||||
doc.transact(() => {
|
||||
for (const key of ['x', 'y', 'width', 'height'] as const) if (map.get(key) !== normalized[key]) map.set(key, normalized[key]);
|
||||
}, origin);
|
||||
}
|
||||
|
||||
export function setDocumentationColor(doc: Y.Doc, id: Id, color: DocumentationColor, origin: unknown): void {
|
||||
const map = docs(doc).get(id);
|
||||
if (map === undefined) return;
|
||||
doc.transact(() => {
|
||||
map.set('red', color.red);
|
||||
map.set('green', color.green);
|
||||
map.set('blue', color.blue);
|
||||
map.set('alpha', color.alpha);
|
||||
}, origin);
|
||||
}
|
||||
|
||||
/** A text's font size, or its style, which replaces the one it had (Java's deriveFont: italic, then bold, is bold). */
|
||||
export function setDocumentationFont(doc: Y.Doc, id: Id, font: { readonly size?: number; readonly style?: DocumentationFontStyle }, origin: unknown): void {
|
||||
const map = docs(doc).get(id);
|
||||
if (map === undefined || map.get('kind') !== 'text') return;
|
||||
doc.transact(() => {
|
||||
if (font.size !== undefined) map.set('size', font.size);
|
||||
if (font.style !== undefined) {
|
||||
map.set('bold', font.style === 'bold');
|
||||
map.set('italic', font.style === 'italic');
|
||||
}
|
||||
}, origin);
|
||||
}
|
||||
|
||||
export function removeDocumentation(doc: Y.Doc, id: Id, origin: unknown): void {
|
||||
doc.transact(() => docs(doc).delete(id), origin);
|
||||
}
|
||||
|
||||
/** A text's words, to type into; undefined for a rectangle or no item. */
|
||||
export function documentationText(doc: Y.Doc, id: Id): Y.Text | undefined {
|
||||
const map = docs(doc).get(id);
|
||||
return map?.get('kind') === 'text' ? (map.get('text') as Y.Text) : undefined;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Public surface of the shared document.
|
||||
export * from './commands.js';
|
||||
export * from './documentation.js';
|
||||
export * from './fields.js';
|
||||
export * from './files.js';
|
||||
export * from './janitor.js';
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// own? }
|
||||
// links id → { order, sourceNode, sourcePort, targetNode, targetPort }
|
||||
// docs id → { kind, x, y, width, height, text, red, green, blue, alpha,
|
||||
// font, size, bold, order }
|
||||
// font, size, bold, italic, order }
|
||||
// archive name → bytes (the project's own Java applications)
|
||||
//
|
||||
// Departures from §5.2, to keep the model whole: a port's settings are the
|
||||
@@ -190,6 +190,7 @@ function documentationMap(item: DocumentationItem): Y.Map<unknown> {
|
||||
map.set('font', item.fontName);
|
||||
map.set('size', item.fontSize);
|
||||
map.set('bold', item.isBold);
|
||||
map.set('italic', item.isItalic);
|
||||
map.set('order', item.order);
|
||||
return map;
|
||||
}
|
||||
@@ -336,6 +337,8 @@ export function readProject(doc: Y.Doc): ImportedProject {
|
||||
fontName: map.get('font') as string,
|
||||
fontSize: map.get('size') as number,
|
||||
isBold: map.get('bold') as boolean,
|
||||
// A document made before F4.8a has no italic.
|
||||
isItalic: map.get('italic') === true,
|
||||
order: map.get('order') as number,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// so that it can run on every change while someone drags a device.
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import { compareIds, stableGeneratedMacAddress, type DocumentationItemKind, type HostLabelPolicy, type InstallableProgram, type NodeKind } from '@mdg-filius/core';
|
||||
import { inJavaDrawingOrder, stableGeneratedMacAddress, type DocumentationItemKind, type HostLabelPolicy, type InstallableProgram, type NodeKind } from '@mdg-filius/core';
|
||||
import { inOrder, ROOTS } from './projectDoc.js';
|
||||
|
||||
export interface NodeView {
|
||||
@@ -44,6 +44,7 @@ export interface DocumentationView {
|
||||
readonly font: string;
|
||||
readonly size: number;
|
||||
readonly bold: boolean;
|
||||
readonly italic: boolean;
|
||||
readonly order: number;
|
||||
}
|
||||
|
||||
@@ -153,12 +154,11 @@ export function topologyView(doc: Y.Doc): TopologyView {
|
||||
font: item.get('font') as string,
|
||||
size: item.get('size') as number,
|
||||
bold: item.get('bold') as boolean,
|
||||
italic: item.get('italic') === true,
|
||||
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 };
|
||||
// In Java's drawing order, the last on top: texts over rectangles, of one kind the earlier over the later.
|
||||
return { nodes, links, docs: inJavaDrawingOrder(docs) };
|
||||
}
|
||||
|
||||
/** What a computer's desktop needs of the document (Java's GUIDesktopWindow, GUIDesktopPanel). */
|
||||
|
||||
@@ -0,0 +1,894 @@
|
||||
{
|
||||
"new": {
|
||||
"text": {
|
||||
"bounds": [
|
||||
100,
|
||||
100,
|
||||
100,
|
||||
50
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
255
|
||||
],
|
||||
"font": {
|
||||
"name": "Dialog",
|
||||
"style": 0,
|
||||
"size": 12
|
||||
},
|
||||
"text": ""
|
||||
},
|
||||
"rectangle": {
|
||||
"bounds": [
|
||||
300,
|
||||
100,
|
||||
100,
|
||||
50
|
||||
],
|
||||
"color": [
|
||||
204,
|
||||
204,
|
||||
204,
|
||||
255
|
||||
],
|
||||
"font": null,
|
||||
"text": null
|
||||
}
|
||||
},
|
||||
"dropped": {
|
||||
"text": {
|
||||
"bounds": [
|
||||
94,
|
||||
94,
|
||||
112,
|
||||
78
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
255
|
||||
],
|
||||
"font": {
|
||||
"name": "Dialog",
|
||||
"style": 0,
|
||||
"size": 12
|
||||
},
|
||||
"text": ""
|
||||
},
|
||||
"textUnfocused": {
|
||||
"bounds": [
|
||||
100,
|
||||
100,
|
||||
100,
|
||||
50
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
255
|
||||
],
|
||||
"font": {
|
||||
"name": "Dialog",
|
||||
"style": 0,
|
||||
"size": 12
|
||||
},
|
||||
"text": ""
|
||||
}
|
||||
},
|
||||
"fonts": [
|
||||
{
|
||||
"name": "Dialog",
|
||||
"style": 0,
|
||||
"size": 18
|
||||
},
|
||||
{
|
||||
"name": "Dialog",
|
||||
"style": 2,
|
||||
"size": 18
|
||||
},
|
||||
{
|
||||
"name": "Dialog",
|
||||
"style": 1,
|
||||
"size": 18
|
||||
},
|
||||
{
|
||||
"name": "Dialog",
|
||||
"style": 1,
|
||||
"size": 12
|
||||
},
|
||||
{
|
||||
"name": "Dialog",
|
||||
"style": 1,
|
||||
"size": 24
|
||||
}
|
||||
],
|
||||
"textColor": [
|
||||
79,
|
||||
130,
|
||||
189,
|
||||
255
|
||||
],
|
||||
"rectangleColor": [
|
||||
255,
|
||||
201,
|
||||
201,
|
||||
255
|
||||
],
|
||||
"menus": {
|
||||
"text": [
|
||||
{
|
||||
"menu": "Schriftgröße",
|
||||
"items": [
|
||||
"Normal *",
|
||||
"Groß",
|
||||
"Sehr Groß"
|
||||
]
|
||||
},
|
||||
{
|
||||
"menu": "Schriftstil",
|
||||
"items": [
|
||||
"Normal *",
|
||||
"Kursiv",
|
||||
"Fett"
|
||||
]
|
||||
},
|
||||
{
|
||||
"colors": "Farbe",
|
||||
"list": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
255
|
||||
],
|
||||
[
|
||||
102,
|
||||
102,
|
||||
102,
|
||||
255
|
||||
],
|
||||
[
|
||||
153,
|
||||
153,
|
||||
153,
|
||||
255
|
||||
],
|
||||
[
|
||||
179,
|
||||
179,
|
||||
179,
|
||||
255
|
||||
],
|
||||
[
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
255
|
||||
],
|
||||
[
|
||||
255,
|
||||
0,
|
||||
0,
|
||||
255
|
||||
],
|
||||
[
|
||||
179,
|
||||
163,
|
||||
199,
|
||||
255
|
||||
],
|
||||
[
|
||||
79,
|
||||
130,
|
||||
189,
|
||||
255
|
||||
],
|
||||
[
|
||||
156,
|
||||
186,
|
||||
89,
|
||||
255
|
||||
],
|
||||
[
|
||||
148,
|
||||
138,
|
||||
84,
|
||||
255
|
||||
],
|
||||
[
|
||||
148,
|
||||
56,
|
||||
54,
|
||||
255
|
||||
],
|
||||
[
|
||||
94,
|
||||
74,
|
||||
122,
|
||||
255
|
||||
],
|
||||
[
|
||||
31,
|
||||
74,
|
||||
125,
|
||||
255
|
||||
],
|
||||
[
|
||||
110,
|
||||
135,
|
||||
56,
|
||||
255
|
||||
],
|
||||
[
|
||||
89,
|
||||
84,
|
||||
48,
|
||||
255
|
||||
]
|
||||
],
|
||||
"chosen": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"rows": 3
|
||||
},
|
||||
{
|
||||
"separator": true
|
||||
},
|
||||
{
|
||||
"item": "Löschen"
|
||||
}
|
||||
],
|
||||
"textAfterLarge": [
|
||||
{
|
||||
"menu": "Schriftgröße",
|
||||
"items": [
|
||||
"Normal",
|
||||
"Groß *",
|
||||
"Sehr Groß"
|
||||
]
|
||||
},
|
||||
{
|
||||
"menu": "Schriftstil",
|
||||
"items": [
|
||||
"Normal *",
|
||||
"Kursiv",
|
||||
"Fett"
|
||||
]
|
||||
},
|
||||
{
|
||||
"colors": "Farbe",
|
||||
"list": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
255
|
||||
],
|
||||
[
|
||||
102,
|
||||
102,
|
||||
102,
|
||||
255
|
||||
],
|
||||
[
|
||||
153,
|
||||
153,
|
||||
153,
|
||||
255
|
||||
],
|
||||
[
|
||||
179,
|
||||
179,
|
||||
179,
|
||||
255
|
||||
],
|
||||
[
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
255
|
||||
],
|
||||
[
|
||||
255,
|
||||
0,
|
||||
0,
|
||||
255
|
||||
],
|
||||
[
|
||||
179,
|
||||
163,
|
||||
199,
|
||||
255
|
||||
],
|
||||
[
|
||||
79,
|
||||
130,
|
||||
189,
|
||||
255
|
||||
],
|
||||
[
|
||||
156,
|
||||
186,
|
||||
89,
|
||||
255
|
||||
],
|
||||
[
|
||||
148,
|
||||
138,
|
||||
84,
|
||||
255
|
||||
],
|
||||
[
|
||||
148,
|
||||
56,
|
||||
54,
|
||||
255
|
||||
],
|
||||
[
|
||||
94,
|
||||
74,
|
||||
122,
|
||||
255
|
||||
],
|
||||
[
|
||||
31,
|
||||
74,
|
||||
125,
|
||||
255
|
||||
],
|
||||
[
|
||||
110,
|
||||
135,
|
||||
56,
|
||||
255
|
||||
],
|
||||
[
|
||||
89,
|
||||
84,
|
||||
48,
|
||||
255
|
||||
]
|
||||
],
|
||||
"chosen": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"rows": 3
|
||||
},
|
||||
{
|
||||
"separator": true
|
||||
},
|
||||
{
|
||||
"item": "Löschen"
|
||||
}
|
||||
],
|
||||
"rectangle": [
|
||||
{
|
||||
"colors": "Farbe",
|
||||
"list": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
255
|
||||
],
|
||||
[
|
||||
204,
|
||||
204,
|
||||
204,
|
||||
255
|
||||
],
|
||||
[
|
||||
235,
|
||||
235,
|
||||
235,
|
||||
255
|
||||
],
|
||||
[
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
255
|
||||
],
|
||||
[
|
||||
255,
|
||||
201,
|
||||
201,
|
||||
255
|
||||
],
|
||||
[
|
||||
252,
|
||||
201,
|
||||
163,
|
||||
255
|
||||
],
|
||||
[
|
||||
255,
|
||||
255,
|
||||
227,
|
||||
255
|
||||
],
|
||||
[
|
||||
212,
|
||||
207,
|
||||
181,
|
||||
255
|
||||
],
|
||||
[
|
||||
222,
|
||||
212,
|
||||
255,
|
||||
255
|
||||
],
|
||||
[
|
||||
209,
|
||||
232,
|
||||
255,
|
||||
255
|
||||
],
|
||||
[
|
||||
191,
|
||||
255,
|
||||
219,
|
||||
255
|
||||
],
|
||||
[
|
||||
204,
|
||||
222,
|
||||
173,
|
||||
255
|
||||
]
|
||||
],
|
||||
"chosen": [
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"rows": 3
|
||||
},
|
||||
{
|
||||
"separator": true
|
||||
},
|
||||
{
|
||||
"item": "Löschen"
|
||||
}
|
||||
]
|
||||
},
|
||||
"smallest": {
|
||||
"rectangle": {
|
||||
"bounds": [
|
||||
300,
|
||||
100,
|
||||
20,
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
255,
|
||||
201,
|
||||
201,
|
||||
255
|
||||
],
|
||||
"font": null,
|
||||
"text": null
|
||||
},
|
||||
"rectangleByItsTopLeft": {
|
||||
"bounds": [
|
||||
300,
|
||||
100,
|
||||
20,
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
255,
|
||||
201,
|
||||
201,
|
||||
255
|
||||
],
|
||||
"font": null,
|
||||
"text": null
|
||||
},
|
||||
"textFocused": {
|
||||
"bounds": [
|
||||
94,
|
||||
94,
|
||||
112,
|
||||
78
|
||||
],
|
||||
"color": [
|
||||
79,
|
||||
130,
|
||||
189,
|
||||
255
|
||||
],
|
||||
"font": {
|
||||
"name": "Dialog",
|
||||
"style": 1,
|
||||
"size": 12
|
||||
},
|
||||
"text": ""
|
||||
},
|
||||
"textFocusedSmallest": {
|
||||
"bounds": [
|
||||
94,
|
||||
94,
|
||||
100,
|
||||
48
|
||||
],
|
||||
"color": [
|
||||
79,
|
||||
130,
|
||||
189,
|
||||
255
|
||||
],
|
||||
"font": {
|
||||
"name": "Dialog",
|
||||
"style": 1,
|
||||
"size": 12
|
||||
},
|
||||
"text": ""
|
||||
},
|
||||
"textSmallest": {
|
||||
"bounds": [
|
||||
100,
|
||||
100,
|
||||
88,
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
79,
|
||||
130,
|
||||
189,
|
||||
255
|
||||
],
|
||||
"font": {
|
||||
"name": "Dialog",
|
||||
"style": 1,
|
||||
"size": 12
|
||||
},
|
||||
"text": ""
|
||||
},
|
||||
"textDraggedUnfocused": {
|
||||
"bounds": [
|
||||
100,
|
||||
100,
|
||||
88,
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
79,
|
||||
130,
|
||||
189,
|
||||
255
|
||||
],
|
||||
"font": {
|
||||
"name": "Dialog",
|
||||
"style": 1,
|
||||
"size": 12
|
||||
},
|
||||
"text": ""
|
||||
}
|
||||
},
|
||||
"painted": {
|
||||
"rectangleEnabled": {
|
||||
"50,25": [
|
||||
255,
|
||||
201,
|
||||
201,
|
||||
200
|
||||
],
|
||||
"1,25": [
|
||||
204,
|
||||
160,
|
||||
160,
|
||||
255
|
||||
],
|
||||
"2,25": [
|
||||
255,
|
||||
201,
|
||||
201,
|
||||
200
|
||||
],
|
||||
"0,25": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"rectangleFocused": {
|
||||
"50,25": [
|
||||
255,
|
||||
201,
|
||||
201,
|
||||
200
|
||||
],
|
||||
"3,25": [
|
||||
56,
|
||||
152,
|
||||
248,
|
||||
77
|
||||
],
|
||||
"1,25": [
|
||||
122,
|
||||
186,
|
||||
250,
|
||||
255
|
||||
]
|
||||
},
|
||||
"rectangleDisabled": {
|
||||
"50,25": [
|
||||
255,
|
||||
212,
|
||||
212,
|
||||
255
|
||||
],
|
||||
"1,25": [
|
||||
204,
|
||||
160,
|
||||
160,
|
||||
255
|
||||
],
|
||||
"2,25": [
|
||||
255,
|
||||
212,
|
||||
212,
|
||||
255
|
||||
],
|
||||
"0,25": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"textEnabled": {
|
||||
"0,25": [
|
||||
179,
|
||||
179,
|
||||
179,
|
||||
255
|
||||
],
|
||||
"1,25": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"50,0": [
|
||||
179,
|
||||
179,
|
||||
179,
|
||||
255
|
||||
],
|
||||
"99,25": [
|
||||
179,
|
||||
179,
|
||||
179,
|
||||
255
|
||||
]
|
||||
},
|
||||
"textDisabled": {
|
||||
"0,25": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"1,25": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"50,0": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"stacking": [
|
||||
"text 1",
|
||||
"text 2",
|
||||
"rectangle 1",
|
||||
"rectangle 2"
|
||||
],
|
||||
"topAt": "text 1",
|
||||
"saved": [
|
||||
{
|
||||
"type": 2,
|
||||
"text": "Kursiv und mittel\nzweite Zeile",
|
||||
"bounds": [
|
||||
40,
|
||||
40,
|
||||
160,
|
||||
60
|
||||
],
|
||||
"color": [
|
||||
79,
|
||||
130,
|
||||
189,
|
||||
255
|
||||
],
|
||||
"font": {
|
||||
"name": "Dialog",
|
||||
"style": 2,
|
||||
"size": 18
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": 2,
|
||||
"text": "Fett",
|
||||
"bounds": [
|
||||
40,
|
||||
140,
|
||||
120,
|
||||
50
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
255
|
||||
],
|
||||
"font": {
|
||||
"name": "Dialog",
|
||||
"style": 1,
|
||||
"size": 24
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"text": null,
|
||||
"bounds": [
|
||||
20,
|
||||
20,
|
||||
300,
|
||||
200
|
||||
],
|
||||
"color": [
|
||||
255,
|
||||
201,
|
||||
201,
|
||||
255
|
||||
],
|
||||
"font": null
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"text": null,
|
||||
"bounds": [
|
||||
400,
|
||||
20,
|
||||
100,
|
||||
50
|
||||
],
|
||||
"color": [
|
||||
204,
|
||||
204,
|
||||
204,
|
||||
255
|
||||
],
|
||||
"font": null
|
||||
}
|
||||
],
|
||||
"loaded": [
|
||||
{
|
||||
"type": 2,
|
||||
"text": "Kursiv und mittel\nzweite Zeile",
|
||||
"bounds": [
|
||||
40,
|
||||
40,
|
||||
160,
|
||||
60
|
||||
],
|
||||
"color": [
|
||||
79,
|
||||
130,
|
||||
189,
|
||||
255
|
||||
],
|
||||
"font": {
|
||||
"name": "Dialog",
|
||||
"style": 2,
|
||||
"size": 18
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": 2,
|
||||
"text": "Fett",
|
||||
"bounds": [
|
||||
40,
|
||||
140,
|
||||
120,
|
||||
50
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
255
|
||||
],
|
||||
"font": {
|
||||
"name": "Dialog",
|
||||
"style": 1,
|
||||
"size": 24
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"text": null,
|
||||
"bounds": [
|
||||
20,
|
||||
20,
|
||||
300,
|
||||
200
|
||||
],
|
||||
"color": [
|
||||
255,
|
||||
201,
|
||||
201,
|
||||
255
|
||||
],
|
||||
"font": null
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"text": null,
|
||||
"bounds": [
|
||||
400,
|
||||
20,
|
||||
100,
|
||||
50
|
||||
],
|
||||
"color": [
|
||||
204,
|
||||
204,
|
||||
204,
|
||||
255
|
||||
],
|
||||
"font": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
// The documentation items against Java's documentation mode
|
||||
// (documentation.java.json; tools/java-oracle's DocuRun on JDocuElement):
|
||||
// a new text and rectangle, the colours and font sizes Java's menus offer,
|
||||
// the font and colour chosen there, the smallest sizes a drag leaves, the
|
||||
// stacking, and a project Java saved with documentation (fixtures/
|
||||
// documentation.fls) read, kept in the document and written again.
|
||||
|
||||
import { 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 {
|
||||
DOCUMENTATION_FONT_SIZES,
|
||||
DOCUMENTATION_RECTANGLE_COLORS,
|
||||
DOCUMENTATION_TEXT_COLORS,
|
||||
documentationFontStyle,
|
||||
documentationItem,
|
||||
inJavaDrawingOrder,
|
||||
resizedFrame,
|
||||
type DocumentationColor,
|
||||
type DocumentationItem,
|
||||
} from '@mdg-filius/core';
|
||||
import { exportProject, openProjectArchive } from '@mdg-filius/fls';
|
||||
import { addDocumentation, documentationText, readProject, removeDocumentation, setDocumentationColor, setDocumentationFont, setDocumentationFrame, topologyView, writeProject } from '../src/index.js';
|
||||
|
||||
type Rgba = [number, number, number, number];
|
||||
interface JavaFont {
|
||||
readonly name: string;
|
||||
readonly style: number;
|
||||
readonly size: number;
|
||||
}
|
||||
interface JavaElement {
|
||||
readonly bounds: [number, number, number, number];
|
||||
readonly color: Rgba;
|
||||
readonly font: JavaFont | null;
|
||||
readonly text: string | null;
|
||||
}
|
||||
interface JavaItem extends JavaElement {
|
||||
readonly type: number;
|
||||
}
|
||||
interface JavaMenuEntry {
|
||||
readonly menu?: string;
|
||||
readonly items?: string[];
|
||||
readonly colors?: string;
|
||||
readonly list?: Rgba[];
|
||||
readonly rows?: number;
|
||||
readonly item?: string;
|
||||
readonly separator?: boolean;
|
||||
}
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const java = JSON.parse(readFileSync(join(here, 'documentation.java.json'), 'utf8')) as {
|
||||
readonly new: { readonly text: JavaElement; readonly rectangle: JavaElement };
|
||||
readonly fonts: JavaFont[];
|
||||
readonly textColor: Rgba;
|
||||
readonly rectangleColor: Rgba;
|
||||
readonly menus: { readonly text: JavaMenuEntry[]; readonly rectangle: JavaMenuEntry[] };
|
||||
readonly smallest: Record<string, JavaElement>;
|
||||
readonly stacking: string[];
|
||||
readonly topAt: string;
|
||||
readonly saved: JavaItem[];
|
||||
readonly loaded: JavaItem[];
|
||||
};
|
||||
|
||||
const channel = (value: number) => Math.round(value * 255);
|
||||
const rgba = (color: DocumentationColor): Rgba => [channel(color.red), channel(color.green), channel(color.blue), channel(color.alpha)];
|
||||
/** An item as Java's GUIDocuItem shows it: its bounds, colour and font (none for a rectangle). */
|
||||
const asJava = (item: DocumentationItem) => ({
|
||||
bounds: [item.frame.x, item.frame.y, item.frame.width, item.frame.height],
|
||||
color: rgba(item.color),
|
||||
font: item.kind === 'text' ? { name: item.fontName, style: documentationFontStyle(item), size: item.fontSize } : null,
|
||||
text: item.kind === 'text' ? item.text : null,
|
||||
});
|
||||
const itemOf = (doc: Y.Doc, id: string) => readProject(doc).documentationItems.find((item) => item.id === id)!;
|
||||
|
||||
describe("the documentation items, against Java's documentation mode", () => {
|
||||
it('makes a new text and rectangle as Java does: 100 × 50 where dropped, a text empty and black in Dialog 12, a rectangle grey', () => {
|
||||
const doc = new Y.Doc();
|
||||
addDocumentation(doc, { id: 'text', kind: 'text', x: 100, y: 100 }, 'me');
|
||||
addDocumentation(doc, { id: 'rectangle', kind: 'rectangle', x: 300.4, y: 99.6 }, 'me');
|
||||
expect(asJava(itemOf(doc, 'text'))).toEqual(java.new.text);
|
||||
expect(asJava(itemOf(doc, 'rectangle'))).toEqual(java.new.rectangle);
|
||||
// After the others: of its kind the lowest.
|
||||
expect([itemOf(doc, 'text').order, itemOf(doc, 'rectangle').order]).toEqual([0, 1]);
|
||||
// An item made without a font has Java's: Dialog 12, plain.
|
||||
expect(documentationItem({ id: 'x', kind: 'text', frame: { x: 0, y: 0, width: 5, height: 5 }, color: DOCUMENTATION_TEXT_COLORS[0]!, order: 0 })).toMatchObject({
|
||||
fontName: 'Dialog',
|
||||
fontSize: 12,
|
||||
isBold: false,
|
||||
isItalic: false,
|
||||
frame: { x: 0, y: 0, width: 5, height: 5 },
|
||||
});
|
||||
});
|
||||
|
||||
it("offers Java's colours and font sizes, and sets font and colour as Java's menu does", () => {
|
||||
const colors = (entries: JavaMenuEntry[]) => entries.find((entry) => entry.list !== undefined)!;
|
||||
expect(DOCUMENTATION_TEXT_COLORS.map(rgba)).toEqual(colors(java.menus.text).list);
|
||||
expect(DOCUMENTATION_RECTANGLE_COLORS.map(rgba)).toEqual(colors(java.menus.rectangle).list);
|
||||
expect([colors(java.menus.text).rows, colors(java.menus.rectangle).rows]).toEqual([3, 3]);
|
||||
expect(java.menus.text.map((entry) => entry.menu ?? entry.colors ?? entry.item ?? '-')).toEqual(['Schriftgröße', 'Schriftstil', 'Farbe', '-', 'Löschen']);
|
||||
expect(java.menus.rectangle.map((entry) => entry.menu ?? entry.colors ?? entry.item ?? '-')).toEqual(['Farbe', '-', 'Löschen']);
|
||||
|
||||
// „Groß“, „Kursiv“, „Fett“, „Normal“, „Sehr Groß“: a style replaces the one before.
|
||||
const doc = new Y.Doc();
|
||||
addDocumentation(doc, { id: 'text', kind: 'text', x: 100, y: 100 }, 'me');
|
||||
const font = () => asJava(itemOf(doc, 'text')).font;
|
||||
const fonts = [];
|
||||
for (const change of [{ size: DOCUMENTATION_FONT_SIZES[1] }, { style: 'italic' }, { style: 'bold' }, { size: DOCUMENTATION_FONT_SIZES[0] }, { size: DOCUMENTATION_FONT_SIZES[2] }] as const) {
|
||||
setDocumentationFont(doc, 'text', change, 'me');
|
||||
fonts.push(font());
|
||||
}
|
||||
expect(fonts).toEqual(java.fonts);
|
||||
// The drawing knows the style too.
|
||||
setDocumentationFont(doc, 'text', { style: 'italic' }, 'me');
|
||||
expect(topologyView(doc).docs.map((item) => [item.bold, item.italic])).toEqual([[false, true]]);
|
||||
setDocumentationFont(doc, 'text', { style: 'plain' }, 'me');
|
||||
expect(font()?.style).toBe(0);
|
||||
setDocumentationColor(doc, 'text', DOCUMENTATION_TEXT_COLORS[7]!, 'me');
|
||||
expect(rgba(itemOf(doc, 'text').color)).toEqual(java.textColor);
|
||||
|
||||
// A rectangle takes a colour, not a font.
|
||||
addDocumentation(doc, { id: 'rectangle', kind: 'rectangle', x: 300, y: 100 }, 'me');
|
||||
setDocumentationFont(doc, 'rectangle', { size: 24, style: 'bold' }, 'me');
|
||||
setDocumentationColor(doc, 'rectangle', DOCUMENTATION_RECTANGLE_COLORS[4]!, 'me');
|
||||
expect(asJava(itemOf(doc, 'rectangle'))).toMatchObject({ color: java.rectangleColor, font: null });
|
||||
expect([itemOf(doc, 'rectangle').fontSize, itemOf(doc, 'rectangle').isBold]).toEqual([12, false]);
|
||||
});
|
||||
|
||||
it('leaves a rectangle at least 20 × 20 and a text at least 88 × 20, the opposite edge where it was', () => {
|
||||
const rectangle = { x: 300, y: 100, width: 100, height: 50 };
|
||||
const [x, y, width, height] = java.smallest.rectangle!.bounds;
|
||||
expect(resizedFrame(rectangle, 'rectangle', { right: true, bottom: true }, { x: 303, y: 103 })).toEqual({ x, y, width, height });
|
||||
// By its top left corner, far past the other: the right and lower edges stay.
|
||||
const small = { x: 300, y: 100, width: 20, height: 20 };
|
||||
const [ax, ay, aw, ah] = java.smallest.rectangleByItsTopLeft!.bounds;
|
||||
expect(resizedFrame(small, 'rectangle', { left: true, top: true }, { x: 500, y: 300 })).toEqual({ x: ax, y: ay, width: aw, height: ah });
|
||||
const [tx, ty, tw, th] = java.smallest.textSmallest!.bounds;
|
||||
expect(resizedFrame({ x: 100, y: 100, width: 100, height: 50 }, 'text', { right: true, bottom: true }, { x: 0, y: 0 })).toEqual({ x: tx, y: ty, width: tw, height: th });
|
||||
// Edges follow the pointer; the others stay.
|
||||
expect(resizedFrame(rectangle, 'rectangle', { left: true }, { x: 250, y: 0 })).toEqual({ x: 250, y: 100, width: 150, height: 50 });
|
||||
expect(resizedFrame(rectangle, 'rectangle', { top: true }, { x: 0, y: 130 })).toEqual({ x: 300, y: 130, width: 100, height: 20 });
|
||||
expect(resizedFrame(rectangle, 'rectangle', { bottom: true }, { x: 999, y: 300 })).toEqual({ x: 300, y: 100, width: 100, height: 200 });
|
||||
expect(resizedFrame(rectangle, 'rectangle', {}, { x: 0, y: 0 })).toEqual(rectangle);
|
||||
|
||||
// The document keeps whole pixels.
|
||||
const doc = new Y.Doc();
|
||||
addDocumentation(doc, { id: 'r', kind: 'rectangle', x: 0, y: 0 }, 'me');
|
||||
setDocumentationFrame(doc, 'r', { x: 10.4, y: 20.6, width: 30.5, height: 40.2 }, 'me');
|
||||
expect(itemOf(doc, 'r').frame).toEqual({ x: 10, y: 21, width: 31, height: 40 });
|
||||
});
|
||||
|
||||
it('stacks as Java does: texts over rectangles, of one kind the earlier over the later', () => {
|
||||
const doc = new Y.Doc();
|
||||
const names = ['text 1', 'rectangle 1', 'text 2', 'rectangle 2'];
|
||||
names.forEach((name, index) => addDocumentation(doc, { id: name, kind: name.startsWith('text') ? 'text' : 'rectangle', x: 50 + 10 * index, y: 50 + 10 * index }, 'me'));
|
||||
// Drawn in this order, the last on top; Java names the top one first.
|
||||
const drawn = topologyView(doc).docs.map((item) => item.id);
|
||||
expect([...drawn].reverse()).toEqual(java.stacking);
|
||||
expect(drawn.at(-1)).toBe(java.topAt);
|
||||
expect(inJavaDrawingOrder(readProject(doc).documentationItems).map((item) => item.id)).toEqual(drawn);
|
||||
});
|
||||
|
||||
it('reads what Java saved, keeps it in the document, and writes it as Java does', () => {
|
||||
let counter = 0;
|
||||
const project = openProjectArchive(new Uint8Array(readFileSync(join(here, 'fixtures/documentation.fls'))), () => `id-${++counter}`);
|
||||
expect(java.loaded).toEqual(java.saved);
|
||||
const byFile = (items: readonly DocumentationItem[]) => [...items].sort((lhs, rhs) => lhs.order - rhs.order);
|
||||
const javaItems = java.loaded.map(({ type, ...rest }) => ({ ...rest, type }));
|
||||
expect(byFile(project.documentationItems).map((item) => ({ ...asJava(item), type: item.kind === 'rectangle' ? 1 : 2 }))).toEqual(javaItems);
|
||||
|
||||
// Through the document and back: italic and all.
|
||||
const doc = new Y.Doc();
|
||||
writeProject(doc, project);
|
||||
expect(readProject(doc).documentationItems).toEqual(project.documentationItems);
|
||||
expect(documentationText(doc, project.documentationItems.find((item) => item.kind === 'text')!.id)).toBeInstanceOf(Y.Text);
|
||||
expect(documentationText(doc, project.documentationItems.find((item) => item.kind === 'rectangle')!.id)).toBeUndefined();
|
||||
|
||||
// Written as Java writes it (rectangles first in the list, which leaves Java's stacking of each kind as it was).
|
||||
const written = exportProject(readProject(doc)).topology.documentation.map((item) => ({
|
||||
...item,
|
||||
color: item.color === null ? null : [...item.color],
|
||||
font: item.font === null ? null : { ...item.font },
|
||||
}));
|
||||
const expected = [...java.saved.filter((item) => item.type === 1), ...java.saved.filter((item) => item.type === 2)].map((item) => ({
|
||||
type: item.type,
|
||||
text: item.text ?? '',
|
||||
x: item.bounds[0],
|
||||
y: item.bounds[1],
|
||||
width: item.bounds[2],
|
||||
height: item.bounds[3],
|
||||
color: item.color,
|
||||
// A rectangle has no font in Java; the model gives every item one.
|
||||
font: item.font ?? { name: 'Dialog', style: 0, size: 12 },
|
||||
}));
|
||||
expect(written).toEqual(expected);
|
||||
|
||||
// A colour of Java's menu is opaque, also over a translucent one from a file.
|
||||
const translucent = project.documentationItems[0]!;
|
||||
setDocumentationColor(doc, translucent.id, { ...translucent.color, alpha: 0.5 }, 'me');
|
||||
expect(itemOf(doc, translucent.id).color.alpha).toBe(0.5);
|
||||
setDocumentationColor(doc, translucent.id, DOCUMENTATION_RECTANGLE_COLORS[4]!, 'me');
|
||||
expect(rgba(itemOf(doc, translucent.id).color)).toEqual([255, 201, 201, 255]);
|
||||
|
||||
removeDocumentation(doc, project.documentationItems[0]!.id, 'me');
|
||||
expect(readProject(doc).documentationItems).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
BIN
Binary file not shown.
@@ -8,7 +8,7 @@ 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 { inJavaDrawingOrder, stableGeneratedMacAddress } from '@mdg-filius/core';
|
||||
import { exportProject, openProjectArchive, type ImportedProject } from '@mdg-filius/fls';
|
||||
import { addDevice, captureCards, desktopView, readProject, renameDevice, setHostAddress, setHostLabels, setInterfaceAddress, topologyView, writeProject } from '../src/index.js';
|
||||
|
||||
@@ -42,7 +42,21 @@ describe('the drawing of a document', () => {
|
||||
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);
|
||||
// In Java's drawing order, the last on top.
|
||||
expect(
|
||||
view.docs.map(({ id, kind, x, y, width, height, text, color, font, size, bold, italic, order }) => ({
|
||||
id,
|
||||
kind,
|
||||
frame: { x, y, width, height },
|
||||
text,
|
||||
color,
|
||||
fontName: font,
|
||||
fontSize: size,
|
||||
isBold: bold,
|
||||
isItalic: italic,
|
||||
order,
|
||||
})),
|
||||
).toEqual(inJavaDrawingOrder(project.documentationItems));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
// interface, as Java's own setters (setStandardGateway, setDNSServer) do:
|
||||
// Java reads them from the first interface only (getStandardGateway,
|
||||
// getDNSServer), and the model keeps no others;
|
||||
// - a documentation item always has a text and a font, bold or plain: where
|
||||
// Java has none, the empty text and Dialog 12 the model gives it;
|
||||
// - a documentation item always has a text, a font and a colour: where
|
||||
// Java has none, the empty text, Dialog 12 and the light grey Java shows;
|
||||
// - a switch's SSID the file lacked (Java rolls one) is the one the model
|
||||
// derives from the node's id;
|
||||
// - files in the model's order (directories first, then by name) and of the
|
||||
@@ -105,6 +105,7 @@ import {
|
||||
type ManualRoute,
|
||||
type RemoteLinkConfiguration,
|
||||
type SwitchConfiguration,
|
||||
documentationFontStyle,
|
||||
type DocumentationItem,
|
||||
type HostLabelPolicy,
|
||||
type HostWirelessConfiguration,
|
||||
@@ -554,6 +555,6 @@ function exportDocumentationItem(item: DocumentationItem): FlsDocumentationItem
|
||||
width: Math.round(item.frame.width),
|
||||
height: Math.round(item.frame.height),
|
||||
color: [channel(item.color.red), channel(item.color.green), channel(item.color.blue), channel(item.color.alpha)],
|
||||
font: { name: item.fontName, style: item.isBold ? 1 : 0, size: item.fontSize },
|
||||
font: { name: item.fontName, style: documentationFontStyle(item), size: item.fontSize },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,8 +65,7 @@ import {
|
||||
VirtualFileSystem,
|
||||
VirtualFileSystemError,
|
||||
topologyNode,
|
||||
DOCUMENTATION_BLACK,
|
||||
DOCUMENTATION_PALE_YELLOW,
|
||||
DOCUMENTATION_LIGHT_GRAY,
|
||||
type DeviceConfiguration,
|
||||
type DhcpClientConfiguration,
|
||||
type DhcpServerConfiguration,
|
||||
@@ -770,7 +769,8 @@ function importDocumentationItem(
|
||||
const seed = `java-doc|${order}|${item.type}|${item.x}|${item.y}|${item.width}|${item.height}|${text}`;
|
||||
let color;
|
||||
if (item.color === null) {
|
||||
color = kind === 'text' ? DOCUMENTATION_BLACK : DOCUMENTATION_PALE_YELLOW;
|
||||
// Java shows it light grey, either kind (JDocuElement.setColor(null)).
|
||||
color = DOCUMENTATION_LIGHT_GRAY;
|
||||
warnings.push(`documentation item ${order + 1} without a color, given the default`);
|
||||
} else {
|
||||
const [red, green, blue, alpha] = item.color;
|
||||
@@ -785,6 +785,7 @@ function importDocumentationItem(
|
||||
fontName: item.font?.name ?? 'Dialog',
|
||||
fontSize: item.font?.size ?? 12,
|
||||
isBold: ((item.font?.style ?? 0) & 1) === 1,
|
||||
isItalic: ((item.font?.style ?? 0) & 2) === 2,
|
||||
order,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
defaultSwitchConfiguration,
|
||||
dhcpServerConfiguration,
|
||||
DOCUMENTATION_MINIMUM_SIZE,
|
||||
firewallConfiguration,
|
||||
firewallRule,
|
||||
stableGeneratedMacAddress,
|
||||
@@ -104,8 +103,8 @@ function topologyOf(view: JavaFile) {
|
||||
* - generatedMac: a card whose MAC the file lacked (Java rolls a new one at
|
||||
* each load) has the one the simulation uses, derived from its port id;
|
||||
* - documentation: the items in the model's render order (rectangles below
|
||||
* text), a missing text empty, a missing font Dialog 12, and at least the
|
||||
* editor's smallest size (DOCUMENTATION_MINIMUM_SIZE);
|
||||
* text), a missing text empty, a missing font Dialog 12, a missing colour
|
||||
* the light grey Java shows for it;
|
||||
* - switchSsid: a switch's SSID the file lacked (Java rolls one) is the one
|
||||
* the model derives from the node's id;
|
||||
* - modemRoles: the model keeps a pair of modems, not which one serves: the
|
||||
@@ -212,8 +211,9 @@ const DEPARTURES = {
|
||||
return {
|
||||
...item,
|
||||
text: item.text ?? '',
|
||||
bounds: [x, y, Math.max(width!, DOCUMENTATION_MINIMUM_SIZE.width), Math.max(height!, DOCUMENTATION_MINIMUM_SIZE.height)],
|
||||
bounds: [x, y, width, height],
|
||||
font: item.font ?? ['Dialog', 12, 0],
|
||||
color: item.color ?? [192, 192, 192, 255],
|
||||
};
|
||||
});
|
||||
return { ...view, documentation: sortedDocumentation(items) };
|
||||
@@ -677,6 +677,7 @@ const SYNTHETIC: ProjectToExport = {
|
||||
fontName: 'Dialog',
|
||||
fontSize: 12,
|
||||
isBold: false,
|
||||
isItalic: false,
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
@@ -688,8 +689,34 @@ const SYNTHETIC: ProjectToExport = {
|
||||
fontName: 'SansSerif',
|
||||
fontSize: 16,
|
||||
isBold: true,
|
||||
isItalic: false,
|
||||
order: 1,
|
||||
},
|
||||
// Italic, bold and italic, smaller than Swift's smallest size: kept as they are.
|
||||
{
|
||||
id: 'd3',
|
||||
kind: 'text',
|
||||
frame: { x: 400, y: 10, width: 15, height: 10 },
|
||||
text: 'kursiv',
|
||||
color: { red: 0.31, green: 0.51, blue: 0.74, alpha: 1 },
|
||||
fontName: 'Dialog',
|
||||
fontSize: 6,
|
||||
isBold: false,
|
||||
isItalic: true,
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
id: 'd4',
|
||||
kind: 'text',
|
||||
frame: { x: 400, y: 40, width: 88, height: 20 },
|
||||
text: 'beides',
|
||||
color: { red: 0, green: 0, blue: 0, alpha: 1 },
|
||||
fontName: 'Serif',
|
||||
fontSize: 24,
|
||||
isBold: true,
|
||||
isItalic: true,
|
||||
order: 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -933,6 +960,9 @@ describe('a model beyond the corpus, as Java loads it', () => {
|
||||
documentation: [
|
||||
{ type: 1, text: '', bounds: [5, 6, 300, 200], color: [255, 128, 0, 128], font: ['Dialog', 12, 0] },
|
||||
{ type: 2, text: 'Netz A\nzweite Zeile', bounds: [40, 51, 120, 30], color: [0, 0, 255, 255], font: ['SansSerif', 16, 1] },
|
||||
// Java's italic (2) and bold italic (3), and sizes below Swift's smallest, as the model keeps them.
|
||||
{ type: 2, text: 'kursiv', bounds: [400, 10, 15, 10], color: [79, 130, 189, 255], font: ['Dialog', 6, 2] },
|
||||
{ type: 2, text: 'beides', bounds: [400, 40, 88, 20], color: [0, 0, 0, 255], font: ['Serif', 24, 3] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -604,7 +604,7 @@
|
||||
508,
|
||||
390,
|
||||
88,
|
||||
24
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -646,7 +646,7 @@
|
||||
453,
|
||||
124,
|
||||
100,
|
||||
24
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -667,7 +667,7 @@
|
||||
308,
|
||||
123,
|
||||
88,
|
||||
24
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -688,7 +688,7 @@
|
||||
631,
|
||||
391,
|
||||
88,
|
||||
24
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -808,7 +808,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"sha256": "edad61949744c17c11d75b6abd180f9916c0f06d643c332209c301778f7ab753"
|
||||
"sha256": "4c8042374d8f8db186bba920831d9cff9b7a0009c9d5bab094de920954b1fc98"
|
||||
},
|
||||
"ipad-fixtures/dns_server.fls": {
|
||||
"success": true,
|
||||
@@ -4403,7 +4403,7 @@
|
||||
570,
|
||||
583,
|
||||
88,
|
||||
24
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -4445,7 +4445,7 @@
|
||||
209,
|
||||
907,
|
||||
88,
|
||||
24
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -4550,7 +4550,7 @@
|
||||
458,
|
||||
383,
|
||||
88,
|
||||
24
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -4571,7 +4571,7 @@
|
||||
298,
|
||||
268,
|
||||
88,
|
||||
24
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -4586,7 +4586,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"sha256": "5bf3c778a721c2bf41c9203d28f07d3052fa0257f80dcc05911126bc336ab066"
|
||||
"sha256": "d31a32691f4b82cb683beda090b810671e269fe82a45b5b0810654998d1d8b7b"
|
||||
},
|
||||
"java-examples/Internet_Komplett_mit_eMail_Webserver_Intranet_Portforwarding_Firewall_DHCP_DE.fls": {
|
||||
"success": true,
|
||||
@@ -7192,7 +7192,7 @@
|
||||
570,
|
||||
583,
|
||||
88,
|
||||
24
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -7234,7 +7234,7 @@
|
||||
255,
|
||||
897,
|
||||
88,
|
||||
24
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -7318,7 +7318,7 @@
|
||||
440,
|
||||
907,
|
||||
108,
|
||||
24
|
||||
23
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -7333,7 +7333,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"sha256": "e56c3b8aec2610d3dedcf7f2c1b3d715aa7bb0eedeb5298ea21c5fd6ff0aba8d"
|
||||
"sha256": "fa1b339614ba08b53a678ac1242cd26d03b36ec224f4e9a42fba20df050bd31f"
|
||||
},
|
||||
"java-examples/Public_and_Private_Networks_2_EN.fls": {
|
||||
"success": true,
|
||||
@@ -7940,7 +7940,7 @@
|
||||
508,
|
||||
390,
|
||||
88,
|
||||
24
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -7982,7 +7982,7 @@
|
||||
453,
|
||||
124,
|
||||
100,
|
||||
24
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -8003,7 +8003,7 @@
|
||||
308,
|
||||
123,
|
||||
88,
|
||||
24
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -8024,7 +8024,7 @@
|
||||
631,
|
||||
391,
|
||||
88,
|
||||
24
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
@@ -8144,7 +8144,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"sha256": "edad61949744c17c11d75b6abd180f9916c0f06d643c332209c301778f7ab753"
|
||||
"sha256": "4c8042374d8f8db186bba920831d9cff9b7a0009c9d5bab094de920954b1fc98"
|
||||
},
|
||||
"java-examples/anwendung_komplett.fls": {
|
||||
"success": true,
|
||||
@@ -19681,8 +19681,50 @@
|
||||
16,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": 2,
|
||||
"text": "kursiv",
|
||||
"bounds": [
|
||||
400,
|
||||
10,
|
||||
15,
|
||||
10
|
||||
],
|
||||
"color": [
|
||||
79,
|
||||
130,
|
||||
189,
|
||||
255
|
||||
],
|
||||
"font": [
|
||||
"Dialog",
|
||||
6,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": 2,
|
||||
"text": "beides",
|
||||
"bounds": [
|
||||
400,
|
||||
40,
|
||||
88,
|
||||
20
|
||||
],
|
||||
"color": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
255
|
||||
],
|
||||
"font": [
|
||||
"Serif",
|
||||
24,
|
||||
3
|
||||
]
|
||||
}
|
||||
],
|
||||
"sha256": "7e1837d138d433f9721d2852de79f582ccb1e0454fcd100e30f37ab9e324ca58"
|
||||
"sha256": "dd6692f81ff1eca0214a1c85bce9dbccf99c62b41a84b3a01213f562ffdef483"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1282,18 +1282,28 @@ describe('importProject rules', () => {
|
||||
{ ...item, type: 2, color: null },
|
||||
{ ...item, type: 1, width: 0, color: [1, 2, 3, 255] },
|
||||
{ ...item, type: 3, color: [1, 2, 3, 255] },
|
||||
{ ...item, type: 1, text: null, color: null },
|
||||
{ ...item, type: 2, width: 15, height: 10, color: [1, 2, 3, 255], font: { name: 'Serif', style: 3, size: 6 } },
|
||||
{ ...item, type: 2, color: [1, 2, 3, 255], font: { name: 'Dialog', style: 2, size: 18 } },
|
||||
],
|
||||
}),
|
||||
counter(),
|
||||
);
|
||||
|
||||
expect(project.documentationItems.map((doc) => [doc.kind, doc.text, doc.color, doc.fontName, doc.fontSize])).toEqual([
|
||||
['text', 'Hallo', { red: 0, green: 0, blue: 0, alpha: 1 }, 'Dialog', 12],
|
||||
// Without a colour, light grey, as Java shows it (JDocuElement.setColor(null)), either kind; italic kept; a small size and font size kept.
|
||||
const grey = { red: 192 / 255, green: 192 / 255, blue: 192 / 255, alpha: 1 };
|
||||
const rgb = { red: 1 / 255, green: 2 / 255, blue: 3 / 255, alpha: 1 };
|
||||
expect(project.documentationItems.map((doc) => [doc.kind, doc.text, doc.color, doc.fontName, doc.fontSize, doc.isBold, doc.isItalic, doc.frame.width, doc.frame.height])).toEqual([
|
||||
['rectangle', '', grey, 'Dialog', 12, false, false, 100, 30],
|
||||
['text', 'Hallo', grey, 'Dialog', 12, false, false, 100, 30],
|
||||
['text', 'Hallo', rgb, 'Serif', 6, true, true, 15, 10],
|
||||
['text', 'Hallo', rgb, 'Dialog', 18, false, true, 100, 30],
|
||||
]);
|
||||
expect(project.warnings).toEqual([
|
||||
'documentation item 1 without a color, given the default',
|
||||
'documentation item without size, left out',
|
||||
'documentation item of type 3, left out (known: RECT = 1, TEXT = 2)',
|
||||
'documentation item 2 without a color, given the default',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user