@sequent-org/moodboard 1.0.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 +44 -0
- package/src/assets/icons/README.md +105 -0
- package/src/assets/icons/attachments.svg +3 -0
- package/src/assets/icons/clear.svg +5 -0
- package/src/assets/icons/comments.svg +3 -0
- package/src/assets/icons/emoji.svg +6 -0
- package/src/assets/icons/frame.svg +3 -0
- package/src/assets/icons/image.svg +3 -0
- package/src/assets/icons/note.svg +3 -0
- package/src/assets/icons/pan.svg +3 -0
- package/src/assets/icons/pencil.svg +3 -0
- package/src/assets/icons/redo.svg +3 -0
- package/src/assets/icons/select.svg +9 -0
- package/src/assets/icons/shapes.svg +3 -0
- package/src/assets/icons/text-add.svg +3 -0
- package/src/assets/icons/topbar/README.md +39 -0
- package/src/assets/icons/topbar/grid-cross.svg +6 -0
- package/src/assets/icons/topbar/grid-dot.svg +3 -0
- package/src/assets/icons/topbar/grid-line.svg +3 -0
- package/src/assets/icons/topbar/grid-off.svg +3 -0
- package/src/assets/icons/topbar/paint.svg +3 -0
- package/src/assets/icons/undo.svg +3 -0
- package/src/core/ApiClient.js +309 -0
- package/src/core/EventBus.js +42 -0
- package/src/core/HistoryManager.js +261 -0
- package/src/core/KeyboardManager.js +710 -0
- package/src/core/PixiEngine.js +439 -0
- package/src/core/SaveManager.js +381 -0
- package/src/core/StateManager.js +64 -0
- package/src/core/commands/BaseCommand.js +68 -0
- package/src/core/commands/CopyObjectCommand.js +44 -0
- package/src/core/commands/CreateObjectCommand.js +46 -0
- package/src/core/commands/DeleteObjectCommand.js +146 -0
- package/src/core/commands/EditFileNameCommand.js +107 -0
- package/src/core/commands/GroupMoveCommand.js +47 -0
- package/src/core/commands/GroupReorderZCommand.js +74 -0
- package/src/core/commands/GroupResizeCommand.js +37 -0
- package/src/core/commands/GroupRotateCommand.js +41 -0
- package/src/core/commands/MoveObjectCommand.js +89 -0
- package/src/core/commands/PasteObjectCommand.js +103 -0
- package/src/core/commands/ReorderZCommand.js +45 -0
- package/src/core/commands/ResizeObjectCommand.js +135 -0
- package/src/core/commands/RotateObjectCommand.js +70 -0
- package/src/core/commands/index.js +14 -0
- package/src/core/events/Events.js +147 -0
- package/src/core/index.js +1632 -0
- package/src/core/rendering/GeometryUtils.js +89 -0
- package/src/core/rendering/HitTestManager.js +186 -0
- package/src/core/rendering/LayerManager.js +137 -0
- package/src/core/rendering/ObjectRenderer.js +363 -0
- package/src/core/rendering/PixiRenderer.js +140 -0
- package/src/core/rendering/index.js +9 -0
- package/src/grid/BaseGrid.js +164 -0
- package/src/grid/CrossGrid.js +75 -0
- package/src/grid/DotGrid.js +148 -0
- package/src/grid/GridFactory.js +173 -0
- package/src/grid/LineGrid.js +115 -0
- package/src/index.js +2 -0
- package/src/moodboard/ActionHandler.js +114 -0
- package/src/moodboard/DataManager.js +114 -0
- package/src/moodboard/MoodBoard.js +359 -0
- package/src/moodboard/WorkspaceManager.js +103 -0
- package/src/objects/BaseObject.js +1 -0
- package/src/objects/CommentObject.js +115 -0
- package/src/objects/DrawingObject.js +114 -0
- package/src/objects/EmojiObject.js +98 -0
- package/src/objects/FileObject.js +318 -0
- package/src/objects/FrameObject.js +127 -0
- package/src/objects/ImageObject.js +72 -0
- package/src/objects/NoteObject.js +227 -0
- package/src/objects/ObjectFactory.js +61 -0
- package/src/objects/ShapeObject.js +134 -0
- package/src/objects/StampObject.js +0 -0
- package/src/objects/StickerObject.js +0 -0
- package/src/objects/TextObject.js +123 -0
- package/src/services/BoardService.js +85 -0
- package/src/services/FileUploadService.js +398 -0
- package/src/services/FrameService.js +138 -0
- package/src/services/ImageUploadService.js +246 -0
- package/src/services/ZOrderManager.js +50 -0
- package/src/services/ZoomPanController.js +78 -0
- package/src/src.7z +0 -0
- package/src/src.zip +0 -0
- package/src/src2.zip +0 -0
- package/src/tools/AlignmentGuides.js +326 -0
- package/src/tools/BaseTool.js +257 -0
- package/src/tools/ResizeHandles.js +381 -0
- package/src/tools/ToolManager.js +580 -0
- package/src/tools/board-tools/PanTool.js +43 -0
- package/src/tools/board-tools/ZoomTool.js +393 -0
- package/src/tools/object-tools/DrawingTool.js +404 -0
- package/src/tools/object-tools/PlacementTool.js +1005 -0
- package/src/tools/object-tools/SelectTool.js +2183 -0
- package/src/tools/object-tools/TextTool.js +416 -0
- package/src/tools/object-tools/selection/BoxSelectController.js +105 -0
- package/src/tools/object-tools/selection/GeometryUtils.js +101 -0
- package/src/tools/object-tools/selection/GroupDragController.js +61 -0
- package/src/tools/object-tools/selection/GroupResizeController.js +90 -0
- package/src/tools/object-tools/selection/GroupRotateController.js +61 -0
- package/src/tools/object-tools/selection/HandlesSync.js +96 -0
- package/src/tools/object-tools/selection/ResizeController.js +68 -0
- package/src/tools/object-tools/selection/RotateController.js +58 -0
- package/src/tools/object-tools/selection/SelectionModel.js +42 -0
- package/src/tools/object-tools/selection/SimpleDragController.js +45 -0
- package/src/ui/CommentPopover.js +187 -0
- package/src/ui/ContextMenu.js +340 -0
- package/src/ui/FilePropertiesPanel.js +298 -0
- package/src/ui/FramePropertiesPanel.js +462 -0
- package/src/ui/HtmlHandlesLayer.js +778 -0
- package/src/ui/HtmlTextLayer.js +279 -0
- package/src/ui/MapPanel.js +290 -0
- package/src/ui/NotePropertiesPanel.js +502 -0
- package/src/ui/SaveStatus.js +250 -0
- package/src/ui/TextPropertiesPanel.js +911 -0
- package/src/ui/Toolbar.js +1118 -0
- package/src/ui/Topbar.js +220 -0
- package/src/ui/ZoomPanel.js +116 -0
- package/src/ui/styles/workspace.css +854 -0
- package/src/utils/colors.js +0 -0
- package/src/utils/geometry.js +0 -0
- package/src/utils/iconLoader.js +270 -0
- package/src/utils/objectIdGenerator.js +17 -0
- package/src/utils/topbarIconLoader.js +114 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Сервис для загрузки и управления изображениями на сервере
|
|
3
|
+
*/
|
|
4
|
+
export class ImageUploadService {
|
|
5
|
+
constructor(apiClient) {
|
|
6
|
+
this.apiClient = apiClient;
|
|
7
|
+
this.uploadEndpoint = '/api/images/upload';
|
|
8
|
+
this.deleteEndpoint = '/api/images';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Загружает изображение на сервер
|
|
13
|
+
* @param {File|Blob} file - файл изображения
|
|
14
|
+
* @param {string} name - имя файла
|
|
15
|
+
* @returns {Promise<{id: string, url: string, width: number, height: number}>}
|
|
16
|
+
*/
|
|
17
|
+
async uploadImage(file, name = null) {
|
|
18
|
+
try {
|
|
19
|
+
// Получаем размеры изображения перед загрузкой
|
|
20
|
+
const dimensions = await this._getImageDimensions(file);
|
|
21
|
+
|
|
22
|
+
// Создаем FormData для отправки файла
|
|
23
|
+
const formData = new FormData();
|
|
24
|
+
formData.append('image', file);
|
|
25
|
+
formData.append('name', name || file.name || 'image.png');
|
|
26
|
+
formData.append('width', dimensions.width.toString());
|
|
27
|
+
formData.append('height', dimensions.height.toString());
|
|
28
|
+
|
|
29
|
+
// Получаем CSRF токен
|
|
30
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
31
|
+
|
|
32
|
+
if (!csrfToken) {
|
|
33
|
+
throw new Error('CSRF токен не найден');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const response = await fetch(this.uploadEndpoint, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: {
|
|
39
|
+
'X-CSRF-TOKEN': csrfToken,
|
|
40
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
41
|
+
},
|
|
42
|
+
credentials: 'same-origin',
|
|
43
|
+
body: formData
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const errorData = await response.json().catch(() => null);
|
|
48
|
+
throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = await response.json();
|
|
52
|
+
|
|
53
|
+
if (!result.success) {
|
|
54
|
+
throw new Error(result.message || 'Ошибка загрузки изображения');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
id: result.data.id,
|
|
59
|
+
url: result.data.url,
|
|
60
|
+
width: result.data.width,
|
|
61
|
+
height: result.data.height,
|
|
62
|
+
name: result.data.name,
|
|
63
|
+
size: result.data.size
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('Ошибка загрузки изображения:', error);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Загружает изображение из base64 DataURL
|
|
74
|
+
* @param {string} dataUrl - base64 DataURL
|
|
75
|
+
* @param {string} name - имя файла
|
|
76
|
+
* @returns {Promise<{id: string, url: string, width: number, height: number}>}
|
|
77
|
+
*/
|
|
78
|
+
async uploadFromDataUrl(dataUrl, name = 'clipboard-image.png') {
|
|
79
|
+
const blob = await this._dataUrlToBlob(dataUrl);
|
|
80
|
+
return this.uploadImage(blob, name);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Удаляет изображение с сервера
|
|
85
|
+
* @param {string} imageId - ID изображения
|
|
86
|
+
*/
|
|
87
|
+
async deleteImage(imageId) {
|
|
88
|
+
try {
|
|
89
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
90
|
+
|
|
91
|
+
const response = await fetch(`${this.deleteEndpoint}/${imageId}`, {
|
|
92
|
+
method: 'DELETE',
|
|
93
|
+
headers: {
|
|
94
|
+
'X-CSRF-TOKEN': csrfToken,
|
|
95
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
96
|
+
'Accept': 'application/json'
|
|
97
|
+
},
|
|
98
|
+
credentials: 'same-origin'
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const errorData = await response.json().catch(() => null);
|
|
103
|
+
throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const result = await response.json();
|
|
107
|
+
return result.success;
|
|
108
|
+
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('Ошибка удаления изображения:', error);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Очищает неиспользуемые изображения с сервера
|
|
117
|
+
* @returns {Promise<{deletedCount: number, errors: Array}>}
|
|
118
|
+
*/
|
|
119
|
+
async cleanupUnusedImages() {
|
|
120
|
+
try {
|
|
121
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
122
|
+
|
|
123
|
+
const response = await fetch(`${this.deleteEndpoint}/cleanup`, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: {
|
|
126
|
+
'X-CSRF-TOKEN': csrfToken,
|
|
127
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
128
|
+
'Accept': 'application/json'
|
|
129
|
+
},
|
|
130
|
+
credentials: 'same-origin'
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
const errorData = await response.json().catch(() => null);
|
|
135
|
+
throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result = await response.json();
|
|
139
|
+
|
|
140
|
+
if (result.success) {
|
|
141
|
+
// Защитная проверка на существование result.data
|
|
142
|
+
const data = result.data || {};
|
|
143
|
+
return {
|
|
144
|
+
deletedCount: data.deleted_count || 0,
|
|
145
|
+
errors: data.errors || []
|
|
146
|
+
};
|
|
147
|
+
} else {
|
|
148
|
+
throw new Error(result.message || 'Ошибка очистки изображений');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('Ошибка очистки неиспользуемых изображений:', error);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Получает информацию об изображении
|
|
159
|
+
* @param {string} imageId - ID изображения
|
|
160
|
+
*/
|
|
161
|
+
async getImageInfo(imageId) {
|
|
162
|
+
try {
|
|
163
|
+
const response = await fetch(`${this.deleteEndpoint}/${imageId}`, {
|
|
164
|
+
method: 'GET',
|
|
165
|
+
headers: {
|
|
166
|
+
'Accept': 'application/json',
|
|
167
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
168
|
+
},
|
|
169
|
+
credentials: 'same-origin'
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = await response.json();
|
|
177
|
+
return result.data;
|
|
178
|
+
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error('Ошибка получения информации об изображении:', error);
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Получает размеры изображения из файла или blob
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
_getImageDimensions(file) {
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
const img = new Image();
|
|
192
|
+
const url = URL.createObjectURL(file);
|
|
193
|
+
|
|
194
|
+
img.onload = () => {
|
|
195
|
+
resolve({
|
|
196
|
+
width: img.naturalWidth || img.width,
|
|
197
|
+
height: img.naturalHeight || img.height
|
|
198
|
+
});
|
|
199
|
+
URL.revokeObjectURL(url);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
img.onerror = () => {
|
|
203
|
+
reject(new Error('Не удалось загрузить изображение для определения размеров'));
|
|
204
|
+
URL.revokeObjectURL(url);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
img.src = url;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Конвертирует DataURL в Blob
|
|
213
|
+
* @private
|
|
214
|
+
*/
|
|
215
|
+
_dataUrlToBlob(dataUrl) {
|
|
216
|
+
return new Promise((resolve) => {
|
|
217
|
+
const arr = dataUrl.split(',');
|
|
218
|
+
const mime = arr[0].match(/:(.*?);/)[1];
|
|
219
|
+
const bstr = atob(arr[1]);
|
|
220
|
+
let n = bstr.length;
|
|
221
|
+
const u8arr = new Uint8Array(n);
|
|
222
|
+
|
|
223
|
+
while (n--) {
|
|
224
|
+
u8arr[n] = bstr.charCodeAt(n);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
resolve(new Blob([u8arr], { type: mime }));
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Проверяет, является ли URL внешней ссылкой на изображение
|
|
233
|
+
*/
|
|
234
|
+
static isExternalImageUrl(url) {
|
|
235
|
+
if (!url || typeof url !== 'string') return false;
|
|
236
|
+
return /^https?:\/\/.+\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(url);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Проверяет, является ли строка base64 DataURL
|
|
241
|
+
*/
|
|
242
|
+
static isDataUrl(str) {
|
|
243
|
+
if (!str || typeof str !== 'string') return false;
|
|
244
|
+
return /^data:image\/.+;base64,/.test(str);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Events } from '../core/events/Events.js';
|
|
2
|
+
|
|
3
|
+
export class ZOrderManager {
|
|
4
|
+
constructor(eventBus, pixi, state) {
|
|
5
|
+
this.eventBus = eventBus;
|
|
6
|
+
this.pixi = pixi;
|
|
7
|
+
this.state = state;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
attach() {
|
|
11
|
+
const ensureFramesBottom = () => {
|
|
12
|
+
const arr = this.state.state.objects || [];
|
|
13
|
+
if (arr.length === 0) return;
|
|
14
|
+
const frames = [];
|
|
15
|
+
const others = [];
|
|
16
|
+
for (const o of arr) {
|
|
17
|
+
if (o?.type === 'frame') frames.push(o); else others.push(o);
|
|
18
|
+
}
|
|
19
|
+
const newOrder = [...frames, ...others];
|
|
20
|
+
let changed = false;
|
|
21
|
+
if (newOrder.length === arr.length) {
|
|
22
|
+
for (let i = 0; i < arr.length; i++) {
|
|
23
|
+
if (arr[i] !== newOrder[i]) { changed = true; break; }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!changed) return;
|
|
27
|
+
this.state.state.objects = newOrder;
|
|
28
|
+
const world = this.pixi?.worldLayer || this.pixi?.app?.stage;
|
|
29
|
+
if (world) world.sortableChildren = true;
|
|
30
|
+
let z = 0;
|
|
31
|
+
for (const o of this.state.state.objects || []) {
|
|
32
|
+
const pixi = this.pixi.objects.get(o.id);
|
|
33
|
+
if (!pixi) continue;
|
|
34
|
+
if (o.type === 'frame') {
|
|
35
|
+
pixi.zIndex = -100000;
|
|
36
|
+
} else {
|
|
37
|
+
pixi.zIndex = z++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
this.state.markDirty();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
this.eventBus.on(Events.Object.Created, () => ensureFramesBottom());
|
|
44
|
+
this.eventBus.on(Events.Object.Deleted, () => ensureFramesBottom());
|
|
45
|
+
this.eventBus.on(Events.Object.Reordered, () => ensureFramesBottom());
|
|
46
|
+
this.eventBus.on(Events.UI.LayerGroupSendToBack, () => ensureFramesBottom());
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Events } from '../core/events/Events.js';
|
|
2
|
+
|
|
3
|
+
export class ZoomPanController {
|
|
4
|
+
constructor(eventBus, pixi) {
|
|
5
|
+
this.eventBus = eventBus;
|
|
6
|
+
this.pixi = pixi;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
attach() {
|
|
10
|
+
// Масштабирование колесом — глобально отрабатываем Ctrl+Wheel
|
|
11
|
+
this.eventBus.on(Events.Tool.WheelZoom, ({ x, y, delta }) => {
|
|
12
|
+
const factor = 1 + (-delta) * 0.0015;
|
|
13
|
+
const world = this.pixi.worldLayer || this.pixi.app.stage;
|
|
14
|
+
const oldScale = world.scale.x || 1;
|
|
15
|
+
const newScale = Math.max(0.1, Math.min(5, oldScale * factor));
|
|
16
|
+
if (newScale === oldScale) return;
|
|
17
|
+
// Вычисляем мировые координаты точки под курсором до изменения скейла
|
|
18
|
+
const worldX = (x - world.x) / oldScale;
|
|
19
|
+
const worldY = (y - world.y) / oldScale;
|
|
20
|
+
// Применяем новый скейл и пересчитываем позицию, чтобы точка под курсором осталась на месте
|
|
21
|
+
world.scale.set(newScale);
|
|
22
|
+
world.x = x - worldX * newScale;
|
|
23
|
+
world.y = y - worldY * newScale;
|
|
24
|
+
this.eventBus.emit(Events.UI.ZoomPercent, { percentage: Math.round(newScale * 100) });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Кнопки зума из UI
|
|
28
|
+
this.eventBus.on(Events.UI.ZoomIn, () => {
|
|
29
|
+
const center = { x: this.pixi.app.view.clientWidth / 2, y: this.pixi.app.view.clientHeight / 2 };
|
|
30
|
+
this.eventBus.emit(Events.Tool.WheelZoom, { x: center.x, y: center.y, delta: -120 });
|
|
31
|
+
});
|
|
32
|
+
this.eventBus.on(Events.UI.ZoomOut, () => {
|
|
33
|
+
const center = { x: this.pixi.app.view.clientWidth / 2, y: this.pixi.app.view.clientHeight / 2 };
|
|
34
|
+
this.eventBus.emit(Events.Tool.WheelZoom, { x: center.x, y: center.y, delta: 120 });
|
|
35
|
+
});
|
|
36
|
+
this.eventBus.on(Events.UI.ZoomReset, () => {
|
|
37
|
+
const world = this.pixi.worldLayer || this.pixi.app.stage;
|
|
38
|
+
const centerX = this.pixi.app.view.clientWidth / 2;
|
|
39
|
+
const centerY = this.pixi.app.view.clientHeight / 2;
|
|
40
|
+
const oldScale = world.scale.x || 1;
|
|
41
|
+
const worldX = (centerX - world.x) / oldScale;
|
|
42
|
+
const worldY = (centerY - world.y) / oldScale;
|
|
43
|
+
world.scale.set(1);
|
|
44
|
+
world.x = centerX - worldX * 1;
|
|
45
|
+
world.y = centerY - worldY * 1;
|
|
46
|
+
this.eventBus.emit(Events.UI.ZoomPercent, { percentage: 100 });
|
|
47
|
+
});
|
|
48
|
+
this.eventBus.on(Events.UI.ZoomFit, () => {
|
|
49
|
+
const objs = (this.pixi?.objects ? Array.from(this.pixi.objects.values()) : []);
|
|
50
|
+
if (objs.length === 0) return;
|
|
51
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
52
|
+
for (const p of objs) {
|
|
53
|
+
const b = p.getBounds();
|
|
54
|
+
minX = Math.min(minX, b.x);
|
|
55
|
+
minY = Math.min(minY, b.y);
|
|
56
|
+
maxX = Math.max(maxX, b.x + b.width);
|
|
57
|
+
maxY = Math.max(maxY, b.y + b.height);
|
|
58
|
+
}
|
|
59
|
+
const bboxW = Math.max(1, maxX - minX);
|
|
60
|
+
const bboxH = Math.max(1, maxY - minY);
|
|
61
|
+
const viewW = this.pixi.app.view.clientWidth;
|
|
62
|
+
const viewH = this.pixi.app.view.clientHeight;
|
|
63
|
+
const padding = 40;
|
|
64
|
+
const scaleX = (viewW - padding) / bboxW;
|
|
65
|
+
const scaleY = (viewH - padding) / bboxH;
|
|
66
|
+
const newScale = Math.max(0.1, Math.min(5, Math.min(scaleX, scaleY)));
|
|
67
|
+
const world = this.pixi.worldLayer || this.pixi.app.stage;
|
|
68
|
+
const worldCenterX = minX + bboxW / 2;
|
|
69
|
+
const worldCenterY = minY + bboxH / 2;
|
|
70
|
+
world.scale.set(newScale);
|
|
71
|
+
world.x = viewW / 2 - worldCenterX * newScale;
|
|
72
|
+
world.y = viewH / 2 - worldCenterY * newScale;
|
|
73
|
+
this.eventBus.emit(Events.UI.ZoomPercent, { percentage: Math.round(newScale * 100) });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
package/src/src.7z
ADDED
|
Binary file
|
package/src/src.zip
ADDED
|
Binary file
|
package/src/src2.zip
ADDED
|
Binary file
|