@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.
- package/package.json +5 -1
- package/src/assets/fonts/inter/inter-cyrillic-400-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-cyrillic-500-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/src/assets/icons/attachments.svg +3 -1
- package/src/assets/icons/comments.svg +2 -2
- package/src/assets/icons/connector.svg +6 -0
- package/src/assets/icons/emoji.svg +6 -1
- package/src/assets/icons/frame.svg +4 -1
- package/src/assets/icons/image.svg +5 -1
- package/src/assets/icons/laser.svg +1 -0
- package/src/assets/icons/lasso.svg +5 -0
- package/src/assets/icons/mindmap.svg +10 -2
- package/src/assets/icons/note.svg +4 -1
- package/src/assets/icons/pan.svg +5 -2
- package/src/assets/icons/pencil.svg +4 -1
- package/src/assets/icons/reactions.svg +5 -0
- package/src/assets/icons/redo.svg +3 -2
- package/src/assets/icons/select.svg +2 -8
- package/src/assets/icons/shapes.svg +5 -1
- package/src/assets/icons/text-add.svg +15 -1
- package/src/assets/icons/undo.svg +3 -2
- package/src/assets/reactions/1f44d.svg +20 -0
- package/src/assets/reactions/1f44e.svg +20 -0
- package/src/assets/reactions/2705.svg +20 -0
- package/src/assets/reactions/274c.svg +19 -0
- package/src/assets/reactions/2753.svg +20 -0
- package/src/assets/reactions/2764.svg +22 -0
- package/src/assets/reactions/2b50.svg +19 -0
- package/src/assets/reactions/plus-one.svg +25 -0
- package/src/core/PixiEngine.js +23 -0
- package/src/core/bootstrap/CoreInitializer.js +43 -0
- package/src/core/commands/GroupDeleteCommand.js +13 -1
- package/src/core/commands/UpdateShapeStyleCommand.js +121 -0
- package/src/core/commands/UpdateTextStyleCommand.js +17 -6
- package/src/core/commands/index.js +3 -0
- package/src/core/events/Events.js +22 -0
- package/src/core/flows/LayerAndViewportFlow.js +1 -0
- package/src/core/flows/ObjectLifecycleFlow.js +155 -7
- package/src/core/index.js +28 -1
- package/src/grid/CrossGridZoomPhases.js +3 -3
- package/src/initNoBundler.js +1 -1
- package/src/moodboard/DataManager.js +28 -0
- package/src/moodboard/MoodBoard.js +27 -0
- package/src/moodboard/bootstrap/MoodBoardInitializer.js +69 -1
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +22 -4
- package/src/moodboard/integration/MoodBoardEventBindings.js +5 -1
- package/src/moodboard/integration/MoodBoardLoadApi.js +10 -1
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +9 -0
- package/src/objects/ConnectorObject.js +2 -2
- package/src/objects/FrameObject.js +119 -59
- package/src/objects/ShapeObject.js +49 -74
- package/src/objects/shape/ShapeDrawer.js +210 -0
- package/src/services/ConnectorBindingResolver.js +112 -0
- package/src/services/ConnectorRouter.js +210 -0
- package/src/services/comments/CommentService.js +344 -0
- package/src/tools/object-tools/CommentTool.js +85 -0
- package/src/tools/object-tools/DrawingTool.js +110 -10
- package/src/tools/object-tools/LaserPointerTool.js +121 -0
- package/src/tools/object-tools/SelectTool.js +25 -1
- package/src/tools/object-tools/TextTool.js +6 -1
- package/src/tools/object-tools/connector/ConnectorDragController.js +50 -3
- package/src/tools/object-tools/connector/connectorGesture.js +33 -19
- package/src/tools/object-tools/placement/PlacementInputRouter.js +22 -1
- package/src/tools/object-tools/selection/BoxSelectController.js +24 -2
- package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +139 -0
- package/src/tools/object-tools/selection/InlineEditorController.js +12 -0
- package/src/tools/object-tools/selection/InlineEditorDomFactory.js +36 -0
- package/src/tools/object-tools/selection/LassoSelectController.js +125 -0
- package/src/tools/object-tools/selection/MindmapInlineEditorController.js +1 -0
- package/src/tools/object-tools/selection/SelectInputRouter.js +64 -5
- package/src/tools/object-tools/selection/SelectToolLifecycleController.js +11 -1
- package/src/tools/object-tools/selection/SelectToolSetup.js +13 -1
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +46 -12
- package/src/tools/object-tools/selection/TextEditorSyncService.js +1 -0
- package/src/tools/object-tools/selection/TextInlineEditorController.js +65 -6
- package/src/ui/CommentPopover.js +6 -0
- package/src/ui/CommentsBar.js +91 -0
- package/src/ui/ConnectorPropertiesPanel.js +150 -0
- package/src/ui/ContextMenu.js +25 -0
- package/src/ui/DrawingPropertiesPanel.js +362 -0
- package/src/ui/FilePropertiesPanel.js +5 -0
- package/src/ui/FramePropertiesPanel.js +5 -0
- package/src/ui/HtmlTextLayer.js +246 -66
- package/src/ui/NotePropertiesPanel.js +6 -0
- package/src/ui/ShapePropertiesPanel.js +307 -0
- package/src/ui/TextPropertiesPanel.js +100 -1
- package/src/ui/Toolbar.js +25 -2
- package/src/ui/Topbar.js +2 -2
- package/src/ui/animation/HoverLiftController.js +6 -7
- package/src/ui/chat/ChatComposer.js +59 -12
- package/src/ui/chat/ChatExtendedPromptModal.js +1 -12
- package/src/ui/chat/ChatWindow.js +60 -144
- package/src/ui/chat/ChatWindowRenderer.js +1 -8
- package/src/ui/chat/icons.js +0 -4
- package/src/ui/comments/CommentListPanel.js +213 -0
- package/src/ui/comments/CommentPinLayer.js +448 -0
- package/src/ui/comments/CommentThreadPopover.js +539 -0
- package/src/ui/comments/commentFormat.js +32 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelBindings.js +223 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelEventBridge.js +114 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelMapper.js +144 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelRenderer.js +447 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelState.js +61 -0
- package/src/ui/connectors/ConnectionAnchorsLayer.js +1 -0
- package/src/ui/connectors/ConnectorHandlesLayer.js +321 -0
- package/src/ui/connectors/ConnectorLabelLayer.js +334 -0
- package/src/ui/connectors/ConnectorLayer.js +264 -57
- package/src/ui/handles/HandlesDomRenderer.js +5 -13
- package/src/ui/handles/HandlesEventBridge.js +1 -0
- package/src/ui/handles/SingleSelectionHandlesController.js +4 -0
- package/src/ui/mindmap/MindmapCollapseLayer.js +1 -0
- package/src/ui/mindmap/MindmapConnectionLayer.js +1 -0
- package/src/ui/mindmap/MindmapHtmlTextLayer.js +6 -0
- package/src/ui/shape-properties/ShapePropertiesPanelDom.js +533 -0
- package/src/ui/shape-properties/ShapePropertiesPanelSync.js +132 -0
- package/src/ui/styles/chat.css +682 -28
- package/src/ui/styles/index.css +1 -0
- package/src/ui/styles/panels.css +112 -2
- package/src/ui/styles/shape-properties-panel.css +250 -0
- package/src/ui/styles/toolbar.css +7 -2
- package/src/ui/styles/topbar.css +1 -1
- package/src/ui/styles/workspace.css +257 -6
- package/src/ui/text-properties/TextFormatControls.js +88 -0
- package/src/ui/text-properties/TextListRenderer.js +137 -0
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +27 -0
- package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +3 -1
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +56 -0
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +24 -0
- package/src/ui/text-properties/TextPropertiesPanelState.js +8 -0
- package/src/ui/toolbar/ReactionsPopupController.js +88 -0
- package/src/ui/toolbar/ToolbarActionRouter.js +71 -5
- package/src/ui/toolbar/ToolbarPopupsController.js +120 -118
- package/src/ui/toolbar/ToolbarRenderer.js +9 -1
- package/src/ui/toolbar/ToolbarStateController.js +4 -1
- package/src/utils/iconLoader.js +17 -16
- package/src/utils/markdown.js +14 -0
- package/src/utils/richText.js +125 -0
package/src/ui/HtmlTextLayer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
138
|
-
|
|
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
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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}
|
|
326
|
-
|
|
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
|
-
|
|
334
|
-
el.
|
|
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
|
-
//
|
|
373
|
-
this.
|
|
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
|
-
|
|
409
|
-
|
|
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
|
-
|
|
496
|
-
|
|
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
|
-
//
|
|
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.
|
|
590
|
-
onComplete: () => this.
|
|
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.
|
|
605
|
-
onComplete: () => this.
|
|
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;
|