synabun 1.3.0

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 (187) hide show
  1. package/CHANGELOG.md +665 -0
  2. package/LICENSE +191 -0
  3. package/README.md +853 -0
  4. package/hooks/claude-code/post-plan.mjs +391 -0
  5. package/hooks/claude-code/post-remember.mjs +239 -0
  6. package/hooks/claude-code/pre-compact.mjs +241 -0
  7. package/hooks/claude-code/pre-websearch.mjs +90 -0
  8. package/hooks/claude-code/prompt-submit.mjs +861 -0
  9. package/hooks/claude-code/session-start.mjs +494 -0
  10. package/hooks/claude-code/shared.mjs +368 -0
  11. package/hooks/claude-code/stop.mjs +646 -0
  12. package/hooks/claude-code/test-user-learning.mjs +531 -0
  13. package/mcp-server/package.json +38 -0
  14. package/mcp-server/run.mjs +10 -0
  15. package/mcp-server/src/config.ts +70 -0
  16. package/mcp-server/src/http.ts +93 -0
  17. package/mcp-server/src/index.ts +203 -0
  18. package/mcp-server/src/preload-http.ts +25 -0
  19. package/mcp-server/src/preload.ts +29 -0
  20. package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
  21. package/mcp-server/src/services/categories.ts +429 -0
  22. package/mcp-server/src/services/discord.ts +702 -0
  23. package/mcp-server/src/services/file-checksums.ts +33 -0
  24. package/mcp-server/src/services/local-embeddings.ts +77 -0
  25. package/mcp-server/src/services/neural-interface.ts +344 -0
  26. package/mcp-server/src/services/sqlite.ts +823 -0
  27. package/mcp-server/src/tools/browser-advanced.ts +123 -0
  28. package/mcp-server/src/tools/browser-interact.ts +255 -0
  29. package/mcp-server/src/tools/browser-navigate.ts +77 -0
  30. package/mcp-server/src/tools/browser-observe.ts +1330 -0
  31. package/mcp-server/src/tools/browser.ts +107 -0
  32. package/mcp-server/src/tools/card-tools.ts +193 -0
  33. package/mcp-server/src/tools/card.ts +22 -0
  34. package/mcp-server/src/tools/category.ts +379 -0
  35. package/mcp-server/src/tools/discord-channel.ts +274 -0
  36. package/mcp-server/src/tools/discord-guild.ts +188 -0
  37. package/mcp-server/src/tools/discord-member.ts +158 -0
  38. package/mcp-server/src/tools/discord-message.ts +231 -0
  39. package/mcp-server/src/tools/discord-onboarding.ts +228 -0
  40. package/mcp-server/src/tools/discord-role.ts +209 -0
  41. package/mcp-server/src/tools/discord-thread.ts +110 -0
  42. package/mcp-server/src/tools/discord-webhook.ts +176 -0
  43. package/mcp-server/src/tools/discord.ts +40 -0
  44. package/mcp-server/src/tools/forget.ts +38 -0
  45. package/mcp-server/src/tools/git-tools.ts +141 -0
  46. package/mcp-server/src/tools/git.ts +13 -0
  47. package/mcp-server/src/tools/image-tools.ts +98 -0
  48. package/mcp-server/src/tools/image.ts +17 -0
  49. package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
  50. package/mcp-server/src/tools/leonardo.ts +28 -0
  51. package/mcp-server/src/tools/loop.ts +348 -0
  52. package/mcp-server/src/tools/memories.ts +118 -0
  53. package/mcp-server/src/tools/recall.ts +323 -0
  54. package/mcp-server/src/tools/reflect.ts +145 -0
  55. package/mcp-server/src/tools/remember.ts +115 -0
  56. package/mcp-server/src/tools/response.ts +26 -0
  57. package/mcp-server/src/tools/restore.ts +34 -0
  58. package/mcp-server/src/tools/sync.ts +114 -0
  59. package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
  60. package/mcp-server/src/tools/tictactoe.ts +13 -0
  61. package/mcp-server/src/tools/utils.ts +25 -0
  62. package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
  63. package/mcp-server/src/tools/whiteboard.ts +22 -0
  64. package/mcp-server/src/tui.ts +940 -0
  65. package/mcp-server/src/types.ts +85 -0
  66. package/mcp-server/tsconfig.json +19 -0
  67. package/neural-interface/package.json +27 -0
  68. package/neural-interface/public/apple-touch-icon.png +0 -0
  69. package/neural-interface/public/category-logos/claude.svg +1 -0
  70. package/neural-interface/public/category-logos/gemini-color.svg +1 -0
  71. package/neural-interface/public/category-logos/openai.svg +1 -0
  72. package/neural-interface/public/category-logos/synabun.png +0 -0
  73. package/neural-interface/public/claude-chat.css +878 -0
  74. package/neural-interface/public/claude-chat.html +92 -0
  75. package/neural-interface/public/claude-chat.js +1168 -0
  76. package/neural-interface/public/favicon-16x16.png +0 -0
  77. package/neural-interface/public/favicon-32x32.png +0 -0
  78. package/neural-interface/public/gallery.svg +6 -0
  79. package/neural-interface/public/icon-192.png +0 -0
  80. package/neural-interface/public/icon-512.png +0 -0
  81. package/neural-interface/public/index-legacy.html +14677 -0
  82. package/neural-interface/public/index.html +212 -0
  83. package/neural-interface/public/index2d-legacy.html +8193 -0
  84. package/neural-interface/public/index2d.html +127 -0
  85. package/neural-interface/public/invite.html +351 -0
  86. package/neural-interface/public/leonardoai.svg +1 -0
  87. package/neural-interface/public/logo-synabun.png +0 -0
  88. package/neural-interface/public/logoHD.png +0 -0
  89. package/neural-interface/public/manifest.json +14 -0
  90. package/neural-interface/public/offline.html +299 -0
  91. package/neural-interface/public/onboarding.html +2335 -0
  92. package/neural-interface/public/openclaw-logo-text.png +0 -0
  93. package/neural-interface/public/pin-svgrepo-com.svg +25 -0
  94. package/neural-interface/public/sessionicon.svg +8 -0
  95. package/neural-interface/public/shared/ansi-parser.js +305 -0
  96. package/neural-interface/public/shared/api.js +1043 -0
  97. package/neural-interface/public/shared/colors.js +153 -0
  98. package/neural-interface/public/shared/constants.js +141 -0
  99. package/neural-interface/public/shared/html-shell.js +777 -0
  100. package/neural-interface/public/shared/html-term-renderer.js +1022 -0
  101. package/neural-interface/public/shared/i18n.js +147 -0
  102. package/neural-interface/public/shared/registry.js +82 -0
  103. package/neural-interface/public/shared/state.js +86 -0
  104. package/neural-interface/public/shared/storage.js +150 -0
  105. package/neural-interface/public/shared/styles.css +21212 -0
  106. package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
  107. package/neural-interface/public/shared/ui-bookmarks.js +231 -0
  108. package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
  109. package/neural-interface/public/shared/ui-command-runner.js +534 -0
  110. package/neural-interface/public/shared/ui-cost-widget.js +602 -0
  111. package/neural-interface/public/shared/ui-detail.js +1454 -0
  112. package/neural-interface/public/shared/ui-explorer.js +1356 -0
  113. package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
  114. package/neural-interface/public/shared/ui-games.js +411 -0
  115. package/neural-interface/public/shared/ui-help.js +107 -0
  116. package/neural-interface/public/shared/ui-image-gallery.js +461 -0
  117. package/neural-interface/public/shared/ui-invite.js +519 -0
  118. package/neural-interface/public/shared/ui-keybinds.js +452 -0
  119. package/neural-interface/public/shared/ui-layouts.js +297 -0
  120. package/neural-interface/public/shared/ui-link.js +731 -0
  121. package/neural-interface/public/shared/ui-loading.js +255 -0
  122. package/neural-interface/public/shared/ui-menubar.js +499 -0
  123. package/neural-interface/public/shared/ui-multiselect.js +93 -0
  124. package/neural-interface/public/shared/ui-navbar.js +222 -0
  125. package/neural-interface/public/shared/ui-notifications.js +287 -0
  126. package/neural-interface/public/shared/ui-panels.js +309 -0
  127. package/neural-interface/public/shared/ui-resume.js +653 -0
  128. package/neural-interface/public/shared/ui-search.js +145 -0
  129. package/neural-interface/public/shared/ui-sessions.js +531 -0
  130. package/neural-interface/public/shared/ui-settings.js +5987 -0
  131. package/neural-interface/public/shared/ui-sidebar.js +1905 -0
  132. package/neural-interface/public/shared/ui-skills.js +2805 -0
  133. package/neural-interface/public/shared/ui-stats.js +38 -0
  134. package/neural-interface/public/shared/ui-sync.js +201 -0
  135. package/neural-interface/public/shared/ui-terminal.js +6385 -0
  136. package/neural-interface/public/shared/ui-tooltip.js +123 -0
  137. package/neural-interface/public/shared/ui-trash.js +439 -0
  138. package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
  139. package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
  140. package/neural-interface/public/shared/ui-tutorial.js +1117 -0
  141. package/neural-interface/public/shared/ui-update.js +244 -0
  142. package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
  143. package/neural-interface/public/shared/ui-workspaces.js +722 -0
  144. package/neural-interface/public/shared/utils.js +360 -0
  145. package/neural-interface/public/shared/vterm-buffer.js +1102 -0
  146. package/neural-interface/public/sw.js +52 -0
  147. package/neural-interface/public/synabun.png +0 -0
  148. package/neural-interface/public/toolbar-album.svg +8 -0
  149. package/neural-interface/public/toolbar-question.svg +6 -0
  150. package/neural-interface/public/toolbar-sessions.svg +6 -0
  151. package/neural-interface/public/variant/2d/background.js +113 -0
  152. package/neural-interface/public/variant/2d/context-menu.js +133 -0
  153. package/neural-interface/public/variant/2d/gfx.js +45 -0
  154. package/neural-interface/public/variant/2d/graph.js +1635 -0
  155. package/neural-interface/public/variant/2d/hulls.js +207 -0
  156. package/neural-interface/public/variant/2d/lasso.js +135 -0
  157. package/neural-interface/public/variant/2d/layout.js +594 -0
  158. package/neural-interface/public/variant/2d/main.js +521 -0
  159. package/neural-interface/public/variant/2d/minimap.js +312 -0
  160. package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
  161. package/neural-interface/public/variant/3d/background.js +884 -0
  162. package/neural-interface/public/variant/3d/camera.js +661 -0
  163. package/neural-interface/public/variant/3d/gfx.js +116 -0
  164. package/neural-interface/public/variant/3d/graph.js +2399 -0
  165. package/neural-interface/public/variant/3d/main.js +500 -0
  166. package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
  167. package/neural-interface/scripts/rebuild-pty.js +100 -0
  168. package/neural-interface/server.js +15000 -0
  169. package/neural-interface/templates/CLAUDE-template.md +211 -0
  170. package/package.json +53 -0
  171. package/postinstall.js +135 -0
  172. package/setup.js +260 -0
  173. package/skills/leonardo/SKILL.md +159 -0
  174. package/skills/leonardo/icon.svg +1 -0
  175. package/skills/leonardo/modules/browser-guide.md +378 -0
  176. package/skills/leonardo/modules/image-prompter.md +277 -0
  177. package/skills/leonardo/modules/model-advisor.md +100 -0
  178. package/skills/leonardo/modules/prompt-library.md +110 -0
  179. package/skills/leonardo/modules/style-guide.md +186 -0
  180. package/skills/leonardo/modules/video-prompter.md +311 -0
  181. package/skills/synabun/SKILL.md +185 -0
  182. package/skills/synabun/icon.png +0 -0
  183. package/skills/synabun/modules/audit.md +374 -0
  184. package/skills/synabun/modules/changelog.md +194 -0
  185. package/skills/synabun/modules/idea.md +144 -0
  186. package/skills/synabun/modules/memorize.md +188 -0
  187. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,661 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — 3D Camera System
3
+ // WASD + Q/E + Shift movement, camera HUD,
4
+ // orbit constraints, node navigation/animation
5
+ // ═══════════════════════════════════════════
6
+
7
+ import { state, emit, on } from '../../shared/state.js';
8
+ import { KEYS } from '../../shared/constants.js';
9
+ import { storage } from '../../shared/storage.js';
10
+ import { gfx } from './gfx.js';
11
+
12
+ // ── Constants ──
13
+ const FLOOR_Y = -500;
14
+ const CEILING_Y = 2500;
15
+ const CAM_BASE_SPEED = 6.0; // base movement speed per frame
16
+ const CAM_BOOST_MULT = 3.0; // shift multiplier
17
+ const CAM_ACCEL = 0.15; // acceleration smoothing (0-1, higher = snappier)
18
+ const CAM_DECEL = 0.08; // deceleration smoothing (lower = more glide)
19
+ const CAM_HEIGHT_SPEED = 4.5; // vertical movement speed
20
+
21
+ // ── Three.js reusable vectors (global THREE from CDN) ──
22
+ const _camVelocity = new THREE.Vector3();
23
+ const _camMoveDir = new THREE.Vector3();
24
+ const _camForward = new THREE.Vector3();
25
+ const _camRight = new THREE.Vector3();
26
+
27
+ // ── Key state ──
28
+ const _camKeys = {
29
+ w: false, a: false, s: false, d: false,
30
+ q: false, e: false, shift: false, space: false,
31
+ };
32
+
33
+ // Focus mode — blocks all keyboard/mouse camera input
34
+ let _cameraPaused = false;
35
+
36
+ // ── Module state ──
37
+ let _graph = null;
38
+ let _origControlsUpdate = null;
39
+ let _camMoving = false;
40
+ let _camHudVisible = false;
41
+ let _camHudTimeout = null;
42
+ let _hudDrag = null;
43
+ let _camHudPinned = storage.getItem(KEYS.CAM_HUD_PINNED) === 'true';
44
+ let _mouseDown = false; // track mouse button for fly-towards-look movement
45
+ let _lastFrameTime = 0; // for deltaTime computation
46
+ let _tweenCancelled = false; // track if we've cancelled tweens this key-press
47
+
48
+ // ── HUD DOM references (cached at init) ──
49
+ let _$camHud, _$compassFov, _$compassNeedle, _$altFill, _$altValue, _$camSpeed;
50
+ let _$keyQ, _$keyW, _$keyE, _$keyA, _$keyS, _$keyD;
51
+
52
+
53
+ // ═══════════════════════════════════════════
54
+ // ORBIT CONSTRAINTS
55
+ // ═══════════════════════════════════════════
56
+
57
+ /**
58
+ * Apply orbit control constraints: damping, speed, zoom limits,
59
+ * floor/ceiling enforcement via controls.update override.
60
+ */
61
+ function applyOrbitConstraints() {
62
+ const controls = _graph.controls();
63
+ if (!controls) return;
64
+
65
+ // Zoom limit: keep camera inside the sky dome
66
+ controls.maxDistance = 2800;
67
+ controls.minDistance = 30;
68
+
69
+ // Enable damping for smooth, polished orbit feel
70
+ controls.enableDamping = true;
71
+ controls.dampingFactor = 0.08;
72
+
73
+ // Smoother zoom
74
+ controls.zoomSpeed = 0.8;
75
+
76
+ // Smoother rotation
77
+ controls.rotateSpeed = 0.6;
78
+
79
+ // Smoother pan
80
+ controls.panSpeed = 0.6;
81
+
82
+ // Don't auto-rotate
83
+ controls.autoRotate = false;
84
+
85
+ // Floor + ceiling constraint via update override
86
+ _origControlsUpdate = controls.update.bind(controls);
87
+ controls.update = function () {
88
+ _origControlsUpdate();
89
+ const cam = _graph.camera();
90
+ if (cam.position.y < FLOOR_Y + 15) cam.position.y = FLOOR_Y + 15;
91
+ if (cam.position.y > CEILING_Y) cam.position.y = CEILING_Y;
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Remove orbit constraint overrides, restoring default controls.
97
+ * Called when background/scene is cleaned up.
98
+ */
99
+ export function removeOrbitConstraints() {
100
+ const controls = _graph && _graph.controls();
101
+ if (controls && _origControlsUpdate) {
102
+ controls.update = _origControlsUpdate;
103
+ _origControlsUpdate = null;
104
+ controls.maxDistance = Infinity;
105
+ controls.minDistance = 0;
106
+ }
107
+ }
108
+
109
+
110
+ // ═══════════════════════════════════════════
111
+ // CAMERA HUD — DOM overlay with compass, altitude, key indicators
112
+ // ═══════════════════════════════════════════
113
+
114
+ /**
115
+ * Cache all HUD DOM references. Must be called after DOM is ready.
116
+ */
117
+ function cacheHudElements() {
118
+ _$camHud = document.getElementById('camera-hud');
119
+ _$compassFov = document.getElementById('compass-fov');
120
+ _$compassNeedle = document.getElementById('compass-needle');
121
+ _$altFill = document.getElementById('alt-fill');
122
+ _$altValue = document.getElementById('alt-value');
123
+ _$camSpeed = document.getElementById('cam-speed-label');
124
+ _$keyQ = document.getElementById('key-q');
125
+ _$keyW = document.getElementById('key-w');
126
+ _$keyE = document.getElementById('key-e');
127
+ _$keyA = document.getElementById('key-a');
128
+ _$keyS = document.getElementById('key-s');
129
+ _$keyD = document.getElementById('key-d');
130
+ }
131
+
132
+ /**
133
+ * Generate compass tick marks (major every 90 deg, minor every 15 deg).
134
+ */
135
+ function buildCompassTicks() {
136
+ const ticksG = document.getElementById('compass-ticks');
137
+ if (!ticksG) return;
138
+ for (let deg = 0; deg < 360; deg += 15) {
139
+ const isMajor = deg % 90 === 0;
140
+ const r1 = isMajor ? 26 : 28;
141
+ const r2 = 32;
142
+ const rad = (deg - 90) * Math.PI / 180;
143
+ const x1 = 36 + r1 * Math.cos(rad);
144
+ const y1 = 36 + r1 * Math.sin(rad);
145
+ const x2 = 36 + r2 * Math.cos(rad);
146
+ const y2 = 36 + r2 * Math.sin(rad);
147
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
148
+ line.setAttribute('x1', x1); line.setAttribute('y1', y1);
149
+ line.setAttribute('x2', x2); line.setAttribute('y2', y2);
150
+ line.setAttribute('class', isMajor ? 'compass-tick-major' : 'compass-tick');
151
+ ticksG.appendChild(line);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Set up pin toggle, drag grip, and restore persisted position.
157
+ */
158
+ function initHudInteractions() {
159
+ if (!_$camHud) return;
160
+
161
+ // Restore pin state
162
+ if (_camHudPinned) {
163
+ _$camHud.classList.add('pinned');
164
+ _camHudVisible = true;
165
+ }
166
+
167
+ // Restore saved position
168
+ const savedPos = storage.getItem(KEYS.CAM_HUD_POS);
169
+ if (savedPos) {
170
+ try {
171
+ const { right, bottom } = JSON.parse(savedPos);
172
+ _$camHud.style.right = right + 'px';
173
+ _$camHud.style.bottom = bottom + 'px';
174
+ } catch {}
175
+ }
176
+
177
+ // Pin toggle
178
+ const pinBtn = document.getElementById('cam-pin-btn');
179
+ if (pinBtn) {
180
+ pinBtn.addEventListener('click', (e) => {
181
+ e.stopPropagation();
182
+ _camHudPinned = !_camHudPinned;
183
+ _$camHud.classList.toggle('pinned', _camHudPinned);
184
+ storage.setItem(KEYS.CAM_HUD_PINNED, _camHudPinned);
185
+ if (_camHudPinned) {
186
+ _camHudVisible = true;
187
+ _$camHud.classList.add('visible');
188
+ clearTimeout(_camHudTimeout);
189
+ }
190
+ });
191
+ }
192
+
193
+ // Drag via grip
194
+ const grip = document.getElementById('cam-drag-grip');
195
+ if (grip) {
196
+ grip.addEventListener('mousedown', (e) => {
197
+ e.preventDefault();
198
+ e.stopPropagation();
199
+ const rect = _$camHud.getBoundingClientRect();
200
+ _hudDrag = {
201
+ startX: e.clientX, startY: e.clientY,
202
+ startRight: window.innerWidth - rect.right,
203
+ startBottom: window.innerHeight - rect.bottom,
204
+ };
205
+ _$camHud.classList.add('dragging');
206
+ document.body.style.cursor = 'grabbing';
207
+ document.body.style.userSelect = 'none';
208
+ });
209
+ }
210
+
211
+ document.addEventListener('mousemove', (e) => {
212
+ if (!_hudDrag) return;
213
+ const dx = e.clientX - _hudDrag.startX;
214
+ const dy = e.clientY - _hudDrag.startY;
215
+ const newRight = Math.max(0, _hudDrag.startRight - dx);
216
+ const newBottom = Math.max(0, _hudDrag.startBottom - dy);
217
+ _$camHud.style.right = newRight + 'px';
218
+ _$camHud.style.bottom = newBottom + 'px';
219
+ });
220
+
221
+ document.addEventListener('mouseup', () => {
222
+ if (!_hudDrag) return;
223
+ _hudDrag = null;
224
+ _$camHud.classList.remove('dragging');
225
+ document.body.style.cursor = '';
226
+ document.body.style.userSelect = '';
227
+ // Persist position
228
+ storage.setItem(KEYS.CAM_HUD_POS, JSON.stringify({
229
+ right: parseFloat(_$camHud.style.right) || 24,
230
+ bottom: parseFloat(_$camHud.style.bottom) || 70,
231
+ }));
232
+ });
233
+ }
234
+
235
+ /**
236
+ * Show the camera HUD (with auto-hide timeout unless pinned).
237
+ */
238
+ function showCamHud() {
239
+ if (_camHudPinned) { _camHudVisible = true; return; }
240
+ if (!_camHudVisible) {
241
+ _camHudVisible = true;
242
+ if (_$camHud) _$camHud.classList.add('visible');
243
+ }
244
+ clearTimeout(_camHudTimeout);
245
+ _camHudTimeout = setTimeout(() => {
246
+ if (!_camMoving && !_camHudPinned && !_hudDrag) {
247
+ _camHudVisible = false;
248
+ if (_$camHud) _$camHud.classList.remove('visible');
249
+ }
250
+ }, 2500);
251
+ }
252
+
253
+ /**
254
+ * Update all camera HUD elements: compass, altitude bar, key states, speed.
255
+ * Called every frame from updateCameraMovement.
256
+ */
257
+ export function updateCamHud() {
258
+ if (!_graph || (!_camHudVisible && !_camHudPinned)) return;
259
+ const cam = _graph.camera();
260
+ const controls = _graph.controls();
261
+ if (!cam || !controls) return;
262
+
263
+ // --- Compass: rotate based on camera yaw ---
264
+ const dir = new THREE.Vector3();
265
+ cam.getWorldDirection(dir);
266
+ // Yaw angle from -Z (north in 3D) projected onto XZ plane
267
+ const yaw = Math.atan2(dir.x, dir.z);
268
+ const yawDeg = yaw * 180 / Math.PI;
269
+
270
+ // Rotate the entire compass ticks/labels group
271
+ const ticksG = document.getElementById('compass-ticks');
272
+ if (ticksG) ticksG.setAttribute('transform', `rotate(${-yawDeg}, 36, 36)`);
273
+
274
+ // Rotate cardinal labels
275
+ const labels = { 'N': 0, 'E': 90, 'S': 180, 'W': 270 };
276
+ const labelEls = document.querySelectorAll('.compass-label');
277
+ labelEls.forEach(el => {
278
+ const card = el.textContent;
279
+ if (labels[card] !== undefined) {
280
+ const angleDeg = labels[card] - yawDeg;
281
+ const angleRad = (angleDeg - 90) * Math.PI / 180;
282
+ const r = 33;
283
+ const x = 36 + r * Math.cos(angleRad);
284
+ const y = 36 + r * Math.sin(angleRad);
285
+ el.setAttribute('x', x);
286
+ el.setAttribute('y', y);
287
+ }
288
+ });
289
+
290
+ // FOV wedge
291
+ const fov = cam.fov || 60;
292
+ const halfFov = fov / 2;
293
+ const wedgeR = 20;
294
+ const a1 = (-90 - halfFov) * Math.PI / 180;
295
+ const a2 = (-90 + halfFov) * Math.PI / 180;
296
+ const x1 = 36 + wedgeR * Math.cos(a1);
297
+ const y1 = 36 + wedgeR * Math.sin(a1);
298
+ const x2 = 36 + wedgeR * Math.cos(a2);
299
+ const y2 = 36 + wedgeR * Math.sin(a2);
300
+ if (_$compassFov) {
301
+ _$compassFov.setAttribute('d', `M36,36 L${x1},${y1} A${wedgeR},${wedgeR} 0 0,1 ${x2},${y2} Z`);
302
+ }
303
+
304
+ // --- Altitude bar (vertical) ---
305
+ const minY = FLOOR_Y + 15;
306
+ const maxY = CEILING_Y;
307
+ const altPct = Math.max(0, Math.min(100, ((cam.position.y - minY) / (maxY - minY)) * 100));
308
+ if (_$altFill) _$altFill.style.height = altPct + '%';
309
+ if (_$altValue) _$altValue.textContent = Math.round(cam.position.y);
310
+
311
+ // --- Key states ---
312
+ if (_$keyQ) _$keyQ.classList.toggle('active', _camKeys.q);
313
+ if (_$keyW) _$keyW.classList.toggle('active', _camKeys.w);
314
+ if (_$keyE) _$keyE.classList.toggle('active', _camKeys.e || _camKeys.space);
315
+ if (_$keyA) _$keyA.classList.toggle('active', _camKeys.a);
316
+ if (_$keyS) _$keyS.classList.toggle('active', _camKeys.s);
317
+ if (_$keyD) _$keyD.classList.toggle('active', _camKeys.d);
318
+
319
+ // --- Speed label ---
320
+ const speed = _camVelocity.length();
321
+ if (speed > 0.1) {
322
+ if (_camKeys.shift) {
323
+ if (_$camSpeed) _$camSpeed.innerHTML = `<span class="boost">BOOST</span> ${speed.toFixed(1)}`;
324
+ } else {
325
+ if (_$camSpeed) _$camSpeed.textContent = `MOVE ${speed.toFixed(1)}`;
326
+ }
327
+ } else {
328
+ if (_$camSpeed) _$camSpeed.textContent = 'MOVE';
329
+ }
330
+ }
331
+
332
+
333
+ // ═══════════════════════════════════════════
334
+ // WASD MOVEMENT — per-frame update
335
+ // ═══════════════════════════════════════════
336
+
337
+ /**
338
+ * Per-frame camera movement update. Call from the animation loop.
339
+ * Reads _camKeys state, computes smoothed velocity, applies to camera + orbit target.
340
+ */
341
+ export function updateCameraMovement() {
342
+ if (_cameraPaused || !_graph) return;
343
+ const cam = _graph.camera();
344
+ const controls = _graph.controls();
345
+ if (!cam || !controls) return;
346
+
347
+ // DeltaTime: normalize to 60fps so constants stay the same
348
+ const now = performance.now();
349
+ const rawDt = _lastFrameTime ? (now - _lastFrameTime) : 16.667;
350
+ _lastFrameTime = now;
351
+ const dt = Math.min(rawDt, 50) / 16.667; // 1.0 at 60fps, 2.0 at 30fps, capped ~3x
352
+
353
+ // Build desired movement direction from key states
354
+ _camMoveDir.set(0, 0, 0);
355
+ const anyKey = _camKeys.w || _camKeys.a || _camKeys.s || _camKeys.d || _camKeys.q || _camKeys.e || _camKeys.space;
356
+
357
+ if (anyKey) {
358
+ // Cancel any active camera TWEEN animation to prevent fighting
359
+ if (!_tweenCancelled) {
360
+ TWEEN.removeAll();
361
+ _tweenCancelled = true;
362
+ }
363
+
364
+ cam.getWorldDirection(_camForward);
365
+
366
+ if (_mouseDown) {
367
+ // Fly mode: W/S move along full 3D look direction (towards where camera faces)
368
+ _camForward.normalize();
369
+ _camRight.crossVectors(_camForward, cam.up).normalize();
370
+ } else {
371
+ // Ground mode: W/S move on XZ plane only
372
+ _camForward.y = 0;
373
+ _camForward.normalize();
374
+ _camRight.crossVectors(_camForward, cam.up).normalize();
375
+ }
376
+
377
+ if (_camKeys.w) _camMoveDir.add(_camForward);
378
+ if (_camKeys.s) _camMoveDir.sub(_camForward);
379
+ if (_camKeys.d) _camMoveDir.add(_camRight);
380
+ if (_camKeys.a) _camMoveDir.sub(_camRight);
381
+
382
+ // Height: Q = down, E = up, Space = up
383
+ if (_camKeys.e) _camMoveDir.y += 1;
384
+ if (_camKeys.q) _camMoveDir.y -= 1;
385
+ if (_camKeys.space) _camMoveDir.y += 1;
386
+
387
+ if (_camMoveDir.lengthSq() > 0) _camMoveDir.normalize();
388
+
389
+ const speed = CAM_BASE_SPEED * (_camKeys.shift ? CAM_BOOST_MULT : 1);
390
+
391
+ // Scale speed by distance from target -- sqrt curve, gentler at close range
392
+ const distToTarget = cam.position.distanceTo(controls.target);
393
+ const distScale = Math.max(0.7, Math.min(4.0, Math.sqrt(distToTarget / 100)));
394
+
395
+ _camMoveDir.multiplyScalar(speed * distScale * dt);
396
+
397
+ // Vertical speed is independent
398
+ if (_camKeys.e || _camKeys.q || _camKeys.space) {
399
+ const hSpeed = CAM_HEIGHT_SPEED * (_camKeys.shift ? CAM_BOOST_MULT : 1) * distScale * dt;
400
+ const hDir = (_camKeys.e || _camKeys.space ? 1 : 0) + (_camKeys.q ? -1 : 0);
401
+ _camMoveDir.y = hDir * hSpeed;
402
+ }
403
+
404
+ _camMoving = true;
405
+ showCamHud();
406
+ } else {
407
+ _camMoving = false;
408
+ _tweenCancelled = false;
409
+ }
410
+
411
+ // Smooth acceleration/deceleration (frame-rate independent)
412
+ const smoothing = anyKey ? CAM_ACCEL : CAM_DECEL;
413
+ const dtSmoothing = 1 - Math.pow(1 - smoothing, dt);
414
+ _camVelocity.lerp(_camMoveDir, dtSmoothing);
415
+
416
+ // Apply movement to both camera and orbit target
417
+ if (_camVelocity.lengthSq() > 0.001) {
418
+ cam.position.add(_camVelocity);
419
+ controls.target.add(_camVelocity);
420
+
421
+ // Enforce floor/ceiling constraints
422
+ if (cam.position.y < FLOOR_Y + 15) {
423
+ const diff = (FLOOR_Y + 15) - cam.position.y;
424
+ cam.position.y = FLOOR_Y + 15;
425
+ controls.target.y += diff;
426
+ }
427
+ if (cam.position.y > CEILING_Y) {
428
+ const diff = CEILING_Y - cam.position.y;
429
+ cam.position.y = CEILING_Y;
430
+ controls.target.y += diff;
431
+ }
432
+
433
+ showCamHud();
434
+ } else {
435
+ _camVelocity.set(0, 0, 0);
436
+ }
437
+
438
+ updateCamHud();
439
+ }
440
+
441
+
442
+ // ═══════════════════════════════════════════
443
+ // KEYBOARD LISTENERS — WASD + Q/E + Shift + Space
444
+ // ═══════════════════════════════════════════
445
+
446
+ /**
447
+ * Bind keydown/keyup/blur listeners for camera movement keys.
448
+ * Returns a cleanup function that removes all listeners.
449
+ */
450
+ function bindKeyListeners() {
451
+ function isEditing() {
452
+ const el = document.activeElement;
453
+ if (!el) return false;
454
+ const tag = el.tagName;
455
+ return tag === 'INPUT' || tag === 'TEXTAREA' || el.isContentEditable;
456
+ }
457
+
458
+ function onKeyDown(e) {
459
+ if (_cameraPaused || isEditing()) return;
460
+
461
+ const k = e.key.toLowerCase();
462
+ if (k === 'w' || k === 'a' || k === 's' || k === 'd' || k === 'q' || k === 'e') {
463
+ e.preventDefault();
464
+ _camKeys[k] = true;
465
+ }
466
+ if (e.key === 'Shift') _camKeys.shift = true;
467
+ if (k === ' ') { e.preventDefault(); _camKeys.space = true; }
468
+ }
469
+
470
+ function onKeyUp(e) {
471
+ if (_cameraPaused || isEditing()) return;
472
+ const k = e.key.toLowerCase();
473
+ if (k === 'w' || k === 'a' || k === 's' || k === 'd' || k === 'q' || k === 'e') {
474
+ _camKeys[k] = false;
475
+ }
476
+ if (e.key === 'Shift') _camKeys.shift = false;
477
+ if (k === ' ') _camKeys.space = false;
478
+ }
479
+
480
+ function onBlur() {
481
+ _camKeys.w = _camKeys.a = _camKeys.s = _camKeys.d = false;
482
+ _camKeys.q = _camKeys.e = _camKeys.shift = _camKeys.space = false;
483
+ _mouseDown = false;
484
+ _lastFrameTime = 0; // avoid deltaTime spike on refocus
485
+ }
486
+
487
+ function onMouseDown(e) { if (!_cameraPaused && (e.button === 0 || e.button === 2)) _mouseDown = true; }
488
+ function onMouseUp() { _mouseDown = false; }
489
+
490
+ document.addEventListener('keydown', onKeyDown);
491
+ document.addEventListener('keyup', onKeyUp);
492
+ document.addEventListener('mousedown', onMouseDown);
493
+ document.addEventListener('mouseup', onMouseUp);
494
+ window.addEventListener('blur', onBlur);
495
+
496
+ return () => {
497
+ document.removeEventListener('keydown', onKeyDown);
498
+ document.removeEventListener('keyup', onKeyUp);
499
+ document.removeEventListener('mousedown', onMouseDown);
500
+ document.removeEventListener('mouseup', onMouseUp);
501
+ window.removeEventListener('blur', onBlur);
502
+ };
503
+ }
504
+
505
+
506
+ // ═══════════════════════════════════════════
507
+ // CAMERA ANIMATION — smooth transitions to nodes
508
+ // ═══════════════════════════════════════════
509
+
510
+ /**
511
+ * Animate the camera to focus on a specific node.
512
+ * Uses the gfx clickZoom/gentleZoom distance from gfx config.
513
+ *
514
+ * @param {object} node - The graph node (must have x, y, z)
515
+ * @param {object} [opts]
516
+ * @param {'close'|'gentle'} [opts.zoom='close'] - Zoom level preset
517
+ * @param {number} [opts.duration=1500] - Transition duration in ms
518
+ */
519
+ export function animateCameraToNode(node, { zoom = 'close', duration = 1500 } = {}) {
520
+ if (!_graph || !node) return;
521
+ const dist = zoom === 'gentle' ? gfx.gentleZoom : gfx.clickZoom;
522
+ _graph.cameraPosition(
523
+ { x: node.x + dist, y: node.y + dist * 0.4, z: node.z + dist },
524
+ { x: node.x, y: node.y, z: node.z },
525
+ duration,
526
+ );
527
+ }
528
+
529
+ /**
530
+ * Animate the camera to frame a bounding region (e.g. tag cluster).
531
+ *
532
+ * @param {object} center - { x, y, z } center of the bounding box
533
+ * @param {number} span - Largest dimension of the bounding box
534
+ * @param {number} [duration=1500] - Transition duration in ms
535
+ */
536
+ export function animateCameraToRegion(center, span, duration = 1500) {
537
+ if (!_graph) return;
538
+ const dist = span * 1.1 + 30;
539
+ _graph.cameraPosition(
540
+ { x: center.x + dist * 0.5, y: center.y + dist * 0.3, z: center.z + dist * 0.5 },
541
+ { x: center.x, y: center.y, z: center.z },
542
+ duration,
543
+ );
544
+ }
545
+
546
+ /**
547
+ * Set camera to a specific position/lookAt with optional smooth transition.
548
+ *
549
+ * @param {object} position - { x, y, z } camera position
550
+ * @param {object} lookAt - { x, y, z } orbit target / look-at point
551
+ * @param {number} [duration=2000] - Transition duration in ms (0 = instant)
552
+ */
553
+ export function setCameraPosition(position, lookAt, duration = 2000) {
554
+ if (!_graph) return;
555
+ _graph.cameraPosition(position, lookAt, duration);
556
+ }
557
+
558
+ /**
559
+ * Frame the camera to show the entire graph based on node extent.
560
+ * Used when loading layouts / initial data.
561
+ *
562
+ * @param {object[]} nodes - Array of nodes with x, y, z
563
+ * @param {number} [duration=2000] - Transition duration in ms
564
+ */
565
+ export function frameCameraToExtent(nodes, duration = 2000) {
566
+ if (!_graph || !nodes || nodes.length === 0) return;
567
+ let maxDist = 0;
568
+ for (const n of nodes) {
569
+ const x = n.x || 0, y = n.y || 0, z = n.z || 0;
570
+ const d = Math.sqrt(x * x + y * y + z * z);
571
+ if (d > maxDist) maxDist = d;
572
+ }
573
+ const camDist = Math.max(400, maxDist * 2.2);
574
+ _graph.cameraPosition(
575
+ { x: camDist * 0.7, y: camDist * 0.5, z: camDist * 0.7 },
576
+ { x: 0, y: 0, z: 0 },
577
+ duration,
578
+ );
579
+ }
580
+
581
+ /**
582
+ * Save current camera state (position + lookAt) for layout presets.
583
+ * @returns {{ position: {x,y,z}, lookAt: {x,y,z} } | null}
584
+ */
585
+ export function saveCameraState() {
586
+ if (!_graph) return null;
587
+ const cam = _graph.camera();
588
+ const controls = _graph.controls();
589
+ if (!cam || !controls) return null;
590
+ return {
591
+ position: { x: cam.position.x, y: cam.position.y, z: cam.position.z },
592
+ lookAt: controls.target
593
+ ? { x: controls.target.x, y: controls.target.y, z: controls.target.z }
594
+ : { x: 0, y: 0, z: 0 },
595
+ };
596
+ }
597
+
598
+ /**
599
+ * Restore camera from a saved state (layout preset).
600
+ * @param {{ position: {x,y,z}, lookAt: {x,y,z} }} cameraState
601
+ * @param {number} [duration=2000]
602
+ */
603
+ export function restoreCameraState(cameraState, duration = 2000) {
604
+ if (!_graph || !cameraState) return;
605
+ _graph.cameraPosition(cameraState.position, cameraState.lookAt, duration);
606
+ }
607
+
608
+
609
+ // ═══════════════════════════════════════════
610
+ // INIT — main entry point
611
+ // ═══════════════════════════════════════════
612
+
613
+ /**
614
+ * Initialize the full camera system: WASD movement, HUD, orbit constraints, keyboard listeners.
615
+ *
616
+ * @param {object} graph - The graph proxy (scene/camera/controls/cameraPosition API)
617
+ * @returns {{ destroy: Function }} Cleanup handle
618
+ */
619
+ export function initCamera(graph) {
620
+ _graph = graph;
621
+
622
+ // Cache HUD DOM references
623
+ cacheHudElements();
624
+
625
+ // Build compass tick marks
626
+ buildCompassTicks();
627
+
628
+ // Set up HUD pin/drag interactions
629
+ initHudInteractions();
630
+
631
+ // Apply orbit constraints (damping, zoom limits, floor/ceiling)
632
+ applyOrbitConstraints();
633
+
634
+ // Bind WASD keyboard listeners
635
+ const unbindKeys = bindKeyListeners();
636
+
637
+ // Return cleanup handle
638
+ return {
639
+ destroy() {
640
+ unbindKeys();
641
+ removeOrbitConstraints();
642
+ clearTimeout(_camHudTimeout);
643
+ _graph = null;
644
+ },
645
+ };
646
+ }
647
+
648
+ /** Pause camera input (focus mode). Clears any held keys. */
649
+ export function pauseCamera() {
650
+ _cameraPaused = true;
651
+ _camKeys.w = _camKeys.a = _camKeys.s = _camKeys.d = false;
652
+ _camKeys.q = _camKeys.e = _camKeys.shift = _camKeys.space = false;
653
+ _mouseDown = false;
654
+ _lastFrameTime = 0;
655
+ }
656
+
657
+ /** Resume camera input after focus mode exit. */
658
+ export function resumeCamera() {
659
+ _cameraPaused = false;
660
+ _lastFrameTime = 0;
661
+ }