diff --git a/src/components/hud/FpsCounter.svelte b/src/components/hud/FpsCounter.svelte index c9c74b72..4c1a9eb3 100644 --- a/src/components/hud/FpsCounter.svelte +++ b/src/components/hud/FpsCounter.svelte @@ -2,19 +2,30 @@ // 31 K3 G3 — the desktop FPS counter: this game's "Show FPS" setting, while playing it. // The headset shows the same reading on the top strip and the wrist card (vrGamePanel). // LOCAL chrome, pointer-events none, over the game's HUD. + // 33 Q1: the SAME counter answers the app-wide "Show FPS + draw calls" preference — in + // any scene, in any mode — and then colours the draw calls against the Quest budget + // (amber past 120, red past 150). One meter, one implementation: the headset's half is + // vrPerfStrip.js, reading the same `fpsReading`. import { isLocked, isVRMode, editorMode } from '../../stores/sceneStore'; import { gameSettingValues } from '$lib/gameSettings'; - import { fpsReading, fpsText } from '$lib/fpsMeter'; + import { fpsReading, perfParts, perfStatsShown } from '$lib/fpsMeter'; const playing = $derived($isLocked === true); - const shown = $derived(!!$gameSettingValues.showFps && !$isVRMode && (playing || $editorMode === 'interact')); - const text = $derived(fpsText($fpsReading)); + const gameShown = $derived(!!$gameSettingValues.showFps && (playing || $editorMode === 'interact')); + const shown = $derived(!$isVRMode && ($perfStatsShown || gameShown)); + const parts = $derived(perfParts($fpsReading)); {#if shown} -
- {text.main} - {#if text.detail}{text.detail}{/if} +
+ {parts.fps} + {#if parts.ms || parts.calls || parts.tris} + + {#if parts.ms}{parts.ms}{/if} + {#if parts.calls}{#if parts.ms}{' · '}{/if}{parts.calls}{/if} + {#if parts.tris}{#if parts.ms || parts.calls}{' · '}{/if}{parts.tris}{/if} + + {/if}
{/if} @@ -49,4 +60,13 @@ font-weight: 500; font-size: 11px; } + /* 33 Q1: the draw-call budget — TIER_COLORS in fpsMeter.js holds the same three */ + .fps-calls[data-tier='warn'] { + color: #fbbf24; + font-weight: 700; + } + .fps-calls[data-tier='over'] { + color: #f87171; + font-weight: 700; + } diff --git a/src/components/menu/Controls.svelte b/src/components/menu/Controls.svelte index 64dc0045..0bcf4399 100644 --- a/src/components/menu/Controls.svelte +++ b/src/components/menu/Controls.svelte @@ -647,6 +647,9 @@ }); let classActive = 'group inline-flex items-center justify-center hover:bg-primary-700 focus:outline-hidden focus:ring-4 focus:ring-primary-300'; + // 33 E2: the toggle cell's twin of classActive — same hover, no click-focus ring + const cellToggleClass = + 'group inline-flex items-center justify-center hover:bg-primary-700 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-300'; // 18-B: object-list window size limits, shared with the clamp helpers const OBJ_WIN_MIN = { minW: 250, minH: 200 }; @@ -981,11 +984,25 @@ * the FAB's own right-click menu (plus Settings' Reset window positions, which * is the hatch for iOS Safari, where a long press fires no `contextmenu`). */ const SPACER = '__spacer'; - // 30 P1: the Edit/Interact toggle ('mode') joins the default bar at the END — the same - // place loadLayout already appends a default id an older record has never heard of, so - // a fresh profile and an upgraded one agree, and the well keeps its slot after Scale - const DEFAULT_ORDER = ['move', 'rotate', 'scale', 'objects', 'flow', 'explorer', 'mode']; - const DEFAULT_SPACER = 3; + // 33 E1: THE USER'S ORDER — the transforms, then Interact, then Play (the well), then + // the views they open most: object list, node editor, Explorer, Animation. Interact + // sits beside Play because the two answer one question ("how am I touching the scene + // right now"), and Animation joined the default bar (it was an opt-in view before). + const DEFAULT_ORDER = ['move', 'rotate', 'scale', 'mode', 'objects', 'flow', 'explorer', 'animation']; + const DEFAULT_SPACER = 4; + /** 33 E1: the bars a profile could hold WITHOUT ever customizing — the default rows the + * app has shipped, read as the VISUAL row (the well as `__spacer`). A stored record + * that still IS one of these is a default nobody chose, so it migrates to the new + * default (keeping where the bar sits and whether it is collapsed); anything else is + * a custom bar and wins as saved. Pre-30 had no 'mode'; 30-31 appended it last. */ + const LEGACY_DEFAULT_ROWS = [ + 'move,rotate,scale,__spacer,objects,flow,explorer', + 'move,rotate,scale,__spacer,objects,flow,explorer,mode' + ]; + /** 33 E1: ids that became DEFAULT after having been opt-in. A custom record that does + * not list one LEFT it off on purpose (it was unticked), so it is not appended there — + * the append rule is for buttons new to the app, and these are not. */ + const PROMOTED_DEFAULTS = ['animation']; /** W8b: the roster is bigger than the bar. `DEFAULT_ORDER` is what a fresh profile * puts ON the bar and has NOT changed — the same six ids, the same order, the same @@ -999,7 +1016,10 @@ * Explorer (already a default button) — so the roster and the "+" cannot disagree * about which views exist. Titles come from `DOCK_TITLES`, the dock's own names, so * a button and its tab read the same word. */ - const OPTIONAL_VIEWS = DOCK_VIEWS.filter((view) => view.key !== 'explorer').map((view) => view.key); + const VIEW_BUTTONS = DOCK_VIEWS.filter((view) => view.key !== 'explorer').map((view) => view.key); + // 33 E1: a view on the DEFAULT bar (Animation) is not also an optional one, or the + // Customize and Swap-with lists would carry it twice + const OPTIONAL_VIEWS = VIEW_BUTTONS.filter((key) => !DEFAULT_ORDER.includes(key)); /** the glyph for each optional view. Chosen from a rendered 18px sheet against the * six already on the bar, not from the names — which is what caught the one real @@ -1077,7 +1097,7 @@ // when it is the one on screen — which is the whole reason these are worth // having as buttons: the "+" list can only ever open them. ...Object.fromEntries( - OPTIONAL_VIEWS.map((key) => [ + VIEW_BUTTONS.map((key) => [ key, { title: DOCK_TITLES[key] ?? key, @@ -1105,6 +1125,16 @@ }; } + /** 33 E1: is this stored record one of the default bars the app used to ship? */ + function isLegacyDefault(order: string[], saved: any): boolean { + const hidden: string[] = Array.isArray(saved.hidden) ? saved.hidden : []; + if (hidden.some((id) => order.includes(id))) return false; + const row = [...order]; + const at = Number.isFinite(saved.spacerIndex) ? Math.max(0, Math.min(saved.spacerIndex, row.length)) : 3; + row.splice(at, 0, SPACER); + return LEGACY_DEFAULT_ROWS.includes(row.join(',')); + } + function defaultLayout(): ControlsLayout { return { order: [...DEFAULT_ORDER], hidden: [], spacerIndex: DEFAULT_SPACER, collapsed: false, posX: null }; } @@ -1132,7 +1162,11 @@ (id: any, at: number) => BUTTONS[id] && saved.order.indexOf(id) === at ) : []; - for (const id of DEFAULT_ORDER) if (!order.includes(id)) order.push(id); + const posX0 = typeof saved.posX === 'number' && Number.isFinite(saved.posX) ? Math.max(0, Math.min(1, saved.posX)) : null; + // 33 E1: a record that is still a SHIPPED default migrates to today's default + if (isLegacyDefault(order, saved)) + return { ...defaultLayout(), collapsed: saved.collapsed === true, posX: posX0 }; + for (const id of DEFAULT_ORDER) if (!order.includes(id) && !PROMOTED_DEFAULTS.includes(id)) order.push(id); const hidden: string[] = Array.isArray(saved.hidden) ? saved.hidden.filter((id: any) => order.includes(id)) : []; @@ -1978,13 +2012,16 @@ 4b: the FAB is the well's own THIRD child now (see below), which is why the right half is addressed as `:nth-child(2)` rather than `:last-child` — the FAB would otherwise steal that position and the - right-hand hover paint would silently stop appearing. --> + right-hand hover paint would silently stop appearing. + 33 E3: the neighbour test is `*:hover`, never `p:hover` — the Interact + toggle is a