@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,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnswerKeyManager - Görsel cevap anahtarı sistemi.
|
|
3
|
+
* Sürüklenebilir nesneler ile hedef alanlar arasındaki ilişkileri
|
|
4
|
+
* görsel SVG bağlantı çizgileriyle kurar, düzenler ve veri modeline işler.
|
|
5
|
+
*/
|
|
6
|
+
class AnswerKeyManager {
|
|
7
|
+
constructor(core, stageManager, objectManager, stageEl) {
|
|
8
|
+
this.core = core;
|
|
9
|
+
this.stageManager = stageManager;
|
|
10
|
+
this.objectManager = objectManager;
|
|
11
|
+
this.stage = stageEl;
|
|
12
|
+
|
|
13
|
+
this.svgConnections = stageEl.querySelector('#svg-connections');
|
|
14
|
+
this.activeSourceId = null; // Bağlantı için seçilen sürüklenebilir nesne
|
|
15
|
+
this.guideBar = null;
|
|
16
|
+
|
|
17
|
+
this.init();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
init() {
|
|
21
|
+
this.core.on('modeChanged', ({ newMode }) => {
|
|
22
|
+
this.activeSourceId = null;
|
|
23
|
+
this.stage.classList.remove('ak-selecting-target');
|
|
24
|
+
if (newMode === 'answer-key') {
|
|
25
|
+
this.renderConnections();
|
|
26
|
+
this.updateGuideBar();
|
|
27
|
+
setTimeout(() => this.stageManager.updateScale(), 0);
|
|
28
|
+
} else {
|
|
29
|
+
this.clearSvg();
|
|
30
|
+
this.updateGuideBar();
|
|
31
|
+
setTimeout(() => this.stageManager.updateScale(), 0);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.core.on('projectLoaded', () => {
|
|
36
|
+
this.cleanupDanglingKeys();
|
|
37
|
+
if (this.core.getMode() === 'answer-key') this.renderConnections();
|
|
38
|
+
this.updateGuideBar();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this.core.on('objectDeleted', () => {
|
|
42
|
+
this.cleanupDanglingKeys();
|
|
43
|
+
if (this.core.getMode() === 'answer-key') this.renderConnections();
|
|
44
|
+
this.updateGuideBar();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
this.core.on('objectUpdated', () => {
|
|
48
|
+
if (this.core.getMode() === 'answer-key') this.renderConnections();
|
|
49
|
+
this.updateGuideBar();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
this.core.on('objectAdded', () => {
|
|
53
|
+
this.updateGuideBar();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.core.on('answerKeyChanged', () => {
|
|
57
|
+
this.updateGuideBar();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
this.setupClickInteraction();
|
|
61
|
+
this.updateGuideBar();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getGuideBar() {
|
|
65
|
+
if (this.guideBar && this.guideBar.isConnected) return this.guideBar;
|
|
66
|
+
const root = (this.stage && this.stage.getRootNode && this.stage.getRootNode() !== document)
|
|
67
|
+
? this.stage.getRootNode()
|
|
68
|
+
: document;
|
|
69
|
+
this.guideBar = (root.getElementById ? root.getElementById('answer-key-guide-bar') : null)
|
|
70
|
+
|| (root.querySelector ? root.querySelector('#answer-key-guide-bar') : null)
|
|
71
|
+
|| document.getElementById('answer-key-guide-bar');
|
|
72
|
+
return this.guideBar;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getGuideState() {
|
|
76
|
+
const objects = this.core.data.objects || [];
|
|
77
|
+
const answerKey = this.core.data.answerKey || {};
|
|
78
|
+
const draggables = objects.filter(o => o.role === 'draggable');
|
|
79
|
+
const targets = objects.filter(o => ['target', 'category', 'sequence_slot'].includes(o.role));
|
|
80
|
+
const targetIds = new Set(targets.map(t => t.id));
|
|
81
|
+
|
|
82
|
+
const totalDraggables = draggables.length;
|
|
83
|
+
let matchedCount = 0;
|
|
84
|
+
draggables.forEach(d => {
|
|
85
|
+
const tId = answerKey[d.id];
|
|
86
|
+
if (tId && (targetIds.has(tId) || this.core.data.project?.activityType === 'sequencing')) {
|
|
87
|
+
matchedCount++;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// 1. Sahne açıldığında, tüm eşleştirmeler yapılmışsa:
|
|
92
|
+
// Eşleştirmeler tamamlandı (Tüm eşleşmeler tamamlandı) / X/Y eşleşme · Cevap anahtarını kontrol et
|
|
93
|
+
// 2. Eşleştirmeler hiç yapılmamışsa:
|
|
94
|
+
// Cevap anahtarı oluştur / 0/Y eşleşme · Seçerek eşleştirmeye başla
|
|
95
|
+
// 3. Eşleştirme eksikse:
|
|
96
|
+
// Eşleştirmeye devam et / X/Y eşleşme · Sıradaki nesneyi seçerek devam et.
|
|
97
|
+
if (totalDraggables > 0 && matchedCount >= totalDraggables) {
|
|
98
|
+
return {
|
|
99
|
+
status: 'all-completed',
|
|
100
|
+
title: 'Eşleştirmeler tamamlandı', // Tüm eşleşmeler tamamlandı
|
|
101
|
+
instruction: `${matchedCount}/${totalDraggables} eşleşme · Cevap anahtarını kontrol et`, // Cevap anahtarını kontrol edebilirsin
|
|
102
|
+
matchedCount,
|
|
103
|
+
totalDraggables
|
|
104
|
+
};
|
|
105
|
+
} else if (matchedCount === 0) {
|
|
106
|
+
return {
|
|
107
|
+
status: 'none-completed',
|
|
108
|
+
title: 'Cevap anahtarı oluştur',
|
|
109
|
+
instruction: this.activeSourceId
|
|
110
|
+
? `${matchedCount}/${totalDraggables} eşleşme · Hedef nesneyi seçerek eşleştirmeyi tamamla`
|
|
111
|
+
: `${matchedCount}/${totalDraggables} eşleşme · Seçerek eşleştirmeye başla`,
|
|
112
|
+
matchedCount,
|
|
113
|
+
totalDraggables
|
|
114
|
+
};
|
|
115
|
+
} else {
|
|
116
|
+
return {
|
|
117
|
+
status: 'incomplete',
|
|
118
|
+
title: 'Eşleştirmeye devam et',
|
|
119
|
+
instruction: this.activeSourceId
|
|
120
|
+
? `${matchedCount}/${totalDraggables} eşleşme · Hedef nesneyi seçerek eşleştirmeyi tamamla`
|
|
121
|
+
: `${matchedCount}/${totalDraggables} eşleşme · Sıradaki nesneyi seçerek devam et.`,
|
|
122
|
+
matchedCount,
|
|
123
|
+
totalDraggables
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
updateGuideBar() {
|
|
129
|
+
const bar = this.getGuideBar();
|
|
130
|
+
if (!bar) return;
|
|
131
|
+
|
|
132
|
+
const isAnswerKey = this.core.getMode() === 'answer-key';
|
|
133
|
+
bar.style.display = isAnswerKey ? 'flex' : 'none';
|
|
134
|
+
|
|
135
|
+
if (!isAnswerKey) return;
|
|
136
|
+
|
|
137
|
+
const state = this.getGuideState();
|
|
138
|
+
|
|
139
|
+
const titleEl = bar.querySelector('#ak-guide-title');
|
|
140
|
+
const instructionEl = bar.querySelector('#ak-guide-instruction');
|
|
141
|
+
const iconContainer = bar.querySelector('#ak-status-icon');
|
|
142
|
+
|
|
143
|
+
if (titleEl) titleEl.textContent = state.title;
|
|
144
|
+
if (instructionEl) instructionEl.textContent = state.instruction;
|
|
145
|
+
|
|
146
|
+
if (iconContainer) {
|
|
147
|
+
if (state.status === 'all-completed') {
|
|
148
|
+
iconContainer.innerHTML = `
|
|
149
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
150
|
+
<circle cx="12" cy="12" r="10"/>
|
|
151
|
+
<polyline points="9 12 12 15 16 10"/>
|
|
152
|
+
</svg>`;
|
|
153
|
+
} else if (state.status === 'incomplete') {
|
|
154
|
+
iconContainer.innerHTML = `
|
|
155
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
156
|
+
<circle cx="12" cy="12" r="10"/>
|
|
157
|
+
<polyline points="12 6 12 12 16 14"/>
|
|
158
|
+
</svg>`;
|
|
159
|
+
} else {
|
|
160
|
+
iconContainer.innerHTML = `
|
|
161
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
162
|
+
<circle cx="12" cy="12" r="10"/>
|
|
163
|
+
<line x1="12" y1="8" x2="12" y2="16"/>
|
|
164
|
+
<line x1="8" y1="12" x2="16" y2="12"/>
|
|
165
|
+
</svg>`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
bar.setAttribute('data-guide-state', state.status);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
setupClickInteraction() {
|
|
173
|
+
// Cevap anahtarı modunda nesnelere tıklama ile eşleştirme
|
|
174
|
+
this.stage.addEventListener('click', (e) => {
|
|
175
|
+
if (this.core.getMode() !== 'answer-key') return;
|
|
176
|
+
|
|
177
|
+
const objEl = e.target.closest('.stage-object');
|
|
178
|
+
if (!objEl) {
|
|
179
|
+
// Boşluğa tıklanırsa seçili kaynağı bırak
|
|
180
|
+
if (this.activeSourceId) {
|
|
181
|
+
this.activeSourceId = null;
|
|
182
|
+
this.updateVisualHighlights();
|
|
183
|
+
this.updateGuideBar();
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const id = objEl.getAttribute('data-id');
|
|
189
|
+
const obj = this.objectManager.getObject(id);
|
|
190
|
+
if (!obj) return;
|
|
191
|
+
|
|
192
|
+
e.stopPropagation();
|
|
193
|
+
|
|
194
|
+
// 1. Durum: Sürüklenebilir bir nesneye tıklandıysa
|
|
195
|
+
if (obj.role === 'draggable') {
|
|
196
|
+
if (this.activeSourceId === id) {
|
|
197
|
+
// Zaten seçiliyse seçimi kaldır
|
|
198
|
+
this.activeSourceId = null;
|
|
199
|
+
} else {
|
|
200
|
+
// Yeni kaynak seçildi
|
|
201
|
+
this.activeSourceId = id;
|
|
202
|
+
}
|
|
203
|
+
this.updateVisualHighlights();
|
|
204
|
+
this.updateGuideBar();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 2. Durum: Kaynak seçiliyken sadece sahne rolü "Hedef" olan nesneler tıklanabilir olmalı
|
|
209
|
+
if (this.activeSourceId) {
|
|
210
|
+
const isTarget = ['target', 'category', 'sequence_slot'].includes(obj.role);
|
|
211
|
+
|
|
212
|
+
if (isTarget) {
|
|
213
|
+
this.setAnswer(this.activeSourceId, obj.id);
|
|
214
|
+
this.activeSourceId = null;
|
|
215
|
+
this.updateVisualHighlights();
|
|
216
|
+
this.renderConnections();
|
|
217
|
+
this.updateGuideBar();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
setAnswer(draggableId, targetId) {
|
|
224
|
+
if (!this.core.data.answerKey) {
|
|
225
|
+
this.core.data.answerKey = {};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.core.data.answerKey[draggableId] = targetId;
|
|
229
|
+
this.core.emit('answerKeyChanged', {
|
|
230
|
+
draggableId,
|
|
231
|
+
targetId,
|
|
232
|
+
answerKey: this.core.data.answerKey
|
|
233
|
+
});
|
|
234
|
+
this.core.emit('dataChanged', { source: 'answerKey' });
|
|
235
|
+
this.core.autoSave();
|
|
236
|
+
this.updateGuideBar();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
removeAnswer(draggableId) {
|
|
240
|
+
if (this.core.data.answerKey && this.core.data.answerKey[draggableId]) {
|
|
241
|
+
delete this.core.data.answerKey[draggableId];
|
|
242
|
+
this.core.emit('answerKeyChanged', {
|
|
243
|
+
draggableId,
|
|
244
|
+
targetId: null,
|
|
245
|
+
answerKey: this.core.data.answerKey
|
|
246
|
+
});
|
|
247
|
+
this.core.emit('dataChanged', { source: 'answerKey' });
|
|
248
|
+
this.core.autoSave();
|
|
249
|
+
this.renderConnections();
|
|
250
|
+
this.updateGuideBar();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
getAnswer(draggableId) {
|
|
255
|
+
return this.core.data.answerKey ? this.core.data.answerKey[draggableId] : null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
cleanupDanglingKeys() {
|
|
259
|
+
if (!this.core.data.answerKey) return;
|
|
260
|
+
const currentIds = new Set(this.core.data.objects.map(o => o.id));
|
|
261
|
+
|
|
262
|
+
let changed = false;
|
|
263
|
+
for (const [dragId, targetId] of Object.entries(this.core.data.answerKey)) {
|
|
264
|
+
if (!currentIds.has(dragId)) {
|
|
265
|
+
delete this.core.data.answerKey[dragId];
|
|
266
|
+
changed = true;
|
|
267
|
+
}
|
|
268
|
+
// Sıralama etkinliğinde targetId bazen indeks olabilir, nesne id'si ise kontrol et
|
|
269
|
+
if (this.core.data.project.activityType !== 'sequencing') {
|
|
270
|
+
if (!currentIds.has(targetId)) {
|
|
271
|
+
delete this.core.data.answerKey[dragId];
|
|
272
|
+
changed = true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (changed) {
|
|
277
|
+
this.core.emit('answerKeyChanged', { answerKey: this.core.data.answerKey });
|
|
278
|
+
this.core.autoSave();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Görsel vurguları güncelle (Sadece seçilen kaynak nesne vurgulanır, diğer nesneler değişmez)
|
|
283
|
+
updateVisualHighlights() {
|
|
284
|
+
this.stage.querySelectorAll('.stage-object').forEach(el => {
|
|
285
|
+
el.classList.remove('ak-source-active', 'ak-target-candidate');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (this.activeSourceId) {
|
|
289
|
+
this.stage.classList.add('ak-selecting-target');
|
|
290
|
+
const activeEl = this.stage.querySelector(`[data-id="${this.activeSourceId}"]`);
|
|
291
|
+
if (activeEl) {
|
|
292
|
+
activeEl.classList.add('ak-source-active');
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
this.stage.classList.remove('ak-selecting-target');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// SVG Bağlantı Çizgilerini Çiz
|
|
300
|
+
renderConnections() {
|
|
301
|
+
if (!this.svgConnections) return;
|
|
302
|
+
this.clearSvg();
|
|
303
|
+
|
|
304
|
+
const answerKey = this.core.data.answerKey || {};
|
|
305
|
+
const objectsMap = new Map();
|
|
306
|
+
this.core.data.objects.forEach(o => objectsMap.set(o.id, o));
|
|
307
|
+
|
|
308
|
+
for (const [dragId, targetId] of Object.entries(answerKey)) {
|
|
309
|
+
const sourceObj = objectsMap.get(dragId);
|
|
310
|
+
const targetObj = objectsMap.get(targetId);
|
|
311
|
+
|
|
312
|
+
if (!sourceObj || !targetObj) continue;
|
|
313
|
+
|
|
314
|
+
this.drawConnectionLine(sourceObj, targetObj, dragId);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// İki nesne arasındaki en optimum karşılıklı kenar merkezlerini ve yönü hesaplar
|
|
319
|
+
getFacingAnchorPoints(source, target) {
|
|
320
|
+
const sLeft = source.x;
|
|
321
|
+
const sRight = source.x + source.width;
|
|
322
|
+
const sTop = source.y;
|
|
323
|
+
const sBottom = source.y + source.height;
|
|
324
|
+
const sCx = source.x + source.width / 2;
|
|
325
|
+
const sCy = source.y + source.height / 2;
|
|
326
|
+
|
|
327
|
+
const tLeft = target.x;
|
|
328
|
+
const tRight = target.x + target.width;
|
|
329
|
+
const tTop = target.y;
|
|
330
|
+
const tBottom = target.y + target.height;
|
|
331
|
+
const tCx = target.x + target.width / 2;
|
|
332
|
+
const tCy = target.y + target.height / 2;
|
|
333
|
+
|
|
334
|
+
const dx = tCx - sCx;
|
|
335
|
+
const dy = tCy - sCy;
|
|
336
|
+
|
|
337
|
+
// 1. Yatay karşılıklı kenar adayı (Sağ -> Sol veya Sol -> Sağ)
|
|
338
|
+
let hPair;
|
|
339
|
+
if (dx >= 0) {
|
|
340
|
+
// Hedef sağda: Kaynak sağ kenarından hedefin sol kenarına
|
|
341
|
+
hPair = {
|
|
342
|
+
p1: { x: sRight, y: sCy },
|
|
343
|
+
p2: { x: tLeft, y: tCy },
|
|
344
|
+
dir: 'horizontal-right',
|
|
345
|
+
dist: Math.hypot(tLeft - sRight, tCy - sCy)
|
|
346
|
+
};
|
|
347
|
+
} else {
|
|
348
|
+
// Hedef solda: Kaynak sol kenarından hedefin sağ kenarına
|
|
349
|
+
hPair = {
|
|
350
|
+
p1: { x: sLeft, y: sCy },
|
|
351
|
+
p2: { x: tRight, y: tCy },
|
|
352
|
+
dir: 'horizontal-left',
|
|
353
|
+
dist: Math.hypot(sLeft - tRight, tCy - sCy)
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 2. Dikey karşılıklı kenar adayı (Alt -> Üst veya Üst -> Alt)
|
|
358
|
+
let vPair;
|
|
359
|
+
if (dy >= 0) {
|
|
360
|
+
// Hedef altta: Kaynak alt kenarından hedefin üst kenarına
|
|
361
|
+
vPair = {
|
|
362
|
+
p1: { x: sCx, y: sBottom },
|
|
363
|
+
p2: { x: tCx, y: tTop },
|
|
364
|
+
dir: 'vertical-down',
|
|
365
|
+
dist: Math.hypot(tCx - sCx, tTop - sBottom)
|
|
366
|
+
};
|
|
367
|
+
} else {
|
|
368
|
+
// Hedef üstte: Kaynak üst kenarından hedefin alt kenarına
|
|
369
|
+
vPair = {
|
|
370
|
+
p1: { x: sCx, y: sTop },
|
|
371
|
+
p2: { x: tCx, y: tBottom },
|
|
372
|
+
dir: 'vertical-up',
|
|
373
|
+
dist: Math.hypot(tCx - sCx, sTop - tBottom)
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Açık yön tayini:
|
|
378
|
+
// Sütunlar şeklinde yan yana dizilimlerde (kaynak sağ kenarı <= hedef sol kenarı) yatay bağlantı esastır
|
|
379
|
+
const isClearlyHorizontal = (sRight <= tLeft || tRight <= sLeft) && Math.abs(dx) >= Math.abs(dy);
|
|
380
|
+
const isClearlyVertical = (sBottom <= tTop || tBottom <= sTop) && Math.abs(dy) >= Math.abs(dx);
|
|
381
|
+
|
|
382
|
+
let chosen;
|
|
383
|
+
if (isClearlyHorizontal && !isClearlyVertical) {
|
|
384
|
+
chosen = hPair;
|
|
385
|
+
} else if (isClearlyVertical && !isClearlyHorizontal) {
|
|
386
|
+
chosen = vPair;
|
|
387
|
+
} else {
|
|
388
|
+
// Kenarlar arası gerçek Öklid mesafesi en kısa olan optimum karşılıklı kenar çiftini seç
|
|
389
|
+
chosen = (hPair.dist <= vPair.dist) ? hPair : vPair;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return { p1: chosen.p1, p2: chosen.p2, dir: chosen.dir };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
drawConnectionLine(source, target, dragId) {
|
|
396
|
+
// Nesnelerin ortasından değil karşılıklı kenarlarından bağla
|
|
397
|
+
const { p1, p2, dir } = this.getFacingAnchorPoints(source, target);
|
|
398
|
+
|
|
399
|
+
const deltaX = Math.abs(p2.x - p1.x);
|
|
400
|
+
const deltaY = Math.abs(p2.y - p1.y);
|
|
401
|
+
|
|
402
|
+
let cx1, cy1, cx2, cy2;
|
|
403
|
+
|
|
404
|
+
if (dir === 'horizontal-right') {
|
|
405
|
+
const curvature = Math.max(30, Math.min(100, Math.max(deltaX * 0.5, 40)));
|
|
406
|
+
cx1 = p1.x + curvature;
|
|
407
|
+
cy1 = p1.y;
|
|
408
|
+
cx2 = p2.x - curvature;
|
|
409
|
+
cy2 = p2.y;
|
|
410
|
+
} else if (dir === 'horizontal-left') {
|
|
411
|
+
const curvature = Math.max(30, Math.min(100, Math.max(deltaX * 0.5, 40)));
|
|
412
|
+
cx1 = p1.x - curvature;
|
|
413
|
+
cy1 = p1.y;
|
|
414
|
+
cx2 = p2.x + curvature;
|
|
415
|
+
cy2 = p2.y;
|
|
416
|
+
} else if (dir === 'vertical-down') {
|
|
417
|
+
const curvature = Math.max(30, Math.min(100, Math.max(deltaY * 0.5, 40)));
|
|
418
|
+
cx1 = p1.x;
|
|
419
|
+
cy1 = p1.y + curvature;
|
|
420
|
+
cx2 = p2.x;
|
|
421
|
+
cy2 = p2.y - curvature;
|
|
422
|
+
} else { // 'vertical-up'
|
|
423
|
+
const curvature = Math.max(30, Math.min(100, Math.max(deltaY * 0.5, 40)));
|
|
424
|
+
cx1 = p1.x;
|
|
425
|
+
cy1 = p1.y - curvature;
|
|
426
|
+
cx2 = p2.x;
|
|
427
|
+
cy2 = p2.y + curvature;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const midX = (p1.x + p2.x) / 2;
|
|
431
|
+
const midY = (p1.y + p2.y) / 2;
|
|
432
|
+
|
|
433
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
434
|
+
g.setAttribute('class', 'ak-connection-group');
|
|
435
|
+
|
|
436
|
+
// Arka plan kalın yol (tıklamayı kolaylaştırmak için)
|
|
437
|
+
const hitPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
438
|
+
hitPath.setAttribute('d', `M ${p1.x} ${p1.y} C ${cx1} ${cy1}, ${cx2} ${cy2}, ${p2.x} ${p2.y}`);
|
|
439
|
+
hitPath.setAttribute('class', 'ak-connection-hitbox');
|
|
440
|
+
|
|
441
|
+
// Görsel renkli çizgi
|
|
442
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
443
|
+
path.setAttribute('d', `M ${p1.x} ${p1.y} C ${cx1} ${cy1}, ${cx2} ${cy2}, ${p2.x} ${p2.y}`);
|
|
444
|
+
path.setAttribute('class', 'ak-connection-path');
|
|
445
|
+
|
|
446
|
+
// Başlangıç ve bitiş noktası daireleri
|
|
447
|
+
const startCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
448
|
+
startCircle.setAttribute('cx', p1.x);
|
|
449
|
+
startCircle.setAttribute('cy', p1.y);
|
|
450
|
+
startCircle.setAttribute('r', '5');
|
|
451
|
+
startCircle.setAttribute('class', 'ak-node-circle start');
|
|
452
|
+
|
|
453
|
+
const endCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
454
|
+
endCircle.setAttribute('cx', p2.x);
|
|
455
|
+
endCircle.setAttribute('cy', p2.y);
|
|
456
|
+
endCircle.setAttribute('r', '6');
|
|
457
|
+
endCircle.setAttribute('class', 'ak-node-circle end');
|
|
458
|
+
|
|
459
|
+
// Orta rozet / Silme butonu (foreignObject ile HTML butonu - tam merkezlenmiş)
|
|
460
|
+
const fo = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
|
|
461
|
+
fo.setAttribute('x', midX - 13);
|
|
462
|
+
fo.setAttribute('y', midY - 13);
|
|
463
|
+
fo.setAttribute('width', '26');
|
|
464
|
+
fo.setAttribute('height', '26');
|
|
465
|
+
|
|
466
|
+
const btn = document.createElement('button');
|
|
467
|
+
btn.className = 'ak-unlink-btn';
|
|
468
|
+
btn.innerHTML = '✕';
|
|
469
|
+
btn.title = 'Eşleştirmeyi Kaldır';
|
|
470
|
+
btn.onclick = (e) => {
|
|
471
|
+
e.stopPropagation();
|
|
472
|
+
this.removeAnswer(dragId);
|
|
473
|
+
};
|
|
474
|
+
fo.appendChild(btn);
|
|
475
|
+
|
|
476
|
+
g.appendChild(hitPath);
|
|
477
|
+
g.appendChild(path);
|
|
478
|
+
g.appendChild(startCircle);
|
|
479
|
+
g.appendChild(endCircle);
|
|
480
|
+
g.appendChild(fo);
|
|
481
|
+
|
|
482
|
+
this.svgConnections.appendChild(g);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
clearSvg() {
|
|
486
|
+
if (this.svgConnections) {
|
|
487
|
+
this.svgConnections.innerHTML = '';
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Global namespace
|
|
493
|
+
window.DND = window.DND || {};
|
|
494
|
+
window.DND.AnswerKeyManager = AnswerKeyManager;
|