Monolito
Última actualización: 03-02-2026
pVISOR — Modularidad (Monolito)
Section titled “pVISOR — Modularidad (Monolito)”Documento técnico del visor tal como está montado hoy (escena + scripts + conexiones). Sin relleno.
Índice modular (lo que vamos a tocar por partes)
Section titled “Índice modular (lo que vamos a tocar por partes)”- M1 — Mapa de conexiones (RED + Input + UI)
- M2 — Escena y jerarquía (GameObjects)
- M3 — RED aplicado (Rt–Ev–Dt) + DI
- M4 — Input System:
ViewerControls(Action MapViewer) - M5 — Desktop Bridge:
ViewerInputAdapter(InputActions → Dt/Ev) - M6 — Touch Bridge:
ViewerTouchGestureAdapter(Touch → Dt) - M7 — Orquestación:
ViewerController(Dt/Ev → Cámara) - M8 — Cámara:
OrbitCameraController(Pivot + Camera) - M9 — Contenido:
ModelRoot(bounds para Frame) - M10 — UI: Debug Menu (UI Toolkit) — tecla M
- M11 — WebGL / Hosting (Itch + iframe) — foco, RMB, wheel
M1 — Mapa de conexiones (RED + Input + UI)
Section titled “M1 — Mapa de conexiones (RED + Input + UI)”Viewer (Desktop):
ViewerControls(Input Actions, Action MapViewer) →ViewerInputAdapterViewerInputAdapterescribe Dt (DataSO) y dispara Ev (EventsSO)ViewerControllerconsume Dt/Ev y ejecuta la cámara víaOrbitCameraController
Touch (Mobile):
Enhanced Touch→ViewerTouchGestureAdapter→ Dt (DataSO) →ViewerController→OrbitCameraController
UI Debug Menu (UI Toolkit):
UiToolkitDebugMenuControllercontrola el panel y la tecla M.- Por ahora, este módulo NO pasa por
ViewerControlsni por RED (solo UI).
M2 — Escena y jerarquía (GameObjects)
Section titled “M2 — Escena y jerarquía (GameObjects)”Jerarquía actual (tal como está en tu escena)
Section titled “Jerarquía actual (tal como está en tu escena)”-
Viewer_Root-
Viewer_PivotViewer_Camera(MainCamera +AspectRatioEnforcer)BackgroundCamera(relleno negro)
-
Viewer_Input(input del viewer)PlayerInput(Actions =ViewerControls, Default Map =Viewer)ViewerInputAdapterViewerTouchGestureAdapter
-
ViewerController(solo el scriptViewerController) -
ModelRootMain LightMesh 3D(contenido)
-
Qué es cada nodo (para que no vuelva a confundirse)
Section titled “Qué es cada nodo (para que no vuelva a confundirse)”-
Viewer_Root- Contiene
RtLifetimeScope(VContainer). - Es el único lugar con referencias serializadas a Dt/Ev:
DataSO,EventsSO. - También serializa
ModelRootcomo scene ref para inyectarlo (se usa para bounds/Frame).
- Contiene
-
Viewer_Pivot-
Transform “pivot” del visor.
-
Tiene
OrbitCameraControllery referencia:Pivot = Viewer_Pivot (Transform)Camera = Viewer_Camera (Camera)
-
-
Viewer_Camera- Cámara que renderiza el contenido.
- Tiene
AspectRatioEnforcer(en tu caso 1:1) que ajustacamera.rectpara forzar el aspect.
-
BackgroundCamera-
Cámara de fondo para que las barras (letterbox/pillarbox) queden negras.
-
Config típica (como la tienes):
- Solid Color negro
- Culling Mask = Nothing
- Priority menor que
Viewer_Camera
-
-
Viewer_Input-
Agrupa todo lo de entrada del viewer:
PlayerInput(Input Actions)ViewerInputAdapter(desktop → Dt/Ev)ViewerTouchGestureAdapter(touch → Dt)
-
-
ViewerController- Orquestador.
- Consume
DataSO, escuchaEventsSOy llama aOrbitCameraController.
-
ModelRoot-
Punto único donde vive el contenido.
-
ViewerControllerlo usa para:- centrado inicial (startup): calcula
Boundscuando existan renderers y fija el centro conSetLastFrameCenter(bounds.center, alsoMoveTargetPivot: true)+ResetView()(arranca centrado sin efecto “como si hiciera F”). Frame()(F): calcular bounds de lo que haya debajo y ejecutarcamera.Frame(bounds)cuando el usuario lo pide (centra y ajusta distancia).
- centrado inicial (startup): calcula
-
Nota: una
Lightno aporta bounds (no es renderer). Si quieres orden, opcional moverMain Lightfuera deModelRoot, pero no es obligatorio.
-
Reglas mínimas
Section titled “Reglas mínimas”ModelRootnunca debe ser hijo deViewer_Pivot.Viewer_Pivotcontiene cámaras (y el script de cámara), no contenido.Viewer_Inputagrupa input;ViewerControlleragrupa lógica.
M3 — RED aplicado (Rt–Ev–Dt) + DI
Section titled “M3 — RED aplicado (Rt–Ev–Dt) + DI”Este módulo define cómo se cablea todo: quién conoce a quién, por qué, y dónde está prohibido resolver dependencias.
M3.1 Regla base
Section titled “M3.1 Regla base”- Rt (Root): único lugar donde se construye el grafo (VContainer) y se serializan refs de escena.
- Dt (DataSO): estado runtime en memoria (buffer entre input y gameplay).
- Ev (EventsSO): señales sin payload (acciones discretas).
Resultado: Input no toca cámara, cámara no lee dispositivos, y el acoplamiento queda concentrado en Rt.
M3.2 Rt — RtLifetimeScope (cómo construye el grafo)
Section titled “M3.2 Rt — RtLifetimeScope (cómo construye el grafo)”Entradas (serializadas en Inspector)
Section titled “Entradas (serializadas en Inspector)”DataSO _dataEventsSO _eventsTransform _modelRoot
Ciclo de vida
Section titled “Ciclo de vida”-
Awake(): si existe_data, llamaResetRuntimeState()antes de construir el contenedor.- Motivo:
DataSOesScriptableObject; su estado runtime puede “quedarse pegado” entre runs si hay Domain Reload deshabilitado o si el asset conserva valores. Se fuerza un estado limpio al arrancar.
- Motivo:
Registro DI (Configure)
Section titled “Registro DI (Configure)”-
RegisterInstance(_data)→ todos reciben el mismoDataSO. -
RegisterInstance(_events)→ todos reciben el mismoEventsSO. -
RegisterInstance(_modelRoot)→ permite inyectarTransformdel contenido (bounds/Frame). -
RegisterComponentInHierarchy<...>()→ inyecta componentes existentes en escena:ViewerInputAdapterViewerTouchGestureAdapterViewerControllerOrbitCameraController
Por qué se hace así
Section titled “Por qué se hace así”DataSO/EventsSO/ModelRootviven en Rt porque son dependencias globales del viewer (y deben ser visibles/serializables en un solo lugar).RegisterComponentInHierarchyevita “auto-resolve” por fuera y mantiene los scripts como componentes de escena normales.
M3.3 Grafo de inyección (quién recibe qué y para qué)
Section titled “M3.3 Grafo de inyección (quién recibe qué y para qué)”| Receptor | Inyecta | Por qué lo necesita | Qué NO debe hacer |
|---|---|---|---|
ViewerInputAdapter | DataSO, EventsSO | Traducir Input Actions a Dt/Ev | No llamar a cámara ni calcular bounds |
ViewerTouchGestureAdapter | DataSO | Traducir touch gestures a Dt | No disparar eventos discretos (por ahora) |
ViewerController | DataSO, EventsSO, OrbitCameraController, Transform modelRoot | Consumir Dt, escuchar Ev y ejecutar cámara + Frame | No leer InputActions ni teclado |
OrbitCameraController | (ninguna por DI) | Control de cámara/pivot con parámetros de Inspector | No leer dispositivos ni DataSO |
Nota: OrbitCameraController no usa DI para sus refs internas (_pivot, _camera) porque son wiring local del módulo cámara (ref directa de escena, estable y visible en Inspector).
M3.4 Dt — DataSO (contrato exacto)
Section titled “M3.4 Dt — DataSO (contrato exacto)”DataSO guarda únicamente estado runtime del viewer (no persistencia).
Estado “Held” (modo)
Section titled “Estado “Held” (modo)”_orbitHeld,_panHeld,_dollyHeld- Setters:
ViewerSetOrbitHeld(bool),ViewerSetPanHeld(bool),ViewerSetDollyHeld(bool) - Getters:
ViewerOrbitHeld(),ViewerPanHeld(),ViewerDollyHeld()
Por qué existe: el input (desktop/touch) define “qué modo está activo” y el orquestador decide qué operación aplicar.
Datos efímeros por frame (acumulados)
Section titled “Datos efímeros por frame (acumulados)”_pointerDelta(Vector2)_zoomScrollY(float)
API:
ViewerAccumulatePointerDelta(Vector2)+ViewerConsumePointerDelta()ViewerAccumulateZoomScrollY(float)+ViewerConsumeZoomScrollY()
Regla: los adapters acumulan, el ViewerController consume (lee y limpia).
Límites defensivos
Section titled “Límites defensivos”MaxPointerDeltayMaxScrollpara clamping.
Por qué: evita spikes raros (p.ej. delta enorme por pérdida de foco o eventos acumulados) que rompan la cámara.
M3.5 Ev — EventsSO (señales exactas)
Section titled “M3.5 Ev — EventsSO (señales exactas)”Eventos disponibles:
FrameRequestedResetRequested
API:
RaiseFrameRequested()RaiseResetRequested()
Conexión:
ViewerInputAdapterdisparaRaise*()cuando el input discreto ocurre.ViewerControllerse suscribe enOnEnable()y se desuscribe enOnDisable().
Por qué sin payload: Frame y Reset no requieren estado extra. El consumidor (ViewerController) decide cómo actuar (ej. bounds de ModelRoot).
M3.6 Qué NO está conectado (a propósito)
Section titled “M3.6 Qué NO está conectado (a propósito)”-
UI Toolkit Debug Menu (tecla M) no pasa por RED todavía: vive como módulo UI independiente.
- Motivo: no afecta gameplay del viewer aún; evita meter dependencias prematuras.
- Si más adelante debe bloquear cámara o disparar acciones, se conectará vía Dt/Ev.
-
DataSOno persiste en disco. Persistencia sería otro sistema.
M4 — Input System: ViewerControls (Action Map Viewer)
Section titled “M4 — Input System: ViewerControls (Action Map Viewer)”Este módulo define el asset de Input Actions y su contrato. No describe gameplay.
- Qué hace M4: define qué acciones existen y qué valores producen.
- Dónde se conectan: ver M5 (Desktop → Dt/Ev) y M6 (Touch → Dt).
- Dónde se ejecutan: ver M7 (consume Dt/Ev) y M8 (cámara).
M4.1 Action Map
Section titled “M4.1 Action Map”- Map activo:
Viewer
M4.2 Acciones (contrato exacto)
Section titled “M4.2 Acciones (contrato exacto)”| Acción | Action Type | Control Type | Binding actual | Qué produce | Quién la consume |
|---|---|---|---|---|---|
Orbit | Button | Button | Left Button [Mouse] | Held (performed/canceled) | M5 → DataSO.ViewerSetOrbitHeld() |
Pan | Button | Button | Middle Button [Mouse] | Held (performed/canceled) | M5 → DataSO.ViewerSetPanHeld() |
Dolly | Button | Button | Right Button [Mouse] | Held (performed/canceled) | M5 → DataSO.ViewerSetDollyHeld() |
PointerDelta | Value | Vector2 | Delta [Pointer] (<Pointer>/delta) | Vector2 delta (px por evento) | M5/M6 → DataSO.ViewerAccumulatePointerDelta() |
Zoom | PassThrough | Axis (float) | Scroll/Y [Mouse] | float scrollY (delta por rueda) | M5/M6 → DataSO.ViewerAccumulateZoomScrollY() |
Frame | Button | Button | F [Keyboard] | discreto (performed) | M5 → EventsSO.RaiseFrameRequested() |
Reset | Button | Button | R [Keyboard] | discreto (performed) | M5 → EventsSO.RaiseResetRequested() |
Qué significa Delta [Pointer]
Section titled “Qué significa Delta [Pointer]”<Pointer>/delta es el movimiento del puntero en píxeles desde el último evento (mouse o pointer compatible). En este proyecto se usa como “delta único” para Orbit/Pan/Dolly.
Por qué está separado de Orbit/Pan/Dolly:
- Orbit/Pan/Dolly solo dicen qué modo está activo (held).
PointerDeltaes el dato (delta).- El orquestador (M7) decide con prioridad Dolly > Pan > Orbit qué operación aplicar usando el mismo delta.
M4.3 Por qué Zoom está en PassThrough
Section titled “M4.3 Por qué Zoom está en PassThrough”En Input System, PassThrough es apropiado para señales tipo delta (rueda/pinch):
- No quieres “estado sostenido”; quieres impulsos repetidos.
- Evita que el sistema intente hacer resolución de conflictos como si fuera un valor “dominante”.
En este proyecto, además, el adapter (M5) lee Zoom en ctx.performed y acumula en Dt. Con rueda, performed ocurre por cada delta.
Conclusión: Zoom = PassThrough + Axis es correcto para el wheel.
M4.4 Regla crítica de nombres
Section titled “M4.4 Regla crítica de nombres”ViewerInputAdapter compara por ctx.action.name usando strings constantes.
- Si renombrar acciones en el asset (
ViewerControls) → rompes el puente. - Mantener nombres:
PointerDelta,Orbit,Pan,Dolly,Zoom,Frame,Reset.
M4.5 Atajos oficiales y dónde viven
Section titled “M4.5 Atajos oficiales y dónde viven”| Tecla | Acción | Dónde está definida | Nota |
|---|---|---|---|
| F | Frame | ViewerControls → M5 → EventsSO | El “cuánto encuadra” se ajusta en M8 (_framePadding). Frame centra en bounds.center y ajusta distancia. |
| R | Reset | ViewerControls → M5 → EventsSO | Resetea a defaults de M8 (_defaultAngles/_defaultDistance/_cameraLocalOffsetXY). Además, si existe un centro válido (lastFrameCenter), centra el pivot ahí (esto hace que el Reset sea consistente tras Frame y también en el arranque). No “restaura” una posición fija inicial del pivot. |
| M | Debug Menu | UI Toolkit (M10) | No está en ViewerControls por ahora. |
M5 — Desktop Bridge: ViewerInputAdapter (InputActions → Dt/Ev)
Section titled “M5 — Desktop Bridge: ViewerInputAdapter (InputActions → Dt/Ev)”Objetivo
Section titled “Objetivo”Traducir Input Actions a:
- Escrituras en
DataSO(held + acumulados) - Señales en
EventsSO(Frame/Reset)
Entradas
Section titled “Entradas”PlayerInput(mismo GO)ViewerControls(Action MapViewer)
Salidas
Section titled “Salidas”- PointerDelta →
DataSO.ViewerAccumulatePointerDelta(Vector2) - Zoom →
DataSO.ViewerAccumulateZoomScrollY(float) - Orbit/Pan/Dolly →
DataSO.ViewerSet*Held(bool)por performed/canceled - Frame/Reset →
EventsSO.Raise*Requested()
Conexiones
Section titled “Conexiones”- No llama a cámara.
- No hace gameplay.
Riesgos
Section titled “Riesgos”- En WebGL: RMB puede abrir
contextmenusi el host/template no lo bloquea. - Teclado depende del foco del canvas.
M6 — Touch Bridge: ViewerTouchGestureAdapter (Touch → Dt)
Section titled “M6 — Touch Bridge: ViewerTouchGestureAdapter (Touch → Dt)”Objetivo
Section titled “Objetivo”Soportar gestos compuestos en Mobile WebGL sin interferir con Desktop.
Entradas
Section titled “Entradas”EnhancedTouchSupportTouch.activeTouches
Salidas
Section titled “Salidas”Escribe en el mismo contrato DataSO:
- 1 dedo: Orbit + PointerDelta
- 2 dedos: Pan (centroide) + Zoom (pinch → scrollY)
Conexiones
Section titled “Conexiones”- No usa Input Actions.
- No emite eventos Ev (por ahora).
Regla anti-interferencia
Section titled “Regla anti-interferencia”Si Touchscreen.current == null → no hace nada. Esto evita apagar el mouse en PC.
Parámetros de tuning (Inspector)
Section titled “Parámetros de tuning (Inspector)”_orbitMultiplier_panMultiplier_pinchToScroll_pinchThresholdPixels
M7 — Orquestación: ViewerController (Dt/Ev → Cámara)
Section titled “M7 — Orquestación: ViewerController (Dt/Ev → Cámara)”Este script existe para una sola cosa: ser el “cerebro” del visor.
- Input (M5/M6) solo produce intención: escribe
DataSOy disparaEventsSO. - Cámara (M8) solo sabe moverse: orbit/pan/dolly/zoom/frame/reset.
ViewerControlleres el puente de ejecución: interpreta Dt/Ev y llama a la cámara.
Por qué existe (y por qué no lo hace la cámara)
Section titled “Por qué existe (y por qué no lo hace la cámara)”- Mantiene la cámara agnóstica de input (no lee mouse/teclado/touch).
- Centraliza la regla: “los adapters acumulan, el controller consume”.
- Decide prioridad de navegación (Dolly > Pan > Orbit) usando un solo
PointerDelta. - Es el único lugar que conoce
ModelRootpara calcular bounds y ejecutarFrame().
Entradas (inyección)
Section titled “Entradas (inyección)”DataSO→ lee y consume acumulados por frame.EventsSO→ se suscribe a señales discretas.OrbitCameraController→ ejecuta operaciones de cámara.Transform modelRoot→ origen para bounds del contenido.
Qué hace en Update() (lo mínimo)
Section titled “Qué hace en Update() (lo mínimo)”delta = DataSO.ViewerConsumePointerDelta()scrollY = DataSO.ViewerConsumeZoomScrollY()- Si hay scroll →
camera.Zoom(scrollY) - Si
DollyHeld→camera.Dolly(delta)si no, siPanHeld→camera.Pan(delta)si no, siOrbitHeld→camera.Orbit(delta)
Qué hace con eventos
Section titled “Qué hace con eventos”FrameRequested→ calcula bounds desdemodelRooty llamacamera.Frame(bounds)ResetRequested→camera.ResetView()
Arranque: iniciar SIEMPRE centrado y en Reset (fix Editor + WebGL)
Section titled “Arranque: iniciar SIEMPRE centrado y en Reset (fix Editor + WebGL)”Problema real encontrado: al iniciar (sobre todo en WebGL) los renderers/bounds pueden no estar listos inmediatamente. Si se intenta hacer Frame() demasiado pronto, se siente como que el visor “hizo F” al arrancar (porque Frame() ajusta distancia).
Solución final aplicada (determinística y estable):
-
En
Start()se marca un flag de inicialización (_pendingInitCenter = true). -
En
Update(), mientras ese flag esté activo, se intenta calcularBoundsdesdeModelRoothasta que existan renderers. -
Cuando hay bounds válidos:
-
Se fija el centro sin ejecutar Frame:
camera.SetLastFrameCenter(bounds.center, alsoMoveTargetPivot: true)
-
Se fuerza la vista inicial:
camera.ResetView()
-
Se apaga
_pendingInitCenter.
-
Resultado: el visor arranca centrado al modelo y en Reset, sin depender de “esperar X frames”, y sin que parezca un Frame() involuntario.
M8 — Cámara: OrbitCameraController (Pivot + Camera)
Section titled “M8 — Cámara: OrbitCameraController (Pivot + Camera)”Este módulo es el corazón del “feeling” del visor. Aquí vive:
- cómo orbit/pan/dolly/zoom afectan al pivot + cámara hija
- cómo funciona Frame (F)
- cómo funciona Reset (R) (y la vista inicial)
Modelo
Section titled “Modelo”-
El pivot rota (yaw/pitch) y se traslada (pan, reset centrado, y frame).
-
La cámara hija se fuerza a:
localRotation = identitylocalPosition = (offsetX, offsetY, -distance)
Esto significa que el “ángulo” real del visor se controla desde el pivot (no desde una rotación manual de la cámara hija).
Operaciones
Section titled “Operaciones”Orbit(delta): yaw += dxsens ; pitch -= dysens (clamp)Pan(delta): mueve el pivot usando right/up de la cámara, escalado por distanciaZoom(scrollY): distancia lineal (rueda / pinch)Dolly(deltaY): distancia exponencial por arrastre (RMB drag)Frame(bounds): centra enbounds.centery calcula distancia por FOV/aspect +_framePaddingResetView(): vuelve a defaults (y si existe último centro, centra ahí)SetLastFrameCenter(center): define un centro válido para Reset sin necesidad de ejecutar Frame
Cambios recientes implementados (lo de este chat)
Section titled “Cambios recientes implementados (lo de este chat)”Captura robusta de Reset (fix de “pitch raro / invertido”)
Section titled “Captura robusta de Reset (fix de “pitch raro / invertido”)”Se corrigió la captura de reset “auto” para que no dependa de eulers frágiles ni de LookRotation contra el centro.
- Se guarda la pose inicial real de la cámara en
Awake()(posición y rotación) antes de que el rig fuerce la cámara. - La captura de defaults se calcula a partir del forward real de esa rotación y una distancia consistente (dot hacia el centro).
Nota práctica: es normal ver yaw como 330° en lugar de -30°. Es el mismo ángulo, solo normalizado a [0..360).
Frame (F) con suavizado real
Section titled “Frame (F) con suavizado real”Frame()fija objetivos (_targetPivotPosy_targetDistance).- El movimiento ocurre interpolado en
LateUpdate()con_smooth.
Calibración principal:
_framePadding→ más alto = encuadra más lejos (menos zoom).
Defensa de arranque si algo llama Frame demasiado pronto
Section titled “Defensa de arranque si algo llama Frame demasiado pronto”Se añadió una defensa interna para que, si Frame(bounds) ocurre durante inicialización, no modifique la distancia (evita el “parece que hizo F al inicio”), pero sí actualice el centro y mantenga clip planes defensivos. Tras el primer LateUpdate, Frame() vuelve a operar normal.
En la práctica, el arranque recomendado se resuelve desde M7 con SetLastFrameCenter(...) + ResetView().
Reset: manual vs auto
Section titled “Reset: manual vs auto”ResetView() se usa también como vista inicial (el visor inicia como si presionaras R).
Hay dos modos, controlados por _resetMode:
1) Manual
Section titled “1) Manual”Reset usa valores del Inspector:
_defaultAngles(Yaw/Pitch)_defaultDistance_cameraLocalOffsetXY
En Play Mode, si editas defaults en Inspector, el script los aplica de inmediato (por OnValidate) cuando estás en modo Manual.
2) Auto (desde pose inicial real de la cámara)
Section titled “2) Auto (desde pose inicial real de la cámara)”- Captura pose inicial real de cámara en
Awake(). - Calcula
_defaultAnglesy_defaultDistancedesde esa pose. - Cuando se llama
Frame(bounds)con unbounds.centerreal, puede recalcular defaults con ese centro (solo en modo Auto).
Parámetros (Inspector)
Section titled “Parámetros (Inspector)”- Orbit:
_orbitSensitivity,_minPitch,_maxPitch - Pan:
_panSensitivity - Zoom:
_zoomSensitivity,_minDistance,_maxDistance - Dolly:
_dollySensitivity - Suavizado:
_smooth - Snap opcional:
_snapToTargetEpsilon(0 = off) - Frame:
_framePadding - Reset:
_resetMode,_defaultAngles,_defaultDistance,_cameraLocalOffsetXY
Pendiente de test y calibración
Section titled “Pendiente de test y calibración”-
Ajustar
_defaultAngles/_defaultDistance/_cameraLocalOffsetXYhasta que el “reset por defecto” sea el deseado. -
Ajustar
_framePaddingpara que el encuadre de F sea el correcto. -
Verificar consistencia de R tras:
- usar
Frame()varias veces - panear
- orbitar
- usar
-
Validar en WebGL (focus + wheel + RMB) que el comportamiento percibido sea el mismo.
M9 — Contenido: ModelRoot (bounds para Frame)
Section titled “M9 — Contenido: ModelRoot (bounds para Frame)”ModelRoot es un Transform de escena que actúa como “contenedor del contenido”. Su única función técnica en el viewer es permitir que el sistema calcule Bounds de forma determinista.
Qué significa “bounds para Frame”
Section titled “Qué significa “bounds para Frame””ViewerControllerrecorre renderers bajoModelRoot.- Con esos renderers calcula un
Bounds(AABB). - Ese mismo
Bounds.centerse usa también en el arranque para centrar el Reset inicial (ver M7). - Ese
Boundsse pasa aOrbitCameraController.Frame(bounds)cuando el usuario pide Frame (F).
Reglas mínimas
Section titled “Reglas mínimas”- Todo lo que deba entrar en el encuadre debe ser renderer y vivir bajo
ModelRoot. - Evitar meter helpers con renderers que no quieras encuadrar (gizmos/planes ocultos/etc.).
- Luces no aportan bounds; no rompen el encuadre, pero si quieres orden puedes sacarlas.
Dónde se calibra “qué tanto se acerca”
Section titled “Dónde se calibra “qué tanto se acerca””No es aquí: se calibra en M8 con _framePadding.
M10 — UI: Debug Menu (UI Toolkit) — tecla M
Section titled “M10 — UI: Debug Menu (UI Toolkit) — tecla M”Objetivo
Section titled “Objetivo”Panel de depuración (no bloqueante) hecho con UI Toolkit.
Piezas
Section titled “Piezas”DebugMenu.uxml+DebugMenu.ussUiToolkitDebugMenuController(RequireComponentUIDocument)
Origen de input
Section titled “Origen de input”- Tecla M leída con Input System directo:
Keyboard.current[toggleKey].wasPressedThisFrame - No está en
ViewerControlspor decisión práctica actual.
Comportamiento
Section titled “Comportamiento”- Abre/cierra con clase USS
opensobremenuRoot. scrimcierra al click.closeBtncierra.- Intenta asegurar foco en WebGL:
root.focusable = trueyroot.Focus()al primer click. - Switches actuales son placeholders (solo togglean clase
ony log).
Qué NO hace (por ahora)
Section titled “Qué NO hace (por ahora)”- No escribe en
DataSO. - No emite en
EventsSO. - No bloquea la cámara explícitamente (si en el futuro molesta, se añade gating).
M11 — WebGL / Hosting (Itch + iframe)
Section titled “M11 — WebGL / Hosting (Itch + iframe)”Objetivo
Section titled “Objetivo”Evitar que el navegador/iframe rompa input.
Riesgos típicos
Section titled “Riesgos típicos”- Focus: el teclado no entra hasta click inicial.
- RMB: aparece menú del navegador (
contextmenu). - Wheel: scrollea la página en lugar del visor.
Estas correcciones viven en template/host HTML/JS, no en gameplay.
Contexto (qué es el producto) y Backlog (qué falta) viven en documentos separados. Este documento es solo el cómo está conectado.