@peter.prib/renderengine 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +203 -2
- package/REFERENCE.md +2198 -0
- package/dist/renderengine-workspace.umd.js +46905 -0
- package/dist/renderengine.umd.js +42223 -0
- package/examples/archimate.html +66 -0
- package/examples/diagram-editor.html +51 -0
- package/package.json +65 -4
- package/src/BaseRenderer.js +57 -0
- package/src/CanvasRenderer.js +194 -0
- package/src/Mat4.js +111 -0
- package/src/OrbitCamera.js +57 -0
- package/src/RendererFactory.js +50 -0
- package/src/SVGRenderer.js +439 -0
- package/src/archimate.js +723 -0
- package/src/archimateArchiFormat.js +1693 -0
- package/src/archimateArchiTypes.js +72 -0
- package/src/archimateArchive.js +140 -0
- package/src/archimateCSV.js +127 -0
- package/src/archimateDrawio.js +182 -0
- package/src/archimateExchangeFormat.js +546 -0
- package/src/archimateRelationshipTable.js +93 -0
- package/src/archimateSpecializations.js +161 -0
- package/src/chartCatalog.js +589 -0
- package/src/charting/ArchimateDiagramEditor.js +1624 -0
- package/src/charting/ArchimateSpecializationSelector.js +134 -0
- package/src/charting/Axis.js +68 -0
- package/src/charting/CameraSeeder.js +30 -0
- package/src/charting/CatalogDialog.js +92 -0
- package/src/charting/Chart.js +37 -0
- package/src/charting/Chart3D.js +99 -0
- package/src/charting/ChartBox.js +80 -0
- package/src/charting/ChartData.js +27 -0
- package/src/charting/ChartRegistry.js +76 -0
- package/src/charting/CheckboxTreeSelector.js +304 -0
- package/src/charting/ColorField.js +107 -0
- package/src/charting/ColorPalette.js +88 -0
- package/src/charting/CommandHistory.js +176 -0
- package/src/charting/DataFilter.js +52 -0
- package/src/charting/Diagram.js +36 -0
- package/src/charting/Diagram3D.js +29 -0
- package/src/charting/DiagramClipboard.js +31 -0
- package/src/charting/DiagramEditor.js +3330 -0
- package/src/charting/DiagramEditorCommand.js +110 -0
- package/src/charting/DiagramEditorCommands.js +237 -0
- package/src/charting/DiagramEditorEdgeCommands.js +142 -0
- package/src/charting/DiagramEditorState.js +98 -0
- package/src/charting/DiagramRegistry.js +86 -0
- package/src/charting/DiagramTypeRegistry.js +326 -0
- package/src/charting/DiagramViewPropertiesDialog.js +46 -0
- package/src/charting/Edge.js +113 -0
- package/src/charting/ErrorBars.js +37 -0
- package/src/charting/FilterSystem.js +285 -0
- package/src/charting/FindReplaceDialog.js +139 -0
- package/src/charting/FormDialog.js +385 -0
- package/src/charting/Gauge.js +58 -0
- package/src/charting/Hierarchy.js +175 -0
- package/src/charting/HintPanel.js +86 -0
- package/src/charting/ImageSelector.js +148 -0
- package/src/charting/InteractionManager.js +725 -0
- package/src/charting/Legend.js +58 -0
- package/src/charting/MenuBar.js +62 -0
- package/src/charting/ModelFileFormat.js +115 -0
- package/src/charting/OrbitControls.js +77 -0
- package/src/charting/Palette.js +119 -0
- package/src/charting/PopupMenu.js +117 -0
- package/src/charting/PropertiesPanel.js +155 -0
- package/src/charting/PropertyListEditor.js +96 -0
- package/src/charting/PropertyLog.js +92 -0
- package/src/charting/SettingsCascade.js +93 -0
- package/src/charting/SettingsDialog.js +53 -0
- package/src/charting/SettingsManager.js +179 -0
- package/src/charting/SettingsViewPropertiesDialog.js +58 -0
- package/src/charting/ShapePaths.js +27 -0
- package/src/charting/Statistics.js +134 -0
- package/src/charting/TemplateCatalog.js +59 -0
- package/src/charting/TemplateLibrary.js +103 -0
- package/src/charting/TextStyle.js +168 -0
- package/src/charting/ViewFit.js +55 -0
- package/src/charting/ViewMenu.js +70 -0
- package/src/charting/ViewPropertiesDialog.js +77 -0
- package/src/charting/ViewTabStrip.js +81 -0
- package/src/charting/archimate/ArchiFileFormat.js +32 -0
- package/src/charting/archimate/ArchimateCanvasEditor.js +815 -0
- package/src/charting/archimate/ArchimateCsvFileFormat.js +35 -0
- package/src/charting/archimate/ArchimateEdge.js +137 -0
- package/src/charting/archimate/ArchimateElementTemplate.js +195 -0
- package/src/charting/archimate/ArchimatePalette.js +102 -0
- package/src/charting/archimate/CsvImportDialog.js +105 -0
- package/src/charting/archimate/CustomViewpoints.js +74 -0
- package/src/charting/archimate/DerivedEdge.js +29 -0
- package/src/charting/archimate/DrawioFileFormat.js +24 -0
- package/src/charting/archimate/ElementFilterRule.js +21 -0
- package/src/charting/archimate/ExchangeFileFormat.js +28 -0
- package/src/charting/archimate/ViewGroupTemplate.js +137 -0
- package/src/charting/archimate/ViewLineEdge.js +84 -0
- package/src/charting/archimate/ViewpointRule.js +45 -0
- package/src/charting/archimate/archimateFileFormats.js +12 -0
- package/src/charting/archimate/archimateNames.js +5 -0
- package/src/charting/archimate/csvMapping.js +122 -0
- package/src/charting/archimate/elementMenuItems.js +32 -0
- package/src/charting/archimate/elementProperties.js +146 -0
- package/src/charting/archimate/elementTypeChoices.js +7 -0
- package/src/charting/archimate/elementTypeGroups.js +19 -0
- package/src/charting/archimate/index.js +17 -0
- package/src/charting/archimate/specializationCatalogDialog.js +64 -0
- package/src/charting/archimate/typeGlyph.js +29 -0
- package/src/charting/arrangement/Arrangement.js +112 -0
- package/src/charting/arrangement/ArrangementCommand.js +29 -0
- package/src/charting/arrangement/BackgroundCommand.js +41 -0
- package/src/charting/arrangement/BackgroundGroup.js +18 -0
- package/src/charting/arrangement/GroupCommand.js +46 -0
- package/src/charting/arrangement/ItemGroup.js +62 -0
- package/src/charting/arrangement/LockCommand.js +32 -0
- package/src/charting/arrangement/UngroupCommand.js +27 -0
- package/src/charting/arrangement/alignment.js +88 -0
- package/src/charting/arrangement/arrangeMenuItems.js +25 -0
- package/src/charting/arrangement/index.js +11 -0
- package/src/charting/arrangement/layouts.js +233 -0
- package/src/charting/canvas/ArchiHint.js +78 -0
- package/src/charting/canvas/CanvasBoxKind.js +39 -0
- package/src/charting/canvas/CanvasPalette.js +49 -0
- package/src/charting/canvas/CanvasTemplateLibrary.js +61 -0
- package/src/charting/canvas/CanvasTemplateMenu.js +151 -0
- package/src/charting/canvas/EditHintCommand.js +28 -0
- package/src/charting/canvas/canvasBoxKinds.js +27 -0
- package/src/charting/canvas/canvasBoxMenuItems.js +40 -0
- package/src/charting/canvas/defaultCanvasTemplates.js +186 -0
- package/src/charting/canvas/diagramBoxKinds.js +12 -0
- package/src/charting/canvas/hintMenuItems.js +49 -0
- package/src/charting/charts/ArcGauge.js +55 -0
- package/src/charting/charts/AreaChart.js +71 -0
- package/src/charting/charts/BarChart.js +80 -0
- package/src/charting/charts/BarChart3D.js +56 -0
- package/src/charting/charts/BarChart3DOrbit.js +53 -0
- package/src/charting/charts/BarChartBase.js +70 -0
- package/src/charting/charts/BarGauge.js +42 -0
- package/src/charting/charts/BoxPlotChart.js +41 -0
- package/src/charting/charts/BubbleChart.js +99 -0
- package/src/charting/charts/BubbleChart3D.js +78 -0
- package/src/charting/charts/BulletGauge.js +87 -0
- package/src/charting/charts/CalendarHeatmapChart.js +204 -0
- package/src/charting/charts/CandlestickChart.js +54 -0
- package/src/charting/charts/ChordDiagram.js +130 -0
- package/src/charting/charts/DensityChart.js +73 -0
- package/src/charting/charts/DialGauge.js +65 -0
- package/src/charting/charts/DonutChart.js +15 -0
- package/src/charting/charts/FunnelChart.js +47 -0
- package/src/charting/charts/GanttChart.js +201 -0
- package/src/charting/charts/GaugeChart.js +175 -0
- package/src/charting/charts/GeoMapChart.js +146 -0
- package/src/charting/charts/GroupedBarChart.js +88 -0
- package/src/charting/charts/GroupedDistributionChart.js +70 -0
- package/src/charting/charts/HeatmapChart.js +80 -0
- package/src/charting/charts/HierarchyChart.js +39 -0
- package/src/charting/charts/HistogramChart.js +45 -0
- package/src/charting/charts/KpiTile.js +72 -0
- package/src/charting/charts/LagPlotChart.js +41 -0
- package/src/charting/charts/LineChart.js +124 -0
- package/src/charting/charts/MultiRingGauge.js +54 -0
- package/src/charting/charts/OutlineChart.js +95 -0
- package/src/charting/charts/ParallelCoordinatesChart.js +130 -0
- package/src/charting/charts/PieChart.js +56 -0
- package/src/charting/charts/PipeChart.js +88 -0
- package/src/charting/charts/PopulationPyramidChart.js +106 -0
- package/src/charting/charts/QQPlotChart.js +64 -0
- package/src/charting/charts/RadarChart.js +76 -0
- package/src/charting/charts/RadialBarChart.js +96 -0
- package/src/charting/charts/RingGauge.js +23 -0
- package/src/charting/charts/SankeyDiagram.js +174 -0
- package/src/charting/charts/ScatterChart.js +104 -0
- package/src/charting/charts/StackedAreaChart.js +15 -0
- package/src/charting/charts/StackedBarChart.js +33 -0
- package/src/charting/charts/StackedBarChart3DOrbit.js +58 -0
- package/src/charting/charts/StreamGraphChart.js +131 -0
- package/src/charting/charts/SunburstChart.js +57 -0
- package/src/charting/charts/ThermometerGauge.js +43 -0
- package/src/charting/charts/TreemapChart.js +54 -0
- package/src/charting/charts/ViolinChart.js +37 -0
- package/src/charting/charts/WaterfallChart.js +66 -0
- package/src/charting/charts/WordCloudChart.js +104 -0
- package/src/charting/diagramModel/AddViewCommand.js +33 -0
- package/src/charting/diagramModel/DiagramModel.js +39 -0
- package/src/charting/diagramModel/DiagramModelSearch.js +22 -0
- package/src/charting/diagramModel/DiagramView.js +50 -0
- package/src/charting/diagramModel/ModelHistory.js +43 -0
- package/src/charting/diagramModel/ModelReport.js +153 -0
- package/src/charting/diagramModel/ModelSearch.js +223 -0
- package/src/charting/diagramModel/RemoveViewCommand.js +25 -0
- package/src/charting/diagramModel/RenameViewCommand.js +21 -0
- package/src/charting/diagramModel/ReplaceTextCommand.js +25 -0
- package/src/charting/diagramModel/ViewSetChecker.js +34 -0
- package/src/charting/diagramModel/ViewSetModel.js +243 -0
- package/src/charting/diagramModel/index.js +6 -0
- package/src/charting/diagramModel/nameTransforms.js +29 -0
- package/src/charting/diagrams/ArchimateCommands.js +141 -0
- package/src/charting/diagrams/ArchimateDiagram.js +263 -0
- package/src/charting/diagrams/ArchimateModel.js +1155 -0
- package/src/charting/diagrams/ArchimateModelChecker.js +125 -0
- package/src/charting/diagrams/ArchimateModelReport.js +128 -0
- package/src/charting/diagrams/ArchimateModelSearch.js +41 -0
- package/src/charting/diagrams/ArchimateValidator.js +357 -0
- package/src/charting/diagrams/CanvasView.js +112 -0
- package/src/charting/diagrams/ContextMenu.js +37 -0
- package/src/charting/diagrams/DataModelDiagram.js +86 -0
- package/src/charting/diagrams/DataModelRelationshipStrategy.js +38 -0
- package/src/charting/diagrams/DerivedRelationships.js +102 -0
- package/src/charting/diagrams/EbnfDiagram.js +39 -0
- package/src/charting/diagrams/EdgeGeometry.js +553 -0
- package/src/charting/diagrams/EditViewItemCommand.js +128 -0
- package/src/charting/diagrams/ElementInstance.js +157 -0
- package/src/charting/diagrams/GraphDiagram.js +1340 -0
- package/src/charting/diagrams/GraphDiagram3D.js +152 -0
- package/src/charting/diagrams/GraphMetrics.js +81 -0
- package/src/charting/diagrams/GridOverlay.js +137 -0
- package/src/charting/diagrams/ImportRowsCommand.js +62 -0
- package/src/charting/diagrams/LabelHelper.js +108 -0
- package/src/charting/diagrams/MergeElementsCommand.js +101 -0
- package/src/charting/diagrams/NestInstanceCommand.js +34 -0
- package/src/charting/diagrams/NodeGeometry.js +158 -0
- package/src/charting/diagrams/PlaceItemsCommand.js +77 -0
- package/src/charting/diagrams/RadialLayout.js +101 -0
- package/src/charting/diagrams/RelationshipInstance.js +93 -0
- package/src/charting/diagrams/RemoveGroupFromViewCommand.js +28 -0
- package/src/charting/diagrams/RemoveLineCommand.js +27 -0
- package/src/charting/diagrams/ShowElementCommand.js +43 -0
- package/src/charting/diagrams/SvgCoordinate.js +17 -0
- package/src/charting/diagrams/SvgZoom.js +127 -0
- package/src/charting/diagrams/Tooltip.js +62 -0
- package/src/charting/diagrams/ViewConnectionInstance.js +102 -0
- package/src/charting/diagrams/ViewObjectInstance.js +56 -0
- package/src/charting/diagrams/commands/AddEdgeCommand.js +34 -0
- package/src/charting/diagrams/commands/AddNodeCommand.js +28 -0
- package/src/charting/diagrams/commands/AddWaypointCommand.js +27 -0
- package/src/charting/diagrams/commands/CreateFolderCommand.js +27 -0
- package/src/charting/diagrams/commands/DeleteEdgeCommand.js +30 -0
- package/src/charting/diagrams/commands/DeleteFolderCommand.js +47 -0
- package/src/charting/diagrams/commands/DeleteNodeCommand.js +35 -0
- package/src/charting/diagrams/commands/DeleteWaypointCommand.js +24 -0
- package/src/charting/diagrams/commands/GraphCommand.js +65 -0
- package/src/charting/diagrams/commands/InvertEdgeCommand.js +24 -0
- package/src/charting/diagrams/commands/MoveNodeCommand.js +44 -0
- package/src/charting/diagrams/commands/MoveWaypointCommand.js +27 -0
- package/src/charting/diagrams/commands/RenameEdgeCommand.js +23 -0
- package/src/charting/diagrams/commands/RenameFolderCommand.js +31 -0
- package/src/charting/diagrams/commands/RenameNodeCommand.js +27 -0
- package/src/charting/diagrams/commands/ResizeNodeCommand.js +46 -0
- package/src/charting/diagrams/commands/UpdateEdgeSideCommand.js +40 -0
- package/src/charting/diagrams/commands/index.js +17 -0
- package/src/charting/diagrams/duplicates.js +49 -0
- package/src/charting/diagrams/nesting.js +30 -0
- package/src/charting/diagrams/roadmap.js +77 -0
- package/src/charting/edgeMarkers/ArrowMarker.js +15 -0
- package/src/charting/edgeMarkers/CircleMarker.js +13 -0
- package/src/charting/edgeMarkers/CrowsFootMarker.js +16 -0
- package/src/charting/edgeMarkers/DiamondMarker.js +15 -0
- package/src/charting/edgeMarkers/EdgeMarker.js +34 -0
- package/src/charting/edgeMarkers/HalfArrowMarker.js +8 -0
- package/src/charting/edgeMarkers/HollowDiamondMarker.js +18 -0
- package/src/charting/edgeMarkers/HollowTriangleMarker.js +18 -0
- package/src/charting/edgeMarkers/OpenArrowMarker.js +17 -0
- package/src/charting/edgeMarkers/index.js +27 -0
- package/src/charting/edgeStyle/EdgeStyleCascade.js +22 -0
- package/src/charting/edgeStyle/EdgeStyleDialog.js +51 -0
- package/src/charting/edgeStyle/EdgeStyleSchema.js +43 -0
- package/src/charting/edgeStyle/SetStyleCommand.js +12 -0
- package/src/charting/edgeStyle/index.js +4 -0
- package/src/charting/emphasis/EmphasisRule.js +42 -0
- package/src/charting/emphasis/HighlightRule.js +24 -0
- package/src/charting/gauges/ArcTrack.js +51 -0
- package/src/charting/gauges/GaugeAnimation.js +51 -0
- package/src/charting/gauges/GaugeBands.js +48 -0
- package/src/charting/gauges/GaugeScale.js +87 -0
- package/src/charting/gauges/GaugeTrack.js +44 -0
- package/src/charting/gauges/GaugeValue.js +61 -0
- package/src/charting/gauges/HorizontalTrack.js +14 -0
- package/src/charting/gauges/LinearTrack.js +29 -0
- package/src/charting/gauges/Sparkline.js +42 -0
- package/src/charting/gauges/VerticalTrack.js +17 -0
- package/src/charting/glyphs/ArrowGlyph.js +21 -0
- package/src/charting/glyphs/BatteryGlyph.js +18 -0
- package/src/charting/glyphs/ChevronGlyph.js +18 -0
- package/src/charting/glyphs/CircleGlyph.js +15 -0
- package/src/charting/glyphs/CloudGlyph.js +19 -0
- package/src/charting/glyphs/ComponentGlyph.js +18 -0
- package/src/charting/glyphs/ConstraintGlyph.js +24 -0
- package/src/charting/glyphs/ContractBoxGlyph.js +10 -0
- package/src/charting/glyphs/CubeGlyph.js +19 -0
- package/src/charting/glyphs/CylinderGlyph.js +19 -0
- package/src/charting/glyphs/DashedDoubleArrowGlyph.js +11 -0
- package/src/charting/glyphs/DeviceGlyph.js +17 -0
- package/src/charting/glyphs/DiamondGlyph.js +17 -0
- package/src/charting/glyphs/DocumentGlyph.js +17 -0
- package/src/charting/glyphs/DoubleArrowGlyph.js +23 -0
- package/src/charting/glyphs/DoubleCircleGlyph.js +24 -0
- package/src/charting/glyphs/EllipseGlyph.js +18 -0
- package/src/charting/glyphs/FactoryGlyph.js +20 -0
- package/src/charting/glyphs/FlagGlyph.js +16 -0
- package/src/charting/glyphs/GapCircleGlyph.js +17 -0
- package/src/charting/glyphs/GearGlyph.js +37 -0
- package/src/charting/glyphs/Glyph.js +98 -0
- package/src/charting/glyphs/GroupingBoxGlyph.js +13 -0
- package/src/charting/glyphs/HalfCircleGlyph.js +22 -0
- package/src/charting/glyphs/HexagonGlyph.js +28 -0
- package/src/charting/glyphs/IconGlyph.js +22 -0
- package/src/charting/glyphs/ImageGlyph.js +16 -0
- package/src/charting/glyphs/JunctionGlyph.js +17 -0
- package/src/charting/glyphs/LollipopGlyph.js +22 -0
- package/src/charting/glyphs/MagnifierGlyph.js +17 -0
- package/src/charting/glyphs/NoteGlyph.js +33 -0
- package/src/charting/glyphs/ObjectBoxGlyph.js +21 -0
- package/src/charting/glyphs/OrJunctionGlyph.js +17 -0
- package/src/charting/glyphs/ParallelogramGlyph.js +20 -0
- package/src/charting/glyphs/PersonGlyph.js +23 -0
- package/src/charting/glyphs/PillGlyph.js +23 -0
- package/src/charting/glyphs/PinGlyph.js +18 -0
- package/src/charting/glyphs/PrincipleGlyph.js +23 -0
- package/src/charting/glyphs/ProductBoxGlyph.js +16 -0
- package/src/charting/glyphs/RectGlyph.js +15 -0
- package/src/charting/glyphs/RoundedBoxGlyph.js +13 -0
- package/src/charting/glyphs/StackGlyph.js +14 -0
- package/src/charting/glyphs/StepsGlyph.js +20 -0
- package/src/charting/glyphs/StickFigureGlyph.js +32 -0
- package/src/charting/glyphs/SystemSoftwareBoxGlyph.js +16 -0
- package/src/charting/glyphs/TabbedBoxGlyph.js +37 -0
- package/src/charting/glyphs/TargetGlyph.js +23 -0
- package/src/charting/glyphs/WavyBoxGlyph.js +17 -0
- package/src/charting/glyphs/WheelGlyph.js +28 -0
- package/src/charting/glyphs/drawGlyph.js +28 -0
- package/src/charting/glyphs/glyphSvg.js +55 -0
- package/src/charting/glyphs/index.js +109 -0
- package/src/charting/modelTree/AnalysisPanel.js +159 -0
- package/src/charting/modelTree/DiagramTabs.js +217 -0
- package/src/charting/modelTree/ModelTree.js +164 -0
- package/src/charting/modelTree/ModelTreePanel.js +914 -0
- package/src/charting/modelTree/TreeBuilder.js +238 -0
- package/src/charting/modelTree/TreeModeRegistry.js +51 -0
- package/src/charting/modelTree/TreeViewMode.js +110 -0
- package/src/charting/modelTree/buildFilterTrees.js +138 -0
- package/src/charting/modelTree/modes/FlatTreeMode.js +46 -0
- package/src/charting/modelTree/modes/LayerGroupedTreeMode.js +62 -0
- package/src/charting/modelTree/modes/TypeGroupedTreeMode.js +60 -0
- package/src/charting/modelTree/treeLayers.js +11 -0
- package/src/charting/report/reportHtml.js +82 -0
- package/src/charting/report/reportViewer.js +118 -0
- package/src/charting/shapes/CircleShape.js +19 -0
- package/src/charting/shapes/PathShape.js +17 -0
- package/src/charting/shapes/RectShape.js +20 -0
- package/src/charting/shapes/Shape.js +20 -0
- package/src/charting/shapes/ShapeFactory.js +23 -0
- package/src/charting/storage/LocalJsonStore.js +43 -0
- package/src/charting/templates/ActorTemplate.js +36 -0
- package/src/charting/templates/BoxTemplate.js +27 -0
- package/src/charting/templates/CircleTemplate.js +18 -0
- package/src/charting/templates/ClassTemplate.js +20 -0
- package/src/charting/templates/CloudTemplate.js +43 -0
- package/src/charting/templates/ComponentTemplate.js +19 -0
- package/src/charting/templates/CylinderTemplate.js +30 -0
- package/src/charting/templates/DataStoreTemplate.js +16 -0
- package/src/charting/templates/DiamondTemplate.js +10 -0
- package/src/charting/templates/DocumentTemplate.js +32 -0
- package/src/charting/templates/EllipseTemplate.js +15 -0
- package/src/charting/templates/FinalStateTemplate.js +12 -0
- package/src/charting/templates/GlyphTemplate.js +47 -0
- package/src/charting/templates/HexagonTemplate.js +17 -0
- package/src/charting/templates/LifelineTemplate.js +20 -0
- package/src/charting/templates/ManualInputTemplate.js +12 -0
- package/src/charting/templates/NoteTemplate.js +23 -0
- package/src/charting/templates/OctagonTemplate.js +7 -0
- package/src/charting/templates/PackageTemplate.js +25 -0
- package/src/charting/templates/ParallelogramTemplate.js +11 -0
- package/src/charting/templates/PentagonTemplate.js +5 -0
- package/src/charting/templates/PolygonTemplate.js +18 -0
- package/src/charting/templates/PredefinedProcessTemplate.js +13 -0
- package/src/charting/templates/RectangleTemplate.js +28 -0
- package/src/charting/templates/RegularPolygonTemplate.js +21 -0
- package/src/charting/templates/RoundedRectangleTemplate.js +5 -0
- package/src/charting/templates/StadiumTemplate.js +8 -0
- package/src/charting/templates/StarTemplate.js +17 -0
- package/src/charting/templates/SwimlaneTemplate.js +18 -0
- package/src/charting/templates/Template.js +208 -0
- package/src/charting/templates/TrapezoidTemplate.js +11 -0
- package/src/charting/templates/TriangleTemplate.js +12 -0
- package/src/charting/templates/index.js +31 -0
- package/src/charting/templates/outline.js +59 -0
- package/src/charting/templates/svg.js +19 -0
- package/src/charting/textToShapes.js +61 -0
- package/src/charting/toolbar/ToolbarButton.js +38 -0
- package/src/charting/toolbar/ToolbarCheckbox.js +31 -0
- package/src/charting/toolbar/ToolbarItem.js +19 -0
- package/src/charting/toolbar/ToolbarMenu.js +36 -0
- package/src/charting/toolbar/ToolbarSelect.js +38 -0
- package/src/charting/toolbar/ToolbarSeparator.js +7 -0
- package/src/charting/toolbar/ToolbarText.js +22 -0
- package/src/charting/toolbar/index.js +8 -0
- package/src/charting/toolbar/pickFile.js +14 -0
- package/src/charting/viewDecorations.js +85 -0
- package/src/charting/zOrder.js +57 -0
- package/src/csvTable.js +61 -0
- package/src/datamodel.js +122 -0
- package/src/datawindow/BoundedWindow.js +36 -0
- package/src/datawindow/FlatDataWindow.js +34 -0
- package/src/datawindow/GraphDataWindow.js +40 -0
- package/src/datawindow/GroupedSampleDataWindow.js +16 -0
- package/src/datawindow/SeriesDataWindow.js +42 -0
- package/src/datawindow/index.js +31 -0
- package/src/diagramCatalog.js +30 -0
- package/src/ebnfDiagram.js +57 -0
- package/src/ebnfParser.js +129 -0
- package/src/exportImage.js +233 -0
- package/src/index.js +2636 -0
- package/src/itIcons.js +84 -0
- package/src/kernelExpr.js +243 -0
- package/src/propertiesDialog.js +463 -0
- package/src/railroadDiagram.js +571 -0
- package/src/treeModeCatalog.js +16 -0
- package/src/workspace/ContextBus.js +79 -0
- package/src/workspace/EditorShell.js +67 -0
- package/src/workspace/Part.js +163 -0
- package/src/workspace/PartFactory.js +49 -0
- package/src/workspace/PartRegistry.js +86 -0
- package/src/workspace/Workspace.js +321 -0
- package/src/workspace/archimateServices.js +33 -0
- package/src/workspace/dom.js +120 -0
- package/src/workspace/index.js +75 -0
- package/src/workspace/layout/GridNode.js +280 -0
- package/src/workspace/layout/LayoutNode.js +89 -0
- package/src/workspace/layout/PartNode.js +77 -0
- package/src/workspace/layout/SplitNode.js +114 -0
- package/src/workspace/layout/TabsNode.js +191 -0
- package/src/workspace/layout/index.js +24 -0
- package/src/workspace/parts/AdapterPart.js +33 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +345 -0
- package/src/workspace/parts/ArchimateEditorPart.js +79 -0
- package/src/workspace/parts/ChartPart.js +115 -0
- package/src/workspace/parts/ContentPart.js +34 -0
- package/src/workspace/parts/DetailPart.js +46 -0
- package/src/workspace/parts/DiagramEditorPart.js +135 -0
- package/src/workspace/parts/EditorPart.js +388 -0
- package/src/workspace/parts/HtmlPart.js +25 -0
- package/src/workspace/parts/ListPart.js +57 -0
- package/src/workspace/parts/ModelFile.js +187 -0
- package/src/workspace/parts/ModelsPart.js +135 -0
- package/src/workspace/styles.js +258 -0
- package/src/workspace/themes.js +96 -0
- package/src/workspace/workArea.js +413 -0
- package/src/workspace/workAreaModel.js +197 -0
- package/src/workspace.js +16 -0
- package/src/zip/ZipArchive.js +160 -0
- package/src/zip/crc32.js +25 -0
- package/src/zip/inflate.js +159 -0
|
@@ -0,0 +1,1693 @@
|
|
|
1
|
+
import { ArchimateNotation } from './archimate.js';
|
|
2
|
+
import { archiElementType, typeProfileId, typeDisplayName, ARCHI_TYPE_FEATURE, ARCHI_TYPE_PROFILE_PREFIX } from './archimateArchiTypes.js';
|
|
3
|
+
import { ArchimateModel } from './charting/diagrams/ArchimateModel.js';
|
|
4
|
+
import './charting/diagrams/CanvasView.js'; // registers the canvas view type
|
|
5
|
+
import { ElementInstance } from './charting/diagrams/ElementInstance.js';
|
|
6
|
+
import { ViewObjectInstance } from './charting/diagrams/ViewObjectInstance.js';
|
|
7
|
+
import { RelationshipInstance } from './charting/diagrams/RelationshipInstance.js';
|
|
8
|
+
import { ViewConnectionInstance } from './charting/diagrams/ViewConnectionInstance.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ArchimateArchiFormat — pure import/export helpers for Archi’s native
|
|
12
|
+
* `.archimate` XML format (not the Open Group Model Exchange File Format).
|
|
13
|
+
*
|
|
14
|
+
* The module maps Archi model elements, relationships, and view/visual
|
|
15
|
+
* children to renderengine’s `archimateDiagram` inputs, and back again.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const ARCHI_VISUAL_OBJECTS = Object.freeze([
|
|
19
|
+
'diagram-model-note',
|
|
20
|
+
'diagram-model-group',
|
|
21
|
+
'diagram-model-connection',
|
|
22
|
+
'diagram-model-image',
|
|
23
|
+
'diagram-model-reference',
|
|
24
|
+
'sketch-model-sticky',
|
|
25
|
+
'sketch-model-actor',
|
|
26
|
+
'canvas-model-block',
|
|
27
|
+
'canvas-model-sticky',
|
|
28
|
+
'canvas-model-image',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export const ARCHI_VIEW_TYPES = Object.freeze([
|
|
32
|
+
'archimate-diagram-model',
|
|
33
|
+
'sketch-model',
|
|
34
|
+
'canvas-model',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const ARCHI_NAMESPACE = 'http://www.archimatetool.com/archimate';
|
|
38
|
+
/** Archi's canvas classes live in their own namespace. */
|
|
39
|
+
const ARCHI_CANVAS_NAMESPACE = 'http://www.archimatetool.com/archimate/canvas';
|
|
40
|
+
|
|
41
|
+
/** Lightweight XML element descriptor used by both the DOMParser wrapper and
|
|
42
|
+
* the Node fallback parser. Mirrors the small subset of DOM we need. */
|
|
43
|
+
export class ArchiXMLElement {
|
|
44
|
+
constructor(tagName, attributes = {}) {
|
|
45
|
+
this.tagName = tagName;
|
|
46
|
+
this.attributes = attributes;
|
|
47
|
+
this.children = [];
|
|
48
|
+
this.textContent = '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getAttribute(name) {
|
|
52
|
+
return this.attributes[name] ?? null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
querySelectorAll(tagName) {
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const child of this.children) {
|
|
58
|
+
if (child.tagName === tagName) out.push(child);
|
|
59
|
+
out.push(...child.querySelectorAll(tagName));
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
querySelector(tagName) {
|
|
65
|
+
for (const child of this.children) {
|
|
66
|
+
if (child.tagName === tagName) return child;
|
|
67
|
+
const deep = child.querySelector(tagName);
|
|
68
|
+
if (deep) return deep;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Parse an XML string into an `ArchiXMLElement` tree. Uses DOMParser when
|
|
75
|
+
* available (browser), otherwise a minimal dependency-free fallback. */
|
|
76
|
+
export function parseXML(xml) {
|
|
77
|
+
if (typeof DOMParser !== 'undefined') {
|
|
78
|
+
const doc = new DOMParser().parseFromString(xml, 'application/xml');
|
|
79
|
+
const parserError = doc.querySelector('parsererror');
|
|
80
|
+
if (parserError) throw new Error('XML parse error');
|
|
81
|
+
return _fromDOMNode(doc.documentElement);
|
|
82
|
+
}
|
|
83
|
+
return _parseXMLFallback(xml);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _fromDOMNode(node) {
|
|
87
|
+
const attrs = {};
|
|
88
|
+
for (const attr of node.attributes || []) {
|
|
89
|
+
attrs[attr.name] = attr.value;
|
|
90
|
+
}
|
|
91
|
+
const el = new ArchiXMLElement(node.tagName, attrs);
|
|
92
|
+
for (const child of node.childNodes || []) {
|
|
93
|
+
if (child.nodeType === 1) { // ELEMENT_NODE
|
|
94
|
+
el.children.push(_fromDOMNode(child));
|
|
95
|
+
} else if (child.nodeType === 3) { // TEXT_NODE
|
|
96
|
+
el.textContent += child.textContent;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!el.textContent.trim()) el.textContent = ''; // indentation between elements, not text
|
|
100
|
+
return el;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _parseXMLFallback(xml) {
|
|
104
|
+
// XML 1.0 (2.11): a parser sees every line break as a newline; a CR written as 
 survives (decoded later).
|
|
105
|
+
const trimmed = xml.replace(/\r\n?/g, '\n').trim();
|
|
106
|
+
const root = _parseNode(trimmed, { pos: 0 });
|
|
107
|
+
if (!root) throw new Error('XML parse error: no root element');
|
|
108
|
+
return root;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function _parseNode(xml, state) {
|
|
112
|
+
_skipWhitespace(xml, state);
|
|
113
|
+
if (state.pos >= xml.length || xml[state.pos] !== '<') return null;
|
|
114
|
+
|
|
115
|
+
// Skip XML declarations, processing instructions and comments.
|
|
116
|
+
if (xml.slice(state.pos, state.pos + 2) === '<?') {
|
|
117
|
+
const end = xml.indexOf('?>', state.pos);
|
|
118
|
+
state.pos = end === -1 ? xml.length : end + 2;
|
|
119
|
+
return _parseNode(xml, state);
|
|
120
|
+
}
|
|
121
|
+
if (xml.startsWith('<!--', state.pos)) {
|
|
122
|
+
const end = xml.indexOf('-->', state.pos);
|
|
123
|
+
state.pos = end === -1 ? xml.length : end + 3;
|
|
124
|
+
return _parseNode(xml, state);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A quoted attribute value may hold '>' or the other quote character.
|
|
128
|
+
const tagMatch = xml.slice(state.pos).match(/^<([^>\s/]+)((?:[^>"']|"[^"]*"|'[^']*')*)>/);
|
|
129
|
+
if (!tagMatch) throw new Error('XML parse error: malformed tag');
|
|
130
|
+
const tagName = tagMatch[1];
|
|
131
|
+
const attrText = tagMatch[2];
|
|
132
|
+
state.pos += tagMatch[0].length;
|
|
133
|
+
|
|
134
|
+
const isSelfClosing = attrText.trimEnd().endsWith('/');
|
|
135
|
+
const attributes = _parseAttributes(attrText);
|
|
136
|
+
const node = new ArchiXMLElement(tagName, attributes);
|
|
137
|
+
|
|
138
|
+
if (isSelfClosing) {
|
|
139
|
+
return node;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Whitespace is text: a content's leading spaces are its own (Archi uses them to clear a picture);
|
|
143
|
+
// indentation between elements is dropped below, once the element is read.
|
|
144
|
+
while (state.pos < xml.length) {
|
|
145
|
+
if (xml.slice(state.pos, state.pos + 2) === '</') {
|
|
146
|
+
const closeMatch = xml.slice(state.pos).match(/^<\/([^>]+)>/);
|
|
147
|
+
if (!closeMatch || closeMatch[1] !== tagName) {
|
|
148
|
+
throw new Error(`XML parse error: mismatched closing tag for ${tagName}`);
|
|
149
|
+
}
|
|
150
|
+
state.pos += closeMatch[0].length;
|
|
151
|
+
if (!node.textContent.trim()) node.textContent = ''; // indentation between elements, not text
|
|
152
|
+
return node;
|
|
153
|
+
}
|
|
154
|
+
if (xml.startsWith('<![CDATA[', state.pos)) {
|
|
155
|
+
const end = xml.indexOf(']]>', state.pos);
|
|
156
|
+
node.textContent += xml.slice(state.pos + 9, end === -1 ? xml.length : end);
|
|
157
|
+
state.pos = end === -1 ? xml.length : end + 3;
|
|
158
|
+
} else if (xml.startsWith('<!--', state.pos)) {
|
|
159
|
+
const end = xml.indexOf('-->', state.pos);
|
|
160
|
+
state.pos = end === -1 ? xml.length : end + 3;
|
|
161
|
+
} else if (xml[state.pos] === '<') {
|
|
162
|
+
const child = _parseNode(xml, state);
|
|
163
|
+
if (child) node.children.push(child);
|
|
164
|
+
} else {
|
|
165
|
+
const textEnd = xml.indexOf('<', state.pos);
|
|
166
|
+
node.textContent += _unescapeXml(xml.slice(state.pos, textEnd === -1 ? xml.length : textEnd));
|
|
167
|
+
state.pos = textEnd === -1 ? xml.length : textEnd;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!node.textContent.trim()) node.textContent = ''; // indentation between elements, not text
|
|
171
|
+
return node;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _skipWhitespace(xml, state) {
|
|
175
|
+
while (state.pos < xml.length && /\s/.test(xml[state.pos])) state.pos++;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function _parseAttributes(text) {
|
|
179
|
+
const attrs = {};
|
|
180
|
+
const regex = /([^\s=/]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
181
|
+
let m;
|
|
182
|
+
while ((m = regex.exec(text)) !== null) {
|
|
183
|
+
attrs[m[1]] = _unescapeXml(m[2] ?? m[3]);
|
|
184
|
+
}
|
|
185
|
+
return attrs;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Decode the predefined entities and character references (the fallback parser's half of _escapeXml). */
|
|
189
|
+
function _unescapeXml(str) {
|
|
190
|
+
return str.replace(/&(#x[0-9a-f]+|#\d+|lt|gt|quot|apos|amp);/gi, (whole, ref) => {
|
|
191
|
+
const r = ref.toLowerCase();
|
|
192
|
+
if (r[0] === '#') {
|
|
193
|
+
const code = r[1] === 'x' ? parseInt(r.slice(2), 16) : parseInt(r.slice(1), 10);
|
|
194
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
|
|
195
|
+
}
|
|
196
|
+
return { lt: '<', gt: '>', quot: '"', apos: "'", amp: '&' }[r];
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Parse an Archi native XML model into plain JavaScript structures. */
|
|
201
|
+
export function parseArchiModel(xml) {
|
|
202
|
+
const root = typeof xml === 'string' ? parseXML(xml) : xml;
|
|
203
|
+
const model = {
|
|
204
|
+
id: root.getAttribute('id'),
|
|
205
|
+
name: root.getAttribute('name'),
|
|
206
|
+
version: root.getAttribute('version'),
|
|
207
|
+
elements: [],
|
|
208
|
+
relationships: [],
|
|
209
|
+
views: [],
|
|
210
|
+
// The model's own documentation (Archi's "purpose") and properties, in order (keys may repeat).
|
|
211
|
+
documentation: _childText(root, 'purpose'),
|
|
212
|
+
properties: _propertyPairs(root),
|
|
213
|
+
archi: _extrasOrNull(_archiExtras(root, ARCHI_MODEL_HANDLED)),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
for (const folder of root.children.filter(c => c.tagName === 'folder')) {
|
|
217
|
+
const folderType = folder.getAttribute('type');
|
|
218
|
+
if (folderType === 'diagrams') {
|
|
219
|
+
_walkViewFolder(folder, model);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Recursively walk folder structure (handles nested sub-folders)
|
|
224
|
+
_walkFolder(folder, folderType, model);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return model;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** A sub-folder as one step of a folder path: its id, name, documentation and properties. @private */
|
|
231
|
+
function _folderStep(folder) {
|
|
232
|
+
const documentation = _childText(folder, 'documentation');
|
|
233
|
+
const properties = _propertyPairs(folder);
|
|
234
|
+
return {
|
|
235
|
+
id: folder.getAttribute('id'),
|
|
236
|
+
name: folder.getAttribute('name') ?? '',
|
|
237
|
+
...(documentation && { documentation }),
|
|
238
|
+
...(properties.length && { properties }),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Every view in the Views folder, sub-folders included, in file order; each
|
|
243
|
+
* remembers its sub-folder path (`archiFolder`). @private */
|
|
244
|
+
function _walkViewFolder(folder, model, path = []) {
|
|
245
|
+
for (const child of folder.children) {
|
|
246
|
+
if (child.tagName === 'element') {
|
|
247
|
+
const view = _parseView(child);
|
|
248
|
+
if (path.length) view.archiFolder = path;
|
|
249
|
+
model.views.push(view);
|
|
250
|
+
} else if (child.tagName === 'folder') {
|
|
251
|
+
_walkViewFolder(child, model, [...path, _folderStep(child)]);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Every element (or relationship) in a top-level folder, sub-folders included;
|
|
257
|
+
* each remembers its sub-folder path (`archiFolder`). @private */
|
|
258
|
+
function _walkFolder(folder, folderType, model, path = []) {
|
|
259
|
+
for (const child of folder.children) {
|
|
260
|
+
if (child.tagName === 'element') {
|
|
261
|
+
const parsed = _parseModelElement(child);
|
|
262
|
+
if (path.length) parsed.archiFolder = path;
|
|
263
|
+
if (folderType === 'relations') {
|
|
264
|
+
model.relationships.push(parsed);
|
|
265
|
+
} else {
|
|
266
|
+
model.elements.push(parsed);
|
|
267
|
+
}
|
|
268
|
+
} else if (child.tagName === 'folder') {
|
|
269
|
+
_walkFolder(child, folderType, model, [...path, _folderStep(child)]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function _parseModelElement(el) {
|
|
275
|
+
const xsiType = el.getAttribute('xsi:type') || '';
|
|
276
|
+
const type = xsiType.replace(/^archimate:/, '');
|
|
277
|
+
const element = {
|
|
278
|
+
id: el.getAttribute('id'),
|
|
279
|
+
name: el.getAttribute('name'),
|
|
280
|
+
type,
|
|
281
|
+
documentation: _childText(el, 'documentation'),
|
|
282
|
+
properties: _parseProperties(el),
|
|
283
|
+
};
|
|
284
|
+
const source = el.getAttribute('source');
|
|
285
|
+
const target = el.getAttribute('target');
|
|
286
|
+
if (source) element.source = source;
|
|
287
|
+
if (target) element.target = target;
|
|
288
|
+
if (el.getAttribute('directed') === 'true') element.directed = true;
|
|
289
|
+
const archi = _extrasOrNull(_archiExtras(el, ARCHI_CONCEPT_HANDLED));
|
|
290
|
+
if (archi) element.archi = archi;
|
|
291
|
+
return element;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function _parseView(el) {
|
|
295
|
+
const xsiType = el.getAttribute('xsi:type') || '';
|
|
296
|
+
const view = {
|
|
297
|
+
id: el.getAttribute('id'),
|
|
298
|
+
name: el.getAttribute('name'),
|
|
299
|
+
type: _viewTypeFromXSI(xsiType),
|
|
300
|
+
documentation: _childText(el, 'documentation'),
|
|
301
|
+
properties: _propertyPairs(el),
|
|
302
|
+
archi: _extrasOrNull(_archiExtras(el, ARCHI_VIEW_HANDLED)),
|
|
303
|
+
children: [],
|
|
304
|
+
};
|
|
305
|
+
for (const child of el.children.filter(c => c.tagName === 'child')) {
|
|
306
|
+
view.children.push(_parseVisual(child));
|
|
307
|
+
}
|
|
308
|
+
_relativizeLocatedBendpoints(view);
|
|
309
|
+
return view;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Archi's bendpoint: offsets from the source's and the target's centre. */
|
|
313
|
+
const BENDPOINT_KEYS = Object.freeze(['startX', 'startY', 'endX', 'endY']);
|
|
314
|
+
|
|
315
|
+
/** Archi's size for a visual saved at its default size (width/height -1). @private */
|
|
316
|
+
const ARCHI_DEFAULT_SIZE = Object.freeze({
|
|
317
|
+
'diagram-model-group': { width: 400, height: 140 },
|
|
318
|
+
'diagram-model-note': { width: 185, height: 80 },
|
|
319
|
+
});
|
|
320
|
+
const ARCHI_ELEMENT_SIZE = Object.freeze({ width: 120, height: 55 });
|
|
321
|
+
|
|
322
|
+
/** Every visual's absolute bounds in a view (or visual tree), by visual id. @private */
|
|
323
|
+
function _visualBoxes(root) {
|
|
324
|
+
const boxes = new Map();
|
|
325
|
+
const walk = (visual, origin) => {
|
|
326
|
+
const bounds = _absoluteBounds(visual, origin);
|
|
327
|
+
if (bounds && visual.id) boxes.set(visual.id, bounds);
|
|
328
|
+
for (const child of visual.children || []) walk(child, bounds ?? origin);
|
|
329
|
+
};
|
|
330
|
+
walk(root, VIEW_ORIGIN);
|
|
331
|
+
return boxes;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** A box's anchor point as Archi (GEF's Rectangle#getCenter) takes it: integer halves. @private */
|
|
335
|
+
const _centre = (b) => b && { x: b.x + Math.floor(b.width / 2), y: b.y + Math.floor(b.height / 2) };
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Where a connection's bendpoints are on the view. Archi places bendpoint i of n
|
|
339
|
+
* between its two offsets, weighted by w = (i + 1) / (n + 1) from source to target:
|
|
340
|
+
* (1 - w)(sourceCentre + start) + w(targetCentre + end). A connection to another
|
|
341
|
+
* connection has no box at that end; the other end's offset is used alone.
|
|
342
|
+
* @private
|
|
343
|
+
*/
|
|
344
|
+
function _bendpointLocations(sc, boxes) {
|
|
345
|
+
const src = _centre(boxes.get(sc.source));
|
|
346
|
+
const tgt = _centre(boxes.get(sc.target));
|
|
347
|
+
const n = sc.bendpoints.length;
|
|
348
|
+
return sc.bendpoints.flatMap((bp, i) => {
|
|
349
|
+
const fromSource = src && { x: src.x + bp.startX, y: src.y + bp.startY };
|
|
350
|
+
const fromTarget = tgt && { x: tgt.x + bp.endX, y: tgt.y + bp.endY };
|
|
351
|
+
if (!fromSource || !fromTarget) return fromSource || fromTarget ? [fromSource || fromTarget] : [];
|
|
352
|
+
const w = (i + 1) / (n + 1);
|
|
353
|
+
return [{ x: (1 - w) * fromSource.x + w * fromTarget.x, y: (1 - w) * fromSource.y + w * fromTarget.y }];
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Archi bendpoints for points on the view, between two boxes (whole pixels, as Archi keeps them). @private */
|
|
358
|
+
function _relativeBendpoints(points, sourceBox, targetBox) {
|
|
359
|
+
const src = _centre(sourceBox) ?? VIEW_ORIGIN;
|
|
360
|
+
const tgt = _centre(targetBox) ?? VIEW_ORIGIN;
|
|
361
|
+
return points.map(p => ({
|
|
362
|
+
startX: Math.round(p.x - src.x), startY: Math.round(p.y - src.y),
|
|
363
|
+
endX: Math.round(p.x - tgt.x), endY: Math.round(p.y - tgt.y),
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Convert a parsed view's x/y bendpoints (an older export) to Archi's form. @private */
|
|
368
|
+
function _relativizeLocatedBendpoints(view) {
|
|
369
|
+
const boxes = _visualBoxes(view);
|
|
370
|
+
const walk = (visual) => {
|
|
371
|
+
for (const sc of visual.sourceConnections || []) {
|
|
372
|
+
if (sc.bendpoints?.some(bp => 'x' in bp)) {
|
|
373
|
+
sc.bendpoints = _relativeBendpoints(sc.bendpoints, boxes.get(sc.source), boxes.get(sc.target));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
for (const child of visual.children || []) walk(child);
|
|
377
|
+
};
|
|
378
|
+
walk(view);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function _viewTypeFromXSI(xsiType) {
|
|
382
|
+
const map = {
|
|
383
|
+
'archimate:ArchimateDiagramModel': 'archimate-diagram-model',
|
|
384
|
+
'archimate:SketchModel': 'sketch-model',
|
|
385
|
+
'archimate:CanvasModel': 'canvas-model',
|
|
386
|
+
'canvas:CanvasModel': 'canvas-model', // Archi writes canvases in their own namespace
|
|
387
|
+
};
|
|
388
|
+
return map[xsiType] || xsiType.replace(/^archimate:/, '');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Archi's XML names for the visual classes it renames (EMF extended metadata in
|
|
393
|
+
* Archi's model): xsi:type -> our visual type. Every other class is written
|
|
394
|
+
* under its own name (DiagramModelImage, DiagramModelReference, ...).
|
|
395
|
+
*/
|
|
396
|
+
const ARCHI_VISUAL_XML_NAMES = Object.freeze({
|
|
397
|
+
Group: 'diagram-model-group',
|
|
398
|
+
Note: 'diagram-model-note',
|
|
399
|
+
DiagramObject: 'diagram-object',
|
|
400
|
+
Connection: 'diagram-model-archimate-connection',
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
function _parseVisual(el) {
|
|
404
|
+
const xsiType = el.getAttribute('xsi:type') || '';
|
|
405
|
+
const className = xsiType.replace(/^[a-z]+:/, ''); // archimate:, canvas:, ...
|
|
406
|
+
const type = ARCHI_VISUAL_XML_NAMES[className] ?? className.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
407
|
+
const visual = {
|
|
408
|
+
id: el.getAttribute('id'),
|
|
409
|
+
type,
|
|
410
|
+
archimateElement: el.getAttribute('archimateElement') || null,
|
|
411
|
+
referencedModel: el.getAttribute('referencedModel') || null,
|
|
412
|
+
imagePath: el.getAttribute('imagePath') || null,
|
|
413
|
+
name: el.getAttribute('name') || null,
|
|
414
|
+
text: _childText(el, 'content') || el.getAttribute('text') || null,
|
|
415
|
+
notes: _childText(el, 'notes') || null,
|
|
416
|
+
bounds: _parseBounds(el),
|
|
417
|
+
style: _parseStyle(el),
|
|
418
|
+
archi: _archiExtras(el),
|
|
419
|
+
features: _parseFeatures(el),
|
|
420
|
+
properties: _parseProperties(el),
|
|
421
|
+
sourceConnections: [],
|
|
422
|
+
children: [],
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
for (const sc of el.children.filter(c => c.tagName === 'sourceConnection')) {
|
|
426
|
+
const bendpoints = [];
|
|
427
|
+
for (const bp of sc.children.filter(c => c.tagName === 'bendpoint')) {
|
|
428
|
+
bendpoints.push(bp.getAttribute('x') !== null && bp.getAttribute('startX') === null
|
|
429
|
+
? { x: Number(bp.getAttribute('x')), y: Number(bp.getAttribute('y')) }
|
|
430
|
+
: Object.fromEntries(BENDPOINT_KEYS.map(k => [k, Number(bp.getAttribute(k) ?? 0)])));
|
|
431
|
+
}
|
|
432
|
+
visual.sourceConnections.push({
|
|
433
|
+
id: sc.getAttribute('id'),
|
|
434
|
+
source: sc.getAttribute('source'),
|
|
435
|
+
target: sc.getAttribute('target'),
|
|
436
|
+
// Archi writes archimateRelationship; 'relationship' is what older exports of ours wrote.
|
|
437
|
+
relationship: sc.getAttribute('archimateRelationship') || sc.getAttribute('relationship') || null,
|
|
438
|
+
type: (sc.getAttribute('xsi:type') || '').replace(/^archimate:/, ''),
|
|
439
|
+
// A plain line's class (no xsi:type is Archi's default, DiagramModelConnection) and its look (type bits).
|
|
440
|
+
archiType: _kebab((sc.getAttribute('xsi:type') || 'DiagramModelConnection').replace(/^[a-z]+:/, '')),
|
|
441
|
+
connectionType: Number(sc.getAttribute('type')) || 0,
|
|
442
|
+
archi: _archiExtras(sc, ARCHI_LINE_HANDLED), // a line's lock passes through as written
|
|
443
|
+
name: sc.getAttribute('name') || '',
|
|
444
|
+
style: _parseStyle(sc),
|
|
445
|
+
features: _parseFeatures(sc),
|
|
446
|
+
...(bendpoints.length && { bendpoints }),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
for (const child of el.children.filter(c => c.tagName === 'child')) {
|
|
451
|
+
visual.children.push(_parseVisual(child));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return visual;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function _parseBounds(el) {
|
|
458
|
+
const bounds = el.querySelector('bounds');
|
|
459
|
+
if (!bounds) return null;
|
|
460
|
+
return {
|
|
461
|
+
x: Number(bounds.getAttribute('x')),
|
|
462
|
+
y: Number(bounds.getAttribute('y')),
|
|
463
|
+
width: Number(bounds.getAttribute('width')),
|
|
464
|
+
height: Number(bounds.getAttribute('height')),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const _kebab = (className) => className.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
469
|
+
|
|
470
|
+
/** Attributes the model reads or writes itself; anything else on a visual or line is passed through. */
|
|
471
|
+
const ARCHI_HANDLED_ATTRIBUTES = new Set([
|
|
472
|
+
'xsi:type', 'id', 'archimateElement', 'archimateRelationship', 'source', 'target', 'targetConnections',
|
|
473
|
+
'name', 'imagePath', 'type', 'text', 'locked',
|
|
474
|
+
]);
|
|
475
|
+
|
|
476
|
+
/** The feature that names the logical group (ItemGroup) a box is in: Archi has none of its own. */
|
|
477
|
+
export const ARCHI_GROUP_FEATURE = 'renderengine.group';
|
|
478
|
+
/** The feature that names the kind of that group when it is not a plain one ('background': a template's, pinned behind). */
|
|
479
|
+
export const ARCHI_GROUP_KIND_FEATURE = 'renderengine.groupKind';
|
|
480
|
+
/** The feature that fixes a box in place where Archi has no lock of its own (anything but a canvas box). */
|
|
481
|
+
export const ARCHI_LOCK_FEATURE = 'renderengine.locked';
|
|
482
|
+
/** The feature that marks a view as a template (kept to copy from): Archi has no such flag. */
|
|
483
|
+
export const ARCHI_TEMPLATE_FEATURE = 'renderengine.template';
|
|
484
|
+
|
|
485
|
+
/** The visuals Archi itself can lock (ILockable: its canvas classes). */
|
|
486
|
+
const ARCHI_LOCKABLE = new Set(['canvas-model-block', 'canvas-model-sticky', 'canvas-model-image', 'canvas-model-connection']);
|
|
487
|
+
/** A line's lock is not the arrangement's (which holds boxes): it passes through. */
|
|
488
|
+
const ARCHI_LINE_HANDLED = {
|
|
489
|
+
attributes: new Set(['xsi:type', 'id', 'archimateElement', 'archimateRelationship', 'source', 'target', 'targetConnections', 'name', 'imagePath', 'type', 'text']),
|
|
490
|
+
elements: null, // set below, once ARCHI_HANDLED_ELEMENTS exists
|
|
491
|
+
style: true,
|
|
492
|
+
};
|
|
493
|
+
/** Child elements the model reads itself. */
|
|
494
|
+
const ARCHI_HANDLED_ELEMENTS = new Set(['child', 'sourceConnection', 'bounds', 'content', 'feature', 'property', 'bendpoint', 'documentation']);
|
|
495
|
+
ARCHI_LINE_HANDLED.elements = ARCHI_HANDLED_ELEMENTS;
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* What a visual or line carries that the model does not use, kept to write back:
|
|
499
|
+
* other attributes (locked, hintTitle, imagePosition, alpha, ...), other child
|
|
500
|
+
* elements (hintContent, notes, ...) and the raw style attributes.
|
|
501
|
+
* @private
|
|
502
|
+
*/
|
|
503
|
+
function _archiExtras(el, handled = { attributes: ARCHI_HANDLED_ATTRIBUTES, elements: ARCHI_HANDLED_ELEMENTS, style: true }) {
|
|
504
|
+
const attributes = {};
|
|
505
|
+
const locked = handled.attributes === ARCHI_HANDLED_ATTRIBUTES && el.getAttribute('locked') === 'true';
|
|
506
|
+
for (const [name, value] of Object.entries(el.attributes ?? {})) {
|
|
507
|
+
if (handled.attributes.has(name) || name.startsWith('xmlns')) continue;
|
|
508
|
+
if (handled.style && ARCHI_STYLE_ATTRIBUTES.includes(name)) continue;
|
|
509
|
+
attributes[name] = value;
|
|
510
|
+
}
|
|
511
|
+
const elements = el.children
|
|
512
|
+
.filter(c => !handled.elements.has(c.tagName))
|
|
513
|
+
.map(c => ({ tag: c.tagName, text: c.textContent, attributes: { ...(c.attributes ?? {}) } }));
|
|
514
|
+
return { attributes, elements, ...(handled.style && { style: _parseStyle(el) }), ...(locked && { locked: true }) };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** What a concept (element or relationship) carries that the model does not read: accessType,
|
|
518
|
+
* strength, profiles, ...; null when nothing. Written back on export. @private */
|
|
519
|
+
const ARCHI_CONCEPT_HANDLED = {
|
|
520
|
+
attributes: new Set(['xsi:type', 'id', 'name', 'source', 'target', 'directed']),
|
|
521
|
+
elements: new Set(['documentation', 'property']),
|
|
522
|
+
};
|
|
523
|
+
/** The same for a view (hintTitle, viewpoint, connectionRouterType, hintContent, ...). */
|
|
524
|
+
const ARCHI_VIEW_HANDLED = {
|
|
525
|
+
attributes: new Set(['xsi:type', 'id', 'name']),
|
|
526
|
+
elements: new Set(['documentation', 'property', 'child']),
|
|
527
|
+
};
|
|
528
|
+
/** The same for the model (its profiles, metadata, ...). */
|
|
529
|
+
const ARCHI_MODEL_HANDLED = {
|
|
530
|
+
attributes: new Set(['id', 'name', 'version']),
|
|
531
|
+
elements: new Set(['folder', 'purpose', 'property']),
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/** Extras with nothing in them are null. @private */
|
|
535
|
+
function _extrasOrNull(extras) {
|
|
536
|
+
return Object.keys(extras.attributes).length || extras.elements.length ? extras : null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Child elements kept as {tag, text, attributes}, as lines. @private */
|
|
540
|
+
function _extraElementLines(elements = [], indent) {
|
|
541
|
+
return elements.map(({ tag, text = '', attributes = {} }) => {
|
|
542
|
+
const attrText = Object.entries(attributes).map(([k, v]) => ` ${k}="${_escapeAttr(v)}"`).join('');
|
|
543
|
+
return text ? `${indent}<${tag}${attrText}>${_escapeXml(text)}</${tag}>` : `${indent}<${tag}${attrText}/>`;
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** An instance's `archi` record from its parsed visual and its appearance as imported
|
|
548
|
+
* (null when there is nothing to keep). @private */
|
|
549
|
+
function _archiRecord(visual, appearance) {
|
|
550
|
+
const record = {
|
|
551
|
+
...(visual.id && { id: visual.id }), // Archi's id for the visual, kept on export
|
|
552
|
+
...(visual.archiImagePath && { imagePath: visual.archiImagePath }), // its picture's path in the archive
|
|
553
|
+
...visual.archi,
|
|
554
|
+
...(visual.name && { name: visual.name }),
|
|
555
|
+
...(Object.keys(visual.features ?? {}).length && { features: { ...visual.features } }),
|
|
556
|
+
};
|
|
557
|
+
return { ...record, appearance: { ...appearance } };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Archi keeps a box's line transparency as a feature, not an attribute. */
|
|
561
|
+
const ARCHI_LINE_ALPHA_FEATURE = 'lineAlpha';
|
|
562
|
+
|
|
563
|
+
/** Which appearance fields each Archi style attribute sets. */
|
|
564
|
+
const STYLE_FIELDS = Object.freeze({
|
|
565
|
+
fillColor: ['fillColor'], lineColor: ['strokeColor'], borderColor: ['strokeColor'], fontColor: ['textColor'],
|
|
566
|
+
font: ['fontFamily', 'fontSize', 'fontWeight', 'fontStyle'], lineWidth: ['strokeWidth'],
|
|
567
|
+
textAlignment: ['textAlign'], textPosition: ['textPosition'],
|
|
568
|
+
// Transparency: no fill is alpha 0, no line is the lineAlpha feature at 0.
|
|
569
|
+
alpha: ['fillColor'], [ARCHI_LINE_ALPHA_FEATURE]: ['strokeColor'],
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Archi's style attributes for an appearance: the original value of each one
|
|
574
|
+
* whose meaning is unchanged since import (so values the model does not model,
|
|
575
|
+
* and a font's full SWT string, survive), and the appearance's own value for
|
|
576
|
+
* any changed. Without a record, what the appearance says.
|
|
577
|
+
* @private
|
|
578
|
+
*/
|
|
579
|
+
function _exportStyle(appearance = {}, record = null, { border = false, box = true } = {}) {
|
|
580
|
+
const original = record?.style ?? {};
|
|
581
|
+
const before = record?.appearance ?? _appearanceFromStyle(original, { box });
|
|
582
|
+
const mapped = _styleFromAppearance(appearance, { border, box });
|
|
583
|
+
const style = {};
|
|
584
|
+
for (const [attribute, fields] of Object.entries(STYLE_FIELDS)) {
|
|
585
|
+
const unchanged = fields.every(f => before[f] === appearance[f]);
|
|
586
|
+
if (unchanged && original[attribute] !== undefined) style[attribute] = original[attribute];
|
|
587
|
+
else if (!unchanged && mapped[attribute] !== undefined) style[attribute] = mapped[attribute];
|
|
588
|
+
}
|
|
589
|
+
return style;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Archi's style attributes on a box or line, as written (colours are "#rrggbb"). */
|
|
593
|
+
const ARCHI_STYLE_ATTRIBUTES = Object.freeze([
|
|
594
|
+
'fillColor', 'lineColor', 'borderColor', 'fontColor', 'font', 'lineWidth', 'textAlignment', 'textPosition', 'alpha',
|
|
595
|
+
]);
|
|
596
|
+
|
|
597
|
+
/** The style attributes an element carries. @private */
|
|
598
|
+
function _parseStyle(el) {
|
|
599
|
+
const style = {};
|
|
600
|
+
for (const name of ARCHI_STYLE_ATTRIBUTES) {
|
|
601
|
+
const value = el.getAttribute(name);
|
|
602
|
+
if (value !== null && value !== undefined && value !== '') style[name] = value;
|
|
603
|
+
}
|
|
604
|
+
const lineAlpha = el.children?.find(c => c.tagName === 'feature' && c.getAttribute('name') === ARCHI_LINE_ALPHA_FEATURE);
|
|
605
|
+
if (lineAlpha) style[ARCHI_LINE_ALPHA_FEATURE] = lineAlpha.getAttribute('value');
|
|
606
|
+
return style;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Archi's text alignment (draw2d PositionConstants) and our names for it. */
|
|
610
|
+
const TEXT_ALIGN = Object.freeze({ 1: 'left', 2: 'center', 4: 'right' });
|
|
611
|
+
/** Archi's text position (top, centre, bottom of the box). */
|
|
612
|
+
const TEXT_POSITION = Object.freeze({ 0: 'top', 1: 'middle', 2: 'bottom' });
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* An instance's appearance (ElementInstance#appearance) from Archi's style:
|
|
616
|
+
* fill, line (Archi's lineColor, or borderColor on groups and notes), text
|
|
617
|
+
* colour, line width, font, and text alignment and position.
|
|
618
|
+
* @private
|
|
619
|
+
*/
|
|
620
|
+
function _appearanceFromStyle(style = {}, { box = true } = {}) {
|
|
621
|
+
const a = {};
|
|
622
|
+
if (style.fillColor) a.fillColor = style.fillColor;
|
|
623
|
+
const line = style.lineColor ?? style.borderColor;
|
|
624
|
+
if (line) a.strokeColor = line;
|
|
625
|
+
if (style.fontColor) a.textColor = style.fontColor;
|
|
626
|
+
if (style.lineWidth) a.strokeWidth = Number(style.lineWidth);
|
|
627
|
+
if (box && style.alpha === '0') a.fillColor = 'none';
|
|
628
|
+
if (box && style[ARCHI_LINE_ALPHA_FEATURE] === '0') a.strokeColor = 'none';
|
|
629
|
+
Object.assign(a, _parseFontFeature(style.font));
|
|
630
|
+
// Archi centres a box's text unless the box says otherwise.
|
|
631
|
+
if (TEXT_ALIGN[style.textAlignment] || box) a.textAlign = TEXT_ALIGN[style.textAlignment] ?? 'center';
|
|
632
|
+
if (TEXT_POSITION[style.textPosition]) a.textPosition = TEXT_POSITION[style.textPosition];
|
|
633
|
+
return a;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Archi's style attributes back from an appearance (the inverse of _appearanceFromStyle). @private */
|
|
637
|
+
function _styleFromAppearance(a = {}, { border = false, box = true } = {}) {
|
|
638
|
+
const style = {};
|
|
639
|
+
if (a.fillColor === 'none') style.alpha = '0';
|
|
640
|
+
else if (a.fillColor) style.fillColor = a.fillColor;
|
|
641
|
+
if (box && a.strokeColor === 'none') style[ARCHI_LINE_ALPHA_FEATURE] = '0';
|
|
642
|
+
if (a.strokeColor && a.strokeColor !== 'none') style[border ? 'borderColor' : 'lineColor'] = a.strokeColor;
|
|
643
|
+
if (a.textColor) style.fontColor = a.textColor;
|
|
644
|
+
if (a.strokeWidth !== undefined) style.lineWidth = String(a.strokeWidth);
|
|
645
|
+
if (a.fontFamily || a.fontSize || a.fontWeight || a.fontStyle) style.font = _fontString(a);
|
|
646
|
+
const align = Object.entries(TEXT_ALIGN).find(([, name]) => name === a.textAlign)?.[0];
|
|
647
|
+
if (align) style.textAlignment = align;
|
|
648
|
+
const position = Object.entries(TEXT_POSITION).find(([, name]) => name === a.textPosition)?.[0];
|
|
649
|
+
if (position) style.textPosition = position;
|
|
650
|
+
return style;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function _parseFeatures(el) {
|
|
654
|
+
const features = {};
|
|
655
|
+
for (const f of el.children.filter(c => c.tagName === 'feature')) {
|
|
656
|
+
if (f.getAttribute('name') === ARCHI_LINE_ALPHA_FEATURE) continue; // style (see _parseStyle)
|
|
657
|
+
features[f.getAttribute('name')] = f.getAttribute('value');
|
|
658
|
+
}
|
|
659
|
+
return features;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** An element's own properties as [{key, value}], in file order (Archi allows a key more than once). @private */
|
|
663
|
+
function _propertyPairs(el) {
|
|
664
|
+
return el.children.filter(c => c.tagName === 'property').map(p => ({ key: p.getAttribute('key') ?? '', value: p.getAttribute('value') ?? '' }));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function _parseProperties(el) {
|
|
668
|
+
const props = {};
|
|
669
|
+
for (const p of el.children.filter(c => c.tagName === 'property')) {
|
|
670
|
+
props[p.getAttribute('key')] = p.getAttribute('value');
|
|
671
|
+
}
|
|
672
|
+
return props;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** Archi native format stores flat string properties. RenderEngine stores a
|
|
676
|
+
* typed array `{name, type, value}`. Import converts everything to 'string'
|
|
677
|
+
* by default; callers can retype in the UI. */
|
|
678
|
+
function _propertiesToTypedArray(props) {
|
|
679
|
+
if (!props || Object.keys(props).length === 0) return undefined;
|
|
680
|
+
return Object.entries(props).map(([name, value]) => ({ name, type: 'string', value }));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Export: stringify typed values back to Archi's flat key/value strings. */
|
|
684
|
+
function _typedArrayToProperties(arr) {
|
|
685
|
+
const out = {};
|
|
686
|
+
for (const { name, value } of arr || []) {
|
|
687
|
+
if (!name) continue;
|
|
688
|
+
out[name] = value == null ? '' : String(value);
|
|
689
|
+
}
|
|
690
|
+
return out;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** The text of an element's own child `tagName` (not a nested element's: a canvas block holds stickies with their own content). @private */
|
|
694
|
+
function _childText(el, tagName) {
|
|
695
|
+
const child = el.children.find(c => c.tagName === tagName);
|
|
696
|
+
return child ? child.textContent : null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** Convert a parsed Archi model into an ArchimateModel (with views).
|
|
700
|
+
* @param {Object} archiModel - parsed Archi XML model
|
|
701
|
+
* @returns {ArchimateModel} with one view per original view (elementIds track which elements appear in each view)
|
|
702
|
+
* Now applies visual bounds/waypoints per view. */
|
|
703
|
+
export function toArchimateModel(archiModel) {
|
|
704
|
+
const elements = [];
|
|
705
|
+
const relationships = [];
|
|
706
|
+
const folders = [];
|
|
707
|
+
const elementFolders = [];
|
|
708
|
+
const views = [];
|
|
709
|
+
const elementById = new Map();
|
|
710
|
+
const relationshipById = new Map();
|
|
711
|
+
|
|
712
|
+
// Map all elements first
|
|
713
|
+
for (const el of archiModel.elements || []) {
|
|
714
|
+
const mapped = _mapElement(el);
|
|
715
|
+
elementById.set(el.id, mapped);
|
|
716
|
+
elements.push(mapped);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Map all relationships
|
|
720
|
+
for (const rel of archiModel.relationships || []) {
|
|
721
|
+
const mapped = _mapRelationship(rel);
|
|
722
|
+
relationshipById.set(rel.id, mapped);
|
|
723
|
+
relationships.push(mapped);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Each view: one instance per box Archi draws, and one per line.
|
|
727
|
+
for (const archiView of archiModel.views || []) {
|
|
728
|
+
const v = _viewInstances(archiView, { elementById, relationshipById });
|
|
729
|
+
// Our template flag is a feature on the view; the rest of its extras pass through.
|
|
730
|
+
const isTemplateFeature = (e) => e.tag === 'feature' && e.attributes?.name === ARCHI_TEMPLATE_FEATURE;
|
|
731
|
+
const template = !!archiView.archi?.elements?.some(e => isTemplateFeature(e) && e.attributes?.value === 'true');
|
|
732
|
+
if (archiView.archi?.elements) archiView.archi.elements = archiView.archi.elements.filter(e => !isTemplateFeature(e));
|
|
733
|
+
views.push({
|
|
734
|
+
id: archiView.id,
|
|
735
|
+
name: archiView.name,
|
|
736
|
+
...(archiView.documentation && { documentation: archiView.documentation }),
|
|
737
|
+
elementIds: v.elementIds,
|
|
738
|
+
elementInstances: v.elementInstances,
|
|
739
|
+
relationshipInstances: v.relationshipInstances,
|
|
740
|
+
viewConnections: v.viewConnections,
|
|
741
|
+
arrangement: v.arrangement,
|
|
742
|
+
archiViewType: archiView.type,
|
|
743
|
+
...(template && { template }),
|
|
744
|
+
...(archiView.properties?.length && { properties: archiView.properties }),
|
|
745
|
+
...(archiView.archiFolder && { archiFolder: archiView.archiFolder }),
|
|
746
|
+
...(archiView.archi && { archi: archiView.archi }),
|
|
747
|
+
...(v.folders.length && { folders: v.folders, folderLayout: v.folderLayout }),
|
|
748
|
+
});
|
|
749
|
+
folders.push(...v.folders);
|
|
750
|
+
elementFolders.push(...v.elementFolders);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return new ArchimateModel({
|
|
754
|
+
id: archiModel.id ?? undefined,
|
|
755
|
+
name: archiModel.name ?? '', elements, relationships, folders, elementFolders, views,
|
|
756
|
+
documentation: archiModel.documentation || undefined, properties: archiModel.properties ?? [],
|
|
757
|
+
// The specializations written for element types Archi lacks are not the model's own.
|
|
758
|
+
archi: archiModel.archi ? {
|
|
759
|
+
...archiModel.archi,
|
|
760
|
+
elements: (archiModel.archi.elements ?? []).filter(e => !(e.tag === 'profile' && String(e.attributes?.id ?? '').startsWith(ARCHI_TYPE_PROFILE_PREFIX))),
|
|
761
|
+
} : null,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** Where a visual sits on the view. Archi stores a nested visual's bounds
|
|
766
|
+
* relative to its parent (a group, or an element drawn inside another), so
|
|
767
|
+
* they are offset by the parent's absolute position. Null without bounds. @private */
|
|
768
|
+
function _absoluteBounds(visual, origin) {
|
|
769
|
+
if (!visual.bounds) return null;
|
|
770
|
+
const { x, y, width, height } = visual.bounds;
|
|
771
|
+
const size = ARCHI_DEFAULT_SIZE[visual.type] ?? ARCHI_ELEMENT_SIZE;
|
|
772
|
+
return {
|
|
773
|
+
x: origin.x + x, y: origin.y + y,
|
|
774
|
+
width: width > 0 ? width : size.width,
|
|
775
|
+
height: height > 0 ? height : size.height,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const VIEW_ORIGIN = Object.freeze({ x: 0, y: 0 });
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Boxes on canvas and sketch views: no model element behind them, drawn as a
|
|
783
|
+
* shape with text (and, for a canvas sticky, maybe a picture). Archi type ->
|
|
784
|
+
* the renderer's shape.
|
|
785
|
+
*/
|
|
786
|
+
const VIEW_BOX_SHAPES = Object.freeze({
|
|
787
|
+
'canvas-model-block': 'rect',
|
|
788
|
+
'canvas-model-sticky': 'rect',
|
|
789
|
+
'sketch-model-sticky': 'rect',
|
|
790
|
+
'sketch-model-actor': 'stick-figure',
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
/** What a view-only box draws (a ViewObjectInstance's content), from its Archi visual. @private */
|
|
794
|
+
function _viewObjectContent(visual) {
|
|
795
|
+
return _viewObjectDrawing(visual);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** @private */
|
|
799
|
+
function _viewObjectDrawing(visual) {
|
|
800
|
+
const text = visual.text || visual.name || '';
|
|
801
|
+
if (visual.type === 'diagram-model-note') {
|
|
802
|
+
// A note's text is its own content; an empty one shows its name, if any (Archi's legend note).
|
|
803
|
+
return { id: visual.id, name: visual.text || visual.name || '', label: visual.text || '', shape: 'note' };
|
|
804
|
+
}
|
|
805
|
+
if (visual.type === 'diagram-model-image' || visual.type === 'canvas-model-image') {
|
|
806
|
+
return {
|
|
807
|
+
id: visual.id, name: visual.name || 'Image', shape: 'image', iconUrl: visual.imagePath,
|
|
808
|
+
...(visual.properties?.href && { href: visual.properties.href }),
|
|
809
|
+
...(visual.type === 'canvas-model-image' && { archiVisualType: visual.type }),
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
const shape = VIEW_BOX_SHAPES[visual.type];
|
|
813
|
+
if (!shape) return null;
|
|
814
|
+
return {
|
|
815
|
+
id: visual.id, name: text, label: text, shape, // an empty box shows no text
|
|
816
|
+
archiVisualType: visual.type,
|
|
817
|
+
...(visual.imagePath && { iconUrl: visual.imagePath }),
|
|
818
|
+
...(visual.notes && { documentation: visual.notes }),
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** Where a box is drawn: its centre, as the renderer places nodes (Archi keeps the top-left corner). @private */
|
|
823
|
+
const _centreOf = (bounds) => ({ x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 });
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* One Archi view as instances: an ElementInstance per box that shows a model
|
|
827
|
+
* element (so the same element twice is two instances), a ViewObjectInstance
|
|
828
|
+
* per note or image, each centred on its box with that box's size and style;
|
|
829
|
+
* a RelationshipInstance per line, joining the two instances it joins, with its
|
|
830
|
+
* bend points (none is a straight line); groups as folders with their bounds.
|
|
831
|
+
* @private
|
|
832
|
+
*/
|
|
833
|
+
function _viewInstances(archiView, { elementById, relationshipById }) {
|
|
834
|
+
const boxes = _visualBoxes(archiView);
|
|
835
|
+
const elementInstances = new Map();
|
|
836
|
+
const instanceOfVisual = new Map(); // Archi visual id -> instance id
|
|
837
|
+
const elementIds = new Set();
|
|
838
|
+
const folders = [];
|
|
839
|
+
const folderLayout = {};
|
|
840
|
+
const elementFolders = [];
|
|
841
|
+
const lines = [];
|
|
842
|
+
|
|
843
|
+
let order = 0; // document order is Archi's drawing order
|
|
844
|
+
const locked = [];
|
|
845
|
+
const groupMembers = new Map(); // group id -> instance ids
|
|
846
|
+
const groupKinds = new Map(); // group id -> its kind, when not a plain group
|
|
847
|
+
const place = (inst, bounds, style, type, visual) => {
|
|
848
|
+
inst.size = { width: bounds.width, height: bounds.height };
|
|
849
|
+
inst.zIndex = order++;
|
|
850
|
+
inst.appearance = _appearanceFromStyle(style);
|
|
851
|
+
// Archi centres a canvas sticky's text vertically unless it says otherwise.
|
|
852
|
+
if (type === 'canvas-model-sticky' && !TEXT_POSITION[style?.textPosition]) inst.appearance.textPosition = 'middle';
|
|
853
|
+
// Archi draws a canvas box without a border colour with no border.
|
|
854
|
+
if (type?.startsWith('canvas-model-') && !style?.borderColor && !style?.lineColor) inst.appearance.strokeColor = 'none';
|
|
855
|
+
inst.archi = _archiRecord(visual, inst.appearance);
|
|
856
|
+
// Archi's lock, and our logical group (kept as a feature), go to the view's arrangement.
|
|
857
|
+
const features = inst.archi?.features ?? {};
|
|
858
|
+
if (visual.archi?.locked || features[ARCHI_LOCK_FEATURE] === 'true') locked.push(inst.id);
|
|
859
|
+
const groupId = features[ARCHI_GROUP_FEATURE];
|
|
860
|
+
if (groupId) (groupMembers.get(groupId) ?? groupMembers.set(groupId, []).get(groupId)).push(inst.id);
|
|
861
|
+
if (groupId && features[ARCHI_GROUP_KIND_FEATURE]) groupKinds.set(groupId, features[ARCHI_GROUP_KIND_FEATURE]);
|
|
862
|
+
if (inst.archi?.features) {
|
|
863
|
+
delete inst.archi.features[ARCHI_GROUP_FEATURE];
|
|
864
|
+
delete inst.archi.features[ARCHI_GROUP_KIND_FEATURE];
|
|
865
|
+
delete inst.archi.features[ARCHI_LOCK_FEATURE];
|
|
866
|
+
if (!Object.keys(inst.archi.features).length) delete inst.archi.features;
|
|
867
|
+
}
|
|
868
|
+
elementInstances.set(inst.id, inst);
|
|
869
|
+
return inst;
|
|
870
|
+
};
|
|
871
|
+
const walk = (visual, parentFolderId, origin, parentBoxId = null) => {
|
|
872
|
+
const bounds = _absoluteBounds(visual, origin);
|
|
873
|
+
const instanceCount = elementInstances.size;
|
|
874
|
+
if (bounds) {
|
|
875
|
+
const { x, y } = _centreOf(bounds);
|
|
876
|
+
if (visual.archimateElement && elementById.has(visual.archimateElement)) {
|
|
877
|
+
const inst = place(new ElementInstance(visual.archimateElement, x, y), bounds, visual.style, visual.type, visual);
|
|
878
|
+
instanceOfVisual.set(visual.id, inst.id);
|
|
879
|
+
elementIds.add(visual.archimateElement);
|
|
880
|
+
if (parentFolderId) elementFolders.push([visual.archimateElement, parentFolderId]);
|
|
881
|
+
} else {
|
|
882
|
+
// Notes, images, and canvas and sketch boxes: drawn on this view only.
|
|
883
|
+
const content = _viewObjectContent(visual);
|
|
884
|
+
if (content) {
|
|
885
|
+
const inst = place(new ViewObjectInstance({ ...content, width: bounds.width, height: bounds.height }, x, y), bounds, visual.style, visual.type, visual);
|
|
886
|
+
instanceOfVisual.set(visual.id, inst.id);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (visual.type === 'diagram-model-group') {
|
|
891
|
+
folders.push({
|
|
892
|
+
id: visual.id,
|
|
893
|
+
name: visual.name || 'Group',
|
|
894
|
+
documentation: visual.documentation || undefined,
|
|
895
|
+
parentId: parentFolderId || undefined,
|
|
896
|
+
children: [],
|
|
897
|
+
});
|
|
898
|
+
if (parentFolderId) folders.find(f => f.id === parentFolderId)?.children.push(visual.id);
|
|
899
|
+
if (bounds) {
|
|
900
|
+
// Its paint as an instance keeps it (the canvas editor draws and edits it), and the record to write back.
|
|
901
|
+
const appearance = _appearanceFromStyle(visual.style);
|
|
902
|
+
folderLayout[visual.id] = {
|
|
903
|
+
x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, collapsed: false, zIndex: order++,
|
|
904
|
+
appearance, archi: _archiRecord(visual, appearance),
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
instanceOfVisual.set(visual.id, visual.id); // a plain line may end at a group
|
|
908
|
+
}
|
|
909
|
+
// A box drawn inside another box, or a note or picture inside a group (an element's group
|
|
910
|
+
// is its folder): kept so the canvas moves it with its parent and export nests it there again.
|
|
911
|
+
const isBox = elementInstances.size > instanceCount;
|
|
912
|
+
const parent = parentBoxId ?? (visual.archimateElement ? null : parentFolderId);
|
|
913
|
+
if (isBox && parent) {
|
|
914
|
+
const inst = [...elementInstances.values()].at(-1);
|
|
915
|
+
inst.archi = { ...(inst.archi ?? {}), parent };
|
|
916
|
+
}
|
|
917
|
+
lines.push(...(visual.sourceConnections || []));
|
|
918
|
+
const folderId = visual.type === 'diagram-model-group' ? visual.id : parentFolderId;
|
|
919
|
+
const boxId = isBox ? visual.id : (visual.type === 'diagram-model-group' ? null : parentBoxId);
|
|
920
|
+
for (const child of visual.children || []) walk(child, folderId, bounds ?? origin, boxId);
|
|
921
|
+
};
|
|
922
|
+
for (const child of archiView.children || []) walk(child, null, VIEW_ORIGIN);
|
|
923
|
+
|
|
924
|
+
const relationshipInstances = new Map();
|
|
925
|
+
const viewConnections = [];
|
|
926
|
+
for (const sc of lines) {
|
|
927
|
+
const source = instanceOfVisual.get(sc.source);
|
|
928
|
+
const target = instanceOfVisual.get(sc.target);
|
|
929
|
+
if (!source || !target) continue;
|
|
930
|
+
if (!sc.relationship) {
|
|
931
|
+
// A plain line: Archi's connection between any two boxes, or a canvas connection.
|
|
932
|
+
const connection = new ViewConnectionInstance({
|
|
933
|
+
id: sc.id, sourceInstanceId: source, targetInstanceId: target,
|
|
934
|
+
waypoints: sc.bendpoints?.length ? _bendpointLocations(sc, boxes) : [],
|
|
935
|
+
type: sc.connectionType, label: sc.name, appearance: _appearanceFromStyle(sc.style, { box: false }), archiType: sc.archiType,
|
|
936
|
+
});
|
|
937
|
+
connection.archi = _archiRecord(sc, connection.appearance);
|
|
938
|
+
viewConnections.push(connection);
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (!relationshipById.has(sc.relationship)) continue;
|
|
942
|
+
const inst = new RelationshipInstance(sc.relationship, sc.bendpoints?.length ? _bendpointLocations(sc, boxes) : [], source, target);
|
|
943
|
+
inst.appearance = _appearanceFromStyle(sc.style, { box: false });
|
|
944
|
+
inst.archi = _archiRecord(sc, inst.appearance);
|
|
945
|
+
relationshipInstances.set(inst.id, inst);
|
|
946
|
+
}
|
|
947
|
+
const arrangement = {
|
|
948
|
+
// A plain group needs two members; a kind of group (a background) may have one.
|
|
949
|
+
groups: [...groupMembers].filter(([id, ids]) => ids.length > 1 || groupKinds.has(id))
|
|
950
|
+
.map(([id, memberIds]) => ({ id, memberIds, ...(groupKinds.has(id) && { kind: groupKinds.get(id) }) })),
|
|
951
|
+
locked,
|
|
952
|
+
};
|
|
953
|
+
return { elementIds: [...elementIds], elementInstances, relationshipInstances, viewConnections, folders, folderLayout, elementFolders, arrangement };
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/** Convert a parsed Archi model into renderengine inputs.
|
|
957
|
+
* Returns `{ elements, relationships, options }` ready for `archimateDiagram()`.
|
|
958
|
+
* If `viewId` is omitted, the first view is used.
|
|
959
|
+
* Kept as original for backward compatibility: applies visual bounds/features and handles all view visuals. */
|
|
960
|
+
export function toRenderEngineModel(archiModel, viewId) {
|
|
961
|
+
const view = viewId
|
|
962
|
+
? archiModel.views.find(v => v.id === viewId)
|
|
963
|
+
: archiModel.views[0];
|
|
964
|
+
|
|
965
|
+
const elements = [];
|
|
966
|
+
const relationships = [];
|
|
967
|
+
const annotations = [];
|
|
968
|
+
const elementById = new Map();
|
|
969
|
+
const relationshipById = new Map();
|
|
970
|
+
|
|
971
|
+
for (const el of archiModel.elements || []) {
|
|
972
|
+
const mapped = _mapElement(el);
|
|
973
|
+
elementById.set(el.id, mapped);
|
|
974
|
+
elements.push(mapped);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
for (const rel of archiModel.relationships || []) {
|
|
978
|
+
const mapped = _mapRelationship(rel);
|
|
979
|
+
relationshipById.set(rel.id, mapped);
|
|
980
|
+
relationships.push(mapped);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const folders = [];
|
|
984
|
+
const folderLayout = {};
|
|
985
|
+
const elementFolders = []; // Array of [elementId, folderId] pairs
|
|
986
|
+
|
|
987
|
+
if (view) {
|
|
988
|
+
const boxes = _visualBoxes(view);
|
|
989
|
+
for (const visual of view.children || []) {
|
|
990
|
+
_mapVisual(visual, { elementById, relationshipById, elements, relationships, annotations, folders, folderLayout, elementFolders, boxes });
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const options = {
|
|
995
|
+
annotations,
|
|
996
|
+
archimateVersion: '4',
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
return { elements, relationships, folders, folderLayout, elementFolders, options };
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function _mapElement(el) {
|
|
1003
|
+
const archiType = _toKebabType(el.type);
|
|
1004
|
+
const layer = ArchimateNotation.elementLayer({ type: archiType });
|
|
1005
|
+
// A type Archi has no class for was written as the nearest one, with its own kept as a feature.
|
|
1006
|
+
const ownType = el.archi?.elements?.find(e => e.tag === 'feature' && e.attributes?.name === ARCHI_TYPE_FEATURE)?.attributes?.value;
|
|
1007
|
+
const mapped = {
|
|
1008
|
+
id: el.id,
|
|
1009
|
+
name: el.name,
|
|
1010
|
+
type: ownType || archiType,
|
|
1011
|
+
layer: layer !== 'other' ? layer : undefined,
|
|
1012
|
+
documentation: el.documentation || undefined,
|
|
1013
|
+
};
|
|
1014
|
+
const props = _propertiesToTypedArray(el.properties);
|
|
1015
|
+
if (props) mapped.properties = props;
|
|
1016
|
+
if (el.archiFolder) mapped.archiFolder = el.archiFolder;
|
|
1017
|
+
if (el.archi) mapped.archi = ownType ? _withoutTypeMarks(el.archi) : el.archi;
|
|
1018
|
+
return mapped;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/** An element's archi record without the feature and specialization written for its own type. @private */
|
|
1022
|
+
function _withoutTypeMarks(archi) {
|
|
1023
|
+
const attributes = { ...(archi.attributes ?? {}) };
|
|
1024
|
+
if (String(attributes.profiles ?? '').startsWith(ARCHI_TYPE_PROFILE_PREFIX)) delete attributes.profiles;
|
|
1025
|
+
return {
|
|
1026
|
+
...archi,
|
|
1027
|
+
attributes,
|
|
1028
|
+
elements: (archi.elements ?? []).filter(e => !(e.tag === 'feature' && e.attributes?.name === ARCHI_TYPE_FEATURE)),
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function _mapRelationship(rel) {
|
|
1033
|
+
const base = _toKebabType(rel.type).replace(/-relationship$/, '');
|
|
1034
|
+
const kind = base === 'association' && rel.directed ? 'association-directed' : base;
|
|
1035
|
+
const mapped = {
|
|
1036
|
+
id: rel.id,
|
|
1037
|
+
source: rel.source,
|
|
1038
|
+
target: rel.target,
|
|
1039
|
+
kind,
|
|
1040
|
+
label: rel.name || undefined,
|
|
1041
|
+
documentation: rel.documentation || undefined,
|
|
1042
|
+
};
|
|
1043
|
+
const props = _propertiesToTypedArray(rel.properties);
|
|
1044
|
+
if (props) mapped.properties = props;
|
|
1045
|
+
if (rel.archiFolder) mapped.archiFolder = rel.archiFolder;
|
|
1046
|
+
if (rel.archi) mapped.archi = rel.archi;
|
|
1047
|
+
return mapped;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function _mapVisual(visual, ctx, parentBounds = null, parentFolderId = null, origin = VIEW_ORIGIN) {
|
|
1051
|
+
const own = _absoluteBounds(visual, origin);
|
|
1052
|
+
const bounds = own || parentBounds || { x: 0, y: 0, width: 120, height: 55 };
|
|
1053
|
+
// Children are placed relative to this visual if it has bounds, else as it is placed.
|
|
1054
|
+
const childOrigin = own ?? origin;
|
|
1055
|
+
// Placed by its centre, as the renderer draws nodes; styled under the renderer's names.
|
|
1056
|
+
const { x, y } = _centreOf(bounds);
|
|
1057
|
+
const { fillColor, ...appearance } = _appearanceFromStyle(visual.style);
|
|
1058
|
+
const visualProps = {
|
|
1059
|
+
x, y,
|
|
1060
|
+
width: bounds.width,
|
|
1061
|
+
height: bounds.height,
|
|
1062
|
+
...appearance,
|
|
1063
|
+
...(fillColor && { color: fillColor }),
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
if (visual.archimateElement) {
|
|
1067
|
+
const element = ctx.elementById.get(visual.archimateElement);
|
|
1068
|
+
if (element) {
|
|
1069
|
+
Object.assign(element, visualProps);
|
|
1070
|
+
// A DiagramObject may carry a per-view text override.
|
|
1071
|
+
if (visual.text) element.label = visual.text;
|
|
1072
|
+
}
|
|
1073
|
+
// Track which folder contains this element (if any)
|
|
1074
|
+
if (parentFolderId && ctx.elementFolders) {
|
|
1075
|
+
ctx.elementFolders.push([visual.archimateElement, parentFolderId]);
|
|
1076
|
+
}
|
|
1077
|
+
// On to its connections and to the elements drawn inside it (below).
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const base = { id: visual.id, ...visualProps };
|
|
1081
|
+
|
|
1082
|
+
if (visual.type === 'diagram-model-note') {
|
|
1083
|
+
ctx.annotations.push({
|
|
1084
|
+
...base,
|
|
1085
|
+
label: visual.text || 'Note',
|
|
1086
|
+
shape: 'note',
|
|
1087
|
+
});
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (visual.type === 'diagram-model-group') {
|
|
1092
|
+
const folder = {
|
|
1093
|
+
id: visual.id,
|
|
1094
|
+
name: visual.name || 'Group',
|
|
1095
|
+
documentation: visual.documentation || undefined,
|
|
1096
|
+
parentId: parentFolderId || undefined,
|
|
1097
|
+
children: [],
|
|
1098
|
+
};
|
|
1099
|
+
ctx.folders.push(folder);
|
|
1100
|
+
ctx.folderLayout[visual.id] = {
|
|
1101
|
+
x: bounds.x,
|
|
1102
|
+
y: bounds.y,
|
|
1103
|
+
width: visualProps.width,
|
|
1104
|
+
height: visualProps.height,
|
|
1105
|
+
collapsed: false,
|
|
1106
|
+
};
|
|
1107
|
+
// Recurse into children with this folder as parent
|
|
1108
|
+
for (const child of visual.children || []) {
|
|
1109
|
+
_mapVisual(child, ctx, bounds, visual.id, childOrigin);
|
|
1110
|
+
}
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (visual.type === 'diagram-model-image') {
|
|
1115
|
+
ctx.annotations.push({
|
|
1116
|
+
...base,
|
|
1117
|
+
label: visual.name || '',
|
|
1118
|
+
iconUrl: visual.imagePath,
|
|
1119
|
+
shape: 'image',
|
|
1120
|
+
href: visual.properties?.href,
|
|
1121
|
+
});
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
if (visual.type === 'diagram-model-reference') {
|
|
1126
|
+
ctx.annotations.push({
|
|
1127
|
+
...base,
|
|
1128
|
+
label: visual.name || 'Reference',
|
|
1129
|
+
reference: visual.referencedModel,
|
|
1130
|
+
shape: 'note',
|
|
1131
|
+
});
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if ([
|
|
1136
|
+
'sketch-model-sticky', 'sketch-model-actor',
|
|
1137
|
+
'canvas-model-block', 'canvas-model-sticky', 'canvas-model-image',
|
|
1138
|
+
].includes(visual.type)) {
|
|
1139
|
+
ctx.annotations.push({
|
|
1140
|
+
...base,
|
|
1141
|
+
label: visual.text || visual.name || visual.type,
|
|
1142
|
+
archiVisualType: visual.type,
|
|
1143
|
+
shape: visual.type.endsWith('-image') ? 'rect' : 'note',
|
|
1144
|
+
});
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// An element's connections, then the visuals nested in it.
|
|
1149
|
+
for (const sc of visual.sourceConnections || []) {
|
|
1150
|
+
if (sc.relationship && sc.bendpoints?.length) {
|
|
1151
|
+
// Apply bendpoints from the visual connection to the relationship model
|
|
1152
|
+
const rel = ctx.relationshipById?.get(sc.relationship);
|
|
1153
|
+
if (rel) {
|
|
1154
|
+
rel.waypoints = _bendpointLocations(sc, ctx.boxes);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
for (const child of visual.children || []) {
|
|
1160
|
+
_mapVisual(child, ctx, bounds, parentFolderId, childOrigin);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function _toKebabType(name) {
|
|
1165
|
+
if (!name) return '';
|
|
1166
|
+
return name
|
|
1167
|
+
.replace(/^archimate:/, '')
|
|
1168
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
1169
|
+
.toLowerCase();
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Archi's font: SWT FontData, "version|name|height in points|style|platform|...",
|
|
1174
|
+
* style bit 1 bold, bit 2 italic. The size is kept in points ("9pt"), which SVG
|
|
1175
|
+
* draws as Archi does.
|
|
1176
|
+
* @private
|
|
1177
|
+
*/
|
|
1178
|
+
function _parseFontFeature(fontString) {
|
|
1179
|
+
if (!fontString) return {};
|
|
1180
|
+
const [, family, height, style] = fontString.split('|');
|
|
1181
|
+
const result = {};
|
|
1182
|
+
if (family) result.fontFamily = family;
|
|
1183
|
+
if (height && Number(height) > 0) result.fontSize = `${Number(height)}pt`;
|
|
1184
|
+
const bits = Number(style) || 0;
|
|
1185
|
+
if (bits & 1) result.fontWeight = 'bold';
|
|
1186
|
+
if (bits & 2) result.fontStyle = 'italic';
|
|
1187
|
+
return result;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/** A font as SWT FontData's leading fields, which Archi reads (inverse of _parseFontFeature). @private */
|
|
1191
|
+
function _fontString({ fontFamily = 'Segoe UI', fontSize = '9pt', fontWeight, fontStyle } = {}) {
|
|
1192
|
+
const points = String(fontSize).endsWith('px') ? parseFloat(fontSize) * 0.75 : parseFloat(fontSize);
|
|
1193
|
+
const bits = (fontWeight === 'bold' ? 1 : 0) | (fontStyle === 'italic' ? 2 : 0);
|
|
1194
|
+
return `1|${fontFamily}|${points}|${bits}`;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/** Convert an ArchimateView to Archi's visual-tree shape, ready for toArchiXML.
|
|
1198
|
+
* Returns array of visual child objects with bounds, sourceConnections (including bendpoints), and nested children.
|
|
1199
|
+
* This is the export-side bridge: ArchimateView → Archi visual tree → XML. */
|
|
1200
|
+
export function fromArchimateView(view) {
|
|
1201
|
+
const visuals = [];
|
|
1202
|
+
const relationshipsBySource = new Map();
|
|
1203
|
+
const folderById = new Map();
|
|
1204
|
+
|
|
1205
|
+
// Index relationships by source visual ID for quick lookup
|
|
1206
|
+
for (const rel of view.relationships || []) {
|
|
1207
|
+
if (rel.viewConnection) continue; // plain lines are written from view.viewConnections (below)
|
|
1208
|
+
if (!relationshipsBySource.has(rel.source)) {
|
|
1209
|
+
relationshipsBySource.set(rel.source, []);
|
|
1210
|
+
}
|
|
1211
|
+
relationshipsBySource.get(rel.source).push(rel);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Groups: an ArchimateView's placedFolders (each after its parent, parentId the
|
|
1215
|
+
// nearest group on the view); a plain view object's `folders` are all top level.
|
|
1216
|
+
const groups = view.placedFolders ?? (view.folders || []).map(f => ({ ...f, parentId: undefined }));
|
|
1217
|
+
const absolute = new Map(); // visual -> its bounds on the view
|
|
1218
|
+
const stacking = new Map(); // visual -> its zIndex (Infinity: on top, in the order placed)
|
|
1219
|
+
/** Put a visual on the view, or inside the group `groupId` with bounds relative to it. */
|
|
1220
|
+
const placedById = new Map(); // visual id -> visual, for nesting a box in its box
|
|
1221
|
+
const place = (visual, groupId, zIndex = Infinity, parentBoxId = null) => {
|
|
1222
|
+
absolute.set(visual, visual.bounds);
|
|
1223
|
+
stacking.set(visual, zIndex);
|
|
1224
|
+
placedById.set(visual.id, visual);
|
|
1225
|
+
const group = (parentBoxId && placedById.get(parentBoxId)) || (groupId && folderById.get(groupId));
|
|
1226
|
+
if (!group) { visuals.push(visual); return; }
|
|
1227
|
+
const origin = absolute.get(group);
|
|
1228
|
+
visual.bounds = { ...visual.bounds, x: visual.bounds.x - origin.x, y: visual.bounds.y - origin.y };
|
|
1229
|
+
(group.children ??= []).push(visual); // a box gets its first child here
|
|
1230
|
+
};
|
|
1231
|
+
for (const folder of groups) {
|
|
1232
|
+
const { x = 0, y = 0, width = 200, height = 150 } = view.folderLayout?.[folder.id] ?? folder;
|
|
1233
|
+
const folderVisual = {
|
|
1234
|
+
id: folder.id,
|
|
1235
|
+
type: 'diagram-model-group',
|
|
1236
|
+
name: folder.name,
|
|
1237
|
+
bounds: { x, y, width, height },
|
|
1238
|
+
children: [],
|
|
1239
|
+
sourceConnections: [],
|
|
1240
|
+
};
|
|
1241
|
+
// Its paint as the view has it now: the original attributes where unchanged (as for a box).
|
|
1242
|
+
_applyArchiRecord(folderVisual, folder.archi, folder.appearance ? _exportStyle(folder.appearance, folder.archi, { border: true }) : (folder.archi?.style ?? {}));
|
|
1243
|
+
const folderZ = view.folderLayout?.[folder.id]?.zIndex ?? folder.zIndex;
|
|
1244
|
+
if (folder.documentation) {
|
|
1245
|
+
folderVisual.documentation = folder.documentation;
|
|
1246
|
+
}
|
|
1247
|
+
folderById.set(folder.id, folderVisual);
|
|
1248
|
+
place(folderVisual, folder.parentId, folderZ ?? Infinity);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/** The visual id for a drawn item: Archi's own when it was imported, else one made from the instance id. */
|
|
1252
|
+
const visualIdOf = (instanceId) => view.getElementInstance?.(instanceId)?.visualId ?? `diag-${instanceId}`;
|
|
1253
|
+
|
|
1254
|
+
// One visual per drawn item. A view's items carry their instance id as `id`
|
|
1255
|
+
// and the model element's as `elementId` (a plain {id} item is both).
|
|
1256
|
+
// Each item is placed by its centre, as the renderer draws it; Archi keeps the
|
|
1257
|
+
// top-left corner, in whole pixels. A box nested in another is placed after it
|
|
1258
|
+
// (whichever was added first), so it goes inside it.
|
|
1259
|
+
const byVisualId = new Map((view.elements || []).map(el => [visualIdOf(el.id), el]));
|
|
1260
|
+
const depthOf = (el, seen = new Set()) => {
|
|
1261
|
+
const parent = byVisualId.get(view.getElementInstance?.(el.id)?.nestedIn);
|
|
1262
|
+
return parent && !seen.has(parent) ? 1 + depthOf(parent, seen.add(el)) : 0;
|
|
1263
|
+
};
|
|
1264
|
+
const inPlacingOrder = (view.elements || []).map((el, i) => [el, depthOf(el), i]).sort((a, b) => a[1] - b[1] || a[2] - b[2]).map(([el]) => el);
|
|
1265
|
+
for (const el of inPlacingOrder) {
|
|
1266
|
+
const width = el.width || ARCHI_ELEMENT_SIZE.width;
|
|
1267
|
+
const height = el.height || ARCHI_ELEMENT_SIZE.height;
|
|
1268
|
+
const bounds = { x: Math.round((el.x || 0) - width / 2), y: Math.round((el.y || 0) - height / 2), width, height };
|
|
1269
|
+
const visual = el.viewObject
|
|
1270
|
+
? { ..._viewObjectVisual(el, bounds), id: visualIdOf(el.id) }
|
|
1271
|
+
: {
|
|
1272
|
+
id: visualIdOf(el.id),
|
|
1273
|
+
type: 'diagram-model-object',
|
|
1274
|
+
archimateElement: el.elementId ?? el.id,
|
|
1275
|
+
bounds,
|
|
1276
|
+
sourceConnections: [],
|
|
1277
|
+
};
|
|
1278
|
+
// This view's style for the box (an ArchimateView keeps it on the instance), and what the file said besides.
|
|
1279
|
+
const instance = view.getElementInstance?.(el.id);
|
|
1280
|
+
_applyArchiRecord(visual, instance?.archi, _exportStyle(instance?.appearance, instance?.archi, { border: !!el.viewObject }));
|
|
1281
|
+
// Fixed in place (on its own or in a locked group): Archi's lock where Archi has one (canvas
|
|
1282
|
+
// boxes), else a feature Archi keeps. Its logical group: a feature.
|
|
1283
|
+
if (view.arrangement?.isLocked(el.id)) {
|
|
1284
|
+
if (ARCHI_LOCKABLE.has(visual.type)) visual.locked = true;
|
|
1285
|
+
else visual.features = { ...(visual.features ?? {}), [ARCHI_LOCK_FEATURE]: 'true' };
|
|
1286
|
+
}
|
|
1287
|
+
const itemGroup = view.arrangement?.groupOf(el.id);
|
|
1288
|
+
if (itemGroup) {
|
|
1289
|
+
const kind = itemGroup.constructor.kind;
|
|
1290
|
+
visual.features = { ...(visual.features ?? {}), [ARCHI_GROUP_FEATURE]: itemGroup.id, ...(kind !== 'group' && { [ARCHI_GROUP_KIND_FEATURE]: kind }) };
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Add connections from this element to others
|
|
1294
|
+
const rels = relationshipsBySource.get(el.id) || [];
|
|
1295
|
+
for (const rel of rels) {
|
|
1296
|
+
const sc = {
|
|
1297
|
+
id: view.relationshipInstances?.get?.(rel.id)?.archi?.id ?? `conn-${rel.id}`,
|
|
1298
|
+
source: visual.id,
|
|
1299
|
+
target: visualIdOf(rel.target),
|
|
1300
|
+
relationship: rel.relationshipId ?? rel.id,
|
|
1301
|
+
type: 'archimate:Connection',
|
|
1302
|
+
};
|
|
1303
|
+
const line = view.relationshipInstances?.get?.(rel.id);
|
|
1304
|
+
_applyArchiRecord(sc, line?.archi, _exportStyle(line?.appearance, line?.archi, { box: false }));
|
|
1305
|
+
if (rel.waypoints?.length) {
|
|
1306
|
+
// Points on the view for now; made relative once every box is known (below).
|
|
1307
|
+
sc.bendpoints = rel.waypoints.map(wp => ({ x: wp.x, y: wp.y }));
|
|
1308
|
+
}
|
|
1309
|
+
visual.sourceConnections.push(sc);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (instance?.archi?.imagePath) visual.archiImagePath = instance.archi.imagePath;
|
|
1313
|
+
place(visual, el.viewObject ? undefined : view.placedFolderOf?.(el.elementId ?? el.id), el.zIndex ?? Infinity, instance?.archi?.parent);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Plain lines (no relationship): on their source box, with their own class and type bits.
|
|
1317
|
+
const visualOf = new Map();
|
|
1318
|
+
const index = (v) => { visualOf.set(v.id, v); (v.children || []).forEach(index); };
|
|
1319
|
+
visuals.forEach(index);
|
|
1320
|
+
for (const connection of view.viewConnections?.values?.() ?? []) {
|
|
1321
|
+
// An end is a box (exported as "diag-<instance id>") or a group (under its own id).
|
|
1322
|
+
const visualFor = (end) => visualOf.get(visualIdOf(end)) ?? visualOf.get(end);
|
|
1323
|
+
const source = visualFor(connection.sourceInstanceId);
|
|
1324
|
+
const target = visualFor(connection.targetInstanceId);
|
|
1325
|
+
if (!source || !target) continue;
|
|
1326
|
+
const sc = {
|
|
1327
|
+
id: connection.id,
|
|
1328
|
+
source: source.id,
|
|
1329
|
+
target: target.id,
|
|
1330
|
+
archiType: connection.archiType,
|
|
1331
|
+
connectionType: connection.type,
|
|
1332
|
+
name: connection.label || '',
|
|
1333
|
+
...(connection.waypoints.length && { bendpoints: connection.waypoints.map(wp => ({ x: wp.x, y: wp.y })) }),
|
|
1334
|
+
};
|
|
1335
|
+
_applyArchiRecord(sc, connection.archi, _exportStyle(connection.appearance, connection.archi, { box: false }));
|
|
1336
|
+
source.sourceConnections.push(sc);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Each level in stacking order (Archi draws in document order); ties keep their order.
|
|
1340
|
+
const byStacking = (list) => {
|
|
1341
|
+
list.sort((a, b) => stacking.get(a) - stacking.get(b));
|
|
1342
|
+
list.forEach(v => byStacking(v.children ?? []));
|
|
1343
|
+
};
|
|
1344
|
+
byStacking(visuals);
|
|
1345
|
+
|
|
1346
|
+
_relativizeLocatedBendpoints({ children: visuals });
|
|
1347
|
+
return visuals;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* A model and its views as the writers take them (toArchiXML, toArchiFile,
|
|
1352
|
+
* toExchangeXML): the model's data, and each view as an Archi view tree.
|
|
1353
|
+
* @param {ArchimateModel} model
|
|
1354
|
+
* @returns {[Object, Array<Object>]} `[modelData, views]`, to spread into a writer
|
|
1355
|
+
*/
|
|
1356
|
+
export function toArchiExport(model) {
|
|
1357
|
+
const views = model.views().map(view => ({
|
|
1358
|
+
id: view.id,
|
|
1359
|
+
name: view.name,
|
|
1360
|
+
type: view.archiViewType,
|
|
1361
|
+
template: view.template,
|
|
1362
|
+
documentation: view.documentation,
|
|
1363
|
+
properties: view.properties,
|
|
1364
|
+
archiFolder: view.archiFolder,
|
|
1365
|
+
archi: view.archi,
|
|
1366
|
+
children: fromArchimateView(view),
|
|
1367
|
+
}));
|
|
1368
|
+
return [{
|
|
1369
|
+
id: model.id || 'model-1',
|
|
1370
|
+
name: model.name || 'ArchiMate Model',
|
|
1371
|
+
version: '5.0.0',
|
|
1372
|
+
documentation: model.documentation,
|
|
1373
|
+
properties: model.properties,
|
|
1374
|
+
archi: model.archi,
|
|
1375
|
+
elements: model.elements(),
|
|
1376
|
+
relationships: model.relationships(),
|
|
1377
|
+
}, views];
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* Put what the file said about a box or line back on its export visual: the
|
|
1382
|
+
* style to write, its name and features (when it has none of its own), and the
|
|
1383
|
+
* attributes and child elements the model does not use.
|
|
1384
|
+
* @private
|
|
1385
|
+
*/
|
|
1386
|
+
function _applyArchiRecord(target, record, style) {
|
|
1387
|
+
target.style = style;
|
|
1388
|
+
if (!record) return;
|
|
1389
|
+
if (record.name && !target.name) target.name = record.name;
|
|
1390
|
+
if (record.features) target.features = { ...record.features, ...(target.features ?? {}) };
|
|
1391
|
+
if (Object.keys(record.attributes ?? {}).length) target.extraAttributes = { ...record.attributes };
|
|
1392
|
+
if (record.elements?.length) target.extraElements = record.elements.map(e => ({ ...e }));
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/** <property> lines for [{key, value}] pairs (a model's or a view's). @private */
|
|
1396
|
+
function _propertyLines(pairs, indent) {
|
|
1397
|
+
return (Array.isArray(pairs) ? pairs : []).map(({ key, value }) => `${indent}<property key="${_escapeAttr(key)}" value="${_escapeAttr(value)}"/>`);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Items written inside a top-level folder, each in the sub-folders its
|
|
1402
|
+
* `archiFolder` path names (rebuilt as Archi had them: ids, names,
|
|
1403
|
+
* documentation, properties); items without a path directly in the folder.
|
|
1404
|
+
* `write(item, indent)` gives an item's lines.
|
|
1405
|
+
* @private
|
|
1406
|
+
*/
|
|
1407
|
+
function _inSubFolders(items, write, indent = ' ') {
|
|
1408
|
+
const lines = [];
|
|
1409
|
+
const direct = items.filter(item => !item.archiFolder?.length);
|
|
1410
|
+
// Sub-folders in the order they are first named, each holding the items one level deeper.
|
|
1411
|
+
const groups = new Map();
|
|
1412
|
+
for (const item of items.filter(item => item.archiFolder?.length)) {
|
|
1413
|
+
const [step, ...rest] = item.archiFolder;
|
|
1414
|
+
if (!groups.has(step.id)) groups.set(step.id, { step, items: [] });
|
|
1415
|
+
groups.get(step.id).items.push({ ...item, archiFolder: rest });
|
|
1416
|
+
}
|
|
1417
|
+
for (const { step, items: inner } of groups.values()) {
|
|
1418
|
+
lines.push(`${indent}<folder name="${_escapeAttr(step.name)}" id="${_escapeAttr(step.id)}">`);
|
|
1419
|
+
if (step.documentation) lines.push(`${indent} <documentation>${_escapeXml(step.documentation)}</documentation>`);
|
|
1420
|
+
lines.push(..._inSubFolders(inner, write, indent + ' '));
|
|
1421
|
+
lines.push(..._propertyLines(step.properties, indent + ' '));
|
|
1422
|
+
lines.push(`${indent}</folder>`);
|
|
1423
|
+
}
|
|
1424
|
+
for (const item of direct) lines.push(...write(item, indent));
|
|
1425
|
+
return lines;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/** One element or relationship: its attributes, documentation and properties. @private */
|
|
1429
|
+
function _serializeConcept(xsiType, attrs, concept, indent, extraLines = []) {
|
|
1430
|
+
const all = { ...attrs, ...(concept.archi?.attributes ?? {}) };
|
|
1431
|
+
const attrText = Object.entries(all).map(([k, v]) => ` ${k}="${_escapeAttr(v)}"`).join('');
|
|
1432
|
+
const body = [];
|
|
1433
|
+
if (concept.documentation) body.push(`${indent} <documentation>${_escapeXml(concept.documentation)}</documentation>`);
|
|
1434
|
+
body.push(..._extraElementLines(concept.archi?.elements, `${indent} `));
|
|
1435
|
+
body.push(...extraLines);
|
|
1436
|
+
for (const [k, v] of Object.entries(_typedArrayToProperties(concept.properties))) {
|
|
1437
|
+
body.push(`${indent} <property key="${_escapeAttr(k)}" value="${_escapeAttr(v)}"/>`);
|
|
1438
|
+
}
|
|
1439
|
+
if (!body.length) return [`${indent}<element xsi:type="${xsiType}"${attrText}/>`];
|
|
1440
|
+
return [`${indent}<element xsi:type="${xsiType}"${attrText}>`, ...body, `${indent}</element>`];
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/** A view-only note or image as its Archi visual. @private */
|
|
1444
|
+
function _viewObjectVisual(el, bounds) {
|
|
1445
|
+
const base = { id: `diag-${el.id}`, bounds, sourceConnections: [] };
|
|
1446
|
+
if (el.archiVisualType) {
|
|
1447
|
+
// A canvas or sketch box: its own class, text and picture.
|
|
1448
|
+
return { ...base, type: el.archiVisualType, text: el.label ?? '', ...(el.iconUrl && { imagePath: el.iconUrl }) };
|
|
1449
|
+
}
|
|
1450
|
+
if (el.shape === 'image') {
|
|
1451
|
+
return { ...base, type: el.archiVisualType === 'canvas-model-image' ? 'canvas-model-image' : 'diagram-model-image', name: el.name || '', imagePath: el.iconUrl, ...(el.href && { properties: [{ name: 'href', type: 'string', value: el.href }] }) };
|
|
1452
|
+
}
|
|
1453
|
+
return { ...base, type: 'diagram-model-note', text: el.label ?? '' };
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/** Reverse mapping: produce an Archi native XML string from renderengine data. */
|
|
1457
|
+
export function toArchiXML(model, views) {
|
|
1458
|
+
const { elements = [], relationships = [] } = model;
|
|
1459
|
+
const viewArray = views || [];
|
|
1460
|
+
|
|
1461
|
+
const lines = [
|
|
1462
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
1463
|
+
`<archimate:model xmlns:archimate="${ARCHI_NAMESPACE}" xmlns:canvas="${ARCHI_CANVAS_NAMESPACE}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="${_escapeAttr(model.name || 'Untitled')}" id="${_escapeAttr(model.id || _newId('id'))}" version="${_escapeAttr(model.version || '5.0')}"${Object.entries(model.archi?.attributes ?? {}).map(([k, v]) => ` ${k}="${_escapeAttr(v)}"`).join('')}>`,
|
|
1464
|
+
];
|
|
1465
|
+
|
|
1466
|
+
// Archi always has its nine top-level folders, and fails to open a model
|
|
1467
|
+
// that lacks some of them while it has a view: write them all, in its order.
|
|
1468
|
+
const byFolder = new Map(ARCHI_FOLDERS.map(folder => [folder.type, []]));
|
|
1469
|
+
// Specializations written for element types Archi lacks, by id.
|
|
1470
|
+
const typeProfiles = new Map();
|
|
1471
|
+
for (const el of elements) {
|
|
1472
|
+
// Archi knows ArchiMate 3.2 only: a version 4 type goes back to its 3.2 one, and a type
|
|
1473
|
+
// Archi has no class for (an IT type, a custom type) to the nearest one.
|
|
1474
|
+
const { folder, type, original } = archiFolderOf(el);
|
|
1475
|
+
let profile = null;
|
|
1476
|
+
// An element Archi already gave a specialization keeps that one.
|
|
1477
|
+
if (original && !el.archi?.attributes?.profiles) {
|
|
1478
|
+
profile = typeProfileId(original);
|
|
1479
|
+
typeProfiles.set(profile, { name: typeDisplayName(original), conceptType: _toPascalType(type) });
|
|
1480
|
+
}
|
|
1481
|
+
byFolder.get(folder.type).push({ el, type, original, profile });
|
|
1482
|
+
}
|
|
1483
|
+
for (const folder of ARCHI_FOLDERS) {
|
|
1484
|
+
const items = byFolder.get(folder.type);
|
|
1485
|
+
const body = [];
|
|
1486
|
+
if (folder.type === 'relations') {
|
|
1487
|
+
body.push(..._inSubFolders(relationships, (rel, indent) => {
|
|
1488
|
+
const name = rel.label ?? rel.name;
|
|
1489
|
+
const directed = rel.kind === 'association-directed';
|
|
1490
|
+
const kind = directed ? 'association' : (rel.kind || 'association');
|
|
1491
|
+
return _serializeConcept(`archimate:${_toPascalType(kind)}Relationship`,
|
|
1492
|
+
{ id: rel.id, ...(name && { name }), source: rel.source, target: rel.target, ...(directed && { directed: 'true' }) }, rel, indent);
|
|
1493
|
+
}));
|
|
1494
|
+
} else if (folder.type === 'diagrams') {
|
|
1495
|
+
body.push(..._inSubFolders(viewArray, (view, indent) => _serializeView(view).map(line => indent.slice(4) + line)));
|
|
1496
|
+
} else {
|
|
1497
|
+
body.push(..._inSubFolders(items.map(item => ({ ...item, archiFolder: item.el.archiFolder })),
|
|
1498
|
+
({ el, type, original, profile }, indent) => _serializeConcept(`archimate:${_toPascalType(type)}`,
|
|
1499
|
+
{ id: el.id, name: el.name || '', ...(profile && { profiles: profile }) }, el, indent,
|
|
1500
|
+
original ? [`${indent} <feature name="${ARCHI_TYPE_FEATURE}" value="${_escapeAttr(original)}"/>`] : [])));
|
|
1501
|
+
}
|
|
1502
|
+
const open = ` <folder name="${_escapeAttr(folder.name)}" id="${folder.type}-folder" type="${folder.type}"`;
|
|
1503
|
+
if (body.length) lines.push(`${open}>`, ...body.map(line => ` ${line}`), ' </folder>');
|
|
1504
|
+
else lines.push(`${open}/>`);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// What else the model carried (profiles, ...), its own properties, then its documentation (Archi's "purpose").
|
|
1508
|
+
lines.push(..._extraElementLines(model.archi?.elements, ' '));
|
|
1509
|
+
for (const [id, { name, conceptType }] of typeProfiles) {
|
|
1510
|
+
lines.push(` <profile name="${_escapeAttr(name)}" id="${id}" conceptType="${conceptType}"/>`);
|
|
1511
|
+
}
|
|
1512
|
+
lines.push(..._propertyLines(model.properties, ' '));
|
|
1513
|
+
if (model.documentation) lines.push(` <purpose>${_escapeXml(model.documentation)}</purpose>`);
|
|
1514
|
+
lines.push('</archimate:model>');
|
|
1515
|
+
return lines.join('\n');
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function _serializeView(view) {
|
|
1519
|
+
const xsiType = {
|
|
1520
|
+
'archimate-diagram-model': 'archimate:ArchimateDiagramModel',
|
|
1521
|
+
'sketch-model': 'archimate:SketchModel',
|
|
1522
|
+
'canvas-model': 'canvas:CanvasModel',
|
|
1523
|
+
}[view.type] || 'archimate:ArchimateDiagramModel';
|
|
1524
|
+
|
|
1525
|
+
const extra = Object.entries(view.archi?.attributes ?? {}).map(([k, v]) => ` ${k}="${_escapeAttr(v)}"`).join('');
|
|
1526
|
+
const lines = [` <element xsi:type="${xsiType}" name="${_escapeAttr(view.name || 'View')}" id="${view.id}"${extra}>`];
|
|
1527
|
+
// Archi lists each visual's incoming connections on it (targetConnections); without
|
|
1528
|
+
// them it draws a line only when its target happens to be created before its source.
|
|
1529
|
+
const incoming = new Map();
|
|
1530
|
+
const collect = (visual) => {
|
|
1531
|
+
for (const sc of visual.sourceConnections || []) {
|
|
1532
|
+
if (!incoming.has(sc.target)) incoming.set(sc.target, []);
|
|
1533
|
+
incoming.get(sc.target).push(sc.id);
|
|
1534
|
+
}
|
|
1535
|
+
(visual.children || []).forEach(collect);
|
|
1536
|
+
};
|
|
1537
|
+
(view.children || []).forEach(collect);
|
|
1538
|
+
for (const visual of view.children || []) {
|
|
1539
|
+
lines.push(..._serializeVisual(visual, ' ', incoming));
|
|
1540
|
+
}
|
|
1541
|
+
// After the boxes, as Archi writes a view: its documentation, other elements, properties.
|
|
1542
|
+
if (view.documentation) lines.push(` <documentation>${_escapeXml(view.documentation)}</documentation>`);
|
|
1543
|
+
lines.push(..._extraElementLines(view.archi?.elements, ' '));
|
|
1544
|
+
if (view.template) lines.push(` <feature name="${ARCHI_TEMPLATE_FEATURE}" value="true"/>`);
|
|
1545
|
+
lines.push(..._propertyLines(view.properties, ' '));
|
|
1546
|
+
lines.push(' </element>');
|
|
1547
|
+
return lines;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function _serializeVisual(visual, indent, incoming = new Map()) {
|
|
1551
|
+
const xsiType = _visualTypeToXSI(visual.type);
|
|
1552
|
+
const attrs = [`xsi:type="${xsiType}"`, `id="${visual.id}"`];
|
|
1553
|
+
if (incoming.get(visual.id)?.length) attrs.push(`targetConnections="${incoming.get(visual.id).join(' ')}"`);
|
|
1554
|
+
if (visual.archimateElement) attrs.push(`archimateElement="${visual.archimateElement}"`);
|
|
1555
|
+
if (visual.referencedModel) attrs.push(`referencedModel="${visual.referencedModel}"`);
|
|
1556
|
+
if (visual.imagePath) attrs.push(`imagePath="${_escapeAttr(visual.imagePath)}"`);
|
|
1557
|
+
if (visual.name) attrs.push(`name="${_escapeAttr(visual.name)}"`);
|
|
1558
|
+
if (visual.locked) attrs.push('locked="true"');
|
|
1559
|
+
const { [ARCHI_LINE_ALPHA_FEATURE]: lineAlpha, ...attributeStyle } = visual.style || {};
|
|
1560
|
+
for (const [name, value] of Object.entries(attributeStyle)) attrs.push(`${name}="${_escapeAttr(value)}"`);
|
|
1561
|
+
for (const [name, value] of Object.entries(visual.extraAttributes || {})) attrs.push(`${name}="${_escapeAttr(value)}"`);
|
|
1562
|
+
|
|
1563
|
+
const lines = [`${indent}<child ${attrs.join(' ')}>`];
|
|
1564
|
+
if (visual.bounds) {
|
|
1565
|
+
const b = visual.bounds;
|
|
1566
|
+
lines.push(`${indent} <bounds x="${b.x}" y="${b.y}" width="${b.width}" height="${b.height}"/>`);
|
|
1567
|
+
}
|
|
1568
|
+
// A note's text: Archi keeps it in a <content> element.
|
|
1569
|
+
if (visual.text) lines.push(`${indent} <content>${_escapeXml(visual.text)}</content>`);
|
|
1570
|
+
for (const [name, value] of Object.entries(visual.features || {})) {
|
|
1571
|
+
lines.push(`${indent} <feature name="${_escapeAttr(name)}" value="${_escapeAttr(String(value))}"/>`);
|
|
1572
|
+
}
|
|
1573
|
+
if (lineAlpha !== undefined) lines.push(`${indent} <feature name="${ARCHI_LINE_ALPHA_FEATURE}" value="${_escapeAttr(String(lineAlpha))}"/>`);
|
|
1574
|
+
for (const [key, value] of Object.entries(_typedArrayToProperties(visual.properties))) {
|
|
1575
|
+
lines.push(`${indent} <property key="${_escapeAttr(key)}" value="${_escapeAttr(value)}"/>`);
|
|
1576
|
+
}
|
|
1577
|
+
lines.push(..._extraElementLines(visual.extraElements, `${indent} `));
|
|
1578
|
+
for (const sc of visual.sourceConnections || []) {
|
|
1579
|
+
// A relationship's line is archimate:Connection; a plain line keeps its own class.
|
|
1580
|
+
const xsiType = sc.relationship ? 'archimate:Connection' : _visualTypeToXSI(sc.archiType ?? 'diagram-model-connection');
|
|
1581
|
+
const scAttrs = [
|
|
1582
|
+
// Archi's default class for a line needs no type, and Archi writes none.
|
|
1583
|
+
...(xsiType === 'archimate:DiagramModelConnection' ? [] : [`xsi:type="${xsiType}"`]),
|
|
1584
|
+
`id="${sc.id}"`,
|
|
1585
|
+
`source="${sc.source}"`,
|
|
1586
|
+
`target="${sc.target}"`,
|
|
1587
|
+
];
|
|
1588
|
+
if (sc.relationship) scAttrs.push(`archimateRelationship="${sc.relationship}"`);
|
|
1589
|
+
if (!sc.relationship && sc.name) scAttrs.push(`name="${_escapeAttr(sc.name)}"`);
|
|
1590
|
+
if (!sc.relationship && sc.connectionType) scAttrs.push(`type="${sc.connectionType}"`);
|
|
1591
|
+
for (const [name, value] of Object.entries(sc.style || {})) scAttrs.push(`${name}="${_escapeAttr(value)}"`);
|
|
1592
|
+
for (const [name, value] of Object.entries(sc.extraAttributes || {})) scAttrs.push(`${name}="${_escapeAttr(value)}"`);
|
|
1593
|
+
|
|
1594
|
+
if (sc.bendpoints?.length) {
|
|
1595
|
+
lines.push(`${indent} <sourceConnection ${scAttrs.join(' ')}>`);
|
|
1596
|
+
for (const bp of sc.bendpoints) {
|
|
1597
|
+
lines.push(`${indent} <bendpoint ${BENDPOINT_KEYS.map(k => `${k}="${bp[k]}"`).join(' ')}/>`);
|
|
1598
|
+
}
|
|
1599
|
+
lines.push(`${indent} </sourceConnection>`);
|
|
1600
|
+
} else {
|
|
1601
|
+
lines.push(`${indent} <sourceConnection ${scAttrs.join(' ')}/>`);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
for (const child of visual.children || []) {
|
|
1605
|
+
lines.push(..._serializeVisual(child, `${indent} `, incoming));
|
|
1606
|
+
}
|
|
1607
|
+
lines.push(`${indent}</child>`);
|
|
1608
|
+
return lines;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/** Archi's top-level folders, in its order, with the layers whose elements each holds. */
|
|
1612
|
+
export const ARCHI_FOLDERS = Object.freeze([
|
|
1613
|
+
{ type: 'strategy', name: 'Strategy', layers: ['strategy'] },
|
|
1614
|
+
{ type: 'business', name: 'Business', layers: ['business'] },
|
|
1615
|
+
{ type: 'application', name: 'Application', layers: ['application'] },
|
|
1616
|
+
{ type: 'technology', name: 'Technology & Physical', layers: ['technology', 'physical'] },
|
|
1617
|
+
{ type: 'motivation', name: 'Motivation', layers: ['motivation'] },
|
|
1618
|
+
{ type: 'implementation_migration', name: 'Implementation & Migration', layers: ['implementation'] },
|
|
1619
|
+
{ type: 'other', name: 'Other', layers: ['composite', 'other'] },
|
|
1620
|
+
{ type: 'relations', name: 'Relations' },
|
|
1621
|
+
{ type: 'diagrams', name: 'Views' },
|
|
1622
|
+
]);
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Where Archi keeps an element: the top-level folder of its ArchiMate 3.2
|
|
1626
|
+
* layer, with the 3.2 type it is written as and, for a type Archi lacks, its
|
|
1627
|
+
* own (archiElementType).
|
|
1628
|
+
* @param {Object} el - A model element
|
|
1629
|
+
* @returns {{folder: Object, type: string, original: string|undefined}}
|
|
1630
|
+
*/
|
|
1631
|
+
export function archiFolderOf(el) {
|
|
1632
|
+
const { type, original } = archiElementType(el);
|
|
1633
|
+
const layer = ArchimateNotation.elementLayer({ type }, '3.2');
|
|
1634
|
+
const folder = ARCHI_FOLDERS.find(x => x.layers?.includes(layer !== 'other' ? layer : el.layer)) ?? ARCHI_FOLDERS.find(x => x.type === 'other');
|
|
1635
|
+
return { folder, type, original };
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function _visualTypeToXSI(type) {
|
|
1639
|
+
const map = {
|
|
1640
|
+
'diagram-model-note': 'archimate:Note',
|
|
1641
|
+
'diagram-model-group': 'archimate:Group',
|
|
1642
|
+
'diagram-model-connection': 'archimate:DiagramModelConnection',
|
|
1643
|
+
'canvas-model-connection': 'canvas:CanvasModelConnection',
|
|
1644
|
+
'diagram-model-image': 'archimate:DiagramModelImage',
|
|
1645
|
+
'diagram-model-reference': 'archimate:DiagramModelReference',
|
|
1646
|
+
'sketch-model-sticky': 'archimate:SketchModelSticky',
|
|
1647
|
+
'sketch-model-actor': 'archimate:SketchModelActor',
|
|
1648
|
+
'canvas-model-block': 'canvas:CanvasModelBlock',
|
|
1649
|
+
'canvas-model-sticky': 'canvas:CanvasModelSticky',
|
|
1650
|
+
'canvas-model-image': 'canvas:CanvasModelImage',
|
|
1651
|
+
};
|
|
1652
|
+
return map[type] || 'archimate:DiagramObject';
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function _toPascalType(kebab) {
|
|
1656
|
+
if (!kebab) return '';
|
|
1657
|
+
return kebab.split('-').map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('');
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function _newId(prefix = 'id') {
|
|
1661
|
+
return `${prefix}_${Math.random().toString(36).slice(2, 9)}`;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
/** Text for element content. A CR is written as a reference: a parser turns a literal one into a newline. */
|
|
1665
|
+
function _escapeXml(str) {
|
|
1666
|
+
return String(str)
|
|
1667
|
+
.replace(/&/g, '&')
|
|
1668
|
+
.replace(/</g, '<')
|
|
1669
|
+
.replace(/>/g, '>')
|
|
1670
|
+
.replace(/"/g, '"')
|
|
1671
|
+
.replace(/\r/g, '
');
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
/** Text for an attribute value: a parser also turns a literal newline or tab there into a space. */
|
|
1675
|
+
function _escapeAttr(str) {
|
|
1676
|
+
return _escapeXml(str).replace(/\n/g, '
').replace(/\t/g, '	');
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/*
|
|
1680
|
+
* Shared with the Open Group Exchange Format (archimateExchangeFormat.js), which
|
|
1681
|
+
* reads and writes the same parsed model and view trees as this module.
|
|
1682
|
+
*/
|
|
1683
|
+
export {
|
|
1684
|
+
_visualBoxes as archiVisualBoxes,
|
|
1685
|
+
_bendpointLocations as archiBendpointLocations,
|
|
1686
|
+
_relativizeLocatedBendpoints as relativizeArchiBendpoints,
|
|
1687
|
+
_parseFontFeature as parseArchiFont,
|
|
1688
|
+
_fontString as archiFontString,
|
|
1689
|
+
_typedArrayToProperties as typedArrayToProperties,
|
|
1690
|
+
_toPascalType as toPascalType,
|
|
1691
|
+
_escapeXml as escapeXml,
|
|
1692
|
+
_escapeAttr as escapeAttr,
|
|
1693
|
+
};
|