PageSourceSearch

https://astroknot.netlify.app/modules/UI/Resize.js

js astroknot.netlify.app collected 2026-10-03 10:30:30 UTC 19,335 bytes, 527 lines download raw bytes

1// ============================================================
2//  UI / Resize.js — 窗口缩放 + 自动布局弹出
3// ============================================================
4import * as THREE from 'three';
5import { appState } from '../module0_AppState.js';
6import { processSidebar2DPanning } from '../richEditor/tree-panel.js';
7import { layoutTree, assignCoordinates, extractNodePositions } from '../2DView/index.js';
8import { rebuildAllLines, generateRandomPosition } from '../VisualComponents/index.js';
9import { saveCurrentProjectData } from '../module2_TreeData.js';
10import { groupRects } from '../2DView/shared.js';
11
12export function bindResize() {
13  window.addEventListener('resize', () => {
14    appState.camera.aspect = window.innerWidth / window.innerHeight;
15    appState.camera.updateProjectionMatrix();
16    appState.renderer.setSize(window.innerWidth, window.innerHeight);
17    appState.effectComposer.setSize(window.innerWidth, window.innerHeight);
18    appState.labelRenderer.setSize(window.innerWidth, window.innerHeight);
19    if (appState.renderer && appState.renderer.domElement) {
20      appState.renderer.domElement.style.width = window.innerWidth + 'px';
21      appState.renderer.domElement.style.height = window.innerHeight + 'px';
22    }
23  });
24
25  // ============================================================
26//  所有图层自动排列(2D 模式下遍历所有图层逐一重排)
27// ============================================================
28function arrangeAllLayers() {
29  const sortedLayers = [...appState.layers].sort((a, b) => a.order - b.order);
30  const rootNode = appState.methodsTree;
31  if (!rootNode || !rootNode.id) return;
32
33  // 保存当前图层
34  const curLayer = appState.getCurrentLayer();
35  if (curLayer) {
36    curLayer.positions2D = new Map(appState.positions2D);
37  }
38
39  // 1. 先用第一个图层跑一次布局,拿到标准位置映射
40  let referencePositions = null;
41  const firstLayer = sortedLayers[0];
42  if (firstLayer) {
43    appState.currentLayerId = firstLayer.id;
44    appState.positions2D = firstLayer.positions2D || new Map();
45    if (appState.autoArrangeTreeLayout) {
46      appState.autoArrangeTreeLayout();
47    }
48    // 保存第一个图层的排列结果作为基准
49    referencePositions = new Map(appState.positions2D);
50    firstLayer.positions2D = new Map(appState.positions2D);
51  }
52
53  // 2. 其余图层:用第一个图层的标准位置覆盖,并偏移对齐到第一个图层的根节点位置
54  // 先找到第一个图层的 primary node(第一个可见节点)作为锚点
55  let anchorPos = null;
56  if (firstLayer) {
57    for (const [id, pos] of (referencePositions || [])) {
58      if (id !== rootNode.id && firstLayer.nodeIds && firstLayer.nodeIds.has(id)) {
59        anchorPos = { x: pos.x, y: pos.y };
60        break;
61      }
62    }
63  }
64
65  for (let i = 1; i < sortedLayers.length; i++) {
66    const layer = sortedLayers[i];
67    appState.currentLayerId = layer.id;
68    appState.positions2D = layer.positions2D || new Map();
69
70    // 用第一个图层的标准位置覆盖当前图层的所有节点位置
71    if (referencePositions) {
72      for (const [nodeId, pos] of referencePositions) {
73        appState.positions2D.set(nodeId, { x: pos.x, y: pos.y });
74      }
75      // 以当前图层的根节点位置为基准重新运行布局,确保树结构一致
76      appState.autoArrangeTreeLayout();
77
78      // 应用偏移对齐:使该图层的 primary node 与第一个图层的 primary node 在同一位置
79      // 根节点位置不参与对齐偏移(只移动后代节点)
80      if (anchorPos && layer.nodeIds) {
81        let layerPrimaryPos = null;
82        for (const [id, pos] of appState.positions2D) {
83          if (id !== rootNode.id && layer.nodeIds.has(id)) {
84            layerPrimaryPos = { x: pos.x, y: pos.y };
85            break;
86          }
87        }
88        if (layerPrimaryPos) {
89          const offsetX = anchorPos.x - layerPrimaryPos.x;
90          const offsetY = anchorPos.y - layerPrimaryPos.y;
91          if (offsetX !== 0 || offsetY !== 0) {
92            const newPositions = new Map();
93            for (const [id, pos] of appState.positions2D) {
94              if (id === rootNode.id) {
95                // 根节点位置保持不变
96                newPositions.set(id, { x: pos.x, y: pos.y });
97              } else {
98                newPositions.set(id, { x: pos.x + offsetX, y: pos.y + offsetY });
99              }
100            }
101            appState.positions2D = newPositions;
102          }
103        }
104      }
105    }
106    layer.positions2D = new Map(appState.positions2D);
107  }
108
109  // 恢复当前图层
110  if (curLayer) {
111    appState.currentLayerId = curLayer.id;
112    appState.positions2D = curLayer.positions2D || new Map();
113  }
114
115  // 刷新 2D 视图
116  if (appState.refresh2DView) appState.refresh2DView();
117  if (appState.refreshTreePanel) appState.refreshTreePanel();
118}
119
120// ============================================================
121//  3D 默认散布排列
122// ============================================================
123function arrange3DDefault() {
124  appState.layer3DLayout = false;
125  // 隐藏图层按钮
126  const layerBtn = document.getElementById('layerIconBtn');
127  if (layerBtn) layerBtn.style.display = 'none';
128  // 清除图层高亮矩形
129  if (appState.layerHighlights) {
130    appState.layerHighlights.forEach(h => appState.scene.remove(h));
131    appState.layerHighlights = [];
132  }
133  // 清除3D组群矩形
134  if (appState.groupRectMeshes) {
135    appState.groupRectMeshes.forEach(m => appState.scene.remove(m));
136    appState.groupRectMeshes = [];
137  }
138
139  const existing = [];
140  const base = new THREE.Vector3(0, 0, 0);
141  const nodeIds = Array.from(appState.positions.keys()).filter(id => id !== appState.VIRTUAL_ROOT_ID);
142  for (const id of nodeIds) {
143    const pos = generateRandomPosition(existing, base);
144    appState.positions.set(id, pos);
145    existing.push(pos);
146    const obj = appState.nodeMeshes.get(id);
147    if (obj) {
148      obj.mesh.position.copy(pos);
149      if (obj.label) {
150        obj.label.position.set(pos.x, pos.y + appState.NODE_RADIUS + 0.28, pos.z);
151      }
152    }
153  }
154  rebuildAllLines();
155  saveCurrentProjectData();
156}
157
158// ============================================================
159//  3D 按 2D 布局排列(不同图层按层序向上堆叠)
160// ============================================================
161function arrange3DWith2DLayout() {
162  const rootNode = appState.methodsTree;
163  if (!rootNode || !rootNode.id) return;
164
165  appState.layer3DLayout = true;
166  const LAYER_SPACING = 4; // 每层之间的 Y 轴间距
167  appState.layer3DSpacing = LAYER_SPACING;
168  const sortedLayers = [...appState.layers].sort((a, b) => a.order - b.order);
169
170  // 保存当前图层的 2D 位置
171  const curLayer = appState.getCurrentLayer();
172  if (curLayer) {
173    curLayer.positions2D = new Map(appState.positions2D);
174  }
175
176  // 1. 先用第一个图层跑一次布局,拿到标准位置映射
177  let referencePositions = null;
178  const firstLayer = sortedLayers[0];
179  if (firstLayer) {
180    appState.currentLayerId = firstLayer.id;
181    appState.positions2D = firstLayer.positions2D || new Map();
182    const rootPos = appState.positions2D.get(rootNode.id) || { x: -500, y: -200 };
183
184    // 保存真实根节点的位置
185    const realRootPositions = new Map();
186    for (const child of (rootNode.children || [])) {
187      if (child && child.id) {
188        const saved = appState.positions2D.get(child.id);
189        if (saved) realRootPositions.set(child.id, { x: saved.x, y: saved.y });
190      }
191    }
192
193    const layout = layoutTree(rootNode);
194    assignCoordinates(layout, 0, 0);
195    const positions = extractNodePositions(layout);
196    // 虚拟根节点 + 真实根节点位置不参与重排
197    positions.set(rootNode.id, { x: rootPos.x, y: rootPos.y });
198    appState.positions2D.set(rootNode.id, { x: rootPos.x, y: rootPos.y });
199    for (const [id, saved] of realRootPositions) {
200      positions.set(id, { x: saved.x, y: saved.y });
201      appState.positions2D.set(id, { x: saved.x, y: saved.y });
202    }
203    const rootLayoutPos = positions.get(rootNode.id);
204    if (rootLayoutPos) {
205      const offsetX = rootPos.x - rootLayoutPos.x;
206      const offsetY = rootPos.y - rootLayoutPos.y;
207      for (const [id, pos] of positions) {
208        if (id === rootNode.id || realRootPositions.has(id)) continue;
209        pos.x += offsetX;
210        pos.y += offsetY;
211      }
212    }
213    referencePositions = new Map(positions);
214  }
215
216  // 2. 用统一的标准位置映射到各图层的 3D 坐标
217  if (referencePositions) {
218    // 找到第一个图层的 primary node(第一个非虚拟根且在图层中的节点)作为锚点
219    let anchorPos = null;
220    if (firstLayer) {
221      for (const [id, pos] of referencePositions.entries()) {
222        if (id !== rootNode.id && firstLayer.nodeIds && firstLayer.nodeIds.has(id)) {
223          anchorPos = { x: pos.x, y: pos.y };
224          break;
225        }
226      }
227    }
228
229    sortedLayers.forEach((layer, layerIdx) => {
230      const yBase = layerIdx * LAYER_SPACING;
231
232      // 计算当前图层的偏移量,使其 primary node 对齐到第一个图层的锚点位置
233      let offsetX = 0, offsetY = 0;
234      if (anchorPos && layer.nodeIds) {
235        for (const [id, pos] of referencePositions.entries()) {
236          if (id !== rootNode.id && layer.nodeIds.has(id)) {
237            offsetX = anchorPos.x - pos.x;
238            offsetY = anchorPos.y - pos.y;
239            break;
240          }
241        }
242      }
243
244      for (const [id, pos] of referencePositions.entries()) {
245        if (layer.nodeIds && !layer.nodeIds.has(id)) continue;
246        // 应用偏移量,使该图层节点对齐到第一个图层的对应位置
247        const adjustedX = pos.x + offsetX;
248        const adjustedY = pos.y + offsetY;
249        const worldPos = new THREE.Vector3(
250          adjustedX * 0.005,
251          yBase,
252          adjustedY * 0.015
253        );
254        appState.positions.set(id, worldPos);
255        const obj = appState.nodeMeshes.get(id);
256        if (obj) {
257          obj.mesh.position.copy(worldPos);
258          if (obj.label) {
259            obj.label.position.set(worldPos.x, worldPos.y + appState.NODE_RADIUS + 0.28, worldPos.z);
260          }
261        }
262      }
263    });
264  }
265
266  // 3. 树遍历修正 X 坐标:逐层还原 2D 边缘到边缘连线夹角
267  // child_3D_X = parent_adjusted_X + (child_base_X - parent_base_X) - parent_width * SCALE
268  {
269    const SCALE = 0.015, BASE_W = 120;
270    function applyEdgeOffset(node, parentId, parentBaseX, parentAdjustedX) {
271      const pos = appState.positions.get(node.id);
272      if (!pos) return;
273      const baseX = pos.x;
274      let adjustedX;
275      if (parentId && parentId !== appState.VIRTUAL_ROOT_ID) {
276        const pNode = appState.nodeMap.get(parentId);
277        adjustedX = pNode
278          ? parentAdjustedX + (baseX - parentBaseX) - (pNode.sizeScale || 1) * BASE_W * SCALE
279          : baseX;
280      } else {
281        adjustedX = baseX;
282      }
283      pos.x = adjustedX;
284      const obj = appState.nodeMeshes.get(node.id);
285      if (obj) {
286        obj.mesh.position.x = adjustedX;
287        if (obj.label) obj.label.position.x = adjustedX;
288      }
289      (node.children || []).forEach(c => applyEdgeOffset(c, node.id, baseX, adjustedX));
290    }
291    applyEdgeOffset(appState.methodsTree, null, 0, 0);
292  }
293
294  // 4. 为每层节点绘制半透明高亮矩形
295  {
296    // 清除旧的高亮矩形
297    if (appState.layerHighlights) {
298      appState.layerHighlights.forEach(h => appState.scene.remove(h));
299    }
300    appState.layerHighlights = [];
301
302    sortedLayers.forEach((layer, layerIdx) => {
303      const yBase = layerIdx * LAYER_SPACING;
304      let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity;
305      let hasNode = false;
306
307      for (const id of (layer.nodeIds || [])) {
308        const pos = appState.positions.get(id);
309        if (!pos) continue;
310        hasNode = true;
311        if (pos.x < minX) minX = pos.x;
312        if (pos.x > maxX) maxX = pos.x;
313        if (pos.z < minZ) minZ = pos.z;
314        if (pos.z > maxZ) maxZ = pos.z;
315      }
316
317      if (!hasNode) return;
318
319      const PAD = 0.5;
320      const w = maxX - minX + PAD * 2;
321      const h = maxZ - minZ + PAD * 2;
322      const geom = new THREE.PlaneGeometry(Math.max(w, 0.1), Math.max(h, 0.1), 64, 64);
323
324      // 水面波纹着色器
325      const waterUniforms = {
326        uTime: { value: 0 },
327        uColor: { value: new THREE.Color(0x4488ff) },
328        uOpacity: { value: 0.45 },
329        uHighlight: { value: 0.0 }
330      };
331      const waterMat = new THREE.ShaderMaterial({
332        uniforms: waterUniforms,
333        transparent: true,
334        side: THREE.DoubleSide,
335        depthWrite: false,
336        vertexShader: `
337          uniform float uTime;
338          varying vec2 vUv;
339          varying float vWave;
340          void main() {
341            vUv = uv;
342            vec3 pos = position;
343            // 多层波纹叠加
344            float wave1 = sin(pos.x * 6.0 + uTime * 1.2) * cos(pos.y * 5.0 + uTime * 0.8) * 0.015;
345            float wave2 = sin(pos.x * 10.0 - uTime * 1.5) * sin(pos.y * 8.0 + uTime * 1.1) * 0.008;
346            float wave3 = cos(pos.x * 3.0 + pos.y * 4.0 + uTime * 0.6) * 0.01;
347            pos.z += wave1 + wave2 + wave3;
348            vWave = wave1 + wave2 + wave3;
349            gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0
349);
350          }
351        `,
352        fragmentShader: `
353          uniform float uTime;
354          uniform vec3 uColor;
355          uniform float uOpacity;
356          uniform float uHighlight;
357          varying vec2 vUv;
358          varying float vWave;
359          void main() {
360            // 流动泛光
361            float flow1 = sin(vUv.x * 12.0 + uTime * 1.5) * cos(vUv.y * 10.0 - uTime * 1.0);
362            float flow2 = sin(vUv.x * 8.0 - uTime * 0.7 + vUv.y * 6.0) * 0.5;
363            float flow = (flow1 + flow2) * 0.5 + 0.5;
364
365            // 自由运动波纹
366            float ripple = sin(length(vUv - 0.5) * 20.0 - uTime * 3.0) * 0.5 + 0.5;
367            ripple *= smoothstep(0.5, 0.0, length(vUv - 0.5));
368
369            // 中心泛光
370            float glow = smoothstep(0.6, 0.0, length(vUv - 0.5)) * 0.3;
371
372            // 波峰高光
373            float specular = pow(max(0.0, vWave * 30.0), 2.0) * 0.4;
374
375            float alpha = (0.15 + flow * 0.12 + ripple * 0.1 + glow + specular) * uOpacity;
376            alpha = clamp(alpha, 0.0, 0.85);
377
378            vec3 col = uColor + specular * vec3(0.3, 0.5, 1.0);
379            col += uHighlight * vec3(0.1, 0.2, 0.4);
380
381            gl_FragColor = vec4(col, alpha);
382          }
383        `
384      });
385
386      const plane = new THREE.Mesh(geom, waterMat);
387      plane.rotation.x = -Math.PI / 2; // 平放在 XZ 平面
388      plane.position.set((minX + maxX) / 2, yBase, (minZ + maxZ) / 2);
389      plane.renderOrder = 999;
390      appState.scene.add(plane);
391      appState.layerHighlights.push(plane);
392    });
393  }
394
395  // 5. 绘制3D组群矩形
396  {
397    if (appState.groupRectMeshes) {
398      appState.groupRectMeshes.forEach(m => appState.scene.remove(m));
399    }
400    appState.groupRectMeshes = [];
401
402    const X_SCALE = 0.005, Z_SCALE = 0.015;
403    for (const gr of groupRects) {
404      // 找到组群所属图层
405      let layerIdx = -1;
406      if (gr.layerId) {
407        layerIdx = sortedLayers.findIndex(l => l.id === gr.layerId);
408      }
409      if (layerIdx < 0) {
410        // 无 layerId 时尝试通过 nodeIds 找图层
411        for (let li = 0; li < sortedLayers.length; li++) {
412          const layer = sortedLayers[li];
413          if (gr.nodeIds && gr.nodeIds.some(nid => layer.nodeIds && layer.nodeIds.has(nid))) {
414            layerIdx = li;
415            break;
416          }
417        }
418      }
419      if (layerIdx < 0) continue;
420
421      const yBase = layerIdx * LAYER_SPACING;
422      const cx = (gr.x + gr.width / 2) * X_SCALE;
423      const cz = (gr.y + gr.height / 2) * Z_SCALE;
424      const w = Math.max(gr.width * X_SCALE, 0.05);
425      const h = Math.max(gr.height * Z_SCALE, 0.05);
426
427      // 半透明填充
428      const geom = new THREE.PlaneGeometry(w, h);
429      const fillAlpha = gr.fillOpacity !== undefined ? gr.fillOpacity : 0.25;
430      const mat = new THREE.MeshBasicMaterial({
431        color: gr.fillColor ? parseInt(gr.fillColor.slice(1), 16) : 0x4a3c7e,
432        transparent: true,
433        opacity: fillAlpha,
434        side: THREE.DoubleSide,
435        depthWrite: false
436      });
437      const plane = new THREE.Mesh(geom, mat);
438      plane.rotation.x = -Math.PI / 2;
439      plane.position.set(cx, yBase + 0.01, cz);
440      plane.renderOrder = 998;
441      appState.scene.add(plane);
442      appState.groupRectMeshes.push(plane);
443
444      // 边框线
445      const edgeGeom = new THREE.EdgesGeometry(new THREE.PlaneGeometry(w, h));
446      const edgeMat = new THREE.LineBasicMaterial({
447        color: gr.borderColor ? parseInt(gr.borderColor.slice(1), 16) : 0x7a6aae,
448        transparent: true,
449        opacity: 0.8
450      });
451      const edgeLine = new THREE.LineSegments(edgeGeom, edgeMat);
452      edgeLine.rotation.x = -Math.PI / 2;
453      edgeLine.position.set(cx, yBase + 0.02, cz);
454      edgeLine.renderOrder = 999;
455      appState.scene.add(edgeLine);
456      appState.groupRectMeshes.push(edgeLine);
457    }
458  }
459
460  // 显示图层按钮
461  const layerBtn = document.getElementById('layerIconBtn');
462  if (layerBtn) layerBtn.style.display = '';
463
464  // 恢复当前图层
465  if (curLayer) {
466    appState.currentLayerId = curLayer.id;
467    appState.positions2D = curLayer.positions2D || new Map();
468  }
469
470  // 重建连线
471  rebuildAllLines();
472  saveCurrentProjectData();
473}
474
475  // ---------- 自动排列按钮(任务栏) ----------
476  const arrangeBtn = document.getElementById('arrangeBtn');
477  const arrangePopup = document.getElementById('arrangePopup');
478  const arrangeTreeBtn = document.getElementById('arrangeTreeBtn');
479  const arrangeAllLayersBtn = document.getElementById('arrangeAllLayersBtn');
480  const arrange3DDefaultBtn = document.getElementById('arrange3DDefaultBtn');
481  const arrange3D2DBtn = document.getElementById('arrange3D2DBtn');
482  if (arrangeBtn && arrangePopup) {
483    arrangeBtn.addEventListener('click', (e) => {
484      e.stopPropagation();
485      arrangePopup.classList.toggle('show');
486    });
487
488    document.addEventListener('pointerdown', (e) => {
489      if (!arrangePopup.contains(e.target) && e.target !== arrangeBtn) {
490        arrangePopup.classList.remove('show');
491      }
492    });
493
494    document.getElementById('arrangeTreeBtn')?.addEventListener('click', (e) => {
495      e.stopPropagation();
496      if (appState.autoArrangeTreeLayout) appState.autoArrangeTreeLayout();
497    });
498
499    document.getElementById('arrangeAllLayersBtn')?.addEventListener('click', (e) => {
500      e.stopPropagation();
501      arrangeAllLayers();
502    });
503
504    document.getElementById('arrange3DDefaultBtn')?.addEventListener('click', (e) => {
505      e.stopPropagation();
506      arrange3DDefault();
507    });
508
509    document.getElementById('arrange3D2DBtn')?.addEventListener('click', (e) => {
510      e.stopPropagation();
511      arrange3DWith2DLayout();
512    });
513  }
514}
515
516// ==================== 2D平移循环(统一处理) ====================
517function start2DPanningLoop() {
518  function loop() {
519    if (appState.is2DView && appState.process2DPanning) {
520      appState.process2DPanning();
521    }
522    processSidebar2DPanning();
523    requestAnimationFrame(loop);
524  }
525  requestAnimationFrame(loop);
526}
527start2DPanningLoop();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.