@cognipilot/rumoca 0.9.5 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/rumoca_bind_wasm.js
CHANGED
|
@@ -1835,7 +1835,7 @@ function __wbg_get_imports() {
|
|
|
1835
1835
|
const ret = Reflect.get(arg0, arg1);
|
|
1836
1836
|
return ret;
|
|
1837
1837
|
}, arguments); },
|
|
1838
|
-
|
|
1838
|
+
__wbg_log_de01f0de2d64abfc: function(arg0, arg1) {
|
|
1839
1839
|
console.log(getStringFromWasm0(arg0, arg1));
|
|
1840
1840
|
},
|
|
1841
1841
|
__wbg_new_361308b2356cecd0: function() {
|
package/rumoca_bind_wasm_bg.wasm
CHANGED
|
Binary file
|
|
Binary file
|
package/rumoca_interactive.js
CHANGED
|
@@ -44,6 +44,104 @@ function clamp(value, limits) {
|
|
|
44
44
|
: value;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function normalizePacingMode(value) {
|
|
48
|
+
const text = trimMaybeString(value).toLowerCase().replace(/[-\s]+/g, '_');
|
|
49
|
+
return text === 'as_fast_as_possible' ? 'as_fast_as_possible' : 'realtime';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pacingModeLabel(mode) {
|
|
53
|
+
return mode === 'as_fast_as_possible' ? 'fast' : 'realtime';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function speedRatioLabel(value) {
|
|
57
|
+
const ratio = finiteNumber(value, 0);
|
|
58
|
+
if (ratio >= 100) {
|
|
59
|
+
return `${ratio.toFixed(0)}x`;
|
|
60
|
+
}
|
|
61
|
+
if (ratio >= 10) {
|
|
62
|
+
return `${ratio.toFixed(1)}x`;
|
|
63
|
+
}
|
|
64
|
+
return `${ratio.toFixed(2)}x`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ensureInteractiveRuntimeStyles(ownerDocument) {
|
|
68
|
+
if (!ownerDocument || ownerDocument.getElementById('rumoca-interactive-runtime-styles')) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const style = ownerDocument.createElement('style');
|
|
72
|
+
style.id = 'rumoca-interactive-runtime-styles';
|
|
73
|
+
style.textContent = `
|
|
74
|
+
.rumoca-interactive-root {
|
|
75
|
+
position: relative;
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
touch-action: none;
|
|
78
|
+
}
|
|
79
|
+
.rumoca-interactive-root:fullscreen {
|
|
80
|
+
width: 100vw;
|
|
81
|
+
height: 100vh;
|
|
82
|
+
background: #071825;
|
|
83
|
+
}
|
|
84
|
+
.rumoca-interactive-root:-webkit-full-screen {
|
|
85
|
+
width: 100vw;
|
|
86
|
+
height: 100vh;
|
|
87
|
+
background: #071825;
|
|
88
|
+
}
|
|
89
|
+
.rumoca-interactive-canvas {
|
|
90
|
+
display: block;
|
|
91
|
+
width: 100%;
|
|
92
|
+
height: 100%;
|
|
93
|
+
}
|
|
94
|
+
.rumoca-interactive-flight-hud {
|
|
95
|
+
position: absolute;
|
|
96
|
+
inset: 0;
|
|
97
|
+
z-index: 4;
|
|
98
|
+
pointer-events: none;
|
|
99
|
+
}
|
|
100
|
+
.rumoca-interactive-controls {
|
|
101
|
+
position: absolute;
|
|
102
|
+
left: 12px;
|
|
103
|
+
right: 12px;
|
|
104
|
+
bottom: 12px;
|
|
105
|
+
z-index: 5;
|
|
106
|
+
display: flex;
|
|
107
|
+
flex-wrap: wrap;
|
|
108
|
+
gap: 6px;
|
|
109
|
+
align-items: center;
|
|
110
|
+
pointer-events: auto;
|
|
111
|
+
}
|
|
112
|
+
.rumoca-interactive-controls button,
|
|
113
|
+
.rumoca-interactive-controls .rumoca-interactive-key-echo {
|
|
114
|
+
min-height: 28px;
|
|
115
|
+
padding: 4px 9px;
|
|
116
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
117
|
+
border-radius: 6px;
|
|
118
|
+
background: rgba(10, 14, 18, 0.82);
|
|
119
|
+
color: #f4f7fb;
|
|
120
|
+
font: inherit;
|
|
121
|
+
font-size: 12px;
|
|
122
|
+
line-height: 18px;
|
|
123
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
|
124
|
+
}
|
|
125
|
+
.rumoca-interactive-controls button {
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
}
|
|
128
|
+
.rumoca-interactive-controls.is-capturing .rumoca-interactive-capture-toggle,
|
|
129
|
+
.rumoca-interactive-controls .rumoca-interactive-pacing-toggle.is-fast,
|
|
130
|
+
.rumoca-interactive-controls .rumoca-interactive-fullscreen-toggle.is-fullscreen {
|
|
131
|
+
border-color: #37b7ff;
|
|
132
|
+
background: rgba(0, 103, 168, 0.88);
|
|
133
|
+
}
|
|
134
|
+
@media (max-width: 640px) {
|
|
135
|
+
.rumoca-interactive-controls button,
|
|
136
|
+
.rumoca-interactive-controls .rumoca-interactive-key-echo {
|
|
137
|
+
min-height: 42px;
|
|
138
|
+
font-size: 16px;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
`;
|
|
142
|
+
(ownerDocument.head || ownerDocument.documentElement).appendChild(style);
|
|
143
|
+
}
|
|
144
|
+
|
|
47
145
|
function localDefault(def) {
|
|
48
146
|
if (!def || typeof def !== 'object') {
|
|
49
147
|
return 0;
|
|
@@ -54,6 +152,36 @@ function localDefault(def) {
|
|
|
54
152
|
return finiteNumber(def.default, 0);
|
|
55
153
|
}
|
|
56
154
|
|
|
155
|
+
function inferredKeyboardDecayTargets(keyboardBindings) {
|
|
156
|
+
const targets = new Set();
|
|
157
|
+
for (const binding of Object.values(keyboardBindings || {})) {
|
|
158
|
+
if (trimMaybeString(binding?.action).toLowerCase() !== 'set') {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const target = trimMaybeString(binding.target);
|
|
162
|
+
if (target) {
|
|
163
|
+
targets.add(target);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return Array.from(targets).sort((a, b) => a.localeCompare(b));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createKeyboardDecaySpec(decay, keyboardBindings) {
|
|
170
|
+
const raw = decay && typeof decay === 'object' ? decay : null;
|
|
171
|
+
const hasExplicitTargets = raw && Object.prototype.hasOwnProperty.call(raw, 'targets');
|
|
172
|
+
const targets = hasExplicitTargets
|
|
173
|
+
? (Array.isArray(raw.targets) ? raw.targets.map(trimMaybeString).filter(Boolean) : [])
|
|
174
|
+
: inferredKeyboardDecayTargets(keyboardBindings);
|
|
175
|
+
if (targets.length === 0) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
factor: finiteNumber(raw?.factor, 0.85),
|
|
180
|
+
ref_dt: raw?.ref_dt ?? raw?.refDt ?? 0.016,
|
|
181
|
+
targets,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
57
185
|
function sourceValue(source, locals, stepper, runtime) {
|
|
58
186
|
const text = trimMaybeString(source);
|
|
59
187
|
if (!text) {
|
|
@@ -137,7 +265,7 @@ function normalizedKeyboardKey(eventOrKey) {
|
|
|
137
265
|
return typeof key === 'string' && key.length === 1 ? key.toLowerCase() : trimMaybeString(key);
|
|
138
266
|
}
|
|
139
267
|
|
|
140
|
-
function createInputRuntime(config) {
|
|
268
|
+
export function createInputRuntime(config) {
|
|
141
269
|
const locals = new Map();
|
|
142
270
|
const keyboardBindings = {};
|
|
143
271
|
for (const [key, binding] of sortedEntries(config?.input?.keyboard?.keys)) {
|
|
@@ -152,8 +280,10 @@ function createInputRuntime(config) {
|
|
|
152
280
|
const pressedKeys = new Set();
|
|
153
281
|
const signals = new Set();
|
|
154
282
|
const debounceUntil = new Map();
|
|
283
|
+
const pressedButtons = new Set();
|
|
155
284
|
let connectedGamepad = null;
|
|
156
285
|
let lastMode = 'keyboard';
|
|
286
|
+
const keyboardDecay = createKeyboardDecaySpec(config?.input?.keyboard?.decay, keyboardBindings);
|
|
157
287
|
|
|
158
288
|
function resetLocals() {
|
|
159
289
|
locals.clear();
|
|
@@ -197,14 +327,26 @@ function createInputRuntime(config) {
|
|
|
197
327
|
}
|
|
198
328
|
}
|
|
199
329
|
|
|
330
|
+
function applyHeldKeyboardAction(id, binding, nowMs) {
|
|
331
|
+
if (trimMaybeString(binding?.action).toLowerCase() === 'set') {
|
|
332
|
+
applyAction(id, binding, true, nowMs);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
200
336
|
function keyDown(event) {
|
|
201
337
|
const key = normalizedKeyboardKey(event);
|
|
202
338
|
const binding = keyboardBindings[key];
|
|
203
339
|
if (!binding) {
|
|
204
340
|
return false;
|
|
205
341
|
}
|
|
342
|
+
const wasPressed = pressedKeys.has(key);
|
|
206
343
|
pressedKeys.add(key);
|
|
207
|
-
|
|
344
|
+
const id = `key:${key}`;
|
|
345
|
+
if (wasPressed) {
|
|
346
|
+
applyHeldKeyboardAction(id, binding, performance.now());
|
|
347
|
+
} else {
|
|
348
|
+
applyAction(id, binding, true, performance.now());
|
|
349
|
+
}
|
|
208
350
|
event.preventDefault();
|
|
209
351
|
return true;
|
|
210
352
|
}
|
|
@@ -239,9 +381,10 @@ function createInputRuntime(config) {
|
|
|
239
381
|
lastMode = 'keyboard';
|
|
240
382
|
}
|
|
241
383
|
const nowMs = performance.now();
|
|
384
|
+
applyKeyboardDecay(keyboardDecay, dt);
|
|
242
385
|
for (const [key, binding] of sortedEntries(keyboardBindings)) {
|
|
243
386
|
if (pressedKeys.has(key)) {
|
|
244
|
-
|
|
387
|
+
applyHeldKeyboardAction(`key:${key}`, binding, nowMs);
|
|
245
388
|
}
|
|
246
389
|
}
|
|
247
390
|
applyIntegrators(input.keyboard?.integrators, dt, null);
|
|
@@ -252,6 +395,31 @@ function createInputRuntime(config) {
|
|
|
252
395
|
}
|
|
253
396
|
}
|
|
254
397
|
|
|
398
|
+
function applyKeyboardDecay(decay, dt) {
|
|
399
|
+
if (!decay || typeof decay !== 'object') {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const targets = Array.isArray(decay.targets)
|
|
403
|
+
? decay.targets.map(trimMaybeString).filter(Boolean)
|
|
404
|
+
: [];
|
|
405
|
+
if (targets.length === 0) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const elapsed = finiteNumber(dt, 0);
|
|
409
|
+
if (elapsed <= 0) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const factor = clamp(finiteNumber(decay.factor, 1), [0, 1]);
|
|
413
|
+
const refDt = Math.max(finiteNumber(decay.ref_dt ?? decay.refDt, 0.016), Number.EPSILON);
|
|
414
|
+
const scale = Math.pow(factor, elapsed / refDt);
|
|
415
|
+
for (const target of targets) {
|
|
416
|
+
const current = locals.get(target);
|
|
417
|
+
if (typeof current === 'number' && Number.isFinite(current)) {
|
|
418
|
+
locals.set(target, current * scale);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
255
423
|
function applyIntegrators(integrators, dt, gamepad) {
|
|
256
424
|
for (const [name, spec] of sortedEntries(integrators)) {
|
|
257
425
|
const raw = gamepad ? sourceGamepadAxis(spec.source || name, gamepad) : sourceValue(spec.source, locals, { get: () => 0, time: () => 0 }, {});
|
|
@@ -274,7 +442,19 @@ function createInputRuntime(config) {
|
|
|
274
442
|
|
|
275
443
|
function applyGamepadButtons(buttons, gamepad, nowMs) {
|
|
276
444
|
for (const [name, spec] of sortedEntries(buttons)) {
|
|
277
|
-
|
|
445
|
+
const id = `button:${name}`;
|
|
446
|
+
const active = sourceGamepadButton(spec.source || name, gamepad);
|
|
447
|
+
const action = trimMaybeString(spec?.action).toLowerCase();
|
|
448
|
+
if (action === 'set') {
|
|
449
|
+
applyAction(id, spec, active, nowMs);
|
|
450
|
+
} else if (active && !pressedButtons.has(id)) {
|
|
451
|
+
applyAction(id, spec, true, nowMs);
|
|
452
|
+
}
|
|
453
|
+
if (active) {
|
|
454
|
+
pressedButtons.add(id);
|
|
455
|
+
} else {
|
|
456
|
+
pressedButtons.delete(id);
|
|
457
|
+
}
|
|
278
458
|
}
|
|
279
459
|
}
|
|
280
460
|
|
|
@@ -294,6 +474,7 @@ function createInputRuntime(config) {
|
|
|
294
474
|
},
|
|
295
475
|
releaseKeys() {
|
|
296
476
|
pressedKeys.clear();
|
|
477
|
+
pressedButtons.clear();
|
|
297
478
|
},
|
|
298
479
|
resetLocals,
|
|
299
480
|
takeSignal,
|
|
@@ -329,7 +510,7 @@ function createViewerRuntime({ THREE, container, viewerSignals, assetBaseUrl, po
|
|
|
329
510
|
const cam = {
|
|
330
511
|
target: new THREE.Vector3(0, 0, 0),
|
|
331
512
|
dist: 7,
|
|
332
|
-
angle: Math.PI,
|
|
513
|
+
angle: -Math.PI / 2,
|
|
333
514
|
elev: 0.22,
|
|
334
515
|
};
|
|
335
516
|
const viewerConfig = config?.viewer || {};
|
|
@@ -742,12 +923,16 @@ export async function createInteractiveSimulation(options) {
|
|
|
742
923
|
onStatus('ready');
|
|
743
924
|
|
|
744
925
|
const simDt = Math.max(0.001, finiteNumber(config?.sim?.dt, 0.01));
|
|
926
|
+
let pacingMode = normalizePacingMode(config?.sim?.mode);
|
|
745
927
|
let frameNum = 0;
|
|
746
928
|
let running = false;
|
|
747
929
|
let raf = null;
|
|
748
930
|
let lastTime = 0;
|
|
749
931
|
let accumulator = 0;
|
|
932
|
+
let speedRatio = 0;
|
|
750
933
|
let updateCaptureUi = () => {};
|
|
934
|
+
let updatePacingUi = () => {};
|
|
935
|
+
let updateFullscreenUi = () => {};
|
|
751
936
|
|
|
752
937
|
function refreshViewerSignals() {
|
|
753
938
|
viewerSignals.clear();
|
|
@@ -777,15 +962,64 @@ export async function createInteractiveSimulation(options) {
|
|
|
777
962
|
onError(error);
|
|
778
963
|
}
|
|
779
964
|
|
|
965
|
+
function statusLine() {
|
|
966
|
+
const inputMode = input.runtimeFields(frameNum, stepper.time()).input_mode;
|
|
967
|
+
return `live t=${stepper.time().toFixed(2)} s · ${pacingModeLabel(pacingMode)} · ${speedRatioLabel(speedRatio)} · ${inputMode}`;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function recordSpeed(simAdvanced, wallDt) {
|
|
971
|
+
if (wallDt <= 0) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const instant = Math.max(0, simAdvanced) / wallDt;
|
|
975
|
+
speedRatio = speedRatio === 0 ? instant : (speedRatio * 0.82 + instant * 0.18);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function resetSimulation(options = {}) {
|
|
979
|
+
const {
|
|
980
|
+
resetLocals = true,
|
|
981
|
+
resetStepper = true,
|
|
982
|
+
render = true,
|
|
983
|
+
statusText = 'reset',
|
|
984
|
+
} = options;
|
|
985
|
+
if (resetLocals) {
|
|
986
|
+
input.resetLocals();
|
|
987
|
+
}
|
|
988
|
+
input.releaseKeys();
|
|
989
|
+
if (resetStepper) {
|
|
990
|
+
stepper.reset();
|
|
991
|
+
}
|
|
992
|
+
frameNum = 0;
|
|
993
|
+
accumulator = 0;
|
|
994
|
+
lastTime = 0;
|
|
995
|
+
speedRatio = 0;
|
|
996
|
+
refreshViewerSignals();
|
|
997
|
+
if (render) {
|
|
998
|
+
viewer.render();
|
|
999
|
+
}
|
|
1000
|
+
updatePacingUi();
|
|
1001
|
+
onStatus(statusText);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function togglePacingMode() {
|
|
1005
|
+
pacingMode = pacingMode === 'realtime' ? 'as_fast_as_possible' : 'realtime';
|
|
1006
|
+
accumulator = 0;
|
|
1007
|
+
lastTime = 0;
|
|
1008
|
+
speedRatio = 0;
|
|
1009
|
+
updatePacingUi();
|
|
1010
|
+
onStatus(statusLine());
|
|
1011
|
+
return pacingMode;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
780
1014
|
function routeFrame(dt) {
|
|
781
1015
|
input.update(dt);
|
|
782
1016
|
if (config?.reset?.on_signal && input.takeSignal(config.reset.on_signal)) {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}
|
|
1017
|
+
resetSimulation({
|
|
1018
|
+
resetLocals: Boolean(config.reset.reset_locals),
|
|
1019
|
+
resetStepper: Boolean(config.reset.rebuild_stepper),
|
|
1020
|
+
render: false,
|
|
1021
|
+
statusText: 'reset',
|
|
1022
|
+
});
|
|
789
1023
|
}
|
|
790
1024
|
if (input.takeSignal(trimMaybeString(config?.quit?.on_signal) || 'quit')) {
|
|
791
1025
|
stopAnimation();
|
|
@@ -817,16 +1051,31 @@ export async function createInteractiveSimulation(options) {
|
|
|
817
1051
|
if (lastTime === 0) {
|
|
818
1052
|
lastTime = now;
|
|
819
1053
|
}
|
|
820
|
-
|
|
1054
|
+
const wallDt = Math.min(0.08, Math.max(0, (now - lastTime) / 1000));
|
|
821
1055
|
lastTime = now;
|
|
1056
|
+
let simAdvanced = 0;
|
|
822
1057
|
let steps = 0;
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1058
|
+
if (pacingMode === 'as_fast_as_possible') {
|
|
1059
|
+
const started = performance.now();
|
|
1060
|
+
do {
|
|
1061
|
+
if (!routeFrame(simDt)) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
simAdvanced += simDt;
|
|
1065
|
+
steps += 1;
|
|
1066
|
+
} while (steps < 250 && performance.now() - started < 12);
|
|
1067
|
+
} else {
|
|
1068
|
+
accumulator += wallDt;
|
|
1069
|
+
while (accumulator >= simDt && steps < 8) {
|
|
1070
|
+
if (!routeFrame(simDt)) {
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
accumulator -= simDt;
|
|
1074
|
+
simAdvanced += simDt;
|
|
1075
|
+
steps += 1;
|
|
826
1076
|
}
|
|
827
|
-
accumulator -= simDt;
|
|
828
|
-
steps += 1;
|
|
829
1077
|
}
|
|
1078
|
+
recordSpeed(simAdvanced, wallDt);
|
|
830
1079
|
if (typeof scene.onFrame === 'function') {
|
|
831
1080
|
scene.onFrame(viewer.api);
|
|
832
1081
|
}
|
|
@@ -834,7 +1083,8 @@ export async function createInteractiveSimulation(options) {
|
|
|
834
1083
|
pointer.dy = 0;
|
|
835
1084
|
pointer.wheel = 0;
|
|
836
1085
|
viewer.render();
|
|
837
|
-
|
|
1086
|
+
updatePacingUi();
|
|
1087
|
+
onStatus(statusLine());
|
|
838
1088
|
raf = ownerWindow.requestAnimationFrame(tick);
|
|
839
1089
|
}
|
|
840
1090
|
|
|
@@ -844,7 +1094,15 @@ export async function createInteractiveSimulation(options) {
|
|
|
844
1094
|
const eventCaptureOptions = { capture: true, passive: false };
|
|
845
1095
|
const ownerDocument = viewer.ownerDocument;
|
|
846
1096
|
const ownerWindow = viewer.ownerWindow;
|
|
847
|
-
|
|
1097
|
+
container.classList.add('rumoca-interactive-root');
|
|
1098
|
+
ensureInteractiveRuntimeStyles(ownerDocument);
|
|
1099
|
+
const keyDown = (event) => {
|
|
1100
|
+
if (!event.repeat && handleViewerKeyDown(event)) {
|
|
1101
|
+
event.preventDefault();
|
|
1102
|
+
return true;
|
|
1103
|
+
}
|
|
1104
|
+
return input.keyDown(event);
|
|
1105
|
+
};
|
|
848
1106
|
const keyUp = (event) => input.keyUp(event);
|
|
849
1107
|
const updatePointerFromEvent = (event) => {
|
|
850
1108
|
const rect = viewer.canvas.getBoundingClientRect();
|
|
@@ -875,6 +1133,37 @@ export async function createInteractiveSimulation(options) {
|
|
|
875
1133
|
pointerLockExitReleasesCapture = true;
|
|
876
1134
|
});
|
|
877
1135
|
};
|
|
1136
|
+
const fullscreenElement = () => ownerDocument.fullscreenElement || ownerDocument.webkitFullscreenElement || null;
|
|
1137
|
+
const isFullscreenActive = () => {
|
|
1138
|
+
const activeElement = fullscreenElement();
|
|
1139
|
+
return activeElement === container || container.contains(activeElement);
|
|
1140
|
+
};
|
|
1141
|
+
const setFullscreenActive = async (active) => {
|
|
1142
|
+
try {
|
|
1143
|
+
if (active) {
|
|
1144
|
+
if (!isFullscreenActive()) {
|
|
1145
|
+
const request = container.requestFullscreen || container.webkitRequestFullscreen;
|
|
1146
|
+
if (typeof request === 'function') {
|
|
1147
|
+
await Promise.resolve(request.call(container));
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
} else if (isFullscreenActive()) {
|
|
1151
|
+
const exit = ownerDocument.exitFullscreen || ownerDocument.webkitExitFullscreen;
|
|
1152
|
+
if (typeof exit === 'function') {
|
|
1153
|
+
await Promise.resolve(exit.call(ownerDocument));
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
} finally {
|
|
1157
|
+
updateFullscreenUi();
|
|
1158
|
+
focus();
|
|
1159
|
+
viewer.render();
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
const toggleFullscreen = () => {
|
|
1163
|
+
setFullscreenActive(!isFullscreenActive()).catch((error) => {
|
|
1164
|
+
onStatus(`fullscreen unavailable: ${error?.message || error}`);
|
|
1165
|
+
});
|
|
1166
|
+
};
|
|
878
1167
|
const setCaptureActive = (active, options = {}) => {
|
|
879
1168
|
inputCaptureActive = Boolean(active);
|
|
880
1169
|
if (!inputCaptureActive) {
|
|
@@ -895,12 +1184,23 @@ export async function createInteractiveSimulation(options) {
|
|
|
895
1184
|
const key = normalizedKeyboardKey(event);
|
|
896
1185
|
if (key === 'c') {
|
|
897
1186
|
lastCapturedKey = `camera ${viewer.cycleCamera()}`;
|
|
898
|
-
updateCaptureUi(
|
|
1187
|
+
updateCaptureUi(inputCaptureActive);
|
|
899
1188
|
return true;
|
|
900
1189
|
}
|
|
901
1190
|
if (key === 'h') {
|
|
902
1191
|
lastCapturedKey = `hud ${viewer.toggleHud() ? 'on' : 'off'}`;
|
|
903
|
-
updateCaptureUi(
|
|
1192
|
+
updateCaptureUi(inputCaptureActive);
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
if (key === 't') {
|
|
1196
|
+
lastCapturedKey = `time ${pacingModeLabel(togglePacingMode())}`;
|
|
1197
|
+
updateCaptureUi(inputCaptureActive);
|
|
1198
|
+
return true;
|
|
1199
|
+
}
|
|
1200
|
+
if (key === 'f') {
|
|
1201
|
+
lastCapturedKey = 'fullscreen';
|
|
1202
|
+
toggleFullscreen();
|
|
1203
|
+
updateCaptureUi(inputCaptureActive);
|
|
904
1204
|
return true;
|
|
905
1205
|
}
|
|
906
1206
|
return false;
|
|
@@ -998,6 +1298,16 @@ export async function createInteractiveSimulation(options) {
|
|
|
998
1298
|
captureToggle.type = 'button';
|
|
999
1299
|
captureToggle.className = 'rumoca-interactive-capture-toggle';
|
|
1000
1300
|
captureToggle.title = 'Capture keyboard and mouse input. Press Escape to release capture.';
|
|
1301
|
+
const pacingToggle = ownerDocument.createElement('button');
|
|
1302
|
+
pacingToggle.type = 'button';
|
|
1303
|
+
pacingToggle.className = 'rumoca-interactive-pacing-toggle';
|
|
1304
|
+
pacingToggle.title = 'Toggle simulation pacing. Shortcut: T.';
|
|
1305
|
+
const speedReadout = ownerDocument.createElement('span');
|
|
1306
|
+
speedReadout.className = 'rumoca-interactive-key-echo rumoca-interactive-speed-readout';
|
|
1307
|
+
const fullscreenToggle = ownerDocument.createElement('button');
|
|
1308
|
+
fullscreenToggle.type = 'button';
|
|
1309
|
+
fullscreenToggle.className = 'rumoca-interactive-fullscreen-toggle';
|
|
1310
|
+
fullscreenToggle.title = 'Toggle fullscreen. Shortcut: F.';
|
|
1001
1311
|
updateCaptureUi = (active) => {
|
|
1002
1312
|
captureToggle.textContent = active
|
|
1003
1313
|
? `Release: Esc${lastCapturedKey ? ` · ${keyDisplayName(lastCapturedKey)}` : ''}${pointer.captured ? ' · mouse' : ''}`
|
|
@@ -1005,14 +1315,43 @@ export async function createInteractiveSimulation(options) {
|
|
|
1005
1315
|
captureToggle.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
1006
1316
|
controls.classList.toggle('is-capturing', active);
|
|
1007
1317
|
};
|
|
1318
|
+
updatePacingUi = () => {
|
|
1319
|
+
const label = pacingModeLabel(pacingMode);
|
|
1320
|
+
pacingToggle.textContent = label === 'fast' ? 'Fast' : 'Realtime';
|
|
1321
|
+
pacingToggle.setAttribute('aria-pressed', pacingMode === 'as_fast_as_possible' ? 'true' : 'false');
|
|
1322
|
+
pacingToggle.classList.toggle('is-fast', pacingMode === 'as_fast_as_possible');
|
|
1323
|
+
speedReadout.textContent = `Speed ${speedRatioLabel(speedRatio)}`;
|
|
1324
|
+
};
|
|
1325
|
+
updateFullscreenUi = () => {
|
|
1326
|
+
const active = isFullscreenActive();
|
|
1327
|
+
fullscreenToggle.textContent = active ? 'Exit Fullscreen' : 'Fullscreen';
|
|
1328
|
+
fullscreenToggle.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
1329
|
+
fullscreenToggle.classList.toggle('is-fullscreen', active);
|
|
1330
|
+
};
|
|
1008
1331
|
captureToggle.addEventListener('pointerdown', (event) => {
|
|
1009
1332
|
event.preventDefault();
|
|
1010
1333
|
event.stopPropagation();
|
|
1011
1334
|
setCaptureActive(!inputCaptureActive, { requestPointerLock: true });
|
|
1012
1335
|
container.focus({ preventScroll: true });
|
|
1013
1336
|
});
|
|
1337
|
+
pacingToggle.addEventListener('pointerdown', (event) => {
|
|
1338
|
+
event.preventDefault();
|
|
1339
|
+
event.stopPropagation();
|
|
1340
|
+
togglePacingMode();
|
|
1341
|
+
container.focus({ preventScroll: true });
|
|
1342
|
+
});
|
|
1343
|
+
fullscreenToggle.addEventListener('pointerdown', (event) => {
|
|
1344
|
+
event.preventDefault();
|
|
1345
|
+
event.stopPropagation();
|
|
1346
|
+
toggleFullscreen();
|
|
1347
|
+
});
|
|
1014
1348
|
controls.appendChild(captureToggle);
|
|
1349
|
+
controls.appendChild(pacingToggle);
|
|
1350
|
+
controls.appendChild(speedReadout);
|
|
1351
|
+
controls.appendChild(fullscreenToggle);
|
|
1015
1352
|
updateCaptureUi(false);
|
|
1353
|
+
updatePacingUi();
|
|
1354
|
+
updateFullscreenUi();
|
|
1016
1355
|
container.appendChild(controls);
|
|
1017
1356
|
container.tabIndex = 0;
|
|
1018
1357
|
viewer.canvas.tabIndex = -1;
|
|
@@ -1032,6 +1371,8 @@ export async function createInteractiveSimulation(options) {
|
|
|
1032
1371
|
ownerDocument.addEventListener('pointercancel', capturePointerEvent, true);
|
|
1033
1372
|
ownerDocument.addEventListener('wheel', captureWheel, eventCaptureOptions);
|
|
1034
1373
|
ownerDocument.addEventListener('pointerlockchange', handlePointerLockChange);
|
|
1374
|
+
ownerDocument.addEventListener('fullscreenchange', updateFullscreenUi);
|
|
1375
|
+
ownerDocument.addEventListener('webkitfullscreenchange', updateFullscreenUi);
|
|
1035
1376
|
ownerWindow.addEventListener('blur', releaseCapture);
|
|
1036
1377
|
container.focus({ preventScroll: true });
|
|
1037
1378
|
const resize = () => viewer.render();
|
|
@@ -1079,20 +1420,14 @@ export async function createInteractiveSimulation(options) {
|
|
|
1079
1420
|
ownerDocument.removeEventListener('pointercancel', capturePointerEvent, true);
|
|
1080
1421
|
ownerDocument.removeEventListener('wheel', captureWheel, eventCaptureOptions);
|
|
1081
1422
|
ownerDocument.removeEventListener('pointerlockchange', handlePointerLockChange);
|
|
1423
|
+
ownerDocument.removeEventListener('fullscreenchange', updateFullscreenUi);
|
|
1424
|
+
ownerDocument.removeEventListener('webkitfullscreenchange', updateFullscreenUi);
|
|
1082
1425
|
ownerWindow.removeEventListener('blur', releaseCapture);
|
|
1083
1426
|
ownerWindow.removeEventListener('resize', resize);
|
|
1084
1427
|
releasePointerCapture();
|
|
1085
1428
|
},
|
|
1086
1429
|
reset() {
|
|
1087
|
-
|
|
1088
|
-
input.releaseKeys();
|
|
1089
|
-
stepper.reset();
|
|
1090
|
-
frameNum = 0;
|
|
1091
|
-
accumulator = 0;
|
|
1092
|
-
lastTime = 0;
|
|
1093
|
-
refreshViewerSignals();
|
|
1094
|
-
viewer.render();
|
|
1095
|
-
onStatus('reset');
|
|
1430
|
+
resetSimulation({ resetLocals: true, resetStepper: true });
|
|
1096
1431
|
},
|
|
1097
1432
|
};
|
|
1098
1433
|
}
|
package/rumoca_package_meta.json
CHANGED