aifastdb-devplan 1.5.0 → 1.6.2

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.
Files changed (121) hide show
  1. package/dist/autopilot.d.ts +58 -0
  2. package/dist/autopilot.d.ts.map +1 -0
  3. package/dist/autopilot.js +250 -0
  4. package/dist/autopilot.js.map +1 -0
  5. package/dist/dev-plan-document-store.d.ts +15 -1
  6. package/dist/dev-plan-document-store.d.ts.map +1 -1
  7. package/dist/dev-plan-document-store.js +122 -0
  8. package/dist/dev-plan-document-store.js.map +1 -1
  9. package/dist/dev-plan-factory.d.ts +69 -3
  10. package/dist/dev-plan-factory.d.ts.map +1 -1
  11. package/dist/dev-plan-factory.js +113 -19
  12. package/dist/dev-plan-factory.js.map +1 -1
  13. package/dist/dev-plan-graph-store.d.ts +79 -1
  14. package/dist/dev-plan-graph-store.d.ts.map +1 -1
  15. package/dist/dev-plan-graph-store.js +420 -3
  16. package/dist/dev-plan-graph-store.js.map +1 -1
  17. package/dist/dev-plan-interface.d.ts +24 -1
  18. package/dist/dev-plan-interface.d.ts.map +1 -1
  19. package/dist/dev-plan-migrate.d.ts +1 -0
  20. package/dist/dev-plan-migrate.d.ts.map +1 -1
  21. package/dist/dev-plan-migrate.js +28 -2
  22. package/dist/dev-plan-migrate.js.map +1 -1
  23. package/dist/index.d.ts +3 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +14 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/mcp-server/index.d.ts +3 -0
  28. package/dist/mcp-server/index.d.ts.map +1 -1
  29. package/dist/mcp-server/index.js +397 -4
  30. package/dist/mcp-server/index.js.map +1 -1
  31. package/dist/types.d.ts +160 -1
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/types.js +9 -1
  34. package/dist/types.js.map +1 -1
  35. package/dist/visualize/graph-canvas/api-compat.d.ts +20 -0
  36. package/dist/visualize/graph-canvas/api-compat.d.ts.map +1 -0
  37. package/dist/visualize/graph-canvas/api-compat.js +344 -0
  38. package/dist/visualize/graph-canvas/api-compat.js.map +1 -0
  39. package/dist/visualize/graph-canvas/clusterer.d.ts +16 -0
  40. package/dist/visualize/graph-canvas/clusterer.d.ts.map +1 -0
  41. package/dist/visualize/graph-canvas/clusterer.js +460 -0
  42. package/dist/visualize/graph-canvas/clusterer.js.map +1 -0
  43. package/dist/visualize/graph-canvas/core.d.ts +11 -0
  44. package/dist/visualize/graph-canvas/core.d.ts.map +1 -0
  45. package/dist/visualize/graph-canvas/core.js +1136 -0
  46. package/dist/visualize/graph-canvas/core.js.map +1 -0
  47. package/dist/visualize/graph-canvas/index.d.ts +22 -0
  48. package/dist/visualize/graph-canvas/index.d.ts.map +1 -0
  49. package/dist/visualize/graph-canvas/index.js +69 -0
  50. package/dist/visualize/graph-canvas/index.js.map +1 -0
  51. package/dist/visualize/graph-canvas/interaction.d.ts +13 -0
  52. package/dist/visualize/graph-canvas/interaction.d.ts.map +1 -0
  53. package/dist/visualize/graph-canvas/interaction.js +457 -0
  54. package/dist/visualize/graph-canvas/interaction.js.map +1 -0
  55. package/dist/visualize/graph-canvas/layout-worker.d.ts +17 -0
  56. package/dist/visualize/graph-canvas/layout-worker.d.ts.map +1 -0
  57. package/dist/visualize/graph-canvas/layout-worker.js +577 -0
  58. package/dist/visualize/graph-canvas/layout-worker.js.map +1 -0
  59. package/dist/visualize/graph-canvas/lod.d.ts +10 -0
  60. package/dist/visualize/graph-canvas/lod.d.ts.map +1 -0
  61. package/dist/visualize/graph-canvas/lod.js +111 -0
  62. package/dist/visualize/graph-canvas/lod.js.map +1 -0
  63. package/dist/visualize/graph-canvas/renderer.d.ts +12 -0
  64. package/dist/visualize/graph-canvas/renderer.d.ts.map +1 -0
  65. package/dist/visualize/graph-canvas/renderer.js +813 -0
  66. package/dist/visualize/graph-canvas/renderer.js.map +1 -0
  67. package/dist/visualize/graph-canvas/spatial-index.d.ts +13 -0
  68. package/dist/visualize/graph-canvas/spatial-index.d.ts.map +1 -0
  69. package/dist/visualize/graph-canvas/spatial-index.js +482 -0
  70. package/dist/visualize/graph-canvas/spatial-index.js.map +1 -0
  71. package/dist/visualize/graph-canvas/styles.d.ts +11 -0
  72. package/dist/visualize/graph-canvas/styles.d.ts.map +1 -0
  73. package/dist/visualize/graph-canvas/styles.js +152 -0
  74. package/dist/visualize/graph-canvas/styles.js.map +1 -0
  75. package/dist/visualize/graph-canvas/viewport.d.ts +17 -0
  76. package/dist/visualize/graph-canvas/viewport.d.ts.map +1 -0
  77. package/dist/visualize/graph-canvas/viewport.js +385 -0
  78. package/dist/visualize/graph-canvas/viewport.js.map +1 -0
  79. package/dist/visualize/server.js +737 -7
  80. package/dist/visualize/server.js.map +1 -1
  81. package/dist/visualize/template-core.d.ts +9 -0
  82. package/dist/visualize/template-core.d.ts.map +1 -0
  83. package/dist/visualize/template-core.js +714 -0
  84. package/dist/visualize/template-core.js.map +1 -0
  85. package/dist/visualize/template-data-loading.d.ts +7 -0
  86. package/dist/visualize/template-data-loading.d.ts.map +1 -0
  87. package/dist/visualize/template-data-loading.js +677 -0
  88. package/dist/visualize/template-data-loading.js.map +1 -0
  89. package/dist/visualize/template-detail-panel.d.ts +14 -0
  90. package/dist/visualize/template-detail-panel.d.ts.map +1 -0
  91. package/dist/visualize/template-detail-panel.js +553 -0
  92. package/dist/visualize/template-detail-panel.js.map +1 -0
  93. package/dist/visualize/template-graph-3d.d.ts +7 -0
  94. package/dist/visualize/template-graph-3d.d.ts.map +1 -0
  95. package/dist/visualize/template-graph-3d.js +1112 -0
  96. package/dist/visualize/template-graph-3d.js.map +1 -0
  97. package/dist/visualize/template-graph-vis.d.ts +8 -0
  98. package/dist/visualize/template-graph-vis.d.ts.map +1 -0
  99. package/dist/visualize/template-graph-vis.js +1204 -0
  100. package/dist/visualize/template-graph-vis.js.map +1 -0
  101. package/dist/visualize/template-html.d.ts +9 -0
  102. package/dist/visualize/template-html.d.ts.map +1 -0
  103. package/dist/visualize/template-html.js +484 -0
  104. package/dist/visualize/template-html.js.map +1 -0
  105. package/dist/visualize/template-pages.d.ts +7 -0
  106. package/dist/visualize/template-pages.d.ts.map +1 -0
  107. package/dist/visualize/template-pages.js +806 -0
  108. package/dist/visualize/template-pages.js.map +1 -0
  109. package/dist/visualize/template-stats-modal.d.ts +7 -0
  110. package/dist/visualize/template-stats-modal.d.ts.map +1 -0
  111. package/dist/visualize/template-stats-modal.js +406 -0
  112. package/dist/visualize/template-stats-modal.js.map +1 -0
  113. package/dist/visualize/template-styles.d.ts +9 -0
  114. package/dist/visualize/template-styles.d.ts.map +1 -0
  115. package/dist/visualize/template-styles.js +487 -0
  116. package/dist/visualize/template-styles.js.map +1 -0
  117. package/dist/visualize/template.d.ts +14 -3
  118. package/dist/visualize/template.d.ts.map +1 -1
  119. package/dist/visualize/template.js +38 -2889
  120. package/dist/visualize/template.js.map +1 -1
  121. package/package.json +1 -1
@@ -0,0 +1,1112 @@
1
+ "use strict";
2
+ /**
3
+ * DevPlan 图可视化 — 3D Force Graph 渲染模块
4
+ *
5
+ * 包含: Three.js WebGL 3D 图渲染、力导向布局、节点交互。
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.getGraph3DScript = getGraph3DScript;
9
+ function getGraph3DScript() {
10
+ return `
11
+ // ========== 3D Force Graph Rendering (统一颜色配置) ==========
12
+ // 从统一节点颜色配置加载 (适用于所有引擎)
13
+ var _3dUniStyle = getUnifiedNodeStyle();
14
+ var _3dNodeColors = getNodeColors();
15
+ function load3DColorsFromSettings() {
16
+ var nc = getNodeColors();
17
+ return {
18
+ 'project': nc.colorProject,
19
+ 'module': nc.colorModule,
20
+ 'main-task': nc.colorMainTask,
21
+ 'sub-task': nc.colorSubTask,
22
+ 'document': nc.colorDocument
23
+ };
24
+ }
25
+ function load3DSizesFromSettings() {
26
+ var s = get3DSettings();
27
+ return {
28
+ 'project': s.sizeProject,
29
+ 'module': s.sizeModule,
30
+ 'main-task': s.sizeMainTask,
31
+ 'sub-task': s.sizeSubTask,
32
+ 'document': s.sizeDocument
33
+ };
34
+ }
35
+ var NODE_3D_COLORS = load3DColorsFromSettings();
36
+ var NODE_3D_SIZES = load3DSizesFromSettings();
37
+ // 主任务状态颜色 (从统一配置)
38
+ var MAIN_TASK_STATUS_COLORS = {
39
+ 'pending': _3dUniStyle.mainTask.pending.bg,
40
+ 'completed': _3dUniStyle.mainTask.completed.bg,
41
+ 'in_progress': _3dUniStyle.mainTask.in_progress.bg,
42
+ 'cancelled': _3dUniStyle.mainTask.cancelled.bg
43
+ };
44
+ // 子任务状态颜色 (从统一配置, completed=亮绿色)
45
+ var SUB_TASK_STATUS_COLORS = {
46
+ 'pending': _3dUniStyle.subTask.pending.bg,
47
+ 'completed': _3dUniStyle.subTask.completed.bg,
48
+ 'in_progress': _3dUniStyle.subTask.in_progress.bg,
49
+ 'cancelled': _3dUniStyle.subTask.cancelled.bg
50
+ };
51
+
52
+ // ========== 3D 呼吸灯动画 (in_progress 主任务) ==========
53
+ var _3dBreathPhase = 0;
54
+ var _3dBreathAnimId = null;
55
+ var _3dBreathItems = []; // { sprite, ring1, ring2: THREE.Sprite, baseScale, ring1Base, ring2Base }
56
+
57
+ /** 启动 3D 呼吸灯动画循环 */
58
+ function start3DBreathAnimation() {
59
+ if (_3dBreathAnimId) return;
60
+ function tick() {
61
+ _3dBreathPhase += 0.025;
62
+ if (_3dBreathPhase > Math.PI * 2) _3dBreathPhase -= Math.PI * 2;
63
+ var breath = (Math.sin(_3dBreathPhase) + 1) / 2; // [0, 1]
64
+
65
+ for (var i = 0; i < _3dBreathItems.length; i++) {
66
+ var item = _3dBreathItems[i];
67
+ // 脉冲光晕 Sprite: 缩放 + 透明度振荡
68
+ if (item.sprite && item.sprite.material) {
69
+ var s = item.baseScale * (0.8 + breath * 1.5);
70
+ item.sprite.scale.set(s, s, 1);
71
+ item.sprite.material.opacity = 0.10 + breath * 0.30;
72
+ }
73
+ // 外圈脉冲环 Sprite: 扩展 + 淡出 (始终面向相机)
74
+ if (item.ring1 && item.ring1.material) {
75
+ var r1 = (item.ring1Base || 35) * (0.85 + breath * 0.8);
76
+ item.ring1.scale.set(r1, r1, 1);
77
+ item.ring1.material.opacity = 0.55 * (1 - breath * 0.55);
78
+ }
79
+ // 内圈脉冲环 Sprite: 反向节奏 (呼吸感更强)
80
+ if (item.ring2 && item.ring2.material) {
81
+ var invBreath = 1 - breath;
82
+ var r2 = (item.ring2Base || 22) * (0.9 + invBreath * 0.6);
83
+ item.ring2.scale.set(r2, r2, 1);
84
+ item.ring2.material.opacity = 0.40 * (1 - invBreath * 0.45);
85
+ }
86
+ }
87
+
88
+ _3dBreathAnimId = requestAnimationFrame(tick);
89
+ }
90
+ _3dBreathAnimId = requestAnimationFrame(tick);
91
+ }
92
+
93
+ /** 停止 3D 呼吸灯动画循环 */
94
+ function stop3DBreathAnimation() {
95
+ if (_3dBreathAnimId) {
96
+ cancelAnimationFrame(_3dBreathAnimId);
97
+ _3dBreathAnimId = null;
98
+ }
99
+ }
100
+
101
+ function get3DNodeColor(node) {
102
+ var t = node._type || 'sub-task';
103
+ var status = (node._props || {}).status || 'pending';
104
+ // 主任务: 深绿色系
105
+ if (t === 'main-task') {
106
+ return MAIN_TASK_STATUS_COLORS[status] || MAIN_TASK_STATUS_COLORS.pending;
107
+ }
108
+ // 子任务: pending=暖肤色, completed=亮绿色
109
+ if (t === 'sub-task') {
110
+ return SUB_TASK_STATUS_COLORS[status] || SUB_TASK_STATUS_COLORS.pending;
111
+ }
112
+ return NODE_3D_COLORS[t] || '#6b7280';
113
+ }
114
+
115
+ function get3DLinkColor(link) {
116
+ var label = link._label || '';
117
+ if (label === 'has_main_task') return 'rgba(147,197,253,0.18)';
118
+ if (label === 'has_sub_task') return 'rgba(129,140,248,0.12)';
119
+ if (label === 'has_document') return 'rgba(96,165,250,0.10)';
120
+ if (label === 'has_module') return 'rgba(255,102,0,0.18)';
121
+ if (label === 'module_has_task') return 'rgba(255,102,0,0.15)';
122
+ if (label === 'doc_has_child') return 'rgba(192,132,252,0.12)';
123
+ return 'rgba(75,85,99,0.10)';
124
+ }
125
+
126
+ /** 创建发光纹理 (radial gradient → 用于 Sprite 的光晕效果) */
127
+ function createGlowTexture(color, size) {
128
+ var canvas = document.createElement('canvas');
129
+ canvas.width = size || 64;
130
+ canvas.height = size || 64;
131
+ var ctx = canvas.getContext('2d');
132
+ var cx = canvas.width / 2, cy = canvas.height / 2, r = canvas.width / 2;
133
+ var gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
134
+ gradient.addColorStop(0, color || 'rgba(255,255,255,0.8)');
135
+ gradient.addColorStop(0.15, color ? colorWithAlpha(color, 0.5) : 'rgba(255,255,255,0.5)');
136
+ gradient.addColorStop(0.4, color ? colorWithAlpha(color, 0.15) : 'rgba(255,255,255,0.15)');
137
+ gradient.addColorStop(1, 'rgba(0,0,0,0)');
138
+ ctx.fillStyle = gradient;
139
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
140
+ return canvas;
141
+ }
142
+
143
+ /** 从 hex/rgb 颜色生成带 alpha 的 rgba 字符串 */
144
+ function colorWithAlpha(hex, alpha) {
145
+ if (hex.startsWith('rgba')) return hex; // 已经是 rgba
146
+ // hex → rgb
147
+ var r = 0, g = 0, b = 0;
148
+ if (hex.startsWith('#')) {
149
+ if (hex.length === 4) {
150
+ r = parseInt(hex[1]+hex[1], 16); g = parseInt(hex[2]+hex[2], 16); b = parseInt(hex[3]+hex[3], 16);
151
+ } else {
152
+ r = parseInt(hex.slice(1,3), 16); g = parseInt(hex.slice(3,5), 16); b = parseInt(hex.slice(5,7), 16);
153
+ }
154
+ }
155
+ return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
156
+ }
157
+
158
+ /** 创建环形纹理 (用于呼吸灯脉冲环 Sprite, 始终面向相机) */
159
+ function createRingTexture(color, size) {
160
+ var canvas = document.createElement('canvas');
161
+ canvas.width = size || 128;
162
+ canvas.height = size || 128;
163
+ var ctx = canvas.getContext('2d');
164
+ var cx = canvas.width / 2, cy = canvas.height / 2;
165
+ var r = cx * 0.75;
166
+ // 外圈辉光
167
+ ctx.beginPath();
168
+ ctx.arc(cx, cy, r, 0, Math.PI * 2);
169
+ ctx.strokeStyle = color ? colorWithAlpha(color, 0.15) : 'rgba(139,92,246,0.15)';
170
+ ctx.lineWidth = cx * 0.35;
171
+ ctx.stroke();
172
+ // 主环
173
+ ctx.beginPath();
174
+ ctx.arc(cx, cy, r, 0, Math.PI * 2);
175
+ ctx.strokeStyle = color || '#8b5cf6';
176
+ ctx.lineWidth = cx * 0.1;
177
+ ctx.globalAlpha = 0.85;
178
+ ctx.stroke();
179
+ return canvas;
180
+ }
181
+
182
+ // 缓存 glow 纹理 (避免每个节点重复创建)
183
+ var _glowTextureCache = {};
184
+
185
+ /**
186
+ * 3D Force Graph 渲染器
187
+ * 使用 Three.js WebGL + d3-force-3d 实现 3D 球体力导向可视化
188
+ */
189
+ function render3DGraph(container, visibleNodes, visibleEdges) {
190
+ log('正在创建 3D Force Graph (Three.js WebGL)...', true);
191
+
192
+ // 清空容器
193
+ container.innerHTML = '';
194
+
195
+ // ── 从自定义设置加载参数 ──
196
+ var _s3d = get3DSettings();
197
+ // 重新加载颜色和大小(确保使用最新设置)
198
+ NODE_3D_COLORS = load3DColorsFromSettings();
199
+ NODE_3D_SIZES = load3DSizesFromSettings();
200
+
201
+ // ── 高亮状态追踪 ──
202
+ var _3dSelectedNodeId = null; // 当前选中节点 ID
203
+ var _3dHighlightLinks = new Set(); // 选中节点的关联边 Set
204
+ var _3dHighlightNodes = new Set(); // 选中节点 + 邻居节点 Set
205
+
206
+ // 边类型 → 高亮色映射(与 vis-network edgeStyle 对齐)
207
+ var LINK_HIGHLIGHT_COLORS = {
208
+ 'has_main_task': '#93c5fd',
209
+ 'has_sub_task': '#818cf8',
210
+ 'has_document': '#60a5fa',
211
+ 'has_module': '#ff8533',
212
+ 'module_has_task': '#ff8533',
213
+ 'task_has_doc': '#f59e0b',
214
+ 'doc_has_child': '#c084fc'
215
+ };
216
+
217
+ // 转换数据格式: vis-network edges → 3d-force-graph links
218
+ var links3d = [];
219
+ for (var i = 0; i < visibleEdges.length; i++) {
220
+ var e = visibleEdges[i];
221
+ links3d.push({
222
+ source: e.from,
223
+ target: e.to,
224
+ _label: e._label,
225
+ _width: e.width || 1,
226
+ _color: get3DLinkColor(e),
227
+ _highlightColor: LINK_HIGHLIGHT_COLORS[e._label] || '#a5b4fc',
228
+ _projectEdgeHidden: !!e._projectEdgeHidden // 主节点连线: 参与力模拟但不渲染
229
+ });
230
+ }
231
+
232
+ // 复制节点数据(3d-force-graph 会修改节点对象,添加 x/y/z/vx/vy/vz)
233
+ var nodes3d = [];
234
+ for (var i = 0; i < visibleNodes.length; i++) {
235
+ var n = visibleNodes[i];
236
+ nodes3d.push({
237
+ id: n.id,
238
+ label: n._origLabel || n.label,
239
+ _type: n._type,
240
+ _props: n._props || {},
241
+ _val: NODE_3D_SIZES[n._type] || 5,
242
+ _color: get3DNodeColor(n)
243
+ });
244
+ }
245
+
246
+ // 构建邻接表(用于快速查找节点的关联边和邻居节点)
247
+ var _3dNodeNeighbors = {}; // nodeId → Set of neighbor nodeIds
248
+ var _3dNodeLinks = {}; // nodeId → Set of link references
249
+ for (var i = 0; i < links3d.length; i++) {
250
+ var l = links3d[i];
251
+ var srcId = typeof l.source === 'object' ? l.source.id : l.source;
252
+ var tgtId = typeof l.target === 'object' ? l.target.id : l.target;
253
+ if (!_3dNodeNeighbors[srcId]) _3dNodeNeighbors[srcId] = new Set();
254
+ if (!_3dNodeNeighbors[tgtId]) _3dNodeNeighbors[tgtId] = new Set();
255
+ _3dNodeNeighbors[srcId].add(tgtId);
256
+ _3dNodeNeighbors[tgtId].add(srcId);
257
+ if (!_3dNodeLinks[srcId]) _3dNodeLinks[srcId] = new Set();
258
+ if (!_3dNodeLinks[tgtId]) _3dNodeLinks[tgtId] = new Set();
259
+ _3dNodeLinks[srcId].add(l);
260
+ _3dNodeLinks[tgtId].add(l);
261
+ }
262
+
263
+ // ── 单击/双击判定状态 ──
264
+ var _3dClickTimer = null;
265
+ var _3dClickCount = 0;
266
+ var _3dPendingClickNode = null;
267
+
268
+ /** 双击聚焦: 计算节点及其所有关联节点的包围球, 将摄像机拉到刚好能完整显示的位置 */
269
+ function focus3DNodeWithNeighbors(node) {
270
+ // 收集目标节点 + 所有邻居节点的坐标
271
+ var points = [{ x: node.x || 0, y: node.y || 0, z: node.z || 0 }];
272
+ var neighbors = _3dNodeNeighbors[node.id];
273
+ if (neighbors) {
274
+ neighbors.forEach(function(nId) {
275
+ for (var i = 0; i < nodes3d.length; i++) {
276
+ if (nodes3d[i].id === nId) {
277
+ points.push({ x: nodes3d[i].x || 0, y: nodes3d[i].y || 0, z: nodes3d[i].z || 0 });
278
+ break;
279
+ }
280
+ }
281
+ });
282
+ }
283
+
284
+ // 计算质心
285
+ var cx = 0, cy = 0, cz = 0;
286
+ for (var i = 0; i < points.length; i++) {
287
+ cx += points[i].x; cy += points[i].y; cz += points[i].z;
288
+ }
289
+ cx /= points.length; cy /= points.length; cz /= points.length;
290
+
291
+ // 计算包围球半径 (到质心的最大距离)
292
+ var maxR = 0;
293
+ for (var i = 0; i < points.length; i++) {
294
+ var dx = points[i].x - cx, dy = points[i].y - cy, dz = points[i].z - cz;
295
+ var r = Math.sqrt(dx * dx + dy * dy + dz * dz);
296
+ if (r > maxR) maxR = r;
297
+ }
298
+
299
+ // 摄像机距离: 包围球半径 × 系数, 确保所有节点都在视锥内
300
+ // 系数 2.8 ~ 3.2 可保证 FOV ≈ 70° 时完整可见, 加 padding 余量
301
+ var camDist = Math.max(maxR * 3.0, 80);
302
+
303
+ // 摄像机位于质心的斜上方偏移, 提供良好的 3D 视角
304
+ try {
305
+ graph3d.cameraPosition(
306
+ { x: cx + camDist * 0.58, y: cy + camDist * 0.42, z: cz + camDist * 0.68 },
307
+ { x: cx, y: cy, z: cz },
308
+ 1200
309
+ );
310
+ } catch(e) {}
311
+ }
312
+
313
+ /** 更新高亮集合 */
314
+ function update3DHighlight(nodeId) {
315
+ _3dHighlightLinks.clear();
316
+ _3dHighlightNodes.clear();
317
+ _3dSelectedNodeId = nodeId;
318
+
319
+ if (nodeId) {
320
+ _3dHighlightNodes.add(nodeId);
321
+ // 添加所有邻居节点
322
+ var neighbors = _3dNodeNeighbors[nodeId];
323
+ if (neighbors) neighbors.forEach(function(nId) { _3dHighlightNodes.add(nId); });
324
+ // 添加所有关联边
325
+ var links = _3dNodeLinks[nodeId];
326
+ if (links) links.forEach(function(link) { _3dHighlightLinks.add(link); });
327
+ }
328
+ }
329
+
330
+ var rect = container.getBoundingClientRect();
331
+
332
+ // 创建 3D 图实例
333
+ var graph3d = ForceGraph3D({ controlType: 'orbit' })(container)
334
+ .width(rect.width)
335
+ .height(rect.height)
336
+ .backgroundColor(_s3d.bgColor)
337
+ .showNavInfo(false)
338
+ // ── 节点样式 ──
339
+ .nodeLabel(function(n) {
340
+ var status = (n._props || {}).status || '';
341
+ var statusBadge = '';
342
+ if (status === 'completed') statusBadge = '<span style="color:#22c55e;font-size:10px;">✓ 已完成</span>';
343
+ else if (status === 'in_progress') statusBadge = '<span style="color:#f59e0b;font-size:10px;">● 进行中</span>';
344
+ return '<div style="background:rgba(15,23,42,0.92);color:#e2e8f0;padding:6px 10px;border-radius:6px;font-size:12px;border:1px solid rgba(99,102,241,0.3);backdrop-filter:blur(4px);max-width:280px;">'
345
+ + '<div style="font-weight:600;margin-bottom:2px;">' + (n.label || n.id) + '</div>'
346
+ + (statusBadge ? '<div>' + statusBadge + '</div>' : '')
347
+ + '<div style="color:#94a3b8;font-size:10px;">' + (n._type || '') + '</div>'
348
+ + '</div>';
349
+ })
350
+ .nodeColor(function(n) {
351
+ // 所有节点始终保持原色(不变暗),仅通过连线变化体现选中关系
352
+ return n._color;
353
+ })
354
+ .nodeVal(function(n) { return n._val; })
355
+ .nodeOpacity(_s3d.nodeOpacity)
356
+ .nodeResolution(16)
357
+ // ── 自定义节点: 几何体 + 发光光晕 Sprite (mitbunny 风格) ──
358
+ .nodeThreeObject(function(n) {
359
+ if (typeof THREE === 'undefined') return false;
360
+
361
+ var t = n._type || 'sub-task';
362
+ var color = n._color;
363
+ // 节点始终保持原色(不变暗),仅通过连线变化体现选中关系
364
+ var isHighlighted = _3dSelectedNodeId && _3dHighlightNodes.has(n.id);
365
+
366
+ // ── 创建容器 Group ──
367
+ var group = new THREE.Group();
368
+
369
+ // ── 节点几何体 (核心实体) ──
370
+ var coreMesh;
371
+ if (t === 'module') {
372
+ var size = 10;
373
+ var geo = new THREE.BoxGeometry(size, size, size);
374
+ var mat = new THREE.MeshLambertMaterial({ color: color, transparent: true, opacity: _s3d.nodeOpacity, emissive: color, emissiveIntensity: 0.3 });
375
+ coreMesh = new THREE.Mesh(geo, mat);
376
+ } else if (t === 'project') {
377
+ var geo = new THREE.OctahedronGeometry(14);
378
+ var mat = new THREE.MeshLambertMaterial({ color: color, transparent: true, opacity: _s3d.nodeOpacity, emissive: color, emissiveIntensity: 0.4 });
379
+ coreMesh = new THREE.Mesh(geo, mat);
380
+ } else if (t === 'document') {
381
+ var geo = new THREE.BoxGeometry(7, 8.5, 2);
382
+ var mat = new THREE.MeshLambertMaterial({ color: color, transparent: true, opacity: _s3d.nodeOpacity * 0.92, emissive: color, emissiveIntensity: 0.25 });
383
+ coreMesh = new THREE.Mesh(geo, mat);
384
+ } else {
385
+ // 主任务 / 子任务 → 球体
386
+ var radius = t === 'main-task' ? 5.5 : 3.5;
387
+ var geo = new THREE.SphereGeometry(radius, 16, 12);
388
+ var mat = new THREE.MeshLambertMaterial({ color: color, transparent: true, opacity: _s3d.nodeOpacity, emissive: color, emissiveIntensity: 0.3 });
389
+ coreMesh = new THREE.Mesh(geo, mat);
390
+ }
391
+ group.add(coreMesh);
392
+
393
+ // ── 发光光晕 Sprite (Glow Aura) ──
394
+ if (true) {
395
+ var glowSize = { 'project': 60, 'module': 40, 'main-task': 26, 'sub-task': 18, 'document': 22 }[t] || 16;
396
+
397
+ // 获取或创建缓存的 glow texture
398
+ var cacheKey = color + '_' + glowSize;
399
+ if (!_glowTextureCache[cacheKey]) {
400
+ var canvas = createGlowTexture(color, 128);
401
+ _glowTextureCache[cacheKey] = new THREE.CanvasTexture(canvas);
402
+ }
403
+ var glowTex = _glowTextureCache[cacheKey];
404
+
405
+ var spriteMat = new THREE.SpriteMaterial({
406
+ map: glowTex,
407
+ transparent: true,
408
+ opacity: 0.6,
409
+ blending: THREE.AdditiveBlending,
410
+ depthWrite: false
411
+ });
412
+ var sprite = new THREE.Sprite(spriteMat);
413
+ sprite.scale.set(glowSize, glowSize, 1);
414
+ group.add(sprite);
415
+ }
416
+
417
+ // ── in_progress 主任务: 呼吸脉冲光效 (参考 vis-network 发光效果) ──
418
+ var nodeStatus = (n._props || {}).status || 'pending';
419
+ if (t === 'main-task' && nodeStatus === 'in_progress') {
420
+ // 增强核心球体自发光强度
421
+ if (coreMesh && coreMesh.material) {
422
+ coreMesh.material.emissiveIntensity = 0.6;
423
+ }
424
+
425
+ // 1) 外层脉冲光晕 Sprite (大范围弥散辉光, 类似 vis-network outerGlow)
426
+ var pulseGlowSize = 55;
427
+ var pulseColor = '#7c3aed';
428
+ var pulseCacheKey = pulseColor + '_pulse';
429
+ if (!_glowTextureCache[pulseCacheKey]) {
430
+ _glowTextureCache[pulseCacheKey] = new THREE.CanvasTexture(createGlowTexture(pulseColor, 128));
431
+ }
432
+ var pulseSpriteMat = new THREE.SpriteMaterial({
433
+ map: _glowTextureCache[pulseCacheKey],
434
+ transparent: true,
435
+ opacity: 0.25,
436
+ blending: THREE.AdditiveBlending,
437
+ depthWrite: false
438
+ });
439
+ var pulseSprite = new THREE.Sprite(pulseSpriteMat);
440
+ pulseSprite.scale.set(pulseGlowSize, pulseGlowSize, 1);
441
+ group.add(pulseSprite);
442
+
443
+ // 2) 外圈脉冲环 Sprite (billboard, 始终面向相机)
444
+ var outerRingSize = 35;
445
+ var outerRingCacheKey = '#8b5cf6_ring';
446
+ if (!_glowTextureCache[outerRingCacheKey]) {
447
+ _glowTextureCache[outerRingCacheKey] = new THREE.CanvasTexture(createRingTexture('#8b5cf6', 128));
448
+ }
449
+ var outerRingMat = new THREE.SpriteMaterial({
450
+ map: _glowTextureCache[outerRingCacheKey],
451
+ transparent: true,
452
+ opacity: 0.55,
453
+ blending: THREE.AdditiveBlending,
454
+ depthWrite: false
455
+ });
456
+ var outerRingSprite = new THREE.Sprite(outerRingMat);
457
+ outerRingSprite.scale.set(outerRingSize, outerRingSize, 1);
458
+ group.add(outerRingSprite);
459
+
460
+ // 3) 内圈脉冲环 Sprite (更紧凑)
461
+ var innerRingSize = 22;
462
+ var innerRingCacheKey = '#a78bfa_ring';
463
+ if (!_glowTextureCache[innerRingCacheKey]) {
464
+ _glowTextureCache[innerRingCacheKey] = new THREE.CanvasTexture(createRingTexture('#a78bfa', 128));
465
+ }
466
+ var innerRingMat = new THREE.SpriteMaterial({
467
+ map: _glowTextureCache[innerRingCacheKey],
468
+ transparent: true,
469
+ opacity: 0.4,
470
+ blending: THREE.AdditiveBlending,
471
+ depthWrite: false
472
+ });
473
+ var innerRingSprite = new THREE.Sprite(innerRingMat);
474
+ innerRingSprite.scale.set(innerRingSize, innerRingSize, 1);
475
+ group.add(innerRingSprite);
476
+
477
+ // 记录到呼吸灯列表
478
+ _3dBreathItems.push({
479
+ sprite: pulseSprite,
480
+ ring1: outerRingSprite,
481
+ ring2: innerRingSprite,
482
+ baseScale: pulseGlowSize,
483
+ ring1Base: outerRingSize,
484
+ ring2Base: innerRingSize
485
+ });
486
+ }
487
+
488
+ return group;
489
+ })
490
+ .nodeThreeObjectExtend(false)
491
+ // ── 边可见性: 主节点连线隐藏但保留力模拟 ──
492
+ .linkVisibility(function(l) {
493
+ return !l._projectEdgeHidden; // 隐藏的主节点连线不渲染,但仍参与力导向计算
494
+ })
495
+ // ── 边样式 (支持高亮) ──
496
+ .linkColor(function(l) {
497
+ if (_3dSelectedNodeId) {
498
+ if (_3dHighlightLinks.has(l)) return l._highlightColor; // 关联边高亮
499
+ return 'rgba(30,30,50,0.08)'; // 非关联边几乎隐藏
500
+ }
501
+ return l._color || 'rgba(75,85,99,0.2)';
502
+ })
503
+ .linkWidth(function(l) {
504
+ if (_3dSelectedNodeId && _3dHighlightLinks.has(l)) {
505
+ return 1.5; // 高亮边加粗
506
+ }
507
+ // 极细的蛛网风格 (mitbunny style)
508
+ var label = l._label || '';
509
+ if (label === 'has_main_task') return 0.2;
510
+ if (label === 'has_module') return 0.2;
511
+ if (label === 'module_has_task') return 0.15;
512
+ return 0.1;
513
+ })
514
+ .linkOpacity(function(l) {
515
+ if (_3dSelectedNodeId) {
516
+ return _3dHighlightLinks.has(l) ? 0.9 : 0.03;
517
+ }
518
+ return Math.min(_s3d.linkOpacity, 0.35); // 更透明的蛛网效果
519
+ })
520
+ .linkDirectionalArrowLength(_s3d.arrows ? 1.5 : 0)
521
+ .linkDirectionalArrowRelPos(1)
522
+ .linkDirectionalParticles(function(l) {
523
+ if (!_s3d.particles) return 0;
524
+ // 选中时: 高亮边显示流动粒子
525
+ if (_3dSelectedNodeId && _3dHighlightLinks.has(l)) return 2;
526
+ // 默认: 仅项目级连接少量粒子
527
+ var label = l._label || '';
528
+ if (label === 'has_main_task' || label === 'has_module') return 1;
529
+ return 0;
530
+ })
531
+ .linkDirectionalParticleWidth(function(l) {
532
+ if (_3dSelectedNodeId && _3dHighlightLinks.has(l)) return 1.2;
533
+ return 0.5;
534
+ })
535
+ .linkDirectionalParticleColor(function(l) {
536
+ if (_3dSelectedNodeId && _3dHighlightLinks.has(l)) return l._highlightColor;
537
+ return null; // 默认颜色
538
+ })
539
+ .linkDirectionalParticleSpeed(0.005)
540
+ // ── 力导向参数 (来自自定义设置) ──
541
+ .d3AlphaDecay(_s3d.alphaDecay)
542
+ .d3VelocityDecay(_s3d.velocityDecay)
543
+ // ── 交互事件: 单击/双击区分 ──
544
+ .onNodeClick(function(node, event) {
545
+ if (!node) return;
546
+ _3dPendingClickNode = node;
547
+ _3dClickCount++;
548
+ if (_3dClickCount === 1) {
549
+ // 第一次点击: 等待判定是否双击
550
+ _3dClickTimer = setTimeout(function() {
551
+ _3dClickCount = 0;
552
+ // 单击: 高亮 + 面板
553
+ update3DHighlight(node.id);
554
+ refresh3DStyles();
555
+ panelHistory = [];
556
+ currentPanelNodeId = null;
557
+ showPanel(node.id);
558
+ }, 280);
559
+ } else if (_3dClickCount >= 2) {
560
+ // 双击: 取消单击定时器
561
+ clearTimeout(_3dClickTimer);
562
+ _3dClickCount = 0;
563
+ // 高亮 + 面板 + 聚焦到节点及其关联节点
564
+ update3DHighlight(node.id);
565
+ refresh3DStyles();
566
+ panelHistory = [];
567
+ currentPanelNodeId = null;
568
+ showPanel(node.id);
569
+ focus3DNodeWithNeighbors(node);
570
+ }
571
+ })
572
+ .onNodeDragEnd(function(node) {
573
+ // 拖拽结束后固定节点位置
574
+ node.fx = node.x;
575
+ node.fy = node.y;
576
+ node.fz = node.z;
577
+ })
578
+ .onBackgroundClick(function() {
579
+ // 点击背景: 取消选中 + 关闭面板
580
+ update3DHighlight(null);
581
+ refresh3DStyles();
582
+ closePanel();
583
+ });
584
+
585
+ /** 刷新连线视觉样式(节点不变,仅刷新边的颜色/宽度/粒子) */
586
+ function refresh3DStyles() {
587
+ graph3d.linkColor(graph3d.linkColor())
588
+ .linkWidth(graph3d.linkWidth())
589
+ .linkOpacity(graph3d.linkOpacity())
590
+ .linkDirectionalParticles(graph3d.linkDirectionalParticles())
591
+ .linkDirectionalParticleWidth(graph3d.linkDirectionalParticleWidth())
592
+ .linkDirectionalParticleColor(graph3d.linkDirectionalParticleColor());
593
+ }
594
+
595
+ // ── 增强场景光照 (mitbunny 风格: 柔和环境光 + 点光源) ──
596
+ try {
597
+ var scene = graph3d.scene();
598
+ if (scene && typeof THREE !== 'undefined') {
599
+ // 移除默认光源,用更柔和的光照
600
+ var toRemove = [];
601
+ scene.children.forEach(function(child) {
602
+ if (child.isLight) toRemove.push(child);
603
+ });
604
+ toRemove.forEach(function(l) { scene.remove(l); });
605
+
606
+ // 柔和环境光(整体照亮)
607
+ var ambientLight = new THREE.AmbientLight(0x334466, 1.5);
608
+ scene.add(ambientLight);
609
+
610
+ // 暖色点光源(从上方照射,类似太阳光)
611
+ var pointLight1 = new THREE.PointLight(0xffffff, 0.8, 0);
612
+ pointLight1.position.set(200, 300, 200);
613
+ scene.add(pointLight1);
614
+
615
+ // 冷色辅助光(从下方,增加立体感)
616
+ var pointLight2 = new THREE.PointLight(0x6366f1, 0.4, 0);
617
+ pointLight2.position.set(-200, -200, -100);
618
+ scene.add(pointLight2);
619
+ }
620
+ } catch(e) { console.warn('Scene lighting setup error:', e); }
621
+
622
+ // ========== 布局模式分支 ==========
623
+ var _isOrbital = (_s3d.layoutMode === 'orbital');
624
+
625
+ if (_isOrbital) {
626
+ // ╔══════════════════════════════════════════════════════╗
627
+ // ║ 🪐 行星轨道布局 (Solar System Orbital Layout) ║
628
+ // ║ 节点按类型排列在固定间距的同心轨道上 ║
629
+ // ╚══════════════════════════════════════════════════════╝
630
+ var _orbitSpacing = _s3d.orbitSpacing; // 轨道间距
631
+ var _orbitStrength = _s3d.orbitStrength; // 轨道引力
632
+ var _orbitFlatten = _s3d.orbitFlatten; // Z 轴压平力度
633
+
634
+ // 节点类型 → 轨道编号 (类似: 太阳→水星→金星→地球→火星)
635
+ var ORBIT_MAP = {
636
+ 'project': 0, // ☀ 太阳 — 中心
637
+ 'module': 1, // ☿ 水星 — 第 1 轨道
638
+ 'main-task': 2, // ♀ 金星 — 第 2 轨道
639
+ 'sub-task': 3, // ♂ 火星 — 第 3 轨道
640
+ 'document': 4 // ♃ 木星 — 第 4 轨道
641
+ };
642
+ var _maxOrbit = 4;
643
+
644
+ // 为每个节点计算目标轨道半径
645
+ for (var i = 0; i < nodes3d.length; i++) {
646
+ var orbitIdx = ORBIT_MAP[nodes3d[i]._type] || 3;
647
+ nodes3d[i]._orbitRadius = orbitIdx * _orbitSpacing;
648
+ nodes3d[i]._orbitIndex = orbitIdx;
649
+ }
650
+
651
+ // ── 减弱排斥力 (轨道模式下不需要强排斥) ──
652
+ graph3d.d3Force('charge').strength(function(n) {
653
+ var t = n._type || 'sub-task';
654
+ if (t === 'project') return -5; // 几乎不排斥
655
+ if (t === 'module') return -15;
656
+ return -8;
657
+ });
658
+
659
+ // ── 连接距离使用轨道间距 ──
660
+ graph3d.d3Force('link').distance(function(l) {
661
+ return _orbitSpacing * 0.8;
662
+ }).strength(0.3); // 较弱的连接力,让轨道力主导
663
+
664
+ // ── 关闭默认中心引力 (由轨道力取代) ──
665
+ try {
666
+ var fg = graph3d.d3Force;
667
+ if (fg('x')) fg('x').strength(0);
668
+ if (fg('y')) fg('y').strength(0);
669
+ if (fg('z')) fg('z').strength(0);
670
+ } catch(e) {}
671
+
672
+ // ── 自定义行星轨道力 ──
673
+ // 将节点拉向其目标轨道半径,同时压平 Z 轴形成太阳系圆盘
674
+ var orbitalForce = (function() {
675
+ var _nodes;
676
+ function force(alpha) {
677
+ for (var i = 0; i < _nodes.length; i++) {
678
+ var n = _nodes[i];
679
+ var targetR = n._orbitRadius || 0;
680
+ var dx = n.x || 0;
681
+ var dy = n.y || 0;
682
+ var dz = n.z || 0;
683
+
684
+ if (targetR === 0) {
685
+ // 项目节点 (太阳): 强力拉向原点
686
+ n.vx = (n.vx || 0) - dx * 0.1 * alpha;
687
+ n.vy = (n.vy || 0) - dy * 0.1 * alpha;
688
+ n.vz = (n.vz || 0) - dz * 0.1 * alpha;
689
+ continue;
690
+ }
691
+
692
+ // XY 平面径向距离
693
+ var xyDist = Math.sqrt(dx * dx + dy * dy);
694
+ if (xyDist > 0.001) {
695
+ // 径向力: 将节点拉向目标轨道半径
696
+ var radialK = (targetR - xyDist) / xyDist * _orbitStrength * alpha;
697
+ n.vx = (n.vx || 0) + dx * radialK;
698
+ n.vy = (n.vy || 0) + dy * radialK;
699
+ } else {
700
+ // 节点几乎在原点: 给一个随机方向的推力
701
+ var angle = Math.random() * Math.PI * 2;
702
+ n.vx = (n.vx || 0) + Math.cos(angle) * _orbitStrength * alpha * targetR * 0.1;
703
+ n.vy = (n.vy || 0) + Math.sin(angle) * _orbitStrength * alpha * targetR * 0.1;
704
+ }
705
+
706
+ // Z 轴压平力: 越大越扁 (0=球壳, 1=完全平面)
707
+ n.vz = (n.vz || 0) - dz * _orbitFlatten * _orbitStrength * alpha * 2;
708
+ }
709
+ }
710
+ force.initialize = function(nodes) { _nodes = nodes; };
711
+ return force;
712
+ })();
713
+
714
+ graph3d.d3Force('orbital', orbitalForce);
715
+
716
+ log('🪐 行星轨道布局: 间距=' + _orbitSpacing + ', 强度=' + _orbitStrength + ', 压平=' + _orbitFlatten, true);
717
+
718
+ } else {
719
+ // ╔══════════════════════════════════════════════════════╗
720
+ // ║ ⚡ 力导向布局 (默认 Force-directed) ║
721
+ // ╚══════════════════════════════════════════════════════╝
722
+ var _repulsion = _s3d.repulsion; // 基准排斥力 (负数)
723
+ graph3d.d3Force('charge').strength(function(n) {
724
+ // 大节点排斥力按比例放大
725
+ var t = n._type || 'sub-task';
726
+ if (t === 'project') return _repulsion * 5; // 项目: 5x
727
+ if (t === 'module') return _repulsion * 2; // 模块: 2x
728
+ if (t === 'main-task') return _repulsion * 1; // 主任务: 1x (基准)
729
+ return _repulsion * 0.35; // 子任务/文档: 0.35x
730
+ });
731
+ var _linkDist = _s3d.linkDistance; // 基准连接距离
732
+ graph3d.d3Force('link').distance(function(l) {
733
+ var label = l._label || '';
734
+ if (label === 'has_main_task') return _linkDist * 1.25;
735
+ if (label === 'has_module') return _linkDist * 1.12;
736
+ if (label === 'has_sub_task') return _linkDist * 0.625;
737
+ if (label === 'module_has_task') return _linkDist * 1.0;
738
+ if (label === 'has_document') return _linkDist * 0.875;
739
+ return _linkDist * 0.75;
740
+ }).strength(function(l) {
741
+ var label = l._label || '';
742
+ if (label === 'has_main_task' || label === 'has_module' || label === 'module_has_task') return 0.7;
743
+ return 0.5;
744
+ });
745
+
746
+ // ── 中心引力 (来自自定义设置) ──
747
+ try {
748
+ var fg = graph3d.d3Force;
749
+ if (fg('x')) fg('x').strength(_s3d.gravity);
750
+ if (fg('y')) fg('y').strength(_s3d.gravity);
751
+ if (fg('z')) fg('z').strength(_s3d.gravity);
752
+ } catch(e) { /* 可能不支持,忽略 */ }
753
+
754
+ // ── 🌍 类型分层力 (Type Separation) ──
755
+ // 不同类型节点按固定间距分布在不同轨道层上,类似天体间距
756
+ // project(中心) → module(层1) → document(层2) → main-task(层3) → sub-task(层4)
757
+ if (_s3d.typeSeparation && _s3d.typeSepStrength > 0) {
758
+ var _typeSepSpacing = _s3d.typeSepSpacing;
759
+ var _typeSepK = _s3d.typeSepStrength; // 0~1 控制力强度
760
+
761
+ // 节点类型 → 轨道层编号
762
+ var TYPE_BAND = {
763
+ 'project': 0, // ☀ 中心
764
+ 'module': 1, // 层 1 — 功能模块 (最近)
765
+ 'document': 2, // 层 2 — 文档
766
+ 'main-task': 3, // 层 3 — 主任务
767
+ 'sub-task': 4 // 层 4 — 子任务 (最远)
768
+ };
769
+
770
+ // 为每个节点计算目标轨道半径
771
+ for (var i = 0; i < nodes3d.length; i++) {
772
+ var band = TYPE_BAND[nodes3d[i]._type];
773
+ if (band === undefined) band = 4;
774
+ nodes3d[i]._targetBand = band;
775
+ nodes3d[i]._targetRadius = band * _typeSepSpacing;
776
+ }
777
+
778
+ // 开启分层时: 保留较强排斥力让同层节点互相散开(尤其子任务数量多)
779
+ // 分层力控制径向距离,排斥力控制同层内散布
780
+ graph3d.d3Force('charge').strength(function(n) {
781
+ var t = n._type || 'sub-task';
782
+ if (t === 'project') return _repulsion * 3; // 项目: 3x
783
+ if (t === 'module') return _repulsion * 1.5; // 模块: 1.5x
784
+ if (t === 'main-task') return _repulsion * 1; // 主任务: 1x
785
+ if (t === 'sub-task') return _repulsion * 0.8; // 子任务: 0.8x (数量多,需要足够散开)
786
+ return _repulsion * 0.6; // 文档: 0.6x
787
+ });
788
+
789
+ // 削弱连接力,避免连线把不同层的节点拽到一起
790
+ graph3d.d3Force('link').strength(function(l) {
791
+ var label = l._label || '';
792
+ if (label === 'has_main_task' || label === 'has_module' || label === 'module_has_task') return 0.15;
793
+ return 0.1;
794
+ });
795
+
796
+ // 自定义 D3 力: 强径向弹簧,将节点拉向目标轨道半径
797
+ var typeSepForce = (function() {
798
+ var _nodes;
799
+ function force(alpha) {
800
+ // 找到项目节点(太阳/中心)
801
+ var cx = 0, cy = 0, cz = 0, projectFound = false;
802
+ for (var i = 0; i < _nodes.length; i++) {
803
+ if (_nodes[i]._type === 'project') {
804
+ cx = _nodes[i].x || 0;
805
+ cy = _nodes[i].y || 0;
806
+ cz = _nodes[i].z || 0;
807
+ projectFound = true;
808
+ break;
809
+ }
810
+ }
811
+ if (!projectFound) {
812
+ // 无项目节点: 使用质心
813
+ for (var i = 0; i < _nodes.length; i++) {
814
+ cx += (_nodes[i].x || 0);
815
+ cy += (_nodes[i].y || 0);
816
+ cz += (_nodes[i].z || 0);
817
+ }
818
+ cx /= _nodes.length; cy /= _nodes.length; cz /= _nodes.length;
819
+ }
820
+
821
+ for (var i = 0; i < _nodes.length; i++) {
822
+ var n = _nodes[i];
823
+ var targetR = n._targetRadius || 0;
824
+
825
+ if (targetR === 0) {
826
+ // 项目节点: 强力锚定在原点
827
+ n.vx = (n.vx || 0) - (n.x || 0) * 0.1 * alpha;
828
+ n.vy = (n.vy || 0) - (n.y || 0) * 0.1 * alpha;
829
+ n.vz = (n.vz || 0) - (n.z || 0) * 0.1 * alpha;
830
+ continue;
831
+ }
832
+
833
+ var dx = (n.x || 0) - cx;
834
+ var dy = (n.y || 0) - cy;
835
+ var dz = (n.z || 0) - cz;
836
+ var dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
837
+
838
+ if (dist < 0.5) {
839
+ // 节点太靠近中心: 随机方向推出去
840
+ var angle = Math.random() * Math.PI * 2;
841
+ var phi = (Math.random() - 0.5) * Math.PI * 0.3;
842
+ n.vx = (n.vx || 0) + Math.cos(angle) * Math.cos(phi) * targetR * _typeSepK * alpha * 0.5;
843
+ n.vy = (n.vy || 0) + Math.sin(angle) * Math.cos(phi) * targetR * _typeSepK * alpha * 0.5;
844
+ n.vz = (n.vz || 0) + Math.sin(phi) * targetR * _typeSepK * alpha * 0.2;
845
+ continue;
846
+ }
847
+
848
+ // 径向弹簧力: F = k * (targetR - dist) / dist * direction
849
+ // 这是一个真正的弹簧——偏差越大推力越大,没有上限截断
850
+ var diff = targetR - dist;
851
+ var k = _typeSepK * alpha;
852
+ var radialAccel = diff * k;
853
+
854
+ var invDist = 1 / dist;
855
+ n.vx = (n.vx || 0) + dx * invDist * radialAccel;
856
+ n.vy = (n.vy || 0) + dy * invDist * radialAccel;
857
+ n.vz = (n.vz || 0) + dz * invDist * radialAccel;
858
+ }
859
+ }
860
+ force.initialize = function(nodes) { _nodes = nodes; };
861
+ return force;
862
+ })();
863
+
864
+ graph3d.d3Force('typeSeparation', typeSepForce);
865
+ log('🌍 类型分层: 模块@' + _typeSepSpacing + ' 文档@' + (_typeSepSpacing*2) + ' 主任务@' + (_typeSepSpacing*3) + ' 子任务@' + (_typeSepSpacing*4) + ' 强度=' + _typeSepK, true);
866
+ }
867
+ }
868
+
869
+ // 注入数据
870
+ _3dBreathItems = []; // 重置呼吸灯列表 (nodeThreeObject 回调会填充)
871
+ graph3d.graphData({ nodes: nodes3d, links: links3d });
872
+
873
+ // ── 3D 呼吸灯: nodeThreeObject 回调在下一帧才执行, 需延迟检测 ──
874
+ stop3DBreathAnimation();
875
+ function _checkAndStartBreath() {
876
+ if (_3dBreathItems.length > 0 && !_3dBreathAnimId) {
877
+ start3DBreathAnimation();
878
+ log('3D 呼吸灯: 检测到 ' + _3dBreathItems.length + ' 个进行中主任务', true);
879
+ }
880
+ }
881
+ // 多次检测: 300ms (首帧渲染后) + 1500ms (大数据集延迟)
882
+ setTimeout(_checkAndStartBreath, 300);
883
+ setTimeout(_checkAndStartBreath, 1500);
884
+
885
+ // ── 🪐 行星轨道: 绘制轨道环线 (Three.js) ──
886
+ if (_isOrbital && _s3d.showOrbits) {
887
+ try {
888
+ var scene = graph3d.scene();
889
+ if (scene && typeof THREE !== 'undefined') {
890
+ var orbitColors = [
891
+ null, // orbit 0 (project = center, no ring)
892
+ '#ff6600', // orbit 1 (module) — 橙色
893
+ '#2563eb', // orbit 2 (document) — 蓝色
894
+ '#047857', // orbit 3 (main-task) — 深绿
895
+ '#e8956a' // orbit 4 (sub-task) — 暖肤色
896
+ ];
897
+ var orbitLabels = ['', '模块', '文档', '主任务', '子任务'];
898
+ for (var oi = 1; oi <= _maxOrbit; oi++) {
899
+ var radius = oi * _s3d.orbitSpacing;
900
+ // 使用 THREE.RingGeometry 创建环形 (内径 略小于 外径)
901
+ var ringGeo = new THREE.RingGeometry(radius - 0.3, radius + 0.3, 128);
902
+ var ringColor = orbitColors[oi] || '#334466';
903
+ var ringMat = new THREE.MeshBasicMaterial({
904
+ color: ringColor,
905
+ transparent: true,
906
+ opacity: 0.12,
907
+ side: THREE.DoubleSide,
908
+ depthWrite: false
909
+ });
910
+ var ringMesh = new THREE.Mesh(ringGeo, ringMat);
911
+ // 将环放到 XY 平面 (z=0),不需要旋转因为 RingGeometry 默认在 XY 平面
912
+ ringMesh.renderOrder = -1; // 渲染在节点之后
913
+ scene.add(ringMesh);
914
+
915
+ // 添加虚线发光效果 (第二层更宽的半透明环)
916
+ var glowGeo = new THREE.RingGeometry(radius - 1.5, radius + 1.5, 128);
917
+ var glowMat = new THREE.MeshBasicMaterial({
918
+ color: ringColor,
919
+ transparent: true,
920
+ opacity: 0.04,
921
+ side: THREE.DoubleSide,
922
+ depthWrite: false
923
+ });
924
+ var glowMesh = new THREE.Mesh(glowGeo, glowMat);
925
+ glowMesh.renderOrder = -2;
926
+ scene.add(glowMesh);
927
+ }
928
+ log('轨道环线: ' + _maxOrbit + ' 条轨道已绘制', true);
929
+ }
930
+ } catch(e) {
931
+ console.warn('Orbit rings error:', e);
932
+ }
933
+ }
934
+
935
+ // ── 离群节点修正: 力导向稳定后检查并拉回远离的节点 ──
936
+ setTimeout(function() {
937
+ try {
938
+ var data = graph3d.graphData();
939
+ var ns = data.nodes;
940
+ if (!ns || ns.length === 0) return;
941
+
942
+ // 计算所有节点位置的质心和标准差
943
+ var cx = 0, cy = 0, cz = 0;
944
+ for (var i = 0; i < ns.length; i++) {
945
+ cx += (ns[i].x || 0); cy += (ns[i].y || 0); cz += (ns[i].z || 0);
946
+ }
947
+ cx /= ns.length; cy /= ns.length; cz /= ns.length;
948
+
949
+ // 计算平均距离
950
+ var avgDist = 0;
951
+ for (var i = 0; i < ns.length; i++) {
952
+ var dx = (ns[i].x || 0) - cx, dy = (ns[i].y || 0) - cy, dz = (ns[i].z || 0) - cz;
953
+ avgDist += Math.sqrt(dx*dx + dy*dy + dz*dz);
954
+ }
955
+ avgDist /= ns.length;
956
+
957
+ // 离群阈值: 超过平均距离 3 倍的节点
958
+ var threshold = Math.max(avgDist * 3, 200);
959
+ var outlierFixed = 0;
960
+
961
+ for (var i = 0; i < ns.length; i++) {
962
+ var n = ns[i];
963
+ var dx = (n.x || 0) - cx, dy = (n.y || 0) - cy, dz = (n.z || 0) - cz;
964
+ var dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
965
+ if (dist > threshold) {
966
+ // 将离群节点拉到质心附近(阈值距离处)
967
+ var scale = threshold / dist;
968
+ n.x = cx + dx * scale * 0.5;
969
+ n.y = cy + dy * scale * 0.5;
970
+ n.z = cz + dz * scale * 0.5;
971
+ n.fx = n.x; n.fy = n.y; n.fz = n.z; // 固定位置
972
+ outlierFixed++;
973
+ log('离群节点修正: ' + (n.label || n.id) + ' (距离 ' + Math.round(dist) + ' → ' + Math.round(threshold * 0.5) + ')', true);
974
+ }
975
+ }
976
+
977
+ if (outlierFixed > 0) {
978
+ log('已修正 ' + outlierFixed + ' 个离群节点', true);
979
+ // 短暂释放固定,让力导向微调
980
+ setTimeout(function() {
981
+ var ns2 = graph3d.graphData().nodes;
982
+ for (var i = 0; i < ns2.length; i++) {
983
+ if (ns2[i].fx !== undefined) {
984
+ ns2[i].fx = undefined;
985
+ ns2[i].fy = undefined;
986
+ ns2[i].fz = undefined;
987
+ }
988
+ }
989
+ // 轻微 reheat 让节点自然融入
990
+ graph3d.d3ReheatSimulation();
991
+ }, 2000);
992
+ }
993
+ } catch(e) {
994
+ console.warn('Outlier correction error:', e);
995
+ }
996
+ }, 5000); // 5 秒后执行(等力导向基本稳定)
997
+
998
+ // 创建兼容性 network wrapper(供其他代码使用 network.fit/destroy 等)
999
+ network = {
1000
+ _graph3d: graph3d,
1001
+ _container: container,
1002
+ destroy: function() {
1003
+ // 停止 3D 呼吸灯动画
1004
+ stop3DBreathAnimation();
1005
+ _3dBreathItems = [];
1006
+ try {
1007
+ if (graph3d && graph3d._destructor) graph3d._destructor();
1008
+ else if (graph3d && graph3d.scene) {
1009
+ // 手动清理 Three.js 资源
1010
+ var scene = graph3d.scene();
1011
+ if (scene && scene.children) {
1012
+ while (scene.children.length > 0) scene.remove(scene.children[0]);
1013
+ }
1014
+ var renderer = graph3d.renderer();
1015
+ if (renderer) renderer.dispose();
1016
+ }
1017
+ } catch(e) { console.warn('3D cleanup error:', e); }
1018
+ container.innerHTML = '';
1019
+ },
1020
+ fit: function(opts) {
1021
+ try {
1022
+ graph3d.zoomToFit(opts && opts.animation ? opts.animation.duration || 500 : 500);
1023
+ } catch(e) {}
1024
+ },
1025
+ redraw: function() { /* 3D auto-renders */ },
1026
+ setOptions: function() { /* no-op for 3D */ },
1027
+ getPositions: function(ids) {
1028
+ var result = {};
1029
+ var nodes = graph3d.graphData().nodes;
1030
+ for (var i = 0; i < nodes.length; i++) {
1031
+ var n = nodes[i];
1032
+ if (!ids || ids.indexOf(n.id) >= 0) {
1033
+ result[n.id] = { x: n.x || 0, y: n.y || 0 };
1034
+ }
1035
+ }
1036
+ return result;
1037
+ },
1038
+ moveNode: function(id, x, y) { /* no-op for 3D */ },
1039
+ getScale: function() { return 1; },
1040
+ focus: function(nodeId, opts) {
1041
+ // vis-network 兼容: 聚焦到指定节点(3D 版本 — 平滑移动摄像机)
1042
+ if (!graph3d) return;
1043
+ var nodes3dAll = graph3d.graphData().nodes;
1044
+ var target = null;
1045
+ for (var i = 0; i < nodes3dAll.length; i++) {
1046
+ if (nodes3dAll[i].id === nodeId) { target = nodes3dAll[i]; break; }
1047
+ }
1048
+ if (!target || target.x === undefined) return;
1049
+ var dur = (opts && opts.animation && opts.animation.duration) || 600;
1050
+ var dist = 200; // 合理的聚焦距离
1051
+ graph3d.cameraPosition(
1052
+ { x: target.x, y: target.y, z: (target.z || 0) + dist },
1053
+ { x: target.x, y: target.y, z: target.z || 0 },
1054
+ dur
1055
+ );
1056
+ },
1057
+ selectNodes: function(ids) {
1058
+ if (ids && ids.length > 0) {
1059
+ update3DHighlight(ids[0]);
1060
+ refresh3DStyles();
1061
+ } else {
1062
+ update3DHighlight(null);
1063
+ refresh3DStyles();
1064
+ }
1065
+ },
1066
+ getConnectedEdges: function(nodeId) {
1067
+ // 返回关联边 ID 列表(用于 highlightConnectedEdges 兼容)
1068
+ var edgeIds = [];
1069
+ if (_3dNodeLinks[nodeId]) {
1070
+ _3dNodeLinks[nodeId].forEach(function(l) {
1071
+ if (l._id) edgeIds.push(l._id);
1072
+ });
1073
+ }
1074
+ return edgeIds;
1075
+ },
1076
+ on: function(event, cb) {
1077
+ // 将 vis-network 事件映射到 3D 事件
1078
+ if (event === 'stabilizationIterationsDone') {
1079
+ // 3D 力导向约 3 秒后模拟稳定
1080
+ setTimeout(function() {
1081
+ try { cb(); } catch(e) {}
1082
+ }, 3000);
1083
+ }
1084
+ },
1085
+ off: function() {}
1086
+ };
1087
+
1088
+ networkReusable = false; // 3D 模式不支持增量更新
1089
+
1090
+ // 隐藏加载指示器
1091
+ document.getElementById('loading').style.display = 'none';
1092
+ log('3D 图谱渲染完成! ' + nodes3d.length + ' 节点, ' + links3d.length + ' 边 (Three.js WebGL)', true);
1093
+
1094
+ // 自动聚焦视图
1095
+ setTimeout(function() {
1096
+ try { graph3d.zoomToFit(800); } catch(e) {}
1097
+ }, 2000);
1098
+
1099
+ // 窗口大小变化时自适应
1100
+ window.addEventListener('resize', function() {
1101
+ var newRect = container.getBoundingClientRect();
1102
+ if (newRect.width > 0 && newRect.height > 0) {
1103
+ graph3d.width(newRect.width).height(newRect.height);
1104
+ }
1105
+ });
1106
+ }
1107
+
1108
+ /* handle3DNodeClick 已移除 — 3D 引擎现在使用共享的 showPanel() */
1109
+
1110
+ `;
1111
+ }
1112
+ //# sourceMappingURL=template-graph-3d.js.map