@peter.prib/renderengine 0.15.1 → 0.15.3
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/dist/renderengine-workspace.umd.js +6485 -3992
- package/dist/renderengine.umd.js +6251 -3877
- package/documentation/REFERENCE.md +384 -28
- package/package.json +1 -1
- package/src/archimateArchiFormat.js +114 -10
- package/src/archimateArchive.js +27 -4
- package/src/archimateExchangeFormat.js +12 -4
- package/src/archimateSpecializations.js +11 -1
- package/src/chartCatalog.js +6 -4
- package/src/charting/ArchimateDiagramEditor.js +7 -109
- package/src/charting/DiagramTypeRegistry.js +20 -1
- package/src/charting/DiagramViewPropertiesDialog.js +54 -10
- package/src/charting/Edge.js +22 -0
- package/src/charting/Legend.js +1 -2
- package/src/charting/Statistics.js +14 -0
- package/src/charting/archimate/ArchimateCanvasEditor.js +21 -5
- package/src/charting/archimate/ViewGroupTemplate.js +5 -2
- package/src/charting/charts/BarChart.js +59 -8
- package/src/charting/charts/HexbinChart.js +15 -0
- package/src/charting/charts/LollipopChart.js +42 -7
- package/src/charting/decision/DecisionWorkbench.js +71 -4
- package/src/charting/diagramModel/DiagramJsonFileFormat.js +32 -0
- package/src/charting/diagramModel/DiagramModel.js +39 -0
- package/src/charting/diagramModel/DiagramView.js +102 -61
- package/src/charting/diagramModel/index.js +11 -10
- package/src/charting/diagrams/AddGroupToViewCommand.js +35 -0
- package/src/charting/diagrams/ArchimateModel.js +53 -13
- package/src/charting/diagrams/GraphDiagram3D.js +15 -0
- package/src/charting/diagrams/NestInstanceCommand.js +2 -1
- package/src/charting/graph/GraphSource.js +72 -0
- package/src/charting/modelTree/NavigatorModel.js +22 -8
- package/src/charting/report/TemplatedReport.js +27 -377
- package/src/charting/report/ViewsReport.js +118 -0
- package/src/charting/report/reportWriters.js +496 -0
- package/src/charting/templates/C4BoundaryTemplate.js +34 -0
- package/src/charting/templates/C4ComponentTemplate.js +30 -0
- package/src/charting/templates/C4ContainerTemplate.js +49 -0
- package/src/charting/templates/C4PersonTemplate.js +35 -0
- package/src/charting/templates/C4SystemTemplate.js +32 -0
- package/src/charting/templates/C4Template.js +95 -0
- package/src/charting/templates/index.js +6 -0
- package/src/charting/templates/templateRegistry.js +77 -0
- package/src/compute/ComputeBackend.js +132 -0
- package/src/compute/CpuBackend.js +209 -0
- package/src/compute/WebGpuBackend.js +630 -0
- package/src/compute/index.js +5 -0
- package/src/compute/selectBackend.js +85 -0
- package/src/index.js +3403 -3370
- package/src/kernelExpr.js +80 -5
- package/src/workspace/parts/ArchimateEditorPart.js +2 -2
- package/src/workspace/parts/DiagramEditorPart.js +146 -131
- package/src/workspace/parts/ViewsPart.js +106 -0
package/src/kernelExpr.js
CHANGED
|
@@ -194,13 +194,25 @@ function _parsePrimary(s) {
|
|
|
194
194
|
throw new Error(`parseKernelExpr: unexpected '${t.value}' at position ${t.pos}`);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
// ── JavaScript emission
|
|
197
|
+
// ── JavaScript and WGSL emission ─────────────────────────────────────────────
|
|
198
198
|
|
|
199
|
-
function
|
|
199
|
+
function _wgslNumber(v) {
|
|
200
|
+
if (!Number.isFinite(v)) return '0.0';
|
|
201
|
+
const str = `${v}`;
|
|
202
|
+
return str.includes('.') || str.includes('e') || str.includes('E') ? str : `${str}.0`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Emit the AST as a JavaScript scalar expression.
|
|
207
|
+
* @param {object} node
|
|
208
|
+
* @param {(name: string) => string} [ref]
|
|
209
|
+
* @returns {string}
|
|
210
|
+
*/
|
|
211
|
+
export function emitJs(node, ref = (name) => name) {
|
|
200
212
|
const go = (n) => {
|
|
201
213
|
switch (n.kind) {
|
|
202
214
|
case 'literal': return `${n.value}`;
|
|
203
|
-
case 'param': return n.name;
|
|
215
|
+
case 'param': return ref(n.name);
|
|
204
216
|
case 'neg': return `(-${go(n.operand)})`;
|
|
205
217
|
case 'not': return `(${go(n.operand)} === 0 ? 1 : 0)`;
|
|
206
218
|
case 'cond': return `((${go(n.cond)}) !== 0 ? ${go(n.then)} : ${go(n.else)})`;
|
|
@@ -216,12 +228,75 @@ function _emitJs(node) {
|
|
|
216
228
|
const args = n.args.map(go).join(', ');
|
|
217
229
|
return `${FN_NAMES[n.name]}(${args})`;
|
|
218
230
|
}
|
|
219
|
-
default: throw new Error(`
|
|
231
|
+
default: throw new Error(`emitJs: unknown node '${n.kind}'`);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
return go(node);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Emit the AST as a WGSL f32 scalar expression.
|
|
239
|
+
* Comparisons and booleans evaluate to 1.0 or 0.0 via select().
|
|
240
|
+
* @param {object} node
|
|
241
|
+
* @param {(name: string) => string} [ref]
|
|
242
|
+
* @returns {string}
|
|
243
|
+
*/
|
|
244
|
+
export function emitWgsl(node, ref = (name) => name) {
|
|
245
|
+
const go = (n) => {
|
|
246
|
+
switch (n.kind) {
|
|
247
|
+
case 'literal': return _wgslNumber(n.value);
|
|
248
|
+
case 'param': return ref(n.name);
|
|
249
|
+
case 'neg': return `(-${go(n.operand)})`;
|
|
250
|
+
case 'not': return `select(0.0, 1.0, ${go(n.operand)} == 0.0)`;
|
|
251
|
+
case 'cond': return `select(${go(n.else)}, ${go(n.then)}, (${go(n.cond)}) != 0.0)`;
|
|
252
|
+
case 'binary': {
|
|
253
|
+
const l = go(n.left), r = go(n.right);
|
|
254
|
+
if (n.op === '&&')
|
|
255
|
+
return `select(0.0, 1.0, ((${l}) != 0.0) && ((${r}) != 0.0))`;
|
|
256
|
+
if (n.op === '||')
|
|
257
|
+
return `select(0.0, 1.0, ((${l}) != 0.0) || ((${r}) != 0.0))`;
|
|
258
|
+
if (['<', '<=', '>', '>=', '==', '!='].includes(n.op))
|
|
259
|
+
return `select(0.0, 1.0, ${l} ${n.op} ${r})`;
|
|
260
|
+
if (n.op === '%')
|
|
261
|
+
return `(${l} - ${r} * trunc(${l} / ${r}))`;
|
|
262
|
+
return `(${l} ${n.op} ${r})`;
|
|
263
|
+
}
|
|
264
|
+
case 'call': {
|
|
265
|
+
const args = n.args.map(go).join(', ');
|
|
266
|
+
return `${n.name}(${args})`;
|
|
267
|
+
}
|
|
268
|
+
default: throw new Error(`emitWgsl: unknown node '${n.kind}'`);
|
|
220
269
|
}
|
|
221
270
|
};
|
|
222
271
|
return go(node);
|
|
223
272
|
}
|
|
224
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Generate a complete WGSL compute shader executing the kernel across arrays.
|
|
276
|
+
* @param {object} node
|
|
277
|
+
* @param {string[]} params
|
|
278
|
+
* @param {object} [opts]
|
|
279
|
+
* @param {number} [opts.workgroupSize=64]
|
|
280
|
+
* @returns {string}
|
|
281
|
+
*/
|
|
282
|
+
export function wgslKernelModule(node, params, { workgroupSize = 64 } = {}) {
|
|
283
|
+
const bindings = params.map((p, i) =>
|
|
284
|
+
`@group(0) @binding(${i}) var<storage, read> ${p}_in : array<f32>;`).join('\n');
|
|
285
|
+
const outBinding = params.length;
|
|
286
|
+
|
|
287
|
+
return `${bindings}
|
|
288
|
+
@group(0) @binding(${outBinding}) var<storage, read_write> out_buf : array<f32>;
|
|
289
|
+
@group(0) @binding(${outBinding + 1}) var<uniform> count : vec4<u32>;
|
|
290
|
+
|
|
291
|
+
@compute @workgroup_size(${workgroupSize})
|
|
292
|
+
fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
|
|
293
|
+
let i = gid.x;
|
|
294
|
+
if (i >= count.x) { return; }
|
|
295
|
+
out_buf[i] = ${emitWgsl(node, (name) => `${name}_in[i]`)};
|
|
296
|
+
}
|
|
297
|
+
`;
|
|
298
|
+
}
|
|
299
|
+
|
|
225
300
|
/**
|
|
226
301
|
* Compile a formula to a callable JavaScript function — `fn(...paramValues)
|
|
227
302
|
* -> number`. Built with `new Function` from an expression THIS module
|
|
@@ -236,7 +311,7 @@ function _emitJs(node) {
|
|
|
236
311
|
export function compileFormula(source, params) {
|
|
237
312
|
const { ast } = parseKernelExpr(source, params);
|
|
238
313
|
const __clamp = (x, lo, hi) => Math.min(Math.max(x, lo), hi);
|
|
239
|
-
const body = `return ${
|
|
314
|
+
const body = `return ${emitJs(ast)};`;
|
|
240
315
|
// eslint-disable-next-line no-new-func
|
|
241
316
|
const fn = new Function('__clamp', ...params, body);
|
|
242
317
|
return (...values) => fn(__clamp, ...values);
|
|
@@ -11,8 +11,8 @@ const DEFAULT_FEATURES = Object.freeze({
|
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* ArchimateEditorPart —
|
|
15
|
-
*
|
|
14
|
+
* ArchimateEditorPart — legacy GraphDiagram-based ArchiMate editor part.
|
|
15
|
+
* @deprecated Use ArchimateCanvasPart for ArchiMate editing.
|
|
16
16
|
*
|
|
17
17
|
* Spec (JSON): `elements`, `relationships`, `views` (as ArchimateModel takes
|
|
18
18
|
* them), `features` (toolbar controls, see DEFAULT_FEATURES), `analysis`
|
|
@@ -1,131 +1,146 @@
|
|
|
1
|
-
import { render } from '../dom.js';
|
|
2
|
-
import { EditorShell } from '../EditorShell.js';
|
|
3
|
-
import { EditorPart } from './EditorPart.js';
|
|
4
|
-
import { DiagramEditor } from '../../charting/DiagramEditor.js';
|
|
5
|
-
import { MenuBar } from '../../charting/MenuBar.js';
|
|
6
|
-
import { DiagramModel } from '../../charting/diagramModel/index.js';
|
|
7
|
-
import { LocalJsonStore } from '../../charting/storage/LocalJsonStore.js';
|
|
8
|
-
import { EdgeStyleCascade } from '../../charting/edgeStyle/index.js';
|
|
9
|
-
import { DiagramViewPropertiesDialog } from '../../charting/DiagramViewPropertiesDialog.js';
|
|
10
|
-
|
|
11
|
-
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* DiagramEditorPart — a complete diagram editor as a workspace part: menu bar,
|
|
15
|
-
* palette, one tab per diagram and the canvas, over a DiagramModel.
|
|
16
|
-
*
|
|
17
|
-
* Spec (JSON): `templateType` (palette category), `tabOrientation`
|
|
18
|
-
* ('horizontal'|'vertical'), `diagramTitle` (first diagram), `toolStyleKey`
|
|
19
|
-
* (localStorage key for the tool-level line style; `null` keeps it in memory).
|
|
20
|
-
*
|
|
21
|
-
* Line styles cascade line → diagram (`view.edgeStyle`) → model
|
|
22
|
-
* (`model.edgeStyle`) → tool. Each diagram keeps its own undo history, so undo
|
|
23
|
-
* survives switching tabs. The diagrams' menu (new, duplicate, rename, delete)
|
|
24
|
-
* comes from EditorPart. Like ChartPart it needs the engine, so it is not in
|
|
25
|
-
* BUILTIN_PARTS: register it explicitly.
|
|
26
|
-
*/
|
|
27
|
-
export class DiagramEditorPart extends EditorPart {
|
|
28
|
-
static type = 'diagram-editor';
|
|
29
|
-
static viewNoun = 'diagram';
|
|
30
|
-
|
|
31
|
-
constructor(spec = {}) {
|
|
32
|
-
super(spec);
|
|
33
|
-
this.tabOrientation = spec.tabOrientation ?? 'horizontal';
|
|
34
|
-
// The editor is rebuilt with the canvas on every render, which follows every model change.
|
|
35
|
-
this._unsubscribe = this.model.onChange(() => { if (this.mounted) this.render(); });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
createModel(spec) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
1
|
+
import { render } from '../dom.js';
|
|
2
|
+
import { EditorShell } from '../EditorShell.js';
|
|
3
|
+
import { EditorPart } from './EditorPart.js';
|
|
4
|
+
import { DiagramEditor } from '../../charting/DiagramEditor.js';
|
|
5
|
+
import { MenuBar } from '../../charting/MenuBar.js';
|
|
6
|
+
import { DiagramModel, DiagramJsonFileFormat } from '../../charting/diagramModel/index.js';
|
|
7
|
+
import { LocalJsonStore } from '../../charting/storage/LocalJsonStore.js';
|
|
8
|
+
import { EdgeStyleCascade } from '../../charting/edgeStyle/index.js';
|
|
9
|
+
import { DiagramViewPropertiesDialog } from '../../charting/DiagramViewPropertiesDialog.js';
|
|
10
|
+
|
|
11
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* DiagramEditorPart — a complete diagram editor as a workspace part: menu bar,
|
|
15
|
+
* palette, one tab per diagram and the canvas, over a DiagramModel.
|
|
16
|
+
*
|
|
17
|
+
* Spec (JSON): `templateType` (palette category), `tabOrientation`
|
|
18
|
+
* ('horizontal'|'vertical'), `diagramTitle` (first diagram), `toolStyleKey`
|
|
19
|
+
* (localStorage key for the tool-level line style; `null` keeps it in memory).
|
|
20
|
+
*
|
|
21
|
+
* Line styles cascade line → diagram (`view.edgeStyle`) → model
|
|
22
|
+
* (`model.edgeStyle`) → tool. Each diagram keeps its own undo history, so undo
|
|
23
|
+
* survives switching tabs. The diagrams' menu (new, duplicate, rename, delete)
|
|
24
|
+
* comes from EditorPart. Like ChartPart it needs the engine, so it is not in
|
|
25
|
+
* BUILTIN_PARTS: register it explicitly.
|
|
26
|
+
*/
|
|
27
|
+
export class DiagramEditorPart extends EditorPart {
|
|
28
|
+
static type = 'diagram-editor';
|
|
29
|
+
static viewNoun = 'diagram';
|
|
30
|
+
|
|
31
|
+
constructor(spec = {}) {
|
|
32
|
+
super(spec);
|
|
33
|
+
this.tabOrientation = spec.tabOrientation ?? 'horizontal';
|
|
34
|
+
// The editor is rebuilt with the canvas on every render, which follows every model change.
|
|
35
|
+
this._unsubscribe = this.model.onChange(() => { if (this.mounted) this.render(); });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
createModel(spec) {
|
|
39
|
+
if (spec.model instanceof DiagramModel) return spec.model;
|
|
40
|
+
const store = spec.toolStyleKey === null ? null
|
|
41
|
+
: new LocalJsonStore(spec.toolStyleKey ?? 'renderengine.diagramEditor.toolEdgeStyle');
|
|
42
|
+
this.toolEdgeStyle = store?.load({}) ?? {};
|
|
43
|
+
const historyOptions = { onChange: () => store?.save(this.toolEdgeStyle) };
|
|
44
|
+
if (spec.views) {
|
|
45
|
+
return DiagramModel.fromJSON(spec, historyOptions);
|
|
46
|
+
}
|
|
47
|
+
const model = new DiagramModel({
|
|
48
|
+
name: spec.name ?? '',
|
|
49
|
+
templateType: spec.templateType ?? 'generic',
|
|
50
|
+
// Tool-level style edits go through the diagram's undo history; persist whenever it moves.
|
|
51
|
+
historyOptions,
|
|
52
|
+
});
|
|
53
|
+
model.createView(spec.diagramTitle ?? 'Diagram 1');
|
|
54
|
+
return model;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fileFormats() {
|
|
58
|
+
return this.spec.fileFormats ? [new DiagramJsonFileFormat()] : [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
saveFormat() {
|
|
62
|
+
return this.fileFormats()[0];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
render() {
|
|
66
|
+
const model = this.model;
|
|
67
|
+
const view = model.currentView;
|
|
68
|
+
const doc = this.document;
|
|
69
|
+
const canvasId = `diagram-canvas-${this.id}`;
|
|
70
|
+
|
|
71
|
+
const canvas = doc.createElementNS(SVG_NS, 'svg');
|
|
72
|
+
canvas.id = canvasId;
|
|
73
|
+
canvas.setAttribute('viewBox', '0 0 800 600');
|
|
74
|
+
canvas.style.cssText = 'flex: 1; width: 100%; height: 100%; display: block; background: #fff; border: 2px solid #333; box-sizing: border-box;';
|
|
75
|
+
const elements = doc.createElementNS(SVG_NS, 'g');
|
|
76
|
+
elements.id = 'elements';
|
|
77
|
+
canvas.appendChild(elements);
|
|
78
|
+
|
|
79
|
+
const tabs = this.viewTabs({ orientation: this.tabOrientation });
|
|
80
|
+
|
|
81
|
+
// The shared workbench layout: toolbar, palette at the side, tabs + canvas in the main area.
|
|
82
|
+
const { toolbar: menuBarEl, side: palette, main } = new EditorShell({ document: doc, sideWidth: '200px', layout: this.layout, labels: { side: 'Palette' } }).mount(this.container);
|
|
83
|
+
palette.classList.add('ws-diagram-palette');
|
|
84
|
+
Object.assign(palette.style, { padding: '8px', fontSize: '12px', display: 'flex', flexDirection: 'column', overflowY: 'hidden' });
|
|
85
|
+
main.style.flexDirection = this.tabOrientation === 'vertical' ? 'row' : 'column';
|
|
86
|
+
render(main, [tabs, { style: { flex: '1', display: 'flex', flexDirection: 'column', minHeight: '0' }, children: [canvas] }], doc);
|
|
87
|
+
|
|
88
|
+
// The editor is bound to the view's own maps and history: nothing to save back on switch.
|
|
89
|
+
const previous = this.editor;
|
|
90
|
+
previous?.dispose();
|
|
91
|
+
this.editor = new DiagramEditor({
|
|
92
|
+
diagramContainerId: canvasId,
|
|
93
|
+
model,
|
|
94
|
+
templateType: model.templateType,
|
|
95
|
+
onTemplateTypeChange: (type) => model.setTemplateType(type),
|
|
96
|
+
templates: view.templates,
|
|
97
|
+
edges: view.edges,
|
|
98
|
+
history: view.history,
|
|
99
|
+
arrangement: view.arrangement,
|
|
100
|
+
edgeStyleLevels: this.edgeStyleLevels(view),
|
|
101
|
+
viewSettings: () => view.viewSettings ?? {},
|
|
102
|
+
});
|
|
103
|
+
if (previous) {
|
|
104
|
+
this.editor.showGrid = previous.showGrid;
|
|
105
|
+
this.editor.gridSize = previous.gridSize;
|
|
106
|
+
this.editor.showExportArea = previous.showExportArea;
|
|
107
|
+
this.editor.exportOptions = previous.exportOptions;
|
|
108
|
+
}
|
|
109
|
+
this.editor.initPaletteForDiagramType(palette, model.templateType);
|
|
110
|
+
this.menuBar?.dispose();
|
|
111
|
+
this.menuBar = new MenuBar({
|
|
112
|
+
container: menuBarEl,
|
|
113
|
+
editor: this.editor,
|
|
114
|
+
items: [this.fileMenu(), this.editMenu(), ...this.hostMenus(), ...this.editor.menuBarItems()],
|
|
115
|
+
}).render();
|
|
116
|
+
this.editor.render();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A diagram's line-style levels, most specific first: the diagram, the model, the tool. */
|
|
120
|
+
edgeStyleLevels(view) {
|
|
121
|
+
return [
|
|
122
|
+
{ key: 'view', label: 'Diagram', style: view.edgeStyle },
|
|
123
|
+
{ key: 'model', label: 'Model', style: this.model.edgeStyle },
|
|
124
|
+
{ key: 'tool', label: 'Tool', style: this.toolEdgeStyle },
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A diagram's properties: name, documentation and its line-style defaults. */
|
|
129
|
+
showViewProperties(view) {
|
|
130
|
+
return new DiagramViewPropertiesDialog({
|
|
131
|
+
model: this.model,
|
|
132
|
+
view,
|
|
133
|
+
document: this.document,
|
|
134
|
+
cascade: new EdgeStyleCascade(this.edgeStyleLevels(view)),
|
|
135
|
+
// The editor shows the current diagram; a style or appearance change there needs a redraw.
|
|
136
|
+
onStyleChange: () => { if (view === this.model.currentView) this.editor?.render(); },
|
|
137
|
+
onSettingsChange: () => { if (view === this.model.currentView) this.editor?.render(); },
|
|
138
|
+
}).open();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
dispose() {
|
|
142
|
+
this.menuBar?.dispose();
|
|
143
|
+
this._unsubscribe();
|
|
144
|
+
super.dispose();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -26,6 +26,8 @@ import { el } from '../dom.js';
|
|
|
26
26
|
import { ChartPart } from './ChartPart.js';
|
|
27
27
|
import { ListPart } from './ListPart.js';
|
|
28
28
|
import { CHART_CATALOG } from '../../chartCatalog.js';
|
|
29
|
+
import { ViewsReport } from '../../charting/report/ViewsReport.js';
|
|
30
|
+
import { downloadBlob } from '../../exportImage.js';
|
|
29
31
|
|
|
30
32
|
/** Chart categories drawn as small tiles across the top of a view rather than as full charts. */
|
|
31
33
|
const TILE_CATEGORIES = new Set(['gauge']);
|
|
@@ -40,6 +42,7 @@ export class ViewsPart extends Part {
|
|
|
40
42
|
super(spec);
|
|
41
43
|
this.views = spec.views ?? [];
|
|
42
44
|
this.allowOpen = spec.open !== false;
|
|
45
|
+
this.allowExport = spec.export !== false;
|
|
43
46
|
this.empty = spec.empty ?? 'Open a file to show its views.';
|
|
44
47
|
this.active = this.views[0]?.id ?? null;
|
|
45
48
|
/** The chart and notes parts made for the views shown, torn down when they change. @private */
|
|
@@ -116,6 +119,27 @@ export class ViewsPart extends Part {
|
|
|
116
119
|
{ tag: 'input', attrs: { type: 'file', accept: this.constructor.accept }, on: { change: (e) => this.openFile(e.target) } },
|
|
117
120
|
]},
|
|
118
121
|
...this.controls(),
|
|
122
|
+
...(this.allowExport ? [{
|
|
123
|
+
tag: 'span',
|
|
124
|
+
cls: 'ws-views-export-group',
|
|
125
|
+
style: { display: 'inline-flex', alignItems: 'center', gap: '4px' },
|
|
126
|
+
children: [
|
|
127
|
+
{ tag: 'button', attrs: { type: 'button' }, cls: 'ws-views-export-btn', text: 'Export report', on: { click: () => this.exportReport(this._exportFormatSelect?.value || 'html') } },
|
|
128
|
+
{
|
|
129
|
+
tag: 'select',
|
|
130
|
+
cls: 'ws-views-export-format',
|
|
131
|
+
style: { padding: '2px 4px', fontSize: '12px' },
|
|
132
|
+
ref: (s) => { this._exportFormatSelect = s; },
|
|
133
|
+
children: [
|
|
134
|
+
{ tag: 'option', attrs: { value: 'html' }, text: 'HTML' },
|
|
135
|
+
{ tag: 'option', attrs: { value: 'word' }, text: 'Word' },
|
|
136
|
+
{ tag: 'option', attrs: { value: 'excel' }, text: 'Excel' },
|
|
137
|
+
{ tag: 'option', attrs: { value: 'markdown' }, text: 'Markdown' },
|
|
138
|
+
{ tag: 'option', attrs: { value: 'csv' }, text: 'CSV' },
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
}] : []),
|
|
119
143
|
{ tag: 'span', cls: 'ws-views-message', style: { opacity: '0.75' }, ref: (n) => { this._message = n; } },
|
|
120
144
|
],
|
|
121
145
|
}));
|
|
@@ -193,4 +217,86 @@ export class ViewsPart extends Part {
|
|
|
193
217
|
options: { ...chart.options, title: chart.title, data: chart.data },
|
|
194
218
|
};
|
|
195
219
|
}
|
|
220
|
+
|
|
221
|
+
/** Collect SVG markup of currently mounted/drawn charts. */
|
|
222
|
+
collectPictures() {
|
|
223
|
+
const pictures = {};
|
|
224
|
+
const activeView = this.views.find((v) => v.id === this.active);
|
|
225
|
+
if (!activeView) return pictures;
|
|
226
|
+
for (let i = 0; i < this._children.length; i++) {
|
|
227
|
+
const child = this._children[i];
|
|
228
|
+
if (child instanceof ChartPart) {
|
|
229
|
+
const svg = child.canvasHost?.querySelector?.('svg')?.outerHTML
|
|
230
|
+
|| child.engine?.renderer?.svg?.outerHTML
|
|
231
|
+
|| '';
|
|
232
|
+
if (svg) {
|
|
233
|
+
pictures[`${activeView.id}:${i}`] = svg;
|
|
234
|
+
const chart = activeView.charts?.[i];
|
|
235
|
+
if (chart?.title) pictures[chart.title] = svg;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return pictures;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Create a ViewsReport for this part's views. */
|
|
243
|
+
createReport(options = {}) {
|
|
244
|
+
const pictures = { ...this.collectPictures(), ...(options.pictures || {}) };
|
|
245
|
+
const title = options.title || this.spec.title || (typeof this.document === 'object' ? (this.document.title || this.document.name || this.document.report?.config) : null) || 'Views Report';
|
|
246
|
+
return new ViewsReport(this.views, { title, pictures, ...options });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Export report to HTML, Word, Excel, Markdown or CSV, and trigger download when in DOM. */
|
|
250
|
+
exportReport(format = 'html', options = {}) {
|
|
251
|
+
const report = this.createReport(options);
|
|
252
|
+
const baseName = (report.title || 'views-report').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'report';
|
|
253
|
+
let data = '';
|
|
254
|
+
let mimeType = 'text/plain';
|
|
255
|
+
let fileName = `${baseName}.txt`;
|
|
256
|
+
|
|
257
|
+
switch (format.toLowerCase()) {
|
|
258
|
+
case 'word':
|
|
259
|
+
case 'doc':
|
|
260
|
+
case 'docx':
|
|
261
|
+
data = report.toWord();
|
|
262
|
+
mimeType = 'application/msword';
|
|
263
|
+
fileName = `${baseName}.doc`;
|
|
264
|
+
break;
|
|
265
|
+
case 'excel':
|
|
266
|
+
case 'xls':
|
|
267
|
+
case 'xlsx':
|
|
268
|
+
data = report.toExcel();
|
|
269
|
+
mimeType = 'application/vnd.ms-excel';
|
|
270
|
+
fileName = `${baseName}.xml`;
|
|
271
|
+
break;
|
|
272
|
+
case 'csv':
|
|
273
|
+
data = report.toCsv(options.table || 'views');
|
|
274
|
+
mimeType = 'text/csv';
|
|
275
|
+
fileName = `${baseName}-${options.table || 'views'}.csv`;
|
|
276
|
+
break;
|
|
277
|
+
case 'markdown':
|
|
278
|
+
case 'md':
|
|
279
|
+
data = report.toMarkdown();
|
|
280
|
+
mimeType = 'text/markdown';
|
|
281
|
+
fileName = `${baseName}.md`;
|
|
282
|
+
break;
|
|
283
|
+
case 'html':
|
|
284
|
+
default:
|
|
285
|
+
data = report.toHtml(options);
|
|
286
|
+
mimeType = 'text/html';
|
|
287
|
+
fileName = `${baseName}.html`;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const doc = this.container?.ownerDocument ?? globalThis.document;
|
|
292
|
+
const BlobClass = doc?.defaultView?.Blob ?? globalThis.Blob;
|
|
293
|
+
if (BlobClass && typeof downloadBlob === 'function') {
|
|
294
|
+
try {
|
|
295
|
+
downloadBlob(new BlobClass([data], { type: mimeType }), fileName);
|
|
296
|
+
} catch {
|
|
297
|
+
// Ignore download failure in headless / mock test environments
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return { data, mimeType, fileName };
|
|
301
|
+
}
|
|
196
302
|
}
|