@mindexec/cli 0.2.456 → 0.2.457
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 +77 -77
- package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
- package/scripts/remote-fleet-render-smoke.mjs +24 -46
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +253 -261
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +1326 -1323
- package/wwwroot/_framework/{MindExecution.Core.glgqkah960.dll → MindExecution.Core.speado072l.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Kernel.1nb0zfmie1.dll → MindExecution.Kernel.mjy31ssdac.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Admin.4pdpv5q991.dll → MindExecution.Plugins.Admin.vbjjmdaao2.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Business.jpl2k4nnhh.dll → MindExecution.Plugins.Business.0cr5jc6wqe.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Concept.rissr3n74r.dll → MindExecution.Plugins.Concept.m7p666e0jd.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Directory.tkopeqv16k.dll → MindExecution.Plugins.Directory.kmdtirnljl.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.jmjftycwo4.dll → MindExecution.Plugins.PlanMaster.m1pfeozacj.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.YouTube.drj98vf3ti.dll → MindExecution.Plugins.YouTube.nvsc9q4s6b.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Shared.cbieupjmfp.dll → MindExecution.Shared.9jaznballd.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Web.xl8ygqgy0i.dll → MindExecution.Web.k5jxwawryl.dll} +0 -0
- package/wwwroot/_framework/blazor.boot.json +21 -21
- package/wwwroot/index.html +1 -1
- package/wwwroot/service-worker-assets.js +25 -25
- package/wwwroot/service-worker.js +1 -1
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
// File name: X:\Dev@Work\MindExecution\Client\wwwroot\js\mind-map-pipeline.js
|
|
2
|
-
// [Refactor] Owns the async rendering queue and node resizing logic.
|
|
1
|
+
// File name: X:\Dev@Work\MindExecution\Client\wwwroot\js\mind-map-pipeline.js
|
|
2
|
+
// [Refactor] Owns the async rendering queue and node resizing logic.
|
|
3
3
|
(function () {
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
const CSS3D_WRAPPER_TRANSITION = 'box-shadow .22s ease, filter .22s ease';
|
|
7
7
|
const CSS3D_WRAPPER_WEBKIT_TRANSITION = '-webkit-box-shadow .22s ease, -webkit-filter .22s ease';
|
|
8
|
-
|
|
9
|
-
// ▼▼▼ [Changed] Add state variables to switch from debounce to throttle ▼▼▼
|
|
10
|
-
const debounceTimers = new Map();
|
|
11
|
-
const scrollUpdateThrottleTimers = new Map();
|
|
12
|
-
const lastScrollCall = new Map();
|
|
13
|
-
const resizeThrottleTimers = new Map(); // [New] throttle for texture updates during resize
|
|
8
|
+
|
|
9
|
+
// ▼▼▼ [Changed] Add state variables to switch from debounce to throttle ▼▼▼
|
|
10
|
+
const debounceTimers = new Map();
|
|
11
|
+
const scrollUpdateThrottleTimers = new Map();
|
|
12
|
+
const lastScrollCall = new Map();
|
|
13
|
+
const resizeThrottleTimers = new Map(); // [New] throttle for texture updates during resize
|
|
14
14
|
const lastResizeUpdateCall = new Map(); // [New] last resize-update timestamp
|
|
15
15
|
const THROTTLE_INTERVAL = 16; // 16ms -> ~60fps
|
|
16
16
|
const RESIZE_HANDLE_SIZE = 16;
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
const CSV_TABLE_MIN_WIDTH = 320;
|
|
22
22
|
const CSV_TABLE_MIN_HEIGHT = 180;
|
|
23
23
|
const REMOTE_FLEET_SEMANTIC_TYPE = 'RemoteFleetMonitor';
|
|
24
|
-
const REMOTE_FLEET_DISPLAY_NAME = '
|
|
25
|
-
const REMOTE_FLEET_LEGACY_DISPLAY_NAME = '
|
|
24
|
+
const REMOTE_FLEET_DISPLAY_NAME = 'Remote Work Console';
|
|
25
|
+
const REMOTE_FLEET_LEGACY_DISPLAY_NAME = 'Multi Desktop Monitor';
|
|
26
|
+
const REMOTE_FLEET_LEGACY_DISPLAY_NAME_V0 = 'Remote Fleet Monitor';
|
|
26
27
|
const REMOTE_FLEET_MONITOR_MIN_WIDTH = 520;
|
|
27
28
|
const REMOTE_FLEET_MONITOR_MIN_HEIGHT = 320;
|
|
28
29
|
|
|
@@ -44,7 +45,9 @@
|
|
|
44
45
|
const contentType = getNodeContentType(nodeModel);
|
|
45
46
|
const title = String(nodeModel?.prompt ?? nodeModel?.Prompt ?? '').trim();
|
|
46
47
|
return contentType === 'templatelauncher'
|
|
47
|
-
&& (title === REMOTE_FLEET_DISPLAY_NAME
|
|
48
|
+
&& (title === REMOTE_FLEET_DISPLAY_NAME
|
|
49
|
+
|| title === REMOTE_FLEET_LEGACY_DISPLAY_NAME
|
|
50
|
+
|| title === REMOTE_FLEET_LEGACY_DISPLAY_NAME_V0);
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
function isCsvTableNodeModel(nodeModel) {
|
|
@@ -145,7 +148,7 @@
|
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
// ▼▼▼ [Perf] Render queue (time-sliced) ▼▼▼
|
|
148
|
-
const renderQueue = [];
|
|
151
|
+
const renderQueue = [];
|
|
149
152
|
const renderQueueSet = new Set();
|
|
150
153
|
let renderQueueScheduled = false;
|
|
151
154
|
let renderModuleRef = null;
|
|
@@ -154,52 +157,52 @@
|
|
|
154
157
|
let renderSeq = 0;
|
|
155
158
|
let renderQueueSortCount = 0;
|
|
156
159
|
let renderQueueMaxLength = 0;
|
|
157
|
-
|
|
158
|
-
function computeRenderPriority(module, nodeId) {
|
|
159
|
-
const entry = module?.nodeObjectsById?.get(nodeId);
|
|
160
|
-
if (!entry) return 1000000;
|
|
161
|
-
|
|
162
|
-
const obj = entry.glObject || entry.cssObject;
|
|
163
|
-
const visible = obj?.visible === true;
|
|
164
|
-
const pos = obj?.position || { x: 0, y: 0 };
|
|
165
|
-
const cam = module.camera?.position || { x: 0, y: 0 };
|
|
166
|
-
|
|
167
|
-
const dx = pos.x - cam.x;
|
|
168
|
-
const dy = pos.y - cam.y;
|
|
169
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
170
|
-
|
|
171
|
-
const isSelected = module.multiSelectedNodeIds?.has(nodeId) || module.selectedNodeIdJs === nodeId;
|
|
172
|
-
|
|
173
|
-
// 낮은 값이 우선: 보이는 노드 > 선택 노드 > 카메라 근접
|
|
174
|
-
let priority = dist;
|
|
175
|
-
if (!visible) priority += 100000;
|
|
176
|
-
if (isSelected) priority -= 5000;
|
|
177
|
-
|
|
178
|
-
return priority;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function enqueueRender(module, nodeId) {
|
|
182
|
-
if (!nodeId) return;
|
|
183
|
-
renderModuleRef = module;
|
|
160
|
+
|
|
161
|
+
function computeRenderPriority(module, nodeId) {
|
|
162
|
+
const entry = module?.nodeObjectsById?.get(nodeId);
|
|
163
|
+
if (!entry) return 1000000;
|
|
164
|
+
|
|
165
|
+
const obj = entry.glObject || entry.cssObject;
|
|
166
|
+
const visible = obj?.visible === true;
|
|
167
|
+
const pos = obj?.position || { x: 0, y: 0 };
|
|
168
|
+
const cam = module.camera?.position || { x: 0, y: 0 };
|
|
169
|
+
|
|
170
|
+
const dx = pos.x - cam.x;
|
|
171
|
+
const dy = pos.y - cam.y;
|
|
172
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
173
|
+
|
|
174
|
+
const isSelected = module.multiSelectedNodeIds?.has(nodeId) || module.selectedNodeIdJs === nodeId;
|
|
175
|
+
|
|
176
|
+
// 낮은 값이 우선: 보이는 노드 > 선택 노드 > 카메라 근접
|
|
177
|
+
let priority = dist;
|
|
178
|
+
if (!visible) priority += 100000;
|
|
179
|
+
if (isSelected) priority -= 5000;
|
|
180
|
+
|
|
181
|
+
return priority;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function enqueueRender(module, nodeId) {
|
|
185
|
+
if (!nodeId) return;
|
|
186
|
+
renderModuleRef = module;
|
|
184
187
|
if (!renderQueueSet.has(nodeId)) {
|
|
185
188
|
renderQueue.push({ nodeId, priority: computeRenderPriority(module, nodeId), seq: renderSeq++ });
|
|
186
189
|
renderQueueSet.add(nodeId);
|
|
187
190
|
renderQueueDirty = true;
|
|
188
191
|
renderQueueMaxLength = Math.max(renderQueueMaxLength, renderQueue.length);
|
|
189
|
-
}
|
|
190
|
-
scheduleRenderQueue();
|
|
191
|
-
}
|
|
192
|
-
|
|
192
|
+
}
|
|
193
|
+
scheduleRenderQueue();
|
|
194
|
+
}
|
|
195
|
+
|
|
193
196
|
function scheduleRenderQueue() {
|
|
194
197
|
if (renderQueueScheduled) return;
|
|
195
198
|
renderQueueScheduled = true;
|
|
196
199
|
scheduleNonVisualFrame(() => processRenderQueueBatch());
|
|
197
200
|
}
|
|
198
|
-
|
|
199
|
-
async function processRenderQueueBatch() {
|
|
200
|
-
const start = performance.now();
|
|
201
|
-
|
|
202
|
-
// 우선순위 정렬 (가시/선택/거리)
|
|
201
|
+
|
|
202
|
+
async function processRenderQueueBatch() {
|
|
203
|
+
const start = performance.now();
|
|
204
|
+
|
|
205
|
+
// 우선순위 정렬 (가시/선택/거리)
|
|
203
206
|
if (renderQueueDirty) {
|
|
204
207
|
if (renderQueue.length > 1) {
|
|
205
208
|
renderQueue.sort((a, b) => (b.priority - a.priority) || (b.seq - a.seq));
|
|
@@ -210,64 +213,64 @@
|
|
|
210
213
|
|
|
211
214
|
while (renderQueue.length > 0) {
|
|
212
215
|
const item = renderQueue.pop();
|
|
213
|
-
const nodeId = item.nodeId;
|
|
214
|
-
renderQueueSet.delete(nodeId);
|
|
215
|
-
if (renderModuleRef) {
|
|
216
|
-
await processRenderQueue(renderModuleRef, nodeId);
|
|
217
|
-
}
|
|
218
|
-
if ((performance.now() - start) >= RENDER_BUDGET_MS) {
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
renderQueueScheduled = false;
|
|
224
|
-
if (renderQueue.length > 0) {
|
|
225
|
-
scheduleRenderQueue();
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
// ▲▲▲ [Perf] ▲▲▲
|
|
229
|
-
|
|
230
|
-
// ▼▼▼ [New] Chunked-node virtual scroll constants ▼▼▼
|
|
231
|
-
const CHUNK_LINE_HEIGHT = 21; // 14px font * 1.5 line-height (matches plain text rendering)
|
|
232
|
-
const CHUNK_SIZE = 100; // lines per chunk (matches C# TextChunkService)
|
|
233
|
-
// ▲▲▲ [New] ▲▲▲
|
|
234
|
-
|
|
235
|
-
// ▼▼▼ [Perf] Deferred disposal to avoid GC/driver spikes ▼▼▼
|
|
216
|
+
const nodeId = item.nodeId;
|
|
217
|
+
renderQueueSet.delete(nodeId);
|
|
218
|
+
if (renderModuleRef) {
|
|
219
|
+
await processRenderQueue(renderModuleRef, nodeId);
|
|
220
|
+
}
|
|
221
|
+
if ((performance.now() - start) >= RENDER_BUDGET_MS) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
renderQueueScheduled = false;
|
|
227
|
+
if (renderQueue.length > 0) {
|
|
228
|
+
scheduleRenderQueue();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// ▲▲▲ [Perf] ▲▲▲
|
|
232
|
+
|
|
233
|
+
// ▼▼▼ [New] Chunked-node virtual scroll constants ▼▼▼
|
|
234
|
+
const CHUNK_LINE_HEIGHT = 21; // 14px font * 1.5 line-height (matches plain text rendering)
|
|
235
|
+
const CHUNK_SIZE = 100; // lines per chunk (matches C# TextChunkService)
|
|
236
|
+
// ▲▲▲ [New] ▲▲▲
|
|
237
|
+
|
|
238
|
+
// ▼▼▼ [Perf] Deferred disposal to avoid GC/driver spikes ▼▼▼
|
|
236
239
|
const disposeQueue = [];
|
|
237
240
|
let disposeQueueHead = 0;
|
|
238
|
-
let disposeScheduled = false;
|
|
239
|
-
const DISPOSE_BUDGET_MS = 6; // soft budget per frame
|
|
240
|
-
|
|
241
|
-
function enqueueDispose(resource) {
|
|
242
|
-
if (!resource || typeof resource.dispose !== 'function') return;
|
|
243
|
-
disposeQueue.push(resource);
|
|
244
|
-
scheduleDispose();
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function scheduleDispose() {
|
|
248
|
-
if (disposeScheduled) return;
|
|
249
|
-
disposeScheduled = true;
|
|
241
|
+
let disposeScheduled = false;
|
|
242
|
+
const DISPOSE_BUDGET_MS = 6; // soft budget per frame
|
|
243
|
+
|
|
244
|
+
function enqueueDispose(resource) {
|
|
245
|
+
if (!resource || typeof resource.dispose !== 'function') return;
|
|
246
|
+
disposeQueue.push(resource);
|
|
247
|
+
scheduleDispose();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function scheduleDispose() {
|
|
251
|
+
if (disposeScheduled) return;
|
|
252
|
+
disposeScheduled = true;
|
|
250
253
|
if (typeof requestIdleCallback === 'function') {
|
|
251
254
|
requestIdleCallback(processDisposeQueue);
|
|
252
255
|
} else {
|
|
253
256
|
scheduleNonVisualFrame(() => processDisposeQueue());
|
|
254
257
|
}
|
|
255
258
|
}
|
|
256
|
-
|
|
257
|
-
function processDisposeQueue(deadline) {
|
|
258
|
-
const start = performance.now();
|
|
259
|
+
|
|
260
|
+
function processDisposeQueue(deadline) {
|
|
261
|
+
const start = performance.now();
|
|
259
262
|
while (disposeQueueHead < disposeQueue.length) {
|
|
260
263
|
const item = disposeQueue[disposeQueueHead++];
|
|
261
|
-
try { item.dispose(); } catch { }
|
|
262
|
-
|
|
263
|
-
if (deadline && typeof deadline.timeRemaining === 'function') {
|
|
264
|
-
if (deadline.timeRemaining() < 1) break;
|
|
265
|
-
} else if ((performance.now() - start) >= DISPOSE_BUDGET_MS) {
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
disposeScheduled = false;
|
|
264
|
+
try { item.dispose(); } catch { }
|
|
265
|
+
|
|
266
|
+
if (deadline && typeof deadline.timeRemaining === 'function') {
|
|
267
|
+
if (deadline.timeRemaining() < 1) break;
|
|
268
|
+
} else if ((performance.now() - start) >= DISPOSE_BUDGET_MS) {
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
disposeScheduled = false;
|
|
271
274
|
if (disposeQueueHead >= disposeQueue.length) {
|
|
272
275
|
disposeQueue.length = 0;
|
|
273
276
|
disposeQueueHead = 0;
|
|
@@ -278,16 +281,16 @@
|
|
|
278
281
|
if (disposeQueueHead < disposeQueue.length) {
|
|
279
282
|
scheduleDispose();
|
|
280
283
|
}
|
|
281
|
-
}
|
|
282
|
-
// ▲▲▲ [Perf] ▲▲▲
|
|
283
|
-
|
|
284
|
+
}
|
|
285
|
+
// ▲▲▲ [Perf] ▲▲▲
|
|
286
|
+
|
|
284
287
|
function removeGlowMesh(group) {
|
|
285
288
|
if (!group?.getObjectByName) return null;
|
|
286
289
|
const glowMesh = group.getObjectByName('glow');
|
|
287
290
|
if (!glowMesh) return null;
|
|
288
|
-
|
|
289
|
-
group.remove(glowMesh);
|
|
290
|
-
enqueueDispose(glowMesh.geometry);
|
|
291
|
+
|
|
292
|
+
group.remove(glowMesh);
|
|
293
|
+
enqueueDispose(glowMesh.geometry);
|
|
291
294
|
enqueueDispose(glowMesh.material);
|
|
292
295
|
return null;
|
|
293
296
|
}
|
|
@@ -298,27 +301,27 @@
|
|
|
298
301
|
window.MindMapNodes.applyImageCoverUv(bodyMesh, width, height);
|
|
299
302
|
}
|
|
300
303
|
}
|
|
301
|
-
|
|
302
|
-
// ▼▼▼ [Changed] Geometry helper for rounded corners ▼▼▼
|
|
304
|
+
|
|
305
|
+
// ▼▼▼ [Changed] Geometry helper for rounded corners ▼▼▼
|
|
303
306
|
function createRoundedRectGeometry(width, height, radius) {
|
|
304
307
|
const shape = new THREE.Shape();
|
|
305
308
|
const x = -width / 2;
|
|
306
309
|
const y = -height / 2;
|
|
307
|
-
|
|
308
|
-
// Start at bottom-left, go clockwise
|
|
309
|
-
shape.moveTo(x, y + radius);
|
|
310
|
-
shape.quadraticCurveTo(x, y, x + radius, y);
|
|
311
|
-
|
|
312
|
-
shape.lineTo(x + width - radius, y);
|
|
313
|
-
shape.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
314
|
-
|
|
315
|
-
shape.lineTo(x + width, y + height - radius);
|
|
316
|
-
shape.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
317
|
-
|
|
318
|
-
shape.lineTo(x + radius, y + height);
|
|
319
|
-
shape.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
320
|
-
|
|
321
|
-
shape.lineTo(x, y + radius);
|
|
310
|
+
|
|
311
|
+
// Start at bottom-left, go clockwise
|
|
312
|
+
shape.moveTo(x, y + radius);
|
|
313
|
+
shape.quadraticCurveTo(x, y, x + radius, y);
|
|
314
|
+
|
|
315
|
+
shape.lineTo(x + width - radius, y);
|
|
316
|
+
shape.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
317
|
+
|
|
318
|
+
shape.lineTo(x + width, y + height - radius);
|
|
319
|
+
shape.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
320
|
+
|
|
321
|
+
shape.lineTo(x + radius, y + height);
|
|
322
|
+
shape.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
323
|
+
|
|
324
|
+
shape.lineTo(x, y + radius);
|
|
322
325
|
const points = shape.getPoints();
|
|
323
326
|
return new THREE.BufferGeometry().setFromPoints(points);
|
|
324
327
|
}
|
|
@@ -404,102 +407,102 @@
|
|
|
404
407
|
}
|
|
405
408
|
|
|
406
409
|
// ▼▼▼ [New] Update visible content for virtual scrolling ▼▼▼
|
|
407
|
-
function updateVisibleContent(model, lineHeight, chunkSize) {
|
|
408
|
-
if (!model.isChunkedText || !model._chunks) {
|
|
409
|
-
model._visibleContent = model.response || '';
|
|
410
|
-
model._visibleScrollOffset = model.scrollOffset || 0;
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const scrollOffset = model.scrollOffset || 0;
|
|
415
|
-
const nodeHeight = model.height || 400;
|
|
416
|
-
|
|
417
|
-
// Visible line range in the current viewport
|
|
418
|
-
const visibleStartLine = Math.floor(scrollOffset / lineHeight);
|
|
419
|
-
const visibleLineCount = Math.ceil(nodeHeight / lineHeight) + 5; // small buffer
|
|
420
|
-
|
|
421
|
-
// Chunk indices needed for that range
|
|
422
|
-
const startChunkIndex = Math.floor(visibleStartLine / chunkSize) * chunkSize;
|
|
423
|
-
const endChunkIndex = Math.floor((visibleStartLine + visibleLineCount) / chunkSize) * chunkSize;
|
|
424
|
-
|
|
425
|
-
// Merge only the chunks that intersect the current viewport
|
|
426
|
-
let visibleContent = '';
|
|
427
|
-
let firstChunkStart = -1;
|
|
428
|
-
let hasContent = false;
|
|
429
|
-
|
|
430
|
-
for (let chunkStart = startChunkIndex; chunkStart <= endChunkIndex; chunkStart += chunkSize) {
|
|
431
|
-
if (model._chunks[chunkStart]) {
|
|
432
|
-
if (firstChunkStart === -1) firstChunkStart = chunkStart;
|
|
433
|
-
visibleContent += model._chunks[chunkStart] + '\n';
|
|
434
|
-
hasContent = true;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// ▼▼▼ [Changed] If no chunks exist in range, use the nearest loaded chunk ▼▼▼
|
|
439
|
-
if (!hasContent) {
|
|
440
|
-
// Find the nearest loaded chunk
|
|
441
|
-
const loadedChunks = Object.keys(model._chunks).map(Number).sort((a, b) => a - b);
|
|
442
|
-
if (loadedChunks.length > 0) {
|
|
443
|
-
// Pick the chunk closest to the current position
|
|
444
|
-
let closestChunk = loadedChunks[0];
|
|
445
|
-
for (const chunkStart of loadedChunks) {
|
|
446
|
-
if (chunkStart <= startChunkIndex) {
|
|
447
|
-
closestChunk = chunkStart;
|
|
448
|
-
} else {
|
|
449
|
-
break;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
firstChunkStart = closestChunk;
|
|
453
|
-
visibleContent = model._chunks[closestChunk] + '\n';
|
|
454
|
-
|
|
455
|
-
// Add the next chunk as well if present
|
|
456
|
-
const nextChunk = closestChunk + chunkSize;
|
|
457
|
-
if (model._chunks[nextChunk]) {
|
|
458
|
-
visibleContent += model._chunks[nextChunk] + '\n';
|
|
459
|
-
}
|
|
460
|
-
} else {
|
|
461
|
-
// If no chunks exist at all, keep the previous response
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
// ▲▲▲ [Changed] ▲▲▲
|
|
466
|
-
|
|
467
|
-
if (firstChunkStart === -1) firstChunkStart = 0;
|
|
468
|
-
|
|
469
|
-
// Compute relative scroll offset from the first chunk
|
|
470
|
-
const firstChunkPixelOffset = firstChunkStart * lineHeight;
|
|
471
|
-
const relativeScrollOffset = scrollOffset - firstChunkPixelOffset;
|
|
472
|
-
|
|
473
|
-
model._visibleContent = visibleContent;
|
|
474
|
-
model._visibleScrollOffset = Math.max(0, relativeScrollOffset);
|
|
475
|
-
|
|
476
|
-
// ▼▼▼ [Changed] Do not overwrite response for chunk nodes (saved/restored via full file path) ▼▼▼
|
|
477
|
-
// Chunk nodes are restored based on SourceFilePath, so avoid mutating response.
|
|
478
|
-
// model.response = visibleContent; // removed
|
|
479
|
-
// ▲▲▲ [Changed] ▲▲▲
|
|
480
|
-
}
|
|
481
|
-
// ▲▲▲ [New] ▲▲▲
|
|
482
|
-
// ▲▲▲ [Changed] ▲▲▲
|
|
483
|
-
|
|
484
|
-
async function processRenderQueue(module, nodeId) {
|
|
485
|
-
// ▼▼▼ [Profiling] Start per-node render timing ▼▼▼
|
|
486
|
-
// console.time(`[Pipeline] 6. Render Node ${nodeId}`);
|
|
487
|
-
// ▲▲▲ [Profiling] ▲▲▲
|
|
488
|
-
|
|
489
|
-
// ▼▼▼ [Changed] module.nodeRenderLocks -> window.MindMapNodes.nodeRenderLocks ▼▼▼
|
|
490
|
-
if (window.MindMapNodes.nodeRenderLocks.get(nodeId)) {
|
|
491
|
-
// console.log(`[Pipeline] [Log Detail] Node ${nodeId} skipped: Render lock active.`);
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// ▼▼▼ [Changed] module.pendingNodeUpdates -> window.MindMapNodes.pendingNodeUpdates ▼▼▼
|
|
496
|
-
const updateData = window.MindMapNodes.pendingNodeUpdates.get(nodeId);
|
|
497
|
-
if (!updateData) {
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// console.log(`[Pipeline] [Log Detail] Node ${nodeId} processing queue.`);
|
|
502
|
-
|
|
410
|
+
function updateVisibleContent(model, lineHeight, chunkSize) {
|
|
411
|
+
if (!model.isChunkedText || !model._chunks) {
|
|
412
|
+
model._visibleContent = model.response || '';
|
|
413
|
+
model._visibleScrollOffset = model.scrollOffset || 0;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const scrollOffset = model.scrollOffset || 0;
|
|
418
|
+
const nodeHeight = model.height || 400;
|
|
419
|
+
|
|
420
|
+
// Visible line range in the current viewport
|
|
421
|
+
const visibleStartLine = Math.floor(scrollOffset / lineHeight);
|
|
422
|
+
const visibleLineCount = Math.ceil(nodeHeight / lineHeight) + 5; // small buffer
|
|
423
|
+
|
|
424
|
+
// Chunk indices needed for that range
|
|
425
|
+
const startChunkIndex = Math.floor(visibleStartLine / chunkSize) * chunkSize;
|
|
426
|
+
const endChunkIndex = Math.floor((visibleStartLine + visibleLineCount) / chunkSize) * chunkSize;
|
|
427
|
+
|
|
428
|
+
// Merge only the chunks that intersect the current viewport
|
|
429
|
+
let visibleContent = '';
|
|
430
|
+
let firstChunkStart = -1;
|
|
431
|
+
let hasContent = false;
|
|
432
|
+
|
|
433
|
+
for (let chunkStart = startChunkIndex; chunkStart <= endChunkIndex; chunkStart += chunkSize) {
|
|
434
|
+
if (model._chunks[chunkStart]) {
|
|
435
|
+
if (firstChunkStart === -1) firstChunkStart = chunkStart;
|
|
436
|
+
visibleContent += model._chunks[chunkStart] + '\n';
|
|
437
|
+
hasContent = true;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ▼▼▼ [Changed] If no chunks exist in range, use the nearest loaded chunk ▼▼▼
|
|
442
|
+
if (!hasContent) {
|
|
443
|
+
// Find the nearest loaded chunk
|
|
444
|
+
const loadedChunks = Object.keys(model._chunks).map(Number).sort((a, b) => a - b);
|
|
445
|
+
if (loadedChunks.length > 0) {
|
|
446
|
+
// Pick the chunk closest to the current position
|
|
447
|
+
let closestChunk = loadedChunks[0];
|
|
448
|
+
for (const chunkStart of loadedChunks) {
|
|
449
|
+
if (chunkStart <= startChunkIndex) {
|
|
450
|
+
closestChunk = chunkStart;
|
|
451
|
+
} else {
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
firstChunkStart = closestChunk;
|
|
456
|
+
visibleContent = model._chunks[closestChunk] + '\n';
|
|
457
|
+
|
|
458
|
+
// Add the next chunk as well if present
|
|
459
|
+
const nextChunk = closestChunk + chunkSize;
|
|
460
|
+
if (model._chunks[nextChunk]) {
|
|
461
|
+
visibleContent += model._chunks[nextChunk] + '\n';
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
// If no chunks exist at all, keep the previous response
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
469
|
+
|
|
470
|
+
if (firstChunkStart === -1) firstChunkStart = 0;
|
|
471
|
+
|
|
472
|
+
// Compute relative scroll offset from the first chunk
|
|
473
|
+
const firstChunkPixelOffset = firstChunkStart * lineHeight;
|
|
474
|
+
const relativeScrollOffset = scrollOffset - firstChunkPixelOffset;
|
|
475
|
+
|
|
476
|
+
model._visibleContent = visibleContent;
|
|
477
|
+
model._visibleScrollOffset = Math.max(0, relativeScrollOffset);
|
|
478
|
+
|
|
479
|
+
// ▼▼▼ [Changed] Do not overwrite response for chunk nodes (saved/restored via full file path) ▼▼▼
|
|
480
|
+
// Chunk nodes are restored based on SourceFilePath, so avoid mutating response.
|
|
481
|
+
// model.response = visibleContent; // removed
|
|
482
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
483
|
+
}
|
|
484
|
+
// ▲▲▲ [New] ▲▲▲
|
|
485
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
486
|
+
|
|
487
|
+
async function processRenderQueue(module, nodeId) {
|
|
488
|
+
// ▼▼▼ [Profiling] Start per-node render timing ▼▼▼
|
|
489
|
+
// console.time(`[Pipeline] 6. Render Node ${nodeId}`);
|
|
490
|
+
// ▲▲▲ [Profiling] ▲▲▲
|
|
491
|
+
|
|
492
|
+
// ▼▼▼ [Changed] module.nodeRenderLocks -> window.MindMapNodes.nodeRenderLocks ▼▼▼
|
|
493
|
+
if (window.MindMapNodes.nodeRenderLocks.get(nodeId)) {
|
|
494
|
+
// console.log(`[Pipeline] [Log Detail] Node ${nodeId} skipped: Render lock active.`);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ▼▼▼ [Changed] module.pendingNodeUpdates -> window.MindMapNodes.pendingNodeUpdates ▼▼▼
|
|
499
|
+
const updateData = window.MindMapNodes.pendingNodeUpdates.get(nodeId);
|
|
500
|
+
if (!updateData) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// console.log(`[Pipeline] [Log Detail] Node ${nodeId} processing queue.`);
|
|
505
|
+
|
|
503
506
|
const HANDLE_SIZE = RESIZE_HANDLE_SIZE;
|
|
504
507
|
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
505
508
|
if (!nodeEntry) {
|
|
@@ -531,24 +534,24 @@
|
|
|
531
534
|
model.IsLoading = !!updateData.isLoading;
|
|
532
535
|
}
|
|
533
536
|
const zoomLevel = 2;
|
|
534
|
-
|
|
535
|
-
// ▼▼▼ [Perf] CSS3D 지원 노드는 거리와 무관하게 텍스처 생성 스킵 ▼▼▼
|
|
536
|
-
const cameraZ = module.camera?.position?.z || 0;
|
|
537
|
-
const threshold = 4000;
|
|
537
|
+
|
|
538
|
+
// ▼▼▼ [Perf] CSS3D 지원 노드는 거리와 무관하게 텍스처 생성 스킵 ▼▼▼
|
|
539
|
+
const cameraZ = module.camera?.position?.z || 0;
|
|
540
|
+
const threshold = 4000;
|
|
538
541
|
const isHighMode = cameraZ < threshold;
|
|
539
542
|
const supportsCss3d = model.contentType === 'text' ||
|
|
540
543
|
model.contentType === 'note' ||
|
|
541
544
|
model.contentType === 'memo' ||
|
|
542
545
|
model.contentType === 'markdown' ||
|
|
543
546
|
model.contentType === 'code';
|
|
544
|
-
|
|
545
|
-
// ★ [Fix] currentType이 'CSS'이면 카메라 거리와 무관하게 텍스처 생성 스킵
|
|
546
|
-
const isCssMode = nodeEntry.currentType === 'CSS';
|
|
547
|
-
const shouldSkipForCss = supportsCss3d && (isHighMode || cameraZ >= threshold || isCssMode);
|
|
548
|
-
|
|
549
|
-
if (shouldSkipForCss) {
|
|
550
|
-
const prevWidth = model.width || 0;
|
|
551
|
-
const prevHeight = model.height || 0;
|
|
547
|
+
|
|
548
|
+
// ★ [Fix] currentType이 'CSS'이면 카메라 거리와 무관하게 텍스처 생성 스킵
|
|
549
|
+
const isCssMode = nodeEntry.currentType === 'CSS';
|
|
550
|
+
const shouldSkipForCss = supportsCss3d && (isHighMode || cameraZ >= threshold || isCssMode);
|
|
551
|
+
|
|
552
|
+
if (shouldSkipForCss) {
|
|
553
|
+
const prevWidth = model.width || 0;
|
|
554
|
+
const prevHeight = model.height || 0;
|
|
552
555
|
const hasExplicitSizeUpdate =
|
|
553
556
|
(updateData.width !== null && updateData.width !== undefined) ||
|
|
554
557
|
(updateData.height !== null && updateData.height !== undefined);
|
|
@@ -558,10 +561,10 @@
|
|
|
558
561
|
model.Response = updateData.content;
|
|
559
562
|
if (updateData.width !== null && updateData.width !== undefined) model.width = updateData.width;
|
|
560
563
|
if (updateData.height !== null && updateData.height !== undefined) model.height = updateData.height;
|
|
561
|
-
|
|
562
|
-
// ▼▼▼ [Fix] Sync CSS3D/GL userData with updated model dimensions ▼▼▼
|
|
563
|
-
// Without this, syncCss3dContent() reads stale userData.worldWidth/Height
|
|
564
|
-
// and reverts the DOM size, causing content/node size mismatch.
|
|
564
|
+
|
|
565
|
+
// ▼▼▼ [Fix] Sync CSS3D/GL userData with updated model dimensions ▼▼▼
|
|
566
|
+
// Without this, syncCss3dContent() reads stale userData.worldWidth/Height
|
|
567
|
+
// and reverts the DOM size, causing content/node size mismatch.
|
|
565
568
|
if (nodeEntry.cssObject?.userData) {
|
|
566
569
|
nodeEntry.cssObject.userData.worldWidth = model.width || 400;
|
|
567
570
|
nodeEntry.cssObject.userData.worldHeight = model.height || 200;
|
|
@@ -619,43 +622,43 @@
|
|
|
619
622
|
inner.style.setProperty('--css3d-node-resolution-zoom', String(Math.max(1, Number(resolutionScale || 1))));
|
|
620
623
|
inner.style.setProperty('zoom', String(Math.max(1, Number(resolutionScale || 1))));
|
|
621
624
|
}
|
|
622
|
-
const textarea = wrapper.querySelector('textarea');
|
|
623
|
-
if (textarea) {
|
|
624
|
-
textarea.style.width = '100%';
|
|
625
|
-
textarea.style.height = '100%';
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// Update glow size if needed (no texture work)
|
|
630
|
-
const group = nodeEntry.glObject;
|
|
631
|
-
if (group) {
|
|
632
|
-
if (!ENABLE_WEBGL_GLOW) {
|
|
633
|
-
removeGlowMesh(group);
|
|
634
|
-
} else if (window.MindMapGlowShader) {
|
|
635
|
-
const glowMesh = group.getObjectByName('glow');
|
|
636
|
-
const w = model.width || 400;
|
|
637
|
-
const h = model.height || 200;
|
|
638
|
-
if (glowMesh) {
|
|
639
|
-
window.MindMapGlowShader.updateGlowSize(glowMesh, w, h, model.contentType || 'text');
|
|
640
|
-
glowMesh.position.set(w / 2, -h / 2, 0); // [Fix] Z=0
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Keep WebGL body/tail geometry aligned with CSS3D size (prevents split)
|
|
646
|
-
if (nodeEntry.glObject) {
|
|
647
|
-
const bodyMesh = nodeEntry.glObject.getObjectByName('body');
|
|
648
|
-
const tailMesh = nodeEntry.glObject.getObjectByName('tail');
|
|
649
|
-
const w = model.width || 400;
|
|
650
|
-
const h = model.height || 200;
|
|
651
|
-
|
|
625
|
+
const textarea = wrapper.querySelector('textarea');
|
|
626
|
+
if (textarea) {
|
|
627
|
+
textarea.style.width = '100%';
|
|
628
|
+
textarea.style.height = '100%';
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Update glow size if needed (no texture work)
|
|
633
|
+
const group = nodeEntry.glObject;
|
|
634
|
+
if (group) {
|
|
635
|
+
if (!ENABLE_WEBGL_GLOW) {
|
|
636
|
+
removeGlowMesh(group);
|
|
637
|
+
} else if (window.MindMapGlowShader) {
|
|
638
|
+
const glowMesh = group.getObjectByName('glow');
|
|
639
|
+
const w = model.width || 400;
|
|
640
|
+
const h = model.height || 200;
|
|
641
|
+
if (glowMesh) {
|
|
642
|
+
window.MindMapGlowShader.updateGlowSize(glowMesh, w, h, model.contentType || 'text');
|
|
643
|
+
glowMesh.position.set(w / 2, -h / 2, 0); // [Fix] Z=0
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Keep WebGL body/tail geometry aligned with CSS3D size (prevents split)
|
|
649
|
+
if (nodeEntry.glObject) {
|
|
650
|
+
const bodyMesh = nodeEntry.glObject.getObjectByName('body');
|
|
651
|
+
const tailMesh = nodeEntry.glObject.getObjectByName('tail');
|
|
652
|
+
const w = model.width || 400;
|
|
653
|
+
const h = model.height || 200;
|
|
654
|
+
|
|
652
655
|
if (bodyMesh) {
|
|
653
656
|
const gp = bodyMesh.geometry && bodyMesh.geometry.parameters;
|
|
654
657
|
if (!gp || gp.width !== w || gp.height !== h) {
|
|
655
658
|
bodyMesh.geometry.dispose();
|
|
656
659
|
bodyMesh.geometry = new THREE.PlaneGeometry(w, h);
|
|
657
|
-
}
|
|
658
|
-
bodyMesh.position.set(w / 2, -h / 2, 0);
|
|
660
|
+
}
|
|
661
|
+
bodyMesh.position.set(w / 2, -h / 2, 0);
|
|
659
662
|
if (bodyMesh.material) {
|
|
660
663
|
bodyMesh.material.color?.setHex?.(0xffffff);
|
|
661
664
|
bodyMesh.material.needsUpdate = true;
|
|
@@ -665,79 +668,79 @@
|
|
|
665
668
|
applyImageCoverUv(bodyMesh, w, h);
|
|
666
669
|
}
|
|
667
670
|
}
|
|
668
|
-
|
|
669
|
-
if (tailMesh) {
|
|
670
|
-
tailMesh.position.set(w / 2, -h - 6, 0);
|
|
671
|
-
if (tailMesh.material) {
|
|
672
|
-
tailMesh.material.color?.setHex?.(0xffffff);
|
|
673
|
-
tailMesh.material.needsUpdate = true;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// Persist size changes for CSS-rendered nodes as well.
|
|
679
|
-
// Without this, auto-grown AI note dimensions are not saved to the board.
|
|
680
|
-
const sizeChanged = Math.abs((model.width || 0) - prevWidth) > 0.5
|
|
681
|
-
|| Math.abs((model.height || 0) - prevHeight) > 0.5;
|
|
671
|
+
|
|
672
|
+
if (tailMesh) {
|
|
673
|
+
tailMesh.position.set(w / 2, -h - 6, 0);
|
|
674
|
+
if (tailMesh.material) {
|
|
675
|
+
tailMesh.material.color?.setHex?.(0xffffff);
|
|
676
|
+
tailMesh.material.needsUpdate = true;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Persist size changes for CSS-rendered nodes as well.
|
|
682
|
+
// Without this, auto-grown AI note dimensions are not saved to the board.
|
|
683
|
+
const sizeChanged = Math.abs((model.width || 0) - prevWidth) > 0.5
|
|
684
|
+
|| Math.abs((model.height || 0) - prevHeight) > 0.5;
|
|
682
685
|
if (!updateData.skipPersistDimensions && (sizeChanged || hasExplicitSizeUpdate) && module.dotNetHelper?.invokeMethodAsync) {
|
|
683
|
-
try {
|
|
684
|
-
await module.dotNetHelper.invokeMethodAsync(
|
|
685
|
-
'UpdateNodeDimensions',
|
|
686
|
-
nodeId,
|
|
687
|
-
Math.round(model.width || 0),
|
|
688
|
-
Math.round(model.height || 0)
|
|
689
|
-
);
|
|
690
|
-
} catch (e) {
|
|
691
|
-
console.warn('[MindMapPipeline] Failed to persist CSS node dimensions:', e);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
window.MindMapNodes.pendingNodeUpdates.delete(nodeId);
|
|
696
|
-
window.MindMapNodes.nodeRenderLocks.set(nodeId, false);
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
// ▲▲▲ [Perf] ▲▲▲
|
|
700
|
-
|
|
701
|
-
const width = (updateData.width !== null && updateData.width !== undefined) ?
|
|
702
|
-
updateData.width : (model.width || (model.contentType === 'image' ? 300 : 400));
|
|
703
|
-
|
|
704
|
-
// ▼▼▼ [Key change] Reduce height-change sensitivity (avoid jitter) ▼▼▼
|
|
705
|
-
// ▼▼▼ [Changed] Chunk nodes already have fixed maxScroll; skip recalculation ▼▼▼
|
|
706
|
-
// ▼▼▼ [Changed] Skip height recalculation for expanded nodes (they auto-grow in updateNodeContent) ▼▼▼
|
|
707
|
-
if ((model.contentType === 'text' || model.contentType === 'note') && !model.isChunkedText && !model.isExpanded) {
|
|
708
|
-
if (updateData.height === null || updateData.height === undefined) {
|
|
709
|
-
const tempModel = { ...model, response: updateData.content };
|
|
710
|
-
// When showFullAiResponse is true, do not cap height
|
|
711
|
-
const defaultMaxHeight = module.aiNodeDefaultHeight || 200;
|
|
712
|
-
const effectiveMaxHeight = module.showFullAiResponse ? 10000 : defaultMaxHeight;
|
|
713
|
-
const { height: requiredHeight, maxScrollPx } = await window.MindMapTextureFactory.measureHtmlHeightAsync(tempModel, zoomLevel, width * zoomLevel, null, effectiveMaxHeight);
|
|
714
|
-
|
|
715
|
-
// Keep AI/loading note nodes at least at the configured default height.
|
|
716
|
-
// This prevents initial one-line collapse while streaming starts.
|
|
717
|
-
const defaultPrompts = ['Note', 'Pasted Text', '에이전트 메모'];
|
|
718
|
-
const promptText = model.prompt ?? model.Prompt ?? '';
|
|
719
|
-
const isAiPrompt = !!promptText && !defaultPrompts.includes(promptText);
|
|
720
|
-
const minPreferredHeight = (model.isLoading || isAiPrompt)
|
|
721
|
-
? (module.aiNodeDefaultHeight || 200)
|
|
722
|
-
: 60;
|
|
723
|
-
const normalizedRequiredHeight = Math.max(requiredHeight, minPreferredHeight);
|
|
724
|
-
|
|
725
|
-
const currentHeight = (model.height || 0);
|
|
726
|
-
// Update only if delta is >= 5px (was 2px -> 5px)
|
|
727
|
-
if (Math.abs(currentHeight - normalizedRequiredHeight) > 5) {
|
|
728
|
-
model.height = normalizedRequiredHeight;
|
|
729
|
-
model.maxScroll = maxScrollPx / zoomLevel;
|
|
730
|
-
model.scrollOffset = Math.min(model.scrollOffset || 0, model.maxScroll);
|
|
731
|
-
updateData.height = normalizedRequiredHeight;
|
|
732
|
-
try {
|
|
733
|
-
module.dotNetHelper?.invokeMethodAsync('UpdateNodeDimensions', nodeId, Math.round(width), Math.round(normalizedRequiredHeight));
|
|
734
|
-
} catch { }
|
|
735
|
-
} else {
|
|
736
|
-
// If the change is tiny, keep the existing height (avoid infinite loop)
|
|
737
|
-
updateData.height = currentHeight;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
686
|
+
try {
|
|
687
|
+
await module.dotNetHelper.invokeMethodAsync(
|
|
688
|
+
'UpdateNodeDimensions',
|
|
689
|
+
nodeId,
|
|
690
|
+
Math.round(model.width || 0),
|
|
691
|
+
Math.round(model.height || 0)
|
|
692
|
+
);
|
|
693
|
+
} catch (e) {
|
|
694
|
+
console.warn('[MindMapPipeline] Failed to persist CSS node dimensions:', e);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
window.MindMapNodes.pendingNodeUpdates.delete(nodeId);
|
|
699
|
+
window.MindMapNodes.nodeRenderLocks.set(nodeId, false);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
// ▲▲▲ [Perf] ▲▲▲
|
|
703
|
+
|
|
704
|
+
const width = (updateData.width !== null && updateData.width !== undefined) ?
|
|
705
|
+
updateData.width : (model.width || (model.contentType === 'image' ? 300 : 400));
|
|
706
|
+
|
|
707
|
+
// ▼▼▼ [Key change] Reduce height-change sensitivity (avoid jitter) ▼▼▼
|
|
708
|
+
// ▼▼▼ [Changed] Chunk nodes already have fixed maxScroll; skip recalculation ▼▼▼
|
|
709
|
+
// ▼▼▼ [Changed] Skip height recalculation for expanded nodes (they auto-grow in updateNodeContent) ▼▼▼
|
|
710
|
+
if ((model.contentType === 'text' || model.contentType === 'note') && !model.isChunkedText && !model.isExpanded) {
|
|
711
|
+
if (updateData.height === null || updateData.height === undefined) {
|
|
712
|
+
const tempModel = { ...model, response: updateData.content };
|
|
713
|
+
// When showFullAiResponse is true, do not cap height
|
|
714
|
+
const defaultMaxHeight = module.aiNodeDefaultHeight || 200;
|
|
715
|
+
const effectiveMaxHeight = module.showFullAiResponse ? 10000 : defaultMaxHeight;
|
|
716
|
+
const { height: requiredHeight, maxScrollPx } = await window.MindMapTextureFactory.measureHtmlHeightAsync(tempModel, zoomLevel, width * zoomLevel, null, effectiveMaxHeight);
|
|
717
|
+
|
|
718
|
+
// Keep AI/loading note nodes at least at the configured default height.
|
|
719
|
+
// This prevents initial one-line collapse while streaming starts.
|
|
720
|
+
const defaultPrompts = ['Note', 'Pasted Text', '에이전트 메모'];
|
|
721
|
+
const promptText = model.prompt ?? model.Prompt ?? '';
|
|
722
|
+
const isAiPrompt = !!promptText && !defaultPrompts.includes(promptText);
|
|
723
|
+
const minPreferredHeight = (model.isLoading || isAiPrompt)
|
|
724
|
+
? (module.aiNodeDefaultHeight || 200)
|
|
725
|
+
: 60;
|
|
726
|
+
const normalizedRequiredHeight = Math.max(requiredHeight, minPreferredHeight);
|
|
727
|
+
|
|
728
|
+
const currentHeight = (model.height || 0);
|
|
729
|
+
// Update only if delta is >= 5px (was 2px -> 5px)
|
|
730
|
+
if (Math.abs(currentHeight - normalizedRequiredHeight) > 5) {
|
|
731
|
+
model.height = normalizedRequiredHeight;
|
|
732
|
+
model.maxScroll = maxScrollPx / zoomLevel;
|
|
733
|
+
model.scrollOffset = Math.min(model.scrollOffset || 0, model.maxScroll);
|
|
734
|
+
updateData.height = normalizedRequiredHeight;
|
|
735
|
+
try {
|
|
736
|
+
module.dotNetHelper?.invokeMethodAsync('UpdateNodeDimensions', nodeId, Math.round(width), Math.round(normalizedRequiredHeight));
|
|
737
|
+
} catch { }
|
|
738
|
+
} else {
|
|
739
|
+
// If the change is tiny, keep the existing height (avoid infinite loop)
|
|
740
|
+
updateData.height = currentHeight;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
741
744
|
// ▲▲▲ [Changed] ▲▲▲
|
|
742
745
|
// ▲▲▲ [Key change] ▲▲▲
|
|
743
746
|
|
|
@@ -815,87 +818,87 @@
|
|
|
815
818
|
|
|
816
819
|
// ★ Invalidate LOD cache when content changes
|
|
817
820
|
window.MindMapTextLOD?.invalidateNodeCache?.(nodeId);
|
|
818
|
-
|
|
819
|
-
const updateDataWithScroll = {
|
|
820
|
-
...updateData,
|
|
821
|
-
scrollDelta: updateData.scrollDelta || 0
|
|
822
|
-
};
|
|
823
|
-
|
|
824
|
-
// ★ Troika 모드 체크: text/note/code 노드면 배경 전용 텍스처 사용
|
|
825
|
-
// ★ Code nodes always use Troika for text rendering
|
|
826
|
-
const isTroikaMode = (model.contentType === 'code') || // Code nodes always use Troika
|
|
827
|
-
(window.MindMapNodes?.getTextRenderMode?.() === 'troika'
|
|
828
|
-
&& (model.contentType === 'text' || model.contentType === 'note')
|
|
829
|
-
&& !model.isChunkedText);
|
|
830
|
-
|
|
831
|
-
let textures, w, h;
|
|
832
|
-
const existingTextures = window.MindMapNodes.textureCache.get(nodeId);
|
|
833
|
-
|
|
834
|
-
if (isTroikaMode) {
|
|
835
|
-
// ★ Troika 최적화: 이미 캐시된 텍스처가 있고, 크기와 내용이 동일하면 재사용
|
|
836
|
-
const canReuseTexture = existingTextures?.default?.body
|
|
837
|
-
&& existingTextures._cachedWidth === model.width
|
|
838
|
-
&& existingTextures._cachedHeight === model.height
|
|
839
|
-
&& existingTextures._cachedContent === model.response; // ★ Content change check
|
|
840
|
-
|
|
841
|
-
if (canReuseTexture) {
|
|
842
|
-
// 캐시된 텍스처 재사용 (텍스처 재생성 스킵)
|
|
843
|
-
textures = existingTextures;
|
|
844
|
-
w = model.width;
|
|
845
|
-
h = model.height;
|
|
846
|
-
} else {
|
|
847
|
-
// 배경 텍스처 생성
|
|
848
|
-
const result = await window.MindMapTextureFactory.generateTroikaBackgroundTextures(module, model, model.width, model.height);
|
|
849
|
-
textures = result.textures;
|
|
850
|
-
// 캐시 메타데이터 저장
|
|
851
|
-
textures._cachedWidth = model.width;
|
|
852
|
-
textures._cachedHeight = model.height;
|
|
853
|
-
textures._cachedContent = model.response; // ★ Track content for change detection
|
|
854
|
-
w = result.width;
|
|
855
|
-
h = result.height;
|
|
856
|
-
}
|
|
857
|
-
} else {
|
|
858
|
-
// Canvas 모드: 기존 방식 (텍스트 포함)
|
|
859
|
-
const isResizeUpdate = updateData.width !== undefined || updateData.height !== undefined;
|
|
860
|
-
const isBatchExpand = updateData.skipSelected === true; // ★ Batch expand operation
|
|
861
|
-
|
|
862
|
-
// 고정 품질 렌더링 (LOD 품질 스케일 제거)
|
|
863
|
-
const qualityScale = 1.0;
|
|
864
|
-
console.log(`[processRenderQueue] Canvas mode for ${nodeId}, qualityScale=${qualityScale}, isResize=${isResizeUpdate}, isBatch=${isBatchExpand}`);
|
|
865
|
-
const result = await window.MindMapTextureFactory.generateAndCacheTextures(module, model, window.MindMapNodes.imageCache, updateDataWithScroll, qualityScale);
|
|
866
|
-
textures = result.textures;
|
|
867
|
-
w = result.width;
|
|
868
|
-
h = result.height;
|
|
869
|
-
|
|
870
|
-
// 캐시 메타데이터 (콘텐츠 변경 감지용)
|
|
871
|
-
if (textures.default?.body) {
|
|
872
|
-
textures.default.body._cachedContent = model.response ?? '';
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// ★ 기존 텍스처 dispose (Troika에서 재사용한 경우 제외)
|
|
877
|
-
if (existingTextures && existingTextures !== textures) {
|
|
878
|
-
enqueueDispose(existingTextures.default?.body);
|
|
879
|
-
enqueueDispose(existingTextures.selected?.body);
|
|
880
|
-
enqueueDispose(existingTextures.glow?.texture);
|
|
881
|
-
}
|
|
882
|
-
window.MindMapNodes.textureCache.set(nodeId, textures);
|
|
883
|
-
|
|
884
|
-
const isSelected = module.multiSelectedNodeIds.has(nodeId);
|
|
885
|
-
// ★ [Fix] If selected texture is null (skipSelected mode), use default texture
|
|
886
|
-
const active = (isSelected && textures.selected?.body) ? textures.selected : textures.default;
|
|
887
|
-
const group = nodeEntry.glObject;
|
|
888
|
-
|
|
889
|
-
const CORNER_RADIUS = 20;
|
|
890
|
-
const OUTLINE_SHRINK = 2;
|
|
891
|
-
const OUTLINE_RADIUS = Math.max(1, CORNER_RADIUS - 1);
|
|
892
|
-
const CHAMFER_SIZE = 16;
|
|
893
|
-
|
|
894
|
-
if (group) {
|
|
895
|
-
const bodyMesh = group.getObjectByName('body');
|
|
896
|
-
const tailMesh = group.getObjectByName('tail');
|
|
897
|
-
let glowMesh = group.getObjectByName('glow');
|
|
898
|
-
const outline = group.getObjectByName('outline');
|
|
821
|
+
|
|
822
|
+
const updateDataWithScroll = {
|
|
823
|
+
...updateData,
|
|
824
|
+
scrollDelta: updateData.scrollDelta || 0
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
// ★ Troika 모드 체크: text/note/code 노드면 배경 전용 텍스처 사용
|
|
828
|
+
// ★ Code nodes always use Troika for text rendering
|
|
829
|
+
const isTroikaMode = (model.contentType === 'code') || // Code nodes always use Troika
|
|
830
|
+
(window.MindMapNodes?.getTextRenderMode?.() === 'troika'
|
|
831
|
+
&& (model.contentType === 'text' || model.contentType === 'note')
|
|
832
|
+
&& !model.isChunkedText);
|
|
833
|
+
|
|
834
|
+
let textures, w, h;
|
|
835
|
+
const existingTextures = window.MindMapNodes.textureCache.get(nodeId);
|
|
836
|
+
|
|
837
|
+
if (isTroikaMode) {
|
|
838
|
+
// ★ Troika 최적화: 이미 캐시된 텍스처가 있고, 크기와 내용이 동일하면 재사용
|
|
839
|
+
const canReuseTexture = existingTextures?.default?.body
|
|
840
|
+
&& existingTextures._cachedWidth === model.width
|
|
841
|
+
&& existingTextures._cachedHeight === model.height
|
|
842
|
+
&& existingTextures._cachedContent === model.response; // ★ Content change check
|
|
843
|
+
|
|
844
|
+
if (canReuseTexture) {
|
|
845
|
+
// 캐시된 텍스처 재사용 (텍스처 재생성 스킵)
|
|
846
|
+
textures = existingTextures;
|
|
847
|
+
w = model.width;
|
|
848
|
+
h = model.height;
|
|
849
|
+
} else {
|
|
850
|
+
// 배경 텍스처 생성
|
|
851
|
+
const result = await window.MindMapTextureFactory.generateTroikaBackgroundTextures(module, model, model.width, model.height);
|
|
852
|
+
textures = result.textures;
|
|
853
|
+
// 캐시 메타데이터 저장
|
|
854
|
+
textures._cachedWidth = model.width;
|
|
855
|
+
textures._cachedHeight = model.height;
|
|
856
|
+
textures._cachedContent = model.response; // ★ Track content for change detection
|
|
857
|
+
w = result.width;
|
|
858
|
+
h = result.height;
|
|
859
|
+
}
|
|
860
|
+
} else {
|
|
861
|
+
// Canvas 모드: 기존 방식 (텍스트 포함)
|
|
862
|
+
const isResizeUpdate = updateData.width !== undefined || updateData.height !== undefined;
|
|
863
|
+
const isBatchExpand = updateData.skipSelected === true; // ★ Batch expand operation
|
|
864
|
+
|
|
865
|
+
// 고정 품질 렌더링 (LOD 품질 스케일 제거)
|
|
866
|
+
const qualityScale = 1.0;
|
|
867
|
+
console.log(`[processRenderQueue] Canvas mode for ${nodeId}, qualityScale=${qualityScale}, isResize=${isResizeUpdate}, isBatch=${isBatchExpand}`);
|
|
868
|
+
const result = await window.MindMapTextureFactory.generateAndCacheTextures(module, model, window.MindMapNodes.imageCache, updateDataWithScroll, qualityScale);
|
|
869
|
+
textures = result.textures;
|
|
870
|
+
w = result.width;
|
|
871
|
+
h = result.height;
|
|
872
|
+
|
|
873
|
+
// 캐시 메타데이터 (콘텐츠 변경 감지용)
|
|
874
|
+
if (textures.default?.body) {
|
|
875
|
+
textures.default.body._cachedContent = model.response ?? '';
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// ★ 기존 텍스처 dispose (Troika에서 재사용한 경우 제외)
|
|
880
|
+
if (existingTextures && existingTextures !== textures) {
|
|
881
|
+
enqueueDispose(existingTextures.default?.body);
|
|
882
|
+
enqueueDispose(existingTextures.selected?.body);
|
|
883
|
+
enqueueDispose(existingTextures.glow?.texture);
|
|
884
|
+
}
|
|
885
|
+
window.MindMapNodes.textureCache.set(nodeId, textures);
|
|
886
|
+
|
|
887
|
+
const isSelected = module.multiSelectedNodeIds.has(nodeId);
|
|
888
|
+
// ★ [Fix] If selected texture is null (skipSelected mode), use default texture
|
|
889
|
+
const active = (isSelected && textures.selected?.body) ? textures.selected : textures.default;
|
|
890
|
+
const group = nodeEntry.glObject;
|
|
891
|
+
|
|
892
|
+
const CORNER_RADIUS = 20;
|
|
893
|
+
const OUTLINE_SHRINK = 2;
|
|
894
|
+
const OUTLINE_RADIUS = Math.max(1, CORNER_RADIUS - 1);
|
|
895
|
+
const CHAMFER_SIZE = 16;
|
|
896
|
+
|
|
897
|
+
if (group) {
|
|
898
|
+
const bodyMesh = group.getObjectByName('body');
|
|
899
|
+
const tailMesh = group.getObjectByName('tail');
|
|
900
|
+
let glowMesh = group.getObjectByName('glow');
|
|
901
|
+
const outline = group.getObjectByName('outline');
|
|
899
902
|
if (!ENABLE_WEBGL_GLOW) {
|
|
900
903
|
glowMesh = removeGlowMesh(group);
|
|
901
904
|
}
|
|
@@ -907,18 +910,18 @@
|
|
|
907
910
|
if (!gp || gp.width !== w || gp.height !== h) {
|
|
908
911
|
bodyMesh.geometry.dispose();
|
|
909
912
|
bodyMesh.geometry = new THREE.PlaneGeometry(w, h);
|
|
910
|
-
|
|
911
|
-
if (outline) {
|
|
912
|
-
outline.geometry.dispose();
|
|
913
|
-
outline.geometry = createRoundedRectGeometry(
|
|
914
|
-
w - OUTLINE_SHRINK,
|
|
915
|
-
h - OUTLINE_SHRINK,
|
|
916
|
-
OUTLINE_RADIUS
|
|
917
|
-
);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
if (active?.body) {
|
|
921
|
-
bodyMesh.material.map = active.body;
|
|
913
|
+
|
|
914
|
+
if (outline) {
|
|
915
|
+
outline.geometry.dispose();
|
|
916
|
+
outline.geometry = createRoundedRectGeometry(
|
|
917
|
+
w - OUTLINE_SHRINK,
|
|
918
|
+
h - OUTLINE_SHRINK,
|
|
919
|
+
OUTLINE_RADIUS
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (active?.body) {
|
|
924
|
+
bodyMesh.material.map = active.body;
|
|
922
925
|
}
|
|
923
926
|
bodyMesh.material.color.setHex(0xffffff);
|
|
924
927
|
bodyMesh.material.needsUpdate = true;
|
|
@@ -939,53 +942,53 @@
|
|
|
939
942
|
outline.position.z = 0; // [Fix] Z=0
|
|
940
943
|
outline.visible = false; // Disabled - using SDF glow instead
|
|
941
944
|
}
|
|
942
|
-
}
|
|
943
|
-
if (tailMesh) {
|
|
944
|
-
if (active?.tail) {
|
|
945
|
-
tailMesh.material.map = active.tail;
|
|
946
|
-
tailMesh.material.needsUpdate = true;
|
|
947
|
-
}
|
|
948
|
-
tailMesh.position.set(w / 2, -h - 6, 0);
|
|
949
|
-
}
|
|
950
|
-
|
|
945
|
+
}
|
|
946
|
+
if (tailMesh) {
|
|
947
|
+
if (active?.tail) {
|
|
948
|
+
tailMesh.material.map = active.tail;
|
|
949
|
+
tailMesh.material.needsUpdate = true;
|
|
950
|
+
}
|
|
951
|
+
tailMesh.position.set(w / 2, -h - 6, 0);
|
|
952
|
+
}
|
|
953
|
+
|
|
951
954
|
// ▼▼▼ [SDF Glow] Use shader-based glow (no textures!) ▼▼▼
|
|
952
|
-
if (ENABLE_WEBGL_GLOW && window.MindMapGlowShader) {
|
|
953
|
-
const contentType = model.contentType || 'text';
|
|
954
|
-
if (glowMesh) {
|
|
955
|
-
window.MindMapGlowShader.updateGlowSize(glowMesh, w, h, contentType);
|
|
956
|
-
window.MindMapGlowShader.updateGlowSelection(glowMesh, isSelected);
|
|
957
|
-
glowMesh.position.set(w / 2, -h / 2, 0); // [Fix] Z=0
|
|
958
|
-
glowMesh.renderOrder = baseRenderOrder - 5;
|
|
959
|
-
} else {
|
|
960
|
-
const newGlow = window.MindMapGlowShader.createSDFGlowMesh(w, h, isSelected, contentType);
|
|
961
|
-
newGlow.position.set(w / 2, -h / 2, 0); // [Fix] Z=0
|
|
962
|
-
newGlow.renderOrder = baseRenderOrder - 5;
|
|
963
|
-
// ★ 카메라 거리에 따라 초기 가시성 설정
|
|
964
|
-
const cameraZ = module.camera?.position?.z ?? Infinity;
|
|
965
|
-
const threshold = 4000;
|
|
966
|
-
newGlow.visible = cameraZ < threshold;
|
|
967
|
-
group.add(newGlow);
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
// ▲▲▲ [SDF Glow] ▲▲▲
|
|
971
|
-
|
|
972
|
-
// ▼▼▼ [Changed] Update resize handles (4 corners + optional 4 edges) ▼▼▼
|
|
973
|
-
if (model.contentType !== 'pdf') {
|
|
974
|
-
// [Fix] 이미지 노드 리사이즈 핸들 크기 통일 (Note와 동일한 16px)
|
|
975
|
-
const actualHandleSize = HANDLE_SIZE;
|
|
976
|
-
|
|
977
|
-
const edgeWidthLength = Math.max(10, w - actualHandleSize * 2);
|
|
978
|
-
const edgeHeightLength = Math.max(10, h - actualHandleSize * 2);
|
|
979
|
-
|
|
980
|
-
const handleConfigs = {
|
|
981
|
-
// ▼▼▼ [Fix] Corners - Z값을 0으로 통일하여 시차(Parallax) 완전 제거 ▼▼▼
|
|
982
|
-
'resizeHandle_TL': { x: actualHandleSize / 2, y: -actualHandleSize / 2, w: actualHandleSize, h: actualHandleSize, z: 0 },
|
|
983
|
-
'resizeHandle_TR': { x: w - actualHandleSize / 2, y: -actualHandleSize / 2, w: actualHandleSize, h: actualHandleSize, z: 0 },
|
|
984
|
-
'resizeHandle_BL': { x: actualHandleSize / 2, y: -h + actualHandleSize / 2, w: actualHandleSize, h: actualHandleSize, z: 0 },
|
|
985
|
-
'resizeHandle_BR': { x: w - actualHandleSize / 2, y: -h + actualHandleSize / 2, w: actualHandleSize, h: actualHandleSize, z: 0 }
|
|
986
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
987
|
-
};
|
|
988
|
-
|
|
955
|
+
if (ENABLE_WEBGL_GLOW && window.MindMapGlowShader) {
|
|
956
|
+
const contentType = model.contentType || 'text';
|
|
957
|
+
if (glowMesh) {
|
|
958
|
+
window.MindMapGlowShader.updateGlowSize(glowMesh, w, h, contentType);
|
|
959
|
+
window.MindMapGlowShader.updateGlowSelection(glowMesh, isSelected);
|
|
960
|
+
glowMesh.position.set(w / 2, -h / 2, 0); // [Fix] Z=0
|
|
961
|
+
glowMesh.renderOrder = baseRenderOrder - 5;
|
|
962
|
+
} else {
|
|
963
|
+
const newGlow = window.MindMapGlowShader.createSDFGlowMesh(w, h, isSelected, contentType);
|
|
964
|
+
newGlow.position.set(w / 2, -h / 2, 0); // [Fix] Z=0
|
|
965
|
+
newGlow.renderOrder = baseRenderOrder - 5;
|
|
966
|
+
// ★ 카메라 거리에 따라 초기 가시성 설정
|
|
967
|
+
const cameraZ = module.camera?.position?.z ?? Infinity;
|
|
968
|
+
const threshold = 4000;
|
|
969
|
+
newGlow.visible = cameraZ < threshold;
|
|
970
|
+
group.add(newGlow);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
// ▲▲▲ [SDF Glow] ▲▲▲
|
|
974
|
+
|
|
975
|
+
// ▼▼▼ [Changed] Update resize handles (4 corners + optional 4 edges) ▼▼▼
|
|
976
|
+
if (model.contentType !== 'pdf') {
|
|
977
|
+
// [Fix] 이미지 노드 리사이즈 핸들 크기 통일 (Note와 동일한 16px)
|
|
978
|
+
const actualHandleSize = HANDLE_SIZE;
|
|
979
|
+
|
|
980
|
+
const edgeWidthLength = Math.max(10, w - actualHandleSize * 2);
|
|
981
|
+
const edgeHeightLength = Math.max(10, h - actualHandleSize * 2);
|
|
982
|
+
|
|
983
|
+
const handleConfigs = {
|
|
984
|
+
// ▼▼▼ [Fix] Corners - Z값을 0으로 통일하여 시차(Parallax) 완전 제거 ▼▼▼
|
|
985
|
+
'resizeHandle_TL': { x: actualHandleSize / 2, y: -actualHandleSize / 2, w: actualHandleSize, h: actualHandleSize, z: 0 },
|
|
986
|
+
'resizeHandle_TR': { x: w - actualHandleSize / 2, y: -actualHandleSize / 2, w: actualHandleSize, h: actualHandleSize, z: 0 },
|
|
987
|
+
'resizeHandle_BL': { x: actualHandleSize / 2, y: -h + actualHandleSize / 2, w: actualHandleSize, h: actualHandleSize, z: 0 },
|
|
988
|
+
'resizeHandle_BR': { x: w - actualHandleSize / 2, y: -h + actualHandleSize / 2, w: actualHandleSize, h: actualHandleSize, z: 0 }
|
|
989
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
990
|
+
};
|
|
991
|
+
|
|
989
992
|
if (model.contentType !== 'image') {
|
|
990
993
|
Object.assign(handleConfigs, {
|
|
991
994
|
// ▼▼▼ [Fix] Edges - Z값을 0으로 통일 ▼▼▼
|
|
@@ -996,17 +999,17 @@
|
|
|
996
999
|
// ▲▲▲ [Fix] ▲▲▲
|
|
997
1000
|
});
|
|
998
1001
|
}
|
|
999
|
-
|
|
1000
|
-
// Iterate all possible handles to update position OR hide if not needed
|
|
1001
|
-
const allHandleNames = [
|
|
1002
|
-
'resizeHandle_TL', 'resizeHandle_TR', 'resizeHandle_BL', 'resizeHandle_BR',
|
|
1003
|
-
'resizeHandle_T', 'resizeHandle_B', 'resizeHandle_L', 'resizeHandle_R'
|
|
1004
|
-
];
|
|
1005
|
-
|
|
1006
|
-
allHandleNames.forEach(name => {
|
|
1007
|
-
const handle = group.getObjectByName(name);
|
|
1008
|
-
const cfg = handleConfigs[name];
|
|
1009
|
-
|
|
1002
|
+
|
|
1003
|
+
// Iterate all possible handles to update position OR hide if not needed
|
|
1004
|
+
const allHandleNames = [
|
|
1005
|
+
'resizeHandle_TL', 'resizeHandle_TR', 'resizeHandle_BL', 'resizeHandle_BR',
|
|
1006
|
+
'resizeHandle_T', 'resizeHandle_B', 'resizeHandle_L', 'resizeHandle_R'
|
|
1007
|
+
];
|
|
1008
|
+
|
|
1009
|
+
allHandleNames.forEach(name => {
|
|
1010
|
+
const handle = group.getObjectByName(name);
|
|
1011
|
+
const cfg = handleConfigs[name];
|
|
1012
|
+
|
|
1010
1013
|
if (handle) {
|
|
1011
1014
|
if (cfg) {
|
|
1012
1015
|
// Active handle: update position/size and ensure visible
|
|
@@ -1027,7 +1030,7 @@
|
|
|
1027
1030
|
handle.visible = false;
|
|
1028
1031
|
}
|
|
1029
1032
|
}
|
|
1030
|
-
});
|
|
1033
|
+
});
|
|
1031
1034
|
}
|
|
1032
1035
|
// ▲▲▲ [Changed] ▲▲▲
|
|
1033
1036
|
|
|
@@ -1055,26 +1058,26 @@
|
|
|
1055
1058
|
}
|
|
1056
1059
|
}
|
|
1057
1060
|
} catch (err) {
|
|
1058
|
-
console.error(`[Pipeline] Render Node ${nodeId} Failed:`, err);
|
|
1059
|
-
} finally {
|
|
1060
|
-
window.MindMapNodes.nodeRenderLocks.set(nodeId, false);
|
|
1061
|
-
// console.timeEnd(`[Pipeline] 6. Render Node ${nodeId}`);
|
|
1062
|
-
|
|
1063
|
-
// If another update arrived while processing, re-run (with a small delay)
|
|
1064
|
-
if (window.MindMapNodes.pendingNodeUpdates.has(nodeId)) {
|
|
1065
|
-
setTimeout(() => enqueueRender(module, nodeId), 50);
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
// --- Resizing handlers (keep existing behavior) ---
|
|
1071
|
-
|
|
1072
|
-
// ▼▼▼ [Changed] Add corner parameter for four-corner resizing ▼▼▼
|
|
1073
|
-
function startResizing(module, nodeId, startX, startY, corner = 'BR') {
|
|
1074
|
-
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
1075
|
-
if (!nodeEntry) return false;
|
|
1076
|
-
if (nodeEntry.model.contentType === 'pdf') return false;
|
|
1077
|
-
|
|
1061
|
+
console.error(`[Pipeline] Render Node ${nodeId} Failed:`, err);
|
|
1062
|
+
} finally {
|
|
1063
|
+
window.MindMapNodes.nodeRenderLocks.set(nodeId, false);
|
|
1064
|
+
// console.timeEnd(`[Pipeline] 6. Render Node ${nodeId}`);
|
|
1065
|
+
|
|
1066
|
+
// If another update arrived while processing, re-run (with a small delay)
|
|
1067
|
+
if (window.MindMapNodes.pendingNodeUpdates.has(nodeId)) {
|
|
1068
|
+
setTimeout(() => enqueueRender(module, nodeId), 50);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// --- Resizing handlers (keep existing behavior) ---
|
|
1074
|
+
|
|
1075
|
+
// ▼▼▼ [Changed] Add corner parameter for four-corner resizing ▼▼▼
|
|
1076
|
+
function startResizing(module, nodeId, startX, startY, corner = 'BR') {
|
|
1077
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
1078
|
+
if (!nodeEntry) return false;
|
|
1079
|
+
if (nodeEntry.model.contentType === 'pdf') return false;
|
|
1080
|
+
|
|
1078
1081
|
module.isResizing = true;
|
|
1079
1082
|
module.resizingNodeId = nodeId;
|
|
1080
1083
|
module.resizeCorner = corner; // TL, TR, BL, BR
|
|
@@ -1095,203 +1098,203 @@
|
|
|
1095
1098
|
window.MindMapInteractions?.setBrowserSelectionLock?.(true, 'resize');
|
|
1096
1099
|
document.body.classList.add('is-resizing');
|
|
1097
1100
|
module.resizeStartMousePos = { x: startX, y: startY };
|
|
1098
|
-
module.resizeStartDimensions = {
|
|
1099
|
-
w: nodeEntry.model.width || 400,
|
|
1100
|
-
h: nodeEntry.model.height || 200
|
|
1101
|
-
};
|
|
1102
|
-
// Store initial position from the active render object (CSS/GL) to avoid
|
|
1103
|
-
// image-node jumps when CSS and GL are slightly out of sync.
|
|
1104
|
-
const activePos = module.resizeNodeObject?.position
|
|
1105
|
-
|| nodeEntry.cssObject?.position
|
|
1106
|
-
|| nodeEntry.glObject?.position
|
|
1107
|
-
|| { x: 0, y: 0 };
|
|
1108
|
-
module.resizeStartPosition = {
|
|
1109
|
-
x: activePos.x,
|
|
1110
|
-
y: activePos.y
|
|
1111
|
-
};
|
|
1112
|
-
|
|
1113
|
-
// Force immediate CSS/GL transform alignment at resize start.
|
|
1114
|
-
if (nodeEntry.glObject) {
|
|
1115
|
-
nodeEntry.glObject.position.x = activePos.x;
|
|
1116
|
-
nodeEntry.glObject.position.y = activePos.y;
|
|
1117
|
-
}
|
|
1118
|
-
if (nodeEntry.cssObject) {
|
|
1119
|
-
nodeEntry.cssObject.position.x = activePos.x;
|
|
1120
|
-
nodeEntry.cssObject.position.y = activePos.y;
|
|
1121
|
-
nodeEntry.cssObject.updateMatrixWorld(true);
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
// ▼▼▼ [Fix] 리사이즈 중 CSS transition 비활성화하여 즉시 크기 변경 ▼▼▼
|
|
1125
|
-
if (nodeEntry.cssObject && nodeEntry.cssObject.element) {
|
|
1126
|
-
const wrapper = nodeEntry.cssObject.element;
|
|
1127
|
-
wrapper.style.transition = 'none';
|
|
1128
|
-
const inner = wrapper.firstElementChild;
|
|
1129
|
-
if (inner) {
|
|
1130
|
-
inner.style.transition = 'none';
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
1134
|
-
|
|
1135
|
-
// ▼▼▼ [UndoRedo] Capture before-state for undo ▼▼▼
|
|
1136
|
-
if (module.dotNetHelper) {
|
|
1137
|
-
try { module.dotNetHelper.invokeMethodAsync('StartResizeCapture', nodeId); } catch { }
|
|
1138
|
-
}
|
|
1139
|
-
// ▲▲▲ [UndoRedo] ▲▲▲
|
|
1140
|
-
|
|
1141
|
-
return true;
|
|
1142
|
-
}
|
|
1143
|
-
// ▲▲▲ [Changed] ▲▲▲
|
|
1144
|
-
|
|
1145
|
-
function doResizing(module, currentX, currentY) {
|
|
1101
|
+
module.resizeStartDimensions = {
|
|
1102
|
+
w: nodeEntry.model.width || 400,
|
|
1103
|
+
h: nodeEntry.model.height || 200
|
|
1104
|
+
};
|
|
1105
|
+
// Store initial position from the active render object (CSS/GL) to avoid
|
|
1106
|
+
// image-node jumps when CSS and GL are slightly out of sync.
|
|
1107
|
+
const activePos = module.resizeNodeObject?.position
|
|
1108
|
+
|| nodeEntry.cssObject?.position
|
|
1109
|
+
|| nodeEntry.glObject?.position
|
|
1110
|
+
|| { x: 0, y: 0 };
|
|
1111
|
+
module.resizeStartPosition = {
|
|
1112
|
+
x: activePos.x,
|
|
1113
|
+
y: activePos.y
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
// Force immediate CSS/GL transform alignment at resize start.
|
|
1117
|
+
if (nodeEntry.glObject) {
|
|
1118
|
+
nodeEntry.glObject.position.x = activePos.x;
|
|
1119
|
+
nodeEntry.glObject.position.y = activePos.y;
|
|
1120
|
+
}
|
|
1121
|
+
if (nodeEntry.cssObject) {
|
|
1122
|
+
nodeEntry.cssObject.position.x = activePos.x;
|
|
1123
|
+
nodeEntry.cssObject.position.y = activePos.y;
|
|
1124
|
+
nodeEntry.cssObject.updateMatrixWorld(true);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// ▼▼▼ [Fix] 리사이즈 중 CSS transition 비활성화하여 즉시 크기 변경 ▼▼▼
|
|
1128
|
+
if (nodeEntry.cssObject && nodeEntry.cssObject.element) {
|
|
1129
|
+
const wrapper = nodeEntry.cssObject.element;
|
|
1130
|
+
wrapper.style.transition = 'none';
|
|
1131
|
+
const inner = wrapper.firstElementChild;
|
|
1132
|
+
if (inner) {
|
|
1133
|
+
inner.style.transition = 'none';
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1137
|
+
|
|
1138
|
+
// ▼▼▼ [UndoRedo] Capture before-state for undo ▼▼▼
|
|
1139
|
+
if (module.dotNetHelper) {
|
|
1140
|
+
try { module.dotNetHelper.invokeMethodAsync('StartResizeCapture', nodeId); } catch { }
|
|
1141
|
+
}
|
|
1142
|
+
// ▲▲▲ [UndoRedo] ▲▲▲
|
|
1143
|
+
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
1147
|
+
|
|
1148
|
+
function doResizing(module, currentX, currentY) {
|
|
1146
1149
|
if (!module.isResizing || !module.resizeNodeObject) return;
|
|
1147
1150
|
// ▼▼▼ [Changed] Cap max node size: X=2000, Y=4000 ▼▼▼
|
|
1148
1151
|
const MAX_NODE_WIDTH = 2000;
|
|
1149
1152
|
const MAX_NODE_HEIGHT = 4000;
|
|
1150
1153
|
// ▲▲▲ [Changed] ▲▲▲
|
|
1151
|
-
|
|
1152
|
-
const vfov = (module.camera.fov * Math.PI) / 180;
|
|
1153
|
-
const viewHeight = 2 * Math.tan(vfov / 2) * module.camera.position.z;
|
|
1154
|
-
const viewWidth = viewHeight * module.camera.aspect;
|
|
1155
|
-
const screenToWorldFactorX = viewWidth / module.container.clientWidth;
|
|
1156
|
-
const screenToWorldFactorY = viewHeight / module.container.clientHeight;
|
|
1157
|
-
const worldDeltaX = (currentX - module.resizeStartMousePos.x) * screenToWorldFactorX;
|
|
1158
|
-
// ▼▼▼ [Fix] Negate Y delta: screen Y increases downward, world Y increases upward ▼▼▼
|
|
1159
|
-
const worldDeltaY = -(currentY - module.resizeStartMousePos.y) * screenToWorldFactorY;
|
|
1160
|
-
|
|
1161
|
-
const HANDLE_SIZE = RESIZE_HANDLE_SIZE;
|
|
1162
|
-
// ▼▼▼ [Changed] Use stored nodeId (CSS3D mode compatible) ▼▼▼
|
|
1163
|
-
const nodeId = module.resizingNodeId || (module.resizeNodeObject.userData?.nodeId || module.resizeNodeObject.element?.dataset?.nodeId);
|
|
1164
|
-
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
1165
|
-
if (!nodeEntry) {
|
|
1166
|
-
console.warn('[Pipeline] doResizing: nodeEntry not found for', nodeId);
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
const corner = module.resizeCorner || 'BR';
|
|
1171
|
-
const startW = module.resizeStartDimensions.w;
|
|
1172
|
-
const startH = module.resizeStartDimensions.h;
|
|
1173
|
-
const startX = module.resizeStartPosition.x;
|
|
1174
|
-
const startY = module.resizeStartPosition.y;
|
|
1175
|
-
|
|
1176
|
-
let finalWidth, finalHeight, newPosX, newPosY;
|
|
1177
|
-
|
|
1178
|
-
// ▼▼▼ [Changed] Eight-direction resize logic (4 corners + 4 edges) ▼▼▼
|
|
1179
|
-
// Node position is top-left corner in Three.js (Y increases upward)
|
|
1180
|
-
// Depending on handle, we compute size and position differently
|
|
1181
|
-
switch (corner) {
|
|
1182
|
-
// === Corners (diagonal resize) ===
|
|
1183
|
-
case 'TL': // Top-Left: anchor bottom-right, move top-left
|
|
1184
|
-
finalWidth = startW - worldDeltaX;
|
|
1185
|
-
finalHeight = startH + worldDeltaY;
|
|
1186
|
-
newPosX = startX + worldDeltaX;
|
|
1187
|
-
newPosY = startY + worldDeltaY;
|
|
1188
|
-
break;
|
|
1189
|
-
case 'TR': // Top-Right: anchor bottom-left, expand right/up
|
|
1190
|
-
finalWidth = startW + worldDeltaX;
|
|
1191
|
-
finalHeight = startH + worldDeltaY;
|
|
1192
|
-
newPosX = startX;
|
|
1193
|
-
newPosY = startY + worldDeltaY;
|
|
1194
|
-
break;
|
|
1195
|
-
case 'BL': // Bottom-Left: anchor top-right, expand left/down
|
|
1196
|
-
finalWidth = startW - worldDeltaX;
|
|
1197
|
-
finalHeight = startH - worldDeltaY;
|
|
1198
|
-
newPosX = startX + worldDeltaX;
|
|
1199
|
-
newPosY = startY;
|
|
1200
|
-
break;
|
|
1201
|
-
case 'BR': // Bottom-Right: anchor top-left (original behavior)
|
|
1202
|
-
finalWidth = startW + worldDeltaX;
|
|
1203
|
-
finalHeight = startH - worldDeltaY;
|
|
1204
|
-
newPosX = startX;
|
|
1205
|
-
newPosY = startY;
|
|
1206
|
-
break;
|
|
1207
|
-
// === Edges (single-direction resize) ===
|
|
1208
|
-
case 'T': // Top edge: vertical only, anchor bottom
|
|
1209
|
-
finalWidth = startW;
|
|
1210
|
-
finalHeight = startH + worldDeltaY;
|
|
1211
|
-
newPosX = startX;
|
|
1212
|
-
newPosY = startY + worldDeltaY;
|
|
1213
|
-
break;
|
|
1214
|
-
case 'B': // Bottom edge: vertical only, anchor top
|
|
1215
|
-
finalWidth = startW;
|
|
1216
|
-
finalHeight = startH - worldDeltaY;
|
|
1217
|
-
newPosX = startX;
|
|
1218
|
-
newPosY = startY;
|
|
1219
|
-
break;
|
|
1220
|
-
case 'L': // Left edge: horizontal only, anchor right
|
|
1221
|
-
finalWidth = startW - worldDeltaX;
|
|
1222
|
-
finalHeight = startH;
|
|
1223
|
-
newPosX = startX + worldDeltaX;
|
|
1224
|
-
newPosY = startY;
|
|
1225
|
-
break;
|
|
1226
|
-
case 'R': // Right edge: horizontal only, anchor left
|
|
1227
|
-
finalWidth = startW + worldDeltaX;
|
|
1228
|
-
finalHeight = startH;
|
|
1229
|
-
newPosX = startX;
|
|
1230
|
-
newPosY = startY;
|
|
1231
|
-
break;
|
|
1232
|
-
default:
|
|
1233
|
-
finalWidth = startW + worldDeltaX;
|
|
1234
|
-
finalHeight = startH - worldDeltaY;
|
|
1235
|
-
newPosX = startX;
|
|
1236
|
-
newPosY = startY;
|
|
1237
|
-
break;
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1154
|
+
|
|
1155
|
+
const vfov = (module.camera.fov * Math.PI) / 180;
|
|
1156
|
+
const viewHeight = 2 * Math.tan(vfov / 2) * module.camera.position.z;
|
|
1157
|
+
const viewWidth = viewHeight * module.camera.aspect;
|
|
1158
|
+
const screenToWorldFactorX = viewWidth / module.container.clientWidth;
|
|
1159
|
+
const screenToWorldFactorY = viewHeight / module.container.clientHeight;
|
|
1160
|
+
const worldDeltaX = (currentX - module.resizeStartMousePos.x) * screenToWorldFactorX;
|
|
1161
|
+
// ▼▼▼ [Fix] Negate Y delta: screen Y increases downward, world Y increases upward ▼▼▼
|
|
1162
|
+
const worldDeltaY = -(currentY - module.resizeStartMousePos.y) * screenToWorldFactorY;
|
|
1163
|
+
|
|
1164
|
+
const HANDLE_SIZE = RESIZE_HANDLE_SIZE;
|
|
1165
|
+
// ▼▼▼ [Changed] Use stored nodeId (CSS3D mode compatible) ▼▼▼
|
|
1166
|
+
const nodeId = module.resizingNodeId || (module.resizeNodeObject.userData?.nodeId || module.resizeNodeObject.element?.dataset?.nodeId);
|
|
1167
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
1168
|
+
if (!nodeEntry) {
|
|
1169
|
+
console.warn('[Pipeline] doResizing: nodeEntry not found for', nodeId);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const corner = module.resizeCorner || 'BR';
|
|
1174
|
+
const startW = module.resizeStartDimensions.w;
|
|
1175
|
+
const startH = module.resizeStartDimensions.h;
|
|
1176
|
+
const startX = module.resizeStartPosition.x;
|
|
1177
|
+
const startY = module.resizeStartPosition.y;
|
|
1178
|
+
|
|
1179
|
+
let finalWidth, finalHeight, newPosX, newPosY;
|
|
1180
|
+
|
|
1181
|
+
// ▼▼▼ [Changed] Eight-direction resize logic (4 corners + 4 edges) ▼▼▼
|
|
1182
|
+
// Node position is top-left corner in Three.js (Y increases upward)
|
|
1183
|
+
// Depending on handle, we compute size and position differently
|
|
1184
|
+
switch (corner) {
|
|
1185
|
+
// === Corners (diagonal resize) ===
|
|
1186
|
+
case 'TL': // Top-Left: anchor bottom-right, move top-left
|
|
1187
|
+
finalWidth = startW - worldDeltaX;
|
|
1188
|
+
finalHeight = startH + worldDeltaY;
|
|
1189
|
+
newPosX = startX + worldDeltaX;
|
|
1190
|
+
newPosY = startY + worldDeltaY;
|
|
1191
|
+
break;
|
|
1192
|
+
case 'TR': // Top-Right: anchor bottom-left, expand right/up
|
|
1193
|
+
finalWidth = startW + worldDeltaX;
|
|
1194
|
+
finalHeight = startH + worldDeltaY;
|
|
1195
|
+
newPosX = startX;
|
|
1196
|
+
newPosY = startY + worldDeltaY;
|
|
1197
|
+
break;
|
|
1198
|
+
case 'BL': // Bottom-Left: anchor top-right, expand left/down
|
|
1199
|
+
finalWidth = startW - worldDeltaX;
|
|
1200
|
+
finalHeight = startH - worldDeltaY;
|
|
1201
|
+
newPosX = startX + worldDeltaX;
|
|
1202
|
+
newPosY = startY;
|
|
1203
|
+
break;
|
|
1204
|
+
case 'BR': // Bottom-Right: anchor top-left (original behavior)
|
|
1205
|
+
finalWidth = startW + worldDeltaX;
|
|
1206
|
+
finalHeight = startH - worldDeltaY;
|
|
1207
|
+
newPosX = startX;
|
|
1208
|
+
newPosY = startY;
|
|
1209
|
+
break;
|
|
1210
|
+
// === Edges (single-direction resize) ===
|
|
1211
|
+
case 'T': // Top edge: vertical only, anchor bottom
|
|
1212
|
+
finalWidth = startW;
|
|
1213
|
+
finalHeight = startH + worldDeltaY;
|
|
1214
|
+
newPosX = startX;
|
|
1215
|
+
newPosY = startY + worldDeltaY;
|
|
1216
|
+
break;
|
|
1217
|
+
case 'B': // Bottom edge: vertical only, anchor top
|
|
1218
|
+
finalWidth = startW;
|
|
1219
|
+
finalHeight = startH - worldDeltaY;
|
|
1220
|
+
newPosX = startX;
|
|
1221
|
+
newPosY = startY;
|
|
1222
|
+
break;
|
|
1223
|
+
case 'L': // Left edge: horizontal only, anchor right
|
|
1224
|
+
finalWidth = startW - worldDeltaX;
|
|
1225
|
+
finalHeight = startH;
|
|
1226
|
+
newPosX = startX + worldDeltaX;
|
|
1227
|
+
newPosY = startY;
|
|
1228
|
+
break;
|
|
1229
|
+
case 'R': // Right edge: horizontal only, anchor left
|
|
1230
|
+
finalWidth = startW + worldDeltaX;
|
|
1231
|
+
finalHeight = startH;
|
|
1232
|
+
newPosX = startX;
|
|
1233
|
+
newPosY = startY;
|
|
1234
|
+
break;
|
|
1235
|
+
default:
|
|
1236
|
+
finalWidth = startW + worldDeltaX;
|
|
1237
|
+
finalHeight = startH - worldDeltaY;
|
|
1238
|
+
newPosX = startX;
|
|
1239
|
+
newPosY = startY;
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1240
1243
|
// Clamp size
|
|
1241
1244
|
const minClampedSize = clampRemoteFleetResizeDimensions(nodeEntry.model, finalWidth, finalHeight);
|
|
1242
1245
|
let clampedWidth = Math.min(MAX_NODE_WIDTH, minClampedSize.width);
|
|
1243
1246
|
let clampedHeight = Math.min(MAX_NODE_HEIGHT, minClampedSize.height);
|
|
1244
|
-
|
|
1245
|
-
// ▼▼▼ [Image Resize Fix] Remove specific Ratio Preservation for now (Treat as normal nodes) ▼▼▼
|
|
1246
|
-
// Normal nodes Logic (apply to all)
|
|
1247
|
-
// Adjust position if size was clamped (keep anchor point fixed)
|
|
1248
|
-
if (corner === 'TL' || corner === 'BL' || corner === 'L') {
|
|
1249
|
-
// Left handles: position adjusts with width changes
|
|
1250
|
-
const widthDiff = finalWidth - clampedWidth;
|
|
1251
|
-
newPosX += widthDiff;
|
|
1252
|
-
}
|
|
1253
|
-
if (corner === 'TL' || corner === 'TR' || corner === 'T') {
|
|
1254
|
-
// Top handles: position adjusts with height changes
|
|
1255
|
-
const heightDiff = finalHeight - clampedHeight;
|
|
1256
|
-
newPosY -= heightDiff;
|
|
1257
|
-
}
|
|
1258
|
-
// ▲▲▲ [Image Resize Fix] ▲▲▲
|
|
1259
|
-
// ▲▲▲ [Image Resize Fix] ▲▲▲
|
|
1260
|
-
|
|
1261
|
-
finalWidth = clampedWidth;
|
|
1262
|
-
finalHeight = clampedHeight;
|
|
1263
|
-
// ▲▲▲ [Changed] ▲▲▲
|
|
1264
|
-
|
|
1265
|
-
// ▼▼▼ [Refactor] 스냅 현상 제거: 실수형 크기/좌표 그대로 사용 ▼▼▼
|
|
1266
|
-
nodeEntry.model.width = finalWidth;
|
|
1267
|
-
nodeEntry.model.height = finalHeight;
|
|
1268
|
-
|
|
1269
|
-
// ▼▼▼ [Changed] Update node position for corner resizing ▼▼▼
|
|
1270
|
-
if (nodeEntry.glObject) {
|
|
1271
|
-
nodeEntry.glObject.position.x = newPosX;
|
|
1272
|
-
nodeEntry.glObject.position.y = newPosY;
|
|
1273
|
-
}
|
|
1274
|
-
if (nodeEntry.cssObject) {
|
|
1275
|
-
nodeEntry.cssObject.position.x = newPosX;
|
|
1276
|
-
nodeEntry.cssObject.position.y = newPosY;
|
|
1277
|
-
// ▼▼▼ [Fix] CSS3D matrixWorld 즉시 갱신하여 CSS3DRenderer가 새 위치를 반영하도록 보장 ▼▼▼
|
|
1278
|
-
nodeEntry.cssObject.updateMatrixWorld(true);
|
|
1279
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
1280
|
-
}
|
|
1281
|
-
// ▼▼▼ [Fix] model position도 동기화하여 다른 sync 로직과의 일관성 보장 ▼▼▼
|
|
1282
|
-
nodeEntry.model.positionX = newPosX;
|
|
1283
|
-
nodeEntry.model.positionY = newPosY;
|
|
1284
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
1285
|
-
// ▲▲▲ [Changed] ▲▲▲
|
|
1286
|
-
|
|
1247
|
+
|
|
1248
|
+
// ▼▼▼ [Image Resize Fix] Remove specific Ratio Preservation for now (Treat as normal nodes) ▼▼▼
|
|
1249
|
+
// Normal nodes Logic (apply to all)
|
|
1250
|
+
// Adjust position if size was clamped (keep anchor point fixed)
|
|
1251
|
+
if (corner === 'TL' || corner === 'BL' || corner === 'L') {
|
|
1252
|
+
// Left handles: position adjusts with width changes
|
|
1253
|
+
const widthDiff = finalWidth - clampedWidth;
|
|
1254
|
+
newPosX += widthDiff;
|
|
1255
|
+
}
|
|
1256
|
+
if (corner === 'TL' || corner === 'TR' || corner === 'T') {
|
|
1257
|
+
// Top handles: position adjusts with height changes
|
|
1258
|
+
const heightDiff = finalHeight - clampedHeight;
|
|
1259
|
+
newPosY -= heightDiff;
|
|
1260
|
+
}
|
|
1261
|
+
// ▲▲▲ [Image Resize Fix] ▲▲▲
|
|
1262
|
+
// ▲▲▲ [Image Resize Fix] ▲▲▲
|
|
1263
|
+
|
|
1264
|
+
finalWidth = clampedWidth;
|
|
1265
|
+
finalHeight = clampedHeight;
|
|
1266
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
1267
|
+
|
|
1268
|
+
// ▼▼▼ [Refactor] 스냅 현상 제거: 실수형 크기/좌표 그대로 사용 ▼▼▼
|
|
1269
|
+
nodeEntry.model.width = finalWidth;
|
|
1270
|
+
nodeEntry.model.height = finalHeight;
|
|
1271
|
+
|
|
1272
|
+
// ▼▼▼ [Changed] Update node position for corner resizing ▼▼▼
|
|
1273
|
+
if (nodeEntry.glObject) {
|
|
1274
|
+
nodeEntry.glObject.position.x = newPosX;
|
|
1275
|
+
nodeEntry.glObject.position.y = newPosY;
|
|
1276
|
+
}
|
|
1277
|
+
if (nodeEntry.cssObject) {
|
|
1278
|
+
nodeEntry.cssObject.position.x = newPosX;
|
|
1279
|
+
nodeEntry.cssObject.position.y = newPosY;
|
|
1280
|
+
// ▼▼▼ [Fix] CSS3D matrixWorld 즉시 갱신하여 CSS3DRenderer가 새 위치를 반영하도록 보장 ▼▼▼
|
|
1281
|
+
nodeEntry.cssObject.updateMatrixWorld(true);
|
|
1282
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1283
|
+
}
|
|
1284
|
+
// ▼▼▼ [Fix] model position도 동기화하여 다른 sync 로직과의 일관성 보장 ▼▼▼
|
|
1285
|
+
nodeEntry.model.positionX = newPosX;
|
|
1286
|
+
nodeEntry.model.positionY = newPosY;
|
|
1287
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1288
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
1289
|
+
|
|
1287
1290
|
if (nodeEntry.cssObject) {
|
|
1288
1291
|
const wrapper = nodeEntry.cssObject.element;
|
|
1289
|
-
// ▼▼▼ [Fix] CSS3D 노드는 resolutionScale을 고려하여 DOM 크기 설정 ▼▼▼
|
|
1290
|
-
// CSS3D 객체의 scale은 1/resolutionScale이므로, DOM 크기는 worldSize * resolutionScale
|
|
1291
|
-
const resolutionScale = nodeEntry.cssObject.userData?.resolutionScale || 1;
|
|
1292
|
-
const domWidth = nodeEntry.model.width * resolutionScale;
|
|
1293
|
-
const domHeight = nodeEntry.model.height * resolutionScale;
|
|
1294
|
-
|
|
1292
|
+
// ▼▼▼ [Fix] CSS3D 노드는 resolutionScale을 고려하여 DOM 크기 설정 ▼▼▼
|
|
1293
|
+
// CSS3D 객체의 scale은 1/resolutionScale이므로, DOM 크기는 worldSize * resolutionScale
|
|
1294
|
+
const resolutionScale = nodeEntry.cssObject.userData?.resolutionScale || 1;
|
|
1295
|
+
const domWidth = nodeEntry.model.width * resolutionScale;
|
|
1296
|
+
const domHeight = nodeEntry.model.height * resolutionScale;
|
|
1297
|
+
|
|
1295
1298
|
wrapper.style.width = domWidth + 'px';
|
|
1296
1299
|
wrapper.style.height = domHeight + 'px';
|
|
1297
1300
|
wrapper.style.minWidth = domWidth + 'px';
|
|
@@ -1310,27 +1313,27 @@
|
|
|
1310
1313
|
innerContent.style.maxHeight = nodeEntry.model.height + 'px';
|
|
1311
1314
|
|
|
1312
1315
|
// ▼▼▼ [Fix] 이미지 노드의 경우 내부 img 요소 크기도 업데이트 ▼▼▼
|
|
1313
|
-
if (nodeEntry.model.contentType === 'image') {
|
|
1314
|
-
const imgEl = innerContent.querySelector('img');
|
|
1315
|
-
if (imgEl) {
|
|
1316
|
-
imgEl.style.width = '100%';
|
|
1317
|
-
imgEl.style.height = '100%';
|
|
1318
|
-
}
|
|
1319
|
-
// contentWrapper와 responseDiv 크기도 업데이트
|
|
1320
|
-
const contentWrapper = innerContent.querySelector('.node-content-wrapper');
|
|
1321
|
-
if (contentWrapper) {
|
|
1322
|
-
contentWrapper.style.width = '100%';
|
|
1323
|
-
contentWrapper.style.height = '100%';
|
|
1324
|
-
}
|
|
1325
|
-
const responseDiv = innerContent.querySelector('.node-response');
|
|
1326
|
-
if (responseDiv) {
|
|
1327
|
-
responseDiv.style.width = '100%';
|
|
1328
|
-
responseDiv.style.height = '100%';
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1316
|
+
if (nodeEntry.model.contentType === 'image') {
|
|
1317
|
+
const imgEl = innerContent.querySelector('img');
|
|
1318
|
+
if (imgEl) {
|
|
1319
|
+
imgEl.style.width = '100%';
|
|
1320
|
+
imgEl.style.height = '100%';
|
|
1321
|
+
}
|
|
1322
|
+
// contentWrapper와 responseDiv 크기도 업데이트
|
|
1323
|
+
const contentWrapper = innerContent.querySelector('.node-content-wrapper');
|
|
1324
|
+
if (contentWrapper) {
|
|
1325
|
+
contentWrapper.style.width = '100%';
|
|
1326
|
+
contentWrapper.style.height = '100%';
|
|
1327
|
+
}
|
|
1328
|
+
const responseDiv = innerContent.querySelector('.node-response');
|
|
1329
|
+
if (responseDiv) {
|
|
1330
|
+
responseDiv.style.width = '100%';
|
|
1331
|
+
responseDiv.style.height = '100%';
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1334
1337
|
// ▼▼▼ [Fix] userData도 업데이트하여 syncCss3dContent에서 올바른 크기 사용 ▼▼▼
|
|
1335
1338
|
nodeEntry.cssObject.userData.worldWidth = nodeEntry.model.width;
|
|
1336
1339
|
nodeEntry.cssObject.userData.worldHeight = nodeEntry.model.height;
|
|
@@ -1344,17 +1347,17 @@
|
|
|
1344
1347
|
|
|
1345
1348
|
if (nodeEntry.glObject) {
|
|
1346
1349
|
const group = nodeEntry.glObject;
|
|
1347
|
-
|
|
1348
|
-
const bodyMesh = group.getObjectByName('body');
|
|
1349
|
-
const outline = group.getObjectByName('outline');
|
|
1350
|
-
let glowMesh = group.getObjectByName('glow');
|
|
1351
|
-
if (!ENABLE_WEBGL_GLOW) {
|
|
1352
|
-
glowMesh = removeGlowMesh(group);
|
|
1353
|
-
}
|
|
1354
|
-
const CORNER_RADIUS = 20;
|
|
1355
|
-
const OUTLINE_SHRINK = 2;
|
|
1356
|
-
const OUTLINE_RADIUS = Math.max(1, CORNER_RADIUS - 1);
|
|
1357
|
-
const CHAMFER_SIZE = 16;
|
|
1350
|
+
|
|
1351
|
+
const bodyMesh = group.getObjectByName('body');
|
|
1352
|
+
const outline = group.getObjectByName('outline');
|
|
1353
|
+
let glowMesh = group.getObjectByName('glow');
|
|
1354
|
+
if (!ENABLE_WEBGL_GLOW) {
|
|
1355
|
+
glowMesh = removeGlowMesh(group);
|
|
1356
|
+
}
|
|
1357
|
+
const CORNER_RADIUS = 20;
|
|
1358
|
+
const OUTLINE_SHRINK = 2;
|
|
1359
|
+
const OUTLINE_RADIUS = Math.max(1, CORNER_RADIUS - 1);
|
|
1360
|
+
const CHAMFER_SIZE = 16;
|
|
1358
1361
|
if (bodyMesh) {
|
|
1359
1362
|
bodyMesh.geometry.dispose();
|
|
1360
1363
|
bodyMesh.geometry = new THREE.PlaneGeometry(nodeEntry.model.width, nodeEntry.model.height);
|
|
@@ -1366,15 +1369,15 @@
|
|
|
1366
1369
|
if (nodeEntry.currentResizeType === 'CSS') {
|
|
1367
1370
|
bodyMesh.visible = false;
|
|
1368
1371
|
}
|
|
1369
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
1370
|
-
|
|
1371
|
-
// ▼▼▼ [Image Resize Fix] Don't clear map during resize to prevent white flash/loss ▼▼▼
|
|
1372
|
-
// if (bodyMesh.material.map) {
|
|
1373
|
-
// bodyMesh.material.map = null;
|
|
1374
|
-
// bodyMesh.material.color.setHex(0xffffff);
|
|
1375
|
-
// bodyMesh.material.needsUpdate = true;
|
|
1376
|
-
// }
|
|
1377
|
-
// ▲▲▲ [Image Resize Fix] ▲▲▲
|
|
1372
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1373
|
+
|
|
1374
|
+
// ▼▼▼ [Image Resize Fix] Don't clear map during resize to prevent white flash/loss ▼▼▼
|
|
1375
|
+
// if (bodyMesh.material.map) {
|
|
1376
|
+
// bodyMesh.material.map = null;
|
|
1377
|
+
// bodyMesh.material.color.setHex(0xffffff);
|
|
1378
|
+
// bodyMesh.material.needsUpdate = true;
|
|
1379
|
+
// }
|
|
1380
|
+
// ▲▲▲ [Image Resize Fix] ▲▲▲
|
|
1378
1381
|
const isSelected = module.multiSelectedNodeIds.has(module.resizingNodeId) || module.selectedNodeIdJs === module.resizingNodeId;
|
|
1379
1382
|
if (nodeEntry.model.contentType === 'image') {
|
|
1380
1383
|
const usesCssViewDuringResize =
|
|
@@ -1412,87 +1415,87 @@
|
|
|
1412
1415
|
outline.material.opacity = 1.0;
|
|
1413
1416
|
}
|
|
1414
1417
|
}
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
// ▼▼▼ [Changed] Update glow position during resize (keep visible, no flickering) ▼▼▼
|
|
1418
|
-
if (ENABLE_WEBGL_GLOW && glowMesh) {
|
|
1419
|
-
// Update position to match bodyMesh
|
|
1420
|
-
glowMesh.position.set(nodeEntry.model.width / 2, -nodeEntry.model.height / 2, 0); // [Fix] Z=0
|
|
1421
|
-
// Don't hide - causes flickering
|
|
1422
|
-
}
|
|
1423
|
-
// ▲▲▲ [Changed] ▲▲▲
|
|
1424
|
-
const tailMesh = group.getObjectByName('tail');
|
|
1425
|
-
if (tailMesh) {
|
|
1426
|
-
tailMesh.position.set(nodeEntry.model.width / 2, -nodeEntry.model.height - 6, 0);
|
|
1427
|
-
}
|
|
1428
|
-
// ▼▼▼ [Changed] Update eight resize handles (4 corners + 4 edges with geometry) ▼▼▼
|
|
1429
|
-
const edgeWidthLength = Math.max(10, nodeEntry.model.width - HANDLE_SIZE * 2);
|
|
1430
|
-
const edgeHeightLength = Math.max(10, nodeEntry.model.height - HANDLE_SIZE * 2);
|
|
1431
|
-
const handleConfigs = {
|
|
1432
|
-
// ▼▼▼ [Fix] Corners Z=0 for parallax elimination ▼▼▼
|
|
1433
|
-
'resizeHandle_TL': { x: HANDLE_SIZE / 2, y: -HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1434
|
-
'resizeHandle_TR': { x: nodeEntry.model.width - HANDLE_SIZE / 2, y: -HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1435
|
-
'resizeHandle_BL': { x: HANDLE_SIZE / 2, y: -nodeEntry.model.height + HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1436
|
-
'resizeHandle_BR': { x: nodeEntry.model.width - HANDLE_SIZE / 2, y: -nodeEntry.model.height + HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1437
|
-
// Edges Z=0
|
|
1438
|
-
'resizeHandle_T': { x: nodeEntry.model.width / 2, y: -HANDLE_SIZE / 2, w: edgeWidthLength, h: HANDLE_SIZE, z: 0 },
|
|
1439
|
-
'resizeHandle_B': { x: nodeEntry.model.width / 2, y: -nodeEntry.model.height + HANDLE_SIZE / 2, w: edgeWidthLength, h: HANDLE_SIZE, z: 0 },
|
|
1440
|
-
'resizeHandle_L': { x: HANDLE_SIZE / 2, y: -nodeEntry.model.height / 2, w: HANDLE_SIZE, h: edgeHeightLength, z: 0 },
|
|
1441
|
-
'resizeHandle_R': { x: nodeEntry.model.width - HANDLE_SIZE / 2, y: -nodeEntry.model.height / 2, w: HANDLE_SIZE, h: edgeHeightLength, z: 0 }
|
|
1442
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
1443
|
-
};
|
|
1444
|
-
Object.entries(handleConfigs).forEach(([name, cfg]) => {
|
|
1445
|
-
const handle = group.getObjectByName(name);
|
|
1446
|
-
if (handle) {
|
|
1447
|
-
handle.position.set(cfg.x, cfg.y, cfg.z);
|
|
1448
|
-
const gp = handle.geometry?.parameters;
|
|
1449
|
-
if (gp && (gp.width !== cfg.w || gp.height !== cfg.h)) {
|
|
1450
|
-
handle.geometry.dispose();
|
|
1451
|
-
handle.geometry = new THREE.PlaneGeometry(cfg.w, cfg.h);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
});
|
|
1455
|
-
// ▲▲▲ [Changed] ▲▲▲
|
|
1456
|
-
|
|
1457
|
-
// ▼▼▼ [New] Update Troika text clipRect on resize for code nodes ▼▼▼
|
|
1458
|
-
if (nodeEntry.model.contentType === 'code') {
|
|
1459
|
-
const troikaText = group.userData.troikaText;
|
|
1460
|
-
if (troikaText && troikaText.userData) {
|
|
1461
|
-
const padding = troikaText.userData.padding || 16;
|
|
1462
|
-
const filenameHeight = troikaText.userData.filenameHeight || 0; // ★ Include filename height
|
|
1463
|
-
const newWidth = nodeEntry.model.width;
|
|
1464
|
-
const newHeight = nodeEntry.model.height;
|
|
1465
|
-
const newTextWidth = newWidth - padding * 2;
|
|
1466
|
-
const newVisibleHeight = newHeight - padding * 2 - filenameHeight; // ★ Subtract filename height
|
|
1467
|
-
|
|
1468
|
-
// Update userData dimensions
|
|
1469
|
-
troikaText.userData.nodeWidth = newWidth;
|
|
1470
|
-
troikaText.userData.nodeHeight = newHeight;
|
|
1471
|
-
troikaText.userData.textWidth = newTextWidth;
|
|
1472
|
-
troikaText.userData.visibleHeight = newVisibleHeight;
|
|
1473
|
-
|
|
1474
|
-
// ★ DO NOT update maxWidth - code nodes should keep maxWidth: Infinity (no word wrap)
|
|
1475
|
-
// troikaText.maxWidth = newTextWidth; // REMOVED - this forces word wrap
|
|
1476
|
-
|
|
1477
|
-
// Keep existing scroll and recalc maxScroll based on totalHeight
|
|
1478
|
-
const totalHeight = troikaText.userData.totalHeight || newVisibleHeight;
|
|
1479
|
-
const maxScroll = Math.max(0, totalHeight - newVisibleHeight);
|
|
1480
|
-
nodeEntry.model.maxScroll = maxScroll;
|
|
1481
|
-
|
|
1482
|
-
// Clamp scroll position
|
|
1483
|
-
let scrollY = troikaText.userData.scrollY || 0;
|
|
1484
|
-
if (scrollY > maxScroll) {
|
|
1485
|
-
scrollY = maxScroll;
|
|
1486
|
-
troikaText.userData.scrollY = scrollY;
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
// Update clipRect - clip to visible area only
|
|
1490
|
-
const clipTop = -scrollY;
|
|
1491
|
-
const clipBottom = -scrollY - newVisibleHeight;
|
|
1492
|
-
troikaText.clipRect = [0, clipBottom, newTextWidth, clipTop];
|
|
1493
|
-
troikaText.sync();
|
|
1494
|
-
|
|
1495
|
-
// Update scrollbar - show/hide based on whether scrolling is needed
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// ▼▼▼ [Changed] Update glow position during resize (keep visible, no flickering) ▼▼▼
|
|
1421
|
+
if (ENABLE_WEBGL_GLOW && glowMesh) {
|
|
1422
|
+
// Update position to match bodyMesh
|
|
1423
|
+
glowMesh.position.set(nodeEntry.model.width / 2, -nodeEntry.model.height / 2, 0); // [Fix] Z=0
|
|
1424
|
+
// Don't hide - causes flickering
|
|
1425
|
+
}
|
|
1426
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
1427
|
+
const tailMesh = group.getObjectByName('tail');
|
|
1428
|
+
if (tailMesh) {
|
|
1429
|
+
tailMesh.position.set(nodeEntry.model.width / 2, -nodeEntry.model.height - 6, 0);
|
|
1430
|
+
}
|
|
1431
|
+
// ▼▼▼ [Changed] Update eight resize handles (4 corners + 4 edges with geometry) ▼▼▼
|
|
1432
|
+
const edgeWidthLength = Math.max(10, nodeEntry.model.width - HANDLE_SIZE * 2);
|
|
1433
|
+
const edgeHeightLength = Math.max(10, nodeEntry.model.height - HANDLE_SIZE * 2);
|
|
1434
|
+
const handleConfigs = {
|
|
1435
|
+
// ▼▼▼ [Fix] Corners Z=0 for parallax elimination ▼▼▼
|
|
1436
|
+
'resizeHandle_TL': { x: HANDLE_SIZE / 2, y: -HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1437
|
+
'resizeHandle_TR': { x: nodeEntry.model.width - HANDLE_SIZE / 2, y: -HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1438
|
+
'resizeHandle_BL': { x: HANDLE_SIZE / 2, y: -nodeEntry.model.height + HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1439
|
+
'resizeHandle_BR': { x: nodeEntry.model.width - HANDLE_SIZE / 2, y: -nodeEntry.model.height + HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1440
|
+
// Edges Z=0
|
|
1441
|
+
'resizeHandle_T': { x: nodeEntry.model.width / 2, y: -HANDLE_SIZE / 2, w: edgeWidthLength, h: HANDLE_SIZE, z: 0 },
|
|
1442
|
+
'resizeHandle_B': { x: nodeEntry.model.width / 2, y: -nodeEntry.model.height + HANDLE_SIZE / 2, w: edgeWidthLength, h: HANDLE_SIZE, z: 0 },
|
|
1443
|
+
'resizeHandle_L': { x: HANDLE_SIZE / 2, y: -nodeEntry.model.height / 2, w: HANDLE_SIZE, h: edgeHeightLength, z: 0 },
|
|
1444
|
+
'resizeHandle_R': { x: nodeEntry.model.width - HANDLE_SIZE / 2, y: -nodeEntry.model.height / 2, w: HANDLE_SIZE, h: edgeHeightLength, z: 0 }
|
|
1445
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1446
|
+
};
|
|
1447
|
+
Object.entries(handleConfigs).forEach(([name, cfg]) => {
|
|
1448
|
+
const handle = group.getObjectByName(name);
|
|
1449
|
+
if (handle) {
|
|
1450
|
+
handle.position.set(cfg.x, cfg.y, cfg.z);
|
|
1451
|
+
const gp = handle.geometry?.parameters;
|
|
1452
|
+
if (gp && (gp.width !== cfg.w || gp.height !== cfg.h)) {
|
|
1453
|
+
handle.geometry.dispose();
|
|
1454
|
+
handle.geometry = new THREE.PlaneGeometry(cfg.w, cfg.h);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
1459
|
+
|
|
1460
|
+
// ▼▼▼ [New] Update Troika text clipRect on resize for code nodes ▼▼▼
|
|
1461
|
+
if (nodeEntry.model.contentType === 'code') {
|
|
1462
|
+
const troikaText = group.userData.troikaText;
|
|
1463
|
+
if (troikaText && troikaText.userData) {
|
|
1464
|
+
const padding = troikaText.userData.padding || 16;
|
|
1465
|
+
const filenameHeight = troikaText.userData.filenameHeight || 0; // ★ Include filename height
|
|
1466
|
+
const newWidth = nodeEntry.model.width;
|
|
1467
|
+
const newHeight = nodeEntry.model.height;
|
|
1468
|
+
const newTextWidth = newWidth - padding * 2;
|
|
1469
|
+
const newVisibleHeight = newHeight - padding * 2 - filenameHeight; // ★ Subtract filename height
|
|
1470
|
+
|
|
1471
|
+
// Update userData dimensions
|
|
1472
|
+
troikaText.userData.nodeWidth = newWidth;
|
|
1473
|
+
troikaText.userData.nodeHeight = newHeight;
|
|
1474
|
+
troikaText.userData.textWidth = newTextWidth;
|
|
1475
|
+
troikaText.userData.visibleHeight = newVisibleHeight;
|
|
1476
|
+
|
|
1477
|
+
// ★ DO NOT update maxWidth - code nodes should keep maxWidth: Infinity (no word wrap)
|
|
1478
|
+
// troikaText.maxWidth = newTextWidth; // REMOVED - this forces word wrap
|
|
1479
|
+
|
|
1480
|
+
// Keep existing scroll and recalc maxScroll based on totalHeight
|
|
1481
|
+
const totalHeight = troikaText.userData.totalHeight || newVisibleHeight;
|
|
1482
|
+
const maxScroll = Math.max(0, totalHeight - newVisibleHeight);
|
|
1483
|
+
nodeEntry.model.maxScroll = maxScroll;
|
|
1484
|
+
|
|
1485
|
+
// Clamp scroll position
|
|
1486
|
+
let scrollY = troikaText.userData.scrollY || 0;
|
|
1487
|
+
if (scrollY > maxScroll) {
|
|
1488
|
+
scrollY = maxScroll;
|
|
1489
|
+
troikaText.userData.scrollY = scrollY;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Update clipRect - clip to visible area only
|
|
1493
|
+
const clipTop = -scrollY;
|
|
1494
|
+
const clipBottom = -scrollY - newVisibleHeight;
|
|
1495
|
+
troikaText.clipRect = [0, clipBottom, newTextWidth, clipTop];
|
|
1496
|
+
troikaText.sync();
|
|
1497
|
+
|
|
1498
|
+
// Update scrollbar - show/hide based on whether scrolling is needed
|
|
1496
1499
|
const scrollbarTrack = group.getObjectByName('scrollbarTrack');
|
|
1497
1500
|
const scrollbarThumb = group.getObjectByName('scrollbarThumb');
|
|
1498
1501
|
const isCssMode = nodeEntry.currentType === 'CSS';
|
|
@@ -1505,131 +1508,131 @@
|
|
|
1505
1508
|
} else {
|
|
1506
1509
|
scrollbarTrack.visible = true;
|
|
1507
1510
|
scrollbarThumb.visible = true;
|
|
1508
|
-
|
|
1509
|
-
const SCROLLBAR_WIDTH = 6;
|
|
1510
|
-
const SCROLLBAR_PADDING = 4;
|
|
1511
|
-
const SCROLLBAR_TOP_MARGIN = 16;
|
|
1512
|
-
const SCROLLBAR_BOTTOM_MARGIN = 16;
|
|
1513
|
-
const SCROLLBAR_RADIUS = 3;
|
|
1514
|
-
const trackHeight = newHeight - SCROLLBAR_TOP_MARGIN - SCROLLBAR_BOTTOM_MARGIN - filenameHeight; // ★ Include filenameHeight
|
|
1515
|
-
const trackX = newWidth - SCROLLBAR_WIDTH - SCROLLBAR_PADDING;
|
|
1516
|
-
const trackY = -SCROLLBAR_TOP_MARGIN - filenameHeight; // ★ Include filenameHeight
|
|
1517
|
-
|
|
1518
|
-
// Update track position and geometry
|
|
1519
|
-
scrollbarTrack.position.set(trackX, trackY, 3);
|
|
1520
|
-
if (scrollbarTrack.geometry) {
|
|
1521
|
-
scrollbarTrack.geometry.dispose();
|
|
1522
|
-
if (window.MindMapObjectManager?.createRoundedRectFilledGeometry) {
|
|
1523
|
-
scrollbarTrack.geometry = window.MindMapObjectManager.createRoundedRectFilledGeometry(SCROLLBAR_WIDTH, trackHeight, SCROLLBAR_RADIUS);
|
|
1524
|
-
} else {
|
|
1525
|
-
scrollbarTrack.geometry = new THREE.PlaneGeometry(SCROLLBAR_WIDTH, trackHeight);
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
// Update thumb
|
|
1530
|
-
const thumbHeightRatio = Math.min(1, newVisibleHeight / (maxScroll + newVisibleHeight));
|
|
1531
|
-
const thumbHeight = Math.max(24, trackHeight * thumbHeightRatio);
|
|
1532
|
-
const scrollRatio = scrollY / maxScroll;
|
|
1533
|
-
const thumbTravel = trackHeight - thumbHeight;
|
|
1534
|
-
const thumbY = trackY - (scrollRatio * thumbTravel); // ★ Use trackY which includes filenameHeight
|
|
1535
|
-
|
|
1536
|
-
scrollbarThumb.position.set(trackX, thumbY, 3.1);
|
|
1537
|
-
if (scrollbarThumb.geometry) {
|
|
1538
|
-
scrollbarThumb.geometry.dispose();
|
|
1539
|
-
if (window.MindMapObjectManager?.createRoundedRectFilledGeometry) {
|
|
1540
|
-
scrollbarThumb.geometry = window.MindMapObjectManager.createRoundedRectFilledGeometry(SCROLLBAR_WIDTH, thumbHeight, SCROLLBAR_RADIUS);
|
|
1541
|
-
} else {
|
|
1542
|
-
scrollbarThumb.geometry = new THREE.PlaneGeometry(SCROLLBAR_WIDTH, thumbHeight);
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
scrollbarThumb.userData.trackHeight = trackHeight;
|
|
1546
|
-
scrollbarThumb.userData.thumbHeight = thumbHeight;
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
// ▲▲▲ [New] ▲▲▲
|
|
1552
|
-
|
|
1553
|
-
// ▼▼▼ [New] Update glow shader dimensions on resize ▼▼▼
|
|
1554
|
-
if (ENABLE_WEBGL_GLOW && glowMesh && window.MindMapGlowShader?.updateGlowSize) {
|
|
1555
|
-
window.MindMapGlowShader.updateGlowSize(glowMesh, nodeEntry.model.width, nodeEntry.model.height, nodeEntry.model.contentType);
|
|
1556
|
-
}
|
|
1557
|
-
// ▲▲▲ [New] ▲▲▲
|
|
1558
|
-
|
|
1559
|
-
group.userData.worldWidth = nodeEntry.model.width;
|
|
1560
|
-
group.userData.worldHeight = nodeEntry.model.height;
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1511
|
+
|
|
1512
|
+
const SCROLLBAR_WIDTH = 6;
|
|
1513
|
+
const SCROLLBAR_PADDING = 4;
|
|
1514
|
+
const SCROLLBAR_TOP_MARGIN = 16;
|
|
1515
|
+
const SCROLLBAR_BOTTOM_MARGIN = 16;
|
|
1516
|
+
const SCROLLBAR_RADIUS = 3;
|
|
1517
|
+
const trackHeight = newHeight - SCROLLBAR_TOP_MARGIN - SCROLLBAR_BOTTOM_MARGIN - filenameHeight; // ★ Include filenameHeight
|
|
1518
|
+
const trackX = newWidth - SCROLLBAR_WIDTH - SCROLLBAR_PADDING;
|
|
1519
|
+
const trackY = -SCROLLBAR_TOP_MARGIN - filenameHeight; // ★ Include filenameHeight
|
|
1520
|
+
|
|
1521
|
+
// Update track position and geometry
|
|
1522
|
+
scrollbarTrack.position.set(trackX, trackY, 3);
|
|
1523
|
+
if (scrollbarTrack.geometry) {
|
|
1524
|
+
scrollbarTrack.geometry.dispose();
|
|
1525
|
+
if (window.MindMapObjectManager?.createRoundedRectFilledGeometry) {
|
|
1526
|
+
scrollbarTrack.geometry = window.MindMapObjectManager.createRoundedRectFilledGeometry(SCROLLBAR_WIDTH, trackHeight, SCROLLBAR_RADIUS);
|
|
1527
|
+
} else {
|
|
1528
|
+
scrollbarTrack.geometry = new THREE.PlaneGeometry(SCROLLBAR_WIDTH, trackHeight);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Update thumb
|
|
1533
|
+
const thumbHeightRatio = Math.min(1, newVisibleHeight / (maxScroll + newVisibleHeight));
|
|
1534
|
+
const thumbHeight = Math.max(24, trackHeight * thumbHeightRatio);
|
|
1535
|
+
const scrollRatio = scrollY / maxScroll;
|
|
1536
|
+
const thumbTravel = trackHeight - thumbHeight;
|
|
1537
|
+
const thumbY = trackY - (scrollRatio * thumbTravel); // ★ Use trackY which includes filenameHeight
|
|
1538
|
+
|
|
1539
|
+
scrollbarThumb.position.set(trackX, thumbY, 3.1);
|
|
1540
|
+
if (scrollbarThumb.geometry) {
|
|
1541
|
+
scrollbarThumb.geometry.dispose();
|
|
1542
|
+
if (window.MindMapObjectManager?.createRoundedRectFilledGeometry) {
|
|
1543
|
+
scrollbarThumb.geometry = window.MindMapObjectManager.createRoundedRectFilledGeometry(SCROLLBAR_WIDTH, thumbHeight, SCROLLBAR_RADIUS);
|
|
1544
|
+
} else {
|
|
1545
|
+
scrollbarThumb.geometry = new THREE.PlaneGeometry(SCROLLBAR_WIDTH, thumbHeight);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
scrollbarThumb.userData.trackHeight = trackHeight;
|
|
1549
|
+
scrollbarThumb.userData.thumbHeight = thumbHeight;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
// ▲▲▲ [New] ▲▲▲
|
|
1555
|
+
|
|
1556
|
+
// ▼▼▼ [New] Update glow shader dimensions on resize ▼▼▼
|
|
1557
|
+
if (ENABLE_WEBGL_GLOW && glowMesh && window.MindMapGlowShader?.updateGlowSize) {
|
|
1558
|
+
window.MindMapGlowShader.updateGlowSize(glowMesh, nodeEntry.model.width, nodeEntry.model.height, nodeEntry.model.contentType);
|
|
1559
|
+
}
|
|
1560
|
+
// ▲▲▲ [New] ▲▲▲
|
|
1561
|
+
|
|
1562
|
+
group.userData.worldWidth = nodeEntry.model.width;
|
|
1563
|
+
group.userData.worldHeight = nodeEntry.model.height;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1563
1566
|
// ▼▼▼ [Fix] LOD 모드에서 리사이즈 시 InstancedMesh 실시간 업데이트 ▼▼▼
|
|
1564
1567
|
if (module.lodRenderer?.isInLODMode && module.lodRenderer?.requestResidentPatch) {
|
|
1565
1568
|
module.lodRenderer.requestResidentPatch('update-node', nodeId);
|
|
1566
1569
|
}
|
|
1567
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
function debouncedProcessRenderQueue(module, nodeId, updateData) {
|
|
1571
|
-
if (debounceTimers.has(nodeId)) {
|
|
1572
|
-
clearTimeout(debounceTimers.get(nodeId));
|
|
1573
|
-
}
|
|
1574
|
-
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1575
|
-
const timer = setTimeout(() => {
|
|
1576
|
-
enqueueRender(module, nodeId);
|
|
1577
|
-
debounceTimers.delete(nodeId);
|
|
1578
|
-
}, 100);
|
|
1579
|
-
debounceTimers.set(nodeId, timer);
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
function throttledProcessRenderQueueDuringResize(module, nodeId, updateData) {
|
|
1583
|
-
const now = performance.now();
|
|
1584
|
-
const lastCallTime = lastResizeUpdateCall.get(nodeId) || 0;
|
|
1585
|
-
|
|
1586
|
-
if (now - lastCallTime < THROTTLE_INTERVAL) {
|
|
1587
|
-
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1588
|
-
if (resizeThrottleTimers.has(nodeId)) return;
|
|
1589
|
-
const timer = setTimeout(() => {
|
|
1590
|
-
const queuedUpdateData = window.MindMapNodes.pendingNodeUpdates.get(nodeId);
|
|
1591
|
-
if (queuedUpdateData) {
|
|
1592
|
-
enqueueRender(module, nodeId);
|
|
1593
|
-
}
|
|
1594
|
-
resizeThrottleTimers.delete(nodeId);
|
|
1595
|
-
}, THROTTLE_INTERVAL);
|
|
1596
|
-
resizeThrottleTimers.set(nodeId, timer);
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
lastResizeUpdateCall.set(nodeId, now);
|
|
1601
|
-
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1602
|
-
enqueueRender(module, nodeId);
|
|
1603
|
-
|
|
1604
|
-
// ▼▼▼ [Fix] Maintain appropriate resize cursor during doResizing ▼▼▼
|
|
1605
|
-
let cursor = 'nwse-resize';
|
|
1606
|
-
switch (corner) {
|
|
1607
|
-
case 'TL': case 'BR': cursor = 'nwse-resize'; break;
|
|
1608
|
-
case 'TR': case 'BL': cursor = 'nesw-resize'; break;
|
|
1609
|
-
case 'T': case 'B': cursor = 'ns-resize'; break;
|
|
1610
|
-
case 'L': case 'R': cursor = 'ew-resize'; break;
|
|
1611
|
-
}
|
|
1612
|
-
document.body.style.cursor = cursor;
|
|
1613
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
function throttledScrollUpdate(module, nodeId, scrollDelta) {
|
|
1617
|
-
const now = performance.now();
|
|
1618
|
-
const lastCallTime = lastScrollCall.get(nodeId) || 0;
|
|
1619
|
-
|
|
1620
|
-
if (now - lastCallTime < THROTTLE_INTERVAL) {
|
|
1621
|
-
return;
|
|
1622
|
-
}
|
|
1623
|
-
lastScrollCall.set(nodeId, now);
|
|
1624
|
-
|
|
1625
|
-
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
1626
|
-
if (!nodeEntry) return;
|
|
1627
|
-
|
|
1628
|
-
const model = nodeEntry.model;
|
|
1629
|
-
const oldOffset = model.scrollOffset || 0;
|
|
1630
|
-
const maxScroll = model.maxScroll || 0;
|
|
1631
|
-
const newOffset = Math.max(0, Math.min(oldOffset + scrollDelta, maxScroll));
|
|
1632
|
-
|
|
1570
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function debouncedProcessRenderQueue(module, nodeId, updateData) {
|
|
1574
|
+
if (debounceTimers.has(nodeId)) {
|
|
1575
|
+
clearTimeout(debounceTimers.get(nodeId));
|
|
1576
|
+
}
|
|
1577
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1578
|
+
const timer = setTimeout(() => {
|
|
1579
|
+
enqueueRender(module, nodeId);
|
|
1580
|
+
debounceTimers.delete(nodeId);
|
|
1581
|
+
}, 100);
|
|
1582
|
+
debounceTimers.set(nodeId, timer);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function throttledProcessRenderQueueDuringResize(module, nodeId, updateData) {
|
|
1586
|
+
const now = performance.now();
|
|
1587
|
+
const lastCallTime = lastResizeUpdateCall.get(nodeId) || 0;
|
|
1588
|
+
|
|
1589
|
+
if (now - lastCallTime < THROTTLE_INTERVAL) {
|
|
1590
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1591
|
+
if (resizeThrottleTimers.has(nodeId)) return;
|
|
1592
|
+
const timer = setTimeout(() => {
|
|
1593
|
+
const queuedUpdateData = window.MindMapNodes.pendingNodeUpdates.get(nodeId);
|
|
1594
|
+
if (queuedUpdateData) {
|
|
1595
|
+
enqueueRender(module, nodeId);
|
|
1596
|
+
}
|
|
1597
|
+
resizeThrottleTimers.delete(nodeId);
|
|
1598
|
+
}, THROTTLE_INTERVAL);
|
|
1599
|
+
resizeThrottleTimers.set(nodeId, timer);
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
lastResizeUpdateCall.set(nodeId, now);
|
|
1604
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1605
|
+
enqueueRender(module, nodeId);
|
|
1606
|
+
|
|
1607
|
+
// ▼▼▼ [Fix] Maintain appropriate resize cursor during doResizing ▼▼▼
|
|
1608
|
+
let cursor = 'nwse-resize';
|
|
1609
|
+
switch (corner) {
|
|
1610
|
+
case 'TL': case 'BR': cursor = 'nwse-resize'; break;
|
|
1611
|
+
case 'TR': case 'BL': cursor = 'nesw-resize'; break;
|
|
1612
|
+
case 'T': case 'B': cursor = 'ns-resize'; break;
|
|
1613
|
+
case 'L': case 'R': cursor = 'ew-resize'; break;
|
|
1614
|
+
}
|
|
1615
|
+
document.body.style.cursor = cursor;
|
|
1616
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function throttledScrollUpdate(module, nodeId, scrollDelta) {
|
|
1620
|
+
const now = performance.now();
|
|
1621
|
+
const lastCallTime = lastScrollCall.get(nodeId) || 0;
|
|
1622
|
+
|
|
1623
|
+
if (now - lastCallTime < THROTTLE_INTERVAL) {
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
lastScrollCall.set(nodeId, now);
|
|
1627
|
+
|
|
1628
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
1629
|
+
if (!nodeEntry) return;
|
|
1630
|
+
|
|
1631
|
+
const model = nodeEntry.model;
|
|
1632
|
+
const oldOffset = model.scrollOffset || 0;
|
|
1633
|
+
const maxScroll = model.maxScroll || 0;
|
|
1634
|
+
const newOffset = Math.max(0, Math.min(oldOffset + scrollDelta, maxScroll));
|
|
1635
|
+
|
|
1633
1636
|
if (Math.abs(newOffset - oldOffset) < 0.1) return;
|
|
1634
1637
|
|
|
1635
1638
|
model.scrollOffset = newOffset;
|
|
@@ -1637,213 +1640,213 @@
|
|
|
1637
1640
|
|
|
1638
1641
|
// ▼▼▼ [New] Code nodes: update Troika text via scrollCodeText ▼▼▼
|
|
1639
1642
|
if (model.contentType === 'code' && nodeEntry.glObject) {
|
|
1640
|
-
const troikaText = nodeEntry.glObject.userData.troikaText;
|
|
1641
|
-
if (troikaText && window.MindMapTroikaText?.scrollCodeText) {
|
|
1642
|
-
// Use scrollCodeText which handles both position and clipRect
|
|
1643
|
-
const scrollResult = window.MindMapTroikaText.scrollCodeText(troikaText, scrollDelta);
|
|
1644
|
-
if (scrollResult) {
|
|
1645
|
-
model.scrollOffset = scrollResult.scrollY;
|
|
1646
|
-
|
|
1647
|
-
// ★ Update scrollbar thumb position (ShapeGeometry anchor is top-left, not center)
|
|
1648
|
-
const scrollbarThumb = nodeEntry.glObject.getObjectByName('scrollbarThumb');
|
|
1649
|
-
if (scrollbarThumb && scrollResult.maxScroll > 0) {
|
|
1650
|
-
const thumbData = scrollbarThumb.userData;
|
|
1651
|
-
const thumbTravel = thumbData.trackHeight - thumbData.thumbHeight;
|
|
1652
|
-
const thumbY = -thumbData.topMargin - (scrollResult.scrollRatio * thumbTravel);
|
|
1653
|
-
scrollbarThumb.position.y = thumbY;
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
// Persist scroll position to C#
|
|
1658
|
-
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1659
|
-
return;
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
// ▲▲▲ [New] ▲▲▲
|
|
1663
|
-
|
|
1664
|
-
// ▼▼▼ [Refactor] Fetch content directly from C# (no cache) ▼▼▼
|
|
1665
|
-
if (model.isChunkedText && module.dotNetHelper) {
|
|
1666
|
-
// Cancel the previous request (process only the latest)
|
|
1667
|
-
if (model._scrollFetchController) {
|
|
1668
|
-
model._scrollFetchController.abort = true;
|
|
1669
|
-
}
|
|
1670
|
-
const controller = { abort: false };
|
|
1671
|
-
model._scrollFetchController = controller;
|
|
1672
|
-
|
|
1673
|
-
// Debounce: render after scrolling stops
|
|
1674
|
-
if (model._scrollDebounceTimer) {
|
|
1675
|
-
clearTimeout(model._scrollDebounceTimer);
|
|
1676
|
-
}
|
|
1677
|
-
model._scrollDebounceTimer = setTimeout(async () => {
|
|
1678
|
-
if (controller.abort) return;
|
|
1679
|
-
|
|
1680
|
-
try {
|
|
1681
|
-
const result = await module.dotNetHelper.invokeMethodAsync(
|
|
1682
|
-
'GetTextByScrollPosition',
|
|
1683
|
-
nodeId,
|
|
1684
|
-
model.scrollOffset,
|
|
1685
|
-
model.height || 400,
|
|
1686
|
-
CHUNK_LINE_HEIGHT
|
|
1687
|
-
);
|
|
1688
|
-
|
|
1689
|
-
if (controller.abort || !result) return;
|
|
1690
|
-
|
|
1691
|
-
// Set content used for rendering
|
|
1692
|
-
model._visibleContent = result.content;
|
|
1693
|
-
model._visibleScrollOffset = result.relativeScrollOffset;
|
|
1694
|
-
|
|
1695
|
-
window.MindMapNodes.pendingNodeUpdates.set(nodeId, {
|
|
1696
|
-
content: result.content,
|
|
1697
|
-
width: model.width,
|
|
1698
|
-
height: model.height,
|
|
1699
|
-
scrollDelta: 0
|
|
1700
|
-
});
|
|
1701
|
-
enqueueRender(module, nodeId);
|
|
1702
|
-
} catch (e) {
|
|
1703
|
-
console.error(`[Pipeline] GetTextByScrollPosition error:`, e);
|
|
1704
|
-
}
|
|
1705
|
-
}, 16); // 16ms - 60fps throttle
|
|
1706
|
-
|
|
1707
|
-
// Persist scroll position to C#
|
|
1708
|
-
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1709
|
-
return;
|
|
1710
|
-
}
|
|
1711
|
-
// ▲▲▲ [Refactor] ▲▲▲
|
|
1712
|
-
|
|
1713
|
-
// Regular nodes (non-chunked)
|
|
1714
|
-
const updateData = {
|
|
1715
|
-
content: model.response,
|
|
1716
|
-
width: model.width,
|
|
1717
|
-
height: model.height,
|
|
1718
|
-
scrollDelta: 0
|
|
1719
|
-
};
|
|
1720
|
-
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1721
|
-
enqueueRender(module, nodeId);
|
|
1722
|
-
|
|
1723
|
-
// ▼▼▼ [New] Persist scroll position for regular nodes too ▼▼▼
|
|
1724
|
-
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1725
|
-
// ▲▲▲ [New] ▲▲▲
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
// ▼▼▼ [New] Set absolute scroll position (for scrollbar drag) ▼▼▼
|
|
1729
|
-
function setScrollPosition(module, nodeId, absoluteScrollPosition) {
|
|
1730
|
-
const now = performance.now();
|
|
1731
|
-
const lastCallTime = lastScrollCall.get(nodeId) || 0;
|
|
1732
|
-
|
|
1733
|
-
// Throttling (ignore calls that arrive too quickly)
|
|
1734
|
-
if (now - lastCallTime < THROTTLE_INTERVAL) {
|
|
1735
|
-
return;
|
|
1736
|
-
}
|
|
1737
|
-
lastScrollCall.set(nodeId, now);
|
|
1738
|
-
|
|
1739
|
-
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
1740
|
-
if (!nodeEntry) return;
|
|
1741
|
-
|
|
1742
|
-
const model = nodeEntry.model;
|
|
1743
|
-
const oldOffset = model.scrollOffset || 0;
|
|
1744
|
-
const maxScroll = model.maxScroll || 0;
|
|
1745
|
-
const newOffset = Math.max(0, Math.min(absoluteScrollPosition, maxScroll));
|
|
1746
|
-
|
|
1643
|
+
const troikaText = nodeEntry.glObject.userData.troikaText;
|
|
1644
|
+
if (troikaText && window.MindMapTroikaText?.scrollCodeText) {
|
|
1645
|
+
// Use scrollCodeText which handles both position and clipRect
|
|
1646
|
+
const scrollResult = window.MindMapTroikaText.scrollCodeText(troikaText, scrollDelta);
|
|
1647
|
+
if (scrollResult) {
|
|
1648
|
+
model.scrollOffset = scrollResult.scrollY;
|
|
1649
|
+
|
|
1650
|
+
// ★ Update scrollbar thumb position (ShapeGeometry anchor is top-left, not center)
|
|
1651
|
+
const scrollbarThumb = nodeEntry.glObject.getObjectByName('scrollbarThumb');
|
|
1652
|
+
if (scrollbarThumb && scrollResult.maxScroll > 0) {
|
|
1653
|
+
const thumbData = scrollbarThumb.userData;
|
|
1654
|
+
const thumbTravel = thumbData.trackHeight - thumbData.thumbHeight;
|
|
1655
|
+
const thumbY = -thumbData.topMargin - (scrollResult.scrollRatio * thumbTravel);
|
|
1656
|
+
scrollbarThumb.position.y = thumbY;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// Persist scroll position to C#
|
|
1661
|
+
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
// ▲▲▲ [New] ▲▲▲
|
|
1666
|
+
|
|
1667
|
+
// ▼▼▼ [Refactor] Fetch content directly from C# (no cache) ▼▼▼
|
|
1668
|
+
if (model.isChunkedText && module.dotNetHelper) {
|
|
1669
|
+
// Cancel the previous request (process only the latest)
|
|
1670
|
+
if (model._scrollFetchController) {
|
|
1671
|
+
model._scrollFetchController.abort = true;
|
|
1672
|
+
}
|
|
1673
|
+
const controller = { abort: false };
|
|
1674
|
+
model._scrollFetchController = controller;
|
|
1675
|
+
|
|
1676
|
+
// Debounce: render after scrolling stops
|
|
1677
|
+
if (model._scrollDebounceTimer) {
|
|
1678
|
+
clearTimeout(model._scrollDebounceTimer);
|
|
1679
|
+
}
|
|
1680
|
+
model._scrollDebounceTimer = setTimeout(async () => {
|
|
1681
|
+
if (controller.abort) return;
|
|
1682
|
+
|
|
1683
|
+
try {
|
|
1684
|
+
const result = await module.dotNetHelper.invokeMethodAsync(
|
|
1685
|
+
'GetTextByScrollPosition',
|
|
1686
|
+
nodeId,
|
|
1687
|
+
model.scrollOffset,
|
|
1688
|
+
model.height || 400,
|
|
1689
|
+
CHUNK_LINE_HEIGHT
|
|
1690
|
+
);
|
|
1691
|
+
|
|
1692
|
+
if (controller.abort || !result) return;
|
|
1693
|
+
|
|
1694
|
+
// Set content used for rendering
|
|
1695
|
+
model._visibleContent = result.content;
|
|
1696
|
+
model._visibleScrollOffset = result.relativeScrollOffset;
|
|
1697
|
+
|
|
1698
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, {
|
|
1699
|
+
content: result.content,
|
|
1700
|
+
width: model.width,
|
|
1701
|
+
height: model.height,
|
|
1702
|
+
scrollDelta: 0
|
|
1703
|
+
});
|
|
1704
|
+
enqueueRender(module, nodeId);
|
|
1705
|
+
} catch (e) {
|
|
1706
|
+
console.error(`[Pipeline] GetTextByScrollPosition error:`, e);
|
|
1707
|
+
}
|
|
1708
|
+
}, 16); // 16ms - 60fps throttle
|
|
1709
|
+
|
|
1710
|
+
// Persist scroll position to C#
|
|
1711
|
+
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
// ▲▲▲ [Refactor] ▲▲▲
|
|
1715
|
+
|
|
1716
|
+
// Regular nodes (non-chunked)
|
|
1717
|
+
const updateData = {
|
|
1718
|
+
content: model.response,
|
|
1719
|
+
width: model.width,
|
|
1720
|
+
height: model.height,
|
|
1721
|
+
scrollDelta: 0
|
|
1722
|
+
};
|
|
1723
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1724
|
+
enqueueRender(module, nodeId);
|
|
1725
|
+
|
|
1726
|
+
// ▼▼▼ [New] Persist scroll position for regular nodes too ▼▼▼
|
|
1727
|
+
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1728
|
+
// ▲▲▲ [New] ▲▲▲
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// ▼▼▼ [New] Set absolute scroll position (for scrollbar drag) ▼▼▼
|
|
1732
|
+
function setScrollPosition(module, nodeId, absoluteScrollPosition) {
|
|
1733
|
+
const now = performance.now();
|
|
1734
|
+
const lastCallTime = lastScrollCall.get(nodeId) || 0;
|
|
1735
|
+
|
|
1736
|
+
// Throttling (ignore calls that arrive too quickly)
|
|
1737
|
+
if (now - lastCallTime < THROTTLE_INTERVAL) {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
lastScrollCall.set(nodeId, now);
|
|
1741
|
+
|
|
1742
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
1743
|
+
if (!nodeEntry) return;
|
|
1744
|
+
|
|
1745
|
+
const model = nodeEntry.model;
|
|
1746
|
+
const oldOffset = model.scrollOffset || 0;
|
|
1747
|
+
const maxScroll = model.maxScroll || 0;
|
|
1748
|
+
const newOffset = Math.max(0, Math.min(absoluteScrollPosition, maxScroll));
|
|
1749
|
+
|
|
1747
1750
|
if (Math.abs(newOffset - oldOffset) < 0.1) return;
|
|
1748
1751
|
|
|
1749
1752
|
model.scrollOffset = newOffset;
|
|
1750
1753
|
window.MindMapCss3DManager?.syncCss3dScrollFromModel?.(module, nodeId);
|
|
1751
1754
|
|
|
1752
1755
|
// ▼▼▼ [Refactor] Fetch content directly from C# (no cache) ▼▼▼
|
|
1753
|
-
if (model.isChunkedText && module.dotNetHelper) {
|
|
1754
|
-
// Cancel the previous request (process only the latest)
|
|
1755
|
-
if (model._scrollFetchController) {
|
|
1756
|
-
model._scrollFetchController.abort = true;
|
|
1757
|
-
}
|
|
1758
|
-
const controller = { abort: false };
|
|
1759
|
-
model._scrollFetchController = controller;
|
|
1760
|
-
|
|
1761
|
-
// Throttle: update every 16ms even during fast scrolling (~60fps)
|
|
1762
|
-
if (model._scrollDebounceTimer) {
|
|
1763
|
-
clearTimeout(model._scrollDebounceTimer);
|
|
1764
|
-
}
|
|
1765
|
-
model._scrollDebounceTimer = setTimeout(async () => {
|
|
1766
|
-
if (controller.abort) return;
|
|
1767
|
-
|
|
1768
|
-
try {
|
|
1769
|
-
const result = await module.dotNetHelper.invokeMethodAsync(
|
|
1770
|
-
'GetTextByScrollPosition',
|
|
1771
|
-
nodeId,
|
|
1772
|
-
model.scrollOffset,
|
|
1773
|
-
model.height || 400,
|
|
1774
|
-
CHUNK_LINE_HEIGHT
|
|
1775
|
-
);
|
|
1776
|
-
|
|
1777
|
-
if (controller.abort || !result) return;
|
|
1778
|
-
|
|
1779
|
-
// Set content used for rendering
|
|
1780
|
-
model._visibleContent = result.content;
|
|
1781
|
-
model._visibleScrollOffset = result.relativeScrollOffset;
|
|
1782
|
-
|
|
1783
|
-
window.MindMapNodes.pendingNodeUpdates.set(nodeId, {
|
|
1784
|
-
content: result.content,
|
|
1785
|
-
width: model.width,
|
|
1786
|
-
height: model.height,
|
|
1787
|
-
scrollDelta: 0
|
|
1788
|
-
});
|
|
1789
|
-
enqueueRender(module, nodeId);
|
|
1790
|
-
} catch (e) {
|
|
1791
|
-
console.error(`[Pipeline] GetTextByScrollPosition error:`, e);
|
|
1792
|
-
}
|
|
1793
|
-
}, 16); // 16ms - 60fps throttle
|
|
1794
|
-
|
|
1795
|
-
// Persist scroll position to C#
|
|
1796
|
-
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1797
|
-
return;
|
|
1798
|
-
}
|
|
1799
|
-
// ▲▲▲ [Refactor] ▲▲▲
|
|
1800
|
-
|
|
1801
|
-
// Regular nodes (non-chunked)
|
|
1802
|
-
const updateData = {
|
|
1803
|
-
content: model.response,
|
|
1804
|
-
width: model.width,
|
|
1805
|
-
height: model.height,
|
|
1806
|
-
scrollDelta: 0
|
|
1807
|
-
};
|
|
1808
|
-
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1809
|
-
enqueueRender(module, nodeId);
|
|
1810
|
-
|
|
1811
|
-
// ▼▼▼ [New] Persist scroll position for regular nodes too ▼▼▼
|
|
1812
|
-
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1813
|
-
// ▲▲▲ [New] ▲▲▲
|
|
1814
|
-
}
|
|
1815
|
-
// ▲▲▲ [New] Set absolute scroll position (for scrollbar drag) ▲▲▲
|
|
1816
|
-
|
|
1817
|
-
function throttledUpdateScrollInCSharp(module, nodeId, scrollOffset) {
|
|
1818
|
-
if (scrollUpdateThrottleTimers.has(nodeId)) {
|
|
1819
|
-
clearTimeout(scrollUpdateThrottleTimers.get(nodeId));
|
|
1820
|
-
}
|
|
1821
|
-
const timer = setTimeout(() => {
|
|
1822
|
-
try {
|
|
1823
|
-
module.dotNetHelper?.invokeMethodAsync('UpdateNodeScroll', nodeId, scrollOffset);
|
|
1824
|
-
} catch (e) {
|
|
1825
|
-
console.error(`[Pipeline] Failed to update scroll in C# for node ${nodeId}:`, e);
|
|
1826
|
-
}
|
|
1827
|
-
scrollUpdateThrottleTimers.delete(nodeId);
|
|
1828
|
-
}, 100);
|
|
1829
|
-
scrollUpdateThrottleTimers.set(nodeId, timer);
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
async function stopResizing(module) {
|
|
1833
|
-
if (!module.isResizing || !module.resizeNodeObject) return;
|
|
1834
|
-
|
|
1835
|
-
// ▼▼▼ [Changed] Use stored nodeId (CSS3D mode compatible) ▼▼▼
|
|
1836
|
-
const nodeId = module.resizingNodeId || (module.resizeNodeObject.userData?.nodeId || module.resizeNodeObject.element?.dataset?.nodeId);
|
|
1837
|
-
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
1838
|
-
|
|
1839
|
-
if (nodeEntry && module.dotNetHelper) {
|
|
1840
|
-
const currentPosX = nodeEntry.glObject?.position?.x ?? nodeEntry.cssObject?.position?.x ?? 0;
|
|
1841
|
-
const currentPosY = nodeEntry.glObject?.position?.y ?? nodeEntry.cssObject?.position?.y ?? 0;
|
|
1842
|
-
let finalPosX = currentPosX;
|
|
1843
|
-
let finalPosY = currentPosY;
|
|
1844
|
-
|
|
1845
|
-
// Respect global snap toggle. Resize itself stays free-form while dragging;
|
|
1846
|
-
// snapping is applied only at the end when enabled.
|
|
1756
|
+
if (model.isChunkedText && module.dotNetHelper) {
|
|
1757
|
+
// Cancel the previous request (process only the latest)
|
|
1758
|
+
if (model._scrollFetchController) {
|
|
1759
|
+
model._scrollFetchController.abort = true;
|
|
1760
|
+
}
|
|
1761
|
+
const controller = { abort: false };
|
|
1762
|
+
model._scrollFetchController = controller;
|
|
1763
|
+
|
|
1764
|
+
// Throttle: update every 16ms even during fast scrolling (~60fps)
|
|
1765
|
+
if (model._scrollDebounceTimer) {
|
|
1766
|
+
clearTimeout(model._scrollDebounceTimer);
|
|
1767
|
+
}
|
|
1768
|
+
model._scrollDebounceTimer = setTimeout(async () => {
|
|
1769
|
+
if (controller.abort) return;
|
|
1770
|
+
|
|
1771
|
+
try {
|
|
1772
|
+
const result = await module.dotNetHelper.invokeMethodAsync(
|
|
1773
|
+
'GetTextByScrollPosition',
|
|
1774
|
+
nodeId,
|
|
1775
|
+
model.scrollOffset,
|
|
1776
|
+
model.height || 400,
|
|
1777
|
+
CHUNK_LINE_HEIGHT
|
|
1778
|
+
);
|
|
1779
|
+
|
|
1780
|
+
if (controller.abort || !result) return;
|
|
1781
|
+
|
|
1782
|
+
// Set content used for rendering
|
|
1783
|
+
model._visibleContent = result.content;
|
|
1784
|
+
model._visibleScrollOffset = result.relativeScrollOffset;
|
|
1785
|
+
|
|
1786
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, {
|
|
1787
|
+
content: result.content,
|
|
1788
|
+
width: model.width,
|
|
1789
|
+
height: model.height,
|
|
1790
|
+
scrollDelta: 0
|
|
1791
|
+
});
|
|
1792
|
+
enqueueRender(module, nodeId);
|
|
1793
|
+
} catch (e) {
|
|
1794
|
+
console.error(`[Pipeline] GetTextByScrollPosition error:`, e);
|
|
1795
|
+
}
|
|
1796
|
+
}, 16); // 16ms - 60fps throttle
|
|
1797
|
+
|
|
1798
|
+
// Persist scroll position to C#
|
|
1799
|
+
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
// ▲▲▲ [Refactor] ▲▲▲
|
|
1803
|
+
|
|
1804
|
+
// Regular nodes (non-chunked)
|
|
1805
|
+
const updateData = {
|
|
1806
|
+
content: model.response,
|
|
1807
|
+
width: model.width,
|
|
1808
|
+
height: model.height,
|
|
1809
|
+
scrollDelta: 0
|
|
1810
|
+
};
|
|
1811
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1812
|
+
enqueueRender(module, nodeId);
|
|
1813
|
+
|
|
1814
|
+
// ▼▼▼ [New] Persist scroll position for regular nodes too ▼▼▼
|
|
1815
|
+
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1816
|
+
// ▲▲▲ [New] ▲▲▲
|
|
1817
|
+
}
|
|
1818
|
+
// ▲▲▲ [New] Set absolute scroll position (for scrollbar drag) ▲▲▲
|
|
1819
|
+
|
|
1820
|
+
function throttledUpdateScrollInCSharp(module, nodeId, scrollOffset) {
|
|
1821
|
+
if (scrollUpdateThrottleTimers.has(nodeId)) {
|
|
1822
|
+
clearTimeout(scrollUpdateThrottleTimers.get(nodeId));
|
|
1823
|
+
}
|
|
1824
|
+
const timer = setTimeout(() => {
|
|
1825
|
+
try {
|
|
1826
|
+
module.dotNetHelper?.invokeMethodAsync('UpdateNodeScroll', nodeId, scrollOffset);
|
|
1827
|
+
} catch (e) {
|
|
1828
|
+
console.error(`[Pipeline] Failed to update scroll in C# for node ${nodeId}:`, e);
|
|
1829
|
+
}
|
|
1830
|
+
scrollUpdateThrottleTimers.delete(nodeId);
|
|
1831
|
+
}, 100);
|
|
1832
|
+
scrollUpdateThrottleTimers.set(nodeId, timer);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
async function stopResizing(module) {
|
|
1836
|
+
if (!module.isResizing || !module.resizeNodeObject) return;
|
|
1837
|
+
|
|
1838
|
+
// ▼▼▼ [Changed] Use stored nodeId (CSS3D mode compatible) ▼▼▼
|
|
1839
|
+
const nodeId = module.resizingNodeId || (module.resizeNodeObject.userData?.nodeId || module.resizeNodeObject.element?.dataset?.nodeId);
|
|
1840
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
1841
|
+
|
|
1842
|
+
if (nodeEntry && module.dotNetHelper) {
|
|
1843
|
+
const currentPosX = nodeEntry.glObject?.position?.x ?? nodeEntry.cssObject?.position?.x ?? 0;
|
|
1844
|
+
const currentPosY = nodeEntry.glObject?.position?.y ?? nodeEntry.cssObject?.position?.y ?? 0;
|
|
1845
|
+
let finalPosX = currentPosX;
|
|
1846
|
+
let finalPosY = currentPosY;
|
|
1847
|
+
|
|
1848
|
+
// Respect global snap toggle. Resize itself stays free-form while dragging;
|
|
1849
|
+
// snapping is applied only at the end when enabled.
|
|
1847
1850
|
if (module.gridSnapEnabled !== false) {
|
|
1848
1851
|
const GRID_SIZE = module.GRID_SIZE || 10;
|
|
1849
1852
|
nodeEntry.model.width = Math.round(nodeEntry.model.width / GRID_SIZE) * GRID_SIZE;
|
|
@@ -1857,20 +1860,20 @@
|
|
|
1857
1860
|
nodeEntry.model.height);
|
|
1858
1861
|
nodeEntry.model.width = finalClampedSize.width;
|
|
1859
1862
|
nodeEntry.model.height = finalClampedSize.height;
|
|
1860
|
-
|
|
1861
|
-
if (nodeEntry.glObject) {
|
|
1862
|
-
nodeEntry.glObject.position.x = finalPosX;
|
|
1863
|
-
nodeEntry.glObject.position.y = finalPosY;
|
|
1864
|
-
}
|
|
1865
|
-
if (nodeEntry.cssObject) {
|
|
1866
|
-
nodeEntry.cssObject.position.x = finalPosX;
|
|
1867
|
-
nodeEntry.cssObject.position.y = finalPosY;
|
|
1868
|
-
nodeEntry.cssObject.updateMatrixWorld(true);
|
|
1869
|
-
|
|
1870
|
-
// ▼▼▼ [Fix] 그리드 스냅 후 CSS3D DOM 크기도 업데이트 ▼▼▼
|
|
1871
|
-
const wrapper = nodeEntry.cssObject.element;
|
|
1872
|
-
if (wrapper) {
|
|
1873
|
-
const resolutionScale = nodeEntry.cssObject.userData?.resolutionScale || 1;
|
|
1863
|
+
|
|
1864
|
+
if (nodeEntry.glObject) {
|
|
1865
|
+
nodeEntry.glObject.position.x = finalPosX;
|
|
1866
|
+
nodeEntry.glObject.position.y = finalPosY;
|
|
1867
|
+
}
|
|
1868
|
+
if (nodeEntry.cssObject) {
|
|
1869
|
+
nodeEntry.cssObject.position.x = finalPosX;
|
|
1870
|
+
nodeEntry.cssObject.position.y = finalPosY;
|
|
1871
|
+
nodeEntry.cssObject.updateMatrixWorld(true);
|
|
1872
|
+
|
|
1873
|
+
// ▼▼▼ [Fix] 그리드 스냅 후 CSS3D DOM 크기도 업데이트 ▼▼▼
|
|
1874
|
+
const wrapper = nodeEntry.cssObject.element;
|
|
1875
|
+
if (wrapper) {
|
|
1876
|
+
const resolutionScale = nodeEntry.cssObject.userData?.resolutionScale || 1;
|
|
1874
1877
|
const domWidth = nodeEntry.model.width * resolutionScale;
|
|
1875
1878
|
const domHeight = nodeEntry.model.height * resolutionScale;
|
|
1876
1879
|
|
|
@@ -1892,25 +1895,25 @@
|
|
|
1892
1895
|
|
|
1893
1896
|
// ▼▼▼ [Fix] 이미지 노드의 경우 내부 img 요소 크기도 업데이트 ▼▼▼
|
|
1894
1897
|
if (nodeEntry.model.contentType === 'image') {
|
|
1895
|
-
const imgEl = innerContent.querySelector('img');
|
|
1896
|
-
if (imgEl) {
|
|
1897
|
-
imgEl.style.width = '100%';
|
|
1898
|
-
imgEl.style.height = '100%';
|
|
1899
|
-
}
|
|
1900
|
-
const contentWrapper = innerContent.querySelector('.node-content-wrapper');
|
|
1901
|
-
if (contentWrapper) {
|
|
1902
|
-
contentWrapper.style.width = '100%';
|
|
1903
|
-
contentWrapper.style.height = '100%';
|
|
1904
|
-
}
|
|
1905
|
-
const responseDiv = innerContent.querySelector('.node-response');
|
|
1906
|
-
if (responseDiv) {
|
|
1907
|
-
responseDiv.style.width = '100%';
|
|
1908
|
-
responseDiv.style.height = '100%';
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1898
|
+
const imgEl = innerContent.querySelector('img');
|
|
1899
|
+
if (imgEl) {
|
|
1900
|
+
imgEl.style.width = '100%';
|
|
1901
|
+
imgEl.style.height = '100%';
|
|
1902
|
+
}
|
|
1903
|
+
const contentWrapper = innerContent.querySelector('.node-content-wrapper');
|
|
1904
|
+
if (contentWrapper) {
|
|
1905
|
+
contentWrapper.style.width = '100%';
|
|
1906
|
+
contentWrapper.style.height = '100%';
|
|
1907
|
+
}
|
|
1908
|
+
const responseDiv = innerContent.querySelector('.node-response');
|
|
1909
|
+
if (responseDiv) {
|
|
1910
|
+
responseDiv.style.width = '100%';
|
|
1911
|
+
responseDiv.style.height = '100%';
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1914
1917
|
// userData 업데이트
|
|
1915
1918
|
nodeEntry.cssObject.userData.worldWidth = nodeEntry.model.width;
|
|
1916
1919
|
nodeEntry.cssObject.userData.worldHeight = nodeEntry.model.height;
|
|
@@ -1918,21 +1921,21 @@
|
|
|
1918
1921
|
}
|
|
1919
1922
|
// ▲▲▲ [Fix] ▲▲▲
|
|
1920
1923
|
}
|
|
1921
|
-
try {
|
|
1922
|
-
await module.dotNetHelper.invokeMethodAsync(
|
|
1923
|
-
'UpdateNodeDimensions',
|
|
1924
|
-
nodeId,
|
|
1925
|
-
Math.round(nodeEntry.model.width),
|
|
1926
|
-
Math.round(nodeEntry.model.height)
|
|
1927
|
-
);
|
|
1928
|
-
// ▼▼▼ [Changed] Also update position in C# ▼▼▼
|
|
1929
|
-
await module.dotNetHelper.invokeMethodAsync(
|
|
1930
|
-
'UpdateNodePosition',
|
|
1931
|
-
nodeId,
|
|
1932
|
-
Math.round(finalPosX),
|
|
1933
|
-
Math.round(finalPosY)
|
|
1934
|
-
);
|
|
1935
|
-
// ▲▲▲ [Changed] ▲▲▲
|
|
1924
|
+
try {
|
|
1925
|
+
await module.dotNetHelper.invokeMethodAsync(
|
|
1926
|
+
'UpdateNodeDimensions',
|
|
1927
|
+
nodeId,
|
|
1928
|
+
Math.round(nodeEntry.model.width),
|
|
1929
|
+
Math.round(nodeEntry.model.height)
|
|
1930
|
+
);
|
|
1931
|
+
// ▼▼▼ [Changed] Also update position in C# ▼▼▼
|
|
1932
|
+
await module.dotNetHelper.invokeMethodAsync(
|
|
1933
|
+
'UpdateNodePosition',
|
|
1934
|
+
nodeId,
|
|
1935
|
+
Math.round(finalPosX),
|
|
1936
|
+
Math.round(finalPosY)
|
|
1937
|
+
);
|
|
1938
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
1936
1939
|
} catch (e) {
|
|
1937
1940
|
console.error('[MindMapPipeline] Error updating dimensions/position:', e);
|
|
1938
1941
|
}
|
|
@@ -1947,18 +1950,18 @@
|
|
|
1947
1950
|
|
|
1948
1951
|
// ▼▼▼ [UndoRedo] Record resize command after dimensions/position are persisted ▼▼▼
|
|
1949
1952
|
try { await module.dotNetHelper.invokeMethodAsync('EndResizeWithUndo'); } catch { }
|
|
1950
|
-
// ▲▲▲ [UndoRedo] ▲▲▲
|
|
1951
|
-
|
|
1952
|
-
if (nodeEntry.glObject) {
|
|
1953
|
-
const outline = nodeEntry.glObject.getObjectByName('outline');
|
|
1954
|
-
let glowMesh = nodeEntry.glObject.getObjectByName('glow');
|
|
1955
|
-
if (!ENABLE_WEBGL_GLOW) {
|
|
1956
|
-
glowMesh = removeGlowMesh(nodeEntry.glObject);
|
|
1957
|
-
}
|
|
1958
|
-
const bodyMesh = nodeEntry.glObject.getObjectByName('body');
|
|
1959
|
-
const isSelected = module.multiSelectedNodeIds.has(nodeId) || module.selectedNodeIdJs === nodeId;
|
|
1960
|
-
|
|
1961
|
-
// ▼▼▼ [Fix] 그리드 스냅 후 body mesh 크기/위치 업데이트 ▼▼▼
|
|
1953
|
+
// ▲▲▲ [UndoRedo] ▲▲▲
|
|
1954
|
+
|
|
1955
|
+
if (nodeEntry.glObject) {
|
|
1956
|
+
const outline = nodeEntry.glObject.getObjectByName('outline');
|
|
1957
|
+
let glowMesh = nodeEntry.glObject.getObjectByName('glow');
|
|
1958
|
+
if (!ENABLE_WEBGL_GLOW) {
|
|
1959
|
+
glowMesh = removeGlowMesh(nodeEntry.glObject);
|
|
1960
|
+
}
|
|
1961
|
+
const bodyMesh = nodeEntry.glObject.getObjectByName('body');
|
|
1962
|
+
const isSelected = module.multiSelectedNodeIds.has(nodeId) || module.selectedNodeIdJs === nodeId;
|
|
1963
|
+
|
|
1964
|
+
// ▼▼▼ [Fix] 그리드 스냅 후 body mesh 크기/위치 업데이트 ▼▼▼
|
|
1962
1965
|
if (bodyMesh) {
|
|
1963
1966
|
bodyMesh.geometry.dispose();
|
|
1964
1967
|
bodyMesh.geometry = new THREE.PlaneGeometry(nodeEntry.model.width, nodeEntry.model.height);
|
|
@@ -1970,10 +1973,10 @@
|
|
|
1970
1973
|
if (nodeEntry.currentResizeType === 'CSS') {
|
|
1971
1974
|
bodyMesh.visible = false;
|
|
1972
1975
|
}
|
|
1973
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
1974
|
-
}
|
|
1975
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
1976
|
-
|
|
1976
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1977
|
+
}
|
|
1978
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1979
|
+
|
|
1977
1980
|
if (nodeEntry.model.contentType === 'image') {
|
|
1978
1981
|
const usesCssViewAfterResize =
|
|
1979
1982
|
nodeEntry.currentResizeType === 'CSS' ||
|
|
@@ -1999,29 +2002,29 @@
|
|
|
1999
2002
|
outline.material.color.setHex(isSelected ? 0x7b7ff2 : 0x374151);
|
|
2000
2003
|
outline.visible = false; // Disabled - using SDF glow instead
|
|
2001
2004
|
}
|
|
2002
|
-
|
|
2003
|
-
// ▼▼▼ [Changed] After resizing, update glow using SDF shader ▼▼▼
|
|
2004
|
-
if (ENABLE_WEBGL_GLOW && glowMesh && window.MindMapGlowShader?.updateGlowSize) {
|
|
2005
|
-
const contentType = nodeEntry.model.contentType || 'text';
|
|
2006
|
-
window.MindMapGlowShader.updateGlowSize(glowMesh, nodeEntry.model.width, nodeEntry.model.height, contentType);
|
|
2007
|
-
glowMesh.position.set(
|
|
2008
|
-
nodeEntry.model.width / 2,
|
|
2009
|
-
-nodeEntry.model.height / 2,
|
|
2010
|
-
0
|
|
2011
|
-
);
|
|
2012
|
-
// ★ 카메라 거리에 따라 가시성 설정
|
|
2013
|
-
const cameraZ = module.camera?.position?.z ?? Infinity;
|
|
2014
|
-
const threshold = 4000;
|
|
2015
|
-
glowMesh.visible = cameraZ < threshold;
|
|
2016
|
-
}
|
|
2017
|
-
// ▲▲▲ [Changed] ▲▲▲
|
|
2018
|
-
|
|
2019
|
-
// ▼▼▼ [Fix] 리사이즈 종료 후 8방향 핸들 위치 업데이트 ▼▼▼
|
|
2020
|
-
const HANDLE_SIZE = RESIZE_HANDLE_SIZE;
|
|
2021
|
-
const w = nodeEntry.model.width;
|
|
2022
|
-
const h = nodeEntry.model.height;
|
|
2023
|
-
const edgeWidthLength = Math.max(10, w - HANDLE_SIZE * 2);
|
|
2024
|
-
const edgeHeightLength = Math.max(10, h - HANDLE_SIZE * 2);
|
|
2005
|
+
|
|
2006
|
+
// ▼▼▼ [Changed] After resizing, update glow using SDF shader ▼▼▼
|
|
2007
|
+
if (ENABLE_WEBGL_GLOW && glowMesh && window.MindMapGlowShader?.updateGlowSize) {
|
|
2008
|
+
const contentType = nodeEntry.model.contentType || 'text';
|
|
2009
|
+
window.MindMapGlowShader.updateGlowSize(glowMesh, nodeEntry.model.width, nodeEntry.model.height, contentType);
|
|
2010
|
+
glowMesh.position.set(
|
|
2011
|
+
nodeEntry.model.width / 2,
|
|
2012
|
+
-nodeEntry.model.height / 2,
|
|
2013
|
+
0
|
|
2014
|
+
);
|
|
2015
|
+
// ★ 카메라 거리에 따라 가시성 설정
|
|
2016
|
+
const cameraZ = module.camera?.position?.z ?? Infinity;
|
|
2017
|
+
const threshold = 4000;
|
|
2018
|
+
glowMesh.visible = cameraZ < threshold;
|
|
2019
|
+
}
|
|
2020
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
2021
|
+
|
|
2022
|
+
// ▼▼▼ [Fix] 리사이즈 종료 후 8방향 핸들 위치 업데이트 ▼▼▼
|
|
2023
|
+
const HANDLE_SIZE = RESIZE_HANDLE_SIZE;
|
|
2024
|
+
const w = nodeEntry.model.width;
|
|
2025
|
+
const h = nodeEntry.model.height;
|
|
2026
|
+
const edgeWidthLength = Math.max(10, w - HANDLE_SIZE * 2);
|
|
2027
|
+
const edgeHeightLength = Math.max(10, h - HANDLE_SIZE * 2);
|
|
2025
2028
|
const handleConfigs = {
|
|
2026
2029
|
// Corners
|
|
2027
2030
|
'resizeHandle_TL': { x: HANDLE_SIZE / 2, y: -HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
@@ -2059,25 +2062,25 @@
|
|
|
2059
2062
|
handle.material.needsUpdate = true;
|
|
2060
2063
|
}
|
|
2061
2064
|
});
|
|
2062
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
if (debounceTimers.has(nodeId)) {
|
|
2066
|
-
clearTimeout(debounceTimers.get(nodeId));
|
|
2067
|
-
debounceTimers.delete(nodeId);
|
|
2068
|
-
}
|
|
2069
|
-
if (resizeThrottleTimers.has(nodeId)) {
|
|
2070
|
-
clearTimeout(resizeThrottleTimers.get(nodeId));
|
|
2071
|
-
resizeThrottleTimers.delete(nodeId);
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2065
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
if (debounceTimers.has(nodeId)) {
|
|
2069
|
+
clearTimeout(debounceTimers.get(nodeId));
|
|
2070
|
+
debounceTimers.delete(nodeId);
|
|
2071
|
+
}
|
|
2072
|
+
if (resizeThrottleTimers.has(nodeId)) {
|
|
2073
|
+
clearTimeout(resizeThrottleTimers.get(nodeId));
|
|
2074
|
+
resizeThrottleTimers.delete(nodeId);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2074
2077
|
if (nodeEntry.currentResizeType === 'CSS') {
|
|
2075
2078
|
console.log(`[stopResizing] CSS type - queuing texture update for ${nodeId}`);
|
|
2076
2079
|
|
|
2077
2080
|
// ▼▼▼ [Fix] CSS3D 노드의 최종 크기를 resolutionScale 고려하여 설정 ▼▼▼
|
|
2078
2081
|
if (nodeEntry.cssObject && nodeEntry.cssObject.element) {
|
|
2079
|
-
const wrapper = nodeEntry.cssObject.element;
|
|
2080
|
-
const resolutionScale = nodeEntry.cssObject.userData?.resolutionScale || 1;
|
|
2082
|
+
const wrapper = nodeEntry.cssObject.element;
|
|
2083
|
+
const resolutionScale = nodeEntry.cssObject.userData?.resolutionScale || 1;
|
|
2081
2084
|
const domWidth = nodeEntry.model.width * resolutionScale;
|
|
2082
2085
|
const domHeight = nodeEntry.model.height * resolutionScale;
|
|
2083
2086
|
|
|
@@ -2097,7 +2100,7 @@
|
|
|
2097
2100
|
innerContent.style.maxWidth = nodeEntry.model.width + 'px';
|
|
2098
2101
|
innerContent.style.maxHeight = nodeEntry.model.height + 'px';
|
|
2099
2102
|
}
|
|
2100
|
-
|
|
2103
|
+
|
|
2101
2104
|
// userData 업데이트
|
|
2102
2105
|
nodeEntry.cssObject.userData.worldWidth = nodeEntry.model.width;
|
|
2103
2106
|
nodeEntry.cssObject.userData.worldHeight = nodeEntry.model.height;
|
|
@@ -2121,72 +2124,72 @@
|
|
|
2121
2124
|
if (textarea) {
|
|
2122
2125
|
window.MindMapNodes.pendingNodeUpdates.set(nodeId, {
|
|
2123
2126
|
content: textarea.value,
|
|
2124
|
-
width: nodeEntry.model.width,
|
|
2125
|
-
height: nodeEntry.model.height
|
|
2126
|
-
});
|
|
2127
|
-
enqueueRender(module, nodeId);
|
|
2128
|
-
}
|
|
2129
|
-
} else if (nodeEntry.currentResizeType === 'GL') {
|
|
2130
|
-
// ▼▼▼ [Refactor] After resizing a chunk node, fetch content directly from C# ▼▼▼
|
|
2131
|
-
if (nodeEntry.model.isChunkedText && module.dotNetHelper) {
|
|
2132
|
-
try {
|
|
2133
|
-
const result = await module.dotNetHelper.invokeMethodAsync(
|
|
2134
|
-
'GetTextByScrollPosition',
|
|
2135
|
-
nodeId,
|
|
2136
|
-
nodeEntry.model.scrollOffset || 0,
|
|
2137
|
-
nodeEntry.model.height || 400,
|
|
2138
|
-
CHUNK_LINE_HEIGHT
|
|
2139
|
-
);
|
|
2140
|
-
|
|
2141
|
-
if (result) {
|
|
2142
|
-
nodeEntry.model._visibleContent = result.content;
|
|
2143
|
-
nodeEntry.model._visibleScrollOffset = result.relativeScrollOffset;
|
|
2144
|
-
|
|
2145
|
-
window.MindMapNodes.pendingNodeUpdates.set(nodeId, {
|
|
2146
|
-
content: result.content,
|
|
2147
|
-
width: nodeEntry.model.width,
|
|
2148
|
-
height: nodeEntry.model.height,
|
|
2149
|
-
scrollDelta: 0
|
|
2150
|
-
});
|
|
2151
|
-
enqueueRender(module, nodeId);
|
|
2152
|
-
}
|
|
2153
|
-
} catch (e) {
|
|
2154
|
-
console.error(`[Pipeline] GetTextByScrollPosition error on resize:`, e);
|
|
2155
|
-
}
|
|
2156
|
-
} else {
|
|
2157
|
-
// ▲▲▲ [Refactor] ▲▲▲
|
|
2158
|
-
// Regular nodes (non-chunked) + IMAGE NODES
|
|
2159
|
-
|
|
2160
|
-
// ▼▼▼ [Fix] 이미지 노드 리사이즈 시 기존 텍스처 유지 ▼▼▼
|
|
2161
|
-
if (nodeEntry.model.contentType === 'image') {
|
|
2162
|
-
// 이미지 노드는 URL이 변하지 않으므로 텍스처 재생성 불필요
|
|
2163
|
-
// geometry만 업데이트하고 기존 텍스처를 유지
|
|
2164
|
-
const bodyMesh = nodeEntry.glObject?.getObjectByName('body');
|
|
2165
|
-
if (bodyMesh && bodyMesh.material.map) {
|
|
2166
|
-
// 기존 텍스처 유지 - 아무것도 하지 않음
|
|
2167
|
-
console.log(`[stopResizing] Image node ${nodeId} - keeping existing texture`);
|
|
2168
|
-
} else {
|
|
2169
|
-
console.warn(`[stopResizing] Image node ${nodeId} - no texture found, may need reload`);
|
|
2170
|
-
}
|
|
2171
|
-
} else {
|
|
2172
|
-
// [Fix] Ensure regular nodes have content for texture generation
|
|
2173
|
-
const content = nodeEntry.model.response;
|
|
2174
|
-
|
|
2175
|
-
console.log(`[stopResizing] GL type - queuing texture update for ${nodeId}, size ${nodeEntry.model.width}x${nodeEntry.model.height}`);
|
|
2176
|
-
window.MindMapNodes.pendingNodeUpdates.set(nodeId, {
|
|
2177
|
-
content: content,
|
|
2178
|
-
width: nodeEntry.model.width,
|
|
2179
|
-
height: nodeEntry.model.height
|
|
2180
|
-
});
|
|
2181
|
-
enqueueRender(module, nodeId);
|
|
2182
|
-
}
|
|
2183
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
window.MindMapNodes.updateNodeSpatialGrid(module, nodeId);
|
|
2188
|
-
|
|
2189
|
-
// ▼▼▼ [Fix] 리사이즈 종료 후 CSS transition 복원 ▼▼▼
|
|
2127
|
+
width: nodeEntry.model.width,
|
|
2128
|
+
height: nodeEntry.model.height
|
|
2129
|
+
});
|
|
2130
|
+
enqueueRender(module, nodeId);
|
|
2131
|
+
}
|
|
2132
|
+
} else if (nodeEntry.currentResizeType === 'GL') {
|
|
2133
|
+
// ▼▼▼ [Refactor] After resizing a chunk node, fetch content directly from C# ▼▼▼
|
|
2134
|
+
if (nodeEntry.model.isChunkedText && module.dotNetHelper) {
|
|
2135
|
+
try {
|
|
2136
|
+
const result = await module.dotNetHelper.invokeMethodAsync(
|
|
2137
|
+
'GetTextByScrollPosition',
|
|
2138
|
+
nodeId,
|
|
2139
|
+
nodeEntry.model.scrollOffset || 0,
|
|
2140
|
+
nodeEntry.model.height || 400,
|
|
2141
|
+
CHUNK_LINE_HEIGHT
|
|
2142
|
+
);
|
|
2143
|
+
|
|
2144
|
+
if (result) {
|
|
2145
|
+
nodeEntry.model._visibleContent = result.content;
|
|
2146
|
+
nodeEntry.model._visibleScrollOffset = result.relativeScrollOffset;
|
|
2147
|
+
|
|
2148
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, {
|
|
2149
|
+
content: result.content,
|
|
2150
|
+
width: nodeEntry.model.width,
|
|
2151
|
+
height: nodeEntry.model.height,
|
|
2152
|
+
scrollDelta: 0
|
|
2153
|
+
});
|
|
2154
|
+
enqueueRender(module, nodeId);
|
|
2155
|
+
}
|
|
2156
|
+
} catch (e) {
|
|
2157
|
+
console.error(`[Pipeline] GetTextByScrollPosition error on resize:`, e);
|
|
2158
|
+
}
|
|
2159
|
+
} else {
|
|
2160
|
+
// ▲▲▲ [Refactor] ▲▲▲
|
|
2161
|
+
// Regular nodes (non-chunked) + IMAGE NODES
|
|
2162
|
+
|
|
2163
|
+
// ▼▼▼ [Fix] 이미지 노드 리사이즈 시 기존 텍스처 유지 ▼▼▼
|
|
2164
|
+
if (nodeEntry.model.contentType === 'image') {
|
|
2165
|
+
// 이미지 노드는 URL이 변하지 않으므로 텍스처 재생성 불필요
|
|
2166
|
+
// geometry만 업데이트하고 기존 텍스처를 유지
|
|
2167
|
+
const bodyMesh = nodeEntry.glObject?.getObjectByName('body');
|
|
2168
|
+
if (bodyMesh && bodyMesh.material.map) {
|
|
2169
|
+
// 기존 텍스처 유지 - 아무것도 하지 않음
|
|
2170
|
+
console.log(`[stopResizing] Image node ${nodeId} - keeping existing texture`);
|
|
2171
|
+
} else {
|
|
2172
|
+
console.warn(`[stopResizing] Image node ${nodeId} - no texture found, may need reload`);
|
|
2173
|
+
}
|
|
2174
|
+
} else {
|
|
2175
|
+
// [Fix] Ensure regular nodes have content for texture generation
|
|
2176
|
+
const content = nodeEntry.model.response;
|
|
2177
|
+
|
|
2178
|
+
console.log(`[stopResizing] GL type - queuing texture update for ${nodeId}, size ${nodeEntry.model.width}x${nodeEntry.model.height}`);
|
|
2179
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, {
|
|
2180
|
+
content: content,
|
|
2181
|
+
width: nodeEntry.model.width,
|
|
2182
|
+
height: nodeEntry.model.height
|
|
2183
|
+
});
|
|
2184
|
+
enqueueRender(module, nodeId);
|
|
2185
|
+
}
|
|
2186
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
window.MindMapNodes.updateNodeSpatialGrid(module, nodeId);
|
|
2191
|
+
|
|
2192
|
+
// ▼▼▼ [Fix] 리사이즈 종료 후 CSS transition 복원 ▼▼▼
|
|
2190
2193
|
if (nodeEntry.cssObject && nodeEntry.cssObject.element) {
|
|
2191
2194
|
const wrapper = nodeEntry.cssObject.element;
|
|
2192
2195
|
wrapper.style.transition = CSS3D_WRAPPER_TRANSITION;
|
|
@@ -2197,9 +2200,9 @@
|
|
|
2197
2200
|
inner.style.webkitTransition = 'none';
|
|
2198
2201
|
}
|
|
2199
2202
|
}
|
|
2200
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2203
2206
|
module.isResizing = false;
|
|
2204
2207
|
module.resizeNodeObject = null;
|
|
2205
2208
|
module.resizingNodeId = null; // ▼▼▼ [Changed] clear resizingNodeId ▼▼▼
|
|
@@ -2208,18 +2211,18 @@
|
|
|
2208
2211
|
|
|
2209
2212
|
// ▼▼▼ [Fix] Reset cursor after resizing ▼▼▼
|
|
2210
2213
|
document.body.style.cursor = '';
|
|
2211
|
-
// ▲▲▲ [Fix] ▲▲▲
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
// Expose MindMapPipeline globally
|
|
2214
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// Expose MindMapPipeline globally
|
|
2215
2218
|
window.MindMapPipeline = {
|
|
2216
|
-
processRenderQueue: processRenderQueue,
|
|
2217
|
-
debouncedProcessRenderQueue: debouncedProcessRenderQueue,
|
|
2218
|
-
throttledScrollUpdate: throttledScrollUpdate,
|
|
2219
|
-
setScrollPosition: setScrollPosition, // [New] for scrollbar dragging
|
|
2220
|
-
throttledProcessRenderQueueDuringResize: throttledProcessRenderQueueDuringResize,
|
|
2221
|
-
startResizing: startResizing,
|
|
2222
|
-
doResizing: doResizing,
|
|
2219
|
+
processRenderQueue: processRenderQueue,
|
|
2220
|
+
debouncedProcessRenderQueue: debouncedProcessRenderQueue,
|
|
2221
|
+
throttledScrollUpdate: throttledScrollUpdate,
|
|
2222
|
+
setScrollPosition: setScrollPosition, // [New] for scrollbar dragging
|
|
2223
|
+
throttledProcessRenderQueueDuringResize: throttledProcessRenderQueueDuringResize,
|
|
2224
|
+
startResizing: startResizing,
|
|
2225
|
+
doResizing: doResizing,
|
|
2223
2226
|
stopResizing: stopResizing,
|
|
2224
2227
|
getQueueDebugMetrics: () => ({
|
|
2225
2228
|
sortCount: renderQueueSortCount,
|
|
@@ -2232,8 +2235,8 @@
|
|
|
2232
2235
|
renderQueueMaxLength = renderQueue.length;
|
|
2233
2236
|
},
|
|
2234
2237
|
throttledUpdateScrollInCSharp: throttledUpdateScrollInCSharp
|
|
2235
|
-
};
|
|
2236
|
-
|
|
2237
|
-
console.log('? mind-map-pipeline.js loaded');
|
|
2238
|
-
})();
|
|
2239
|
-
|
|
2238
|
+
};
|
|
2239
|
+
|
|
2240
|
+
console.log('? mind-map-pipeline.js loaded');
|
|
2241
|
+
})();
|
|
2242
|
+
|