@peter.prib/renderengine 0.12.0 → 0.13.0

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 (31) hide show
  1. package/README.md +10 -4
  2. package/dist/renderengine-workspace.umd.js +1920 -1055
  3. package/dist/renderengine.umd.js +1901 -1052
  4. package/documentation/REFERENCE.md +66 -5
  5. package/package.json +1 -1
  6. package/src/charting/Axis.js +47 -5
  7. package/src/charting/DiagramEditor.js +239 -34
  8. package/src/charting/ModelFileFormat.js +47 -0
  9. package/src/charting/archimate/ArchimateCanvasEditor.js +1108 -1012
  10. package/src/charting/archimate/deleteFromModel.js +57 -0
  11. package/src/charting/archimate/elementMenuItems.js +3 -1
  12. package/src/charting/archimate/heatMaps.js +169 -0
  13. package/src/charting/archimate/index.js +1 -0
  14. package/src/charting/charts/BarChart.js +5 -5
  15. package/src/charting/charts/BarChartBase.js +21 -3
  16. package/src/charting/charts/GroupedBarChart.js +7 -9
  17. package/src/charting/charts/TableChart.js +18 -2
  18. package/src/charting/diagrams/ArchimateModel.js +33 -0
  19. package/src/charting/diagrams/DeleteFromModelCommand.js +81 -0
  20. package/src/charting/diagrams/EdgeGeometry.js +16 -0
  21. package/src/charting/edgeStyle/EdgeStyleSchema.js +6 -2
  22. package/src/charting/pivot/PivotGridChart.js +1 -1
  23. package/src/charting/pivot/PivotView.js +11 -2
  24. package/src/charting/table/TableView.js +10 -1
  25. package/src/charting/table/TableViewChart.js +1 -1
  26. package/src/charting/templates/Template.js +10 -0
  27. package/src/exportImage.js +45 -18
  28. package/src/index.js +17 -4
  29. package/src/workspace/parts/ArchimateCanvasPart.js +3 -0
  30. package/src/workspace/parts/DiagramEditorPart.js +2 -0
  31. package/src/workspace/parts/ModelHosted.js +15 -4
@@ -82,7 +82,7 @@ export class TableViewChart extends TableChart {
82
82
  columns.forEach((c, i) => {
83
83
  const left = x0 + i * colW;
84
84
  const hit = this.renderer.rect(left, top, colW, ROW_H, 'transparent', null);
85
- const text = this._cell(TableChart.cellText(row[c.key], nullText), c, left, top, colW, { fill: row[c.key] == null ? '#999' : '#222' });
85
+ const text = this._cell(view.valueText(c.key, row[c.key], nullText), c, left, top, colW, { fill: row[c.key] == null ? '#999' : '#222' });
86
86
  this._on(interactive, [hit, text], (e) => this.showMenu(e.clientX, e.clientY, view.cellMenu(row, c.key)), null);
87
87
  });
88
88
  top += ROW_H;
@@ -102,6 +102,16 @@ export class Template {
102
102
  return { x: this.x + ux * t, y: this.y + uy * t };
103
103
  }
104
104
 
105
+ /**
106
+ * Where a line from (px, py), a point inside the shape, heading in
107
+ * direction (dx, dy) leaves its outline (the shape unrotated).
108
+ * @returns {{x: number, y: number}}
109
+ */
110
+ exitPoint(px, py, dx, dy) {
111
+ const t = rayExitDistance(this.outline(), px, py, dx, dy);
112
+ return { x: px + dx * t, y: py + dy * t };
113
+ }
114
+
105
115
  /** Centre of the name text; the z-index line sits just below it. */
106
116
  get labelAnchor() { return { x: this.x, y: this.y - 8 }; }
107
117
 
@@ -31,10 +31,10 @@ export const MAX_IMAGE_PIXELS = 32_000_000;
31
31
  /**
32
32
  * Work out the bitmap size to rasterize at.
33
33
  *
34
- * `scale` is what the caller asked for (2 = retina-sharp). It is reduced, never
35
- * the diagram's own dimensions, when the result would be too large: halving the
36
- * scale of a huge diagram still shows all of it, whereas cropping or shrinking
37
- * the diagram loses content or legibility.
34
+ * `scale` is what the caller asked for (1 = 100%, 2 = retina-sharp, 0.5 = half
35
+ * size). It is reduced, never the diagram's own dimensions, when the result
36
+ * would be too large: halving the scale of a huge diagram still shows all of
37
+ * it, whereas cropping or shrinking the diagram loses content or legibility.
38
38
  *
39
39
  * @returns {{width:number, height:number, scale:number, clamped:boolean}}
40
40
  */
@@ -49,10 +49,6 @@ export function imageSize({ width, height, scale = 2, maxPixels = MAX_IMAGE_PIXE
49
49
  s = Math.sqrt(maxPixels / (w * h));
50
50
  clamped = true;
51
51
  }
52
- // A scale below 1:1 would be a downscale the caller never asked for, but a
53
- // diagram that is itself over the budget leaves no choice.
54
- if (s < 1 && w * h <= maxPixels) s = 1;
55
-
56
52
  return { width: Math.max(1, Math.round(w * s)), height: Math.max(1, Math.round(h * s)), scale: s, clamped };
57
53
  }
58
54
 
@@ -67,8 +63,10 @@ export function imageSize({ width, height, scale = 2, maxPixels = MAX_IMAGE_PIXE
67
63
  * @param {number} [options.width] pixels; omitted leaves what is there
68
64
  * @param {number} [options.height]
69
65
  * @param {string|null} [options.background='#ffffff'] null for transparent
66
+ * @param {string} [options.viewBox] the drawing's own units, when it has none
67
+ * (so a picture sized other than as drawn scales rather than crops)
70
68
  */
71
- export function prepareSvgMarkup(markup, { width, height, background = '#ffffff' } = {}) {
69
+ export function prepareSvgMarkup(markup, { width, height, background = '#ffffff', viewBox } = {}) {
72
70
  let out = String(markup ?? '').trim();
73
71
  const open = out.match(/^<svg\b[^>]*>/i);
74
72
  if (!open) return out; // not an <svg> — hand it back untouched
@@ -87,13 +85,23 @@ export function prepareSvgMarkup(markup, { width, height, background = '#ffffff'
87
85
  };
88
86
  if (Number.isFinite(width) && width > 0) setAttr('width', Math.round(width));
89
87
  if (Number.isFinite(height) && height > 0) setAttr('height', Math.round(height));
88
+ if (viewBox && !/\sviewBox\s*=/i.test(tag)) setAttr('viewBox', viewBox);
90
89
 
91
90
  out = tag + out.slice(open[0].length);
92
91
 
93
92
  if (background) {
94
- // First child, so it sits behind everything. Percentages rather than
95
- // pixels so it still covers the drawing if a viewBox rescales it.
96
- out = out.replace(tag, `${tag}<rect x="0" y="0" width="100%" height="100%" fill="${background}"/>`);
93
+ // First child, so it sits behind everything. With a viewBox it reaches
94
+ // a whole viewBox past each side (the picture clips it): a percentage
95
+ // counts from (0,0), so a viewBox starting elsewhere (a cropped
96
+ // drawing) would leave a strip bare, and the viewBox alone would leave
97
+ // the bands a picture of another shape letterboxes it with.
98
+ // Without one, percentages, so it covers whatever size it is shown at.
99
+ const box = tag.match(/\sviewBox\s*=\s*["']\s*([-\d.e+]+)[\s,]+([-\d.e+]+)[\s,]+([-\d.e+]+)[\s,]+([-\d.e+]+)\s*["']/i);
100
+ const [x, y, w, h] = box ? box.slice(1).map(Number) : [];
101
+ const rect = box
102
+ ? `<rect x="${x - w}" y="${y - h}" width="${3 * w}" height="${3 * h}" fill="${background}"/>`
103
+ : `<rect x="0" y="0" width="100%" height="100%" fill="${background}"/>`;
104
+ out = out.replace(tag, `${tag}${rect}`);
97
105
  }
98
106
  return out;
99
107
  }
@@ -126,12 +134,12 @@ export function findDrawable(host) {
126
134
  * Size is taken from the element's laid-out box rather than its attributes,
127
135
  * because that is what the user is actually looking at.
128
136
  */
129
- export function serializeSvg(svgEl, { background = '#ffffff', width, height } = {}) {
137
+ export function serializeSvg(svgEl, { background = '#ffffff', width, height, scale = 1 } = {}) {
130
138
  const box = svgEl.getBoundingClientRect?.() ?? {};
131
139
  const w = width ?? svgEl.width?.baseVal?.value ?? box.width;
132
140
  const h = height ?? svgEl.height?.baseVal?.value ?? box.height;
133
141
  const markup = new XMLSerializer().serializeToString(svgEl);
134
- return prepareSvgMarkup(markup, { width: w, height: h, background });
142
+ return prepareSvgMarkup(markup, { width: w * scale, height: h * scale, background, viewBox: `0 0 ${w} ${h}` });
135
143
  }
136
144
 
137
145
  /**
@@ -150,9 +158,16 @@ export async function toPngBlob(host, { scale = 2, background = '#ffffff', maxPi
150
158
  }
151
159
 
152
160
  const box = drawable.getBoundingClientRect();
153
- const size = imageSize({ width: box.width, height: box.height, scale, maxPixels });
154
161
  const markup = serializeSvg(drawable, { background, width: box.width, height: box.height });
162
+ return svgMarkupToPngBlob(markup, { width: box.width, height: box.height, scale, background, maxPixels });
163
+ }
155
164
 
165
+ /**
166
+ * Rasterize standalone SVG markup (prepareSvgMarkup's) drawn `width` × `height`
167
+ * to a PNG Blob at `scale`. DOM only.
168
+ */
169
+ export async function svgMarkupToPngBlob(markup, { width, height, scale = 1, background = '#ffffff', maxPixels = MAX_IMAGE_PIXELS } = {}) {
170
+ const size = imageSize({ width, height, scale, maxPixels });
156
171
  // A data: URL rather than a blob: URL — a blob: URL taints nothing either,
157
172
  // but Safari has historically refused to draw SVG from one.
158
173
  const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(markup)}`;
@@ -200,7 +215,15 @@ export function downloadBlob(blob, filename) {
200
215
  * @returns {Promise<{via:'clipboard'|'download', filename?:string}>}
201
216
  */
202
217
  export async function copyImage(host, { scale = 2, background = '#ffffff', name = 'diagram' } = {}) {
203
- const blob = await toPngBlob(host, { scale, background });
218
+ return copyPngBlob(await toPngBlob(host, { scale, background }), { name });
219
+ }
220
+
221
+ /**
222
+ * Put a PNG Blob on the clipboard, or download it where the browser cannot
223
+ * (as copyImage). DOM only.
224
+ * @returns {Promise<{via:'clipboard'|'download', filename?:string}>}
225
+ */
226
+ export async function copyPngBlob(blob, { name = 'diagram' } = {}) {
204
227
  const canWrite = typeof ClipboardItem !== 'undefined'
205
228
  && typeof navigator !== 'undefined'
206
229
  && navigator.clipboard?.write;
@@ -220,10 +243,14 @@ export async function copyImage(host, { scale = 2, background = '#ffffff', name
220
243
  }
221
244
 
222
245
  /** Save a diagram as an SVG file. DOM only. */
223
- export function downloadSvg(host, { background = '#ffffff', name = 'diagram' } = {}) {
246
+ export function downloadSvg(host, { background = '#ffffff', name = 'diagram', scale = 1 } = {}) {
224
247
  const drawable = findDrawable(host);
225
248
  if (!drawable || drawable.tagName.toLowerCase() !== 'svg') throw new Error('No <svg> to export');
226
- const markup = serializeSvg(drawable, { background });
249
+ downloadSvgMarkup(serializeSvg(drawable, { background, scale }), { name });
250
+ }
251
+
252
+ /** Save standalone SVG markup as a file. DOM only. */
253
+ export function downloadSvgMarkup(markup, { name = 'diagram' } = {}) {
227
254
  downloadBlob(new Blob([markup], { type: 'image/svg+xml;charset=utf-8' }), imageFileName(name, 'svg'));
228
255
  }
229
256
 
package/src/index.js CHANGED
@@ -188,13 +188,18 @@ import {
188
188
  parseXML, parseArchiModel, toRenderEngineModel, toArchimateModel, toArchiXML, fromArchimateView, toArchiExport,
189
189
  } from './archimateArchiFormat.js';
190
190
  import { parseExchangeModel, toExchangeXML, EXCHANGE_NAMESPACE, EXCHANGE_TYPE_PROPERTY, EXCHANGE_VIEWPOINTS } from './archimateExchangeFormat.js';
191
- import { ModelFileFormat, modelFileMenuItems, importFile } from './charting/ModelFileFormat.js';
191
+ import { ModelFileFormat, modelFileMenuItems, importFile, findPayload, askPasteImport } from './charting/ModelFileFormat.js';
192
192
  import { ArchiFileFormat, ExchangeFileFormat, ArchimateCsvFileFormat, ARCHIMATE_FILE_FORMATS } from './charting/archimate/index.js';
193
193
  import { ShowElementCommand } from './charting/diagrams/ShowElementCommand.js';
194
194
  import { EmphasisRule, EMPHASIS_OPACITY, EMPHASIS_LEVELS, strongerEmphasis } from './charting/emphasis/EmphasisRule.js';
195
195
  import { HighlightRule } from './charting/emphasis/HighlightRule.js';
196
196
  import { CatalogDialog } from './charting/CatalogDialog.js';
197
- import { CustomViewpoints, CUSTOM_VIEWPOINTS_STORE, specializationCatalogDialog, elementTypeChoices } from './charting/archimate/index.js';
197
+ import { CustomViewpoints, CUSTOM_VIEWPOINTS_STORE, specializationCatalogDialog, elementTypeChoices, askDeleteFromModel, deleteFromModelMessage } from './charting/archimate/index.js';
198
+ import { DeleteFromModelCommand } from './charting/diagrams/DeleteFromModelCommand.js';
199
+ import {
200
+ CHANGE_COLOURS, CHANGE_MAKERS, CHANGE_WORDS, changeAction, relationshipChange, elementChange,
201
+ propertyParams, expressionValues, resultBands, sequentialColours,
202
+ } from './charting/archimate/heatMaps.js';
198
203
  import { ViewpointRule, ElementFilterRule } from './charting/archimate/index.js';
199
204
  import { MODEL_ELEMENT_MIME, TREE_LAYERS, treeLayerOf } from './charting/modelTree/ModelTreePanel.js';
200
205
  import { parseArchiFile, toArchiFile, ARCHI_MODEL_ENTRY } from './archimateArchive.js';
@@ -207,6 +212,7 @@ import {
207
212
  // reference sits inside a function body, so loading it under node does nothing.
208
213
  import {
209
214
  copyImage, toPngBlob, downloadPng, downloadSvg, downloadBlob,
215
+ svgMarkupToPngBlob, copyPngBlob, downloadSvgMarkup,
210
216
  serializeSvg, prepareSvgMarkup, imageSize, imageFileName, findDrawable, MAX_IMAGE_PIXELS,
211
217
  } from './exportImage.js';
212
218
  // Same reasoning as exportImage: every DOM reference sits inside a function
@@ -249,12 +255,16 @@ export {
249
255
  importElementsCSV, importRelationshipsCSV, importModelCSV,
250
256
  parseArchiModel, toArchimateModel, fromArchimateView, toArchiExport,
251
257
  parseExchangeModel, toExchangeXML, EXCHANGE_NAMESPACE, EXCHANGE_TYPE_PROPERTY, EXCHANGE_VIEWPOINTS,
252
- ModelFileFormat, modelFileMenuItems, importFile, ArchiFileFormat, ExchangeFileFormat, ArchimateCsvFileFormat, ARCHIMATE_FILE_FORMATS,
258
+ ModelFileFormat, modelFileMenuItems, importFile, findPayload, askPasteImport, ArchiFileFormat, ExchangeFileFormat, ArchimateCsvFileFormat, ARCHIMATE_FILE_FORMATS,
253
259
  ShowElementCommand, MODEL_ELEMENT_MIME, TREE_LAYERS, treeLayerOf,
254
260
  EmphasisRule, EMPHASIS_OPACITY, EMPHASIS_LEVELS, strongerEmphasis, HighlightRule, ViewpointRule, ElementFilterRule,
255
261
  CatalogDialog, CustomViewpoints, CUSTOM_VIEWPOINTS_STORE, specializationCatalogDialog, elementTypeChoices,
262
+ askDeleteFromModel, deleteFromModelMessage, DeleteFromModelCommand,
263
+ CHANGE_COLOURS, CHANGE_MAKERS, CHANGE_WORDS, changeAction, relationshipChange, elementChange,
264
+ propertyParams, expressionValues, resultBands, sequentialColours,
256
265
  parseArchiFile, toArchiFile, ARCHI_MODEL_ENTRY, ZipArchive,
257
266
  copyImage, toPngBlob, downloadPng, downloadSvg, downloadBlob,
267
+ svgMarkupToPngBlob, copyPngBlob, downloadSvgMarkup,
258
268
  serializeSvg, prepareSvgMarkup, imageSize, imageFileName, findDrawable, MAX_IMAGE_PIXELS,
259
269
  showPropertiesDialog, showDiagramPropertiesDialog, showGlobalSettingsDialog,
260
270
  propertyRowsFor, coerceFieldValue, derivedRefreshPlan, LAYER_CHOICES, NODE_SECTIONS,
@@ -1460,6 +1470,8 @@ export class RenderEngine {
1460
1470
  * @param {string} [options.title]
1461
1471
  * @param {number} [options.chartHeight] a chart's height (360)
1462
1472
  * @param {boolean} [options.fitHeight] size the container to what is drawn (true)
1473
+ * @param {function|Intl.NumberFormat|object} [options.format] how numbers show: for every column, or
1474
+ * per column ({ Amount: Intl.NumberFormat }); copies keep the raw numbers
1463
1475
  * @returns {TableView|null} the table's state, for the host to read or change
1464
1476
  */
1465
1477
  resultView(rows, options = {}) {
@@ -1499,7 +1511,8 @@ export class RenderEngine {
1499
1511
  * the axes. The container's height follows the grid, as resultView's does.
1500
1512
  * @param {Array<object>} rows
1501
1513
  * @param {object} [options] rowAxis, columnAxis, measures, columns (hints, with the rollup column's),
1502
- * dimensions, onDrill, menus, title, nullText, fitHeight
1514
+ * dimensions, onDrill, menus, title, nullText, fitHeight, format (how numbers show, per measure or for
1515
+ * all: cells, totals and the traffic lights' cut-offs; the copy keeps the raw numbers)
1503
1516
  * @returns {PivotView|null} the grid's state, for the host to read or change
1504
1517
  */
1505
1518
  pivotView(rows, options = {}) {
@@ -253,6 +253,8 @@ export class ArchimateCanvasPart extends EditorPart {
253
253
  if (previous) {
254
254
  this.editor.showGrid = previous.showGrid;
255
255
  this.editor.gridSize = previous.gridSize;
256
+ this.editor.showExportArea = previous.showExportArea;
257
+ this.editor.exportOptions = previous.exportOptions;
256
258
  }
257
259
 
258
260
  // A canvas's palette is Archi's canvas palette; another view's, ArchiMate's element types.
@@ -351,6 +353,7 @@ export class ArchimateCanvasPart extends EditorPart {
351
353
  this.editor?.selectTemplate(instanceId);
352
354
  },
353
355
  addToView: (el) => this.editor?.showElement(el.id, this.editor.visibleCentre()),
356
+ deleteFromModel: (el) => this.editor?.askDeleteFromModel([el.id]),
354
357
  });
355
358
  }
356
359
 
@@ -89,6 +89,8 @@ export class DiagramEditorPart extends EditorPart {
89
89
  if (previous) {
90
90
  this.editor.showGrid = previous.showGrid;
91
91
  this.editor.gridSize = previous.gridSize;
92
+ this.editor.showExportArea = previous.showExportArea;
93
+ this.editor.exportOptions = previous.exportOptions;
92
94
  }
93
95
  this.editor.initPaletteForDiagramType(palette, model.templateType);
94
96
  this.menuBar?.dispose();
@@ -1,6 +1,6 @@
1
1
  import { ModelFile } from './ModelFile.js';
2
2
  import { EditorPart } from './EditorPart.js';
3
- import { modelFileMenuItems } from '../../charting/ModelFileFormat.js';
3
+ import { modelFileMenuItems, askPasteImport } from '../../charting/ModelFileFormat.js';
4
4
  import { pickFile } from '../../charting/toolbar/pickFile.js';
5
5
 
6
6
  /**
@@ -121,9 +121,20 @@ export const ModelHosted = (Base) => class extends Base {
121
121
 
122
122
  /** Import ▸: each format that only reads (another tool's export), opened as a new model. */
123
123
  importItems() {
124
- return modelFileMenuItems(this.fileFormats().filter(f => f.exports === false), {
125
- model: () => this.model, only: 'import', onImport: (model) => this.onImported(model), document: () => this.document,
126
- });
124
+ return [
125
+ ...modelFileMenuItems(this.fileFormats().filter(f => f.exports === false), {
126
+ model: () => this.model, only: 'import', onImport: (model) => this.onImported(model), document: () => this.document,
127
+ }),
128
+ ...this.pasteItems(),
129
+ ];
130
+ }
131
+
132
+ /** Paste …: a model in each of its text formats from pasted text (an assistant's answer, say), as Open… would bring it. */
133
+ pasteItems() {
134
+ return this.fileFormats().filter(f => f.imports && !f.readsBytes).map(format => ({
135
+ label: `From pasted text (${format.label})…`,
136
+ action: () => askPasteImport(format, { onImport: (model) => this.onImported(model), document: this.document ?? globalThis.document }),
137
+ }));
127
138
  }
128
139
 
129
140
  /**