Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ import { startMusicToolbox } from './lib/musicToolbox'
import { startSharedLibrary } from '$lib/sharedLibrary'
import { startSceneIdentity } from '$lib/sceneIdentity'
import GameChip from './components/hud/GameChip.svelte'
import SimplifiedWaterNotice from './components/water/SimplifiedWaterNotice.svelte' // 36-fb-water F27
import AutoQualityNotice from './components/AutoQualityNotice.svelte' // 40 F12 (was SimplifiedWaterNotice)
import ShortcutSheet from './components/menu/ShortcutSheet.svelte' // 36 I3
import HudLayer from './components/hud/HudLayer.svelte'
import PickCycleHint from './components/menu/PickCycleHint.svelte' // 36 F22 / S6 / S12
Expand Down Expand Up @@ -452,7 +452,7 @@ import { startMusicToolbox } from './lib/musicToolbox'
editor's stand-in for a game's screens, which HudLayer no longer draws outside Play.
Editor-only (it hides itself in Play, in VR and in embed mode). -->
<GameChip />
<SimplifiedWaterNotice />
<AutoQualityNotice />
<ShortcutSheet />
<!-- 38 R8 (NOTES-38 #14): Ctrl+K -->
<CommandPalette />
Expand Down
93 changes: 93 additions & 0 deletions src/components/AutoQualityNotice.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<script>
// 40 F12 — THE AUTO-QUALITY NOTICE. When this device lowers a quality toggle ON ITS OWN, the
// person is told once: "Lowered water quality to keep it smooth · Keep full quality". The rule
// (which toggles, the wording, once per toggle per session) is the pure `qualityNoticeCore`;
// this file only watches the stores and rides the ordinary toast pipeline (kit card, the
// notification history). It replaces two older notices: the first-step quality toast that
// lived in the object list (gone whenever that window was closed) and SimplifiedWaterNotice.
//
// Only AUTOMATIC drops count (the governor's frames / long tasks, the phone's lighter start):
// a game's own Quality preset, the headset's entry floor and the person's own picks are not
// news. Never shown in Play or in a headset — a drop there waits until you are back.
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import { qualityOverrides, qualityState, keepFullQuality } from '$lib/qualityGovernor';
import { simplifiedWater } from '$lib/water/simplifiedNotice.js';
import { newlyLowered, toAnnounce, noticeText, KEEP_FULL_LABEL } from '$lib/qualityNoticeCore.js';
import { FULL_QUALITY } from '$lib/qualityGovernorCore';
import { isLocked, globalRenderer } from '../stores/sceneStore';
import { showToast } from '../stores/appStore';

const KEY = 'quality:announced';
/** the reasons a level moves by itself (qualityGovernor publish reasons) */
const AUTOMATIC = new Set(['frames', 'long tasks', 'phone: a lighter start']);
/** a drop's store writes land over a frame or two (the water tier follows on its next tick) */
const BATCH_MS = 400;

function announced() {
try {
return JSON.parse(sessionStorage.getItem(KEY) || '[]');
} catch {
return [];
}
}
/** @param {string[]} keys */
function remember(keys) {
try {
sessionStorage.setItem(KEY, JSON.stringify([...new Set([...announced(), ...keys])]));
} catch {}
}

onMount(() => {
/** @type {import('$lib/qualityNoticeCore.js').QualitySnapshot} */
let prev = { ...FULL_QUALITY, water: false, fluid: false };
/** @type {Set<string>} */
const pending = new Set();
/** @type {any} */ let timer = null;

const snapshot = () => ({ ...get(qualityOverrides), ...get(simplifiedWater) });
const away = () => get(isLocked) === true || !!(/** @type {any} */ (get(globalRenderer))?.xr?.isPresenting);

function flush() {
timer = null;
if (!pending.size || away()) return; // kept until leaving Play / the headset
const keys = toAnnounce([...pending], announced());
pending.clear();
if (!keys.length) return;
remember(keys);
showToast(noticeText(keys), [{ label: KEEP_FULL_LABEL, action: () => keepFullQuality() }]);
}
function schedule() {
if (!timer) timer = setTimeout(flush, BATCH_MS);
}
function check() {
const next = snapshot();
const keys = newlyLowered(prev, next);
prev = next;
if (!keys.length || !AUTOMATIC.has(get(qualityState).reason)) return;
for (const k of keys) pending.add(k);
schedule();
}

// a level change writes qualityOverrides BEFORE qualityState (publish), so the reason
// that caused it is only readable once the write is finished: judge in a microtask
let queued = false;
function soon() {
if (queued) return;
queued = true;
queueMicrotask(() => {
queued = false;
check();
});
}
const offs = [
qualityOverrides.subscribe(soon),
simplifiedWater.subscribe(soon),
isLocked.subscribe(() => pending.size && schedule())
];
return () => {
offs.forEach((off) => off());
clearTimeout(timer);
};
});
</script>
25 changes: 6 additions & 19 deletions src/components/menu/Controls.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import { sendPing } from '$lib/ping';
import { buildObjectMenuItems } from '$lib/objectMenu';
import * as THREE from 'three';
import { onMount, setContext, tick, untrack } from 'svelte';
import { onMount, setContext, tick } from 'svelte';
import { createGesture } from '$lib/modalGrab';
import { writable, get } from 'svelte/store';
import { shareObject } from '$lib/objectPermissions';
Expand Down Expand Up @@ -599,30 +599,17 @@
const what = q.labels.join(', ').toLowerCase();
return q.pinned
? 'Held at reduced quality (' + what + '). Click to restore full quality.'
: 'This scene is heavy for this device, so drawing was reduced: ' + what + '. It comes back on its own when frames recover. Click to keep it this way.';
: q.held
? 'This scene is heavy for this device, so drawing was reduced: ' + what + '. It stays this way for this scene (it came back once and the frames dropped again). Click to keep it this way.'
: 'This scene is heavy for this device, so drawing was reduced: ' + what + '. It comes back on its own when frames recover. Click to keep it this way.';
});
function qualityChipClick(node: HTMLElement) {
const click = () => (get(qualityState).pinned ? releaseQuality() : pinQuality());
node.addEventListener('click', click);
return { destroy() { node.removeEventListener('click', click); } };
}
// …and once per session, a toast when it FIRST acts, because the chip lives in the object
// list's footer and that window can be closed
let qualityToasted = false;
$effect(() => {
const q = $qualityState;
if (q.level > 0 && !qualityToasted) {
qualityToasted = true;
untrack(() =>
showQualityToast('Quality reduced — this scene is heavy for this device (' + q.labels.join(', ').toLowerCase() + '). It comes back on its own.', [
// 33 integrate: not "Restore …" — on a phone this toast now shows on every load, beside the
// session's own "Restore" (the L1 Restore path), and a tap on the wrong one undid the phone fix
{ label: 'Use full quality', action: () => releaseQuality() },
{ label: 'Keep it', action: () => pinQuality() }
])
);
}
});
// 40 F12: the once-per-session "Lowered … to keep it smooth · Keep full quality" toast is
// AutoQualityNotice (App root) — it used to live here and was gone whenever this window was closed.

// bottom status line: totals across the whole tree (N objects · M hidden)
let objectCount = $state(0);
Expand Down
102 changes: 0 additions & 102 deletions src/components/water/SimplifiedWaterNotice.svelte

This file was deleted.

42 changes: 35 additions & 7 deletions src/lib/qualityGovernor.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export const ingestDrawGap = writable(0);

/**
* For the chip and the suite.
* @type {import('svelte/store').Writable<{level: number, max: number, pinned: boolean, labels: string[], reason: string, at: number, snoozedUntil: number}>}
* `held` (40 F12): recovery is held at this level because coming back from it flapped.
* @type {import('svelte/store').Writable<{level: number, max: number, pinned: boolean, labels: string[], reason: string, at: number, snoozedUntil: number, held: boolean}>}
*/
export const qualityState = writable({
level: 0,
Expand All @@ -68,7 +69,8 @@ export const qualityState = writable({
labels: /** @type {string[]} */ ([]),
reason: '',
at: 0,
snoozedUntil: 0
snoozedUntil: 0,
held: false
});

/** The opt-out. LOCAL preference, default ON. */
Expand Down Expand Up @@ -132,7 +134,8 @@ function publish(level, reason) {
labels: stepLabelsAt(level),
reason,
at,
snoozedUntil
snoozedUntil,
held: level > 0 && governor.flapLock() >= level
});
}

Expand Down Expand Up @@ -187,6 +190,9 @@ export function noteFrameForQuality(ms) {
if (wasLoading) {
wasLoading = false;
governor.settleFor(t);
// 40 F12: a new scene is new evidence — the last scene's flap lock does not carry over
governor.clearFlapLock();
if (get(qualityState).held) qualityState.update((s) => ({ ...s, held: false }));
}
governor.noteFrame(ms, t);
if (phone) notePhoneFrame(ms);
Expand Down Expand Up @@ -254,10 +260,25 @@ export function pinQuality() {
export function releaseQuality() {
pinned = false;
snoozedUntil = Date.now() + RELEASE_SNOOZE_MS;
governor.clearFlapLock();
governor.setLevel(0, now());
publish(0, 'released');
}

/** 40 F12 — "Keep full quality" (the auto-quality notice): full quality now and NO automatic
* step for the rest of this page session. Not saved — Settings' auto switch stays on, so the
* next visit is protected again; turning that switch off and on ends the hold too. */
export function keepFullQuality() {
releaseQuality();
snoozedUntil = Infinity;
publish(0, 'kept full quality');
}

/** 40 F12: the level recovery is held at because it flapped there (-1 = none). For the suite. */
export function qualityFlapLock() {
return governor.flapLock();
}

/** @param {boolean} on */
export function setAutoQuality(on) {
autoQuality.set(!!on);
Expand All @@ -273,6 +294,9 @@ autoQuality.subscribe((on) => {
return;
}
safeStorage.setItem('autoQuality', on ? 'true' : 'false');
// 40 F12: either way the switch is a fresh start — no flap lock, no "keep full quality" hold
governor.clearFlapLock();
snoozedUntil = 0;
if (!on) {
pinned = false;
governor.setLevel(0, now());
Expand Down Expand Up @@ -328,22 +352,26 @@ export const governorForTest = {
longTask(t) {
governor.noteLongTask(t);
},
/** @param {number} level */
setLevel(level) {
governor.setLevel(level, now());
/** @param {number} level @param {number} [t] (40 F12: a synthetic clock, so real frames stay inert) */
setLevel(level, t) {
governor.setLevel(level, t ?? now());
publish(governor.level(), 'test');
},
reset() {
pinned = false;
snoozedUntil = 0;
drawGapEngaged = false;
lastDecideAt = 0;
governor.clearFlapLock();
governor.setLevel(0, -1e9);
governor.forget();
ingestDrawGap.set(0);
publish(0, 'reset');
},
level: () => governor.level()
level: () => governor.level(),
/** 40 F12: the flap lock, and the pre-F12 rule (clear it after every decision) */
flapLock: () => governor.flapLock(),
clearFlapLock: () => governor.clearFlapLock()
};

registerFrameObserver(noteFrameForQuality);
Expand Down
Loading
Loading