@peter.prib/renderengine 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -213
- package/REFERENCE.md +272 -6
- package/dist/renderengine-workspace.umd.js +11980 -7421
- package/dist/renderengine.umd.js +8991 -5784
- package/examples/archimate.html +68 -66
- package/examples/data-model.html +534 -0
- package/package.json +70 -68
- package/src/bian.js +182 -0
- package/src/charting/CheckboxTreeSelector.js +373 -304
- package/src/charting/DiagramEditor.js +53 -32
- package/src/charting/DiagramTypeRegistry.js +13 -3
- package/src/charting/ModelCheckDialog.js +63 -0
- package/src/charting/ModelChecker.js +26 -0
- package/src/charting/ViewTabStrip.js +94 -81
- package/src/charting/archimate/ArchimateCanvasEditor.js +825 -815
- package/src/charting/archimate/ArchimateElementTemplate.js +208 -195
- package/src/charting/datamodel/BianLandscapeReader.js +185 -0
- package/src/charting/datamodel/BianSemanticApiReader.js +183 -0
- package/src/charting/datamodel/DataModel.js +645 -0
- package/src/charting/datamodel/DataModelChecker.js +92 -0
- package/src/charting/datamodel/DataModelCompared.js +127 -0
- package/src/charting/datamodel/DataModelContext.js +108 -0
- package/src/charting/datamodel/DataModelFileFormat.js +33 -0
- package/src/charting/datamodel/DataModelPerspective.js +129 -0
- package/src/charting/datamodel/DataType.js +108 -0
- package/src/charting/datamodel/EnumerationType.js +51 -0
- package/src/charting/datamodel/LevelMapping.js +38 -0
- package/src/charting/datamodel/LevelSchema.js +41 -0
- package/src/charting/datamodel/ServiceDomain.js +177 -0
- package/src/charting/datamodel/ServiceLandscape.js +104 -0
- package/src/charting/datamodel/StructuredType.js +63 -0
- package/src/charting/datamodel/index.js +15 -0
- package/src/charting/diagramModel/MergeModelCommand.js +43 -37
- package/src/charting/diagramModel/OpenViewList.js +75 -0
- package/src/charting/diagramModel/ViewSetChecker.js +22 -34
- package/src/charting/diagrams/DataModelDiagram.js +160 -86
- package/src/charting/diagrams/DataModelRelationshipStrategy.js +94 -38
- package/src/charting/diagrams/EdgeGeometry.js +578 -553
- package/src/charting/diagrams/GraphDiagram.js +11 -1
- package/src/charting/diagrams/NodeGeometry.js +169 -158
- package/src/charting/diagrams/nesting.js +31 -0
- package/src/charting/glyphs/PersonGlyph.js +24 -23
- package/src/charting/modelTree/ModelTree.js +202 -164
- package/src/charting/modelTree/ModelTreePanel.js +36 -3
- package/src/charting/modelTree/TreeViewMode.js +131 -110
- package/src/charting/modelTree/modes/FlatTreeMode.js +32 -46
- package/src/charting/modelTree/modes/LayerGroupedTreeMode.js +47 -62
- package/src/charting/modelTree/modes/TypeGroupedTreeMode.js +46 -60
- package/src/datamodel.js +132 -122
- package/src/diagramCatalog.js +35 -30
- package/src/index.js +2666 -2659
- package/src/workspace/EditorShell.js +166 -67
- package/src/workspace/PanelLayout.js +56 -0
- package/src/workspace/index.js +1 -0
- package/src/workspace/layout/TabsNode.js +205 -191
- package/src/workspace/parts/ArchimateCanvasPart.js +10 -9
- package/src/workspace/parts/ChartPart.js +6 -1
- package/src/workspace/parts/DataModelPart.js +544 -0
- package/src/workspace/parts/DiagramEditorPart.js +2 -8
- package/src/workspace/parts/EditorPart.js +461 -407
- package/src/workspace/parts/ModelFile.js +216 -187
- package/src/workspace/parts/ModelsPart.js +483 -192
- package/src/workspace/storage/BackupFolder.js +145 -0
- package/src/workspace/storage/KeyValueStore.js +92 -0
- package/src/workspace/storage/ModelLibrary.js +65 -0
- package/src/workspace/workArea.js +417 -413
- package/src/yaml/YamlReader.js +372 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BackupFolder — where backups go: a folder chosen once (the File System
|
|
3
|
+
* Access API, in Chrome and Edge, from file:// too) and remembered (its handle
|
|
4
|
+
* in a KeyValueStore), else the browser's download folder. Choose a folder that
|
|
5
|
+
* OneDrive, Google Drive or Dropbox syncs and the backups reach the cloud with
|
|
6
|
+
* no sign-in in the page. After au-accounts-app's backups.
|
|
7
|
+
*
|
|
8
|
+
* Each backup is written twice: stamped with the date and time to the second
|
|
9
|
+
* (`name-20261015-093012.ext`), so they gather as a history and none is
|
|
10
|
+
* overwritten, and (unless `keepUnstamped` is off) under its plain name,
|
|
11
|
+
* refreshed each time, for anything that expects one fixed name. Permission to
|
|
12
|
+
* write is checked at the time of writing, the only moment a browser grants it;
|
|
13
|
+
* refused, or failing, the backup goes to downloads instead. A short log says
|
|
14
|
+
* when each was taken and where it went.
|
|
15
|
+
*/
|
|
16
|
+
export class BackupFolder {
|
|
17
|
+
static HANDLE_KEY = 'backup:folder';
|
|
18
|
+
static LOG_KEY = 'backup:log';
|
|
19
|
+
static LOG_MAX = 20;
|
|
20
|
+
static DOWNLOADS = 'browser download folder';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} options
|
|
24
|
+
* @param {KeyValueStore} options.store - Remembers the folder and the log
|
|
25
|
+
* @param {Window} [options.window] - For showDirectoryPicker
|
|
26
|
+
* @param {(name: string, data: string|Uint8Array, mimeType: string) => void} options.download - The fallback
|
|
27
|
+
* @param {boolean} [options.keepUnstamped] - Also write each under its plain name
|
|
28
|
+
*/
|
|
29
|
+
constructor({ store, window: win = globalThis, download, keepUnstamped = true }) {
|
|
30
|
+
this.store = store;
|
|
31
|
+
this.win = win;
|
|
32
|
+
this.download = download;
|
|
33
|
+
this.keepUnstamped = keepUnstamped;
|
|
34
|
+
/** The chosen folder (a FileSystemDirectoryHandle), or null. */
|
|
35
|
+
this.dir = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Whether this browser lets a page write to a folder it chooses. */
|
|
39
|
+
get supported() {
|
|
40
|
+
return typeof this.win?.showDirectoryPicker === 'function';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Where backups go now, as precisely as a page can say (a folder's name, never its path). */
|
|
44
|
+
get location() {
|
|
45
|
+
return this.dir ? `folder "${this.dir.name}"` : BackupFolder.DOWNLOADS;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The folder chosen in an earlier session, if the store kept it. */
|
|
49
|
+
async restore() {
|
|
50
|
+
if (!this.supported) return null;
|
|
51
|
+
try {
|
|
52
|
+
const handle = await this.store.get(BackupFolder.HANDLE_KEY);
|
|
53
|
+
if (handle?.kind === 'directory') this.dir = handle;
|
|
54
|
+
} catch { /* a handle this browser will not read back: downloads still work */ }
|
|
55
|
+
return this.dir;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Ask for a folder (a user's click); resolves to it, or null when cancelled or unsupported. */
|
|
59
|
+
async choose() {
|
|
60
|
+
if (!this.supported) return null;
|
|
61
|
+
try {
|
|
62
|
+
this.dir = await this.win.showDirectoryPicker({ id: 'renderengine-backups', mode: 'readwrite' });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err?.name === 'AbortError') return null;
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
try { await this.store.set(BackupFolder.HANDLE_KEY, this.dir); } catch { /* chosen for this page only */ }
|
|
68
|
+
return this.dir;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Back up to downloads from now on. */
|
|
72
|
+
async forget() {
|
|
73
|
+
this.dir = null;
|
|
74
|
+
await this.store.delete(BackupFolder.HANDLE_KEY);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** `name.ext` stamped with the date and time to the second: `name-20261015-093012.ext`. */
|
|
78
|
+
static stamped(fileName, at = new Date()) {
|
|
79
|
+
const two = (n) => String(n).padStart(2, '0');
|
|
80
|
+
const stamp = `${at.getFullYear()}${two(at.getMonth() + 1)}${two(at.getDate())}-${two(at.getHours())}${two(at.getMinutes())}${two(at.getSeconds())}`;
|
|
81
|
+
const dot = fileName.lastIndexOf('.');
|
|
82
|
+
return dot > 0 ? `${fileName.slice(0, dot)}-${stamp}${fileName.slice(dot)}` : `${fileName}-${stamp}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Whether the folder may be written now (asking, which needs a user's click). @private */
|
|
86
|
+
async _writable() {
|
|
87
|
+
if (!this.dir) return false;
|
|
88
|
+
try {
|
|
89
|
+
const opts = { mode: 'readwrite' };
|
|
90
|
+
if (await this.dir.queryPermission?.(opts) === 'granted') return true;
|
|
91
|
+
return await this.dir.requestPermission?.(opts) === 'granted';
|
|
92
|
+
} catch { return false; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @private */
|
|
96
|
+
async _write(name, data) {
|
|
97
|
+
const file = await this.dir.getFileHandle(name, { create: true });
|
|
98
|
+
const writable = await file.createWritable();
|
|
99
|
+
await writable.write(data);
|
|
100
|
+
await writable.close();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Back up some files: into the folder (stamped, and plain), else to downloads
|
|
105
|
+
* (stamped). Resolves to where they went and their names; the log records it.
|
|
106
|
+
* @param {Array<{name: string, data: string|Uint8Array, mimeType?: string}>} files
|
|
107
|
+
* @param {Date} [at]
|
|
108
|
+
* @returns {Promise<{location: string, names: string[]}>}
|
|
109
|
+
*/
|
|
110
|
+
async backUp(files, at = new Date()) {
|
|
111
|
+
const names = files.map(f => BackupFolder.stamped(f.name, at));
|
|
112
|
+
let location = BackupFolder.DOWNLOADS;
|
|
113
|
+
if (this.dir && await this._writable()) {
|
|
114
|
+
try {
|
|
115
|
+
for (const [i, f] of files.entries()) {
|
|
116
|
+
await this._write(names[i], f.data);
|
|
117
|
+
if (this.keepUnstamped) { try { await this._write(f.name, f.data); } catch { /* the stamped one is written */ } }
|
|
118
|
+
}
|
|
119
|
+
location = this.location;
|
|
120
|
+
} catch { location = BackupFolder.DOWNLOADS; }
|
|
121
|
+
}
|
|
122
|
+
if (location === BackupFolder.DOWNLOADS) {
|
|
123
|
+
files.forEach((f, i) => this.download(names[i], f.data, f.mimeType ?? 'application/octet-stream'));
|
|
124
|
+
}
|
|
125
|
+
await this._log({ at: at.toISOString(), location, names, bytes: files.reduce((n, f) => n + (f.data?.length ?? 0), 0) });
|
|
126
|
+
return { location, names };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The backups taken, newest first. */
|
|
130
|
+
async log() {
|
|
131
|
+
return (await this.store.get(BackupFolder.LOG_KEY)) ?? [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Days since the last backup, or null when there has been none. */
|
|
135
|
+
async daysSinceLast(now = new Date()) {
|
|
136
|
+
const [last] = await this.log();
|
|
137
|
+
return last ? (now - new Date(last.at)) / 86400000 : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @private */
|
|
141
|
+
async _log(record) {
|
|
142
|
+
const log = [record, ...(await this.log())].slice(0, BackupFolder.LOG_MAX);
|
|
143
|
+
await this.store.set(BackupFolder.LOG_KEY, log);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KeyValueStore — values kept by key, asynchronously, that outlive the page:
|
|
3
|
+
* the base of where the workbench keeps models and remembers its backup
|
|
4
|
+
* folder. `IndexedDbStore` keeps them in the browser (any structured-clone
|
|
5
|
+
* value: text, bytes, a folder handle); `MemoryStore` for tests and for a
|
|
6
|
+
* browser that has no storage (a private window), where nothing outlives
|
|
7
|
+
* the page.
|
|
8
|
+
*/
|
|
9
|
+
export class KeyValueStore {
|
|
10
|
+
constructor() {
|
|
11
|
+
if (new.target === KeyValueStore) throw new TypeError('KeyValueStore is abstract — subclass it');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The value under `key`, or undefined. @abstract */
|
|
15
|
+
async get(key) { throw new Error(`${this.constructor.name} must implement get()`); }
|
|
16
|
+
|
|
17
|
+
/** Keep `value` under `key`. @abstract */
|
|
18
|
+
async set(key, value) { throw new Error(`${this.constructor.name} must implement set()`); }
|
|
19
|
+
|
|
20
|
+
/** Forget `key`. @abstract */
|
|
21
|
+
async delete(key) { throw new Error(`${this.constructor.name} must implement delete()`); }
|
|
22
|
+
|
|
23
|
+
/** Every key (that starts with `prefix`). @abstract */
|
|
24
|
+
async keys(prefix = '') { throw new Error(`${this.constructor.name} must implement keys()`); }
|
|
25
|
+
|
|
26
|
+
/** Whether values outlive the page. */
|
|
27
|
+
get lasting() { return true; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Values in memory only: gone with the page. */
|
|
31
|
+
export class MemoryStore extends KeyValueStore {
|
|
32
|
+
constructor() {
|
|
33
|
+
super();
|
|
34
|
+
this.values = new Map();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async get(key) { return this.values.get(key); }
|
|
38
|
+
async set(key, value) { this.values.set(key, value); }
|
|
39
|
+
async delete(key) { this.values.delete(key); }
|
|
40
|
+
async keys(prefix = '') { return [...this.values.keys()].filter(k => k.startsWith(prefix)); }
|
|
41
|
+
get lasting() { return false; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Values in the browser's IndexedDB: one database, one object store, keyed by
|
|
46
|
+
* string. Far larger than localStorage allows, and it keeps bytes and file
|
|
47
|
+
* handles as they are. `IndexedDbStore.open(name)` falls back to a MemoryStore
|
|
48
|
+
* where the browser has no IndexedDB (or refuses it).
|
|
49
|
+
*/
|
|
50
|
+
export class IndexedDbStore extends KeyValueStore {
|
|
51
|
+
/** @param {IDBDatabase} db @param {string} storeName */
|
|
52
|
+
constructor(db, storeName) {
|
|
53
|
+
super();
|
|
54
|
+
this.db = db;
|
|
55
|
+
this.storeName = storeName;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Open (making it the first time) a database of one object store.
|
|
60
|
+
* @param {string} name
|
|
61
|
+
* @param {{indexedDB?: IDBFactory, storeName?: string}} [options]
|
|
62
|
+
* @returns {Promise<KeyValueStore>} an IndexedDbStore, else a MemoryStore
|
|
63
|
+
*/
|
|
64
|
+
static open(name, { indexedDB = globalThis.indexedDB, storeName = 'values' } = {}) {
|
|
65
|
+
if (!indexedDB) return Promise.resolve(new MemoryStore());
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
let req;
|
|
68
|
+
try { req = indexedDB.open(name, 1); } catch { resolve(new MemoryStore()); return; }
|
|
69
|
+
req.onupgradeneeded = () => { if (!req.result.objectStoreNames.contains(storeName)) req.result.createObjectStore(storeName); };
|
|
70
|
+
req.onsuccess = () => resolve(new IndexedDbStore(req.result, storeName));
|
|
71
|
+
req.onerror = () => resolve(new MemoryStore());
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Run one request in a transaction; resolves to its result. @private */
|
|
76
|
+
_request(mode, make) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const tx = this.db.transaction(this.storeName, mode);
|
|
79
|
+
const req = make(tx.objectStore(this.storeName));
|
|
80
|
+
req.onsuccess = () => resolve(req.result);
|
|
81
|
+
req.onerror = () => reject(req.error);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get(key) { return this._request('readonly', s => s.get(key)); }
|
|
86
|
+
async set(key, value) { await this._request('readwrite', s => s.put(value, key)); }
|
|
87
|
+
async delete(key) { await this._request('readwrite', s => s.delete(key)); }
|
|
88
|
+
async keys(prefix = '') {
|
|
89
|
+
const all = await this._request('readonly', s => s.getAllKeys());
|
|
90
|
+
return all.map(String).filter(k => k.startsWith(prefix));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModelLibrary — the models kept in this browser (a KeyValueStore), so work
|
|
3
|
+
* outlives the page without saving a file: each in its editor's own file
|
|
4
|
+
* format (`data`: text or bytes, as the format exports it), with its name,
|
|
5
|
+
* when it was kept, the file it was last opened from or saved to, the kind of
|
|
6
|
+
* editor that kept it (`editorType`: pages of different editors share the
|
|
7
|
+
* browser's store, and each reads only its own), and whether it was open when
|
|
8
|
+
* the page closed (`open`), to reopen it.
|
|
9
|
+
*/
|
|
10
|
+
export class ModelLibrary {
|
|
11
|
+
/** The key prefix of a kept model. */
|
|
12
|
+
static PREFIX = 'model:';
|
|
13
|
+
|
|
14
|
+
/** @param {KeyValueStore} store */
|
|
15
|
+
constructor(store) {
|
|
16
|
+
this.store = store;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A new id for a model to keep. */
|
|
20
|
+
static newId() {
|
|
21
|
+
return `m${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Keep a model (an entry with the same id is replaced).
|
|
26
|
+
* @param {{id: string, name: string, format: string, data: string|Uint8Array, fileName?: string|null, editorType?: string, open?: boolean}} entry
|
|
27
|
+
* `format`: its ModelFileFormat's label; `editorType`: the editor part type that kept it
|
|
28
|
+
*/
|
|
29
|
+
async keep(entry) {
|
|
30
|
+
if (!entry?.id) throw new TypeError('A kept model needs an id');
|
|
31
|
+
const kept = { open: true, fileName: null, ...entry, keptAt: new Date().toISOString() };
|
|
32
|
+
await this.store.set(ModelLibrary.PREFIX + entry.id, kept);
|
|
33
|
+
return kept;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A kept model, data and all, or null. */
|
|
37
|
+
async load(id) {
|
|
38
|
+
return (await this.store.get(ModelLibrary.PREFIX + id)) ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What is kept, newest first: each entry without its data (`size` instead). */
|
|
42
|
+
async list() {
|
|
43
|
+
const keys = await this.store.keys(ModelLibrary.PREFIX);
|
|
44
|
+
const entries = (await Promise.all(keys.map(k => this.store.get(k)))).filter(Boolean);
|
|
45
|
+
return entries
|
|
46
|
+
.map(({ data, ...meta }) => ({ ...meta, size: data?.length ?? 0 }))
|
|
47
|
+
.sort((a, b) => String(b.keptAt).localeCompare(String(a.keptAt)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The ones open when the page last closed, oldest first (their tab order). */
|
|
51
|
+
async openOnes() {
|
|
52
|
+
return (await this.list()).filter(e => e.open).reverse();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Mark a kept model open or not (its tab closed: kept, not reopened next time). */
|
|
56
|
+
async setOpen(id, open) {
|
|
57
|
+
const entry = await this.load(id);
|
|
58
|
+
if (entry) await this.store.set(ModelLibrary.PREFIX + id, { ...entry, open: !!open });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Forget a kept model. */
|
|
62
|
+
async remove(id) {
|
|
63
|
+
await this.store.delete(ModelLibrary.PREFIX + id);
|
|
64
|
+
}
|
|
65
|
+
}
|