@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.
@@ -0,0 +1,295 @@
1
+ /**
2
+ * StageManager - Mantıksal koordinat sistemi (1200x675), yakınlaştırma,
3
+ * responsive ölçeklendirme, ızgara (grid) ve hizalama kılavuzlarını yönetir.
4
+ */
5
+ class StageManager {
6
+ constructor(core, containerEl, stageEl, options = {}) {
7
+ this.core = core;
8
+ this.container = containerEl;
9
+ this.stage = stageEl;
10
+ this.stageScaler = null;
11
+ this.stageContainer = null;
12
+ this.svgOverlay = stageEl ? stageEl.querySelector('.stage-svg-overlay') : null;
13
+ this.signal = options.signal;
14
+
15
+ this.logicalWidth = 1200;
16
+ this.logicalHeight = 675;
17
+
18
+ this.zoomLevel = 1.0; // 0.4 - 2.5
19
+ this.currentScale = 1.0;
20
+ this.showGrid = true;
21
+ this.snapToGrid = false;
22
+ this.gridSize = 20;
23
+
24
+ this.guidelines = []; // Aktif hizalama çizgileri
25
+
26
+ this.init();
27
+ }
28
+
29
+ init() {
30
+ this.setupStageWrapper();
31
+ this.setupWheelZoom();
32
+ this.setupResizeObserver();
33
+ this.renderGrid();
34
+ this.updateDimensions();
35
+
36
+ this.core.on('stageChanged', () => this.applyStageStyles());
37
+ this.core.on('projectLoaded', (p) => {
38
+ this.logicalWidth = p.project.stage.width || 1200;
39
+ this.logicalHeight = p.project.stage.height || 675;
40
+ this.applyStageStyles();
41
+ this.fitToScreen();
42
+ });
43
+ }
44
+
45
+ setupStageWrapper() {
46
+ if (!this.stage) return;
47
+ let scaler = this.stage.closest ? this.stage.closest('.stage-scaler') : null;
48
+ if (!scaler && this.stage.parentElement && this.stage.parentElement.classList.contains('stage-scaler')) {
49
+ scaler = this.stage.parentElement;
50
+ }
51
+ if (!scaler) {
52
+ let container = this.container ? this.container.querySelector('.stage-container') : null;
53
+ if (!container) {
54
+ container = document.createElement('div');
55
+ container.className = 'stage-container';
56
+ container.id = 'stage-container';
57
+ if (this.container && this.stage.parentNode) {
58
+ this.stage.parentNode.insertBefore(container, this.stage);
59
+ }
60
+ }
61
+ scaler = document.createElement('div');
62
+ scaler.className = 'stage-scaler';
63
+ scaler.id = 'stage-scaler';
64
+ scaler.appendChild(this.stage);
65
+ if (container) {
66
+ container.appendChild(scaler);
67
+ }
68
+ }
69
+ this.stageScaler = scaler;
70
+ this.stageContainer = scaler ? scaler.parentElement : null;
71
+ }
72
+
73
+ setupWheelZoom() {
74
+ if (!this.container) return;
75
+ const onWheel = (e) => {
76
+ if (e.ctrlKey || e.metaKey) {
77
+ e.preventDefault();
78
+ const delta = -e.deltaY;
79
+ const step = Math.sign(delta) * 0.1;
80
+ this.setZoom(Math.round((this.zoomLevel + step) * 100) / 100);
81
+ }
82
+ };
83
+ const opts = this.signal ? { passive: false, signal: this.signal } : { passive: false };
84
+ this.container.addEventListener('wheel', onWheel, opts);
85
+ }
86
+
87
+ setupResizeObserver() {
88
+ if (window.ResizeObserver) {
89
+ this.resizeObserver = new ResizeObserver(() => {
90
+ this.updateScale();
91
+ });
92
+ this.resizeObserver.observe(this.container);
93
+ } else {
94
+ window.addEventListener('resize', () => this.updateScale(), this.signal ? { signal: this.signal } : undefined);
95
+ }
96
+ }
97
+
98
+ updateDimensions() {
99
+ this.stage.style.width = `${this.logicalWidth}px`;
100
+ this.stage.style.height = `${this.logicalHeight}px`;
101
+ if (this.svgOverlay) {
102
+ this.svgOverlay.setAttribute('viewBox', `0 0 ${this.logicalWidth} ${this.logicalHeight}`);
103
+ this.svgOverlay.setAttribute('width', this.logicalWidth);
104
+ this.svgOverlay.setAttribute('height', this.logicalHeight);
105
+ }
106
+ this.applyStageStyles();
107
+ this.updateScale();
108
+ }
109
+
110
+ applyStageStyles() {
111
+ const stageData = this.core.data.stage;
112
+ this.stage.style.backgroundColor = stageData.backgroundColor || '#ffffff';
113
+ if (stageData.backgroundImage) {
114
+ this.stage.style.backgroundImage = `url("${stageData.backgroundImage}")`;
115
+ this.stage.style.backgroundSize = 'contain';
116
+ this.stage.style.backgroundPosition = 'center';
117
+ this.stage.style.backgroundRepeat = 'no-repeat';
118
+ } else {
119
+ this.stage.style.backgroundImage = 'none';
120
+ }
121
+ }
122
+
123
+ updateScale() {
124
+ if (!this.container || !this.stage) return;
125
+ if (!this.stageScaler) {
126
+ this.setupStageWrapper();
127
+ }
128
+
129
+ const containerRect = this.container.getBoundingClientRect();
130
+ const padding = 80;
131
+ const availableW = Math.max(containerRect.width - padding, 200);
132
+ const availableH = Math.max(containerRect.height - padding, 200);
133
+
134
+ const baseScale = Math.min(
135
+ availableW / this.logicalWidth,
136
+ availableH / this.logicalHeight
137
+ );
138
+
139
+ const hasScroll = this.container.scrollWidth > this.container.clientWidth || this.container.scrollHeight > this.container.clientHeight;
140
+ let focusRatioX = 0.5;
141
+ let focusRatioY = 0.5;
142
+ if (hasScroll && this.container.scrollWidth > 0 && this.container.scrollHeight > 0) {
143
+ focusRatioX = (this.container.scrollLeft + this.container.clientWidth / 2) / this.container.scrollWidth;
144
+ focusRatioY = (this.container.scrollTop + this.container.clientHeight / 2) / this.container.scrollHeight;
145
+ }
146
+
147
+ this.currentScale = baseScale * this.zoomLevel;
148
+
149
+ const scaledW = Math.round(this.logicalWidth * this.currentScale);
150
+ const scaledH = Math.round(this.logicalHeight * this.currentScale);
151
+
152
+ if (this.stageScaler) {
153
+ this.stageScaler.style.width = `${scaledW}px`;
154
+ this.stageScaler.style.height = `${scaledH}px`;
155
+ }
156
+
157
+ this.stage.style.width = `${this.logicalWidth}px`;
158
+ this.stage.style.height = `${this.logicalHeight}px`;
159
+ this.stage.style.transform = `scale(${this.currentScale})`;
160
+ this.stage.style.transformOrigin = 'top left';
161
+
162
+ if (this.zoomLevel > 1.0) {
163
+ if (hasScroll) {
164
+ requestAnimationFrame(() => {
165
+ if (this.container) {
166
+ this.container.scrollLeft = Math.max(0, focusRatioX * this.container.scrollWidth - this.container.clientWidth / 2);
167
+ this.container.scrollTop = Math.max(0, focusRatioY * this.container.scrollHeight - this.container.clientHeight / 2);
168
+ }
169
+ });
170
+ }
171
+ } else {
172
+ this.container.scrollLeft = 0;
173
+ this.container.scrollTop = 0;
174
+ }
175
+
176
+ this.core.emit('stageScaleChanged', {
177
+ scale: this.currentScale,
178
+ zoom: this.zoomLevel
179
+ });
180
+ }
181
+
182
+ setZoom(zoom) {
183
+ this.zoomLevel = Math.max(0.4, Math.min(2.5, Math.round(zoom * 100) / 100));
184
+ this.updateScale();
185
+ }
186
+
187
+ zoomIn() {
188
+ this.setZoom(this.zoomLevel + 0.15);
189
+ }
190
+
191
+ zoomOut() {
192
+ this.setZoom(this.zoomLevel - 0.15);
193
+ }
194
+
195
+ resetZoom() {
196
+ this.fitToScreen();
197
+ }
198
+
199
+ fitToScreen() {
200
+ this.zoomLevel = 1.0;
201
+ this.updateScale();
202
+ if (this.container) {
203
+ this.container.scrollLeft = 0;
204
+ this.container.scrollTop = 0;
205
+ }
206
+ }
207
+
208
+ destroy() {
209
+ if (this.resizeObserver) {
210
+ this.resizeObserver.disconnect();
211
+ }
212
+ }
213
+
214
+ // Koordinat Dönüşümleri
215
+ clientToStage(clientX, clientY) {
216
+ const rect = this.stage.getBoundingClientRect();
217
+ const scale = rect.width / this.logicalWidth;
218
+ return {
219
+ x: (clientX - rect.left) / scale,
220
+ y: (clientY - rect.top) / scale
221
+ };
222
+ }
223
+
224
+ stageToClient(stageX, stageY) {
225
+ const rect = this.stage.getBoundingClientRect();
226
+ const scale = rect.width / this.logicalWidth;
227
+ return {
228
+ clientX: rect.left + stageX * scale,
229
+ clientY: rect.top + stageY * scale
230
+ };
231
+ }
232
+
233
+ // Izgaraya yapışma hesabı
234
+ snap(val) {
235
+ if (!this.snapToGrid) return Math.round(val);
236
+ return Math.round(val / this.gridSize) * this.gridSize;
237
+ }
238
+
239
+ toggleGrid(forcedState) {
240
+ this.showGrid = forcedState !== undefined ? forcedState : !this.showGrid;
241
+ if (this.showGrid) {
242
+ this.stage.classList.add('grid-visible');
243
+ } else {
244
+ this.stage.classList.remove('grid-visible');
245
+ }
246
+ return this.showGrid;
247
+ }
248
+
249
+ toggleSnap(forcedState) {
250
+ this.snapToGrid = forcedState !== undefined ? forcedState : !this.snapToGrid;
251
+ return this.snapToGrid;
252
+ }
253
+
254
+ renderGrid() {
255
+ if (this.showGrid) {
256
+ this.stage.classList.add('grid-visible');
257
+ }
258
+ }
259
+
260
+ // Akıllı Kılavuz Çizgileri (Hizalama Rehberi)
261
+ showGuidelines(hLines = [], vLines = []) {
262
+ let guideLayer = this.stage.querySelector('.stage-guidelines');
263
+ if (!guideLayer) {
264
+ guideLayer = document.createElement('div');
265
+ guideLayer.className = 'stage-guidelines';
266
+ this.stage.appendChild(guideLayer);
267
+ }
268
+ guideLayer.innerHTML = '';
269
+
270
+ hLines.forEach(y => {
271
+ const line = document.createElement('div');
272
+ line.className = 'guideline guideline-h';
273
+ line.style.top = `${y}px`;
274
+ guideLayer.appendChild(line);
275
+ });
276
+
277
+ vLines.forEach(x => {
278
+ const line = document.createElement('div');
279
+ line.className = 'guideline guideline-v';
280
+ line.style.left = `${x}px`;
281
+ guideLayer.appendChild(line);
282
+ });
283
+ }
284
+
285
+ clearGuidelines() {
286
+ const guideLayer = this.stage.querySelector('.stage-guidelines');
287
+ if (guideLayer) {
288
+ guideLayer.innerHTML = '';
289
+ }
290
+ }
291
+ }
292
+
293
+ // Global namespace
294
+ window.DND = window.DND || {};
295
+ window.DND.StageManager = StageManager;