@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.
Files changed (42) hide show
  1. package/dist/renderengine-workspace.umd.js +5963 -3766
  2. package/dist/renderengine.umd.js +5863 -3772
  3. package/documentation/REFERENCE.md +37 -3
  4. package/package.json +1 -1
  5. package/src/archimateArchiFormat.js +114 -10
  6. package/src/archimateArchive.js +27 -4
  7. package/src/archimateExchangeFormat.js +12 -4
  8. package/src/archimateSpecializations.js +11 -1
  9. package/src/chartCatalog.js +6 -4
  10. package/src/charting/ArchimateDiagramEditor.js +5 -108
  11. package/src/charting/DiagramTypeRegistry.js +20 -1
  12. package/src/charting/DiagramViewPropertiesDialog.js +54 -10
  13. package/src/charting/Legend.js +1 -2
  14. package/src/charting/Statistics.js +14 -0
  15. package/src/charting/charts/BarChart.js +59 -8
  16. package/src/charting/charts/HexbinChart.js +15 -0
  17. package/src/charting/charts/LollipopChart.js +42 -7
  18. package/src/charting/decision/DecisionWorkbench.js +71 -4
  19. package/src/charting/diagramModel/DiagramView.js +7 -2
  20. package/src/charting/diagrams/GraphDiagram3D.js +15 -0
  21. package/src/charting/graph/GraphSource.js +72 -0
  22. package/src/charting/modelTree/NavigatorModel.js +22 -8
  23. package/src/charting/report/TemplatedReport.js +27 -377
  24. package/src/charting/report/ViewsReport.js +118 -0
  25. package/src/charting/report/reportWriters.js +496 -0
  26. package/src/charting/templates/C4BoundaryTemplate.js +34 -0
  27. package/src/charting/templates/C4ComponentTemplate.js +30 -0
  28. package/src/charting/templates/C4ContainerTemplate.js +49 -0
  29. package/src/charting/templates/C4PersonTemplate.js +35 -0
  30. package/src/charting/templates/C4SystemTemplate.js +32 -0
  31. package/src/charting/templates/C4Template.js +95 -0
  32. package/src/charting/templates/index.js +6 -0
  33. package/src/charting/templates/templateRegistry.js +7 -0
  34. package/src/compute/ComputeBackend.js +132 -0
  35. package/src/compute/CpuBackend.js +209 -0
  36. package/src/compute/WebGpuBackend.js +630 -0
  37. package/src/compute/index.js +5 -0
  38. package/src/compute/selectBackend.js +85 -0
  39. package/src/index.js +3403 -3371
  40. package/src/kernelExpr.js +80 -5
  41. package/src/workspace/parts/DiagramEditorPart.js +3 -1
  42. 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 _emitJs(node) {
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(`_emitJs: unknown node '${n.kind}'`);
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 ${_emitJs(ast)};`;
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
  }