@sequent-org/moodboard 1.2.119 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -1
- package/src/assets/icons/rotate-icon.svg +1 -1
- package/src/core/HistoryManager.js +16 -16
- package/src/core/KeyboardManager.js +48 -539
- package/src/core/PixiEngine.js +9 -9
- package/src/core/SaveManager.js +56 -31
- package/src/core/bootstrap/CoreInitializer.js +65 -0
- package/src/core/commands/DeleteObjectCommand.js +8 -0
- package/src/core/commands/GroupDeleteCommand.js +75 -0
- package/src/core/commands/GroupRotateCommand.js +6 -0
- package/src/core/commands/UpdateContentCommand.js +52 -0
- package/src/core/commands/UpdateFramePropertiesCommand.js +98 -0
- package/src/core/commands/UpdateFrameTypeCommand.js +85 -0
- package/src/core/commands/UpdateNoteStyleCommand.js +88 -0
- package/src/core/commands/UpdateTextStyleCommand.js +90 -0
- package/src/core/commands/index.js +6 -0
- package/src/core/events/Events.js +6 -0
- package/src/core/flows/ClipboardFlow.js +553 -0
- package/src/core/flows/LayerAndViewportFlow.js +283 -0
- package/src/core/flows/ObjectLifecycleFlow.js +336 -0
- package/src/core/flows/SaveFlow.js +34 -0
- package/src/core/flows/TransformFlow.js +277 -0
- package/src/core/flows/TransformFlowResizeHelpers.js +83 -0
- package/src/core/index.js +41 -1773
- package/src/core/keyboard/KeyboardClipboardImagePaste.js +190 -0
- package/src/core/keyboard/KeyboardContextGuards.js +35 -0
- package/src/core/keyboard/KeyboardEventRouter.js +92 -0
- package/src/core/keyboard/KeyboardSelectionActions.js +103 -0
- package/src/core/keyboard/KeyboardShortcutMap.js +31 -0
- package/src/core/keyboard/KeyboardToolSwitching.js +26 -0
- package/src/core/rendering/ObjectRenderer.js +3 -7
- package/src/grid/BaseGrid.js +26 -0
- package/src/grid/CrossGrid.js +7 -6
- package/src/grid/DotGrid.js +89 -33
- package/src/grid/DotGridZoomPhases.js +42 -0
- package/src/grid/LineGrid.js +22 -21
- package/src/moodboard/MoodBoard.js +31 -532
- package/src/moodboard/bootstrap/MoodBoardInitializer.js +47 -0
- package/src/moodboard/bootstrap/MoodBoardManagersFactory.js +38 -0
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +109 -0
- package/src/moodboard/integration/MoodBoardEventBindings.js +65 -0
- package/src/moodboard/integration/MoodBoardLoadApi.js +82 -0
- package/src/moodboard/integration/MoodBoardScreenshotApi.js +33 -0
- package/src/moodboard/integration/MoodBoardScreenshotCanvas.js +98 -0
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +97 -0
- package/src/objects/FileObject.js +17 -6
- package/src/objects/FrameObject.js +50 -10
- package/src/objects/NoteObject.js +5 -4
- package/src/services/BoardService.js +42 -2
- package/src/services/FrameService.js +83 -42
- package/src/services/ResizePolicyService.js +152 -0
- package/src/services/SettingsApplier.js +7 -2
- package/src/services/ZoomPanController.js +35 -9
- package/src/tools/ToolManager.js +30 -537
- package/src/tools/board-tools/PanTool.js +5 -11
- package/src/tools/manager/ToolActivationController.js +49 -0
- package/src/tools/manager/ToolEventRouter.js +396 -0
- package/src/tools/manager/ToolManagerGuards.js +33 -0
- package/src/tools/manager/ToolManagerLifecycle.js +110 -0
- package/src/tools/manager/ToolRegistry.js +33 -0
- package/src/tools/object-tools/DrawingTool.js +48 -14
- package/src/tools/object-tools/PlacementTool.js +50 -1049
- package/src/tools/object-tools/PlacementToolV2.js +88 -0
- package/src/tools/object-tools/SelectTool.js +174 -2681
- package/src/tools/object-tools/placement/GhostController.js +504 -0
- package/src/tools/object-tools/placement/PlacementCoordinateResolver.js +20 -0
- package/src/tools/object-tools/placement/PlacementEventsBridge.js +91 -0
- package/src/tools/object-tools/placement/PlacementInputRouter.js +267 -0
- package/src/tools/object-tools/placement/PlacementPayloadFactory.js +111 -0
- package/src/tools/object-tools/placement/PlacementSessionStore.js +18 -0
- package/src/tools/object-tools/selection/BoxSelectController.js +0 -5
- package/src/tools/object-tools/selection/CloneFlowController.js +71 -0
- package/src/tools/object-tools/selection/CoordinateMapper.js +10 -0
- package/src/tools/object-tools/selection/CursorController.js +78 -0
- package/src/tools/object-tools/selection/FileNameInlineEditorController.js +184 -0
- package/src/tools/object-tools/selection/HitTestService.js +102 -0
- package/src/tools/object-tools/selection/InlineEditorController.js +24 -0
- package/src/tools/object-tools/selection/InlineEditorDomFactory.js +50 -0
- package/src/tools/object-tools/selection/InlineEditorListenersRegistry.js +14 -0
- package/src/tools/object-tools/selection/InlineEditorPositioningService.js +25 -0
- package/src/tools/object-tools/selection/NoteInlineEditorController.js +113 -0
- package/src/tools/object-tools/selection/SelectInputRouter.js +267 -0
- package/src/tools/object-tools/selection/SelectToolLifecycleController.js +128 -0
- package/src/tools/object-tools/selection/SelectToolSetup.js +134 -0
- package/src/tools/object-tools/selection/SelectionOverlayService.js +81 -0
- package/src/tools/object-tools/selection/SelectionStateController.js +91 -0
- package/src/tools/object-tools/selection/TextEditorDomFactory.js +65 -0
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +266 -0
- package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +90 -0
- package/src/tools/object-tools/selection/TextEditorPositioningService.js +158 -0
- package/src/tools/object-tools/selection/TextEditorSyncService.js +110 -0
- package/src/tools/object-tools/selection/TextInlineEditorController.js +457 -0
- package/src/tools/object-tools/selection/TransformInteractionController.js +466 -0
- package/src/ui/FilePropertiesPanel.js +61 -32
- package/src/ui/FramePropertiesPanel.js +176 -101
- package/src/ui/HtmlHandlesLayer.js +121 -999
- package/src/ui/MapPanel.js +12 -7
- package/src/ui/NotePropertiesPanel.js +17 -2
- package/src/ui/TextPropertiesPanel.js +124 -738
- package/src/ui/Toolbar.js +71 -1180
- package/src/ui/Topbar.js +23 -25
- package/src/ui/ZoomPanel.js +16 -5
- package/src/ui/handles/GroupSelectionHandlesController.js +29 -0
- package/src/ui/handles/HandlesDomRenderer.js +278 -0
- package/src/ui/handles/HandlesEventBridge.js +102 -0
- package/src/ui/handles/HandlesInteractionController.js +772 -0
- package/src/ui/handles/HandlesPositioningService.js +206 -0
- package/src/ui/handles/SingleSelectionHandlesController.js +22 -0
- package/src/ui/styles/toolbar.css +2 -0
- package/src/ui/styles/workspace.css +13 -6
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +92 -0
- package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +77 -0
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +173 -0
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +434 -0
- package/src/ui/text-properties/TextPropertiesPanelState.js +39 -0
- package/src/ui/toolbar/ToolbarActionRouter.js +193 -0
- package/src/ui/toolbar/ToolbarDialogsController.js +186 -0
- package/src/ui/toolbar/ToolbarPopupsController.js +662 -0
- package/src/ui/toolbar/ToolbarRenderer.js +97 -0
- package/src/ui/toolbar/ToolbarStateController.js +79 -0
- package/src/ui/toolbar/ToolbarTooltipController.js +52 -0
- package/src/utils/emojiLoaderNoBundler.js +1 -1
|
@@ -26,7 +26,6 @@ export class BoardService {
|
|
|
26
26
|
this.grid?.setEnabled(false);
|
|
27
27
|
this.grid?.updateVisual();
|
|
28
28
|
this.pixi.setGrid(this.grid);
|
|
29
|
-
// Обновляем сохранённые данные
|
|
30
29
|
try {
|
|
31
30
|
this.eventBus.emit(Events.Grid.BoardDataChanged, {
|
|
32
31
|
grid: { type: 'off', options: this.grid?.serialize ? this.grid.serialize() : {} }
|
|
@@ -34,6 +33,7 @@ export class BoardService {
|
|
|
34
33
|
} catch (_) {}
|
|
35
34
|
return;
|
|
36
35
|
}
|
|
36
|
+
this.grid?.destroy?.();
|
|
37
37
|
const gridOptions = {
|
|
38
38
|
...GridFactory.getDefaultOptions(type),
|
|
39
39
|
enabled: true,
|
|
@@ -44,8 +44,8 @@ export class BoardService {
|
|
|
44
44
|
};
|
|
45
45
|
try {
|
|
46
46
|
this.grid = GridFactory.createGrid(type, gridOptions);
|
|
47
|
-
this.grid.updateVisual();
|
|
48
47
|
this.pixi.setGrid(this.grid);
|
|
48
|
+
this.refreshGridViewport();
|
|
49
49
|
this.eventBus.emit(Events.UI.GridCurrent, { type });
|
|
50
50
|
// Сообщаем об обновлении данных сетки для сохранения в boardData
|
|
51
51
|
try {
|
|
@@ -67,6 +67,7 @@ export class BoardService {
|
|
|
67
67
|
req.view = { width: viewEl.clientWidth, height: viewEl.clientHeight };
|
|
68
68
|
// Прокидываем только метаданные объектов через ядро (сам список формирует Core)
|
|
69
69
|
});
|
|
70
|
+
this.eventBus.on(Events.Viewport.Changed, () => this.refreshGridViewport());
|
|
70
71
|
this.eventBus.on(Events.UI.MinimapCenterOn, ({ worldX, worldY }) => {
|
|
71
72
|
const world = this.pixi.worldLayer || this.pixi.app.stage;
|
|
72
73
|
const viewW = this.pixi.app.view.clientWidth;
|
|
@@ -74,6 +75,11 @@ export class BoardService {
|
|
|
74
75
|
const s = world.scale?.x || 1;
|
|
75
76
|
world.x = viewW / 2 - worldX * s;
|
|
76
77
|
world.y = viewH / 2 - worldY * s;
|
|
78
|
+
if (this.pixi.gridLayer) {
|
|
79
|
+
this.pixi.gridLayer.x = world.x;
|
|
80
|
+
this.pixi.gridLayer.y = world.y;
|
|
81
|
+
}
|
|
82
|
+
this.refreshGridViewport();
|
|
77
83
|
});
|
|
78
84
|
}
|
|
79
85
|
|
|
@@ -81,9 +87,43 @@ export class BoardService {
|
|
|
81
87
|
if (!this.grid) return;
|
|
82
88
|
const size = this._getCanvasSize?.() || { width: 800, height: 600 };
|
|
83
89
|
this.grid.resize(size.width, size.height);
|
|
90
|
+
this.grid.viewportBounds = null;
|
|
84
91
|
this.grid.updateVisual();
|
|
85
92
|
this.pixi.setGrid(this.grid);
|
|
86
93
|
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Синхронизирует gridLayer с world (позиция + масштаб) и перерисовывает сетку.
|
|
97
|
+
*/
|
|
98
|
+
refreshGridViewport() {
|
|
99
|
+
if (!this.grid?.enabled || !this.pixi?.gridLayer) return;
|
|
100
|
+
const view = this.pixi.app?.view;
|
|
101
|
+
if (!view) return;
|
|
102
|
+
const world = this.pixi.worldLayer || this.pixi.app.stage;
|
|
103
|
+
const gl = this.pixi.gridLayer;
|
|
104
|
+
const scale = world.scale?.x ?? 1;
|
|
105
|
+
|
|
106
|
+
// Синхронизация gridLayer с world — сетка зуммируется вместе с доской
|
|
107
|
+
gl.x = world.x;
|
|
108
|
+
gl.y = world.y;
|
|
109
|
+
if (gl.scale) {
|
|
110
|
+
gl.scale.set(scale);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// DotGrid: передаём zoom до setVisibleBounds (чтобы createVisual видел актуальный zoom)
|
|
114
|
+
if (this.grid.type === 'dot' && typeof this.grid.setZoom === 'function') {
|
|
115
|
+
this.grid.setZoom(scale);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Видимая область в мировых координатах (с учётом scale)
|
|
119
|
+
const gridSize = this.grid._getEffectiveSize?.() ?? this.grid.size;
|
|
120
|
+
const pad = Math.max(100, (gridSize || 20) * 4);
|
|
121
|
+
const left = (-gl.x - pad) / scale;
|
|
122
|
+
const top = (-gl.y - pad) / scale;
|
|
123
|
+
const right = (view.clientWidth - gl.x + pad) / scale;
|
|
124
|
+
const bottom = (view.clientHeight - gl.y + pad) / scale;
|
|
125
|
+
this.grid.setVisibleBounds(left, top, right, bottom);
|
|
126
|
+
}
|
|
87
127
|
}
|
|
88
128
|
|
|
89
129
|
|
|
@@ -50,8 +50,10 @@ export class FrameService {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
attach() {
|
|
53
|
-
|
|
54
|
-
this.
|
|
53
|
+
if (this._attached) return;
|
|
54
|
+
this._attached = true;
|
|
55
|
+
|
|
56
|
+
this._onObjectCreated = ({ objectId, objectData }) => {
|
|
55
57
|
try {
|
|
56
58
|
if (!objectData || objectData.type !== 'frame') return;
|
|
57
59
|
const isArbitrary = (objectData.properties && objectData.properties.lockedAspect === false)
|
|
@@ -66,14 +68,14 @@ export class FrameService {
|
|
|
66
68
|
// И оповестим общий менеджер на всякий случай
|
|
67
69
|
this.eventBus.emit(Events.Object.Reordered, { reason: 'attach_arbitrary_frame_children' });
|
|
68
70
|
} catch (_) { /* no-op */ }
|
|
69
|
-
}
|
|
71
|
+
};
|
|
72
|
+
this.eventBus.on(Events.Object.Created, this._onObjectCreated);
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
this.eventBus.on(Events.Tool.DragStart, (data) => {
|
|
74
|
+
this._onDragStart = (data) => {
|
|
73
75
|
const moved = this.state.state.objects.find(o => o.id === data.object);
|
|
74
76
|
if (moved && moved.type === 'frame') {
|
|
75
77
|
// Серый фон
|
|
76
|
-
this.pixi.setFrameFill(moved.id, moved.width, moved.height,
|
|
78
|
+
this.pixi.setFrameFill(moved.id, moved.width, moved.height, 0xFAFAFA);
|
|
77
79
|
// Cнимок стартовых позиций по центру PIXI
|
|
78
80
|
const fp = this.pixi.objects.get(moved.id);
|
|
79
81
|
this._frameDragFrameStart = { x: fp?.x || 0, y: fp?.y || 0 };
|
|
@@ -84,9 +86,10 @@ export class FrameService {
|
|
|
84
86
|
if (childPixi) this._frameDragChildStart.set(childId, { x: childPixi.x, y: childPixi.y });
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
|
-
}
|
|
89
|
+
};
|
|
90
|
+
this.eventBus.on(Events.Tool.DragStart, this._onDragStart);
|
|
88
91
|
|
|
89
|
-
this.
|
|
92
|
+
this._onDragUpdate = (data) => {
|
|
90
93
|
const moved = this.state.state.objects.find(o => o.id === data.object);
|
|
91
94
|
if (!moved) return;
|
|
92
95
|
if (moved.type === 'frame') {
|
|
@@ -125,40 +128,14 @@ export class FrameService {
|
|
|
125
128
|
});
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
|
-
// Во время перетаскивания тоже гарантируем порядок
|
|
129
|
-
this._forceFramesBelow();
|
|
130
131
|
} else {
|
|
131
|
-
// Hover-эффект:
|
|
132
|
-
|
|
133
|
-
const centerY = moved.position.y + (moved.height || 0) / 2;
|
|
134
|
-
const frames = (this.state.state.objects || []).filter(o => o.type === 'frame');
|
|
135
|
-
const ordered = frames.slice().sort((a, b) => {
|
|
136
|
-
const pa = this.pixi.objects.get(a.id);
|
|
137
|
-
const pb = this.pixi.objects.get(b.id);
|
|
138
|
-
return (pb?.zIndex || 0) - (pa?.zIndex || 0);
|
|
139
|
-
});
|
|
140
|
-
let hoverId = null;
|
|
141
|
-
for (const f of ordered) {
|
|
142
|
-
const rect = { x: f.position.x, y: f.position.y, w: f.width || 0, h: f.height || 0 };
|
|
143
|
-
if (centerX >= rect.x && centerX <= rect.x + rect.w && centerY >= rect.y && centerY <= rect.y + rect.h) {
|
|
144
|
-
hoverId = f.id; break;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
if (hoverId !== this._frameHoverId) {
|
|
148
|
-
if (this._frameHoverId) {
|
|
149
|
-
const prev = frames.find(fr => fr.id === this._frameHoverId);
|
|
150
|
-
if (prev) this.pixi.setFrameFill(prev.id, prev.width, prev.height, 0xFFFFFF);
|
|
151
|
-
}
|
|
152
|
-
if (hoverId) {
|
|
153
|
-
const cur = frames.find(fr => fr.id === hoverId);
|
|
154
|
-
if (cur) this.pixi.setFrameFill(cur.id, cur.width, cur.height, 0xEEEEEE);
|
|
155
|
-
}
|
|
156
|
-
this._frameHoverId = hoverId || null;
|
|
157
|
-
}
|
|
132
|
+
// Hover-эффект: throttle, чтобы не вызывать setFrameFill на каждый кадр
|
|
133
|
+
this._applyHoverThrottled(data.object);
|
|
158
134
|
}
|
|
159
|
-
}
|
|
135
|
+
};
|
|
136
|
+
this.eventBus.on(Events.Tool.DragUpdate, this._onDragUpdate);
|
|
160
137
|
|
|
161
|
-
this.
|
|
138
|
+
this._onDragEnd = (data) => {
|
|
162
139
|
const movedObj = this.state.state.objects.find(o => o.id === data.object);
|
|
163
140
|
if (!movedObj) return;
|
|
164
141
|
if (movedObj.type === 'frame') {
|
|
@@ -172,9 +149,32 @@ export class FrameService {
|
|
|
172
149
|
const frames = (this.state.state.objects || []).filter(o => o.type === 'frame');
|
|
173
150
|
const prev = frames.find(fr => fr.id === this._frameHoverId);
|
|
174
151
|
if (prev) this.pixi.setFrameFill(prev.id, prev.width, prev.height, 0xFFFFFF);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
152
|
+
this._frameHoverId = null;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
this.eventBus.on(Events.Tool.DragEnd, this._onDragEnd);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
detach() {
|
|
159
|
+
if (!this._attached) return;
|
|
160
|
+
this._attached = false;
|
|
161
|
+
if (this._hoverRafId != null) {
|
|
162
|
+
cancelAnimationFrame(this._hoverRafId);
|
|
163
|
+
this._hoverRafId = null;
|
|
164
|
+
}
|
|
165
|
+
this._hoverRafScheduled = false;
|
|
166
|
+
this._hoverPendingObjectId = null;
|
|
167
|
+
if (this._onObjectCreated) this.eventBus.off(Events.Object.Created, this._onObjectCreated);
|
|
168
|
+
if (this._onDragStart) this.eventBus.off(Events.Tool.DragStart, this._onDragStart);
|
|
169
|
+
if (this._onDragUpdate) this.eventBus.off(Events.Tool.DragUpdate, this._onDragUpdate);
|
|
170
|
+
if (this._onDragEnd) this.eventBus.off(Events.Tool.DragEnd, this._onDragEnd);
|
|
171
|
+
this._onObjectCreated = null;
|
|
172
|
+
this._onDragStart = null;
|
|
173
|
+
this._onDragUpdate = null;
|
|
174
|
+
this._onDragEnd = null;
|
|
175
|
+
this._frameDragFrameStart = null;
|
|
176
|
+
this._frameDragChildStart = null;
|
|
177
|
+
this._frameHoverId = null;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
_getFrameChildren(frameId) {
|
|
@@ -186,6 +186,47 @@ export class FrameService {
|
|
|
186
186
|
return res;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
_applyHoverThrottled(movedObjectId) {
|
|
190
|
+
if (this._hoverRafScheduled) return;
|
|
191
|
+
this._hoverRafScheduled = true;
|
|
192
|
+
this._hoverPendingObjectId = movedObjectId;
|
|
193
|
+
this._hoverRafId = requestAnimationFrame(() => {
|
|
194
|
+
this._hoverRafScheduled = false;
|
|
195
|
+
this._hoverRafId = null;
|
|
196
|
+
const oid = this._hoverPendingObjectId;
|
|
197
|
+
this._hoverPendingObjectId = null;
|
|
198
|
+
if (!oid) return;
|
|
199
|
+
const moved = this.state.state.objects.find(o => o.id === oid);
|
|
200
|
+
if (!moved || moved.type === 'frame') return;
|
|
201
|
+
const centerX = moved.position.x + (moved.width || 0) / 2;
|
|
202
|
+
const centerY = moved.position.y + (moved.height || 0) / 2;
|
|
203
|
+
const frames = (this.state.state.objects || []).filter(o => o.type === 'frame');
|
|
204
|
+
const ordered = frames.slice().sort((a, b) => {
|
|
205
|
+
const pa = this.pixi.objects.get(a.id);
|
|
206
|
+
const pb = this.pixi.objects.get(b.id);
|
|
207
|
+
return (pb?.zIndex || 0) - (pa?.zIndex || 0);
|
|
208
|
+
});
|
|
209
|
+
let hoverId = null;
|
|
210
|
+
for (const f of ordered) {
|
|
211
|
+
const rect = { x: f.position.x, y: f.position.y, w: f.width || 0, h: f.height || 0 };
|
|
212
|
+
if (centerX >= rect.x && centerX <= rect.x + rect.w && centerY >= rect.y && centerY <= rect.y + rect.h) {
|
|
213
|
+
hoverId = f.id; break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (hoverId !== this._frameHoverId) {
|
|
217
|
+
if (this._frameHoverId) {
|
|
218
|
+
const prev = frames.find(fr => fr.id === this._frameHoverId);
|
|
219
|
+
if (prev) this.pixi.setFrameFill(prev.id, prev.width, prev.height, 0xFFFFFF);
|
|
220
|
+
}
|
|
221
|
+
if (hoverId) {
|
|
222
|
+
const cur = frames.find(fr => fr.id === hoverId);
|
|
223
|
+
if (cur) this.pixi.setFrameFill(cur.id, cur.width, cur.height, 0xFAFAFA);
|
|
224
|
+
}
|
|
225
|
+
this._frameHoverId = hoverId || null;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
189
230
|
_recomputeFrameAttachment(objectId) {
|
|
190
231
|
const obj = (this.state.state.objects || []).find(o => o.id === objectId);
|
|
191
232
|
if (!obj) return;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const MIN_FRAME_AREA = 1800;
|
|
2
|
+
|
|
3
|
+
function getRoundedSize(width, height) {
|
|
4
|
+
return {
|
|
5
|
+
width: Math.max(1, Math.round(width || 1)),
|
|
6
|
+
height: Math.max(1, Math.round(height || 1)),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isHandleEdge(handle) {
|
|
11
|
+
return ['n', 's', 'e', 'w'].includes(handle);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getSingleResizePolicy({ objectType = null, properties = {} } = {}) {
|
|
15
|
+
const isEmojiImage = objectType === 'image' && !!properties?.isEmojiIcon;
|
|
16
|
+
const isEmoji = objectType === 'emoji' || isEmojiImage;
|
|
17
|
+
const isNote = objectType === 'note';
|
|
18
|
+
const lockedAspect = objectType === 'frame' && properties?.lockedAspect === true;
|
|
19
|
+
const keepAspect = objectType === 'image' || lockedAspect || isEmoji || isNote;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
keepAspect,
|
|
23
|
+
square: isEmoji || isNote,
|
|
24
|
+
minArea: objectType === 'frame' ? MIN_FRAME_AREA : 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveAnchoredResizePosition({
|
|
29
|
+
startPosition,
|
|
30
|
+
startSize,
|
|
31
|
+
normalizedSize,
|
|
32
|
+
handle,
|
|
33
|
+
}) {
|
|
34
|
+
if (!startPosition || !startSize || !normalizedSize || !handle) return null;
|
|
35
|
+
|
|
36
|
+
const normalizedHandle = String(handle).toLowerCase();
|
|
37
|
+
const startW = Math.max(1, startSize.width || 1);
|
|
38
|
+
const startH = Math.max(1, startSize.height || 1);
|
|
39
|
+
const nextW = Math.max(1, normalizedSize.width || 1);
|
|
40
|
+
const nextH = Math.max(1, normalizedSize.height || 1);
|
|
41
|
+
let x = startPosition.x;
|
|
42
|
+
let y = startPosition.y;
|
|
43
|
+
|
|
44
|
+
if (normalizedHandle.includes('w')) x = startPosition.x + (startW - nextW);
|
|
45
|
+
if (normalizedHandle.includes('n')) y = startPosition.y + (startH - nextH);
|
|
46
|
+
|
|
47
|
+
if (isHandleEdge(normalizedHandle)) {
|
|
48
|
+
if (normalizedHandle === 'n' || normalizedHandle === 's') {
|
|
49
|
+
x = startPosition.x + ((startW - nextW) / 2);
|
|
50
|
+
} else if (normalizedHandle === 'e' || normalizedHandle === 'w') {
|
|
51
|
+
y = startPosition.y + ((startH - nextH) / 2);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
x: Math.round(x),
|
|
57
|
+
y: Math.round(y),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveSingleResizeDominantAxis({
|
|
62
|
+
startSize,
|
|
63
|
+
size,
|
|
64
|
+
objectType = null,
|
|
65
|
+
properties = {},
|
|
66
|
+
preferredDominantAxis = null,
|
|
67
|
+
}) {
|
|
68
|
+
if (!size) return 'none';
|
|
69
|
+
|
|
70
|
+
const policy = getSingleResizePolicy({ objectType, properties });
|
|
71
|
+
if (policy.square) return 'square';
|
|
72
|
+
if (!policy.keepAspect) return 'free';
|
|
73
|
+
if (preferredDominantAxis === 'width' || preferredDominantAxis === 'height') {
|
|
74
|
+
return preferredDominantAxis;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const startW = Math.max(1, startSize?.width || size.width || 1);
|
|
78
|
+
const startH = Math.max(1, startSize?.height || size.height || 1);
|
|
79
|
+
const rawWidth = Math.max(1, size.width || 1);
|
|
80
|
+
const rawHeight = Math.max(1, size.height || 1);
|
|
81
|
+
const deltaWidth = Math.abs(rawWidth - startW);
|
|
82
|
+
const deltaHeight = Math.abs(rawHeight - startH);
|
|
83
|
+
return deltaWidth >= deltaHeight ? 'width' : 'height';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function normalizeSingleResizeGeometry({
|
|
87
|
+
startSize,
|
|
88
|
+
startPosition,
|
|
89
|
+
handle,
|
|
90
|
+
size,
|
|
91
|
+
position = null,
|
|
92
|
+
objectType = null,
|
|
93
|
+
properties = {},
|
|
94
|
+
preferredDominantAxis = null,
|
|
95
|
+
}) {
|
|
96
|
+
if (!size) {
|
|
97
|
+
return { size: null, position };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const startW = Math.max(1, startSize?.width || size.width || 1);
|
|
101
|
+
const startH = Math.max(1, startSize?.height || size.height || 1);
|
|
102
|
+
const policy = getSingleResizePolicy({ objectType, properties });
|
|
103
|
+
let width = Math.max(1, size.width || 1);
|
|
104
|
+
let height = Math.max(1, size.height || 1);
|
|
105
|
+
const dominantAxis = resolveSingleResizeDominantAxis({
|
|
106
|
+
startSize: { width: startW, height: startH },
|
|
107
|
+
size: { width, height },
|
|
108
|
+
objectType,
|
|
109
|
+
properties,
|
|
110
|
+
preferredDominantAxis,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (policy.square) {
|
|
114
|
+
const squareSize = Math.max(width, height);
|
|
115
|
+
width = squareSize;
|
|
116
|
+
height = squareSize;
|
|
117
|
+
} else if (policy.keepAspect) {
|
|
118
|
+
const aspect = startW / startH;
|
|
119
|
+
if (dominantAxis === 'width') {
|
|
120
|
+
height = width / aspect;
|
|
121
|
+
} else {
|
|
122
|
+
width = height * aspect;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (policy.minArea > 0) {
|
|
127
|
+
const area = Math.max(1, width * height);
|
|
128
|
+
if (area < policy.minArea) {
|
|
129
|
+
const scale = Math.sqrt(policy.minArea / area);
|
|
130
|
+
width *= scale;
|
|
131
|
+
height *= scale;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const normalizedSize = getRoundedSize(width, height);
|
|
136
|
+
const shouldResolvePosition = (policy.keepAspect || policy.square || policy.minArea > 0)
|
|
137
|
+
&& !!startPosition
|
|
138
|
+
&& !!handle;
|
|
139
|
+
const resolvedPosition = shouldResolvePosition
|
|
140
|
+
? resolveAnchoredResizePosition({
|
|
141
|
+
startPosition,
|
|
142
|
+
startSize: { width: startW, height: startH },
|
|
143
|
+
normalizedSize,
|
|
144
|
+
handle,
|
|
145
|
+
})
|
|
146
|
+
: position;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
size: normalizedSize,
|
|
150
|
+
position: resolvedPosition,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -72,9 +72,10 @@ export class SettingsApplier {
|
|
|
72
72
|
if (partial.zoom && s.zoom && (typeof s.zoom.current === 'number')) {
|
|
73
73
|
const world = this.pixi?.worldLayer || this.pixi?.app?.stage;
|
|
74
74
|
if (world) {
|
|
75
|
-
const z = Math.max(0.
|
|
75
|
+
const z = Math.max(0.02, Math.min(5, s.zoom.current));
|
|
76
76
|
world.scale.set(z);
|
|
77
77
|
try { this.eventBus.emit(Events.UI.ZoomPercent, { percentage: Math.round(z * 100) }); } catch (_) {}
|
|
78
|
+
try { this.eventBus.emit(Events.Viewport.Changed); } catch (_) {}
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -84,7 +85,11 @@ export class SettingsApplier {
|
|
|
84
85
|
if (world) {
|
|
85
86
|
world.x = s.pan.x;
|
|
86
87
|
world.y = s.pan.y;
|
|
87
|
-
|
|
88
|
+
if (this.pixi?.gridLayer) {
|
|
89
|
+
this.pixi.gridLayer.x = s.pan.x;
|
|
90
|
+
this.pixi.gridLayer.y = s.pan.y;
|
|
91
|
+
}
|
|
92
|
+
try { this.eventBus.emit(Events.Viewport.Changed); } catch (_) {}
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
}
|
|
@@ -1,29 +1,48 @@
|
|
|
1
1
|
import { Events } from '../core/events/Events.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Уровни зума, выровненные под профиль Miro.
|
|
5
|
+
* Верхняя граница — 500%.
|
|
6
|
+
*/
|
|
7
|
+
const ZOOM_LEVELS = [
|
|
8
|
+
2, 5, 10, 15, 20, 25, 33, 50, 75, 100, 125, 150, 200, 250, 300, 400, 500
|
|
9
|
+
];
|
|
10
|
+
|
|
3
11
|
export class ZoomPanController {
|
|
4
12
|
constructor(eventBus, pixi) {
|
|
5
13
|
this.eventBus = eventBus;
|
|
6
14
|
this.pixi = pixi;
|
|
7
15
|
}
|
|
8
16
|
|
|
17
|
+
_nearestLevelIndex(percent) {
|
|
18
|
+
let best = 0;
|
|
19
|
+
let bestDist = Math.abs(ZOOM_LEVELS[0] - percent);
|
|
20
|
+
for (let i = 1; i < ZOOM_LEVELS.length; i++) {
|
|
21
|
+
const d = Math.abs(ZOOM_LEVELS[i] - percent);
|
|
22
|
+
if (d < bestDist) {
|
|
23
|
+
bestDist = d;
|
|
24
|
+
best = i;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return best;
|
|
28
|
+
}
|
|
29
|
+
|
|
9
30
|
attach() {
|
|
10
31
|
// Масштабирование колесом — глобально отрабатываем Ctrl+Wheel
|
|
11
32
|
this.eventBus.on(Events.Tool.WheelZoom, ({ x, y, delta }) => {
|
|
12
|
-
// Дискретный шаг зума 10%
|
|
13
33
|
const world = this.pixi.worldLayer || this.pixi.app.stage;
|
|
14
34
|
const oldScale = world.scale.x || 1;
|
|
15
35
|
const oldPercent = Math.round(oldScale * 100);
|
|
36
|
+
const idx = this._nearestLevelIndex(oldPercent);
|
|
16
37
|
let targetPercent;
|
|
17
38
|
if (delta < 0) {
|
|
18
|
-
|
|
19
|
-
targetPercent = Math.min(500, Math.floor(oldPercent / 10) * 10 + 10);
|
|
39
|
+
targetPercent = ZOOM_LEVELS[Math.min(ZOOM_LEVELS.length - 1, idx + 1)];
|
|
20
40
|
} else if (delta > 0) {
|
|
21
|
-
|
|
22
|
-
targetPercent = Math.max(10, Math.ceil(oldPercent / 10) * 10 - 10);
|
|
41
|
+
targetPercent = ZOOM_LEVELS[Math.max(0, idx - 1)];
|
|
23
42
|
} else {
|
|
24
43
|
return;
|
|
25
44
|
}
|
|
26
|
-
const newScale = Math.max(0.
|
|
45
|
+
const newScale = Math.max(0.02, Math.min(5, targetPercent / 100));
|
|
27
46
|
if (Math.abs(newScale - oldScale) < 0.0001) return;
|
|
28
47
|
// Вычисляем мировые координаты точки под курсором до изменения скейла
|
|
29
48
|
const worldX = (x - world.x) / oldScale;
|
|
@@ -33,15 +52,20 @@ export class ZoomPanController {
|
|
|
33
52
|
world.x = x - worldX * newScale;
|
|
34
53
|
world.y = y - worldY * newScale;
|
|
35
54
|
this.eventBus.emit(Events.UI.ZoomPercent, { percentage: Math.round(newScale * 100) });
|
|
55
|
+
this.eventBus.emit(Events.Viewport.Changed);
|
|
36
56
|
});
|
|
37
57
|
|
|
38
58
|
// Кнопки зума из UI
|
|
39
59
|
this.eventBus.on(Events.UI.ZoomIn, () => {
|
|
40
|
-
const
|
|
60
|
+
const view = this.pixi?.app?.view;
|
|
61
|
+
if (!view) return;
|
|
62
|
+
const center = { x: view.clientWidth / 2, y: view.clientHeight / 2 };
|
|
41
63
|
this.eventBus.emit(Events.Tool.WheelZoom, { x: center.x, y: center.y, delta: -120 });
|
|
42
64
|
});
|
|
43
65
|
this.eventBus.on(Events.UI.ZoomOut, () => {
|
|
44
|
-
const
|
|
66
|
+
const view = this.pixi?.app?.view;
|
|
67
|
+
if (!view) return;
|
|
68
|
+
const center = { x: view.clientWidth / 2, y: view.clientHeight / 2 };
|
|
45
69
|
this.eventBus.emit(Events.Tool.WheelZoom, { x: center.x, y: center.y, delta: 120 });
|
|
46
70
|
});
|
|
47
71
|
this.eventBus.on(Events.UI.ZoomReset, () => {
|
|
@@ -55,6 +79,7 @@ export class ZoomPanController {
|
|
|
55
79
|
world.x = centerX - worldX * 1;
|
|
56
80
|
world.y = centerY - worldY * 1;
|
|
57
81
|
this.eventBus.emit(Events.UI.ZoomPercent, { percentage: 100 });
|
|
82
|
+
this.eventBus.emit(Events.Viewport.Changed);
|
|
58
83
|
});
|
|
59
84
|
this.eventBus.on(Events.UI.ZoomFit, () => {
|
|
60
85
|
const objs = (this.pixi?.objects ? Array.from(this.pixi.objects.values()) : []);
|
|
@@ -74,7 +99,7 @@ export class ZoomPanController {
|
|
|
74
99
|
const padding = 40;
|
|
75
100
|
const scaleX = (viewW - padding) / bboxW;
|
|
76
101
|
const scaleY = (viewH - padding) / bboxH;
|
|
77
|
-
const newScale = Math.max(0.
|
|
102
|
+
const newScale = Math.max(0.02, Math.min(5, Math.min(scaleX, scaleY)));
|
|
78
103
|
const world = this.pixi.worldLayer || this.pixi.app.stage;
|
|
79
104
|
const worldCenterX = minX + bboxW / 2;
|
|
80
105
|
const worldCenterY = minY + bboxH / 2;
|
|
@@ -82,6 +107,7 @@ export class ZoomPanController {
|
|
|
82
107
|
world.x = viewW / 2 - worldCenterX * newScale;
|
|
83
108
|
world.y = viewH / 2 - worldCenterY * newScale;
|
|
84
109
|
this.eventBus.emit(Events.UI.ZoomPercent, { percentage: Math.round(newScale * 100) });
|
|
110
|
+
this.eventBus.emit(Events.Viewport.Changed);
|
|
85
111
|
});
|
|
86
112
|
}
|
|
87
113
|
}
|