pict-section-flow 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +44 -13
  2. package/docs/Architecture.md +8 -148
  3. package/docs/Data_Model.md +2 -11
  4. package/docs/README.md +8 -38
  5. package/docs/Theme_Integration.md +13 -13
  6. package/docs/_cover.md +7 -1
  7. package/docs/_playground.json +24 -0
  8. package/docs/_sidebar.md +4 -0
  9. package/docs/_topbar.md +1 -1
  10. package/docs/_version.json +3 -3
  11. package/docs/card-help/FREAD.md +1 -1
  12. package/docs/diagrams/architecture-at-a-glance.excalidraw +4270 -0
  13. package/docs/diagrams/architecture-at-a-glance.mmd +30 -0
  14. package/docs/diagrams/architecture-at-a-glance.svg +2 -0
  15. package/docs/diagrams/data-flow.excalidraw +1451 -0
  16. package/docs/diagrams/data-flow.mmd +17 -0
  17. package/docs/diagrams/data-flow.svg +2 -0
  18. package/docs/diagrams/high-level-design.excalidraw +5767 -0
  19. package/docs/diagrams/high-level-design.mmd +86 -0
  20. package/docs/diagrams/high-level-design.svg +2 -0
  21. package/docs/diagrams/relationships.excalidraw +3852 -0
  22. package/docs/diagrams/relationships.mmd +9 -0
  23. package/docs/diagrams/relationships.svg +2 -0
  24. package/docs/diagrams/service-initialization-sequence.excalidraw +1466 -0
  25. package/docs/diagrams/service-initialization-sequence.mmd +19 -0
  26. package/docs/diagrams/service-initialization-sequence.svg +2 -0
  27. package/docs/diagrams/svg-layer-structure.excalidraw +1060 -0
  28. package/docs/diagrams/svg-layer-structure.mmd +18 -0
  29. package/docs/diagrams/svg-layer-structure.svg +2 -0
  30. package/docs/examples/README.md +9 -0
  31. package/docs/examples/simple_cards/README.md +677 -0
  32. package/docs/examples/simple_cards/css/flowexample.css +65 -0
  33. package/docs/examples/simple_cards/index.html +32 -0
  34. package/docs/examples/simple_cards/js/pict.min.js +12 -0
  35. package/docs/examples/simple_cards/pict-section-flow-example-simple-cards.compatible.min.js +1 -0
  36. package/docs/index.html +6 -7
  37. package/docs/playground/app.json +6 -0
  38. package/docs/playground/appdata.json +85 -0
  39. package/docs/playground/application.js +23 -0
  40. package/docs/playground/pict.json +17 -0
  41. package/docs/playground/runtime/pict-application.min.js +2 -0
  42. package/docs/playground/runtime/pict-section-flow.min.js +2 -0
  43. package/docs/playground/runtime/pict-section-modal.min.js +2 -0
  44. package/docs/playground/runtime/pict.min.js +12 -0
  45. package/docs/retold-catalog.json +241 -166
  46. package/docs/retold-keyword-index.json +19312 -7226
  47. package/example_applications/simple_cards/package.json +9 -1
  48. package/example_applications/simple_cards/source/views/PictView-FlowExample-BottomBar.js +2 -2
  49. package/package.json +5 -5
  50. package/source/PictFlowCard.js +2 -2
  51. package/source/providers/PictProvider-Flow-CSS.js +38 -12
  52. package/source/providers/PictProvider-Flow-ConnectorShapes.js +8 -8
  53. package/source/providers/PictProvider-Flow-Icons.js +33 -33
  54. package/source/providers/PictProvider-Flow-NodeTypes.js +9 -9
  55. package/source/providers/PictProvider-Flow-PanelChrome.js +2 -1
  56. package/source/providers/PictProvider-Flow-Renderer.js +516 -0
  57. package/source/providers/PictProvider-Flow-StylePresets.js +259 -0
  58. package/source/providers/PictProvider-Flow-Theme.js +97 -669
  59. package/source/services/PictService-Flow-ConnectionRenderer.js +6 -6
  60. package/source/services/PictService-Flow-DataManager.js +6 -0
  61. package/source/services/PictService-Flow-InteractionManager.js +10 -1
  62. package/source/services/PictService-Flow-PanelManager.js +106 -2
  63. package/source/services/PictService-Flow-PortRenderer.js +6 -6
  64. package/source/views/PictView-Flow-Node.js +1 -1
  65. package/source/views/PictView-Flow-PropertiesPanel.js +71 -4
  66. package/source/views/PictView-Flow-Toolbar.js +24 -16
  67. package/source/views/PictView-Flow.js +225 -47
  68. package/test/PanelManager_tests.js +172 -0
  69. package/test/Renderer_tests.js +133 -0
  70. package/test/StylePresets_tests.js +153 -0
  71. package/docs/css/docuserve.css +0 -327
@@ -23,6 +23,8 @@ const libPictProviderFlowCSS = require('../providers/PictProvider-Flow-CSS.js');
23
23
  const libPictProviderFlowIcons = require('../providers/PictProvider-Flow-Icons.js');
24
24
  const libPictProviderFlowConnectorShapes = require('../providers/PictProvider-Flow-ConnectorShapes.js');
25
25
  const libPictProviderFlowTheme = require('../providers/PictProvider-Flow-Theme.js');
26
+ const libPictProviderFlowRenderer = require('../providers/PictProvider-Flow-Renderer.js');
27
+ const libPictProviderFlowStylePresets = require('../providers/PictProvider-Flow-StylePresets.js');
26
28
  const libPictProviderFlowNoise = require('../providers/PictProvider-Flow-Noise.js');
27
29
 
28
30
  const libPictViewFlowNode = require('./PictView-Flow-Node.js');
@@ -69,6 +71,11 @@ const _DefaultConfiguration =
69
71
  DefaultNodeWidth: 180,
70
72
  DefaultNodeHeight: 80,
71
73
 
74
+ // Properties panel for connections (edges). Connections are not typed, so one config serves
75
+ // them all: { PanelType, DefaultWidth, DefaultHeight, Title, Configuration }. When set, a
76
+ // double-click on a connection opens this panel; when false, double-click adds a bezier handle.
77
+ ConnectionPropertiesPanel: false,
78
+
72
79
  // Layout-algorithm subsystem defaults
73
80
  DefaultLayoutAlgorithm: 'Custom',
74
81
  DefaultLayoutParameters: {},
@@ -165,6 +172,11 @@ class PictViewFlow extends libPictView
165
172
  { ServiceType: 'PictProviderFlowNoise', Library: libPictProviderFlowNoise, Property: '_NoiseProvider', NoFlowView: true },
166
173
 
167
174
  // Providers (need FlowView)
175
+ // Renderer + StylePresets must be created before the legacy Theme
176
+ // shim (which delegates to them) and before CSS PostInit so that
177
+ // registerCSS() sees an initialized renderer.
178
+ { ServiceType: 'PictProviderFlowRenderer', Library: libPictProviderFlowRenderer, Property: '_RendererProvider' },
179
+ { ServiceType: 'PictProviderFlowStylePresets', Library: libPictProviderFlowStylePresets, Property: '_StylePresetsProvider' },
168
180
  { ServiceType: 'PictProviderFlowTheme', Library: libPictProviderFlowTheme, Property: '_ThemeProvider' },
169
181
  { ServiceType: 'PictProviderFlowCSS', Library: libPictProviderFlowCSS, Property: '_CSSProvider', PostInit: 'registerCSS' },
170
182
  { ServiceType: 'PictProviderFlowIcons', Library: libPictProviderFlowIcons, Property: '_IconProvider', PostInit: 'registerIconTemplates' },
@@ -255,6 +267,8 @@ class PictViewFlow extends libPictView
255
267
  this._IconProvider = null;
256
268
  this._ConnectorShapesProvider = null;
257
269
  this._ThemeProvider = null;
270
+ this._RendererProvider = null;
271
+ this._StylePresetsProvider = null;
258
272
  this._NoiseProvider = null;
259
273
  this._SVGHelperProvider = null;
260
274
  this._GeometryProvider = null;
@@ -343,23 +357,40 @@ class PictViewFlow extends libPictView
343
357
  {
344
358
  super.onBeforeInitialize();
345
359
 
346
- // Theme + Noise must be created first (CSS PostInit depends on theme state)
347
- this._ThemeProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowTheme', { FlowView: this });
348
- this._NoiseProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowNoise');
349
-
350
- // Apply initial theme from options
351
- if (this.options.Theme)
360
+ // Noise + Renderer + StylePresets + Theme shim must be created before
361
+ // CSS PostInit so registerCSS() sees an initialized renderer + presets.
362
+ this._NoiseProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowNoise');
363
+ this._RendererProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowRenderer', { FlowView: this });
364
+ this._StylePresetsProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowStylePresets', { FlowView: this });
365
+ this._ThemeProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowTheme', { FlowView: this });
366
+
367
+ // Apply initial style preset / per-axis overrides from options.
368
+ // `Theme` and `StylePreset` are aliases for the same preset-by-hash apply.
369
+ let tmpInitialPreset = this.options.StylePreset || this.options.Theme;
370
+ if (tmpInitialPreset)
371
+ {
372
+ this._StylePresetsProvider.applyPreset(tmpInitialPreset);
373
+ }
374
+ if (this.options.Renderer)
352
375
  {
353
- this._ThemeProvider.setTheme(this.options.Theme);
376
+ this._RendererProvider.setRenderer(this.options.Renderer);
377
+ this._StylePresetsProvider.markCustomized();
354
378
  }
355
379
  if (typeof this.options.NoiseLevel === 'number')
356
380
  {
357
- this._ThemeProvider.setNoiseLevel(this.options.NoiseLevel);
381
+ this._RendererProvider.setNoiseLevel(this.options.NoiseLevel);
358
382
  }
359
383
 
360
- // Instantiate all remaining services (skips Theme + Noise since already set)
384
+ // Instantiate all remaining services (skips Noise/Renderer/StylePresets/Theme
385
+ // since already set above)
361
386
  this._instantiateServices();
362
387
 
388
+ // Now that CSSProvider exists, inject the active renderer's GeometryCSS.
389
+ if (this._CSSProvider && typeof this._CSSProvider.registerRendererCSS === 'function')
390
+ {
391
+ this._CSSProvider.registerRendererCSS(this._RendererProvider.getActiveRenderer());
392
+ }
393
+
363
394
  // Subscribe to the host application's pict-provider-theme so the flow
364
395
  // editor's marker arrowhead colors and shape overrides update when the
365
396
  // host swaps light/dark or palette themes. CSS variables (--theme-*)
@@ -902,89 +933,188 @@ class PictViewFlow extends libPictView
902
933
  return this._ViewportManager.exitFullscreen();
903
934
  }
904
935
 
905
- // ── Theme API ────────────────────────────────────────────────────────
936
+ // ── Theme / Renderer / Style-Preset API ─────────────────────────────
937
+ //
938
+ // Three axes you can drive independently:
939
+ // - ColorTheme — delegates to pict-provider-theme (a pict-section-theme
940
+ // catalog hash like 'flow-sketch' or 'pict-default')
941
+ // - Renderer — delegates to PictProviderFlowRenderer (controls
942
+ // bracket/rect node body, jitter, shadows, fonts)
943
+ // - EdgeTheme — delegates to PictService-Flow-Layout (Bezier /
944
+ // Straight / Orthogonal / Perimeter / …; see
945
+ // PictView-Flow.setEdgeTheme below)
946
+ //
947
+ // Most users pick a curated combo via `setStylePreset()` — the preset
948
+ // applies all three axes in order. Per-axis overrides mark the active
949
+ // preset as 'customized' (getStylePreset returns null afterward).
950
+ //
951
+ // For backwards-compatibility, `setTheme()` / `getThemeKey()` continue
952
+ // to work as aliases for `setStylePreset()` / `getStylePreset()`.
906
953
 
907
954
  /**
908
- * Switch the active theme and re-render.
909
- * @param {string} pThemeKey - Theme key (e.g. 'default', 'sketch', 'blueprint', 'mono', 'retro-80s', 'retro-90s')
955
+ * Apply a named style preset — sets ColorTheme, Renderer, EdgeTheme
956
+ * (and optional NoiseLevel) in one call.
957
+ * @param {string} pPresetHash
910
958
  */
911
- setTheme(pThemeKey)
959
+ setStylePreset(pPresetHash)
912
960
  {
913
- if (!this._ThemeProvider)
961
+ if (!this._StylePresetsProvider)
914
962
  {
915
- this.log.warn('PictSectionFlow setTheme: ThemeProvider not available');
963
+ this.log.warn('PictSectionFlow setStylePreset: StylePresets provider not available');
916
964
  return;
917
965
  }
966
+ let tmpApplied = this._StylePresetsProvider.applyPreset(pPresetHash);
967
+ if (!tmpApplied) { return; }
968
+ this._refreshAfterStyleChange();
969
+ if (this._EventHandlerProvider)
970
+ {
971
+ this._EventHandlerProvider.fireEvent('onStylePresetChanged', pPresetHash);
972
+ // Back-compat — old code listens for 'onThemeChanged'
973
+ this._EventHandlerProvider.fireEvent('onThemeChanged', pPresetHash);
974
+ }
975
+ }
918
976
 
919
- let tmpApplied = this._ThemeProvider.setTheme(pThemeKey);
920
- if (!tmpApplied) return;
977
+ /**
978
+ * Hash of the active style preset, or null when in customized state.
979
+ * @returns {string|null}
980
+ */
981
+ getStylePreset()
982
+ {
983
+ return this._StylePresetsProvider ? this._StylePresetsProvider.getActivePresetHash() : null;
984
+ }
921
985
 
922
- // Re-register CSS with the new theme overrides
923
- if (this._CSSProvider)
986
+ /**
987
+ * Override just the color theme — delegates to pict-provider-theme.
988
+ * @param {string} pThemeHash - a pict-section-theme catalog hash
989
+ */
990
+ setColorTheme(pThemeHash)
991
+ {
992
+ if (this.fable.providers && this.fable.providers.Theme)
924
993
  {
925
- this._CSSProvider.registerCSS();
994
+ try { this.fable.providers.Theme.applyTheme(pThemeHash); }
995
+ catch (pErr) { this.log.warn(`PictSectionFlow setColorTheme: applyTheme failed — ${pErr.message}`); return; }
926
996
  }
997
+ else
998
+ {
999
+ this.log.warn('PictSectionFlow setColorTheme: pict-provider-theme not available in host');
1000
+ return;
1001
+ }
1002
+ if (this._StylePresetsProvider) { this._StylePresetsProvider.markCustomized(); }
1003
+ this._refreshAfterStyleChange();
1004
+ if (this._EventHandlerProvider)
1005
+ {
1006
+ this._EventHandlerProvider.fireEvent('onColorThemeChanged', pThemeHash);
1007
+ }
1008
+ }
927
1009
 
928
- // Re-inject marker defs (arrowhead colors may have changed)
929
- this._reinjectMarkerDefs();
930
-
931
- // Full re-render
932
- if (this.initialRenderComplete)
1010
+ /**
1011
+ * The active color theme hash (from pict-provider-theme).
1012
+ * @returns {string|null}
1013
+ */
1014
+ getColorThemeKey()
1015
+ {
1016
+ if (this.fable.providers && this.fable.providers.Theme && typeof this.fable.providers.Theme.getActiveTheme === 'function')
933
1017
  {
934
- this.renderFlow();
1018
+ let tmpActive = this.fable.providers.Theme.getActiveTheme();
1019
+ if (tmpActive && tmpActive.Hash) { return tmpActive.Hash; }
935
1020
  }
1021
+ return null;
1022
+ }
936
1023
 
1024
+ /**
1025
+ * Override just the renderer — controls node body shape, jitter, shadows.
1026
+ * @param {string} pRendererKey
1027
+ */
1028
+ setRenderer(pRendererKey)
1029
+ {
1030
+ if (!this._RendererProvider)
1031
+ {
1032
+ this.log.warn('PictSectionFlow setRenderer: Renderer provider not available');
1033
+ return;
1034
+ }
1035
+ let tmpApplied = this._RendererProvider.setRenderer(pRendererKey);
1036
+ if (!tmpApplied) { return; }
1037
+ if (this._StylePresetsProvider) { this._StylePresetsProvider.markCustomized(); }
1038
+ this._refreshAfterStyleChange();
937
1039
  if (this._EventHandlerProvider)
938
1040
  {
939
- this._EventHandlerProvider.fireEvent('onThemeChanged', pThemeKey);
1041
+ this._EventHandlerProvider.fireEvent('onRendererChanged', pRendererKey);
940
1042
  }
941
1043
  }
942
1044
 
943
1045
  /**
944
- * Set the noise level (0 to 1) and re-render.
1046
+ * The active renderer key.
1047
+ * @returns {string}
1048
+ */
1049
+ getRendererKey()
1050
+ {
1051
+ return this._RendererProvider ? this._RendererProvider.getActiveRendererKey() : 'clean';
1052
+ }
1053
+
1054
+ /**
1055
+ * Set the noise level (0 to 1) and re-render. Noise applies only when
1056
+ * the active renderer enables it (see Renderer.NoiseConfig).
945
1057
  * @param {number} pLevel - 0 = precise, 1 = maximum wobble
946
1058
  */
947
1059
  setNoiseLevel(pLevel)
948
1060
  {
949
- if (!this._ThemeProvider)
1061
+ if (this._RendererProvider)
950
1062
  {
951
- this.log.warn('PictSectionFlow setNoiseLevel: ThemeProvider not available');
952
- return;
1063
+ this._RendererProvider.setNoiseLevel(pLevel);
953
1064
  }
954
-
955
- this._ThemeProvider.setNoiseLevel(pLevel);
956
-
957
- // Full re-render to apply new noise
958
- if (this.initialRenderComplete)
1065
+ else if (this._ThemeProvider)
959
1066
  {
960
- this.renderFlow();
1067
+ this._ThemeProvider.setNoiseLevel(pLevel);
961
1068
  }
1069
+ if (this.initialRenderComplete) { this.renderFlow(); }
962
1070
  }
963
1071
 
964
1072
  /**
965
- * Get the current noise level (0 to 1).
1073
+ * Current noise level (0 to 1).
966
1074
  * @returns {number}
967
1075
  */
968
1076
  getNoiseLevel()
969
1077
  {
970
- if (this._ThemeProvider)
971
- {
972
- return this._ThemeProvider.getNoiseLevel();
973
- }
1078
+ if (this._RendererProvider) { return this._RendererProvider.getNoiseLevel(); }
1079
+ if (this._ThemeProvider) { return this._ThemeProvider.getNoiseLevel(); }
974
1080
  return 0;
975
1081
  }
976
1082
 
977
1083
  /**
978
- * Get the active theme key.
979
- * @returns {string}
1084
+ * @deprecated since the 3-axis refactor — use setStylePreset() instead.
1085
+ * Kept as an alias for back-compat with existing host apps and views.
1086
+ * @param {string} pPresetHash
1087
+ */
1088
+ setTheme(pPresetHash)
1089
+ {
1090
+ this.setStylePreset(pPresetHash);
1091
+ }
1092
+
1093
+ /**
1094
+ * @deprecated since the 3-axis refactor — use getStylePreset() instead.
1095
+ * Returns the active preset hash (or null if customized).
1096
+ * @returns {string|null}
980
1097
  */
981
1098
  getThemeKey()
982
1099
  {
983
- if (this._ThemeProvider)
1100
+ return this.getStylePreset();
1101
+ }
1102
+
1103
+ /**
1104
+ * Common refresh path used by all axis-change methods.
1105
+ * Re-registers the renderer CSS, re-injects marker defs (arrowhead colors
1106
+ * may have shifted with the new theme), and full-renders the flow.
1107
+ * @private
1108
+ */
1109
+ _refreshAfterStyleChange()
1110
+ {
1111
+ if (this._CSSProvider && typeof this._CSSProvider.registerRendererCSS === 'function' && this._RendererProvider)
984
1112
  {
985
- return this._ThemeProvider.getActiveThemeKey();
1113
+ this._CSSProvider.registerRendererCSS(this._RendererProvider.getActiveRenderer());
986
1114
  }
987
- return 'default';
1115
+ if (this._CSSProvider) { this._CSSProvider.registerCSS(); }
1116
+ this._reinjectMarkerDefs();
1117
+ if (this.initialRenderComplete) { this.renderFlow(); }
988
1118
  }
989
1119
 
990
1120
  _reinjectMarkerDefs() { return this._RenderManager.reinjectMarkerDefs(); }
@@ -1203,6 +1333,54 @@ class PictViewFlow extends libPictView
1203
1333
  return this._PanelManager.togglePanel(pNodeHash);
1204
1334
  }
1205
1335
 
1336
+ /**
1337
+ * Open a properties panel for a connection (edge). Requires the ConnectionPropertiesPanel
1338
+ * option; returns false otherwise.
1339
+ * @param {string} pConnectionHash
1340
+ * @returns {Object|false}
1341
+ */
1342
+ openConnectionPanel(pConnectionHash)
1343
+ {
1344
+ return this._PanelManager.openConnectionPanel(pConnectionHash);
1345
+ }
1346
+
1347
+ /**
1348
+ * Toggle a properties panel for a connection.
1349
+ * @param {string} pConnectionHash
1350
+ * @returns {Object|false}
1351
+ */
1352
+ toggleConnectionPanel(pConnectionHash)
1353
+ {
1354
+ return this._PanelManager.toggleConnectionPanel(pConnectionHash);
1355
+ }
1356
+
1357
+ /**
1358
+ * Close all panels for a given connection.
1359
+ * @param {string} pConnectionHash
1360
+ * @returns {boolean}
1361
+ */
1362
+ closePanelForConnection(pConnectionHash)
1363
+ {
1364
+ return this._PanelManager.closePanelForConnection(pConnectionHash);
1365
+ }
1366
+
1367
+ /**
1368
+ * The midpoint of a connection in SVG coordinates, averaged from its two endpoint ports. Used
1369
+ * to place and tether a connection's properties panel. Returns null if the connection or
1370
+ * either port can not be resolved.
1371
+ * @param {string} pConnectionHash
1372
+ * @returns {{x: number, y: number}|null}
1373
+ */
1374
+ getConnectionMidpoint(pConnectionHash)
1375
+ {
1376
+ let tmpConnection = this.getConnection(pConnectionHash);
1377
+ if (!tmpConnection) return null;
1378
+ let tmpSource = this.getPortPosition(tmpConnection.SourceNodeHash, tmpConnection.SourcePortHash);
1379
+ let tmpTarget = this.getPortPosition(tmpConnection.TargetNodeHash, tmpConnection.TargetPortHash);
1380
+ if (!tmpSource || !tmpTarget) return null;
1381
+ return { x: (tmpSource.x + tmpTarget.x) / 2, y: (tmpSource.y + tmpTarget.y) / 2 };
1382
+ }
1383
+
1206
1384
  /**
1207
1385
  * Update a panel's position (for drag).
1208
1386
  * @param {string} pPanelHash
@@ -0,0 +1,172 @@
1
+ const libFable = require('fable');
2
+ const libChai = require('chai');
3
+ const libExpect = libChai.expect;
4
+
5
+ const libPanelManager = require('../source/services/PictService-Flow-PanelManager.js');
6
+
7
+ /**
8
+ * Connection (edge) properties panels. The node-panel path is well covered through the view; these
9
+ * focus on the connection additions: gating on ConnectionPropertiesPanel, placement near the edge
10
+ * midpoint, the open/toggle/close lifecycle, and that node panels are not disturbed.
11
+ */
12
+ suite
13
+ (
14
+ 'PictService-Flow-PanelManager (connection panels)',
15
+ function ()
16
+ {
17
+ let _Fable;
18
+ let _PanelManager;
19
+ let _MockFlowView;
20
+
21
+ setup
22
+ (
23
+ function ()
24
+ {
25
+ _Fable = new libFable({});
26
+
27
+ _MockFlowView =
28
+ {
29
+ fable: _Fable,
30
+ log: _Fable.log,
31
+ options:
32
+ {
33
+ ViewIdentifier: 'Test-Flow',
34
+ ConnectionPropertiesPanel: false
35
+ },
36
+ _FlowData:
37
+ {
38
+ Nodes:
39
+ [
40
+ { Hash: 'n1', Type: 'state', X: 0, Y: 0, Width: 100, Height: 60, Ports: [ { Hash: 'n1-out', Direction: 'output' } ] },
41
+ { Hash: 'n2', Type: 'state', X: 300, Y: 0, Width: 100, Height: 60, Ports: [ { Hash: 'n2-in', Direction: 'input' } ] }
42
+ ],
43
+ Connections:
44
+ [
45
+ { Hash: 'c1', SourceNodeHash: 'n1', SourcePortHash: 'n1-out', TargetNodeHash: 'n2', TargetPortHash: 'n2-in', Data: {} }
46
+ ],
47
+ OpenPanels: [],
48
+ ViewState: { SelectedTetherHash: null }
49
+ },
50
+ getConnection: function (pHash) { return this._FlowData.Connections.find((pConn) => pConn.Hash === pHash) || null; },
51
+ getNode: function (pHash) { return this._FlowData.Nodes.find((pNode) => pNode.Hash === pHash) || null; },
52
+ getConnectionMidpoint: function (pHash) { return this.getConnection(pHash) ? { x: 200, y: 30 } : null; },
53
+ _NodeTypeProvider:
54
+ {
55
+ getNodeType: function () { return { Label: 'State', PropertiesPanel: { PanelType: 'Form', DefaultWidth: 300, DefaultHeight: 220, Title: 'State' } }; }
56
+ },
57
+ renderFlow: function () {},
58
+ marshalFromView: function () {},
59
+ _PropertiesPanelView: { destroyPanel: function () {} },
60
+ _EventHandlerProvider: { fireEvent: function () {} }
61
+ };
62
+
63
+ _PanelManager = new libPanelManager(_Fable, { FlowView: _MockFlowView }, 'PM-Test');
64
+ }
65
+ );
66
+
67
+ test
68
+ (
69
+ 'openConnectionPanel returns false when no ConnectionPropertiesPanel is configured',
70
+ function ()
71
+ {
72
+ let tmpResult = _PanelManager.openConnectionPanel('c1');
73
+ libExpect(tmpResult).to.equal(false);
74
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(0);
75
+ }
76
+ );
77
+
78
+ test
79
+ (
80
+ 'openConnectionPanel returns false for an unknown connection',
81
+ function ()
82
+ {
83
+ _MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
84
+ let tmpResult = _PanelManager.openConnectionPanel('no-such-connection');
85
+ libExpect(tmpResult).to.equal(false);
86
+ }
87
+ );
88
+
89
+ test
90
+ (
91
+ 'openConnectionPanel opens a panel carrying the ConnectionHash, placed near the midpoint',
92
+ function ()
93
+ {
94
+ _MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form', DefaultWidth: 320, DefaultHeight: 240, Title: 'Transition' };
95
+ let tmpPanel = _PanelManager.openConnectionPanel('c1');
96
+
97
+ libExpect(tmpPanel).to.be.an('object');
98
+ libExpect(tmpPanel.ConnectionHash).to.equal('c1');
99
+ libExpect(tmpPanel.NodeHash).to.equal(null);
100
+ libExpect(tmpPanel.Title).to.equal('Transition');
101
+ libExpect(tmpPanel.Width).to.equal(320);
102
+ libExpect(tmpPanel.Height).to.equal(240);
103
+ // Midpoint is (200, 30); the panel is offset from it.
104
+ libExpect(tmpPanel.X).to.equal(240);
105
+ libExpect(tmpPanel.Y).to.equal(50);
106
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(1);
107
+ }
108
+ );
109
+
110
+ test
111
+ (
112
+ 'openConnectionPanel is idempotent: a second open returns the same panel',
113
+ function ()
114
+ {
115
+ _MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
116
+ let tmpFirst = _PanelManager.openConnectionPanel('c1');
117
+ let tmpSecond = _PanelManager.openConnectionPanel('c1');
118
+ libExpect(tmpSecond.Hash).to.equal(tmpFirst.Hash);
119
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(1);
120
+ }
121
+ );
122
+
123
+ test
124
+ (
125
+ 'toggleConnectionPanel opens then closes',
126
+ function ()
127
+ {
128
+ _MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
129
+ let tmpOpened = _PanelManager.toggleConnectionPanel('c1');
130
+ libExpect(tmpOpened).to.be.an('object');
131
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(1);
132
+
133
+ let tmpClosed = _PanelManager.toggleConnectionPanel('c1');
134
+ libExpect(tmpClosed).to.equal(false);
135
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(0);
136
+ }
137
+ );
138
+
139
+ test
140
+ (
141
+ 'closePanelForConnection removes the connection panel',
142
+ function ()
143
+ {
144
+ _MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
145
+ _PanelManager.openConnectionPanel('c1');
146
+ let tmpRemoved = _PanelManager.closePanelForConnection('c1');
147
+ libExpect(tmpRemoved).to.equal(true);
148
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(0);
149
+ }
150
+ );
151
+
152
+ test
153
+ (
154
+ 'node panels still open alongside connection panels, keyed separately',
155
+ function ()
156
+ {
157
+ _MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
158
+ let tmpNodePanel = _PanelManager.openPanel('n1');
159
+ let tmpConnPanel = _PanelManager.openConnectionPanel('c1');
160
+
161
+ libExpect(tmpNodePanel.NodeHash).to.equal('n1');
162
+ libExpect(tmpConnPanel.ConnectionHash).to.equal('c1');
163
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(2);
164
+
165
+ // Closing the connection panel leaves the node panel intact.
166
+ _PanelManager.closePanelForConnection('c1');
167
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(1);
168
+ libExpect(_MockFlowView._FlowData.OpenPanels[0].NodeHash).to.equal('n1');
169
+ }
170
+ );
171
+ }
172
+ );
@@ -0,0 +1,133 @@
1
+ const libFable = require('fable');
2
+ const libChai = require('chai');
3
+ const libExpect = libChai.expect;
4
+
5
+ const libRenderer = require('../source/providers/PictProvider-Flow-Renderer.js');
6
+
7
+ suite('PictProvider-Flow-Renderer',
8
+ function ()
9
+ {
10
+ let _Fable;
11
+ let _Renderer;
12
+
13
+ setup(function ()
14
+ {
15
+ _Fable = new libFable({});
16
+ _Renderer = new libRenderer(_Fable, {}, 'Renderer-Test');
17
+ });
18
+
19
+ suite('Built-in renderer registry', function ()
20
+ {
21
+ test('registers clean, bracket, sketch, crt, workstation by default', function ()
22
+ {
23
+ let tmpKeys = _Renderer.getRendererKeys();
24
+ libExpect(tmpKeys).to.include('clean');
25
+ libExpect(tmpKeys).to.include('bracket');
26
+ libExpect(tmpKeys).to.include('sketch');
27
+ libExpect(tmpKeys).to.include('crt');
28
+ libExpect(tmpKeys).to.include('workstation');
29
+ });
30
+
31
+ test('clean is the default active renderer', function ()
32
+ {
33
+ libExpect(_Renderer.getActiveRendererKey()).to.equal('clean');
34
+ });
35
+
36
+ test('each built-in renderer has the required shape', function ()
37
+ {
38
+ let tmpKeys = _Renderer.getRendererKeys();
39
+ for (let i = 0; i < tmpKeys.length; i++)
40
+ {
41
+ let tmpR = _Renderer.getActiveRenderer.call(
42
+ { _Renderers: _Renderer._Renderers, _ActiveRendererKey: tmpKeys[i] });
43
+ libExpect(tmpR, `renderer ${tmpKeys[i]}`).to.have.property('Key', tmpKeys[i]);
44
+ libExpect(tmpR).to.have.property('NodeBodyMode');
45
+ libExpect(tmpR).to.have.property('NoiseConfig');
46
+ libExpect(tmpR).to.have.property('ConnectionConfig');
47
+ libExpect(tmpR).to.have.property('GeometryCSS');
48
+ }
49
+ });
50
+ });
51
+
52
+ suite('setRenderer()', function ()
53
+ {
54
+ test('switches the active renderer + updates noise default', function ()
55
+ {
56
+ _Renderer.setRenderer('sketch');
57
+ libExpect(_Renderer.getActiveRendererKey()).to.equal('sketch');
58
+ let tmpActive = _Renderer.getActiveRenderer();
59
+ libExpect(tmpActive.NodeBodyMode).to.equal('bracket');
60
+ libExpect(_Renderer.getNoiseLevel()).to.equal(0.4);
61
+ });
62
+
63
+ test('returns false for unknown renderer + leaves state unchanged', function ()
64
+ {
65
+ _Renderer.setRenderer('sketch');
66
+ let tmpResult = _Renderer.setRenderer('nonexistent');
67
+ libExpect(tmpResult).to.equal(false);
68
+ libExpect(_Renderer.getActiveRendererKey()).to.equal('sketch');
69
+ });
70
+
71
+ test('switching to a renderer with noise disabled resets noise to 0', function ()
72
+ {
73
+ _Renderer.setRenderer('sketch'); // 0.4
74
+ _Renderer.setRenderer('clean'); // 0
75
+ libExpect(_Renderer.getNoiseLevel()).to.equal(0);
76
+ });
77
+ });
78
+
79
+ suite('register()', function ()
80
+ {
81
+ test('registers a custom renderer + reads back through getActiveRenderer', function ()
82
+ {
83
+ _Renderer.register('custom', {
84
+ Label: 'Custom',
85
+ NodeBodyMode: 'rect',
86
+ NoiseConfig: { Enabled: false, DefaultLevel: 0, MaxJitterPx: 0, AffectsNodes: false, AffectsConnections: false },
87
+ ConnectionConfig: { StrokeWidth: 3, ArrowheadStyle: 'triangle' },
88
+ GeometryCSS: '',
89
+ AdditionalCSS: ''
90
+ });
91
+ let tmpResult = _Renderer.setRenderer('custom');
92
+ libExpect(tmpResult).to.equal(true);
93
+ let tmpActive = _Renderer.getActiveRenderer();
94
+ libExpect(tmpActive.Key).to.equal('custom');
95
+ libExpect(tmpActive.Label).to.equal('Custom');
96
+ });
97
+ });
98
+
99
+ suite('Noise APIs', function ()
100
+ {
101
+ test('setNoiseLevel clamps to [0,1]', function ()
102
+ {
103
+ _Renderer.setNoiseLevel(-1);
104
+ libExpect(_Renderer.getNoiseLevel()).to.equal(0);
105
+ _Renderer.setNoiseLevel(2);
106
+ libExpect(_Renderer.getNoiseLevel()).to.equal(1);
107
+ _Renderer.setNoiseLevel(0.5);
108
+ libExpect(_Renderer.getNoiseLevel()).to.equal(0.5);
109
+ });
110
+
111
+ test('getNodeNoiseAmplitude is 0 when active renderer disables noise', function ()
112
+ {
113
+ _Renderer.setRenderer('clean');
114
+ _Renderer.setNoiseLevel(1);
115
+ libExpect(_Renderer.getNodeNoiseAmplitude()).to.equal(0);
116
+ });
117
+
118
+ test('getNodeNoiseAmplitude scales with noise level when sketch is active', function ()
119
+ {
120
+ _Renderer.setRenderer('sketch');
121
+ _Renderer.setNoiseLevel(0.5);
122
+ // sketch.MaxJitterPx is 4 — 0.5 * 4 = 2
123
+ libExpect(_Renderer.getNodeNoiseAmplitude()).to.equal(2);
124
+ });
125
+
126
+ test('processPathString is a no-op when noise is disabled', function ()
127
+ {
128
+ _Renderer.setRenderer('clean');
129
+ let tmpResult = _Renderer.processPathString('M 0 0 L 10 10', 'seed');
130
+ libExpect(tmpResult).to.equal('M 0 0 L 10 10');
131
+ });
132
+ });
133
+ });