@peter.prib/renderengine 0.15.2 → 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 +5963 -3766
- package/dist/renderengine.umd.js +5863 -3772
- package/documentation/REFERENCE.md +37 -3
- 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 +5 -108
- package/src/charting/DiagramTypeRegistry.js +20 -1
- package/src/charting/DiagramViewPropertiesDialog.js +54 -10
- package/src/charting/Legend.js +1 -2
- package/src/charting/Statistics.js +14 -0
- 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/DiagramView.js +7 -2
- package/src/charting/diagrams/GraphDiagram3D.js +15 -0
- 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 +7 -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 -3371
- package/src/kernelExpr.js +80 -5
- package/src/workspace/parts/DiagramEditorPart.js +3 -1
- 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);
|
|
@@ -98,6 +98,7 @@ export class DiagramEditorPart extends EditorPart {
|
|
|
98
98
|
history: view.history,
|
|
99
99
|
arrangement: view.arrangement,
|
|
100
100
|
edgeStyleLevels: this.edgeStyleLevels(view),
|
|
101
|
+
viewSettings: () => view.viewSettings ?? {},
|
|
101
102
|
});
|
|
102
103
|
if (previous) {
|
|
103
104
|
this.editor.showGrid = previous.showGrid;
|
|
@@ -131,8 +132,9 @@ export class DiagramEditorPart extends EditorPart {
|
|
|
131
132
|
view,
|
|
132
133
|
document: this.document,
|
|
133
134
|
cascade: new EdgeStyleCascade(this.edgeStyleLevels(view)),
|
|
134
|
-
// The editor shows the current diagram; a style change there needs a redraw.
|
|
135
|
+
// The editor shows the current diagram; a style or appearance change there needs a redraw.
|
|
135
136
|
onStyleChange: () => { if (view === this.model.currentView) this.editor?.render(); },
|
|
137
|
+
onSettingsChange: () => { if (view === this.model.currentView) this.editor?.render(); },
|
|
136
138
|
}).open();
|
|
137
139
|
}
|
|
138
140
|
|
|
@@ -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
|
}
|