@@ -18,12 +18,13 @@ const DEFAULT_WORLD_STAGES = Object.freeze([
1818 "vector-overlay"
1919] ) ;
2020
21- const DEFAULT_COMMAND_BINDINGS = Object . freeze ( [
22- { key : "F6" , command : "help" , label : "help" } ,
23- { key : "F7" , command : "status" , label : "status" } ,
24- { key : "F8" , command : "scene.info" , label : "scene.info" } ,
25- { key : "F9" , command : "unknown.command" , label : "invalid" }
26- ] ) ;
21+ const DEFAULT_COMBO_BINDINGS = Object . freeze ( {
22+ toggleConsole : Object . freeze ( { key : "Backquote" , shift : true , ctrl : false , alt : false , meta : false } ) ,
23+ toggleOverlay : Object . freeze ( { key : "Backquote" , shift : true , ctrl : true , alt : false , meta : false } ) ,
24+ reload : Object . freeze ( { key : "KeyR" , shift : true , ctrl : true , alt : false , meta : false } ) ,
25+ nextPanel : Object . freeze ( { key : "BracketRight" , shift : true , ctrl : true , alt : false , meta : false } ) ,
26+ previousPanel : Object . freeze ( { key : "BracketLeft" , shift : true , ctrl : true , alt : false , meta : false } )
27+ } ) ;
2728
2829function sanitizeText ( value ) {
2930 return typeof value === "string" ? value . trim ( ) : "" ;
@@ -33,6 +34,13 @@ function isObject(value) {
3334 return value !== null && typeof value === "object" && ! Array . isArray ( value ) ;
3435}
3536
37+ function cloneJson ( value ) {
38+ if ( typeof structuredClone === "function" ) {
39+ return structuredClone ( value ) ;
40+ }
41+ return JSON . parse ( JSON . stringify ( value ) ) ;
42+ }
43+
3644function toContextSection ( context , field ) {
3745 return isObject ( context ?. [ field ] ) ? context [ field ] : { } ;
3846}
@@ -60,6 +68,51 @@ function getInputEdgePress(input, keyCode, edgeState) {
6068 return isDown && ! wasDown ;
6169}
6270
71+ function isModifierDown ( input , keyCode ) {
72+ if ( typeof input ?. isDown !== "function" ) {
73+ return false ;
74+ }
75+ return input . isDown ( keyCode ) === true ;
76+ }
77+
78+ function getComboEdgePress ( input , combo , edgeState ) {
79+ const normalizedCombo = isObject ( combo ) ? combo : { } ;
80+ const key = sanitizeText ( normalizedCombo . key ) ;
81+ if ( ! key ) {
82+ return false ;
83+ }
84+
85+ const keyEdge = getInputEdgePress ( input , key , edgeState ) ;
86+ if ( ! keyEdge ) {
87+ return false ;
88+ }
89+
90+ const requiresShift = normalizedCombo . shift === true ;
91+ const requiresCtrl = normalizedCombo . ctrl === true ;
92+ const requiresAlt = normalizedCombo . alt === true ;
93+ const requiresMeta = normalizedCombo . meta === true ;
94+
95+ const shiftDown = isModifierDown ( input , "ShiftLeft" ) || isModifierDown ( input , "ShiftRight" ) ;
96+ const ctrlDown = isModifierDown ( input , "ControlLeft" ) || isModifierDown ( input , "ControlRight" ) ;
97+ const altDown = isModifierDown ( input , "AltLeft" ) || isModifierDown ( input , "AltRight" ) ;
98+ const metaDown = isModifierDown ( input , "MetaLeft" ) || isModifierDown ( input , "MetaRight" ) ;
99+
100+ if ( requiresShift !== shiftDown ) {
101+ return false ;
102+ }
103+ if ( requiresCtrl !== ctrlDown ) {
104+ return false ;
105+ }
106+ if ( requiresAlt !== altDown ) {
107+ return false ;
108+ }
109+ if ( requiresMeta !== metaDown ) {
110+ return false ;
111+ }
112+
113+ return true ;
114+ }
115+
63116function buildCommandContext ( diagnostics ) {
64117 return {
65118 runtime : toContextSection ( diagnostics , "runtime" ) ,
@@ -131,17 +184,16 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
131184 const runtime = buildRuntimeFromOptions ( options ) ;
132185 const keyEdgeState = new Map ( ) ;
133186
134- const toggleConsoleKey = sanitizeText ( options ?. toggleConsoleKey ) || "Backquote" ;
135- const toggleOverlayKey = sanitizeText ( options ?. toggleOverlayKey ) || "F3" ;
136- const commandBindings = Array . isArray ( options ?. commandBindings )
137- ? options . commandBindings
138- : DEFAULT_COMMAND_BINDINGS ;
187+ const comboBindings = isObject ( options ?. comboBindings )
188+ ? options . comboBindings
189+ : DEFAULT_COMBO_BINDINGS ;
139190
140191 let diagnosticsSnapshot = null ;
141192 let diagnosticsReports = [ ] ;
142193 let overlayRender = { status : "hidden" , sections : [ ] , reports : [ ] } ;
143194 let lastCommandExecution = null ;
144195 let lastCommandBinding = "" ;
196+ let panelCursorIndex = - 1 ;
145197
146198 function executeCommand ( commandText , context = null ) {
147199 const command = sanitizeText ( commandText ) ;
@@ -159,47 +211,84 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
159211 return execution ;
160212 }
161213
214+ function cyclePanel ( direction ) {
215+ if ( ! runtime ?. panelRegistry || typeof runtime . panelRegistry . getOrderedPanels !== "function" ) {
216+ return null ;
217+ }
218+
219+ const orderedPanels = runtime . panelRegistry . getOrderedPanels ( true ) ;
220+ if ( ! Array . isArray ( orderedPanels ) || orderedPanels . length === 0 ) {
221+ return null ;
222+ }
223+
224+ if ( panelCursorIndex < 0 || panelCursorIndex >= orderedPanels . length ) {
225+ const currentlyEnabledIndex = orderedPanels . findIndex ( ( panel ) => panel . enabled === true ) ;
226+ panelCursorIndex = currentlyEnabledIndex >= 0 ? currentlyEnabledIndex : 0 ;
227+ } else {
228+ panelCursorIndex = ( panelCursorIndex + direction + orderedPanels . length ) % orderedPanels . length ;
229+ }
230+
231+ const targetPanel = orderedPanels [ panelCursorIndex ] ;
232+ orderedPanels . forEach ( ( panel ) => {
233+ runtime . panelRegistry . setPanelEnabled ( panel . id , panel . id === targetPanel . id ) ;
234+ } ) ;
235+ runtime . showOverlay ( ) ;
236+
237+ const label = direction > 0 ? "panel.next" : "panel.previous" ;
238+ lastCommandBinding = `${ label } -> ${ targetPanel . id } ` ;
239+ lastCommandExecution = {
240+ status : "ready" ,
241+ output : {
242+ lines : [ `panel=${ targetPanel . id } ` , `title=${ targetPanel . title } ` ]
243+ } ,
244+ reports : [ ]
245+ } ;
246+ return targetPanel ;
247+ }
248+
162249 function update ( frame = { } ) {
163250 const engine = frame . engine || { } ;
164251 const input = engine . input || null ;
165252
166- if ( getInputEdgePress ( input , toggleConsoleKey , keyEdgeState ) ) {
253+ const diagnosticsContext = isObject ( frame . diagnosticsContext ) ? frame . diagnosticsContext : { } ;
254+ const diagnosticsResult = runtime . collectDiagnostics ( diagnosticsContext ) ;
255+ diagnosticsSnapshot = diagnosticsResult ?. diagnostics || null ;
256+ diagnosticsReports = Array . isArray ( diagnosticsResult ?. reports ) ? diagnosticsResult . reports . slice ( ) : [ ] ;
257+
258+ if ( getComboEdgePress ( input , comboBindings . toggleOverlay , keyEdgeState ) ) {
167259 const state = runtime . getState ( ) ;
168- if ( state . consoleVisible ) {
169- runtime . hideConsole ( ) ;
260+ if ( state . overlayVisible ) {
261+ runtime . hideOverlay ( ) ;
170262 } else {
171- runtime . showConsole ( ) ;
263+ runtime . showOverlay ( ) ;
172264 }
173265 }
174266
175- if ( getInputEdgePress ( input , toggleOverlayKey , keyEdgeState ) ) {
267+ if ( getComboEdgePress ( input , comboBindings . toggleConsole , keyEdgeState ) ) {
176268 const state = runtime . getState ( ) ;
177- if ( state . overlayVisible ) {
178- runtime . hideOverlay ( ) ;
269+ if ( state . consoleVisible ) {
270+ runtime . hideConsole ( ) ;
179271 } else {
180- runtime . showOverlay ( ) ;
272+ runtime . showConsole ( ) ;
181273 }
182274 }
183275
184- const diagnosticsContext = isObject ( frame . diagnosticsContext ) ? frame . diagnosticsContext : { } ;
185- const diagnosticsResult = runtime . collectDiagnostics ( diagnosticsContext ) ;
186- diagnosticsSnapshot = diagnosticsResult ?. diagnostics || null ;
187- diagnosticsReports = Array . isArray ( diagnosticsResult ?. reports ) ? diagnosticsResult . reports . slice ( ) : [ ] ;
276+ if ( getComboEdgePress ( input , comboBindings . reload , keyEdgeState ) ) {
277+ const runtimeContext = buildCommandContext ( diagnosticsSnapshot || { } ) ;
278+ executeCommand ( "scene.reload" , runtimeContext ) ;
279+ runtime . applyHotReload ( {
280+ status : "ready" ,
281+ mode : "targeted" ,
282+ runtimeState : toContextSection ( diagnosticsSnapshot , "runtime" )
283+ } ) ;
284+ }
188285
189- const runtimeState = runtime . getState ( ) ;
190- if ( runtimeState . consoleVisible ) {
191- for ( let index = 0 ; index < commandBindings . length ; index += 1 ) {
192- const binding = commandBindings [ index ] ;
193- const key = sanitizeText ( binding ?. key ) ;
194- const command = sanitizeText ( binding ?. command ) ;
195- if ( ! key || ! command ) {
196- continue ;
197- }
198- if ( ! getInputEdgePress ( input , key , keyEdgeState ) ) {
199- continue ;
200- }
201- executeCommand ( command ) ;
202- }
286+ if ( getComboEdgePress ( input , comboBindings . nextPanel , keyEdgeState ) ) {
287+ cyclePanel ( 1 ) ;
288+ }
289+
290+ if ( getComboEdgePress ( input , comboBindings . previousPanel , keyEdgeState ) ) {
291+ cyclePanel ( - 1 ) ;
203292 }
204293
205294 return {
@@ -232,7 +321,7 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
232321 if ( overlayRender . status === "ready" ) {
233322 const overlayLines = flattenOverlaySections ( overlayRender . sections , 9 ) ;
234323 overlayLines . push ( `order: ${ renderOrder . slice ( - 2 ) . join ( " -> " ) } ` ) ;
235- drawPanel ( renderer , overlayX , overlayY , overlayWidth , overlayHeight , "Debug Overlay (F3 )" , overlayLines . slice ( 0 , 10 ) ) ;
324+ drawPanel ( renderer , overlayX , overlayY , overlayWidth , overlayHeight , "Debug Overlay (Ctrl+Shift+` )" , overlayLines . slice ( 0 , 10 ) ) ;
236325 }
237326
238327 const state = runtime . getState ( ) ;
@@ -246,10 +335,13 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
246335 ? lastCommandExecution . output . lines . map ( ( line ) => sanitizeText ( line ) ) . filter ( Boolean )
247336 : [ "No command output yet." ] ;
248337
249- const commandHint = commandBindings
250- . map ( ( binding ) => `${ sanitizeText ( binding ?. key ) } =${ sanitizeText ( binding ?. label ) || sanitizeText ( binding ?. command ) } ` )
251- . filter ( Boolean )
252- . join ( " | " ) ;
338+ const commandHint = [
339+ "Shift+`=console" ,
340+ "Ctrl+Shift+`=overlay" ,
341+ "Ctrl+Shift+R=reload" ,
342+ "Ctrl+Shift+]=next panel" ,
343+ "Ctrl+Shift+[=prev panel"
344+ ] . join ( " | " ) ;
253345
254346 const lines = [
255347 `last: ${ lastCommandBinding || "none" } ` ,
@@ -259,7 +351,7 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
259351 ...outputLines . slice ( 0 , 5 )
260352 ] ;
261353
262- drawPanel ( renderer , consoleX , consoleY , consoleWidth , consoleHeight , "Dev Console (`)" , lines . slice ( 0 , 9 ) ) ;
354+ drawPanel ( renderer , consoleX , consoleY , consoleWidth , consoleHeight , "Dev Console (Shift+ `)" , lines . slice ( 0 , 9 ) ) ;
263355 }
264356
265357 return {
@@ -287,10 +379,7 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
287379 overlayRender,
288380 lastCommandExecution,
289381 lastCommandBinding,
290- toggleKeys : {
291- console : toggleConsoleKey ,
292- overlay : toggleOverlayKey
293- }
382+ toggleCombos : cloneJson ( comboBindings )
294383 } ;
295384 }
296385 } ;
0 commit comments