@sequent-org/moodboard 1.4.31 → 1.4.33

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 (139) 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/comments/CommentService.js +344 -0
  58. package/src/tools/object-tools/CommentTool.js +85 -0
  59. package/src/tools/object-tools/DrawingTool.js +110 -10
  60. package/src/tools/object-tools/LaserPointerTool.js +121 -0
  61. package/src/tools/object-tools/SelectTool.js +25 -1
  62. package/src/tools/object-tools/TextTool.js +6 -1
  63. package/src/tools/object-tools/connector/ConnectorDragController.js +50 -3
  64. package/src/tools/object-tools/connector/connectorGesture.js +33 -19
  65. package/src/tools/object-tools/placement/PlacementInputRouter.js +22 -1
  66. package/src/tools/object-tools/selection/BoxSelectController.js +24 -2
  67. package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +139 -0
  68. package/src/tools/object-tools/selection/InlineEditorController.js +12 -0
  69. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +36 -0
  70. package/src/tools/object-tools/selection/LassoSelectController.js +125 -0
  71. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +1 -0
  72. package/src/tools/object-tools/selection/SelectInputRouter.js +64 -5
  73. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +11 -1
  74. package/src/tools/object-tools/selection/SelectToolSetup.js +13 -1
  75. package/src/tools/object-tools/selection/TextEditorInteractionController.js +46 -12
  76. package/src/tools/object-tools/selection/TextEditorSyncService.js +1 -0
  77. package/src/tools/object-tools/selection/TextInlineEditorController.js +65 -6
  78. package/src/ui/CommentPopover.js +6 -0
  79. package/src/ui/CommentsBar.js +91 -0
  80. package/src/ui/ConnectorPropertiesPanel.js +150 -0
  81. package/src/ui/ContextMenu.js +25 -0
  82. package/src/ui/DrawingPropertiesPanel.js +362 -0
  83. package/src/ui/FilePropertiesPanel.js +5 -0
  84. package/src/ui/FramePropertiesPanel.js +5 -0
  85. package/src/ui/HtmlTextLayer.js +246 -66
  86. package/src/ui/NotePropertiesPanel.js +6 -0
  87. package/src/ui/ShapePropertiesPanel.js +307 -0
  88. package/src/ui/TextPropertiesPanel.js +100 -1
  89. package/src/ui/Toolbar.js +25 -2
  90. package/src/ui/Topbar.js +2 -2
  91. package/src/ui/animation/HoverLiftController.js +6 -7
  92. package/src/ui/chat/ChatComposer.js +59 -12
  93. package/src/ui/chat/ChatExtendedPromptModal.js +1 -12
  94. package/src/ui/chat/ChatWindow.js +60 -144
  95. package/src/ui/chat/ChatWindowRenderer.js +1 -8
  96. package/src/ui/chat/icons.js +0 -4
  97. package/src/ui/comments/CommentListPanel.js +213 -0
  98. package/src/ui/comments/CommentPinLayer.js +448 -0
  99. package/src/ui/comments/CommentThreadPopover.js +539 -0
  100. package/src/ui/comments/commentFormat.js +32 -0
  101. package/src/ui/connector-properties/ConnectorPropertiesPanelBindings.js +223 -0
  102. package/src/ui/connector-properties/ConnectorPropertiesPanelEventBridge.js +114 -0
  103. package/src/ui/connector-properties/ConnectorPropertiesPanelMapper.js +144 -0
  104. package/src/ui/connector-properties/ConnectorPropertiesPanelRenderer.js +447 -0
  105. package/src/ui/connector-properties/ConnectorPropertiesPanelState.js +61 -0
  106. package/src/ui/connectors/ConnectionAnchorsLayer.js +1 -0
  107. package/src/ui/connectors/ConnectorHandlesLayer.js +321 -0
  108. package/src/ui/connectors/ConnectorLabelLayer.js +334 -0
  109. package/src/ui/connectors/ConnectorLayer.js +264 -57
  110. package/src/ui/handles/HandlesDomRenderer.js +5 -13
  111. package/src/ui/handles/HandlesEventBridge.js +1 -0
  112. package/src/ui/handles/SingleSelectionHandlesController.js +4 -0
  113. package/src/ui/mindmap/MindmapCollapseLayer.js +1 -0
  114. package/src/ui/mindmap/MindmapConnectionLayer.js +1 -0
  115. package/src/ui/mindmap/MindmapHtmlTextLayer.js +6 -0
  116. package/src/ui/shape-properties/ShapePropertiesPanelDom.js +533 -0
  117. package/src/ui/shape-properties/ShapePropertiesPanelSync.js +132 -0
  118. package/src/ui/styles/chat.css +682 -28
  119. package/src/ui/styles/index.css +1 -0
  120. package/src/ui/styles/panels.css +112 -2
  121. package/src/ui/styles/shape-properties-panel.css +250 -0
  122. package/src/ui/styles/toolbar.css +7 -2
  123. package/src/ui/styles/topbar.css +1 -1
  124. package/src/ui/styles/workspace.css +257 -6
  125. package/src/ui/text-properties/TextFormatControls.js +88 -0
  126. package/src/ui/text-properties/TextListRenderer.js +137 -0
  127. package/src/ui/text-properties/TextPropertiesPanelBindings.js +27 -0
  128. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +3 -1
  129. package/src/ui/text-properties/TextPropertiesPanelMapper.js +56 -0
  130. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +24 -0
  131. package/src/ui/text-properties/TextPropertiesPanelState.js +8 -0
  132. package/src/ui/toolbar/ReactionsPopupController.js +88 -0
  133. package/src/ui/toolbar/ToolbarActionRouter.js +71 -5
  134. package/src/ui/toolbar/ToolbarPopupsController.js +120 -118
  135. package/src/ui/toolbar/ToolbarRenderer.js +9 -1
  136. package/src/ui/toolbar/ToolbarStateController.js +4 -1
  137. package/src/utils/iconLoader.js +17 -16
  138. package/src/utils/markdown.js +14 -0
  139. package/src/utils/richText.js +125 -0
@@ -2,6 +2,8 @@ import gsap from 'gsap';
2
2
  import { CustomEase } from 'gsap/CustomEase';
3
3
  import { Events } from '../core/events/Events.js';
4
4
  import * as PIXI from 'pixi.js';
5
+ import { renderRichText, hasMath } from '../utils/richText.js';
6
+ import { renderTextList } from './text-properties/TextListRenderer.js';
5
7
 
6
8
  gsap.registerPlugin(CustomEase);
7
9
  const TEXT_HOVER_EASE = 'hoverLiftSpring';
@@ -16,6 +18,55 @@ const prefersReducedMotion =
16
18
  typeof window !== 'undefined' &&
17
19
  window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
18
20
 
21
+ // Сильные сигналы markdown-разметки: заголовки, блоки/инлайн-код, цитаты,
22
+ // списки (маркированные и нумерованные), таблицы, ссылки, жирный/курсив.
23
+ // Намеренно консервативно, чтобы обычный однострочный текст не считался markdown.
24
+ function looksLikeMarkdown(text) {
25
+ if (!text || typeof text !== 'string') return false;
26
+ return (
27
+ /^#{1,6}\s+/m.test(text) ||
28
+ /```/.test(text) ||
29
+ /`[^`]+`/.test(text) ||
30
+ /^>\s+/m.test(text) ||
31
+ /^\s*[-*+]\s+/m.test(text) ||
32
+ /^\s*\d+\.\s+/m.test(text) ||
33
+ /^\s*\|.*\|.*$/m.test(text) ||
34
+ /\[[^\]]+\]\([^)]+\)/.test(text) ||
35
+ /(\*\*|__)[^*_\s][^*_]*(\*\*|__)/.test(text)
36
+ );
37
+ }
38
+
39
+ // Итоговое решение: явный флаг (true/false) перебивает авто-детект, иначе —
40
+ // по содержимому. Формулы KaTeX тоже включают богатый рендер: текст с одной
41
+ // формулой без markdown-разметки должен рендериться через renderRichText.
42
+ function resolveMarkdown(properties, content) {
43
+ if (properties?.markdown === true) return true;
44
+ if (properties?.markdown === false) return false;
45
+ return looksLikeMarkdown(content) || hasMath(content);
46
+ }
47
+
48
+ /**
49
+ * Возвращает коэффициент межстрочного интервала по базовому (немасштабированному) размеру шрифта.
50
+ * Коэффициент зависит от базового размера, а не от отрисованного, — это гарантирует,
51
+ * что соотношение line-height/font-size остаётся постоянным при любом зуме.
52
+ * @param {number} baseFontSizePx — базовый размер шрифта без учёта зума
53
+ * @param {object|undefined} properties
54
+ * @returns {number} коэффициент (не пиксели)
55
+ */
56
+ function resolveLineHeightRatio(baseFontSizePx, properties) {
57
+ if (typeof properties?.lineHeight === 'number') {
58
+ return properties.lineHeight;
59
+ }
60
+ const fs = baseFontSizePx;
61
+ if (fs <= 12) return 1.40;
62
+ if (fs <= 18) return 1.34;
63
+ if (fs <= 36) return 1.26;
64
+ if (fs <= 48) return 1.24;
65
+ if (fs <= 72) return 1.22;
66
+ if (fs <= 96) return 1.20;
67
+ return 1.18;
68
+ }
69
+
19
70
  /**
20
71
  * HtmlTextLayer — рисует текст как HTML-элементы поверх PIXI для максимальной чёткости
21
72
  * Синхронизирует позицию/размер/масштаб с миром (worldLayer) и состоянием объектов
@@ -54,7 +105,7 @@ export class HtmlTextLayer {
54
105
  // Подписки
55
106
  this.eventBus.on(Events.Object.Created, ({ objectId, objectData }) => {
56
107
  if (!objectData) return;
57
- if (objectData.type === 'text' || objectData.type === 'simple-text') {
108
+ if (objectData.type === 'text' || objectData.type === 'simple-text' || objectData.type === 'shape') {
58
109
  this._ensureTextEl(objectId, objectData);
59
110
  this.updateOne(objectId);
60
111
  }
@@ -113,7 +164,15 @@ export class HtmlTextLayer {
113
164
  console.log(`🔍 HtmlTextLayer: обновляю содержимое для объекта ${objectId}:`, content);
114
165
  const el = this.idToEl.get(objectId);
115
166
  if (el && typeof content === 'string') {
116
- el.textContent = content;
167
+ const _obj = this.core?.state?.state?.objects?.find(o => o.id === objectId);
168
+ const isMarkdown = resolveMarkdown(_obj?.properties, content);
169
+ this._syncElementContent(el, content, isMarkdown);
170
+ if (el.classList.contains('mb-text--md') !== isMarkdown) {
171
+ el.classList.toggle('mb-text--md', isMarkdown);
172
+ el.style.whiteSpace = isMarkdown ? 'normal' : 'pre';
173
+ el.style.overflowWrap = isMarkdown ? 'break-word' : '';
174
+ if (!isMarkdown) el.style.padding = '0';
175
+ }
117
176
  console.log(`🔍 HtmlTextLayer: содержимое обновлено для ${objectId}:`, content);
118
177
  } else {
119
178
  console.warn(`❌ HtmlTextLayer: не удалось обновить содержимое для ${objectId}:`, { el: !!el, content });
@@ -132,17 +191,9 @@ export class HtmlTextLayer {
132
191
  }
133
192
  if (updates.fontSize) {
134
193
  el.style.fontSize = `${updates.fontSize}px`;
135
- // Также обновляем line-height согласно новой шкале
136
194
  const fs = updates.fontSize;
137
- const lh = (fs <= 12) ? Math.round(fs * 1.40)
138
- : (fs <= 18) ? Math.round(fs * 1.34)
139
- : (fs <= 36) ? Math.round(fs * 1.26)
140
- : (fs <= 48) ? Math.round(fs * 1.24)
141
- : (fs <= 72) ? Math.round(fs * 1.22)
142
- : (fs <= 96) ? Math.round(fs * 1.20)
143
- : Math.round(fs * 1.18);
144
- el.style.lineHeight = `${lh}px`;
145
- // Синхронизируем базовый размер шрифта для дальнейших пересчётов (zoom/resize)
195
+ const curObj = (this.core?.state?.state?.objects || []).find(o => o.id === objectId);
196
+ el.style.lineHeight = `${resolveLineHeightRatio(fs, curObj?.properties)}`;
146
197
  el.dataset.baseFontSize = String(fs);
147
198
  console.log(`🔍 HtmlTextLayer: обновлен размер шрифта для ${objectId}:`, updates.fontSize);
148
199
  }
@@ -163,6 +214,8 @@ export class HtmlTextLayer {
163
214
  // На все операции зума/пэна — полное обновление
164
215
  this.eventBus.on(Events.UI.ZoomPercent, () => this.updateAll());
165
216
  this.eventBus.on(Events.Tool.PanUpdate, () => this.updateAll());
217
+ this._onViewportChanged = () => this.updateAll();
218
+ this.eventBus.on(Events.Viewport.Changed, this._onViewportChanged);
166
219
  // Обновления в реальном времени при перетаскивании/ресайзе/повороте
167
220
  this.eventBus.on(Events.Tool.DragUpdate, ({ object }) => this.updateOne(object));
168
221
  this.eventBus.on(Events.Tool.ResizeUpdate, ({ object }) => this.updateOne(object));
@@ -255,6 +308,10 @@ export class HtmlTextLayer {
255
308
  this._hoveredTextId = null;
256
309
  // Отписываемся от событий
257
310
  if (this.eventBus) {
311
+ if (this._onViewportChanged) {
312
+ this.eventBus.off(Events.Viewport.Changed, this._onViewportChanged);
313
+ this._onViewportChanged = null;
314
+ }
258
315
  if (this._onTransformStart) {
259
316
  this.eventBus.off(Events.Tool.DragStart, this._onTransformStart);
260
317
  this.eventBus.off(Events.Tool.GroupDragStart, this._onTransformStart);
@@ -283,7 +340,7 @@ export class HtmlTextLayer {
283
340
  console.log(`🔍 HtmlTextLayer: rebuildFromState, найдено объектов:`, objs.length);
284
341
 
285
342
  objs.forEach((o) => {
286
- if (o.type === 'text' || o.type === 'simple-text') {
343
+ if (o.type === 'text' || o.type === 'simple-text' || o.type === 'shape') {
287
344
  console.log(`🔍 HtmlTextLayer: создаю HTML-элемент для текстового объекта:`, o);
288
345
  this._ensureTextEl(o.id, o);
289
346
  }
@@ -294,6 +351,34 @@ export class HtmlTextLayer {
294
351
  _ensureTextEl(objectId, objectData) {
295
352
  if (!this.layer || !objectId) return;
296
353
  if (this.idToEl.has(objectId)) return;
354
+
355
+ // Shape: flex-оверлей по центру bounds фигуры; pointer-events: none — клики проходят к PIXI
356
+ if (objectData.type === 'shape') {
357
+ const el = document.createElement('div');
358
+ el.className = 'mb-text mb-shape-text';
359
+ el.dataset.id = objectId;
360
+ const textProps = objectData.properties?.text || {};
361
+ const shapeFontSize = textProps.fontSize || 16;
362
+ el.style.fontFamily = textProps.fontFamily || 'Inter, sans-serif';
363
+ el.style.fontSize = `${shapeFontSize}px`;
364
+ el.style.color = textProps.color || '#111111';
365
+ el.style.display = 'flex';
366
+ el.style.alignItems = 'center';
367
+ el.style.justifyContent = 'center';
368
+ el.style.textAlign = 'center';
369
+ el.style.overflow = 'hidden';
370
+ el.style.pointerEvents = 'none';
371
+ el.style.whiteSpace = 'pre-wrap';
372
+ el.style.wordBreak = 'break-word';
373
+ el.style.lineHeight = '1.4';
374
+ el.dataset.baseFontSize = String(shapeFontSize);
375
+ const content = objectData.properties?.content || '';
376
+ el.textContent = content;
377
+ this.layer.appendChild(el);
378
+ this.idToEl.set(objectId, el);
379
+ this._hoverStates.set(objectId, { ty: 0, sc: 1 });
380
+ return;
381
+ }
297
382
 
298
383
  console.log(`🔍 HtmlTextLayer: создаю HTML-элемент для текста ${objectId}:`, objectData);
299
384
 
@@ -301,37 +386,37 @@ export class HtmlTextLayer {
301
386
  el.className = 'mb-text';
302
387
  el.dataset.id = objectId;
303
388
  // Получаем свойства из properties объекта
304
- const fontFamily = objectData.properties?.fontFamily || objectData.fontFamily || 'Caveat, Arial, cursive';
305
- const color = objectData.color || objectData.properties?.color || objectData.properties?.textColor || '#000000';
306
- const backgroundColor = objectData.backgroundColor || objectData.properties?.backgroundColor || 'transparent';
307
-
308
- // Базовый line-height исходя из стартового размера шрифта
309
- const baseFs = objectData.fontSize || objectData.properties?.fontSize || 32;
310
- const baseLineHeight = (() => {
311
- const fs = baseFs;
312
- if (fs <= 12) return Math.round(fs * 1.40);
313
- if (fs <= 18) return Math.round(fs * 1.34);
314
- if (fs <= 36) return Math.round(fs * 1.26);
315
- if (fs <= 48) return Math.round(fs * 1.24);
316
- if (fs <= 72) return Math.round(fs * 1.22);
317
- if (fs <= 96) return Math.round(fs * 1.20);
318
- return Math.round(fs * 1.18);
319
- })();
320
-
321
- el.classList.add('mb-text');
389
+ const props = objectData.properties || {};
390
+ const fontFamily = props.fontFamily || objectData.fontFamily || 'Caveat, Arial, cursive';
391
+ const color = objectData.color || props.color || props.textColor || '#000000';
392
+ const backgroundColor = objectData.backgroundColor || props.backgroundColor || 'transparent';
393
+
394
+ // Безразмерный множитель line-height: браузер сам считает интервал относительно
395
+ // font-size, поэтому соотношение строк не зависит от зума и не страдает от округления.
396
+ const baseFs = objectData.fontSize || props.fontSize || 32;
397
+ const baseLineHeight = resolveLineHeightRatio(baseFs, props);
398
+
399
+ const content = objectData.content || objectData.properties?.content || '';
400
+ const isMarkdown = resolveMarkdown(objectData.properties, content);
401
+ if (isMarkdown) el.classList.add('mb-text--md');
322
402
  el.style.color = color;
323
403
  el.style.fontFamily = fontFamily;
324
404
  el.style.backgroundColor = backgroundColor === 'transparent' ? '' : backgroundColor;
325
- el.style.lineHeight = `${baseLineHeight}px`;
326
- // Выравнивание рендеринга с textarea
327
- el.style.whiteSpace = 'pre';
405
+ el.style.lineHeight = `${baseLineHeight}`;
406
+ el.style.whiteSpace = isMarkdown ? 'normal' : 'pre';
328
407
  el.style.overflow = 'visible';
408
+ if (isMarkdown) el.style.overflowWrap = 'break-word';
329
409
  el.style.letterSpacing = '0px';
330
410
  el.style.fontKerning = 'normal';
331
411
  el.style.textRendering = 'optimizeLegibility';
332
- el.style.padding = '0'; // без внутренних отступов
333
- const content = objectData.content || objectData.properties?.content || '';
334
- el.textContent = content;
412
+ if (!isMarkdown) el.style.padding = '0';
413
+ // Начертания и выравнивание из properties
414
+ el.style.fontWeight = props.bold ? 'bold' : '';
415
+ el.style.fontStyle = props.italic ? 'italic' : '';
416
+ const initDec = [props.underline && 'underline', props.strikethrough && 'line-through'].filter(Boolean).join(' ');
417
+ el.style.textDecoration = initDec || '';
418
+ el.style.textAlign = props.textAlign || '';
419
+ this._syncElementContent(el, content, isMarkdown);
335
420
  // Базовые размеры сохраняем в dataset
336
421
  const fs = objectData.fontSize || objectData.properties?.fontSize || 32;
337
422
  const bw = Math.max(1, objectData.width || objectData.properties?.baseW || 160);
@@ -369,8 +454,14 @@ export class HtmlTextLayer {
369
454
  const el = this.idToEl.get(objectId);
370
455
  if (!el || !this.core) return;
371
456
 
372
- // Лениво вешаем PIXI pointer-слушатели на хит-rect текста (он уже создан к моменту updateOne)
373
- this._ensurePixiHover(objectId);
457
+ // obj нужен раньше, чтобы охранять hover-lift и shape-специфичные пути
458
+ const obj = (this.core.state.state.objects || []).find(o => o.id === objectId);
459
+ if (!obj) return;
460
+
461
+ // Hover-lift только для text/simple-text (shape использует собственный PIXI-hover)
462
+ if (obj.type !== 'shape') {
463
+ this._ensurePixiHover(objectId);
464
+ }
374
465
 
375
466
  console.log(`🔍 HtmlTextLayer: обновляю позицию для текста ${objectId}`);
376
467
 
@@ -379,8 +470,6 @@ export class HtmlTextLayer {
379
470
  const tx = world?.x || 0;
380
471
  const ty = world?.y || 0;
381
472
  const res = (this.core?.pixi?.app?.renderer?.resolution) || 1;
382
- const obj = (this.core.state.state.objects || []).find(o => o.id === objectId);
383
- if (!obj) return;
384
473
  const x = obj.position?.x || 0;
385
474
  const y = obj.position?.y || 0;
386
475
  const w = obj.width || 0;
@@ -397,25 +486,16 @@ export class HtmlTextLayer {
397
486
  const baseH = parseFloat(el.dataset.baseH || '36') || 36;
398
487
  const scaleX = w && baseW ? (w / baseW) : 1;
399
488
  const scaleY = h && baseH ? (h / baseH) : 1;
400
- // Для записок также не масштабируем шрифт от размера блока — редактор совпадает точно
401
- const sObj = (obj?.type === 'text' || obj?.type === 'simple-text' || obj?.type === 'note')
489
+ // Для text/note/shape не масштабируем шрифт от размера блока — только зум
490
+ const sObj = (obj?.type === 'text' || obj?.type === 'simple-text' || obj?.type === 'note' || obj?.type === 'shape')
402
491
  ? 1
403
492
  : Math.min(scaleX, scaleY);
404
493
  const sCss = s / res;
405
494
  const fontSizePx = Math.max(1, baseFS * sObj * sCss);
406
495
  el.style.fontSize = `${fontSizePx}px`;
407
- // Адаптивный межстрочный интервал по размеру шрифта
408
- const computeLineHeightPx = (fs) => {
409
- if (fs <= 12) return Math.round(fs * 1.40);
410
- if (fs <= 18) return Math.round(fs * 1.34);
411
- if (fs <= 36) return Math.round(fs * 1.26);
412
- if (fs <= 48) return Math.round(fs * 1.24);
413
- if (fs <= 72) return Math.round(fs * 1.22);
414
- if (fs <= 96) return Math.round(fs * 1.20);
415
- return Math.round(fs * 1.18);
416
- };
417
- // Применяем новый line-height только если он отличается от текущего, чтобы избежать конфликтов CSS
418
- const newLH = `${computeLineHeightPx(fontSizePx)}px`;
496
+ // Безразмерный множитель: интервал между строками масштабируется браузером
497
+ // пропорционально font-size, поэтому не зависит от зума и не страдает от округления.
498
+ const newLH = `${resolveLineHeightRatio(baseFS, obj.properties)}`;
419
499
  if (el.style.lineHeight !== newLH) {
420
500
  el.style.lineHeight = newLH;
421
501
  }
@@ -489,21 +569,82 @@ export class HtmlTextLayer {
489
569
  const rotatePart = angle ? `rotate(${angle}deg)` : '';
490
570
  el.style.transformOrigin = 'center center';
491
571
  el.style.transform = [hoverPart, rotatePart].filter(Boolean).join(' ');
492
- // Текст
572
+ // Shape: обновляем шрифт/цвет из properties.text и baseFontSize, остальное — без изменений
573
+ if (obj.type === 'shape') {
574
+ const textProps = obj.properties?.text || {};
575
+ const newShapeFS = textProps.fontSize || 16;
576
+ if (el.dataset.baseFontSize !== String(newShapeFS)) {
577
+ el.dataset.baseFontSize = String(newShapeFS);
578
+ }
579
+ el.style.fontFamily = textProps.fontFamily || 'Inter, sans-serif';
580
+ el.style.color = textProps.color || '#111111';
581
+ el.style.fontWeight = textProps.bold ? 'bold' : '';
582
+ el.style.fontStyle = textProps.italic ? 'italic' : '';
583
+ const shapeContent = obj.properties?.content || '';
584
+ if (el.textContent !== shapeContent) el.textContent = shapeContent;
585
+ return;
586
+ }
587
+
588
+ // Начертания и выравнивание из properties
589
+ const props = obj.properties || {};
590
+ el.style.fontWeight = props.bold ? 'bold' : '';
591
+ el.style.fontStyle = props.italic ? 'italic' : '';
592
+ const textDec = [props.underline && 'underline', props.strikethrough && 'line-through'].filter(Boolean).join(' ');
593
+ el.style.textDecoration = textDec || '';
594
+ el.style.textAlign = props.textAlign || '';
595
+
596
+ // Текст: список или plain/markdown
493
597
  const content = obj.content || obj.properties?.content;
598
+ const listType = props.listType;
599
+ const useList = listType && listType !== 'none';
600
+ const isMarkdown = !useList && resolveMarkdown(obj.properties, content);
494
601
  if (typeof content === 'string') {
495
- el.textContent = content;
496
- console.log(`🔍 HtmlTextLayer: содержимое обновлено в updateOne для ${objectId}:`, content);
602
+ if (useList) {
603
+ el.dataset.renderedContent = '';
604
+ el.dataset.renderedMd = '';
605
+ const listChecked = props.listChecked || [];
606
+ const listKey = `${listType}:${content}:${JSON.stringify(listChecked)}`;
607
+ if (el.dataset.renderedList !== listKey) {
608
+ const onToggle = (lineIndex) => {
609
+ const cur = (this.core?.state?.state?.objects || []).find(o => o.id === objectId);
610
+ const curChecked = cur?.properties?.listChecked || [];
611
+ const next = [...curChecked];
612
+ next[lineIndex] = !next[lineIndex];
613
+ this.eventBus.emit(Events.Object.StateChanged, {
614
+ objectId, updates: { properties: { listChecked: next } }
615
+ });
616
+ };
617
+ renderTextList(el, content, listType, listChecked, onToggle);
618
+ el.dataset.renderedList = listKey;
619
+ }
620
+ el.classList.remove('mb-text--md');
621
+ el.style.whiteSpace = 'normal';
622
+ el.style.overflowWrap = 'break-word';
623
+ el.style.padding = '';
624
+ } else {
625
+ el.dataset.renderedList = '';
626
+ const contentChanged = this._syncElementContent(el, content, isMarkdown);
627
+ if (contentChanged) {
628
+ console.log(`🔍 HtmlTextLayer: содержимое обновлено в updateOne для ${objectId}:`, content);
629
+ }
630
+ const hasMdClass = el.classList.contains('mb-text--md');
631
+ if (hasMdClass !== isMarkdown) {
632
+ el.classList.toggle('mb-text--md', isMarkdown);
633
+ el.style.whiteSpace = isMarkdown ? 'normal' : 'pre';
634
+ el.style.overflowWrap = isMarkdown ? 'break-word' : '';
635
+ if (!isMarkdown) el.style.padding = '0';
636
+ }
637
+ }
497
638
  }
498
639
 
499
640
  // Гарантируем, что рамка не прилипает к тексту справа: ширина блока всегда
500
641
  // не меньше реальной ширины текста + правый отступ. Слой ручек строит рамку
501
642
  // по getBoundingClientRect этого .mb-text, поэтому запас распространяется и на неё.
502
- // Применяем ко всем объектам т.ч. старым, сохранённым без запаса) и независимо
503
- // от шрифта. Без поворота: width:auto у absolute-элемента = ширина контента.
643
+ // Для markdown-элементов блок не применяется: перенос слов управляется CSS,
644
+ // и width:auto сломал бы wrapping.
504
645
  try {
505
646
  const hasContent = !!(el.textContent && el.textContent.trim());
506
- if (hasContent && !angle) {
647
+ if (hasContent && !angle && !isMarkdown && !useList && !(props.textAlign && props.textAlign !== 'left')) {
507
648
  const rightMargin = Math.ceil(fontSizePx * 0.7) + 6;
508
649
  const prevWidth = el.style.width;
509
650
  el.style.width = 'auto';
@@ -539,6 +680,42 @@ export class HtmlTextLayer {
539
680
  });
540
681
  }
541
682
 
683
+ /** Обновляет innerHTML/textContent только при реальной смене content или флага markdown */
684
+ _syncElementContent(el, content, isMarkdown) {
685
+ if (typeof content !== 'string') return false;
686
+ const mdFlag = isMarkdown ? '1' : '0';
687
+ if (el.dataset.renderedContent === content && el.dataset.renderedMd === mdFlag) return false;
688
+ if (isMarkdown) {
689
+ el.innerHTML = renderRichText(content);
690
+ } else {
691
+ el.textContent = content;
692
+ }
693
+ el.dataset.renderedContent = content;
694
+ el.dataset.renderedMd = mdFlag;
695
+ return true;
696
+ }
697
+
698
+ /** Только hover-transform + поворот, без пересчёта позиции/размеров/контента */
699
+ _applyHoverTransform(objectId) {
700
+ const el = this.idToEl.get(objectId);
701
+ if (!el || !this.core) return;
702
+ const obj = (this.core.state.state.objects || []).find(o => o.id === objectId);
703
+ if (!obj) return;
704
+ const pixiObj = this.core?.pixi?.objects?.get ? this.core.pixi.objects.get(objectId) : null;
705
+ const angle = (pixiObj && typeof pixiObj.rotation === 'number')
706
+ ? (pixiObj.rotation * 180 / Math.PI)
707
+ : (obj.rotation || obj.transform?.rotation || 0);
708
+ const hover = this._hoverStates.get(objectId);
709
+ const hoverTy = hover?.ty ?? 0;
710
+ const hoverSc = hover?.sc ?? 1;
711
+ const hoverPart = (Math.abs(hoverTy) > 0.001 || Math.abs(hoverSc - 1) > 0.001)
712
+ ? `translate3d(0, ${hoverTy}px, 0) scale(${hoverSc})`
713
+ : '';
714
+ const rotatePart = angle ? `rotate(${angle}deg)` : '';
715
+ el.style.transformOrigin = 'center center';
716
+ el.style.transform = [hoverPart, rotatePart].filter(Boolean).join(' ');
717
+ }
718
+
542
719
  /** Лениво вешает pointerover/pointerout на PIXI хит-rect текста */
543
720
  _ensurePixiHover(objectId) {
544
721
  if (this._pixiHoverHandlers.has(objectId)) return;
@@ -586,8 +763,8 @@ export class HtmlTextLayer {
586
763
  sc: TEXT_HOVER_SC,
587
764
  duration: TEXT_HOVER_DUR,
588
765
  ease: TEXT_HOVER_EASE,
589
- onUpdate: () => this.updateOne(objectId),
590
- onComplete: () => this.updateOne(objectId),
766
+ onUpdate: () => this._applyHoverTransform(objectId),
767
+ onComplete: () => this._applyHoverTransform(objectId),
591
768
  });
592
769
  }
593
770
 
@@ -601,8 +778,8 @@ export class HtmlTextLayer {
601
778
  sc: 1,
602
779
  duration: TEXT_BACK_DUR,
603
780
  ease: 'power2.out',
604
- onUpdate: () => this.updateOne(objectId),
605
- onComplete: () => this.updateOne(objectId),
781
+ onUpdate: () => this._applyHoverTransform(objectId),
782
+ onComplete: () => this._applyHoverTransform(objectId),
606
783
  });
607
784
  }
608
785
 
@@ -610,6 +787,10 @@ export class HtmlTextLayer {
610
787
  const el = this.idToEl.get(objectId);
611
788
  if (!el || !this.core) return;
612
789
  try {
790
+ // Фигуры имеют фиксированные пользовательские bounds: текст центрируется внутри,
791
+ // а форма (квадрат остаётся квадратом) не подгоняется под высоту текста.
792
+ const obj = (this.core.state.state.objects || []).find(o => o.id === objectId);
793
+ if (obj?.type === 'shape') return;
613
794
  // Измеряем фактическую высоту HTML-текста
614
795
  el.style.height = 'auto';
615
796
  const measured = Math.max(1, Math.round(el.scrollHeight));
@@ -620,7 +801,6 @@ export class HtmlTextLayer {
620
801
  const res = (this.core?.pixi?.app?.renderer?.resolution) || 1;
621
802
  const worldH = (measured * res) / s;
622
803
  // Узнаём текущую ширину в мире
623
- const obj = (this.core.state.state.objects || []).find(o => o.id === objectId);
624
804
  const worldW = obj?.width || 0;
625
805
  const position = obj?.position || null;
626
806
  if (worldW > 0 && position) {
@@ -45,6 +45,8 @@ export class NotePropertiesPanel {
45
45
  this.eventBus.on(Events.Tool.PanUpdate, () => {
46
46
  if (this.currentId) this.reposition();
47
47
  });
48
+ this._onViewportChanged = () => { if (this.currentId) this.reposition(); };
49
+ this.eventBus.on(Events.Viewport.Changed, this._onViewportChanged);
48
50
 
49
51
  // Скрываем панель при активации других инструментов
50
52
  this.eventBus.on(Events.Tool.Activated, ({ tool }) => {
@@ -596,6 +598,10 @@ export class NotePropertiesPanel {
596
598
  }
597
599
 
598
600
  destroy() {
601
+ if (this._onViewportChanged && this.eventBus?.off) {
602
+ this.eventBus.off(Events.Viewport.Changed, this._onViewportChanged);
603
+ this._onViewportChanged = null;
604
+ }
599
605
  if (this._onStateChanged && this.eventBus?.off) {
600
606
  this.eventBus.off(Events.Object.StateChanged, this._onStateChanged);
601
607
  this._onStateChanged = null;