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.
- package/CHANGELOG.md +665 -0
- package/LICENSE +191 -0
- package/README.md +853 -0
- package/hooks/claude-code/post-plan.mjs +391 -0
- package/hooks/claude-code/post-remember.mjs +239 -0
- package/hooks/claude-code/pre-compact.mjs +241 -0
- package/hooks/claude-code/pre-websearch.mjs +90 -0
- package/hooks/claude-code/prompt-submit.mjs +861 -0
- package/hooks/claude-code/session-start.mjs +494 -0
- package/hooks/claude-code/shared.mjs +368 -0
- package/hooks/claude-code/stop.mjs +646 -0
- package/hooks/claude-code/test-user-learning.mjs +531 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/run.mjs +10 -0
- package/mcp-server/src/config.ts +70 -0
- package/mcp-server/src/http.ts +93 -0
- package/mcp-server/src/index.ts +203 -0
- package/mcp-server/src/preload-http.ts +25 -0
- package/mcp-server/src/preload.ts +29 -0
- package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
- package/mcp-server/src/services/categories.ts +429 -0
- package/mcp-server/src/services/discord.ts +702 -0
- package/mcp-server/src/services/file-checksums.ts +33 -0
- package/mcp-server/src/services/local-embeddings.ts +77 -0
- package/mcp-server/src/services/neural-interface.ts +344 -0
- package/mcp-server/src/services/sqlite.ts +823 -0
- package/mcp-server/src/tools/browser-advanced.ts +123 -0
- package/mcp-server/src/tools/browser-interact.ts +255 -0
- package/mcp-server/src/tools/browser-navigate.ts +77 -0
- package/mcp-server/src/tools/browser-observe.ts +1330 -0
- package/mcp-server/src/tools/browser.ts +107 -0
- package/mcp-server/src/tools/card-tools.ts +193 -0
- package/mcp-server/src/tools/card.ts +22 -0
- package/mcp-server/src/tools/category.ts +379 -0
- package/mcp-server/src/tools/discord-channel.ts +274 -0
- package/mcp-server/src/tools/discord-guild.ts +188 -0
- package/mcp-server/src/tools/discord-member.ts +158 -0
- package/mcp-server/src/tools/discord-message.ts +231 -0
- package/mcp-server/src/tools/discord-onboarding.ts +228 -0
- package/mcp-server/src/tools/discord-role.ts +209 -0
- package/mcp-server/src/tools/discord-thread.ts +110 -0
- package/mcp-server/src/tools/discord-webhook.ts +176 -0
- package/mcp-server/src/tools/discord.ts +40 -0
- package/mcp-server/src/tools/forget.ts +38 -0
- package/mcp-server/src/tools/git-tools.ts +141 -0
- package/mcp-server/src/tools/git.ts +13 -0
- package/mcp-server/src/tools/image-tools.ts +98 -0
- package/mcp-server/src/tools/image.ts +17 -0
- package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
- package/mcp-server/src/tools/leonardo.ts +28 -0
- package/mcp-server/src/tools/loop.ts +348 -0
- package/mcp-server/src/tools/memories.ts +118 -0
- package/mcp-server/src/tools/recall.ts +323 -0
- package/mcp-server/src/tools/reflect.ts +145 -0
- package/mcp-server/src/tools/remember.ts +115 -0
- package/mcp-server/src/tools/response.ts +26 -0
- package/mcp-server/src/tools/restore.ts +34 -0
- package/mcp-server/src/tools/sync.ts +114 -0
- package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
- package/mcp-server/src/tools/tictactoe.ts +13 -0
- package/mcp-server/src/tools/utils.ts +25 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
- package/mcp-server/src/tools/whiteboard.ts +22 -0
- package/mcp-server/src/tui.ts +940 -0
- package/mcp-server/src/types.ts +85 -0
- package/mcp-server/tsconfig.json +19 -0
- package/neural-interface/package.json +27 -0
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/claude.svg +1 -0
- package/neural-interface/public/category-logos/gemini-color.svg +1 -0
- package/neural-interface/public/category-logos/openai.svg +1 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude-chat.css +878 -0
- package/neural-interface/public/claude-chat.html +92 -0
- package/neural-interface/public/claude-chat.js +1168 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/gallery.svg +6 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index-legacy.html +14677 -0
- package/neural-interface/public/index.html +212 -0
- package/neural-interface/public/index2d-legacy.html +8193 -0
- package/neural-interface/public/index2d.html +127 -0
- package/neural-interface/public/invite.html +351 -0
- package/neural-interface/public/leonardoai.svg +1 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +14 -0
- package/neural-interface/public/offline.html +299 -0
- package/neural-interface/public/onboarding.html +2335 -0
- package/neural-interface/public/openclaw-logo-text.png +0 -0
- package/neural-interface/public/pin-svgrepo-com.svg +25 -0
- package/neural-interface/public/sessionicon.svg +8 -0
- package/neural-interface/public/shared/ansi-parser.js +305 -0
- package/neural-interface/public/shared/api.js +1043 -0
- package/neural-interface/public/shared/colors.js +153 -0
- package/neural-interface/public/shared/constants.js +141 -0
- package/neural-interface/public/shared/html-shell.js +777 -0
- package/neural-interface/public/shared/html-term-renderer.js +1022 -0
- package/neural-interface/public/shared/i18n.js +147 -0
- package/neural-interface/public/shared/registry.js +82 -0
- package/neural-interface/public/shared/state.js +86 -0
- package/neural-interface/public/shared/storage.js +150 -0
- package/neural-interface/public/shared/styles.css +21212 -0
- package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
- package/neural-interface/public/shared/ui-bookmarks.js +231 -0
- package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
- package/neural-interface/public/shared/ui-command-runner.js +534 -0
- package/neural-interface/public/shared/ui-cost-widget.js +602 -0
- package/neural-interface/public/shared/ui-detail.js +1454 -0
- package/neural-interface/public/shared/ui-explorer.js +1356 -0
- package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
- package/neural-interface/public/shared/ui-games.js +411 -0
- package/neural-interface/public/shared/ui-help.js +107 -0
- package/neural-interface/public/shared/ui-image-gallery.js +461 -0
- package/neural-interface/public/shared/ui-invite.js +519 -0
- package/neural-interface/public/shared/ui-keybinds.js +452 -0
- package/neural-interface/public/shared/ui-layouts.js +297 -0
- package/neural-interface/public/shared/ui-link.js +731 -0
- package/neural-interface/public/shared/ui-loading.js +255 -0
- package/neural-interface/public/shared/ui-menubar.js +499 -0
- package/neural-interface/public/shared/ui-multiselect.js +93 -0
- package/neural-interface/public/shared/ui-navbar.js +222 -0
- package/neural-interface/public/shared/ui-notifications.js +287 -0
- package/neural-interface/public/shared/ui-panels.js +309 -0
- package/neural-interface/public/shared/ui-resume.js +653 -0
- package/neural-interface/public/shared/ui-search.js +145 -0
- package/neural-interface/public/shared/ui-sessions.js +531 -0
- package/neural-interface/public/shared/ui-settings.js +5987 -0
- package/neural-interface/public/shared/ui-sidebar.js +1905 -0
- package/neural-interface/public/shared/ui-skills.js +2805 -0
- package/neural-interface/public/shared/ui-stats.js +38 -0
- package/neural-interface/public/shared/ui-sync.js +201 -0
- package/neural-interface/public/shared/ui-terminal.js +6385 -0
- package/neural-interface/public/shared/ui-tooltip.js +123 -0
- package/neural-interface/public/shared/ui-trash.js +439 -0
- package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
- package/neural-interface/public/shared/ui-tutorial.js +1117 -0
- package/neural-interface/public/shared/ui-update.js +244 -0
- package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
- package/neural-interface/public/shared/ui-workspaces.js +722 -0
- package/neural-interface/public/shared/utils.js +360 -0
- package/neural-interface/public/shared/vterm-buffer.js +1102 -0
- package/neural-interface/public/sw.js +52 -0
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/toolbar-album.svg +8 -0
- package/neural-interface/public/toolbar-question.svg +6 -0
- package/neural-interface/public/toolbar-sessions.svg +6 -0
- package/neural-interface/public/variant/2d/background.js +113 -0
- package/neural-interface/public/variant/2d/context-menu.js +133 -0
- package/neural-interface/public/variant/2d/gfx.js +45 -0
- package/neural-interface/public/variant/2d/graph.js +1635 -0
- package/neural-interface/public/variant/2d/hulls.js +207 -0
- package/neural-interface/public/variant/2d/lasso.js +135 -0
- package/neural-interface/public/variant/2d/layout.js +594 -0
- package/neural-interface/public/variant/2d/main.js +521 -0
- package/neural-interface/public/variant/2d/minimap.js +312 -0
- package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
- package/neural-interface/public/variant/3d/background.js +884 -0
- package/neural-interface/public/variant/3d/camera.js +661 -0
- package/neural-interface/public/variant/3d/gfx.js +116 -0
- package/neural-interface/public/variant/3d/graph.js +2399 -0
- package/neural-interface/public/variant/3d/main.js +500 -0
- package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
- package/neural-interface/scripts/rebuild-pty.js +100 -0
- package/neural-interface/server.js +15000 -0
- package/neural-interface/templates/CLAUDE-template.md +211 -0
- package/package.json +53 -0
- package/postinstall.js +135 -0
- package/setup.js +260 -0
- package/skills/leonardo/SKILL.md +159 -0
- package/skills/leonardo/icon.svg +1 -0
- package/skills/leonardo/modules/browser-guide.md +378 -0
- package/skills/leonardo/modules/image-prompter.md +277 -0
- package/skills/leonardo/modules/model-advisor.md +100 -0
- package/skills/leonardo/modules/prompt-library.md +110 -0
- package/skills/leonardo/modules/style-guide.md +186 -0
- package/skills/leonardo/modules/video-prompter.md +311 -0
- package/skills/synabun/SKILL.md +185 -0
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +374 -0
- package/skills/synabun/modules/changelog.md +194 -0
- package/skills/synabun/modules/idea.md +144 -0
- package/skills/synabun/modules/memorize.md +188 -0
- 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
|
+
}
|