Estante 3D: entradas, orden A–Z, sin conexión, pósit y giroscopio calibrado

- Estante: entradas desde el menú, la librería y la ficha; orden reciente o A–Z; foto del estante, arte de lomos y portadas
  guardados en el dispositivo; sin conexión muestra solo lo guardado.
- Notas de 40 letras sin cortar palabras, lomo curvo, sombra en las portadas de frente y lomos con logo sin el título.
- Pósit cuadrado con el capítulo y la página por la que va el usuario; paso al lector sin pantalla de carga.
- Giroscopio: integra rotationRate (sin los saltos de β/γ con el dispositivo vertical), tope suave, menos sensibilidad al
  caminar, lado en horizontal según la gravedad, suavizado por tiempo y más rango en el estante y el libro.
- Laboratorio del giroscopio en Ajustes (temporal, solo desarrollo) para grabar los sensores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 21:12:58 -06:00
co-authored by Claude Opus 5.5
parent e40127265f
commit d7400f27ad
8 changed files with 539 additions and 108 deletions
+37
View File
@@ -0,0 +1,37 @@
// Laboratorio del giroscopio (solo para desarrollo; se borrará junto con public/gyrolab.js y su botón en Ajustes).
// Guarda las grabaciones de los sensores en DATA_DIR/gyrolab/, una por paso del guion.
import fs from 'node:fs'
import path from 'node:path'
const MAX_BYTES = 4 * 1024 * 1024
const SESSION = /^[a-z0-9]{4,24}$/, STEP = /^[a-z0-9-]{2,24}$/
export function setupGyroLab({ DATA_DIR, HttpError, json }) {
const dir = path.join(DATA_DIR, 'gyrolab')
async function readRaw(req) {
let size = 0
const chunks = []
for await (const c of req) {
size += c.length
if (size > MAX_BYTES) throw new HttpError(413, 'Grabación demasiado grande')
chunks.push(c)
}
try { return JSON.parse(Buffer.concat(chunks).toString()) } catch { throw new HttpError(400, 'JSON inválido') }
}
async function route(p, method, req, res, user) {
if (p !== '/api/gyrolab') return false
if (method === 'GET') {
const mine = fs.existsSync(dir) ? fs.readdirSync(dir).filter(f => f.startsWith(`u${user.id}-`)) : []
return json(res, 200, { files: mine.sort() })
}
if (method !== 'POST') throw new HttpError(405, 'Método no permitido')
const body = await readRaw(req)
if (!SESSION.test(body?.session || '') || !STEP.test(body?.step || '')) throw new HttpError(400, 'Sesión o paso no válidos')
if (!Array.isArray(body.ori) || !Array.isArray(body.mot)) throw new HttpError(400, 'Faltan lecturas')
fs.mkdirSync(dir, { recursive: true })
const name = `u${user.id}-${body.session}-${body.step}.json`
fs.writeFileSync(path.join(dir, name), JSON.stringify({ ...body, user: user.username, savedAt: Date.now() }))
return json(res, 200, { ok: true, file: name })
}
return { route }
}