@peter.prib/renderengine 0.14.0 → 0.15.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.
- package/README.md +45 -19
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49391 -39748
- package/dist/renderengine.umd.js +25366 -15650
- package/documentation/REFERENCE.md +38 -0
- package/package.json +6 -1
- package/src/CanvasRenderer.js +225 -200
- package/src/SVGRenderer.js +476 -454
- package/src/archimate.js +22 -2
- package/src/archimateArchiFormat.js +1735 -1701
- package/src/archimateArchive.js +36 -7
- package/src/archimateCSV.js +93 -37
- package/src/archimateDrawio.js +2 -0
- package/src/archimateExchangeFormat.js +3 -1
- package/src/chartCatalog.js +219 -0
- package/src/charting/Annotations.js +169 -0
- package/src/charting/ArchimateDiagramEditor.js +54 -10
- package/src/charting/Axis.js +217 -5
- package/src/charting/Chart.js +99 -9
- package/src/charting/Chart3D.js +3 -0
- package/src/charting/ChartBox.js +8 -1
- package/src/charting/ColorPalette.js +42 -4
- package/src/charting/DiagramEditor.js +174 -39
- package/src/charting/DiagramEditorCommands.js +28 -0
- package/src/charting/Edge.js +7 -0
- package/src/charting/Hierarchy.js +114 -0
- package/src/charting/InteractionManager.js +4 -0
- package/src/charting/ModelFileFormat.js +5 -1
- package/src/charting/SettingsManager.js +35 -0
- package/src/charting/ShapePaths.js +82 -10
- package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
- package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +52 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +5 -3
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +23 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +105 -2
- package/src/charting/charts/CirclePackChart.js +81 -0
- package/src/charting/charts/ComboChart.js +233 -0
- package/src/charting/charts/ContourChart.js +310 -0
- package/src/charting/charts/DonutChart3D.js +13 -0
- package/src/charting/charts/DumbbellChart.js +217 -0
- package/src/charting/charts/EcdfChart.js +150 -0
- package/src/charting/charts/FunnelChart.js +4 -1
- package/src/charting/charts/GeoMapChart.js +132 -11
- package/src/charting/charts/GroupedBarChart.js +44 -3
- package/src/charting/charts/GroupedDistributionChart.js +26 -1
- package/src/charting/charts/HeatmapChart.js +2 -2
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/RadarChart.js +7 -1
- package/src/charting/charts/RadialBarChart.js +5 -2
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +127 -4
- package/src/charting/charts/ScatterChart.js +13 -0
- package/src/charting/charts/ScatterMatrixChart.js +119 -0
- package/src/charting/charts/SlopegraphChart.js +205 -0
- package/src/charting/charts/SparklineTableChart.js +166 -0
- package/src/charting/charts/StackedBarChart.js +48 -5
- package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
- package/src/charting/charts/StreamGraphChart.js +10 -3
- package/src/charting/charts/SunburstChart.js +27 -7
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +28 -8
- package/src/charting/charts/ViolinChart.js +18 -0
- package/src/charting/datamodel/ArchimateDataLink.js +70 -0
- package/src/charting/datamodel/DataLineage.js +150 -0
- package/src/charting/datamodel/DataModel.js +83 -1
- package/src/charting/datamodel/ModelMapping.js +87 -0
- package/src/charting/datamodel/SqlDdl.js +110 -85
- package/src/charting/datamodel/index.js +3 -0
- package/src/charting/decision/DecisionWorkbench.js +295 -0
- package/src/charting/diagrams/ArchimateDiagram.js +25 -9
- package/src/charting/diagrams/ArchimateModel.js +1382 -1238
- package/src/charting/diagrams/GraphDiagram.js +1424 -1350
- package/src/charting/diagrams/SketchView.js +121 -0
- package/src/charting/diagrams/sketchBoxKinds.js +30 -0
- package/src/charting/modelTree/AnalysisPanel.js +101 -23
- package/src/charting/modelTree/NavigatorModel.js +410 -0
- package/src/charting/modelTree/NavigatorPanel.js +453 -0
- package/src/charting/pivot/PivotChart.js +109 -4
- package/src/charting/pivot/PivotGridChart.js +321 -33
- package/src/charting/pivot/PivotView.js +82 -0
- package/src/charting/report/TemplatedReport.js +547 -0
- package/src/charting/templates/BoxTemplate.js +28 -27
- package/src/charting/templates/PackageTemplate.js +26 -25
- package/src/charting/templates/SwimlaneTemplate.js +19 -18
- package/src/charting/templates/Template.js +219 -212
- package/src/cli.js +146 -0
- package/src/index.js +3370 -2995
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- package/src/workspace/styles.js +36 -1
|
@@ -3200,6 +3200,44 @@ to it) / `deleteEdgeCommand(id)`. A subclass whose shapes and lines stand for so
|
|
|
3200
3200
|
overrides those factories (ArchimateCanvasEditor does). A template class says whether the editor
|
|
3201
3201
|
offers it a rotate handle (`static rotatable`, true by default; ArchiMate's templates are not). `pickFile(accept)` resolves with the chosen `File` (or `null`).
|
|
3202
3202
|
|
|
3203
|
+
## Decision Workbench (`DecisionWorkbench`)
|
|
3204
|
+
|
|
3205
|
+
The views of a SADIS recommendation made by decisionengine (`gitlab.com/peter.prib/decisionengine`),
|
|
3206
|
+
drawn with this library's charts. It reads decisionengine's JSON, not decisionengine: a workbench
|
|
3207
|
+
document
|
|
3208
|
+
|
|
3209
|
+
```js
|
|
3210
|
+
{ kind: 'sadis-workbench', report, plan?, before? } // report.toJSON(), plan.toJSON(), an earlier report.toJSON()
|
|
3211
|
+
```
|
|
3212
|
+
|
|
3213
|
+
which decisionengine's SADIS Explorer downloads ("Download for the Decision Workbench").
|
|
3214
|
+
|
|
3215
|
+
```js
|
|
3216
|
+
import { DecisionWorkbench } from '@peter.prib/renderengine';
|
|
3217
|
+
const workbench = DecisionWorkbench.from(doc, { start: '2026-11-02', periodDays: 14 });
|
|
3218
|
+
for (const view of workbench.views()) {
|
|
3219
|
+
view.id, view.title; // 'executive', 'Executive', …
|
|
3220
|
+
view.charts(); // [{ title, chartType, data, options }] — chartType is a CHART_CATALOG key
|
|
3221
|
+
view.notes(); // plain lines for what the charts cannot show
|
|
3222
|
+
}
|
|
3223
|
+
```
|
|
3224
|
+
|
|
3225
|
+
| View | Needs | Charts |
|
|
3226
|
+
|---|---|---|
|
|
3227
|
+
| Executive | a report | `kpi-tile` (what to build next; half the value delivered by), `stackedBar` (score part by part), `line` (value delivered by period) |
|
|
3228
|
+
| Architecture | a report | `scatter` (visible rating against basin position: foundations sit high and left), `lollipop` (share each frees) |
|
|
3229
|
+
| Delivery | a plan | `gantt` (the plan on a calendar), `stackedBar` (capacity used per period, per team when planned by team) |
|
|
3230
|
+
| Portfolio | a report with perspectives | `heatmap` (rank by perspective), `dumbbell` (best and worst rank, most contested first) |
|
|
3231
|
+
| Replay | an earlier report | `slopegraph` (rank then → now); notes say what entered, left (and why) and moved |
|
|
3232
|
+
|
|
3233
|
+
A view the document cannot fill is not offered. `from()` refuses what is not a workbench document,
|
|
3234
|
+
saying why. The delivery plan's periods are put on a calendar: period 1 starts at `start` (default:
|
|
3235
|
+
the report's as-of business time, else today), each `periodDays` long (default 14). Each view is a
|
|
3236
|
+
`WorkbenchView` subclass (`WORKBENCH_VIEWS`); a new view is a new subclass.
|
|
3237
|
+
|
|
3238
|
+
`examples/decision-workbench.html` is the page: open a workbench file and each view is a workspace
|
|
3239
|
+
perspective, its charts in `ChartPart`s and its notes in a list.
|
|
3240
|
+
|
|
3203
3241
|
## Navigation menus
|
|
3204
3242
|
|
|
3205
3243
|
`NavMenu` is a vertical navigation menu (a sidebar's), written as data: no markup. Each entry is
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peter.prib/renderengine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "ArchiMate modeller, data modeller, diagram editor, OLAP cube & pivot chart studio, and 50+ type chart library for the browser, with no runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"module": "src/index.js",
|
|
8
8
|
"browser": "dist/renderengine.umd.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"renderengine": "./bin/renderengine.mjs"
|
|
11
|
+
},
|
|
9
12
|
"exports": {
|
|
10
13
|
".": "./src/index.js",
|
|
11
14
|
"./workspace": "./src/workspace/index.js",
|
|
@@ -14,6 +17,7 @@
|
|
|
14
17
|
"./package.json": "./package.json"
|
|
15
18
|
},
|
|
16
19
|
"files": [
|
|
20
|
+
"bin/*.mjs",
|
|
17
21
|
"src/",
|
|
18
22
|
"dist/*.umd.js",
|
|
19
23
|
"examples/archimate.html",
|
|
@@ -24,6 +28,7 @@
|
|
|
24
28
|
"LICENSE"
|
|
25
29
|
],
|
|
26
30
|
"scripts": {
|
|
31
|
+
"cli": "node bin/renderengine.mjs",
|
|
27
32
|
"start": "node scripts/open.mjs examples/archimate.html",
|
|
28
33
|
"archimate": "node scripts/open.mjs examples/archimate.html",
|
|
29
34
|
"diagram": "node scripts/open.mjs examples/diagram-editor.html",
|
package/src/CanvasRenderer.js
CHANGED
|
@@ -1,201 +1,226 @@
|
|
|
1
|
-
import { BaseRenderer } from './BaseRenderer.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @class CanvasRenderer
|
|
5
|
-
* Renders shapes by drawing on a 2D canvas context.
|
|
6
|
-
*/
|
|
7
|
-
export class CanvasRenderer extends BaseRenderer {
|
|
8
|
-
get type() { return 'canvas'; }
|
|
9
|
-
get centerTextAnchor() { return 'center'; }
|
|
10
|
-
|
|
11
|
-
constructor(container, width, height) {
|
|
12
|
-
super(container, width, height);
|
|
13
|
-
|
|
14
|
-
this.canvas
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
this.canvas.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return p;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.ctx.stroke
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.ctx.
|
|
84
|
-
this.ctx.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this.ctx.
|
|
91
|
-
|
|
92
|
-
this.ctx.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
this.ctx.
|
|
114
|
-
this.ctx.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this.
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
this.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this.
|
|
155
|
-
this.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (path2d ? this.ctx.
|
|
174
|
-
return shape;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const targetShape = this._shapes.find(s => s.id === shape.id);
|
|
191
|
-
if (targetShape) {
|
|
192
|
-
|
|
193
|
-
this._redraw();
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
1
|
+
import { BaseRenderer } from './BaseRenderer.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class CanvasRenderer
|
|
5
|
+
* Renders shapes by drawing on a 2D canvas context.
|
|
6
|
+
*/
|
|
7
|
+
export class CanvasRenderer extends BaseRenderer {
|
|
8
|
+
get type() { return 'canvas'; }
|
|
9
|
+
get centerTextAnchor() { return 'center'; }
|
|
10
|
+
|
|
11
|
+
constructor(container, width, height) {
|
|
12
|
+
super(container, width, height);
|
|
13
|
+
const doc = typeof document !== 'undefined' ? document : container?.ownerDocument;
|
|
14
|
+
this.canvas = doc?.createElement ? doc.createElement('canvas') : (container?.canvas || {
|
|
15
|
+
style: {}, width, height,
|
|
16
|
+
getContext: () => ({ fillRect: () => {}, measureText: () => ({ width: 10 }), clearRect: () => {}, beginPath: () => {}, arc: () => {}, fill: () => {}, stroke: () => {} }),
|
|
17
|
+
});
|
|
18
|
+
this.canvas.width = width;
|
|
19
|
+
this.canvas.height = height;
|
|
20
|
+
// Inline elements (the default for <canvas>) leave a few px of baseline
|
|
21
|
+
// whitespace below them inside a block container, making the
|
|
22
|
+
// container's scrollHeight exceed its clientHeight for every chart.
|
|
23
|
+
if (this.canvas?.style) this.canvas.style.display = 'block';
|
|
24
|
+
this._shapes = [];
|
|
25
|
+
this.container?.appendChild?.(this.canvas);
|
|
26
|
+
this.ctx = this.canvas?.getContext?.('2d') || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resize the canvas. Setting a canvas's size clears it, so redraw after. */
|
|
30
|
+
resize(width, height) {
|
|
31
|
+
super.resize(width, height);
|
|
32
|
+
this.canvas.width = width;
|
|
33
|
+
this.canvas.height = height;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
clear() {
|
|
37
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
38
|
+
this._shapes = [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Always returns an isolated Path2D — used by hitTest() to test one
|
|
42
|
+
// shape's geometry at a time. Previously rect/circle called this.ctx.rect()/
|
|
43
|
+
// .arc() directly onto the canvas's own current path without resetting it
|
|
44
|
+
// first, so each hitTest() loop iteration kept accumulating geometry from
|
|
45
|
+
// every shape checked before it, corrupting hit results for anything but
|
|
46
|
+
// the very first shape tested (tooltips/clicks/hover on Canvas charts
|
|
47
|
+
// could resolve to the wrong shape).
|
|
48
|
+
_drawPath(shape) {
|
|
49
|
+
const p = new Path2D();
|
|
50
|
+
switch (shape.type) {
|
|
51
|
+
case 'rect':
|
|
52
|
+
p.rect(shape.x, shape.y, shape.width, shape.height);
|
|
53
|
+
return p;
|
|
54
|
+
case 'circle':
|
|
55
|
+
p.arc(shape.cx, shape.cy, shape.r, 0, 2 * Math.PI);
|
|
56
|
+
return p;
|
|
57
|
+
case 'path':
|
|
58
|
+
return new Path2D(shape.d);
|
|
59
|
+
default:
|
|
60
|
+
return p; // empty — no geometry to hit-test
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
_setShapeType={
|
|
64
|
+
rect:(shape)=>this.ctx.rect(shape.x, shape.y, shape.width, shape.height),
|
|
65
|
+
circle:(shape)=>this.ctx.arc(shape.cx, shape.cy, shape.r, 0, 2 * Math.PI),
|
|
66
|
+
path:(shape,fill,stroke)=>{
|
|
67
|
+
const p = new Path2D(shape.d);
|
|
68
|
+
if (fill && fill !== 'none') { this.ctx.fillStyle = fill; this.ctx.fill(p); }
|
|
69
|
+
if (stroke && stroke.color !== 'none') {
|
|
70
|
+
this.ctx.strokeStyle = stroke.color;
|
|
71
|
+
this.ctx.lineWidth = stroke.width || 1;
|
|
72
|
+
// Used by the gauge arcs' rounded end-caps ({cap: 'round'}).
|
|
73
|
+
this.ctx.lineCap = stroke.cap || 'butt';
|
|
74
|
+
this.ctx.stroke(p);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Shared by text() (initial draw) and _draw()'s 'text' case (redraw on
|
|
79
|
+
// hover enter/leave) so rotation/alignment logic isn't duplicated.
|
|
80
|
+
_paintText(shape) {
|
|
81
|
+
const hoverStyle = shape._hoverStyle;
|
|
82
|
+
const fill = hoverStyle?.fill ?? shape.fill;
|
|
83
|
+
this.ctx.fillStyle = fill || 'black';
|
|
84
|
+
this.ctx.font = `${shape.fontStyle || ''} ${shape.fontWeight || ''} ${shape.fontSize || '12px'} ${shape.fontFamily || 'sans-serif'}`.trim().replace(/\s+/g, ' ');
|
|
85
|
+
let textAlign = shape.textAnchor || 'start';
|
|
86
|
+
if (textAlign === 'middle') textAlign = 'center';
|
|
87
|
+
this.ctx.textAlign = textAlign;
|
|
88
|
+
this.ctx.textBaseline = 'middle';
|
|
89
|
+
if (shape.rotation) {
|
|
90
|
+
this.ctx.save();
|
|
91
|
+
this.ctx.translate(shape.x, shape.y);
|
|
92
|
+
this.ctx.rotate(shape.rotation);
|
|
93
|
+
this.ctx.fillText(shape.text, 0, 0);
|
|
94
|
+
this.ctx.restore();
|
|
95
|
+
} else {
|
|
96
|
+
this.ctx.fillText(shape.text, shape.x, shape.y);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
_draw(shape) {
|
|
100
|
+
if (shape.type === 'text') { this._paintText(shape); return; }
|
|
101
|
+
|
|
102
|
+
const hoverStyle = shape._hoverStyle;
|
|
103
|
+
const fill = hoverStyle?.fill ?? shape.fill;
|
|
104
|
+
const stroke = hoverStyle?.stroke ?? shape.stroke;
|
|
105
|
+
|
|
106
|
+
// For path, we need to handle fill and stroke separately inside the case.
|
|
107
|
+
const isPath = shape.type === 'path';
|
|
108
|
+
|
|
109
|
+
this.ctx.beginPath();
|
|
110
|
+
this._setShapeType[shape.type](shape, fill, stroke);
|
|
111
|
+
if (isPath) return
|
|
112
|
+
if (fill && fill !== 'none') {
|
|
113
|
+
this.ctx.fillStyle = fill;
|
|
114
|
+
this.ctx.fill();
|
|
115
|
+
}
|
|
116
|
+
if (stroke) {
|
|
117
|
+
this.ctx.strokeStyle = stroke.color || 'none';
|
|
118
|
+
this.ctx.lineWidth = stroke.width || 1;
|
|
119
|
+
this.ctx.stroke();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_redraw() {
|
|
124
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
125
|
+
this._shapes.forEach(shape => this._draw(shape));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
rect(x, y, width, height, fill, stroke, extra = {}) {
|
|
129
|
+
const shape = { type: 'rect', x, y, width, height, fill, stroke, id: `shape_${this._shapes.length}`, ...extra };
|
|
130
|
+
this._shapes.push(shape);
|
|
131
|
+
this._draw(shape);
|
|
132
|
+
return shape;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
circle(cx, cy, r, fill, stroke, extra = {}) {
|
|
136
|
+
const shape = { type: 'circle', cx, cy, r, fill, stroke, id: `shape_${this._shapes.length}`, ...extra };
|
|
137
|
+
this._shapes.push(shape);
|
|
138
|
+
this._draw(shape);
|
|
139
|
+
return shape;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
path(d, fill, stroke, extra = {}) {
|
|
143
|
+
const shape = { type: 'path', d, fill, stroke, id: `shape_${this._shapes.length}`, ...extra };
|
|
144
|
+
this._shapes.push(shape);
|
|
145
|
+
this._draw(shape);
|
|
146
|
+
return shape;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Text width measured by this renderer's own 2D context. */
|
|
150
|
+
measureText(text, font = {}) {
|
|
151
|
+
if (!this.ctx?.measureText) return super.measureText(text, font);
|
|
152
|
+
this.ctx.save();
|
|
153
|
+
this.ctx.font = BaseRenderer.cssFont(font);
|
|
154
|
+
const width = this.ctx.measureText(String(text)).width;
|
|
155
|
+
this.ctx.restore();
|
|
156
|
+
return width;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
text(text, x, y, options = {}) {
|
|
160
|
+
const shape = { type: 'text', text, x, y, ...options, id: `shape_${this._shapes.length}` };
|
|
161
|
+
this._shapes.push(shape);
|
|
162
|
+
this._paintText(shape);
|
|
163
|
+
return shape;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
hitTest(x, y) {
|
|
167
|
+
for (let i = this._shapes.length - 1; i >= 0; i--) {
|
|
168
|
+
const shape = this._shapes[i];
|
|
169
|
+
const path2d = this._drawPath(shape);
|
|
170
|
+
|
|
171
|
+
// 1. Check for a fill hit first.
|
|
172
|
+
if (shape.fill && shape.fill !== 'none') {
|
|
173
|
+
if (path2d ? this.ctx.isPointInPath(path2d, x, y) : this.ctx.isPointInPath(x, y)) {
|
|
174
|
+
return shape;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 2. If no fill hit, check for a stroke hit.
|
|
179
|
+
if (shape.stroke && shape.stroke.color !== 'none') {
|
|
180
|
+
this.ctx.lineWidth = shape.stroke.width || 1;
|
|
181
|
+
if (path2d ? this.ctx.isPointInStroke(path2d, x, y) : this.ctx.isPointInStroke(x, y)) {
|
|
182
|
+
return shape;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
enterState(shape, stateName) {
|
|
190
|
+
const targetShape = this._shapes.find(s => s.id === shape.id);
|
|
191
|
+
if (targetShape) {
|
|
192
|
+
targetShape._hoverStyle = { fill: 'orange', stroke: { color: '#333', width: 2 } };
|
|
193
|
+
this._redraw();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
leaveState(shape, stateName) {
|
|
198
|
+
const targetShape = this._shapes.find(s => s.id === shape.id);
|
|
199
|
+
if (targetShape) {
|
|
200
|
+
delete targetShape._hoverStyle;
|
|
201
|
+
this._redraw();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
renderDiagram(diagram) {
|
|
206
|
+
// Draw the diagram directly onto the canvas context.
|
|
207
|
+
diagram.drawOn(this.ctx);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create a linear gradient on this canvas context.
|
|
212
|
+
* @param {string} _id
|
|
213
|
+
* @param {Array<{offset: string|number, color: string}>} stops
|
|
214
|
+
* @param {{x0?: number, y0?: number, x1?: number, y1?: number}} [coords]
|
|
215
|
+
* @returns {CanvasGradient|null}
|
|
216
|
+
*/
|
|
217
|
+
linearGradient(_id, stops = [], { x0 = 0, y0 = 0, x1 = 0, y1 = 100 } = {}) {
|
|
218
|
+
if (!this.ctx?.createLinearGradient) return null;
|
|
219
|
+
const grad = this.ctx.createLinearGradient(x0, y0, x1, y1);
|
|
220
|
+
for (const stop of stops) {
|
|
221
|
+
const offset = typeof stop.offset === 'number' ? stop.offset : (parseFloat(stop.offset) || 0) / 100;
|
|
222
|
+
grad.addColorStop(offset, stop.color);
|
|
223
|
+
}
|
|
224
|
+
return grad;
|
|
225
|
+
}
|
|
201
226
|
}
|