@mhmttosun/dnd 0.0.0-stage → 1.1.1
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/LICENSE +21 -0
- package/README.md +327 -2
- package/css/answer-key.css +200 -0
- package/css/editor.css +1797 -0
- package/css/player.css +782 -0
- package/css/stage.css +490 -0
- package/dist/dnd-player-element.js +1758 -0
- package/dist/dnd.js +2041 -0
- package/dist/editor.css +3278 -0
- package/dist/editor.d.ts +23 -0
- package/dist/editor.js +8506 -0
- package/dist/index.d.ts +102 -0
- package/dist/player.css +782 -0
- package/dist/player.d.ts +1 -0
- package/dist/player.js +2041 -0
- package/dist/player.umd.cjs +1758 -0
- package/index.html +203 -0
- package/js/app.js +188 -0
- package/js/core/AnswerKeyManager.js +494 -0
- package/js/core/EditorCore.js +220 -0
- package/js/core/HistoryManager.js +194 -0
- package/js/core/LayerManager.js +86 -0
- package/js/core/ObjectManager.js +468 -0
- package/js/core/SelectionManager.js +459 -0
- package/js/core/StageManager.js +295 -0
- package/js/data/SampleProjects.js +883 -0
- package/js/engine/TransformEngine.js +632 -0
- package/js/engine/ValidationEngine.js +147 -0
- package/js/player/ActivityEngine.js +1272 -0
- package/js/player/ExportManager.js +820 -0
- package/js/ui/MiniToolbar.js +1546 -0
- package/js/ui/PropertyPanel.js +776 -0
- package/js/ui/UIManager.js +596 -0
- package/js/utils/AudioSynthesizer.js +111 -0
- package/package.json +69 -4
- package/src/ActivityPlayer.js +309 -0
- package/src/DNDEditor.js +217 -0
- package/src/components/DNDPlayerElement.js +191 -0
- package/src/index.js +12 -0
- package/src/player.js +13 -0
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TransformEngine - Pointer Events API kullanarak tasarım modunda nesnelerin
|
|
3
|
+
* taşınmasını, 8 tutamaçla boyutlandırılmasını, döndürülmesini ve
|
|
4
|
+
* sahne üzeri çoklu seçim (marquee selection) işlemlerini yönetir.
|
|
5
|
+
*/
|
|
6
|
+
class TransformEngine {
|
|
7
|
+
constructor(core, stageManager, objectManager, selectionManager, historyManager, stageEl, options = {}) {
|
|
8
|
+
this.core = core;
|
|
9
|
+
this.stageManager = stageManager;
|
|
10
|
+
this.objectManager = objectManager;
|
|
11
|
+
this.selectionManager = selectionManager;
|
|
12
|
+
this.historyManager = historyManager;
|
|
13
|
+
this.stage = stageEl;
|
|
14
|
+
this.signal = options.signal;
|
|
15
|
+
|
|
16
|
+
this.activeAction = null; // 'move' | 'resize' | 'rotate' | 'marquee'
|
|
17
|
+
this.activeHandle = null; // 'nw', 'n', 'se', vb.
|
|
18
|
+
|
|
19
|
+
this.startStagePos = { x: 0, y: 0 };
|
|
20
|
+
this.startClientPos = null;
|
|
21
|
+
this.hasMovedOrDragged = false;
|
|
22
|
+
this.isPointerDown = false;
|
|
23
|
+
this.startObjectsData = []; // Başlangıçtaki nesne verileri
|
|
24
|
+
this.activePointerId = null;
|
|
25
|
+
|
|
26
|
+
// Marquee seçim kutusu elementi
|
|
27
|
+
this.marqueeEl = null;
|
|
28
|
+
|
|
29
|
+
this.init();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
init() {
|
|
33
|
+
const options = this.signal ? { signal: this.signal } : undefined;
|
|
34
|
+
this.stage.addEventListener('pointerdown', (e) => this.onPointerDown(e), options);
|
|
35
|
+
window.addEventListener('pointermove', (e) => this.onPointerMove(e), options);
|
|
36
|
+
window.addEventListener('pointerup', (e) => this.onPointerUp(e), options);
|
|
37
|
+
window.addEventListener('pointercancel', (e) => this.onPointerCancel(e), options);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onPointerDown(e) {
|
|
41
|
+
// Sadece tasarım modunda ve sol fare/dokunma ile çalışır
|
|
42
|
+
if (this.core.getMode() !== 'design') return;
|
|
43
|
+
if (e.button !== 0) return; // Sadece sol tık
|
|
44
|
+
|
|
45
|
+
// Eğer kullanıcı bir metin düzenleme alanına tıkladıysa, tarayıcının yerel metin imleci/seçimini engelleme
|
|
46
|
+
if (e.target.isContentEditable || e.target.closest('[contenteditable="true"]')) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Eğer başka bir yerde metin düzenleniyorsa ve dışarı tıklandıysa, düzenlemeyi sonlandır
|
|
51
|
+
if (window.app && window.app.uiManager && window.app.uiManager.isEditingText) {
|
|
52
|
+
if (!e.target.closest('#contextual-mini-toolbar')) {
|
|
53
|
+
window.app.uiManager.finishInlineEditing();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const clientPos = { x: e.clientX, y: e.clientY };
|
|
58
|
+
const stagePos = this.stageManager.clientToStage(clientPos.x, clientPos.y);
|
|
59
|
+
|
|
60
|
+
this.startStagePos = stagePos;
|
|
61
|
+
this.startClientPos = clientPos;
|
|
62
|
+
this.activePointerId = e.pointerId;
|
|
63
|
+
this.hasMovedOrDragged = false;
|
|
64
|
+
this.isPointerDown = true;
|
|
65
|
+
|
|
66
|
+
// Sahneye basıldığı anda mini toolbar hemen gizlenir (basıldığı an değil bırakıldığı an görünür)
|
|
67
|
+
this.core.emit('stagePointerDown', { clientX: e.clientX, clientY: e.clientY });
|
|
68
|
+
|
|
69
|
+
// 1. Döndürme Tutamacı Kontrolü
|
|
70
|
+
const rotateHandle = e.target.closest('.rotate-handle');
|
|
71
|
+
if (rotateHandle) {
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
this.startRotate(stagePos);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Boyutlandırma Tutamacı Kontrolü
|
|
79
|
+
const resizeHandle = e.target.closest('.resize-handle');
|
|
80
|
+
if (resizeHandle) {
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
const handleType = resizeHandle.getAttribute('data-handle');
|
|
84
|
+
this.startResize(handleType, stagePos);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 3. Seçim Kutusu Üzerine Tıklama (Taşıma Başlatma)
|
|
89
|
+
const unlockBtn = e.target.closest('.lock-pill-unlock-btn');
|
|
90
|
+
if (unlockBtn) {
|
|
91
|
+
e.stopPropagation();
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
const primary = this.selectionManager.getPrimarySelected();
|
|
94
|
+
if (primary) {
|
|
95
|
+
this.objectManager.updateObject(primary.id, { locked: false });
|
|
96
|
+
this.selectionManager.updateSelectionBox();
|
|
97
|
+
this.core.emit('selectionChanged', {
|
|
98
|
+
selectedIds: Array.from(this.selectionManager.selectedIds),
|
|
99
|
+
primaryObject: this.objectManager.getObject(primary.id)
|
|
100
|
+
});
|
|
101
|
+
if (window.app && window.app.miniToolbar) {
|
|
102
|
+
window.app.miniToolbar.show();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const selectionBorder = e.target.closest('.selection-border, .selection-box');
|
|
109
|
+
if (selectionBorder) {
|
|
110
|
+
e.stopPropagation();
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
const primary = this.selectionManager.getPrimarySelected();
|
|
113
|
+
if (primary && primary.locked) {
|
|
114
|
+
return; // Kilitli nesne taşınamaz
|
|
115
|
+
}
|
|
116
|
+
this.startMove(stagePos);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 4. Sahne Nesnesine Tıklama
|
|
121
|
+
const objEl = e.target.closest('.stage-object');
|
|
122
|
+
if (objEl) {
|
|
123
|
+
e.stopPropagation();
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
|
|
126
|
+
const objId = objEl.getAttribute('data-id');
|
|
127
|
+
const obj = this.objectManager.getObject(objId);
|
|
128
|
+
|
|
129
|
+
// Doğrudan kilit ikonuna tıklandıysa kilidi aç
|
|
130
|
+
if (e.target.closest('.lock-indicator, .lock-badge-btn')) {
|
|
131
|
+
this.objectManager.updateObject(objId, { locked: false });
|
|
132
|
+
this.selectionManager.select(objId, false);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (obj) {
|
|
137
|
+
if (e.shiftKey) {
|
|
138
|
+
this.selectionManager.toggleSelect(objId);
|
|
139
|
+
} else {
|
|
140
|
+
if (!this.selectionManager.selectedIds.has(objId)) {
|
|
141
|
+
this.selectionManager.select(objId, false);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Sadece kilitli değilse taşımayı başlat
|
|
146
|
+
if (!obj.locked) {
|
|
147
|
+
this.startMove(stagePos);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 5. Boş Sahneye Tıklama: Marquee Seçim veya Seçimi Temizleme
|
|
154
|
+
if (!e.shiftKey) {
|
|
155
|
+
this.selectionManager.clearSelection();
|
|
156
|
+
}
|
|
157
|
+
this.startMarquee(stagePos, e.shiftKey);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
startMove(stagePos) {
|
|
161
|
+
const selected = this.selectionManager.getSelectedObjects().filter(o => !o.locked);
|
|
162
|
+
if (selected.length === 0) {
|
|
163
|
+
this.activeAction = null;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.activeAction = 'move';
|
|
168
|
+
this.startObjectsData = selected.map(o => ({
|
|
169
|
+
id: o.id,
|
|
170
|
+
x: o.x,
|
|
171
|
+
y: o.y,
|
|
172
|
+
width: o.width,
|
|
173
|
+
height: o.height
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
startResize(handle, stagePos) {
|
|
178
|
+
this.activeAction = 'resize';
|
|
179
|
+
this.activeHandle = handle;
|
|
180
|
+
const selected = this.selectionManager.getSelectedObjects();
|
|
181
|
+
this.startObjectsData = selected.map(o => {
|
|
182
|
+
const item = {
|
|
183
|
+
id: o.id,
|
|
184
|
+
type: o.type,
|
|
185
|
+
x: o.x,
|
|
186
|
+
y: o.y,
|
|
187
|
+
width: o.width,
|
|
188
|
+
height: o.height,
|
|
189
|
+
rotation: o.rotation || 0
|
|
190
|
+
};
|
|
191
|
+
if (o.type === 'line') {
|
|
192
|
+
const rad = (item.rotation * Math.PI) / 180;
|
|
193
|
+
const cos = Math.cos(rad);
|
|
194
|
+
const sin = Math.sin(rad);
|
|
195
|
+
const cx = item.x + item.width / 2;
|
|
196
|
+
const cy = item.y + item.height / 2;
|
|
197
|
+
const hw = item.width / 2;
|
|
198
|
+
item.p0 = { x: cx - hw * cos, y: cy - hw * sin };
|
|
199
|
+
item.p1 = { x: cx + hw * cos, y: cy + hw * sin };
|
|
200
|
+
}
|
|
201
|
+
return item;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
startRotate(stagePos) {
|
|
206
|
+
this.activeAction = 'rotate';
|
|
207
|
+
const primary = this.selectionManager.getPrimarySelected();
|
|
208
|
+
if (!primary) return;
|
|
209
|
+
|
|
210
|
+
this.startObjectsData = [{
|
|
211
|
+
id: primary.id,
|
|
212
|
+
x: primary.x,
|
|
213
|
+
y: primary.y,
|
|
214
|
+
width: primary.width,
|
|
215
|
+
height: primary.height,
|
|
216
|
+
rotation: primary.rotation || 0,
|
|
217
|
+
centerX: primary.x + primary.width / 2,
|
|
218
|
+
centerY: primary.y + primary.height / 2
|
|
219
|
+
}];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
startMarquee(stagePos, shiftKey = false) {
|
|
223
|
+
this.activeAction = 'marquee';
|
|
224
|
+
this.isMarqueeShift = shiftKey;
|
|
225
|
+
this.initialMarqueeSelectedIds = new Set(this.selectionManager.selectedIds);
|
|
226
|
+
|
|
227
|
+
if (!this.marqueeEl) {
|
|
228
|
+
this.marqueeEl = document.createElement('div');
|
|
229
|
+
this.marqueeEl.className = 'stage-marquee-selection';
|
|
230
|
+
this.stage.appendChild(this.marqueeEl);
|
|
231
|
+
}
|
|
232
|
+
this.marqueeEl.style.display = 'block';
|
|
233
|
+
this.marqueeEl.style.left = `${stagePos.x}px`;
|
|
234
|
+
this.marqueeEl.style.top = `${stagePos.y}px`;
|
|
235
|
+
this.marqueeEl.style.width = '0px';
|
|
236
|
+
this.marqueeEl.style.height = '0px';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
onPointerMove(e) {
|
|
240
|
+
if (!this.activeAction || this.activePointerId !== e.pointerId) return;
|
|
241
|
+
|
|
242
|
+
if (this.startClientPos) {
|
|
243
|
+
const clientDist = Math.hypot(e.clientX - this.startClientPos.x, e.clientY - this.startClientPos.y);
|
|
244
|
+
if (clientDist > 4) {
|
|
245
|
+
this.hasMovedOrDragged = true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const currentStagePos = this.stageManager.clientToStage(e.clientX, e.clientY);
|
|
250
|
+
const dx = currentStagePos.x - this.startStagePos.x;
|
|
251
|
+
const dy = currentStagePos.y - this.startStagePos.y;
|
|
252
|
+
|
|
253
|
+
if (this.activeAction === 'move') {
|
|
254
|
+
this.handleMoveAction(dx, dy);
|
|
255
|
+
} else if (this.activeAction === 'resize') {
|
|
256
|
+
this.handleResizeAction(dx, dy, e.shiftKey);
|
|
257
|
+
} else if (this.activeAction === 'rotate') {
|
|
258
|
+
this.handleRotateAction(currentStagePos, e.shiftKey);
|
|
259
|
+
} else if (this.activeAction === 'marquee') {
|
|
260
|
+
this.handleMarqueeAction(currentStagePos);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
handleMoveAction(dx, dy) {
|
|
265
|
+
const stageW = this.core.data.stage.width;
|
|
266
|
+
const stageH = this.core.data.stage.height;
|
|
267
|
+
|
|
268
|
+
// Kılavuz hizalama çizgileri hesaplama
|
|
269
|
+
let snappedDx = dx;
|
|
270
|
+
let snappedDy = dy;
|
|
271
|
+
const hGuides = [];
|
|
272
|
+
const vGuides = [];
|
|
273
|
+
|
|
274
|
+
// Birincil nesneye göre hizalama hesapla
|
|
275
|
+
if (this.startObjectsData.length > 0) {
|
|
276
|
+
const primaryStart = this.startObjectsData[0];
|
|
277
|
+
let targetX = primaryStart.x + dx;
|
|
278
|
+
let targetY = primaryStart.y + dy;
|
|
279
|
+
|
|
280
|
+
if (this.stageManager.snapToGrid) {
|
|
281
|
+
targetX = this.stageManager.snap(targetX);
|
|
282
|
+
targetY = this.stageManager.snap(targetY);
|
|
283
|
+
snappedDx = targetX - primaryStart.x;
|
|
284
|
+
snappedDy = targetY - primaryStart.y;
|
|
285
|
+
} else {
|
|
286
|
+
// Akıllı merkez ve kenar kılavuzları
|
|
287
|
+
const objMidX = targetX + primaryStart.width / 2;
|
|
288
|
+
const objMidY = targetY + primaryStart.height / 2;
|
|
289
|
+
const stageMidX = stageW / 2;
|
|
290
|
+
const stageMidY = stageH / 2;
|
|
291
|
+
|
|
292
|
+
const snapThreshold = 6;
|
|
293
|
+
if (Math.abs(objMidX - stageMidX) < snapThreshold) {
|
|
294
|
+
snappedDx = stageMidX - primaryStart.width / 2 - primaryStart.x;
|
|
295
|
+
vGuides.push(stageMidX);
|
|
296
|
+
}
|
|
297
|
+
if (Math.abs(objMidY - stageMidY) < snapThreshold) {
|
|
298
|
+
snappedDy = stageMidY - primaryStart.height / 2 - primaryStart.y;
|
|
299
|
+
hGuides.push(stageMidY);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Sahne sınırları kısıtlaması: Seçili nesneler sahne dışına ASLA taşamaz
|
|
305
|
+
let minAllowedDx = -Infinity;
|
|
306
|
+
let maxAllowedDx = Infinity;
|
|
307
|
+
let minAllowedDy = -Infinity;
|
|
308
|
+
let maxAllowedDy = Infinity;
|
|
309
|
+
|
|
310
|
+
this.startObjectsData.forEach(item => {
|
|
311
|
+
minAllowedDx = Math.max(minAllowedDx, -item.x);
|
|
312
|
+
maxAllowedDx = Math.min(maxAllowedDx, stageW - item.width - item.x);
|
|
313
|
+
minAllowedDy = Math.max(minAllowedDy, -item.y);
|
|
314
|
+
maxAllowedDy = Math.min(maxAllowedDy, stageH - item.height - item.y);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (minAllowedDx <= maxAllowedDx) {
|
|
318
|
+
snappedDx = Math.max(minAllowedDx, Math.min(maxAllowedDx, snappedDx));
|
|
319
|
+
}
|
|
320
|
+
if (minAllowedDy <= maxAllowedDy) {
|
|
321
|
+
snappedDy = Math.max(minAllowedDy, Math.min(maxAllowedDy, snappedDy));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this.stageManager.showGuidelines(hGuides, vGuides);
|
|
325
|
+
|
|
326
|
+
this.startObjectsData.forEach(item => {
|
|
327
|
+
const newX = Math.round(Math.max(0, Math.min(stageW - item.width, item.x + snappedDx)));
|
|
328
|
+
const newY = Math.round(Math.max(0, Math.min(stageH - item.height, item.y + snappedDy)));
|
|
329
|
+
|
|
330
|
+
this.objectManager.updateObject(item.id, { x: newX, y: newY }, false);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
this.selectionManager.updateSelectionBox();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
handleResizeAction(dx, dy, keepAspectRatio = false) {
|
|
337
|
+
if (this.startObjectsData.length === 0) return;
|
|
338
|
+
const start = this.startObjectsData[0];
|
|
339
|
+
const handle = this.activeHandle;
|
|
340
|
+
const stageW = this.core.data.stage.width;
|
|
341
|
+
const stageH = this.core.data.stage.height;
|
|
342
|
+
|
|
343
|
+
let newX = start.x;
|
|
344
|
+
let newY = start.y;
|
|
345
|
+
let newW = start.width;
|
|
346
|
+
let newH = start.height;
|
|
347
|
+
|
|
348
|
+
const isLine = start.type === 'line';
|
|
349
|
+
const minW = isLine ? 20 : 30;
|
|
350
|
+
const minH = isLine ? 6 : 30;
|
|
351
|
+
|
|
352
|
+
if (isLine && start.p0 && start.p1) {
|
|
353
|
+
const p0 = start.p0;
|
|
354
|
+
const p1 = start.p1;
|
|
355
|
+
|
|
356
|
+
if (handle.includes('e')) {
|
|
357
|
+
let p1x = Math.max(0, Math.min(stageW, p1.x + dx));
|
|
358
|
+
let p1y = Math.max(0, Math.min(stageH, p1.y + dy));
|
|
359
|
+
let vx = p1x - p0.x;
|
|
360
|
+
let vy = p1y - p0.y;
|
|
361
|
+
let len = Math.hypot(vx, vy);
|
|
362
|
+
if (len < minW) {
|
|
363
|
+
len = minW;
|
|
364
|
+
const curAngle = Math.atan2(vy, vx);
|
|
365
|
+
p1x = p0.x + len * Math.cos(curAngle);
|
|
366
|
+
p1y = p0.y + len * Math.sin(curAngle);
|
|
367
|
+
vx = p1x - p0.x;
|
|
368
|
+
vy = p1y - p0.y;
|
|
369
|
+
}
|
|
370
|
+
let deg = (Math.atan2(vy, vx) * 180) / Math.PI;
|
|
371
|
+
if (keepAspectRatio) {
|
|
372
|
+
deg = Math.round(deg / 15) * 15;
|
|
373
|
+
} else {
|
|
374
|
+
[-180, -90, 0, 90, 180].forEach(snapDeg => {
|
|
375
|
+
if (Math.abs(deg - snapDeg) < 3.5) deg = snapDeg;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
const snapRad = (deg * Math.PI) / 180;
|
|
379
|
+
const finalP1 = {
|
|
380
|
+
x: p0.x + len * Math.cos(snapRad),
|
|
381
|
+
y: p0.y + len * Math.sin(snapRad)
|
|
382
|
+
};
|
|
383
|
+
const cx = (p0.x + finalP1.x) / 2;
|
|
384
|
+
const cy = (p0.y + finalP1.y) / 2;
|
|
385
|
+
const newW = Math.round(len);
|
|
386
|
+
const newH = start.height;
|
|
387
|
+
const newX = Math.round(cx - newW / 2);
|
|
388
|
+
const newY = Math.round(cy - newH / 2);
|
|
389
|
+
const newRot = Math.round((deg % 360 + 360) % 360);
|
|
390
|
+
|
|
391
|
+
this.objectManager.updateObject(start.id, {
|
|
392
|
+
x: newX,
|
|
393
|
+
y: newY,
|
|
394
|
+
width: newW,
|
|
395
|
+
height: newH,
|
|
396
|
+
rotation: newRot
|
|
397
|
+
}, false);
|
|
398
|
+
this.selectionManager.updateSelectionBox();
|
|
399
|
+
return;
|
|
400
|
+
} else if (handle.includes('w')) {
|
|
401
|
+
let p0x = Math.max(0, Math.min(stageW, p0.x + dx));
|
|
402
|
+
let p0y = Math.max(0, Math.min(stageH, p0.y + dy));
|
|
403
|
+
let vx = p1.x - p0x;
|
|
404
|
+
let vy = p1.y - p0y;
|
|
405
|
+
let len = Math.hypot(vx, vy);
|
|
406
|
+
if (len < minW) {
|
|
407
|
+
len = minW;
|
|
408
|
+
const curAngle = Math.atan2(vy, vx);
|
|
409
|
+
p0x = p1.x - len * Math.cos(curAngle);
|
|
410
|
+
p0y = p1.y - len * Math.sin(curAngle);
|
|
411
|
+
vx = p1.x - p0x;
|
|
412
|
+
vy = p1.y - p0y;
|
|
413
|
+
}
|
|
414
|
+
let deg = (Math.atan2(vy, vx) * 180) / Math.PI;
|
|
415
|
+
if (keepAspectRatio) {
|
|
416
|
+
deg = Math.round(deg / 15) * 15;
|
|
417
|
+
} else {
|
|
418
|
+
[-180, -90, 0, 90, 180].forEach(snapDeg => {
|
|
419
|
+
if (Math.abs(deg - snapDeg) < 3.5) deg = snapDeg;
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
const snapRad = (deg * Math.PI) / 180;
|
|
423
|
+
const finalP0 = {
|
|
424
|
+
x: p1.x - len * Math.cos(snapRad),
|
|
425
|
+
y: p1.y - len * Math.sin(snapRad)
|
|
426
|
+
};
|
|
427
|
+
const cx = (finalP0.x + p1.x) / 2;
|
|
428
|
+
const cy = (finalP0.y + p1.y) / 2;
|
|
429
|
+
const newW = Math.round(len);
|
|
430
|
+
const newH = start.height;
|
|
431
|
+
const newX = Math.round(cx - newW / 2);
|
|
432
|
+
const newY = Math.round(cy - newH / 2);
|
|
433
|
+
const newRot = Math.round((deg % 360 + 360) % 360);
|
|
434
|
+
|
|
435
|
+
this.objectManager.updateObject(start.id, {
|
|
436
|
+
x: newX,
|
|
437
|
+
y: newY,
|
|
438
|
+
width: newW,
|
|
439
|
+
height: newH,
|
|
440
|
+
rotation: newRot
|
|
441
|
+
}, false);
|
|
442
|
+
this.selectionManager.updateSelectionBox();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Tutamaç yönüne göre boyut hesaplama (sahne sınırları dahilinde)
|
|
448
|
+
if (handle.includes('e')) {
|
|
449
|
+
newW = Math.max(minW, Math.min(stageW - start.x, start.width + dx));
|
|
450
|
+
}
|
|
451
|
+
if (handle.includes('s')) {
|
|
452
|
+
newH = Math.max(minH, Math.min(stageH - start.y, start.height + dy));
|
|
453
|
+
}
|
|
454
|
+
if (handle.includes('w')) {
|
|
455
|
+
const maxDeltaLeft = start.x; // en fazla sahne soluna (0) kadar uzayabilir
|
|
456
|
+
const safeDx = Math.max(-maxDeltaLeft, dx);
|
|
457
|
+
const candidateW = start.width - safeDx;
|
|
458
|
+
if (candidateW >= minW) {
|
|
459
|
+
newW = candidateW;
|
|
460
|
+
newX = start.x + safeDx;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (handle.includes('n')) {
|
|
464
|
+
const maxDeltaTop = start.y; // en fazla sahne üstüne (0) kadar uzayabilir
|
|
465
|
+
const safeDy = Math.max(-maxDeltaTop, dy);
|
|
466
|
+
const candidateH = start.height - safeDy;
|
|
467
|
+
if (candidateH >= minH) {
|
|
468
|
+
newH = candidateH;
|
|
469
|
+
newY = start.y + safeDy;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// En-boy oranı koruma (Shift basılıysa veya görsel ise)
|
|
474
|
+
if (keepAspectRatio && !isLine) {
|
|
475
|
+
const ratio = start.width / start.height;
|
|
476
|
+
if (['nw', 'ne', 'se', 'sw'].includes(handle)) {
|
|
477
|
+
newH = Math.max(minH, Math.min(stageH - newY, newW / ratio));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (this.stageManager.snapToGrid) {
|
|
482
|
+
newW = this.stageManager.snap(newW);
|
|
483
|
+
newH = this.stageManager.snap(newH);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Sahne dışına taşmayı kesin olarak engelle
|
|
487
|
+
newX = Math.max(0, Math.min(stageW - minW, newX));
|
|
488
|
+
newY = Math.max(0, Math.min(stageH - minH, newY));
|
|
489
|
+
newW = Math.max(minW, Math.min(stageW - newX, newW));
|
|
490
|
+
newH = Math.max(minH, Math.min(stageH - newY, newH));
|
|
491
|
+
|
|
492
|
+
this.objectManager.updateObject(start.id, {
|
|
493
|
+
x: Math.round(newX),
|
|
494
|
+
y: Math.round(newY),
|
|
495
|
+
width: Math.round(newW),
|
|
496
|
+
height: Math.round(newH)
|
|
497
|
+
}, false);
|
|
498
|
+
|
|
499
|
+
this.selectionManager.updateSelectionBox();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
handleRotateAction(currentPos, snap15 = false) {
|
|
503
|
+
if (this.startObjectsData.length === 0) return;
|
|
504
|
+
const start = this.startObjectsData[0];
|
|
505
|
+
|
|
506
|
+
const dx = currentPos.x - start.centerX;
|
|
507
|
+
const dy = currentPos.y - start.centerY;
|
|
508
|
+
|
|
509
|
+
let deg = Math.atan2(dy, dx) * (180 / Math.PI) - 90;
|
|
510
|
+
deg = (deg % 360 + 360) % 360;
|
|
511
|
+
|
|
512
|
+
if (snap15) {
|
|
513
|
+
deg = Math.round(deg / 15) * 15;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
this.objectManager.updateObject(start.id, {
|
|
517
|
+
rotation: Math.round(deg)
|
|
518
|
+
}, false);
|
|
519
|
+
|
|
520
|
+
this.selectionManager.updateSelectionBox();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
handleMarqueeAction(currentPos) {
|
|
524
|
+
if (!this.marqueeEl) return;
|
|
525
|
+
|
|
526
|
+
const minX = Math.min(this.startStagePos.x, currentPos.x);
|
|
527
|
+
const minY = Math.min(this.startStagePos.y, currentPos.y);
|
|
528
|
+
const maxX = Math.max(this.startStagePos.x, currentPos.x);
|
|
529
|
+
const maxY = Math.max(this.startStagePos.y, currentPos.y);
|
|
530
|
+
|
|
531
|
+
const w = maxX - minX;
|
|
532
|
+
const h = maxY - minY;
|
|
533
|
+
|
|
534
|
+
this.marqueeEl.style.left = `${minX}px`;
|
|
535
|
+
this.marqueeEl.style.top = `${minY}px`;
|
|
536
|
+
this.marqueeEl.style.width = `${w}px`;
|
|
537
|
+
this.marqueeEl.style.height = `${h}px`;
|
|
538
|
+
|
|
539
|
+
// 1. Seçim alanı ile anlık olarak kesişen nesneleri tespit et
|
|
540
|
+
const objects = this.objectManager.getObjects();
|
|
541
|
+
const currentIntersectingIds = new Set();
|
|
542
|
+
const groupIdsToInclude = new Set();
|
|
543
|
+
|
|
544
|
+
objects.forEach(obj => {
|
|
545
|
+
if (obj.locked || obj.visible === false) return;
|
|
546
|
+
const intersects = (
|
|
547
|
+
obj.x < maxX &&
|
|
548
|
+
obj.x + obj.width > minX &&
|
|
549
|
+
obj.y < maxY &&
|
|
550
|
+
obj.y + obj.height > minY
|
|
551
|
+
);
|
|
552
|
+
if (intersects) {
|
|
553
|
+
currentIntersectingIds.add(obj.id);
|
|
554
|
+
if (obj.groupId) {
|
|
555
|
+
groupIdsToInclude.add(obj.groupId);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Gruba ait nesnelerden biri seçim alanındaysa grubun tüm üyelerini dahil et
|
|
561
|
+
if (groupIdsToInclude.size > 0) {
|
|
562
|
+
objects.forEach(obj => {
|
|
563
|
+
if (obj.groupId && groupIdsToInclude.has(obj.groupId)) {
|
|
564
|
+
currentIntersectingIds.add(obj.id);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// 2. Seçili nesneler kümesini dinamik olarak güncelle:
|
|
570
|
+
// Seçim alanı değiştikçe kapsam dışına çıkan nesneler otomatik olarak seçimden çıkarılır
|
|
571
|
+
this.selectionManager.selectedIds.clear();
|
|
572
|
+
|
|
573
|
+
if (this.isMarqueeShift && this.initialMarqueeSelectedIds) {
|
|
574
|
+
this.initialMarqueeSelectedIds.forEach(id => this.selectionManager.selectedIds.add(id));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
currentIntersectingIds.forEach(id => this.selectionManager.selectedIds.add(id));
|
|
578
|
+
|
|
579
|
+
// 3. Tuval üzerindeki seçim kutusu ve nesne vurgularını anlık canlı olarak yenile
|
|
580
|
+
this.selectionManager.updateSelectionBox();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
onPointerUp(e) {
|
|
584
|
+
if (this.activePointerId !== e.pointerId) return;
|
|
585
|
+
|
|
586
|
+
this.stageManager.clearGuidelines();
|
|
587
|
+
|
|
588
|
+
if (this.activeAction === 'marquee' && this.marqueeEl) {
|
|
589
|
+
this.marqueeEl.style.display = 'none';
|
|
590
|
+
this.core.emit('selectionChanged', {
|
|
591
|
+
selectedIds: Array.from(this.selectionManager.selectedIds),
|
|
592
|
+
primaryObject: this.selectionManager.getPrimarySelected()
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (['move', 'resize', 'rotate'].includes(this.activeAction)) {
|
|
597
|
+
if (this.hasMovedOrDragged) {
|
|
598
|
+
// Değişiklik geçmişe kaydedilir
|
|
599
|
+
this.historyManager.pushState();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const prevAction = this.activeAction;
|
|
604
|
+
const wasDragged = this.hasMovedOrDragged;
|
|
605
|
+
const isMarquee = prevAction === 'marquee';
|
|
606
|
+
this.activeAction = null;
|
|
607
|
+
this.activeHandle = null;
|
|
608
|
+
this.activePointerId = null;
|
|
609
|
+
this.startObjectsData = [];
|
|
610
|
+
this.isPointerDown = false;
|
|
611
|
+
this.isMarqueeShift = false;
|
|
612
|
+
this.initialMarqueeSelectedIds = null;
|
|
613
|
+
|
|
614
|
+
// Bırakıldığı anda mini toolbar durumunu bildir:
|
|
615
|
+
// Tutulduğu anla bırakıldığı an sürükleme yapılmışsa mini toolbar görünmesin, yapılmamışsa görünsün
|
|
616
|
+
this.core.emit('stagePointerUp', {
|
|
617
|
+
hasDragged: wasDragged,
|
|
618
|
+
isMarquee: isMarquee,
|
|
619
|
+
action: prevAction,
|
|
620
|
+
clientX: e.clientX,
|
|
621
|
+
clientY: e.clientY
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
onPointerCancel(e) {
|
|
626
|
+
this.onPointerUp(e);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Global namespace
|
|
631
|
+
window.DND = window.DND || {};
|
|
632
|
+
window.DND.TransformEngine = TransformEngine;
|