@sequent-org/moodboard 1.4.32 → 1.4.34

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 (137) hide show
  1. package/package.json +5 -1
  2. package/src/assets/fonts/inter/inter-cyrillic-400-normal.woff2 +0 -0
  3. package/src/assets/fonts/inter/inter-cyrillic-500-normal.woff2 +0 -0
  4. package/src/assets/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  5. package/src/assets/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  6. package/src/assets/icons/attachments.svg +3 -1
  7. package/src/assets/icons/comments.svg +2 -2
  8. package/src/assets/icons/connector.svg +6 -0
  9. package/src/assets/icons/emoji.svg +6 -1
  10. package/src/assets/icons/frame.svg +4 -1
  11. package/src/assets/icons/image.svg +5 -1
  12. package/src/assets/icons/laser.svg +1 -0
  13. package/src/assets/icons/lasso.svg +5 -0
  14. package/src/assets/icons/mindmap.svg +10 -2
  15. package/src/assets/icons/note.svg +4 -1
  16. package/src/assets/icons/pan.svg +5 -2
  17. package/src/assets/icons/pencil.svg +4 -1
  18. package/src/assets/icons/reactions.svg +5 -0
  19. package/src/assets/icons/redo.svg +3 -2
  20. package/src/assets/icons/select.svg +2 -8
  21. package/src/assets/icons/shapes.svg +5 -1
  22. package/src/assets/icons/text-add.svg +15 -1
  23. package/src/assets/icons/undo.svg +3 -2
  24. package/src/assets/reactions/1f44d.svg +20 -0
  25. package/src/assets/reactions/1f44e.svg +20 -0
  26. package/src/assets/reactions/2705.svg +20 -0
  27. package/src/assets/reactions/274c.svg +19 -0
  28. package/src/assets/reactions/2753.svg +20 -0
  29. package/src/assets/reactions/2764.svg +22 -0
  30. package/src/assets/reactions/2b50.svg +19 -0
  31. package/src/assets/reactions/plus-one.svg +25 -0
  32. package/src/core/PixiEngine.js +23 -0
  33. package/src/core/bootstrap/CoreInitializer.js +43 -0
  34. package/src/core/commands/GroupDeleteCommand.js +13 -1
  35. package/src/core/commands/UpdateShapeStyleCommand.js +121 -0
  36. package/src/core/commands/UpdateTextStyleCommand.js +17 -6
  37. package/src/core/commands/index.js +3 -0
  38. package/src/core/events/Events.js +22 -0
  39. package/src/core/flows/LayerAndViewportFlow.js +1 -0
  40. package/src/core/flows/ObjectLifecycleFlow.js +155 -7
  41. package/src/core/index.js +28 -1
  42. package/src/grid/CrossGridZoomPhases.js +3 -3
  43. package/src/initNoBundler.js +1 -1
  44. package/src/moodboard/DataManager.js +28 -0
  45. package/src/moodboard/MoodBoard.js +27 -0
  46. package/src/moodboard/bootstrap/MoodBoardInitializer.js +69 -1
  47. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +22 -4
  48. package/src/moodboard/integration/MoodBoardEventBindings.js +5 -1
  49. package/src/moodboard/integration/MoodBoardLoadApi.js +10 -1
  50. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +9 -0
  51. package/src/objects/ConnectorObject.js +2 -2
  52. package/src/objects/FrameObject.js +119 -59
  53. package/src/objects/ShapeObject.js +49 -74
  54. package/src/objects/shape/ShapeDrawer.js +210 -0
  55. package/src/services/ConnectorBindingResolver.js +112 -0
  56. package/src/services/ConnectorRouter.js +210 -0
  57. package/src/services/ai/ChatSessionController.js +14 -8
  58. package/src/services/comments/CommentService.js +344 -0
  59. package/src/tools/object-tools/CommentTool.js +85 -0
  60. package/src/tools/object-tools/DrawingTool.js +110 -10
  61. package/src/tools/object-tools/LaserPointerTool.js +121 -0
  62. package/src/tools/object-tools/SelectTool.js +25 -1
  63. package/src/tools/object-tools/TextTool.js +6 -1
  64. package/src/tools/object-tools/connector/ConnectorDragController.js +50 -3
  65. package/src/tools/object-tools/connector/connectorGesture.js +33 -19
  66. package/src/tools/object-tools/placement/PlacementInputRouter.js +22 -1
  67. package/src/tools/object-tools/selection/BoxSelectController.js +24 -2
  68. package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +139 -0
  69. package/src/tools/object-tools/selection/InlineEditorController.js +12 -0
  70. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +36 -0
  71. package/src/tools/object-tools/selection/LassoSelectController.js +125 -0
  72. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +1 -0
  73. package/src/tools/object-tools/selection/SelectInputRouter.js +64 -5
  74. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +11 -1
  75. package/src/tools/object-tools/selection/SelectToolSetup.js +13 -1
  76. package/src/tools/object-tools/selection/TextEditorInteractionController.js +46 -12
  77. package/src/tools/object-tools/selection/TextEditorSyncService.js +1 -0
  78. package/src/tools/object-tools/selection/TextInlineEditorController.js +65 -6
  79. package/src/ui/CommentPopover.js +6 -0
  80. package/src/ui/CommentsBar.js +91 -0
  81. package/src/ui/ConnectorPropertiesPanel.js +150 -0
  82. package/src/ui/ContextMenu.js +25 -0
  83. package/src/ui/DrawingPropertiesPanel.js +362 -0
  84. package/src/ui/FilePropertiesPanel.js +5 -0
  85. package/src/ui/FramePropertiesPanel.js +5 -0
  86. package/src/ui/HtmlTextLayer.js +246 -66
  87. package/src/ui/NotePropertiesPanel.js +6 -0
  88. package/src/ui/ShapePropertiesPanel.js +307 -0
  89. package/src/ui/TextPropertiesPanel.js +100 -1
  90. package/src/ui/Toolbar.js +25 -2
  91. package/src/ui/Topbar.js +2 -2
  92. package/src/ui/animation/HoverLiftController.js +6 -7
  93. package/src/ui/chat/ChatComposer.js +63 -9
  94. package/src/ui/chat/ChatWindow.js +329 -166
  95. package/src/ui/comments/CommentListPanel.js +213 -0
  96. package/src/ui/comments/CommentPinLayer.js +448 -0
  97. package/src/ui/comments/CommentThreadPopover.js +539 -0
  98. package/src/ui/comments/commentFormat.js +32 -0
  99. package/src/ui/connector-properties/ConnectorPropertiesPanelBindings.js +223 -0
  100. package/src/ui/connector-properties/ConnectorPropertiesPanelEventBridge.js +114 -0
  101. package/src/ui/connector-properties/ConnectorPropertiesPanelMapper.js +144 -0
  102. package/src/ui/connector-properties/ConnectorPropertiesPanelRenderer.js +447 -0
  103. package/src/ui/connector-properties/ConnectorPropertiesPanelState.js +61 -0
  104. package/src/ui/connectors/ConnectionAnchorsLayer.js +1 -0
  105. package/src/ui/connectors/ConnectorHandlesLayer.js +321 -0
  106. package/src/ui/connectors/ConnectorLabelLayer.js +334 -0
  107. package/src/ui/connectors/ConnectorLayer.js +264 -57
  108. package/src/ui/handles/HandlesDomRenderer.js +5 -13
  109. package/src/ui/handles/HandlesEventBridge.js +1 -0
  110. package/src/ui/handles/SingleSelectionHandlesController.js +4 -0
  111. package/src/ui/mindmap/MindmapCollapseLayer.js +1 -0
  112. package/src/ui/mindmap/MindmapConnectionLayer.js +1 -0
  113. package/src/ui/mindmap/MindmapHtmlTextLayer.js +6 -0
  114. package/src/ui/shape-properties/ShapePropertiesPanelDom.js +533 -0
  115. package/src/ui/shape-properties/ShapePropertiesPanelSync.js +132 -0
  116. package/src/ui/styles/chat.css +710 -18
  117. package/src/ui/styles/index.css +1 -0
  118. package/src/ui/styles/panels.css +112 -2
  119. package/src/ui/styles/shape-properties-panel.css +250 -0
  120. package/src/ui/styles/toolbar.css +7 -2
  121. package/src/ui/styles/topbar.css +1 -1
  122. package/src/ui/styles/workspace.css +257 -6
  123. package/src/ui/text-properties/TextFormatControls.js +88 -0
  124. package/src/ui/text-properties/TextListRenderer.js +137 -0
  125. package/src/ui/text-properties/TextPropertiesPanelBindings.js +27 -0
  126. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +3 -1
  127. package/src/ui/text-properties/TextPropertiesPanelMapper.js +56 -0
  128. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +24 -0
  129. package/src/ui/text-properties/TextPropertiesPanelState.js +8 -0
  130. package/src/ui/toolbar/ReactionsPopupController.js +88 -0
  131. package/src/ui/toolbar/ToolbarActionRouter.js +71 -5
  132. package/src/ui/toolbar/ToolbarPopupsController.js +120 -118
  133. package/src/ui/toolbar/ToolbarRenderer.js +9 -1
  134. package/src/ui/toolbar/ToolbarStateController.js +4 -1
  135. package/src/utils/iconLoader.js +17 -16
  136. package/src/utils/markdown.js +14 -0
  137. package/src/utils/richText.js +125 -0
@@ -1,28 +1,46 @@
1
1
  import * as PIXI from 'pixi.js';
2
+ import { drawShape } from './shape/ShapeDrawer.js';
3
+
4
+ const DEFAULTS = {
5
+ borderColor: 0xd4d4d4,
6
+ borderWidth: 1,
7
+ borderStyle: 'solid',
8
+ borderOpacity: 1,
9
+ };
2
10
 
3
11
  /**
4
12
  * Класс объекта «Фигура»
5
13
  * Отвечает за создание и перерисовку фигур разных типов с сохранением формы при ресайзе.
14
+ *
15
+ * State-контракт:
16
+ * color: <number> — заливка
17
+ * properties.kind — square|rounded|circle|triangle|diamond|parallelogram|arrow
18
+ * properties.cornerRadius — радиус скругления (применяется к square тоже, 0..N)
19
+ * properties.borderColor — цвет обводки (PIXI number), дефолт 0xD4D4D4
20
+ * properties.borderWidth — толщина обводки (world px), дефолт 1
21
+ * properties.borderStyle — 'solid'|'dashed'|'dotted', дефолт 'solid'
22
+ * properties.borderOpacity — 0..1, дефолт 1
6
23
  */
7
24
  export class ShapeObject {
8
25
  /**
9
26
  * @param {Object} objectData Полные данные объекта из состояния
10
- * - width, height
11
- * - color
12
- * - properties.kind: 'square' | 'rounded' | 'circle' | 'triangle' | 'diamond' | 'parallelogram' | 'arrow'
13
- * - properties.cornerRadius?: number (для rounded)
14
27
  */
15
28
  constructor(objectData = {}) {
16
29
  this.objectData = objectData;
17
30
  this.width = objectData.width || 100;
18
31
  this.height = objectData.height || 100;
19
32
  this.fillColor = objectData.color ?? 0xffffff;
33
+
20
34
  const props = objectData.properties || {};
21
35
  this.kind = props.kind || 'square';
22
- this.cornerRadius = props.cornerRadius || 10;
36
+ this.cornerRadius = props.cornerRadius ?? (this.kind === 'rounded' ? 10 : 0);
37
+ this.borderColor = props.borderColor ?? DEFAULTS.borderColor;
38
+ this.borderWidth = props.borderWidth ?? DEFAULTS.borderWidth;
39
+ this.borderStyle = props.borderStyle ?? DEFAULTS.borderStyle;
40
+ this.borderOpacity = props.borderOpacity ?? DEFAULTS.borderOpacity;
23
41
 
24
42
  this.graphics = new PIXI.Graphics();
25
- this._draw(this.width, this.height, this.fillColor, this.kind, this.cornerRadius);
43
+ this._draw(this.width, this.height);
26
44
  }
27
45
 
28
46
  /** Возвращает PIXI-объект */
@@ -34,15 +52,25 @@ export class ShapeObject {
34
52
  setColor(color) {
35
53
  if (typeof color === 'number') {
36
54
  this.fillColor = color;
37
- this._redrawPreserveTransform(this.width, this.height, this.fillColor, this.kind, this.cornerRadius);
55
+ this._redrawPreserveTransform(this.width, this.height);
38
56
  }
39
57
  }
40
58
 
41
- /** Обновить свойства фигуры (тип, радиус скругления) */
42
- setProperties({ kind, cornerRadius } = {}) {
59
+ /** Обновить параметры обводки */
60
+ setStroke({ borderColor, borderWidth, borderStyle, borderOpacity } = {}) {
61
+ if (typeof borderColor === 'number') this.borderColor = borderColor;
62
+ if (typeof borderWidth === 'number') this.borderWidth = borderWidth;
63
+ if (borderStyle !== undefined) this.borderStyle = borderStyle;
64
+ if (typeof borderOpacity === 'number') this.borderOpacity = borderOpacity;
65
+ this._redrawPreserveTransform(this.width, this.height);
66
+ }
67
+
68
+ /** Обновить свойства фигуры (тип, радиус скругления, стиль обводки) */
69
+ setProperties({ kind, cornerRadius, borderStyle } = {}) {
43
70
  if (kind) this.kind = kind;
44
71
  if (typeof cornerRadius === 'number') this.cornerRadius = cornerRadius;
45
- this._redrawPreserveTransform(this.width, this.height, this.fillColor, this.kind, this.cornerRadius);
72
+ if (borderStyle !== undefined) this.borderStyle = borderStyle;
73
+ this._redrawPreserveTransform(this.width, this.height);
46
74
  }
47
75
 
48
76
  /** Обновить размер фигуры */
@@ -52,84 +80,31 @@ export class ShapeObject {
52
80
  const h = Math.max(0, size.height || 0);
53
81
  this.width = w;
54
82
  this.height = h;
55
- this._redrawPreserveTransform(w, h, this.fillColor, this.kind, this.cornerRadius);
83
+ this._redrawPreserveTransform(w, h);
56
84
  }
57
85
 
58
86
  /** Перерисовать с сохранением трансформаций */
59
- _redrawPreserveTransform(width, height, color, kind, cornerRadius) {
87
+ _redrawPreserveTransform(width, height) {
60
88
  const g = this.graphics;
61
- // Сохраняем текущий центр и поворот
62
89
  const centerX = g.x;
63
90
  const centerY = g.y;
64
91
  const rot = g.rotation || 0;
65
92
 
66
- this._draw(width, height, color, kind, cornerRadius);
67
- // ВАЖНО: для согласованности с ядром (позиция левый-верх, PIXI — центр)
68
- // pivot должен всегда быть в центре объекта (w/2, h/2)
93
+ this._draw(width, height);
94
+ // pivot всегда центр, чтобы согласовываться с ядром (top-left center)
69
95
  g.pivot.set(width / 2, height / 2);
70
- // Восстанавливаем центр
71
96
  g.x = centerX;
72
97
  g.y = centerY;
73
98
  g.rotation = rot;
74
99
  }
75
100
 
76
101
  /** Непосредственная отрисовка фигуры */
77
- _draw(w, h, color, kind, cornerRadius) {
78
- const g = this.graphics;
79
- g.clear();
80
- g.beginFill(color, 1);
81
- switch (kind) {
82
- case 'circle': {
83
- const r = Math.min(w, h) / 2;
84
- g.drawCircle(w / 2, h / 2, r);
85
- break;
86
- }
87
- case 'rounded': {
88
- const r = cornerRadius || 10;
89
- g.drawRoundedRect(0, 0, w, h, r);
90
- break;
91
- }
92
- case 'triangle': {
93
- g.moveTo(w / 2, 0);
94
- g.lineTo(w, h);
95
- g.lineTo(0, h);
96
- g.lineTo(w / 2, 0);
97
- break;
98
- }
99
- case 'diamond': {
100
- g.moveTo(w / 2, 0);
101
- g.lineTo(w, h / 2);
102
- g.lineTo(w / 2, h);
103
- g.lineTo(0, h / 2);
104
- g.lineTo(w / 2, 0);
105
- break;
106
- }
107
- case 'parallelogram': {
108
- const skew = Math.min(w * 0.25, 20);
109
- g.moveTo(skew, 0);
110
- g.lineTo(w, 0);
111
- g.lineTo(w - skew, h);
112
- g.lineTo(0, h);
113
- g.lineTo(skew, 0);
114
- break;
115
- }
116
- case 'arrow': {
117
- const shaftH = Math.max(6, h * 0.3);
118
- const shaftY = (h - shaftH) / 2;
119
- g.drawRect(0, shaftY, w * 0.6, shaftH);
120
- g.moveTo(w * 0.6, 0);
121
- g.lineTo(w, h / 2);
122
- g.lineTo(w * 0.6, h);
123
- g.lineTo(w * 0.6, 0);
124
- break;
125
- }
126
- case 'square':
127
- default: {
128
- g.drawRect(0, 0, w, h);
129
- break;
130
- }
131
- }
132
- g.endFill();
102
+ _draw(w, h) {
103
+ drawShape(this.graphics, w, h, this.fillColor, this.kind, this.cornerRadius, {
104
+ borderColor: this.borderColor,
105
+ borderWidth: this.borderWidth,
106
+ borderStyle: this.borderStyle,
107
+ borderOpacity: this.borderOpacity,
108
+ });
133
109
  }
134
110
  }
135
-
@@ -0,0 +1,210 @@
1
+ import * as PIXI from 'pixi.js';
2
+
3
+ /**
4
+ * Рисует сегменты пунктирного контура вдоль пути из точек (замкнутый полигон).
5
+ * PIXI v7 не поддерживает dash natively — имитируем сегментами moveTo/lineTo.
6
+ * @param {PIXI.Graphics} g
7
+ * @param {Array<{x:number,y:number}>} pts — вершины замкнутого полигона
8
+ * @param {number} dash — длина штриха (px world)
9
+ * @param {number} gap — длина пробела (px world)
10
+ */
11
+ function drawDashedPolygon(g, pts, dash, gap) {
12
+ const n = pts.length;
13
+ let offset = 0;
14
+ let drawing = true;
15
+
16
+ for (let i = 0; i < n; i++) {
17
+ const a = pts[i];
18
+ const b = pts[(i + 1) % n];
19
+ const dx = b.x - a.x;
20
+ const dy = b.y - a.y;
21
+ const segLen = Math.sqrt(dx * dx + dy * dy);
22
+ const ux = dx / segLen;
23
+ const uy = dy / segLen;
24
+ let pos = 0;
25
+
26
+ while (pos < segLen) {
27
+ const rem = drawing ? dash - offset : gap - offset;
28
+ const step = Math.min(rem, segLen - pos);
29
+ if (drawing) {
30
+ g.moveTo(a.x + ux * pos, a.y + uy * pos);
31
+ g.lineTo(a.x + ux * (pos + step), a.y + uy * (pos + step));
32
+ }
33
+ pos += step;
34
+ offset += step;
35
+ const period = drawing ? dash : gap;
36
+ if (offset >= period) {
37
+ offset = 0;
38
+ drawing = !drawing;
39
+ }
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Рисует пунктирный прямоугольник (замкнутый).
46
+ * @param {PIXI.Graphics} g
47
+ * @param {number} x @param {number} y @param {number} w @param {number} h
48
+ * @param {number} dash @param {number} gap
49
+ */
50
+ function drawDashedRect(g, x, y, w, h, dash, gap) {
51
+ const pts = [
52
+ { x, y },
53
+ { x: x + w, y },
54
+ { x: x + w, y: y + h },
55
+ { x, y: y + h },
56
+ ];
57
+ drawDashedPolygon(g, pts, dash, gap);
58
+ }
59
+
60
+ /**
61
+ * Рисует пунктирную окружность сегментами дуги.
62
+ * @param {PIXI.Graphics} g
63
+ * @param {number} cx @param {number} cy @param {number} r
64
+ * @param {number} dash @param {number} gap
65
+ */
66
+ function drawDashedCircle(g, cx, cy, r, dash, gap) {
67
+ const circum = 2 * Math.PI * r;
68
+ const total = dash + gap;
69
+ const steps = Math.round(circum / total) || 1;
70
+ const dashAngle = (dash / circum) * 2 * Math.PI;
71
+ const gapAngle = (gap / circum) * 2 * Math.PI;
72
+
73
+ for (let i = 0; i < steps; i++) {
74
+ const startAngle = i * (dashAngle + gapAngle) - Math.PI / 2;
75
+ const endAngle = startAngle + dashAngle;
76
+ g.arc(cx, cy, r, startAngle, endAngle);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Применяет lineStyle к Graphics-объекту.
82
+ * При borderWidth === 0 выключает контур.
83
+ */
84
+ export function applyLineStyle(g, borderWidth, borderColor, borderOpacity) {
85
+ if (borderWidth === 0) {
86
+ g.lineStyle(0);
87
+ } else {
88
+ g.lineStyle(borderWidth, borderColor, borderOpacity);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Настройки штриха.
94
+ * @param {'solid'|'dashed'|'dotted'} style
95
+ * @returns {{ dash: number, gap: number }}
96
+ */
97
+ function dashParams(style) {
98
+ if (style === 'dotted') return { dash: 2, gap: 4 };
99
+ if (style === 'dashed') return { dash: 8, gap: 6 };
100
+ return { dash: 0, gap: 0 };
101
+ }
102
+
103
+ /**
104
+ * Рисует фигуру в Graphics с учётом обводки, заливки и borderStyle.
105
+ *
106
+ * @param {PIXI.Graphics} g
107
+ * @param {number} w ширина (world px)
108
+ * @param {number} h высота (world px)
109
+ * @param {number} color заливка (PIXI color number)
110
+ * @param {string} kind тип фигуры
111
+ * @param {number} cornerRadius
112
+ * @param {object} stroke
113
+ * @param {number} stroke.borderColor
114
+ * @param {number} stroke.borderWidth
115
+ * @param {'solid'|'dashed'|'dotted'} stroke.borderStyle
116
+ * @param {number} stroke.borderOpacity
117
+ */
118
+ export function drawShape(g, w, h, color, kind, cornerRadius, stroke) {
119
+ const { borderColor, borderWidth, borderStyle, borderOpacity } = stroke;
120
+ const isDash = borderWidth > 0 && (borderStyle === 'dashed' || borderStyle === 'dotted');
121
+ const { dash, gap } = dashParams(borderStyle);
122
+
123
+ g.clear();
124
+
125
+ // --- заливка (всегда solid через beginFill/endFill) ---
126
+ g.lineStyle(0);
127
+ g.beginFill(color, 1);
128
+ _drawFillShape(g, w, h, kind, cornerRadius);
129
+ g.endFill();
130
+
131
+ // --- обводка ---
132
+ if (borderWidth === 0) return;
133
+
134
+ if (!isDash || (kind !== 'square' && kind !== 'rounded' && kind !== 'circle')) {
135
+ // solid или форма без поддержки dash — одна линия через lineStyle
136
+ applyLineStyle(g, borderWidth, borderColor, borderOpacity);
137
+ g.beginFill(0, 0);
138
+ _drawFillShape(g, w, h, kind, cornerRadius);
139
+ g.endFill();
140
+ // TODO: dashed/dotted для triangle/diamond/parallelogram/arrow — следующая фаза
141
+ } else {
142
+ // dash/dotted для rect/rounded/circle: рисуем сегменты без fill
143
+ applyLineStyle(g, borderWidth, borderColor, borderOpacity);
144
+ if (kind === 'circle') {
145
+ const r = Math.min(w, h) / 2;
146
+ drawDashedCircle(g, w / 2, h / 2, r, dash, gap);
147
+ } else {
148
+ // square и rounded — прямоугольный периметр
149
+ drawDashedRect(g, 0, 0, w, h, dash, gap);
150
+ }
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Рисует замкнутую форму для заливки (без lineStyle).
156
+ * Используется внутри beginFill/endFill.
157
+ */
158
+ function _drawFillShape(g, w, h, kind, cornerRadius) {
159
+ switch (kind) {
160
+ case 'circle': {
161
+ const r = Math.min(w, h) / 2;
162
+ g.drawCircle(w / 2, h / 2, r);
163
+ break;
164
+ }
165
+ case 'rounded': {
166
+ const r = cornerRadius || 10;
167
+ g.drawRoundedRect(0, 0, w, h, r);
168
+ break;
169
+ }
170
+ case 'triangle': {
171
+ g.moveTo(w / 2, 0);
172
+ g.lineTo(w, h);
173
+ g.lineTo(0, h);
174
+ g.lineTo(w / 2, 0);
175
+ break;
176
+ }
177
+ case 'diamond': {
178
+ g.moveTo(w / 2, 0);
179
+ g.lineTo(w, h / 2);
180
+ g.lineTo(w / 2, h);
181
+ g.lineTo(0, h / 2);
182
+ g.lineTo(w / 2, 0);
183
+ break;
184
+ }
185
+ case 'parallelogram': {
186
+ const skew = Math.min(w * 0.25, 20);
187
+ g.moveTo(skew, 0);
188
+ g.lineTo(w, 0);
189
+ g.lineTo(w - skew, h);
190
+ g.lineTo(0, h);
191
+ g.lineTo(skew, 0);
192
+ break;
193
+ }
194
+ case 'arrow': {
195
+ const shaftH = Math.max(6, h * 0.3);
196
+ const shaftY = (h - shaftH) / 2;
197
+ g.drawRect(0, shaftY, w * 0.6, shaftH);
198
+ g.moveTo(w * 0.6, 0);
199
+ g.lineTo(w, h / 2);
200
+ g.lineTo(w * 0.6, h);
201
+ g.lineTo(w * 0.6, 0);
202
+ break;
203
+ }
204
+ case 'square':
205
+ default: {
206
+ g.drawRect(0, 0, w, h);
207
+ break;
208
+ }
209
+ }
210
+ }
@@ -73,6 +73,26 @@ function clipRayToAABB(from, to, hw, hh) {
73
73
  };
74
74
  }
75
75
 
76
+ /**
77
+ * Грань и единичная внешняя нормаль из нормализованного якоря [0..1].
78
+ * Возвращает локальные координаты (центр объекта = 0,0): на какой кромке сидит якорь
79
+ * и куда смотрит нормаль. Координата вдоль кромки сохраняется из якоря.
80
+ *
81
+ * @returns {{ dir: {x:number,y:number}, point: {x:number,y:number} }}
82
+ */
83
+ function faceFromAnchor(anchor, width, height, hw, hh) {
84
+ const ax = Math.max(0, Math.min(1, anchor?.x ?? 0.5));
85
+ const ay = Math.max(0, Math.min(1, anchor?.y ?? 0.5));
86
+ const lax = ax * width - hw;
87
+ const lay = ay * height - hh;
88
+ const dTop = ay, dBottom = 1 - ay, dLeft = ax, dRight = 1 - ax;
89
+ const min = Math.min(dTop, dBottom, dLeft, dRight);
90
+ if (min === dTop) return { dir: { x: 0, y: -1 }, point: { x: lax, y: -hh } };
91
+ if (min === dBottom) return { dir: { x: 0, y: 1 }, point: { x: lax, y: hh } };
92
+ if (min === dLeft) return { dir: { x: -1, y: 0 }, point: { x: -hw, y: lay } };
93
+ return { dir: { x: 1, y: 0 }, point: { x: hw, y: lay } };
94
+ }
95
+
76
96
  // ---------------------------------------------------------------------------
77
97
 
78
98
  export class ConnectorBindingResolver {
@@ -171,6 +191,98 @@ export class ConnectorBindingResolver {
171
191
 
172
192
  return worldExit;
173
193
  }
194
+
195
+ /**
196
+ * Разрешает терминал в дескриптор {point, dir} — точку на грани и внешнюю нормаль.
197
+ * Используется для elbow и bezier, где выход обязан быть перпендикулярен грани.
198
+ *
199
+ * @param {Object} terminal
200
+ * Привязанный: { boundId, anchor:{x,y}, isPrecise, isExact }
201
+ * Свободный: { point:{x,y} }
202
+ * @param {Object|null} target
203
+ * @param {{ x: number, y: number }|null} otherCenter
204
+ * Центр другого объекта (или свободная точка другого конца) для выбора стороны.
205
+ * @returns {{ point: {x:number,y:number}, dir: {x:number,y:number} }}
206
+ */
207
+ static resolveWithSide(terminal, target, otherCenter = null) {
208
+ // Свободный терминал: точка как есть, dir — к другому концу
209
+ if (!terminal?.boundId) {
210
+ const pt = { x: terminal?.point?.x ?? 0, y: terminal?.point?.y ?? 0 };
211
+ const dx = otherCenter ? otherCenter.x - pt.x : 0;
212
+ const dy = otherCenter ? otherCenter.y - pt.y : 0;
213
+ const len = Math.hypot(dx, dy);
214
+ const dir = len > 1e-6 ? { x: dx / len, y: dy / len } : { x: 1, y: 0 };
215
+ return { point: pt, dir };
216
+ }
217
+
218
+ if (!target) {
219
+ return { point: { x: 0, y: 0 }, dir: { x: 1, y: 0 } };
220
+ }
221
+
222
+ const left = target.position?.x ?? 0;
223
+ const top = target.position?.y ?? 0;
224
+ const width = target.width ?? target.properties?.width ?? 0;
225
+ const height = target.height ?? target.properties?.height ?? 0;
226
+ const angle = target.rotation ?? target.properties?.rotation ?? 0;
227
+
228
+ const cx = left + width / 2;
229
+ const cy = top + height / 2;
230
+ const hw = width / 2;
231
+ const hh = height / 2;
232
+
233
+ // Вектор к другому объекту в локальных координатах цели
234
+ const rawDx = (otherCenter?.x ?? cx) - cx;
235
+ const rawDy = (otherCenter?.y ?? cy) - cy;
236
+ const localD = angle !== 0
237
+ ? rotateVector({ x: rawDx, y: rawDy }, -angle)
238
+ : { x: rawDx, y: rawDy };
239
+
240
+ let localDir;
241
+ let facePoint;
242
+
243
+ // Привязка к конкретной точке-коннектору (isPrecise): грань и нормаль берём
244
+ // из самого якоря — коннектор обязан выходить именно из той точки, куда привязан,
245
+ // а не из геометрически ближайшей грани.
246
+ if (terminal.isPrecise && hw > 0 && hh > 0) {
247
+ const face = faceFromAnchor(terminal.anchor, width, height, hw, hh);
248
+ localDir = face.dir;
249
+ facePoint = face.point;
250
+ } else if (Math.abs(localD.x) >= Math.abs(localD.y)) {
251
+ // Привязка к центру → выбор грани по доминирующей оси к другому объекту
252
+ if (localD.x >= 0) {
253
+ localDir = { x: 1, y: 0 };
254
+ facePoint = { x: hw, y: 0 };
255
+ } else {
256
+ localDir = { x: -1, y: 0 };
257
+ facePoint = { x: -hw, y: 0 };
258
+ }
259
+ } else if (localD.y >= 0) {
260
+ localDir = { x: 0, y: 1 };
261
+ facePoint = { x: 0, y: hh };
262
+ } else {
263
+ localDir = { x: 0, y: -1 };
264
+ facePoint = { x: 0, y: -hh };
265
+ }
266
+
267
+ // Перевод в world-space
268
+ const worldFace = angle !== 0
269
+ ? {
270
+ x: cx + rotateVector(facePoint, angle).x,
271
+ y: cy + rotateVector(facePoint, angle).y,
272
+ }
273
+ : { x: cx + facePoint.x, y: cy + facePoint.y };
274
+
275
+ const worldDirRaw = angle !== 0 ? rotateVector(localDir, angle) : localDir;
276
+ const dLen = Math.hypot(worldDirRaw.x, worldDirRaw.y);
277
+ const dir = dLen > 1e-6
278
+ ? { x: worldDirRaw.x / dLen, y: worldDirRaw.y / dLen }
279
+ : { x: 1, y: 0 };
280
+
281
+ return {
282
+ point: { x: Math.round(worldFace.x), y: Math.round(worldFace.y) },
283
+ dir,
284
+ };
285
+ }
174
286
  }
175
287
 
176
288
  // ---------------------------------------------------------------------------