@peter.prib/renderengine 0.15.2 → 0.16.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/dist/renderengine-workspace.umd.js +6030 -3766
- package/dist/renderengine.umd.js +5930 -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 +137 -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
|
@@ -2907,6 +2907,7 @@ new RectangleTemplate('r1', 'Billing', { fontFamily: 'Georgia', fontSize: '14pt'
|
|
|
2907
2907
|
| Boxes | `RectangleTemplate`, `RoundedRectangleTemplate`, `StadiumTemplate`, `PredefinedProcessTemplate`, `ClassTemplate`, `ComponentTemplate`, `SwimlaneTemplate`, `PackageTemplate`, `BoxTemplate` (3D) |
|
|
2908
2908
|
| Round | `CircleTemplate`, `EllipseTemplate`, `FinalStateTemplate`, `CloudTemplate` |
|
|
2909
2909
|
| Polygons | `DiamondTemplate`, `TriangleTemplate`, `PentagonTemplate`, `HexagonTemplate`, `OctagonTemplate`, `StarTemplate`, `ParallelogramTemplate`, `TrapezoidTemplate`, `ManualInputTemplate` |
|
|
2910
|
+
| C4 Architecture | `C4PersonTemplate`, `C4SystemTemplate`, `C4ContainerTemplate`, `C4ComponentTemplate`, `C4BoundaryTemplate` |
|
|
2910
2911
|
| Other | `CylinderTemplate`, `DocumentTemplate`, `NoteTemplate`, `DataStoreTemplate`, `ActorTemplate`, `LifelineTemplate` |
|
|
2911
2912
|
|
|
2912
2913
|
A template draws its outline in `shapeElements(doc)`, or in `points()` when it extends
|
|
@@ -2939,6 +2940,19 @@ empty canvas, or on the shape at the other end, cancels. This is one undo step
|
|
|
2939
2940
|
|
|
2940
2941
|
**Container nesting and hierarchy.** Shapes such as `PackageTemplate`, `SwimlaneTemplate`, and `BoxTemplate` are containers (`isContainer = true`). When an element is dropped inside a container (`nestsOnDrop(parentId, childId)`), it is parented via `NestElementCommand` (setting `child.parentId`). Moving a container recursively moves all nested descendants (`moversFor(templateId)`). Containers can automatically expand their bounds to fit their contents via `fitNesting(changes, { padding })`. Ancestor loops are prevented (`isAncestor(childId, parentId)`).
|
|
2941
2942
|
|
|
2943
|
+
**C4 Model notation & architecture diagrams.** The `'c4-architecture'` category in `DIAGRAM_TYPES` (and `editor.setPaletteCategory('c4-architecture')`) provides first-class support for Simon Brown's C4 model across Context, Container, Component, and Code levels:
|
|
2944
|
+
- `C4PersonTemplate`: A human user, actor, persona, or stakeholder (`#08427b`, white text) with an integrated person silhouette badge and `[Person]` tagline.
|
|
2945
|
+
- `C4SystemTemplate`: Software system in focus (`#1168bd`, white text, `[Software System]`) or external software systems (`external: true`, `#999999`, `[Software System: External]`).
|
|
2946
|
+
- `C4ContainerTemplate`: Applications, web services, or microservices (`#438dd5`, white text, `[Container: <technology>]`), and database datastores (`containerType: 'database'`, cylinder icon).
|
|
2947
|
+
- `C4ComponentTemplate`: Modular components, controllers, and services (`#85bbf0`, dark text, `[Component: <technology>]`) with the UML component glyph.
|
|
2948
|
+
- `C4BoundaryTemplate`: Enterprise, system, or container scope boundaries (`isContainer = true`, dashed border `#707070`, bracketed header `[<boundaryType> Boundary]`) that enclose nested child elements.
|
|
2949
|
+
|
|
2950
|
+
```javascript
|
|
2951
|
+
const person = new C4PersonTemplate('p1', 'Customer', { description: 'Personal banking customer' });
|
|
2952
|
+
const system = new C4SystemTemplate('s1', 'Internet Banking System', { description: 'Allows customers to view accounts and make payments' });
|
|
2953
|
+
const boundary = new C4BoundaryTemplate('b1', 'Big Bank plc', { boundaryType: 'Enterprise' });
|
|
2954
|
+
```
|
|
2955
|
+
|
|
2942
2956
|
**Line style levels.** A line's style resolves through `EdgeStyleCascade`: the line itself,
|
|
2943
2957
|
then each level in `editor.edgeStyleLevels` (most specific first, `[{ key, label, style }]`),
|
|
2944
2958
|
then the schema default. A value a level doesn't set is inherited. `EDGE_STYLE_PROPERTIES` is the
|
|
@@ -3506,7 +3520,7 @@ drawn with this library's charts. It reads decisionengine's JSON, not decisionen
|
|
|
3506
3520
|
document
|
|
3507
3521
|
|
|
3508
3522
|
```js
|
|
3509
|
-
{ kind: 'sadis-workbench', report, plan?, before? } // report.toJSON(), plan.toJSON(), an earlier report.toJSON()
|
|
3523
|
+
{ kind: 'sadis-workbench', report, plan?, before?, graph? } // report.toJSON(), plan.toJSON(), an earlier report.toJSON(), the scored graph
|
|
3510
3524
|
```
|
|
3511
3525
|
|
|
3512
3526
|
which decisionengine's SADIS Explorer downloads ("Download for the Decision Workbench").
|
|
@@ -3525,6 +3539,7 @@ for (const view of workbench.views()) {
|
|
|
3525
3539
|
|---|---|---|
|
|
3526
3540
|
| Executive | a report | `kpi-tile` (what to build next; half the value delivered by), `stackedBar` (score part by part), `line` (value delivered by period) |
|
|
3527
3541
|
| Architecture | a report | `scatter` (visible rating against basin position: foundations sit high and left), `lollipop` (share each frees) |
|
|
3542
|
+
| Explore | the scored graph | `graph` diagram of the first candidate's neighbourhood: its ways up to each objective it reaches (shortest, along contributions) and what waits on it (depends on it, or is blocked by it); notes spell each way out. Walked with `NavigatorModel.forGraph` |
|
|
3528
3543
|
| Delivery | a plan | `gantt` (the plan on a calendar), `stackedBar` (capacity used per period, per team when planned by team) |
|
|
3529
3544
|
| Portfolio | a report with perspectives | `heatmap` (rank by perspective), `dumbbell` (best and worst rank, most contested first) |
|
|
3530
3545
|
| Replay | an earlier report | `slopegraph` (rank then → now); notes say what entered, left (and why) and moved |
|
|
@@ -3682,7 +3697,7 @@ nav.back(); // returns to 'billing-app'
|
|
|
3682
3697
|
const paths = nav.findPaths('billing-app', 'ledger-db', 5);
|
|
3683
3698
|
|
|
3684
3699
|
// Trace transitive downstream or upstream impact:
|
|
3685
|
-
const impact = nav.
|
|
3700
|
+
const impact = nav.traceImpact('billing-app', { maxDepth: 4, direction: 'outgoing' });
|
|
3686
3701
|
|
|
3687
3702
|
// 2. Build or render the Navigator UI panel:
|
|
3688
3703
|
const panel = new NavigatorPanel({
|
|
@@ -3697,7 +3712,26 @@ const panel = new NavigatorPanel({
|
|
|
3697
3712
|
- **Cycle detection and path tracking:** Tracks visited nodes along active branches to prevent infinite recursion in cyclic relationship graphs, annotating cycles in tree outputs.
|
|
3698
3713
|
- **Directional filters:** Traversal can follow outgoing relationships, incoming relationships, or bidirectional connections (`setDirection('outgoing' | 'incoming' | 'both')`).
|
|
3699
3714
|
- **History stack:** Maintains full forward and backward navigation stacks (`focus(id)`, `back()`, `forward()`, `canGoBack`, `canGoForward`).
|
|
3700
|
-
- **Impact and path finding:** Built-in BFS/DFS algorithms find connecting pathways (`findPaths(fromId, toId, maxDepth)`) and downstream dependency cascades (`
|
|
3715
|
+
- **Impact and path finding:** Built-in BFS/DFS algorithms find connecting pathways (`findPaths(fromId, toId, maxDepth)`) and downstream dependency cascades (`traceImpact(elementId, { maxDepth, direction })`).
|
|
3716
|
+
|
|
3717
|
+
|
|
3718
|
+
**Any graph, not only ArchiMate.** `NavigatorModel` reads its graph only through four questions —
|
|
3719
|
+
`getElement(id)`, `relationshipsFor(id)` (`{ incoming, outgoing }`, each `{ id, source, target, kind }`),
|
|
3720
|
+
`viewsContaining(id)` and `layerOf(element)` — the `GraphSource` interface
|
|
3721
|
+
(`src/charting/graph/GraphSource.js`). `ArchimateModel` answers them already (its layers from
|
|
3722
|
+
`treeLayerOf`); `DocumentGraph` answers them for a graph document, `{ nodes: [{ id, labels, props }],
|
|
3723
|
+
edges: [{ from, to, type, props }] }` — sqlengine's graphs and decisionengine's portfolios — a node's type its
|
|
3724
|
+
first label, its layer its type:
|
|
3725
|
+
|
|
3726
|
+
```javascript
|
|
3727
|
+
import { NavigatorModel, DocumentGraph } from '@peter.prib/renderengine';
|
|
3728
|
+
const nav = NavigatorModel.forGraph(graphDocument, { rootId: 'cap-semantic-registry' });
|
|
3729
|
+
nav.findPaths('cap-semantic-registry', 'obj-trusted-data', { direction: 'outgoing', allowedKinds: ['ENABLES', 'SUPPORTS'] });
|
|
3730
|
+
nav.traceImpact('cap-semantic-registry', { direction: 'incoming', allowedKinds: ['DEPENDS_ON'] });
|
|
3731
|
+
```
|
|
3732
|
+
|
|
3733
|
+
Why an interface rather than a second walker: traversal is the same for every graph; only these reads
|
|
3734
|
+
differ, so each kind of graph answers them and the one walker (paths, impact, history, the tree) serves all.
|
|
3701
3735
|
|
|
3702
3736
|
## Templated Reports (`TemplatedReport`)
|
|
3703
3737
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peter.prib/renderengine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ArchimateNotation } from './archimate.js';
|
|
2
2
|
import { archiElementType, typeProfileId, typeDisplayName, ARCHI_TYPE_FEATURE, ARCHI_TYPE_PROFILE_PREFIX } from './archimateArchiTypes.js';
|
|
3
|
-
import { ArchimateModel } from './charting/diagrams/ArchimateModel.js';
|
|
3
|
+
import { ArchimateModel } from './charting/diagrams/ArchimateModel.js';
|
|
4
|
+
import { ArchimateSpecializationRegistry } from './archimateSpecializations.js';
|
|
4
5
|
import './charting/diagrams/CanvasView.js'; // registers the canvas view type
|
|
5
6
|
import './charting/diagrams/SketchView.js'; // registers the sketch view type
|
|
6
7
|
import { ElementInstance } from './charting/diagrams/ElementInstance.js';
|
|
@@ -720,7 +721,39 @@ function _childText(el, tagName) {
|
|
|
720
721
|
* @param {Object} archiModel - parsed Archi XML model
|
|
721
722
|
* @returns {ArchimateModel} with one view per original view (elementIds track which elements appear in each view)
|
|
722
723
|
* Now applies visual bounds/waypoints per view. */
|
|
723
|
-
|
|
724
|
+
/** Register any model-level Archi specializations (<profile>) with ArchimateSpecializationRegistry. @private */
|
|
725
|
+
function _registerModelProfiles(archiModel) {
|
|
726
|
+
const profilesById = new Map();
|
|
727
|
+
for (const e of archiModel?.archi?.elements || []) {
|
|
728
|
+
if (e.tag === 'profile' && !String(e.attributes?.id ?? '').startsWith(ARCHI_TYPE_PROFILE_PREFIX)) {
|
|
729
|
+
const id = e.attributes?.id || e.attributes?.name;
|
|
730
|
+
const name = e.attributes?.name || e.attributes?.id;
|
|
731
|
+
const conceptType = e.attributes?.conceptType;
|
|
732
|
+
const imagePath = e.attributes?.imagePath;
|
|
733
|
+
const archiImagePath = e.attributes?.archiImagePath;
|
|
734
|
+
const profileDef = {
|
|
735
|
+
id,
|
|
736
|
+
name,
|
|
737
|
+
conceptType,
|
|
738
|
+
baseTypes: conceptType ? [_toKebabType(conceptType)] : [],
|
|
739
|
+
iconUrl: imagePath || undefined,
|
|
740
|
+
archiImagePath,
|
|
741
|
+
};
|
|
742
|
+
if (id) profilesById.set(id, profileDef);
|
|
743
|
+
if (name) profilesById.set(name, profileDef);
|
|
744
|
+
ArchimateSpecializationRegistry.register({
|
|
745
|
+
id,
|
|
746
|
+
name,
|
|
747
|
+
baseTypes: profileDef.baseTypes,
|
|
748
|
+
iconUrl: profileDef.iconUrl,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return profilesById;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export function toArchimateModel(archiModel) {
|
|
756
|
+
const profilesById = _registerModelProfiles(archiModel);
|
|
724
757
|
const elements = [];
|
|
725
758
|
const relationships = [];
|
|
726
759
|
const folders = [];
|
|
@@ -731,7 +764,7 @@ export function toArchimateModel(archiModel) {
|
|
|
731
764
|
|
|
732
765
|
// Map all elements first
|
|
733
766
|
for (const el of archiModel.elements || []) {
|
|
734
|
-
const mapped = _mapElement(el);
|
|
767
|
+
const mapped = _mapElement(el, profilesById);
|
|
735
768
|
elementById.set(el.id, mapped);
|
|
736
769
|
elements.push(mapped);
|
|
737
770
|
}
|
|
@@ -978,7 +1011,8 @@ function _viewInstances(archiView, { elementById, relationshipById }) {
|
|
|
978
1011
|
* Returns `{ elements, relationships, options }` ready for `archimateDiagram()`.
|
|
979
1012
|
* If `viewId` is omitted, the first view is used.
|
|
980
1013
|
* Kept as original for backward compatibility: applies visual bounds/features and handles all view visuals. */
|
|
981
|
-
export function toRenderEngineModel(archiModel, viewId) {
|
|
1014
|
+
export function toRenderEngineModel(archiModel, viewId) {
|
|
1015
|
+
const profilesById = _registerModelProfiles(archiModel);
|
|
982
1016
|
const view = viewId
|
|
983
1017
|
? archiModel.views.find(v => v.id === viewId)
|
|
984
1018
|
: archiModel.views[0];
|
|
@@ -990,7 +1024,7 @@ export function toRenderEngineModel(archiModel, viewId) {
|
|
|
990
1024
|
const relationshipById = new Map();
|
|
991
1025
|
|
|
992
1026
|
for (const el of archiModel.elements || []) {
|
|
993
|
-
const mapped = _mapElement(el);
|
|
1027
|
+
const mapped = _mapElement(el, profilesById);
|
|
994
1028
|
elementById.set(el.id, mapped);
|
|
995
1029
|
elements.push(mapped);
|
|
996
1030
|
}
|
|
@@ -1020,7 +1054,7 @@ export function toRenderEngineModel(archiModel, viewId) {
|
|
|
1020
1054
|
return { elements, relationships, folders, folderLayout, elementFolders, options };
|
|
1021
1055
|
}
|
|
1022
1056
|
|
|
1023
|
-
function _mapElement(el) {
|
|
1057
|
+
function _mapElement(el, profilesById = null) {
|
|
1024
1058
|
const archiType = _toKebabType(el.type);
|
|
1025
1059
|
const layer = ArchimateNotation.elementLayer({ type: archiType });
|
|
1026
1060
|
// A type Archi has no class for was written as the nearest one, with its own kept as a feature.
|
|
@@ -1032,6 +1066,21 @@ function _mapElement(el) {
|
|
|
1032
1066
|
layer: layer !== 'other' ? layer : undefined,
|
|
1033
1067
|
documentation: el.documentation || undefined,
|
|
1034
1068
|
};
|
|
1069
|
+
const profileAttr = el.archi?.attributes?.profiles || el.attributes?.profiles;
|
|
1070
|
+
if (profileAttr && !profileAttr.startsWith(ARCHI_TYPE_PROFILE_PREFIX)) {
|
|
1071
|
+
const firstId = profileAttr.trim().split(/\s+/)[0];
|
|
1072
|
+
const profileDef = profilesById?.get(firstId) || ArchimateSpecializationRegistry.resolve(firstId);
|
|
1073
|
+
mapped.specialization = profileDef?.name || firstId;
|
|
1074
|
+
if (profileDef?.iconUrl && !mapped.iconUrl) {
|
|
1075
|
+
mapped.iconUrl = profileDef.iconUrl;
|
|
1076
|
+
}
|
|
1077
|
+
} else if (el.specialization) {
|
|
1078
|
+
mapped.specialization = el.specialization;
|
|
1079
|
+
const spec = ArchimateSpecializationRegistry.resolve(el.specialization);
|
|
1080
|
+
if (spec?.iconUrl && !mapped.iconUrl) {
|
|
1081
|
+
mapped.iconUrl = spec.iconUrl;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1035
1084
|
const props = _propertiesToTypedArray(el.properties);
|
|
1036
1085
|
if (props) mapped.properties = props;
|
|
1037
1086
|
if (el.archiFolder) mapped.archiFolder = el.archiFolder;
|
|
@@ -1388,15 +1437,56 @@ export function toArchiExport(model) {
|
|
|
1388
1437
|
archi: view.archi,
|
|
1389
1438
|
children: fromArchimateView(view),
|
|
1390
1439
|
}));
|
|
1440
|
+
const archiElements = [...(model.archi?.elements ?? [])];
|
|
1441
|
+
const elements = model.elements().map(el => {
|
|
1442
|
+
if (!el.specialization) return el;
|
|
1443
|
+
const spec = ArchimateSpecializationRegistry.resolve(el.specialization);
|
|
1444
|
+
if (!spec) return el;
|
|
1445
|
+
const specId = spec.id.startsWith('id-') ? spec.id : `id-prof-${spec.id}`;
|
|
1446
|
+
const existing = archiElements.find(e => e.tag === 'profile' && (e.attributes?.id === specId || e.attributes?.name === spec.name));
|
|
1447
|
+
if (!existing) {
|
|
1448
|
+
archiElements.push({
|
|
1449
|
+
tag: 'profile',
|
|
1450
|
+
attributes: {
|
|
1451
|
+
id: specId,
|
|
1452
|
+
name: spec.name,
|
|
1453
|
+
conceptType: _toPascalType(el.type),
|
|
1454
|
+
...(spec.iconUrl && { imagePath: spec.iconUrl }),
|
|
1455
|
+
},
|
|
1456
|
+
});
|
|
1457
|
+
} else if (spec.iconUrl && !existing.attributes?.imagePath) {
|
|
1458
|
+
existing.attributes = { ...existing.attributes, imagePath: spec.iconUrl };
|
|
1459
|
+
}
|
|
1460
|
+
if (!el.archi?.attributes?.profiles) {
|
|
1461
|
+
return {
|
|
1462
|
+
...el,
|
|
1463
|
+
archi: {
|
|
1464
|
+
...(el.archi ?? {}),
|
|
1465
|
+
attributes: {
|
|
1466
|
+
...(el.archi?.attributes ?? {}),
|
|
1467
|
+
profiles: specId,
|
|
1468
|
+
},
|
|
1469
|
+
},
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
return el;
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
const archi = (model.archi || archiElements.length) ? {
|
|
1476
|
+
...(model.archi ?? {}),
|
|
1477
|
+
attributes: { ...(model.archi?.attributes ?? {}) },
|
|
1478
|
+
elements: archiElements,
|
|
1479
|
+
} : null;
|
|
1480
|
+
|
|
1391
1481
|
return [{
|
|
1392
1482
|
id: model.id || 'model-1',
|
|
1393
1483
|
name: model.name || 'ArchiMate Model',
|
|
1394
1484
|
version: '5.0.0',
|
|
1395
1485
|
documentation: model.documentation,
|
|
1396
1486
|
properties: model.properties,
|
|
1397
|
-
archi
|
|
1487
|
+
archi,
|
|
1398
1488
|
emptyViewFolders: model.emptyViewFolders ?? [],
|
|
1399
|
-
elements
|
|
1489
|
+
elements,
|
|
1400
1490
|
relationships: model.relationships(),
|
|
1401
1491
|
}, views];
|
|
1402
1492
|
}
|
|
@@ -1510,6 +1600,20 @@ export function toArchiXML(model, views) {
|
|
|
1510
1600
|
if (original && !el.archi?.attributes?.profiles) {
|
|
1511
1601
|
profile = typeProfileId(original);
|
|
1512
1602
|
typeProfiles.set(profile, { name: typeDisplayName(original), conceptType: _toPascalType(type) });
|
|
1603
|
+
} else if (el.specialization && !el.archi?.attributes?.profiles) {
|
|
1604
|
+
const spec = ArchimateSpecializationRegistry.resolve(el.specialization);
|
|
1605
|
+
if (spec) {
|
|
1606
|
+
const specId = spec.id.startsWith('id-') ? spec.id : ('id-prof-' + spec.id);
|
|
1607
|
+
profile = specId;
|
|
1608
|
+
const existing = (model.archi?.elements ?? []).find(e => e.tag === 'profile' && (e.attributes?.id === specId || e.attributes?.name === spec.name));
|
|
1609
|
+
if (!existing && !typeProfiles.has(specId)) {
|
|
1610
|
+
typeProfiles.set(specId, {
|
|
1611
|
+
name: spec.name,
|
|
1612
|
+
conceptType: _toPascalType(type),
|
|
1613
|
+
...(spec.iconUrl && { imagePath: spec.iconUrl }),
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1513
1617
|
}
|
|
1514
1618
|
byFolder.get(folder.type).push({ el, type, original, profile });
|
|
1515
1619
|
}
|
|
@@ -1540,8 +1644,8 @@ export function toArchiXML(model, views) {
|
|
|
1540
1644
|
|
|
1541
1645
|
// What else the model carried (profiles, ...), its own properties, then its documentation (Archi's "purpose").
|
|
1542
1646
|
lines.push(..._extraElementLines(model.archi?.elements, ' '));
|
|
1543
|
-
for (const [id, { name, conceptType }] of typeProfiles) {
|
|
1544
|
-
lines.push(` <profile name="${_escapeAttr(name)}" id="${id}" conceptType="${conceptType}"/>`);
|
|
1647
|
+
for (const [id, { name, conceptType, imagePath }] of typeProfiles) {
|
|
1648
|
+
lines.push(` <profile name="${_escapeAttr(name)}" id="${id}" conceptType="${conceptType}"${imagePath ? ` imagePath="${_escapeAttr(imagePath)}"` : ''}/>`);
|
|
1545
1649
|
}
|
|
1546
1650
|
lines.push(..._propertyLines(model.properties, ' '));
|
|
1547
1651
|
if (model.documentation) lines.push(` <purpose>${_escapeXml(model.documentation)}</purpose>`);
|
package/src/archimateArchive.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { parseArchiModel, toArchiXML } from './archimateArchiFormat.js';
|
|
1
|
+
import { parseArchiModel, toArchiXML, toPascalType } from './archimateArchiFormat.js';
|
|
2
|
+
import { ArchimateSpecializationRegistry } from './archimateSpecializations.js';
|
|
2
3
|
import { ZipArchive } from './zip/ZipArchive.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -95,15 +96,37 @@ export function toArchiFile(model, views) {
|
|
|
95
96
|
_eachVisual(copies, (visual) => registerPicture(visual));
|
|
96
97
|
|
|
97
98
|
let modelForXml = model;
|
|
98
|
-
|
|
99
|
+
let archiElements = [...(model?.archi?.elements || [])];
|
|
100
|
+
for (const el of model?.elements || []) {
|
|
101
|
+
if (el.specialization) {
|
|
102
|
+
const spec = ArchimateSpecializationRegistry.resolve(el.specialization);
|
|
103
|
+
if (spec?.iconUrl?.startsWith('data:')) {
|
|
104
|
+
const specId = spec.id.startsWith('id-') ? spec.id : ('id-prof-' + spec.id);
|
|
105
|
+
if (!archiElements.some(e => e.tag === 'profile' && (e.attributes?.id === specId || e.attributes?.name === spec.name))) {
|
|
106
|
+
archiElements.push({
|
|
107
|
+
tag: 'profile',
|
|
108
|
+
attributes: {
|
|
109
|
+
id: specId,
|
|
110
|
+
name: spec.name,
|
|
111
|
+
conceptType: toPascalType(el.type),
|
|
112
|
+
imagePath: spec.iconUrl,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (archiElements.some(e => e.attributes?.imagePath?.startsWith('data:'))) {
|
|
99
120
|
modelForXml = {
|
|
100
121
|
...model,
|
|
101
122
|
archi: {
|
|
102
|
-
...model
|
|
103
|
-
|
|
123
|
+
...(model?.archi ?? {}),
|
|
124
|
+
attributes: { ...(model?.archi?.attributes ?? {}) },
|
|
125
|
+
elements: archiElements.map(e => {
|
|
104
126
|
if (!e.attributes?.imagePath?.startsWith('data:')) return e;
|
|
105
127
|
const clonedAttr = { ...e.attributes };
|
|
106
128
|
registerPicture(clonedAttr);
|
|
129
|
+
delete clonedAttr.archiImagePath;
|
|
107
130
|
return { ...e, attributes: clonedAttr };
|
|
108
131
|
}),
|
|
109
132
|
},
|
|
@@ -144,12 +144,20 @@ function _organizationPaths(root) {
|
|
|
144
144
|
return { paths, emptyViewFolders };
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
function _asPropertyPairs(pairs) {
|
|
148
|
+
const list = pairs.map(p => ({ ...p }));
|
|
149
|
+
for (const { key, value } of pairs) {
|
|
150
|
+
if (!(key in list)) list[key] = value;
|
|
151
|
+
}
|
|
152
|
+
return list;
|
|
153
|
+
}
|
|
154
|
+
|
|
147
155
|
/** An element as parseArchiModel gives it (type: Archi's class name). @private */
|
|
148
156
|
function _parseElement(el, propertyPairs) {
|
|
149
157
|
const xsiType = _xsiType(el);
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
158
|
+
const ownType = propertyPairs.find(p => p.key === EXCHANGE_TYPE_PROPERTY)?.value;
|
|
159
|
+
const filteredPairs = propertyPairs.filter(p => p.key !== EXCHANGE_TYPE_PROPERTY);
|
|
160
|
+
const properties = _asPropertyPairs(filteredPairs);
|
|
153
161
|
const attributes = {};
|
|
154
162
|
const elements = [];
|
|
155
163
|
if (JUNCTION_TYPES[xsiType]) attributes.type = 'or';
|
|
@@ -180,7 +188,7 @@ function _parseRelationship(el, propertyPairs) {
|
|
|
180
188
|
target: el.getAttribute('target'),
|
|
181
189
|
...(el.getAttribute('isDirected') === 'true' && { directed: true }),
|
|
182
190
|
documentation: _text(el, 'documentation'),
|
|
183
|
-
properties:
|
|
191
|
+
properties: _asPropertyPairs(propertyPairs),
|
|
184
192
|
...(Object.keys(attributes).length && { archi: { attributes, elements: [] } }),
|
|
185
193
|
};
|
|
186
194
|
}
|
|
@@ -99,6 +99,12 @@ export class ArchimateSpecializationRegistry {
|
|
|
99
99
|
/** Register or replace a specialization. */
|
|
100
100
|
static register(spec) {
|
|
101
101
|
const s = new ArchimateSpecialization(spec);
|
|
102
|
+
const existingByName = this.list().find(e => e.name === s.name && e.id !== s.id);
|
|
103
|
+
if (existingByName) {
|
|
104
|
+
if (s.iconUrl && !existingByName.iconUrl) existingByName.iconUrl = s.iconUrl;
|
|
105
|
+
if (s.icon && !existingByName.icon) existingByName.icon = s.icon;
|
|
106
|
+
if (s.color && !existingByName.color) existingByName.color = s.color;
|
|
107
|
+
}
|
|
102
108
|
this._store.set(s.id, s);
|
|
103
109
|
return s;
|
|
104
110
|
}
|
|
@@ -116,7 +122,11 @@ export class ArchimateSpecializationRegistry {
|
|
|
116
122
|
/** Look up by id or by exact name. */
|
|
117
123
|
static resolve(idOrName) {
|
|
118
124
|
if (!idOrName) return undefined;
|
|
119
|
-
|
|
125
|
+
const byId = this._store.get(idOrName);
|
|
126
|
+
if (byId) return byId;
|
|
127
|
+
const matches = this.list().filter(s => s.name === idOrName);
|
|
128
|
+
if (!matches.length) return undefined;
|
|
129
|
+
return matches.find(s => s.iconUrl || s.icon) || matches[0];
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
/** All registered specializations. */
|
package/src/chartCatalog.js
CHANGED
|
@@ -91,12 +91,13 @@ export const CAMERA_MODE = {
|
|
|
91
91
|
export const CHART_CATALOG = [
|
|
92
92
|
{
|
|
93
93
|
key: 'bar', label: 'Bar', method: 'barChart', category: '2d',
|
|
94
|
-
coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.
|
|
94
|
+
coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.GROUPED_FIELD, camera: CAMERA_MODE.NONE,
|
|
95
95
|
axes: [
|
|
96
96
|
{ role: 'category', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.POSITION_X, required: true },
|
|
97
97
|
{ role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.POSITION_Y_LENGTH, required: true },
|
|
98
|
+
{ role: 'group', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.COLOR, required: false },
|
|
98
99
|
],
|
|
99
|
-
dataShape: 'Array<{label, value}>',
|
|
100
|
+
dataShape: 'Array<{label, value}> — optional `colorCol` (field name or compiled formula) colors bars by group',
|
|
100
101
|
streamShape: 'flat',
|
|
101
102
|
},
|
|
102
103
|
{
|
|
@@ -609,14 +610,15 @@ export const CHART_CATALOG = [
|
|
|
609
610
|
},
|
|
610
611
|
{
|
|
611
612
|
key: 'lollipop', label: 'Lollipop', method: 'lollipopChart', category: '2d',
|
|
612
|
-
coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.
|
|
613
|
+
coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.GROUPED_FIELD, camera: CAMERA_MODE.NONE,
|
|
613
614
|
seriesCapable: true,
|
|
614
615
|
axes: [
|
|
615
616
|
{ role: 'category', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.POSITION_X, required: true },
|
|
616
617
|
{ role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.POSITION_Y, required: true },
|
|
617
618
|
{ role: 'series', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.COLOR, required: false },
|
|
619
|
+
{ role: 'group', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.COLOR, required: false },
|
|
618
620
|
],
|
|
619
|
-
dataShape: 'Array<{label, value}> or multi-series Array<{name, values:Array<{label,value}>}> — stem-and-dot alternative to bars',
|
|
621
|
+
dataShape: 'Array<{label, value}> or multi-series Array<{name, values:Array<{label,value}>}> — stem-and-dot alternative to bars; `colorCol` supported on 1D data',
|
|
620
622
|
streamShape: 'flat',
|
|
621
623
|
},
|
|
622
624
|
{
|
|
@@ -141,11 +141,6 @@ export class ArchimateDiagramEditor extends DiagramEditor {
|
|
|
141
141
|
|
|
142
142
|
// Enable tree drag-drop (delayed to allow DOM to be ready)
|
|
143
143
|
setTimeout(() => this.enableTreeDragDrop(), 100);
|
|
144
|
-
|
|
145
|
-
// Wire controls if not using auto-toolbar
|
|
146
|
-
if (!this.features) {
|
|
147
|
-
this.wireControls();
|
|
148
|
-
}
|
|
149
144
|
}
|
|
150
145
|
|
|
151
146
|
/**
|
|
@@ -1366,19 +1361,10 @@ export class ArchimateDiagramEditor extends DiagramEditor {
|
|
|
1366
1361
|
}
|
|
1367
1362
|
|
|
1368
1363
|
/**
|
|
1369
|
-
* Get a control value
|
|
1364
|
+
* Get a control value.
|
|
1370
1365
|
*/
|
|
1371
1366
|
getOption(controlId, defaultValue) {
|
|
1372
|
-
|
|
1373
|
-
// Hosts that bring their own controls (wireControls) keep them in the DOM.
|
|
1374
|
-
try {
|
|
1375
|
-
const el = this.document.getElementById(controlId);
|
|
1376
|
-
if (!el) return defaultValue;
|
|
1377
|
-
if (el.type === 'checkbox') return el.checked;
|
|
1378
|
-
return el.value || defaultValue;
|
|
1379
|
-
} catch {
|
|
1380
|
-
return defaultValue;
|
|
1381
|
-
}
|
|
1367
|
+
return super.getOption(controlId, defaultValue);
|
|
1382
1368
|
}
|
|
1383
1369
|
|
|
1384
1370
|
/** Save specializations to localStorage. */
|
|
@@ -1572,98 +1558,9 @@ export class ArchimateDiagramEditor extends DiagramEditor {
|
|
|
1572
1558
|
}
|
|
1573
1559
|
|
|
1574
1560
|
/**
|
|
1575
|
-
*
|
|
1576
|
-
* Triggers render on change. Optional: provide custom handler functions.
|
|
1561
|
+
* @deprecated Legacy control wiring. Controls are now auto-rendered into the toolbar via `features`.
|
|
1577
1562
|
*/
|
|
1578
|
-
wireControls(
|
|
1579
|
-
|
|
1580
|
-
onVersionChange,
|
|
1581
|
-
onViewpointChange,
|
|
1582
|
-
onViewpointModeChange,
|
|
1583
|
-
onDistinctShapesChange,
|
|
1584
|
-
onShowDerivedChange,
|
|
1585
|
-
onManageViewpoints,
|
|
1586
|
-
onManageSpecializations,
|
|
1587
|
-
populateViewpointSelect,
|
|
1588
|
-
} = options;
|
|
1589
|
-
|
|
1590
|
-
// Version selector
|
|
1591
|
-
const versionEl = this.document.getElementById('archimate-version');
|
|
1592
|
-
if (versionEl) {
|
|
1593
|
-
versionEl.addEventListener('change', () => {
|
|
1594
|
-
if (onVersionChange) onVersionChange();
|
|
1595
|
-
this.render();
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
// Viewpoint selector and mode
|
|
1600
|
-
const viewpointEl = this.document.getElementById('archimate-viewpoint');
|
|
1601
|
-
const viewpointModeEl = this.document.getElementById('archimate-viewpoint-mode');
|
|
1602
|
-
if (viewpointEl) {
|
|
1603
|
-
viewpointEl.addEventListener('change', () => {
|
|
1604
|
-
if (onViewpointChange) onViewpointChange();
|
|
1605
|
-
this.render();
|
|
1606
|
-
});
|
|
1607
|
-
}
|
|
1608
|
-
if (viewpointModeEl) {
|
|
1609
|
-
viewpointModeEl.addEventListener('change', () => {
|
|
1610
|
-
if (onViewpointModeChange) onViewpointModeChange();
|
|
1611
|
-
this.render();
|
|
1612
|
-
});
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
// Distinct shapes checkbox
|
|
1616
|
-
const distinctShapesEl = this.document.getElementById('archimate-distinct-shapes');
|
|
1617
|
-
if (distinctShapesEl) {
|
|
1618
|
-
distinctShapesEl.addEventListener('change', () => {
|
|
1619
|
-
if (onDistinctShapesChange) onDistinctShapesChange();
|
|
1620
|
-
this.render();
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
// Show derived relationships checkbox
|
|
1625
|
-
const showDerivedEl = this.document.getElementById('archimate-show-derived');
|
|
1626
|
-
if (showDerivedEl) {
|
|
1627
|
-
showDerivedEl.addEventListener('change', () => {
|
|
1628
|
-
if (onShowDerivedChange) onShowDerivedChange();
|
|
1629
|
-
this.render();
|
|
1630
|
-
});
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
// Manage viewpoints dialog
|
|
1634
|
-
const manageVpEl = this.document.getElementById('archimate-manage-viewpoints');
|
|
1635
|
-
if (manageVpEl && onManageViewpoints) {
|
|
1636
|
-
manageVpEl.addEventListener('click', onManageViewpoints);
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
// Manage specializations dialog
|
|
1640
|
-
const manageSpecEl = this.document.getElementById('archimate-manage-specializations');
|
|
1641
|
-
if (manageSpecEl && onManageSpecializations) {
|
|
1642
|
-
manageSpecEl.addEventListener('click', onManageSpecializations);
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
// Settings dialog
|
|
1646
|
-
const settingsEl = this.document.getElementById('archimate-settings');
|
|
1647
|
-
if (settingsEl) {
|
|
1648
|
-
settingsEl.addEventListener('click', () => {
|
|
1649
|
-
this.workspace.showGlobalSettingsDialog(this.document, this.settingsManager, 'default');
|
|
1650
|
-
});
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
// Filter controls
|
|
1654
|
-
const filterInput = this.document.getElementById('archimate-filter');
|
|
1655
|
-
const filterClearEl = this.document.getElementById('archimate-filter-clear');
|
|
1656
|
-
if (filterInput && this.currentEngine) {
|
|
1657
|
-
const applyFilter = () => {
|
|
1658
|
-
this.currentEngine.filterGraphNodes(filterInput.value);
|
|
1659
|
-
};
|
|
1660
|
-
filterInput.addEventListener('input', applyFilter);
|
|
1661
|
-
if (filterClearEl) {
|
|
1662
|
-
filterClearEl.addEventListener('click', () => {
|
|
1663
|
-
filterInput.value = '';
|
|
1664
|
-
applyFilter();
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1563
|
+
wireControls(_options = {}) {
|
|
1564
|
+
// No-op: toolbar and controls are managed via features and MenuBar
|
|
1668
1565
|
}
|
|
1669
1566
|
}
|
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
EllipseTemplate, DiamondTemplate, TriangleTemplate, PentagonTemplate, HexagonTemplate,
|
|
13
13
|
OctagonTemplate, StarTemplate, ParallelogramTemplate, TrapezoidTemplate, ManualInputTemplate,
|
|
14
14
|
CylinderTemplate, DocumentTemplate, NoteTemplate, CloudTemplate, ActorTemplate,
|
|
15
|
-
PackageTemplate, BoxTemplate, DataStoreTemplate, LifelineTemplate,
|
|
15
|
+
PackageTemplate, BoxTemplate, DataStoreTemplate, LifelineTemplate,
|
|
16
|
+
C4PersonTemplate, C4SystemTemplate, C4ContainerTemplate, C4ComponentTemplate, C4BoundaryTemplate,
|
|
16
17
|
} from './templates/index.js';
|
|
17
18
|
|
|
18
19
|
const C = {
|
|
@@ -202,6 +203,24 @@ export const DIAGRAM_TYPES = {
|
|
|
202
203
|
],
|
|
203
204
|
},
|
|
204
205
|
|
|
206
|
+
'c4-architecture': {
|
|
207
|
+
name: 'C4 Architecture',
|
|
208
|
+
group: 'Architecture',
|
|
209
|
+
description: 'Hierarchical software architecture: Person, System, Container, Component, and Boundaries',
|
|
210
|
+
templates: [
|
|
211
|
+
shape('c4-person', 'Person', C4PersonTemplate, { fillColor: '#08427b', textColor: '#ffffff' }, 'A human user, actor, persona, or stakeholder interacting with systems'),
|
|
212
|
+
shape('c4-system', 'Software System', C4SystemTemplate, { fillColor: '#1168bd', textColor: '#ffffff' }, 'The software system in focus delivering business value'),
|
|
213
|
+
shape('c4-system-external', 'External System', C4SystemTemplate, { external: true, fillColor: '#999999', textColor: '#ffffff' }, 'An external software system outside the boundary of change'),
|
|
214
|
+
shape('c4-container', 'Container', C4ContainerTemplate, { fillColor: '#438dd5', textColor: '#ffffff' }, 'An application, web service, mobile app, or microservice'),
|
|
215
|
+
shape('c4-database', 'Database Container', C4ContainerTemplate, { containerType: 'database', fillColor: '#438dd5', textColor: '#ffffff' }, 'A database, persistence store, cache, or datastore container'),
|
|
216
|
+
shape('c4-component', 'Component', C4ComponentTemplate, { fillColor: '#85bbf0', textColor: '#000000' }, 'A modular component, controller, or service within a container'),
|
|
217
|
+
shape('c4-system-boundary', 'System Boundary', C4BoundaryTemplate, { boundaryType: 'System', zIndex: -1 }, 'Boundary box enclosing containers belonging to a Software System'),
|
|
218
|
+
shape('c4-enterprise-boundary', 'Enterprise Boundary', C4BoundaryTemplate, { boundaryType: 'Enterprise', zIndex: -2 }, 'Boundary box enclosing all systems belonging to an enterprise'),
|
|
219
|
+
shape('c4-container-boundary', 'Container Boundary', C4BoundaryTemplate, { boundaryType: 'Container', zIndex: -1 }, 'Boundary box enclosing components belonging to a Container'),
|
|
220
|
+
note,
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
|
|
205
224
|
'er-diagram': {
|
|
206
225
|
name: 'Entity-Relationship',
|
|
207
226
|
group: 'Data',
|