# WebAR Fácil · manual completo Generado desde documentación vigente y HTML de ejemplo. Lee primero el contrato de IA. Las rutas de ejemplo no son universales. --- Fuente: docs/ai-guide.md # Contrato para asistentes de IA Para la instalación XAMPP de desarrollo existe un recolector opcional: scripts/install-local-diagnostics.mjs conecta los ejemplos y guarda envelopes en reports/local-devices/data//. El diagnóstico está en envelope.report. Estos datos privados no se incluyen en Git ni en el SDK distribuido. El teléfono usa el mismo origen HTTPS del ejemplo, nunca su propio localhost. El panel está en diagnostics/ y el recorrido en diagnostics/mobile.html. No clasifiques registros execution=automated como evidencia física ni interpretes recibido como aprobado. La instalación no activa análisis autónomos del asistente. Este documento describe **webar-facil 0.1.0 implementada**. La ruta predeterminada para nuevas aplicaciones es **HTML declarativo**, con referencia en [inicio](getting-started.md) y [etiquetas](declarative.md). La API programática sigue disponible para casos avanzados; no es un requisito de una aplicación básica. ## Cómo decidir qué generar La carpeta [PRUEBAS](../ejemplos/) proporciona 25 HTML completos y comentados para GLB, mover/girar/pellizcar, botones de contenido, texto, MP4 y luz con marcador o SLAM óptico, y un mapa guardado en IndexedDB. Sus recursos multimedia son locales y originales; la receta 25 añade A-Frame con su licencia. Cada HTML tiene sus propios listeners; el movimiento sobre el plano, el Sprite de texto y la iluminación son código de aplicación mediante Three.js, no etiquetas nuevas del SDK. Los ejemplos ópticos utilizan colocación manual y escala relativa. Para presupuestos de rendimiento usa performance="auto" (predeterminado), battery, balanced o quality en webar-scene; consulta cámara/rendimiento para límites. camera-width/camera-height son preferencias de captura; analysis-width/analysis-pixels y fps limitan el análisis. El motor mide carga en lugar de mantener una lista de modelos de móvil. No prometas selección óptima universal, control térmico ni que menor resolución siempre reconozca mejor. En odometría no cambies dimensiones dentro del mapa; un cambio de perfil exige reiniciar. Los mapas ópticos son referencias dispersas locales en memoria; guardarlos es optativo, no una malla ni un mapa exportado de ARCore/ARKit. | Petición | Entrega recomendada | | --- | --- | | GLB, varios marcadores, giro/tamaño/clip | HTML con webar-scene, webar-target y webar-model | | MP4/chroma | webar-video dentro del target o directamente en SLAM | | Gestos | atributo gestures; consultar interacción y límites de escala | | SLAM sin marcador | webar-scene mode="slam" con webar-model directo; fallback óptico por defecto | | Despegar contenido de imagen a espacio | Escena de imagen con atributo slam; explicar resultado de transición | | Botones propios, diagnóstico o interacción | Etiquetas más listeners DOM opcionales | | Blob, renderer propio o modificación del backend | [Programática](programmatic.md), [API avanzada](advanced.md) | | Proyecto A-Frame existente | [Wrapper A-Frame](aframe.md); no confundir su schema con las etiquetas propias | ## Flujo predeterminado 1. Usa un `

Cargando WebAR… Si no aparece Iniciar AR, revisa la ruta y la versión del script. Abre mediante HTTP/HTTPS.

``` Sirve esa carpeta por HTTP/HTTPS. Para teléfonos usa HTTPS confiable y accesible desde el dispositivo; el localhost del teléfono no es tu computador. Consulta [despliegue](deployment.md). ## Colocar tu imagen y tu modelo Copia tus archivos a la carpeta del sitio y cambia únicamente los dos `src` del contenido: ```html ``` La imagen JPG/PNG se compila automáticamente. No debes crear ni cargar un `.dat` o JSON. Apunta la cámara a **esa misma imagen** impresa o en otra pantalla. `physical-width="0.2"` significa 20 cm de ancho físico; si no conoces la medida, se usa como referencia de escala, sin exactitud métrica garantizada. ```text mi-sitio/ index.html webar-facil.min.js marcador.jpg Kai.glb ``` El standalone inicia los recursos integrados en el navegador. Si alojas el JS en un servidor común, varios sitios pueden referenciar la misma URL del script. Cada aplicación conserva sus propias rutas de imagen y modelo. ## Evitar una página en blanco | Dónde está tu HTML | `src` del script | | --- | --- | | En cualquier proyecto con el SDK al lado del HTML | `./webar-facil.min.js` | | En la raíz del repositorio fuente | `./dist/webar-facil.min.js` | | Dentro de `examples/` del repositorio | `../dist/webar-facil.min.js` | El HTML completo de arriba corresponde a la **primera fila**. No cambies el script de los ejemplos del repositorio a `./webar-facil...` si no copiaste el bundle junto al HTML. Actualiza también el JS de tu sitio: una versión anterior puede no registrar las etiquetas. El párrafo de respaldo se oculta cuando el componente carga correctamente. Más diagnóstico en [HTML declarativo](declarative.md) y [problemas frecuentes](troubleshooting.md). ## Continuar - [Tamaño, giro y animaciones](models.md). - [Varios marcadores](multiple-targets.md). - [SLAM declarativo](spatial-tracking.md). - [Eventos y retención](events.md). - [Recetas declarativas](recipes.md). - [Referencia completa de etiquetas](declarative.md). Para vídeo/chroma, gestos, controles de animación y cambios de archivos sin reiniciar, continúa con [interacción declarativa](interaction.md). Si necesitas controlar el renderer, recursos Blob o sustituir piezas del motor, sigue la [integración programática avanzada](programmatic.md). --- Fuente: docs/phone-detection.md # Reconocimiento de celulares El [ejemplo 31](../examples/PRUEBAS/31-reconocer-celular.html) usa un detector de objetos opcional. Apunta a otro celular y activa «Mostrar detección y confianza» para ver el rectángulo detectado. Kai sigue ese rectángulo en pantalla. La detección se ejecuta localmente en un Worker. ## Instalar el detector de celulares (ejemplo 31) El detector es opcional y se distribuye por separado de `webar-facil.min.js`. Para incorporarlo a tu proyecto, copia `31-reconocer-celular.html`, la carpeta completa `phone-detector/` junto al HTML y el modelo GLB que quieras mostrar. - Conserva `demo.js`, `worker.js`, `tf.min.js`, `coco-ssd.min.js`, `model.json` y los cinco archivos `group1-shard1of5` a `group1-shard5of5` juntos. El manifiesto de pesos usa esos nombres exactos. Incluye también `LICENSE.txt` y conserva `README.md` como referencia. - En el HTML, apunta el primer ``. Si cambias el GLB o la estructura de carpetas, ajusta la ruta en `demo.js`. La ruta del modelo se resuelve desde `demo.js`, no desde el HTML. - Sirve los archivos por HTTPS o en `localhost` durante el desarrollo. El Worker carga los pesos desde la misma carpeta y procesa la cámara localmente. Las aplicaciones que solo usan marcadores o SLAM pueden usar el SDK sin copiar el detector. --- Fuente: docs/declarative.md # Aplicaciones con HTML declarativo El bundle standalone registra Web Components automáticamente. Cámara, renderer, controles de inicio/parada, carga de modelo, compilación de imágenes, animación y limpieza quedan dentro de la biblioteca. Para una aplicación básica no necesitas escribir JavaScript ni cargar A-Frame. Empieza con [el HTML para tu propio sitio](getting-started.md); esta página es la referencia completa de etiquetas y también muestra las rutas específicas del repositorio. ## Cámara, captura y pantalla completa El [ejemplo 27](../examples/PRUEBAS/27-pantalla-completa.html) activa ambos atributos en ``. El botón Iniciar AR pide pantalla completa cuando está disponible. En los demás navegadores el visor ocupa la ventana visible. Pulsa Sesión para recuperar el modal y detener la cámara. El menú permite elegir Automática, Equilibrada, Alta o Ahorro. Activar la casilla de ahorro aplica el perfil de menor carga. Cambiar la calidad durante una sesión reinicia la captura y el tracking para renegociar la cámara. En SLAM vuelve a colocar el contenido después del cambio. La foto y el vídeo incluyen la cámara web y el contenido renderizado. El micrófono se solicita al pulsar Grabar. El vídeo usa MP4 cuando el navegador admite esa salida, o WebM como alternativa. Cuando el archivo está listo, usa Guardar archivo para descargarlo al dispositivo. En iPhone también puedes usar Abrir archivo o Compartir para acceder a las opciones del sistema. Compartir aparece habilitado si el navegador acepta ese archivo en su hoja de compartir. La cámara nativa de una sesión WebXR inmersiva se gestiona desde el sistema. El evento `ar-media` entrega `{file}` a aplicaciones que quieran añadir sus propias acciones locales. ## Ejemplo completo Abre [la aplicación declarativa](../examples/declarative.html) con el servidor del proyecto. Este es su HTML real: ```html WebAR declarativo

Cargando WebAR… Si este mensaje permanece, comprueba que el script existe y es el build actualizado con componentes declarativos. Abre este ejemplo mediante HTTP/HTTPS: npm run dev y http://127.0.0.1:5173/examples/declarative.html. No uses file://.

``` Para tu sitio, coloca `webar-facil.min.js` junto a tu `index.html`. Cambia el script a `./webar-facil.min.js`, la imagen a `./marcador.jpg` y el modelo a `./Kai.glb`. Los controles los crea la biblioteca al registrar las etiquetas. ## Si la página queda en blanco Las etiquetas solo adquieren controles cuando carga el **standalone actualizado** y registra `webar-scene`. Un archivo JS anterior a esta integración puede cargar sin error de red y no registrar las etiquetas. Sustituye el bundle de tu sitio por el build actual y actualiza la caché del navegador. | Ubicación del HTML | Ruta de script | | --- | --- | | `examples/declarative.html` del repositorio | `../dist/webar-facil.min.js` | | HTML en la raíz del repositorio | `./dist/webar-facil.min.js` | | HTML junto al bundle en una copia de `dist/` | `./webar-facil.min.js` | Ejecuta `npm run dev` y abre `http://127.0.0.1:5173/examples/declarative.html`. Abrir con doble clic produce una URL `file://`, que no es un modo soportado para cámara/Workers. Comprueba en Network que la URL del JS responde con JavaScript y estado 200, no con un HTML de fallback. El párrafo de carga de los ejemplos es contenido de respaldo: se ve si no se registra el componente y se oculta automáticamente cuando aparece su shadow DOM. Puedes conservarlo en tu app para evitar una página vacía incluso si falla la descarga del SDK. En consola puedes verificar `customElements.get('webar-scene')`: debe devolver una clase, no `undefined`. ## Opciones de escena | Atributo de `webar-scene` | Valor por defecto y significado | | --- | --- | | `mode` | `image`; `slam` inicia el flujo espacial sin imágenes | | `backend` | `auto` en imagen; `optical-flow` en modo SLAM | | `slam` | Añade botón de transición de imagen a seguimiento espacial | | `keep-content-on-lost` | Ausente: false; presente: conserva visualmente la última transformación | | `loss-grace` | 500 ms de tolerancia de pérdida | | `performance` | auto; perfiles auto, balanced, battery y quality; límites y comportamiento en cámara/rendimiento | | `camera-width`, `camera-height` | Preferencias de captura del perfil; el navegador puede conceder otro tamaño | | `analysis-width` | Ancho máximo del perfil: 640 en auto/balanced, 480 en battery, 960 en quality | | `analysis-pixels` | Presupuesto total por frame: 307200 en auto/balanced; limita también la orientación vertical | | `pixel-ratio` | 2; tope de densidad de renderizado (mínimo entre DPR del dispositivo y este valor positivo). 1 reduce píxeles sin cambiar resolución del tracker | | `antialias` | true; false desactiva MSAA al crear el renderer | | `fps` | 30 en auto/balanced/quality, 20 en battery; objetivo de captura/análisis, no garantía de rendimiento | | `asset-base` | Opcional. En el standalone, definirlo activa la carga externa para esa escena. En ESM, se usa el directorio configurado al registrar | | `opencv-url` | OpenCV integrado en standalone. Si se configura, se resuelve respecto a asset-base | | `draco-path` | Draco integrado en standalone. Si se configura, se resuelve respecto a asset-base | | `gestures`, `min-scale`, `max-scale` | Gestos opt-in; límites iniciales 0.1 y 100 | | `scan-guide` | Marco animado mientras busca marcadores. Predeterminado `true`. | | `surface-grid` | Cuadrícula de referencia sobre la propuesta de superficie. Predeterminado `true`. | | `media-menu` | Menú opcional de perfiles de cámara, foto, vídeo con micrófono, guardado y compartir. | | `fullscreen-view` | Visor que cubre la pantalla disponible y modal de sesión. | | `keyframe-correction` | false; corrección local de odometría experimental, ver comparación y límites | | `diagnostics` | false; registro local acotado y botón para descargar JSON | | `placement` | auto o manual; manual exige confirmar una propuesta óptica reciente | Las opciones booleanas admiten atributo vacío, `true` o `false`. La escena ocupa 100dvh por defecto; puedes cambiar su altura con CSS, por ejemplo `webar-scene {height: 75vh}`. Los controles y mensajes están arriba del visor dentro de su shadow root; no necesitas bajar hasta el final de la cámara para iniciar. El inicio requiere pulsar el botón para solicitar la cámara. ## Marcadores y contenido `webar-target` recibe `src` (JPG/PNG o WART `.dat`), `target-id` y `physical-width` en metros. Si omites el ID, se generan `target-0`, `target-1`, etc., según el orden del HTML. `physical-width` vale 0.2. La imagen se compila automáticamente; no necesitas suministrar JSON. Puedes declarar hasta 8 imágenes diferentes y colocar modelos dentro de cada target. No repitas IDs ni uses la misma imagen para simular múltiples instancias físicas. No hay un motor/cámara por etiqueta de modelo. ```html ``` ## Tamaño, giro, posición y animación | Atributo de `webar-model` | Contrato | | --- | --- | | `src` | GLB autocontenido normal, Draco o Meshopt | | `size` | Dimensión máxima inicial; 0.16 por defecto; valor positivo | | `scale` | Multiplicador uniforme (`2`) o XYZ (`2 2 2`); por defecto 1 | | `rotation` | Grados XYZ separados por espacios; por defecto `0 0 0` | | `position` | XYZ local al anclaje; por defecto `0 0 0` | | `animation` | Nombre exacto, índice desde 0 o `none`; ausente reproduce la primera si existe | ```html ``` El GLB debe contener esa animación. Un nombre inexistente se informa como error. Scale, rotation, position, animation y size cambian en vivo; src carga el archivo de reemplazo sin cerrar cámara. Espera `element.updateComplete` tras cambiar src. La jerarquía y los targets se sincronizan con `scene.refresh()`. Las opciones de cámara/mode/backend siguen requiriendo parar y reiniciar. [Interacción y cambios en vivo](interaction.md) detalla cancelación, errores y propiedad de recursos. ## Vídeo, gestos y reproducción avanzada Existe `webar-video` para MP4 y chroma, con width, orientation, muted, loop, autoplay, chroma-key y parámetros de color. Existe gestures en escena o contenido para arrastrar/girar, pellizcar/escalar y seleccionar. Los modelos aceptan animation-speed, animation-loop, animation-repetitions, animation-fade y animation-paused. Consulta [la referencia de interacción](interaction.md) para valores y ejemplos completos. ## SLAM con HTML ```html ``` El modo SLAM usa odometría WASM por defecto y no registra una imagen. El modelo aparece cuando exista una pose con plano verificado. Necesita textura y paralaje; la escala inicial es relativa, no se garantiza un metro físico. [Ejemplo ejecutable](../examples/declarative-slam.html). Para una transición desde una imagen, añade `slam` a una escena de imagen: aparece el botón **Activar SLAM** después de cargar. El backend es automático salvo que lo configures. En `mode="slam" backend="auto"` o `backend="webxr"`, también se presenta ese segundo botón para solicitar la sesión nativa desde un gesto, después de preparar cámara y recursos. No se solicita WebXR tras una descarga asíncrona ocultando el requisito de activación del navegador. Conservar contenido al perder imagen no es anclarlo. La misma biblioteca de tracking y las mismas limitaciones geométricas se aplican a esta capa. Lee [seguimiento espacial](spatial-tracking.md). ## Eventos y código propio ```js const scene=document.querySelector('webar-scene'); scene.addEventListener('targetFound',event=>console.log(event.detail.targetId)); scene.addEventListener('targetLost',event=>console.log(event.detail.retained)); scene.addEventListener('trackingQuality',event=>console.log(event.detail.issues)); scene.addEventListener('ar-error',event=>console.error(event.detail)); scene.addEventListener('ar-ready',()=>console.log(scene.engine,scene.app)); scene.addEventListener('ar-slam',event=>console.log(event.detail.placement)); ``` | Evento DOM | `detail` | | --- | --- | | `ar-ready` | `{app: ThreeARScene}`: contenido y configuración preparados; no confirma una detección o un plano | | `ar-error` | `Error` | | `ar-status` | `{message: string}` del motor | | `ar-pose` | `ARPose` real aceptada | | `ar-slam` | `SurfaceTransitionResult`; puede requerir escaneo o selección | | `ar-stopped` | `null` | | `modelLoaded` | `{animations: string[]}`; se emite desde webar-model y burbujea | | `videoLoaded` | `{width, height}`: metadata del MP4 listo | | `videoPlayBlocked` | Error de reproducción; se presenta un botón para autorizar | | `animationFinished` | `{name}`: clip terminado | | `ar-select`, `ar-gesture` | Selección y transformaciones por Pointer Events | | `contentUpdated` | null: jerarquía/targets sincronizados; no confirma detección | | `ar-placement` | `{state}`: idle/scanning/surface-found/placed/lost | | `targetFound`, `targetLost`, `targetVisibility`, `trackingQuality` | Mismo payload que los eventos tipados del motor | Los métodos de escena son `start()`, `stop()`, `refresh()` y `enableSLAM(options?)`; devuelven promesas. Scene.updateComplete expone la última sincronización encolada; para esperar un cambio de jerarquía que acabas de hacer llama a refresh explícitamente. `scene.app` y `scene.engine` son `null` antes de iniciar y tras parar. En esta capa `stop()` destruye la escena interna y libera render/modelos/cámara; el siguiente `start()` reconstruye desde el HTML. No es el mismo alcance que llamar solo a `engine.stop()`. También expone diagnostics, getDiagnosticsReport(), downloadDiagnostics(), placementState y confirmPlacement(). [Diagnóstico](diagnostics.md) y [colocación](placement.md) especifican cuándo están disponibles. Un diagnóstico no aprueba hardware y una superficie candidata no confirma colocación. `webar-model.model` expone `ThreeARModel` mientras existe. Sus métodos `playAnimation(nameOrIndex)` y `stopAnimations()` permiten controlar clips sin tocar el renderer. Los eventos de calidad, control de cámara y matrices avanzadas siguen disponibles en `scene.engine`. En una SPA se liberan los recursos al retirar la etiqueta de escena; también se detienen al abandonar la página. ## Script remoto y distribución Puedes alojar el archivo standalone en un servidor estático y referenciar una versión fija mediante HTTPS. El archivo incluye OpenCV, Draco y los avisos de licencia. El proyecto todavía no tiene una URL pública publicada. El standalone incluye **los recursos del motor** incluso si el script está alojado en otro dominio. Tus imágenes y modelos se resuelven respecto al HTML. Si eliges recursos externos para OpenCV o Draco, su servidor debe permitir las solicitudes CORS correspondientes. Esto se prueba con dos orígenes locales y permite publicar el mismo archivo desde un servidor compartido. El JS ya incluye Three.js, sus loaders y Meshopt. OpenCV/WASM y los decoders Draco continúan como recursos auxiliares descargados automáticamente, sin que el desarrollador tenga que declarar sus rutas en cada aplicación. No afirmamos que todos sus bytes estén embebidos en un único archivo. En ESM el registro es explícito: ```ts import {registerARComponents} from 'webar-facil'; registerARComponents({assetBase:new URL('/vendor/webar/',location.href)}); ``` La capa nativa registra cuatro etiquetas: webar-scene, webar-target, webar-model y webar-video. Para un framework o renderer existente siguen disponibles [A-Frame](aframe.md), [Three.js](threejs.md) y las clases de medios. `pixel-ratio` y `antialias` se leen al iniciar; para cambiarlos, detén la escena y vuelve a iniciarla. Afectan al renderer de Three.js. WebXR inmersivo administra su framebuffer por separado: estos ajustes no controlan su resolución. ## Arranque de cámara y contenido Al pulsar Iniciar AR, el componente pide cámara antes de cargar los modelos. Un GLB grande puede seguir descargándose con la vista previa activa; ar-ready llega cuando termina la preparación del contenido/targets. Un error de carga libera el stream. Detener permite cancelar una preparación pendiente. El botón nativo de la escena usa su propio controlador de inicio sin intermediarios de la herramienta de pruebas. --- Fuente: docs/interaction.md # Vídeo, gestos, animaciones y contenido en vivo Estas capacidades utilizan el mismo standalone del [inicio](getting-started.md). No necesitan otro renderer ni otra cámara. Los atributos de contenido se aplican en vivo; los ajustes de captura siguen requiriendo reiniciar. ## Recetas completas para copiar La [galería PRUEBAS](../ejemplos/) contiene 25 HTML independientes con recursos locales y comentarios. Los ejemplos [02](../examples/PRUEBAS/02-gestos-marcador.html) y [08](../examples/PRUEBAS/08-gestos-slam.html) añaden movimiento sobre el plano mediante Raycaster/Plane: el modo Mover es código de aplicación. El atributo gestures del SDK sigue controlando selección, giro y pellizco. No se añade un atributo de traslación inexistente ni se escribe en la matriz del anchor. Los ejemplos [04](../examples/PRUEBAS/04-texto-marcador.html) y [10](../examples/PRUEBAS/10-texto-slam.html) usan CanvasTexture/Sprite para mostrar texto real en la escena; [06](../examples/PRUEBAS/06-iluminacion-marcador.html) y [12](../examples/PRUEBAS/12-iluminacion-slam.html) mueven una luz direccional alrededor del contenido. Son extensiones mediante scene.app y Three.js incluido en el standalone, no nuevas etiquetas de la biblioteca. Se liberan sus callbacks, texturas y materiales al detener. ## MP4 y chroma con HTML ```html ``` `webar-video` puede ir dentro de un target o directamente en una escena SLAM. [Ejemplo completo con MP4 original incluido](../examples/declarative-video.html). | Atributo | Inicial y significado | | --- | --- | | `src` | URL del vídeo relativa al HTML | | `width` | 0.2; ancho positivo | | `orientation` | `flat` sobre XZ o `upright` vertical; inicial flat | | `muted`, `loop`, `autoplay` | true; escribir `false` para desactivar | | `chroma-key` | false; presente activa transparencia por color | | `key-color` | #00ff00 | | `similarity`, `smoothness`, `spill` | 0.12, 0.08, 0.3 | | `position`, `rotation`, `scale` | Igual que en modelos; rotación en grados | Similarity y spill van de 0 a 1; smoothness es >0 y <=1. Cambiarlos no recarga el vídeo. Tampoco lo hacen width, orientación o transformaciones. La compatibilidad MP4 depende del codec/dispositivo: prueba el archivo real. La biblioteca intenta autoplay cuando se permite. Si se bloquea, emite `videoPlayBlocked` y muestra **Reproducir vídeos**; ese botón llama a play dentro del gesto. `autoplay="false"` evita el intento y presenta ese botón; también puedes usar uno propio con `element.play().catch(...)`. `element.pause()` pausa; `element.video.videoElement` expone el elemento nativo mientras está cargado. ## Gestos optativos ```html ``` Solo actúan cuando se toca geometría visible mediante raycast. Arrastrar horizontalmente gira sobre Y; pellizcar escala; el giro de dos dedos también modifica Y. Un toque corto sin arrastre emite `ar-select`. No es traslación 3D ni reconocimiento de manos. `gestures` puede habilitarse por contenido o heredarse de escena; `gestures="false"` excluye un elemento. Los límites son magnitudes de escala: 0.1 y 100 por defecto. Se conserva la relación entre componentes XYZ. El estado se escribe en los atributos scale/rotation, por lo que una lectura posterior del HTML refleja el gesto. Con gestos activados, todo el visor (incluidos los márgenes del canvas) reserva el toque para AR: no desplaza ni amplía la página. Empieza tocando el modelo visible para seleccionarlo; después arrastra o añade el segundo dedo para escalar. Los botones y el resto del documento siguen siendo utilizables y desplazables. Al detener se restaura la interacción normal. No necesitas desactivar el zoom de toda la página. Los gestos usan Pointer Events sobre el visor, con raycast respecto al canvas en tracking de imagen y fallback óptico. Se deshabilitan durante WebXR inmersivo: allí la interacción requiere eventos XR específicos. Se liberan capturas y listeners al detener la escena. Para cambiar la habilitación/límites de elementos activos llama a `await scene.refresh()`. `ar-gesture.detail` contiene `{scale: [x,y,z], rotation: [x,y,z]}` en grados. `ar-select.detail` contiene `{element, object}`. [Ejemplo de gestos y cambios de modelo](../examples/declarative-interactive.html). ## Animaciones sin boilerplate ```html ``` | Atributo | Contrato | | --- | --- | | `animation` | Nombre exacto, índice desde 0 o none; ausente usa el primer clip si existe | | `animation-loop` | repeat (inicial), once o pingpong | | `animation-speed` | Positivo, inicial 1 | | `animation-repetitions` | Entero positivo; ausente repite sin límite salvo once | | `animation-fade` | Segundos de transición entre clips; 0 a 10, inicial 0 | | `animation-paused` | Pausa; quitarlo continúa | Once mantiene la última pose al terminar y emite `animationFinished` con `{name}`. Cambiar velocidad, pausa o bucle no reinicia el clip. Cambiar animation sí selecciona/reinicia, aplicando el crossfade configurado si corresponde. No lo hagas en cada pose del tracker. La etiqueta ofrece `playAnimation(nameOrIndex, options?)`, `pauseAnimations()`, `resumeAnimations()` y `stopAnimations()`. La clase ThreeARModel añade `configureAnimation(options)`, `animationState` y `onAnimationFinished(listener)`. En código, las opciones usan `loop`, `speed`, `repetitions`, `crossFadeSeconds` y `clampWhenFinished`. ## Cambiar archivos sin cerrar cámara ```js const model=document.querySelector('webar-model'); model.setAttribute('src','./otro.glb'); await model.updateComplete; model.setAttribute('size','0.8'); // Ajusta sin descargar otra vez. const video=document.querySelector('webar-video'); video.setAttribute('src','./otro.mp4'); await video.updateComplete; ``` El código presupone escena iniciada y elementos existentes. El último src solicitado prevalece; una carga anterior se cancela/descarta. Si falla descarga/formato/decodificación, se conserva el contenido anterior. El vídeo anterior sigue reproduciéndose mientras se prepara el nuevo; un nuevo archivo comienza una reproducción nueva. Las promesas rechazan en caso de error y también se informa `ar-error`. No se detiene cámara, renderer ni mapa por cambiar un modelo/vídeo. ## Añadir o retirar contenido y targets ```js const scene=document.querySelector('webar-scene'); const model=document.createElement('webar-model'); model.setAttribute('src','./otro.glb'); scene.querySelector('webar-target').append(model); await scene.refresh(); // Más adelante: model.remove(); await scene.refresh(); ``` Un MutationObserver sincroniza automáticamente jerarquía y atributos de targets. Para esperar explícitamente un cambio del DOM, usa `await scene.refresh()`; leer scene.updateComplete antes de que corra el observer podría devolver la operación anterior. El evento `contentUpdated` indica sincronización de la configuración, no una detección nueva. La adición/carga y retirada se serializan y liberan sus recursos. Cambiar src/physical-width/target-id de un target recompila o recarga sus datos sin reabrir la cámara; el seguimiento puede necesitar recuperar ese marcador. Las actualizaciones son por operación, no una transacción atómica de todos los targets. Usa IDs explícitos y conserva el límite de 8 imágenes diferentes. Se permite retirar todos y añadir otros después. Durante SLAM puedes cambiar modelos/vídeos, pero no los targets de imagen: cambia esos targets al volver al modo imagen. Para cambiar mode, backend, calibración, rutas de runtime o configuración de cámara sigue siendo necesario parar y reiniciar. Las actualizaciones de contenido no emiten ar-ready otra vez: usa modelLoaded, videoLoaded o contentUpdated. --- Fuente: docs/image-targets.md # Marcadores y compilación ## Flujo recomendado Indica la imagen original en el HTML del [inicio rápido](getting-started.md): ```html ``` La etiqueta se coloca dentro de `webar-scene`. `physical-width` está en metros y corresponde a `physicalWidthMeters` del motor. El mismo archivo que indicas en `src` debe mostrarse a la cámara; no se necesita un formulario de compilación ni cargar JSON. ## Compilación mediante API avanzada Para recibir el binario y la metadata desde tu propio código, utiliza un `engine` como en [integración programática](programmatic.md): ```ts const target=await engine.addImageTarget('cartel','./cartel.jpg',{ physicalWidthMeters:0.3, maxDimension:1024, }); console.log(target.metadata.quality); ``` Acepta JPG/PNG en URL o `File`. La compilación genera descriptores ORB y vistas adicionales en un Worker. Se realiza al cargar el target, no vuelve a analizarse la imagen de referencia en cada frame. Durante tracking se analizan los frames de cámara, se siguen puntos con flujo óptico y se recuperan correspondencias cuando hace falta. No necesitas bordes negros ni calibración manual. Una imagen a color se procesa por contraste y estructura; los cambios de color sin contraste de luminancia aportan pocas correspondencias. No todas las imágenes sirven igual: evita superficies lisas, texto repetido, rejillas, reflejos, transparencias y dibujos con muy pocos detalles. Reparte textura por toda la imagen. ## Límites de entrada - Archivos de imagen de hasta 32 MiB; guardas de decodificación de 16 megapíxeles. - `maxDimension`: 64 a 4096; 1024 como valor habitual. - Usa una imagen que represente exactamente lo que verá la cámara. No añadas márgenes o recortes diferentes al imprimir. - `physicalWidthMeters` es el ancho de la imagen física, sin contar un marco ajeno al target. Las imágenes pequeñas dentro del feed necesitan suficiente detalle después del reescalado de análisis. Aumentar `maxDimension` del compilador no recupera detalle que la cámara no está viendo. ## Resultado de compilación `ImageTargetResult` contiene: | Campo | Contenido | | --- | --- | | `buffer` | `ArrayBuffer` del marcador WART v2 (`.dat`) | | `metadata.format`, `version` | `WART`, `2` | | `width`, `height` | Dimensiones de la referencia procesada | | `physicalWidthMeters` | Escala indicada al compilar | | `extractorId` | Identificador de compatibilidad del descriptor | | `quality` | `featureCount`, `coverage`, `duplicateRatio`, `usable`, `warnings` | La evaluación de calidad ayuda a seleccionar imágenes; no certifica todas las condiciones de iluminación o inclinación. Para permitir al usuario descargar los resultados, consulta [recetas programáticas](recipes-programmatic.md). No hay caché persistente automática entre visitas: si quieres evitar recompilación, guarda el buffer en tu aplicación o distribuye un `.dat`. ## Compilar una vez y distribuir ```sh npm run compile:marker -- mi-imagen.jpg public/mi-target.dat 0.2 ``` El CLI genera el binario y su metadata JSON. También puedes abrir el compilador web (`tools/compile.html` en el repositorio). Para reutilizarlo: ```html ``` En la API programática, la operación equivalente es: ```ts await engine.addTarget('cartel','./mi-target.dat'); ``` El `.dat` contiene los datos necesarios para tracking. El `.json` es para inspección o herramientas, no se necesita al cargar. Este formato es propio: no acepta `.mind`, `.iset` o bases de otros SDKs. WART v2 incorpora cabecera, checksum y versión de extractor; no edites bytes ni omitas la validación para forzar compatibilidad. ## API de compilación sin motor `ImageCompiler.compile(bytesOrBlob, options)` y `compileRGBA({data,width,height}, options)` devuelven el binario. En esta API avanzada **sí es obligatorio** `physicalWidthMeters`. Para no bloquear UI con operaciones OpenCV utiliza `MarkerCompiler`, que gestiona el Worker; Node puede usar un adaptador de decodificación como el CLI incluido. Las opciones `simulate` y `extract` existen para pruebas o backends propios. Los descriptores simulados no son marcadores rastreables y no deben publicarse como resultados reales. --- Fuente: docs/models.md # Modelos GLB y animaciones ## Modelo en HTML Dentro de `webar-target` (imagen) o directamente en `webar-scene mode="slam"`: ```html ``` `size` ajusta la mayor dimensión inicial (0.16 por defecto). `scale` es un multiplicador uniforme o XYZ; `rotation` son grados XYZ, no radianes. En modo SLAM sin referencia física, las unidades del mapa son relativas. No se garantiza que size=1 represente un metro real. La animación inicial es el primer clip si existe. Para seleccionar uno: ```html ``` El nombre debe existir. También se admite un índice desde 0 o `animation="none"`. Para conocer los nombres, registra `scene.addEventListener('modelLoaded', event => console.log(event.detail.animations))` antes de pulsar Iniciar AR. Tras cargar, `document.querySelector('#modelo').setAttribute('animation','Bailar')` cambia el clip. Scale, rotation, position y size se aplican en vivo. Src reemplaza el GLB sin cerrar cámara; espera element.updateComplete y maneja su rechazo. Las animaciones admiten pausa, velocidad, once/pingpong, repeticiones y crossfade: [atributos de interacción](interaction.md). Se actualizan automáticamente, sin un loop propio. ## API programática avanzada El resto de esta guía describe `ThreeARModel` para renderer propio, ajustes que no expone la etiqueta o integración con vídeo/contenido compuesto. `app` debe ser una escena creada como en [integración programática](programmatic.md). ```ts const model=new ThreeARModel({ dracoDecoderPath:new URL('./decoders/draco/',document.baseURI), maxBytes:64*1024*1024, }); app.addContent('poster',model); await model.load('./modelo.glb',{fitSize:0.16,autoplay:true}); ``` `load()` acepta `Blob`/`File`, string o `URL` y devuelve `Promise`. Los GLB deben ser versión 2 y autocontenidos: geometría y texturas embebidas o data URI. El límite predeterminado es 64 MiB. No es un cargador de `.gltf` con recursos externos. `KHR_texture_basisu`/KTX2 no está implementado por este wrapper. ## Compresión - **GLB normal:** no requiere un decoder adicional. - **Draco:** sirve el directorio `decoders/draco/`; la etiqueta lo localiza junto al SDK. Con la clase programática configura la URL si difiere del valor predeterminado; se utiliza un Worker de decodificación. - **Meshopt:** decoder integrado; no se configura un directorio adicional. Compresión geométrica no significa que las texturas consuman poca GPU. Reduce dimensiones de texturas, cantidad de materiales y complejidad del modelo según el dispositivo. ## Ajuste del tamaño `fitSize` vale 0.16 por defecto: ajusta la mayor dimensión del modelo, centra XZ y coloca la base en Y=0 mediante un grupo padre, conservando las transformaciones y el rig del archivo. Usa `{fitSize:false}` para respetar las unidades del GLB. ```ts await model.load(file,{fitSize:false,autoplay:false}); console.log(model.animations.map(clip=>clip.name)); model.playAnimation('Caminar'); // También acepta índice; sin argumento, 0. model.stopAnimations(); ``` `playAnimation(nameOrIndex, options?)` reinicia la acción elegida. Con crossFadeSeconds hace transición desde la anterior; sin fade la detiene inmediatamente. Options acepta loop (repeat/once/pingpong), speed positivo, repetitions, crossFadeSeconds de 0 a 10 y clampWhenFinished. No lo llames en cada pose. `pauseAnimations()`/`resumeAnimations()` conservan el tiempo. `configureAnimation(options)` cambia parámetros sin reiniciar; `animationState` permite inspección. `onAnimationFinished(listener)` informa el clip terminado y devuelve desuscripción. `setSize(size)` reajusta la geometría cargada sin descargarla otra vez. Los wrappers declarativos usan estos métodos; el renderer propio conserva su obligación de llamar a update. ## Actualización y limpieza `ThreeARScene.addContent()` registra los modelos y vídeos presentes en el árbol en ese momento para su actualización y disposición. Con renderer propio debes llamar a `model.update(deltaSeconds)` desde tu loop. El método limita pasos mayores de 0.1 s para evitar saltos extremos. ```ts // Solo con renderer propio; ThreeARScene ya realiza esta actualización. renderer.setAnimationLoop(()=>{ model.update(clock.getDelta()); renderer.render(scene,camera); }); ``` `model.model` expone el `Group` cargado o `null`. `unload()` libera el contenido manteniendo el cargador reutilizable; `dispose()` es final y libera geometrías, texturas y decoder. Una nueva carga cancela la pendiente anterior; una carga fallida no reemplaza un modelo válido por un falso éxito. Si añades un modelo a un grupo después de que ese grupo fuese registrado con `addContent`, regístralo explícitamente mediante `addContent(id,model)` o administra tú su actualización y limpieza. No asumas que el contenedor observa cualquier mutación futura del árbol. --- Fuente: docs/multiple-targets.md # Varios marcadores Un motor puede seguir hasta **8 imágenes distintas** con un frame compartido y estados independientes. Dos copias del mismo cartel no se convierten en dos anclajes diferentes. ## HTML recomendado ```html ``` Usa el script standalone del [HTML completo](getting-started.md). La biblioteca inicia una sola cámara y carga los targets secuencialmente. Los eventos DOM incluyen `event.detail.targetId`; no crees un motor por etiqueta. Puedes cambiar jerarquía y retirar/añadir marcadores sin reiniciar cámara; modifica el DOM y espera `scene.refresh()`. La sincronización es por operación y los targets pueden necesitar redetección. Solo se permite cambiar targets en modo imagen; durante SLAM se permite cambiar el contenido. Para control directo de objetos y recursos sigue disponible la API avanzada. ## API programática avanzada Los siguientes fragmentos suponen un `app` de [integración programática](programmatic.md). ```ts const first=new ThreeARModel(); const second=new ThreeARModel(); app.addContent('a',first); app.addContent('b',second); await first.load('./producto-a.glb'); await second.load('./producto-b.glb'); await app.engine.startCamera(); await app.engine.addImageTarget('a','./imagen-a.jpg'); await app.engine.addImageTarget('b','./imagen-b.png'); app.engine.on('targetFound',({targetId})=>console.log('Apareció',targetId)); app.engine.setKeepContentOnLost(true,'a'); app.engine.setKeepContentOnLost(false,'b'); ``` Con varios targets usa `addImageTarget` o `addTarget`. `loadImageTarget` y `loadTarget` reemplazan todos los targets por `default` y no son operaciones aditivas. El ID distingue contenido, eventos y política de retención; no utiliza el nombre de archivo automáticamente. Para retirar uno: ```ts await app.engine.removeTarget('b'); app.removeContent(second); second.dispose(); ``` `removeContent()` solo separa el objeto y su actualización automática; debes disponerlo si no lo reutilizarás. `getAnchor(id)` devuelve el grupo de contenido, no una prueba de que el target esté cargado. El Worker comparte conversión gris, detección y extracción cuando corresponde; no se crea una cámara ni un Worker completo por imagen. El coste de matching y seguimiento aumenta con los targets. El límite de ocho es un límite de API, no una promesa de rendimiento con ocho marcadores en un móvil. En seguimiento espacial se selecciona un solo target para la transición. Los demás contenidos de imagen se ocultan; no se convierten todos en anclajes SLAM automáticamente. Ejemplo ejecutable: [multimarcador](../examples/multi-target.html). --- Fuente: docs/events.md # Eventos, continuidad y diagnóstico ## Detección sin parpadeo La detección y la presentación tienen ritmos distintos. Por defecto el motor tolera **500 ms** de pérdida antes de confirmar `targetLost`. Durante ese intervalo el contenido conserva su última transformación y las animaciones siguen avanzando. En HTML configúralo en la escena: ```html ``` Omite `keep-content-on-lost` para ocultar tras la pérdida confirmada. Un intervalo demasiado alto conserva contenido desactualizado durante más tiempo. El filtrado temporal se configura con `imageSmoothingMs` en la [API programática](api-engine.md); no existe un atributo de filtrado en esta versión. ## Eventos DOM en etiquetas ```js const scene=document.querySelector('webar-scene'); scene.addEventListener('targetFound',event=>console.log('Encontrado',event.detail.targetId)); scene.addEventListener('targetLost',event=>console.log('Perdido',event.detail.targetId)); scene.addEventListener('trackingQuality',event=>console.log(event.detail.issues)); scene.addEventListener('ar-error',event=>console.error(event.detail)); ``` Coloca este JavaScript opcional después del HTML de la escena. `ar-pose` lleva `ARPose` en `event.detail`; `ar-ready` significa contenido preparado, no detección confirmada. Todos los eventos están en [referencia declarativa](declarative.md). Los payloads de tracking de la tabla siguiente son los mismos en DOM y motor. ## Eventos tipados | Evento | Campos | Uso | | --- | --- | --- | | `targetFound` | `targetId`, `timestampMs`, `pose` | Primera detección o recuperación tras pérdida confirmada | | `targetLost` | `targetId`, `timestampMs`, `retained`, `reason` | Pérdida confirmada, retirada o parada | | `targetVisibility` | `targetId`, `visible`, `reason` | Mostrar/ocultar contenido en un renderer propio | | `trackingQuality` | `targetId`, `timestampMs`, `state`, `score`, `processingMs`, `issues`, `diagnosis` y métricas | Diagnóstico continuo | `targetLost.reason`: `lost`, `removed`, `stopped`. `targetVisibility.reason`: `tracked`, `recovering`, `lost`, `removed`, `stopped`, `policy`. `trackingQuality.state`: `tracking`, `recovering`, `lost`. `score` va de 0 a 1; es una puntuación heurística, no probabilidad calibrada. No se emite `targetFound` en cada frame. Una pérdida breve que se recupera durante la tolerancia no produce la pareja lost/found. No cargues el GLB ni reinicies el MP4 en estos eventos; cárgalos una vez y controla solo la lógica de tu aplicación. ```js const qualityListener=event=>{ const quality=event.detail; if(quality.issues.includes('small-target')) showHelp('Acerca la cámara'); else if(quality.issues.includes('blur')) showHelp('Enfoca y reduce movimiento'); else hideHelp(); }; scene.addEventListener('trackingQuality',qualityListener); // Al retirar este listener: scene.removeEventListener('trackingQuality',qualityListener); // showHelp/hideHelp son funciones de UI que implementa tu aplicación. ``` ## Retener contenido tras la pérdida El atributo de escena se lee al iniciar. Para cambiar política durante una sesión declarativa, después de `ar-ready`, usa `scene.engine.setKeepContentOnLost(enabled)`. Si quieres que la próxima sesión utilice la misma opción, actualiza también el atributo. [Recetas](recipes.md) muestra el flujo. Con la API programática y un `engine` ya inicializado: ```ts engine.setKeepContentOnLost(true); // Global y targets actuales. engine.setKeepContentOnLost(false,'producto'); // Solo un ID ya registrado. ``` La opción global define también la política inicial de targets futuros. Con retención activa se confirma la pérdida y `retained` es verdadero, pero el objeto permanece visible en su última transformación de cámara. **No es un anclaje en el mundo**: si mueves la cámara con el marcador ausente no hay información nueva para colocarlo correctamente en el entorno. Detener, retirar el target o suspender la cámara oculta el contenido aunque se haya activado retención. Cambiar la política no recarga el modelo o vídeo. `onPose` deja de emitir observaciones del target perdido; no asumas que una matriz retenida es una observación actual. ## Diagnósticos | `TrackingIssue` | Interpretación orientativa | | --- | --- | | `low-texture` | Contraste o distribución de estructura insuficientes | | `small-target` | Referencia muy pequeña dentro del frame analizado | | `blur` | Medida de nitidez baja | | `high-motion` | Desplazamiento alto entre frames | | `insufficient-matches` | Pocas correspondencias geométricas fiables | | `occlusion-or-out-of-view` | Pérdida compatible con oclusión o salida de vista | | `device-load` | Tiempo de procesamiento elevado | `diagnosis` es siempre `heuristic`. Las métricas incluyen `gradient`, `laplacianVariance`, `motionPixels`, `targetSizePx`, `featureCount`, `inliers` y `reprojectionErrorPx`. Durante seguimiento `featureCount` refleja soporte de correspondencias; durante búsqueda puede describir características del frame. No presentes el diagnóstico como identificación infalible de la causa. `onError` informa errores operativos; una simple ausencia del marcador es un estado de tracking, no un error fatal. `onMetrics` informa captura/carga: [rendimiento](camera-performance.md). --- Fuente: docs/recipes.md # Recetas con HTML declarativo Parte del [HTML completo de inicio](getting-started.md). Estos fragmentos reemplazan las etiquetas de contenido o añaden interacción opcional; conserva el script standalone y sus recursos. Las rutas `./` de esta guía suponen que tu HTML está junto al bundle en tu propio sitio. Para aprender con archivos completos, abre [PRUEBAS: 27 recetas](../ejemplos/). Incluye modelos GLB distintos, botones de sustitución, movimiento/giro/pellizco, texto editable, MP4 e iluminación, cada uno con marcador o SLAM óptico, persistencia de mapas y [colocación con mira](../examples/PRUEBAS/26-colocacion-con-mira.html). Los GLB, imagen y MP4 originales están en su carpeta assets. La escala de los ejemplos ópticos es relativa; sus tamaños iniciales son ajustables. Las recetas 14–25 amplían la galería con multimarcador, selección de clips, chroma ajustable, archivos del usuario, retención, picking de piezas, catálogo/colores, transición verificada a odometría, WebXR nativo, perfiles de rendimiento, contenido compuesto y A-Frame. Sus rutas fuente son ../dist/webar-facil.min.js y ../assets/marcador.jpg; [la guía de PRUEBAS](../examples/PRUEBAS/README.md) detalla los ocho recursos necesarios y el árbol de carpetas. Los archivos 01–13 conservan sus rutas previas. ## Una imagen y un modelo ```html ``` El modelo reproduce su primera animación si la tiene. La biblioteca añade los botones de cámara; no escribas otros listeners de inicio para el caso básico. ## Tamaño, orientación y clip ```html ``` `size` ajusta la mayor dimensión inicial; `scale` la multiplica. `rotation` son grados XYZ. `animation` debe existir en el GLB; usa `none` para detener o el índice `0` para seleccionar el primero si existe. En SLAM sin referencia física la escala es relativa. ## Control de tamaño y giro en vivo En tu escena, asigna `id="modelo"` a `webar-model`. Añade los controles **fuera de webar-scene**: la escena no presenta sus hijos como UI, sino como descripciones de contenido. ```html ``` Coloca este script después de las etiquetas y controles. Cambiar `scale`, `rotation`, `position` o `animation` se aplica en vivo y no modifica el anclaje. ## Ver los nombres de animación ```js const scene=document.querySelector('webar-scene'); scene.addEventListener('modelLoaded',event=>console.log(event.detail.animations)); // Una vez cargado, desde un botón o una acción de aplicación: // document.querySelector('#modelo').setAttribute('animation','Caminar'); ``` No selecciones la misma animación en cada pose: la reiniciarías. En [modelos](models.md) se muestra cómo seleccionarla por nombre o índice. ## Conservar contenido al perder imagen ```html ``` `loss-grace` es tolerancia en milisegundos. Retención mantiene la última transformación, no crea un anclaje espacial. Para una política en vivo, después de `ar-ready` puedes usar `scene.engine.setKeepContentOnLost(enabled)`. El atributo de escena se lee al iniciar; si quieres conservar la política al reiniciar, actualiza también ese atributo. ## SLAM y transición ```html ``` Inicia odometría óptica sin marcador. Para pasar desde una imagen, usa una escena de imagen y añade `slam`: la biblioteca muestra el botón de transición. [Seguimiento espacial](spatial-tracking.md) explica escaneo, WebXR y los resultados que todavía requieren selección o paralaje. ## Sustituir archivos sin reiniciar Con la escena ya registrada y fuera de otra operación: ```js async function replaceContent(){ const scene=document.querySelector('webar-scene'); const model=scene.querySelector('webar-model'); model.setAttribute('src','./otro-modelo.glb'); await model.updateComplete; scene.querySelector('webar-target').setAttribute('src','./otra-imagen.jpg'); await scene.refresh(); } // Desde un botón: void replaceContent().catch(console.error); ``` La cámara y el renderer se conservan. Los cambios de targets están disponibles en modo imagen; durante SLAM puedes cambiar modelos/vídeos. Size es inmediato. La jerarquía se sincroniza automáticamente y puedes esperar scene.refresh(). Captura, mode y backend sí requieren parar y reiniciar. Bloquea controles incompatibles mientras una actualización esté pendiente; [interacción](interaction.md) define las promesas, cancelación y errores. ## Código avanzado Selección de archivos `File`, descarga opcional de `.dat`/JSON, contenido Three.js compuesto, mapas en IndexedDB y gestión manual de una SPA se documentan en [recetas programáticas](recipes-programmatic.md). Vídeo/chroma, gestos y reproducción avanzada ya tienen [integración declarativa](interaction.md). --- Fuente: docs/spatial-tracking.md # Seguimiento espacial: WebXR y fallback El seguimiento de imágenes funciona en escritorio sin WebXR. La detección de superficies y el seguimiento espacial son modos adicionales que dependen del backend disponible. Ahora puedes añadir placement="manual" para recibir un indicador y confirmar una propuesta óptica reciente; [guía visual](placement.md). Para pruebas comparables de iPhone y Android, [diagnósticos](diagnostics.md) registra sesiones y observaciones sin subir datos. Las nuevas validaciones de distribución espacial y confirmación temporal de relocalizaciones débiles se describen en [odometría y deriva](odometry-validation.md). La corrección local periódica está disponible de forma experimental y desactivada por defecto: el benchmark no demuestra una mejora general de precisión. ## SLAM con etiquetas HTML Con el standalone del [inicio rápido](getting-started.md), utiliza: ```html ``` La biblioteca crea cámara y controles. `mode="slam"` utiliza odometría WASM por defecto, sin marcador. El objeto aparece cuando existen mapa y plano válidos; la escala inicial es relativa. [Ejemplo HTML completo](../examples/declarative-slam.html). ## Escala métrica óptica experimental El [ejemplo 28](../examples/PRUEBAS/28-escala-metrica.html) compara WebXR nativo con un modo óptico que estima metros usando la aceleración lineal del teléfono. Se activa mediante `EngineOptions.experimentalMetricScale`, pasando una instancia de `ExperimentalMetricScale`. Llama a `start()` en el gesto de usuario antes de iniciar `enableSLAM({backend:'optical-flow', placement:'hit-test'})`. El motor cancela la escucha al detenerse. El protocolo inicial apunta al iPhone 17 Pro en vertical. Valida cualquier otro teléfono antes de anunciar compatibilidad. El estimador compara desplazamientos visuales e inerciales en varias ventanas. Usa la orientación visual para expresar la aceleración en coordenadas del mapa y descarta ventanas con huecos, rotaciones bruscas o movimiento insuficiente. Los tiempos corresponden a la lectura del fotograma y la llegada del evento en la página. La alineación de hardware, el retardo exacto de captura y la calibración cámara–IMU siguen pendientes de validación. El giroscopio sirve para descartar giros rápidos. Esta versión estima un factor de escala y no corrige la trayectoria con fusión visual-inercial completa. Cuando las ventanas concuerdan, la pose anuncia `scale: 'metric'` y `scaleSource: 'experimental-inertial'`. El factor se fija hasta que cambia el mapa. La exportación conserva las unidades internas del mapa. Un mapa óptico relativo importado tiene que volver a estimar escala. La aplicación puede seguir mostrando contenido relativo mientras no exista una estimación. El ejemplo mantiene oculta la barra de 2 m hasta que también hay un plano y una pose reciente. Para probarlo, permite cámara y movimiento, apunta al suelo y realiza varios desplazamientos laterales con pausas. Mide por separado el error entre los extremos de la barra y una cinta, el desplazamiento al caminar alrededor y el salto al recuperar el seguimiento. El diagnóstico guarda intervalos y huecos de sensores, poses, ventanas y residuo. El residuo indica consistencia interna y no sustituye la medición física. El diagnóstico cuenta ventanas evaluadas, descartes por huecos, cobertura o movimiento insuficiente, e interrupciones por pérdida, salto o reinicio. También compara la misma trayectoria con desfases de −120 a +120 ms en pasos de 30 ms. Un resultado positivo indica que las muestras de movimiento que mejor encajan llegaron después del tiempo de vídeo. Es un desfase efectivo en el reloj de la página y su estimación queda vacía si el mejor ajuste es ambiguo. El diagnóstico conserva el último análisis de desfase de la sesión. El ajuste de escala vigente se invalida cuando se interrumpe su conjunto de ventanas. El desfase se registra para diagnóstico y no se aplica a las poses ni al criterio de colocación. Con la cámara abierta y el permiso de movimiento concedido, mueve el teléfono despacio hasta que se habilite **Iniciar prueba guiada**. La guía espera poses ópticas recientes y continuas. Si se pierde el seguimiento, espera a recuperarlo y repite el tramo. La secuencia dura 28 segundos sin repeticiones y empieza con cuatro segundos de reposo, que se repiten si se detecta movimiento. Luego pide tres recorridos de ida y vuelta de 20 cm, con arranque y frenado claros, y una pausa tras cada desplazamiento. Puedes medir dos posiciones separadas por 20 cm antes del ejercicio y marcar **Medí 20 cm entre las dos posiciones del teléfono**. La referencia queda en el informe y no participa en el ajuste. La comparación usa los desplazamientos completados y muestra el error absoluto cuando confirmaste la medida y hay escala del mismo mapa con poses cerca de ambos extremos. `guidedPhases` guarda la cobertura de poses y el desplazamiento visual en unidades del mapa, además de las muestras válidas y utilizables, pico absoluto, RMS y desviación estándar de aceleración por eje. La desviación estándar de la fase inicial mide el ruido y el movimiento involuntario durante el reposo. Las ventanas integradas se atribuyen a una fase cuando caben enteras en ella. El informe también agrega, por eje, la aceleración RMS del teléfono y del mapa, la integral neta RMS y la suma media de las contribuciones absolutas. Guarda resultados de la sesión y del ejercicio, incluyendo un resumen de las ventanas descartadas por señal débil. Una integral neta baja con contribuciones absolutas altas apunta a cancelación dentro de la ventana. El informe contiene estadísticas agregadas y no almacena muestras crudas. La odometría conserva temporalmente la pose si los puntos repartidos por la imagen permanecen casi inmóviles y su reproyección sigue siendo compatible con la pose anterior. Compara con referencias fijas durante el reposo para detectar desplazamientos pequeños acumulados. El ejemplo 28 informa cuántas poses se conservaron en reposo, la excursión máxima y el error al regresar a la posición inicial en cada ida y vuelta con cobertura. Esas distancias siguen en unidades del mapa hasta que se valida una escala. El ejemplo también solicita al Worker fijar una referencia de salida y compararla con el vídeo al final de la pausa de retorno. `checkSLAMVisualReference('capture', id)` y `checkSLAMVisualReference('verify', id)` devuelven evidencia del emparejamiento cuando hay soporte suficiente. El informe muestra el desplazamiento mediano de puntos en píxeles, el retorno estimado mediante la referencia y la diferencia con la trayectoria seguida por flujo óptico. Los dos últimos valores usan unidades del mapa. La verificación vuelve a observar la imagen, aunque comparte la geometría del mapa. El Worker conserva una referencia temporal con descriptores y puntos; el informe solo guarda estadísticas y la verificación no corrige la pose en vivo. ## Pasar desde una imagen ```html ``` Tras iniciar y detectar la imagen, pulsa el botón **Activar SLAM** que crea la biblioteca. `backend="optical-flow"` permite probar la odometría; `backend="auto"` intenta el camino nativo disponible y admite fallback. Mantén marcador y entorno con textura en vista durante la inicialización. Para solicitar WebXR sin imagen puedes usar `mode="slam" backend="auto"` o `backend="webxr"`. Tras preparar recursos se muestra un segundo botón para solicitar la sesión desde un gesto del usuario. Comprueba soporte real; `webxr` no fuerza una capacidad inexistente. La transición declarativa publica `ar-slam` con el resultado en `event.detail`; `ar-status` indica pasos de escaneo y `ar-pose` lleva las poses aceptadas. `ar-ready` solo confirma preparación. La tabla siguiente se aplica también a los resultados declarativos. ## Activación programática avanzada Si necesitas controlar botones y motor, [slam-iphone.html](../examples/slam-iphone.html) conserva el ejemplo programático completo. El siguiente fragmento supone una aplicación de [integración programática](programmatic.md), con `ThreeARScene`, target `poster` y botón `#slam`: ```js document.querySelector('#slam').onclick=async()=>{ try{ const result=await app.engine.enableSLAM({ targetId:'poster',backend:'auto',placement:'auto', initializationTimeoutMs:12000, }); console.log(result.backend,result.placement); }catch(error){console.error(error);} }; app.engine.onStatus(message=>console.log('Estado espacial:',message)); ``` Invoca el método directamente en el click. No hagas antes una descarga ni esperes otro frame dentro de ese handler: la sesión XR puede necesitar la activación transitoria del usuario. Registra listeners y carga recursos previamente. ## Resultados de la transición | `placement` devuelto | Qué hacer | | --- | --- | | `preserved` | El contenido ya se transfirió a un marco espacial compatible | | `scan-marker` | Vuelve a mostrar el marcador dentro de XR para alinear los marcos | | `select-surface` | Espera un hit-test válido y selecciona la superficie | | `scan-floor` | Mueve la cámara con traslación sobre una zona con textura y paralaje | La promesa puede resolverse antes de que exista una colocación final. Observa `onPose`/`onSurface`, estados y errores; no muestres “anclado” solo porque `enableSLAM()` resolvió. `placement:'preserve'` exige una observación reciente del marcador (menos de 750 ms) y una alineación real. Una matriz que se quedó visible por retención no basta. `placement:'hit-test'` permite recolocar explícitamente; no conserva necesariamente la posición anterior. En modo automático puede pedirse escaneo o selección. ## Backend WebXR Comprueba soporte con `await SLAMManager.isSupported()`. Internamente usa `navigator.xr.isSessionSupported('immersive-ar')`. La sesión requiere `hit-test`, solicita `dom-overlay` si hay raíz de overlay y utiliza `anchors` cuando está disponible. Una pose de hit-test no es una malla del entorno ni garantiza que sea el suelo. Para preservar la posición desde getUserMedia hace falta un marco de cámara común: el motor puede usar `camera-access`/Raw Camera Access para alinear la imagen dentro de XR. Esta capacidad no está disponible en todos los navegadores que soportan hit-test. No puedes copiar una matriz de webcam al espacio XR y llamarlo continuidad. Si no hay alineación, la operación debe pedir recolocación o fallar según la opción. El renderer debe integrarse con la sesión; `ThreeARScene` ya lo hace. `backend:'webxr'` exige ese backend; no interpreta la ausencia como permiso para inventar un anclaje. Una denegación explícita de permisos no debe resolverse con reintentos automáticos para volver a abrir la cámara. ## Backend óptico `backend:'auto'` intenta el camino apropiado y permite odometría WASM cuando XR no está disponible. Para probar explícitamente el fallback: `backend:'optical-flow'`. Se ejecuta en Worker con Lucas–Kanade, correspondencias, triangulación, mapa local y relocalización. Si existe una observación útil del marcador, puede sembrar el mapa y su escala de referencia. Aun así necesita geometría del entorno para mantener la cámara al retirar el marcador. Sin esa semilla, la escala es relativa y se requiere suficiente traslación y paralaje; rotar el teléfono sin desplazarse no revela profundidad. El ROI inferior propone puntos candidatos de suelo. Solo se entrega un plano cuando hay verificación geométrica; no existe reconocimiento semántico infalible del piso, escala métrica absoluta sin referencia, ni oclusión automática del contenido con el entorno. La deriva, la pérdida en superficies lisas y las limitaciones monoculares siguen presentes. ## iPhone y pruebas físicas Sirve por HTTPS el [ejemplo SLAM declarativo](../examples/declarative-slam.html), acepta cámara y desplázala lentamente sobre un entorno con textura para dar paralaje. Para sembrar desde imagen, utiliza el HTML de transición anterior y pulsa Activar SLAM tras detectar. Si no hay mapa utilizable el motor debe declarar pérdida en lugar de congelar una falsa estimación. No presupongas disponibilidad de `immersive-ar` por el nombre del navegador: compruébala en ejecución. WebKit de escritorio automatizado no certifica Safari en iPhone. La matriz de validación pendiente está en [pruebas físicas](device-validation.md). ## Volver a seguimiento de imágenes Si empezaste con una escena de imagen y su botón SLAM, `await scene.stop(); await scene.start();` reconstruye la configuración HTML original y vuelve a cargar la imagen. Si tu HTML utiliza `mode="slam"`, debes cambiar su configuración y añadir targets válidos antes de reiniciar para obtener una escena de imagen. Con API programática el flujo es `await engine.stop()`, `await engine.startCamera()` y volver a cargar targets. No existe `disableSLAM()` público que conserve todos los targets implícitamente. --- Fuente: docs/placement.md # Guía visual y confirmación de colocación Después de colocar contenido en el ejemplo 30, puedes moverte con normalidad. El mapa incorpora automáticamente perspectivas del recorrido. Si se pierde el seguimiento, el ejemplo intenta recuperar la colocación original. «Otra zona» inicia un mapa nuevo. El [ejemplo 30](../examples/PRUEBAS/30-explorar-superficies.html) permite elegir con un toque dónde inspeccionar el plano del SLAM óptico. La mira amarilla señala la zona en evaluación, los puntos azules resaltan contraste en la cámara y los amarillos muestran referencias del mapa cercanas al plano. El área verde delimita la región respaldada por el mapa. Toca el área o pulsa «Usar zona» para previsualizar un GLB o MP4 y confirma con «Colocar». Pellizca para ampliar la cuadrícula dentro del área verde o para escalar el contenido. La guía distingue escaneo, superficie candidata, contenido colocado y pérdida. No sustituye la evidencia geométrica ni convierte el hit-test en reconocimiento semántico de suelo. ## Coincidencias y recuperación en el ejemplo 30 El SLAM óptico compara cada fotograma de recuperación con hasta **50 vistas guardadas**. Evalúa cada vista por separado. Una coincidencia visual vincula dos descriptores ORB parecidos. Un punto coherente con la geometría (*inlier*) es una coincidencia que también encaja con la posición y orientación estimadas de la cámara. | Mensaje del ejemplo | Qué significa | | --- | --- | | No se encontraron coincidencias | Ninguna vista guardada produjo coincidencias. | | Se encontraron pocas coincidencias | La mejor vista produjo entre 1 y 14. El motor necesita 15 para intentar estimar la posición. | | Hay coincidencias, pero aún no bastan | Alguna vista produjo 15 o más, pero ninguna superó la verificación geométrica. | | Posición candidata encontrada | La estimación superó la verificación básica y necesita una segunda observación compatible. | La **verificación básica** exige al menos 15 puntos geométricamente coherentes, que representen al menos el 40 % de las coincidencias de esa vista y ocupen al menos 4 celdas de la imagen. El rectángulo que abarca esos puntos debe cubrir al menos el 8 % del fotograma analizado. La **confirmación en un solo fotograma** exige al menos 30 puntos coherentes, un 65 % de coincidencias coherentes, 6 celdas y un error de reproyección inferior a 1,5 píxeles. Si la estimación solo cumple la verificación básica, el motor requiere dos observaciones compatibles. Una mesa con poca textura puede ofrecer más referencias si encuadras también sus bordes y alrededores. ## Odometría óptica con confirmación ```html ``` Carga el standalone como en [inicio](getting-started.md). [Ejemplo completo](../examples/declarative-placement.html), para iPhone o Android con cámara/WebGL disponibles. 1. Pulsa Iniciar AR y desplaza la cámara sobre un entorno con textura y paralaje. 2. Espera una pose de superficie verificada: aparecen una cuadrícula y un anillo de propuesta. 3. Pulsa **Colocar aquí** cuando se habilite. Confirmar solo cambia presentación; no crea una matriz identidad ni modifica el anclaje calculado. 4. Si se pierde seguimiento, se oculta el indicador/contenido. Vuelve hacia una zona observada; una nueva pose válida puede recuperar el contenido. No se habilita confirmación mientras se inicializa, cambia de backend o falta una pose reciente. Se usa el timestamp de captura, no el momento de llegada del Worker: una respuesta tardía no renueva su frescura. El límite de confirmación es 700 ms; la guía declara pérdida tras más de un segundo sin datos válidos. Eso no garantiza precisión física. La escala del fallback sin referencia sigue siendo relativa y puede haber deriva. El anillo representa la propuesta del motor, no una malla reconstruida. En una transición sembrada desde imagen puede corresponder a su posición conservada, no a un suelo reconocido. Solo el target seleccionado se hace visible al confirmar; los demás no se convierten en anclajes. ## Referencias visuales en la escena `` muestra un marco de encuadre al buscar imágenes. Se oculta cuando reconoce cualquier marcador, permanece oculto durante la tolerancia de pérdida y reaparece si se pierden todos los marcadores. El marco deja pasar los gestos y respeta la preferencia de movimiento reducido. Para ocultarlo usa `scan-guide="false"`. En odometría, el motor consolida tres estimaciones recientes compatibles de la superficie antes de fijar el ancla, incluso si una observación intermedia resulta insuficiente. La cuadrícula se dibuja sobre ese plano y su giro dentro del plano toma como referencia la cámara en el momento de detectarlo. En WebXR se centra y orienta con la propuesta del hit-test. Su tamaño es una referencia visual, sin estimación de los bordes físicos. En colocación manual permanece hasta confirmar o perder la propuesta. En colocación óptica automática aparece brevemente. `surface-grid="false"` oculta la cuadrícula y conserva el anillo de colocación. ## Guía de escaneo en odometría óptica `engine.on('scanGuidance', listener)` y el evento declarativo `ar-scan-guidance` publican `{hint, timestampMs}` cuando cambia la indicación. El componente explica cuándo buscar más textura (`low-texture`), desplazarse lateralmente (`move-sideways`), continuar construyendo el mapa (`map-initializing`), encuadrar una superficie con más evidencia (`plane-unverified`) o volver a una zona observada (`return-to-map`). `surface-ready` indica que existe una pose para el ancla. Son indicaciones derivadas del estimador óptico. El flujo óptico ajusta su cupo de puntos cuando el coste se mantiene alto. Selecciona puntos de distintas zonas y reserva parte del cupo para ampliar el mapa, manteniendo los criterios geométricos de pose, triangulación y plano. La ventana inicial de flujo óptico se elige con el desplazamiento medido en la captura anterior y el intervalo entre capturas. Si la pasada normal deja pocos puntos y el frame conserva textura, se permite un reintento con ventana amplia. Si se pierden las referencias iniciales, la odometría repone esquinas y vuelve a intentar construir el mapa. Una vez colocado el objeto, una relocalización en el mismo mapa recupera el ancla confirmada. Un reinicio de mapa invalida la confirmación y exige una colocación nueva. ## Estados y eventos `scene.placementState` es idle, scanning, surface-found, placed o lost. El evento DOM ar-placement lleva `{state}` en detail. Puedes llamar a `scene.confirmPlacement()` desde un botón propio; rechaza si la colocación no es válida/reciente. Por defecto placement es auto para conservar las aplicaciones anteriores. En ese modo una pose aceptada coloca el contenido sin este botón. Cambiar la política requiere reiniciar la escena. ## WebXR nativo En backend webxr/auto, el hit-test muestra la propuesta. La selección se hace mediante el evento select de la sesión XR; el botón DOM Colocar aquí se reserva al fallback óptico. La pose nativa posterior confirma colocación. No se promete que los controles DOM estén disponibles dentro de todos los modos inmersivos. Con backend auto, un rechazo de la configuración de sesión activa la odometría por cámara si está habilitada. Un rechazo de permisos se comunica a la aplicación. Con backend webxr explícito, el error se devuelve al llamador. El ejemplo 22 permite comprobar el modo automático o forzar el nativo con `?backend=webxr`. Si una transición automática desde imagen no consigue conservar la pose, vuelve a buscar superficie. Una solicitud explícita con `placement: "preserve"` devuelve un error y permite reintentar desde el marcador. Comprueba las capacidades en ejecución. Android no implica que WebXR esté disponible y iPhone no implica que todos los navegadores tengan las mismas capacidades. Si no hay soporte nativo, usa el escenario óptico; no marques una prueba nativa como aprobada. En móviles los controles están arriba del visor, antes de la imagen de cámara. Si el ejemplo incluye un formulario anterior, desplázate hasta el inicio de la escena. Con `gestures`, toca el modelo y arrastra para girar o pellizca para escalar; los márgenes del visor tampoco desplazan la página. En tools/device-tests.html, Probar SLAM sin marcador configura e inicia el modo óptico directamente; no requiere imagen. La opción Imagen → odometría sí requiere imagen para la transición. En WebXR, el botón Activar SLAM solicita la sesión nativa con gesto del usuario, y la selección de superficie se realiza dentro de XR. --- Fuente: docs/diagnostics.md # Diagnóstico local para iPhone y Android La misma API sirve para ambos sistemas. El SDK no decide capacidades por user-agent: se consulta el soporte disponible. Modelo, SO y navegador exactos se declaran al ejecutar el protocolo físico. ## Recoger los 28 ejemplos automáticamente en XAMPP La carpeta del proyecto contiene el código fuente, los ejemplos, la documentación y las pruebas automatizadas. Corrige ahí cualquier fallo y ejecuta `npm run build` para generar `dist/`. Usa `C:/xampp/htdocs/2` como instalación de pruebas para servir el SDK por HTTPS y recibir informes de los teléfonos. Al actualizar esa instalación, copia la distribución generada y conserva las rutas de recursos y la configuración del recolector. El recolector local opcional evita descargar JSON en cada teléfono. Se instala en la copia estática de Examples y conserva las rutas existentes: ```sh node scripts/install-local-diagnostics.mjs --site C:/xampp/htdocs/2 node scripts/install-local-diagnostics.mjs --site C:/xampp/htdocs/2 --apply ``` La primera llamada prepara la instalación; --apply respalda los HTML e instala diagnostics/ y Examples/local-reports.js. Requiere PHP en Apache. La biblioteca de producción no envía telemetría por incluir el SDK; el cliente local solo está conectado a esta instalación de desarrollo. En el teléfono abre https://192.168.1.16/2/diagnostics/mobile.html (sustituye el host por el que ya utilizas). Pon una etiqueta para distinguir iPhone y Android. Abre un ejemplo e inicia cámara; el panel inferior confirma los envíos. Guardar y siguiente detiene y avanza. Los gestos, el movimiento de cámara y la colocación SLAM siguen siendo acciones de la prueba física. En el computador abre http://localhost/2/diagnostics/. Los snapshots se envían al mismo origen HTTPS de cada ejemplo al iniciar, cada 15 segundos y al detener/salir. No se utiliza localhost como destino desde el teléfono. PHP guarda un archivo por sesión y actualiza su última secuencia; un paquete retrasado no sustituye una medición posterior. El panel permite conservar el historial o mostrar el último intento por equipo/ejemplo. Los JSON quedan en reports/local-devices/data// del repositorio, fuera de htdocs y excluidos de Git. El snapshot está dentro de report, acompañado por client, example, sequence y receivedAt. El asistente puede leerlos cuando se le pida; el recolector no inicia chats ni análisis por sí mismo. No incluye imágenes, vídeo, matrices de pose ni mapas. La cola del navegador conserva hasta 16 sesiones pendientes y aproximadamente 700 KB, y reintenta al recuperar red o abrir otro ejemplo. sendBeacon al salir no garantiza recepción: se mantiene la copia hasta obtener confirmación. No se promete recuperar datos si se cierra el navegador sin aviso o se borra su almacenamiento. El servidor limita los envíos a 256 KB y el historial a 2000 sesiones, sin borrar resultados automáticamente. El endpoint es solo para red privada/loopback, verifica origen y token de instalación. Es una herramienta de laboratorio local. No requiere otro puerto, WebSocket ni servicio de terceros. En 25 se usa el mismo engine A-Frame con un ARDiagnostics propio; sus contadores de render se muestrean, sin inventar intervalos de render no observados. Las verificaciones automatizadas se identifican como execution=automated y se ocultan por defecto en el panel. Un informe recibido no demuestra aprobación física, precisión ni 60 FPS. El marcador de prueba físico es una declaración opcional del operador, no una certificación. ## Un atributo y un botón ```html ``` Con el standalone actualizado, aparece **Descargar diagnóstico** al iniciar. El informe sigue disponible después de Detener y se sustituye al iniciar una sesión nueva. No se habilita por defecto: la instrumentación es opt-in y puede añadir coste. Para una aplicación declarativa: ```js const scene=document.querySelector('webar-scene'); scene.addEventListener('ar-ready',()=>{ scene.diagnostics.setContext({platform:'android',execution:'physical-user-reported', deviceModel:'Modelo declarado por operador',osVersion:'Versión exacta',browserVersion:'Versión exacta',scenario:'image'}); }); // Desde un botón: // scene.downloadDiagnostics(); // const report=scene.getDiagnosticsReport(); ``` No copies el ejemplo de plataforma o ejecución sin adaptarlo al dispositivo real. `platform` admite ios, android, desktop u other; execution admite unspecified, automated o physical-user-reported. No significa certificación independiente. ## Qué contiene - Versión SDK y build determinista, fecha y duración de registro. - Configuración inicial declarada: modo/backend, resolución de análisis, objetivo FPS, tolerancia/retención, colocación y pixel ratio. No es un historial completo de cambios dinámicos. - User-agent, contexto seguro, existencia de APIs y resultado de la consulta immersive-ar (true/false/null si no pudo comprobarse). - Última resolución/frecuencia efectiva getUserMedia y opciones disponibles. No representa necesariamente la cámara del compositor XR. No se incluyen deviceId ni groupId. - Tiempos separados de lectura de cámara y consumo posterior del frame (`metrics.readbackMs` y `metrics.consumerMs`). En odometría local integrada, `odometry.stages` separa gris, esquinas, flujo óptico, inicialización, pose, mapa, keyframes, plano, relocalización y total del Worker. Cada etapa cuenta solo los frames donde se ejecutó. `planeFitMs` mide los intentos de ajuste del plano y `planeConsensusMs` cuenta los casos que produjeron una estimación candidata para comparar con las anteriores. `flowMs` incluye la pasada principal y, si se realiza, el reintento. `flowNormalMs` mide la pasada principal con ventana normal, `flowWideMs` mide la pasada principal con ventana amplia y `flowRetryMs` mide solo el reintento con esa ventana. - Poses por backend, escala y origen de proyección; primera pose desde el inicio del registro, relocalizaciones y correcciones experimentales. - Found/lost por ID y recuperación desde una pérdida confirmada; los finales por stop/removed se cuentan por separado. - Tiempos de procesamiento/lectura y del loop de render; estadísticas de cambios entre poses y reproyección. - Diagnósticos heurísticos, cronología acotada y observaciones manuales. La primera pose incluye la preparación que ocurrió durante ese registro: no es un benchmark puro de detección. Recuperación se mide desde targetLost confirmado, excluyendo parte de la tolerancia previa. Los cambios entre poses **no equivalen a jitter o deriva física**: incluyen movimiento de cámara/contenido y dependen de escala y coordenadas. El loop mide intervalos de callbacks, no finalización de GPU. Las muestras de trackingQuality son por target, no un contador de frames de render. ProcessingMs incluye el tiempo del consumidor de frames, no una medición exclusiva de CPU WASM. Para comparaciones físicas mantén configuración y contenido equivalentes. No se persisten ni exportan frames, mapas, matrices, URLs de contenido o IDs de cámara. Para calcular pasos se conserva temporalmente la pose anterior de hasta 16 IDs/backends en memoria, y se descarta al finalizar. Las URLs y rutas de errores se redactan. Los campos escritos por el operador y el user-agent sí forman parte del informe; revisa el JSON antes de compartirlo. No hay subida automática. ## Límites de memoria y estadísticas Hasta 1024 muestras recientes por métrica por defecto (configurable de 16 a 8192), 256 eventos, 128 observaciones y registros acotados de targets/recuperaciones. P50/p95 describen la ventana reciente; media/min/max/count agregan las muestras aceptadas durante toda la sesión. No interpretes un percentile reciente como toda una prueba de varios minutos. ## API programática ```ts import {ARDiagnostics} from 'webar-facil/core'; const diagnostics=new ARDiagnostics(engine,{sampleLimit:1024, context:{platform:'ios',execution:'unspecified',scenario:'image'}}); await diagnostics.probeCapabilities(); // No solicita una sesión XR ni permisos adicionales. // Conecta desde tu propio render loop, si quieres medir sus callbacks: // diagnostics.recordRenderFrame(); diagnostics.mark('test-start','stationary'); diagnostics.observe('stationary','pending','Pendiente de evaluar por operador'); const report=diagnostics.snapshot(); diagnostics.stop(); // Desuscribe y conserva el informe; no detiene el motor. ``` `engine` debe existir. Con ThreeARScene puedes llamar recordRenderFrame desde onRender y retirar esa suscripción al cerrar. Las etiquetas ya lo hacen. Una instancia de ARDiagnostics registra una sesión; crea otra si deseas reiniciar estadísticas. ## Herramienta de pruebas y comparación Abre pruebas iPhone/Android (`tools/device-tests.html` en el repositorio) mediante HTTPS en cada teléfono. Hay escenarios de imagen, odometría, transición y WebXR. Introduce versiones, realiza cada paso y marca resultado manualmente; el temporizador no aprueba nada. Se exportan hasta 16 sesiones por página y resultados por escenario como `webar-device-tests.json`. Context.scenario identifica modo y formato (por ejemplo image:draco). Completa metadatos antes de iniciar: las sesiones cerradas no se reetiquetan al elegir otra configuración. Utiliza una página/suite diferente por dispositivo/build/SO/navegador; el comparador rechaza mezclas. Automatización detectada por webdriver se etiqueta automated, incluso si se marca la casilla de dispositivo físico. Eso no identifica hardware de forma infalible: evita confundir emulación con aceptación física. En el repositorio, con los JSON descargados: ```sh npm run reports:summarize -- informe-iphone.json informe-android.json ``` Se escribe artifacts/device-validation-summary.json (puedes elegir otra ruta con --output). El resumen distingue pendientes, fallos, límites y aprobaciones **declaradas por operador**; certification siempre es false. Los escenarios marcados pass requieren sesiones con poses del backend correspondiente, o quedan como missing-scenario-evidence. Esto no demuestra precisión física. Una plataforma sin evidencia sigue pendiente. No se agrupan como si fueran iguales builds, modelos o versiones diferentes. Consulta [protocolo físico](device-validation.md), [colocación guiada](placement.md) y [comparación de odometría](odometry-validation.md). ## Contadores de renderizado En escenas declarativas con `diagnostics`, `report.renderer` incluye latest (drawCalls, triangles, geometries, textures, drawingBufferWidth, drawingBufferHeight, pixelRatio) y peakDrawCalls/peakTriangles. Se muestrea el render anterior sin esperar a la GPU. No son bytes de VRAM ni tiempos GPU. Antes del primer render latest puede ser null. `configuration.antialias` y renderPixelRatio indican los ajustes efectivos. Ver [comparación de rendimiento de modelos](camera-performance.md). El núcleo sin renderer puede aportar estos contadores mediante `diagnostics.recordRenderer(...)`; no inspecciona escenas ni modelos automáticamente. ## Perfil del tracker de imágenes El standalone actualizado añade `report.tracking` y `metrics.latestCamera.tracking` para separar el coste interno del Worker de los tiempos de cámara/render. No necesitas modificar el HTML: activa `diagnostics`. `tools/device-tests.html` muestra Worker, LK, matching y contadores de rutas. | `tracking.stages` | Qué mide | | --- | --- | | grayMs | Copia al heap y conversión RGBA a gris | | supportMs | Evaluación de distribución y calidad del soporte LK/caché | | qualityMs | Muestreo de textura/desenfoque | | opticalFlowMs | LK piramidal forward/backward y sus filtros | | flowValidationMs | Consistencia geométrica de correspondencias LK | | orbMs | Extracción ORB; compartida entre targets del mismo frame | | matchingMs | Emparejamiento binario, incluidas caché y búsqueda completa si ambas se intentan | | homographyMs | Homografía RANSAC de las coincidencias ORB | | poseMs | Resoluciones de pose de LK y ORB | | workerMs | Tiempo total de procesamiento dentro del Worker, antes de postMessage | | roundTripMs | Envío hasta reanudación en Tracker, en el reloj del hilo principal | | transportAndSchedulingMs | Máximo entre cero y roundTripMs - workerMs; incluye transporte y planificación, no es coste puro de copiar | Cada etapa tiene count/mean/p50/p95 con el mismo límite de muestras del diagnóstico. Los tiempos de etapas no usadas son cero. La media/p95 es por frame perfilado, no solo por invocación de esa función. workerMs contiene las etapas y preparación adicional: no sumes workerMs a sus componentes ni roundTripMs al total. processingMs de cámara incluye lectura y consumo completo del frame; no equivale a tiempo CPU de visión puro. `tracking.frames` cuenta frames perfilados. `tracking.paths` acumula flowAttempts, flowAccepted, redetections, orbExtractions, flowInputPoints, cachedAttempts, fullDetections y matchingQueryDescriptors. Las rutas no son excluyentes: un mismo target puede usar LK, intentar caché y terminar en búsqueda global. flowInputPoints y matchingQueryDescriptors suman puntos/descriptores procesados, no objetos únicos. En lotes, las etapas se agregan entre todos los targets y la extracción compartida se cuenta una sola vez. `Tracker.latestProfile` expone una copia del último frame procesado; es null antes de procesar, tras descarte por backpressure o al cargar otra sesión/target. Los resultados FRAME/FRAME_BATCH incluyen profile cuando está disponible; Workers de prueba anteriores pueden omitirlo. Los perfiles se validan y no contienen imágenes, coordenadas ni matrices. El módulo de imagen proporciona este perfil. El backend de odometría integrado informa sus propias etapas en `odometry.stages`. Una caducidad de la pose para colocación se registra como `placement-stale` y una pérdida declarada por el estimador como `odometry-lost`. ## Motivos de redetección Los nuevos contadores tracking.paths incluyen flowReliable, flowSparseReliable (12–23 inliers fiables), redetectMissing, redetectWeakSupport, redetectDeadline, redetectVerification y globalDeferred. weakDistribution, weakReprojection y weakRetention detallan la degradación; cacheConflicts registra desacuerdos entre dos estimaciones del mismo frame. globalDeferred significa conservar LK fiable tras una verificación de caché fallida, con revisión global todavía pendiente. No significa que una pose se haya confirmado mediante descriptores. Los contadores no son excluyentes: puede coincidir un plazo vencido con falta de soporte. Un soporte fiable es una decisión heurística comprobada geométricamente, no una medida de error físico. Los perfiles de Workers y SDK deben pertenecer a la misma distribución. tools/device-tests.html muestra seguimiento fiable con pocos puntos para comprobar que el cambio está activo. ## Backend de matching tracking.paths.exactMatchingCalls cuenta llamadas al kernel WASM exacto, opencvMatchingCalls llamadas a la alternativa OpenCV y emptyMatchingCalls llamadas omitidas por no haber al menos dos descriptores en cada entrada. Una redetección puede intentar caché y pool completo, así que no son equivalentes a frames ni a fullDetections. matchingMs mide la ruta ejecutada, incluyendo preparación y lectura. No se mezclan estas llamadas con un tiempo GPU. La herramienta device-tests.html muestra esos contadores. Con el SDK actualizado y matching ejecutado normalmente debe aumentar WASM exacto. Si aumenta OpenCV, está activa la alternativa de compatibilidad. Un JSON de un build anterior no permite evaluar esta optimización. Ver [mediciones y límites](camera-performance.md). ## Elegir el escenario en la herramienta móvil El selector Perfil de cámara ofrece Automático, Ahorro, Equilibrado y Más detalle. Aplica el perfil y detiene la sesión anterior antes de iniciar otra; no cambia dimensiones dentro de un mapa activo. El informe registra configuration.performance y maxFramePixels, además de analysisWidth/analysisHeight reales en metrics.latestCamera. Compara el mismo escenario y movimientos; no interpretes un menor tiempo a menor resolución como prueba de igual precisión. Los ajustes de render siguen siendo independientes. En tools/device-tests.html, cambiar Modo, Contenido o ajustes de render aplica automáticamente la selección y detiene la sesión anterior. No necesitas pulsar Aplicar configuración para que el siguiente inicio use lo elegido; ese botón permite reiniciar/aplicar explícitamente. Los selectores aplican su configuración al cambiar. El inicio del encabezado espera cualquier aplicación pendiente; el botón Iniciar AR de la escena conserva su controlador nativo, sin interceptación de clicks por la herramienta. Cambiar opciones no solicita cámara ni inicia una sesión por sí solo. - **Probar SLAM sin marcador**: acceso directo que selecciona el backend óptico e inicia sin webar-target. Desplaza lentamente la cámara sobre textura, espera evidencia de superficie y pulsa Colocar aquí. No es una garantía de mapa correcto ni de ausencia de deriva. - **Odometría sin marcador**: el mismo modo óptico desde el selector; pulsa Iniciar SLAM sin marcador o Iniciar AR. - **WebXR nativo si está disponible**: inicia la cámara y después pulsa Activar SLAM. La petición immersive-ar/hit-test se hace con ese gesto dedicado. El soporte se consulta en ejecución; un true de isSessionSupported no garantiza que se concedan sesión/features. - **Imagen → odometría**: requiere reconocer el marcador inicialmente para conservar su posición. Es distinto de arrancar sin marcador. El encabezado muestra el build cargado antes de iniciar. En la distribución, el script lleva ?build=... y el HTML declara la compilación esperada. Si el bundle y el HTML tienen builds diferentes pero el SDK ofrece las APIs necesarias, la herramienta muestra ambos identificadores en una advertencia persistente y permite iniciar la cámara. Para medir las últimas optimizaciones, copia dist completo. Una diferencia de build por sí sola no desactiva los botones. El servidor Vite expande la misma marca al servir el HTML fuente. Esto evita confundir caché o archivos mezclados con una regresión de rendimiento. Si se cambia el modo sin iniciar otra sesión, las métricas anteriores se etiquetan como última sesión registrada. Una advertencia de build no exige confirmación para iniciar. Los botones del encabezado permanecen disponibles durante el inicio; aria-busy señala trabajo en curso y otro toque informa del progreso sin abrir una segunda cámara. El botón nativo del componente conserva su estado de inicio/parada. Si el SDK no cargó o faltan las APIs que requiere la herramienta, se muestra un mensaje específico de carga/incompatibilidad. El navegador sigue decidiendo los permisos reales de cámara. ## Comparar SLAM por cámara en una página Abre `/2/Examples/slam-comparison.html` desde ambos teléfonos después de actualizar el recolector local. Selecciona Automático, recorre la misma zona con luz y distancia similares, confirma con Colocar aquí y luego cubre la cámara durante tres segundos. Vuelve al mismo encuadre y finaliza. El informe incluye tiempo hasta la superficie y la primera colocación, pérdidas de odometría, poses caducadas y recuperación del ancla. Una pérdida de odometría puede ocurrir después de una caducidad en el mismo episodio. El reloj comienza cuando se activa el escaneo, después de iniciar cámara. Los 45 segundos son una referencia de comparación, la sesión puede continuar después. En el panel del PC filtra por `slam-comparison.html`. El intento queda en el informe de cada equipo, junto al perfil y al identificador de compilación. La recuperación exige volver a colocar solo si se reinició el mapa. Para probar otro perfil en el mismo teléfono, detén la sesión antes de cambiarlo. Usa el iPhone 17 Pro como referencia de gama alta y el Android actual como prueba de estrés. Para comprobar la continuidad de cámara, repite la transición de imagen a odometría y observa si el encuadre salta al acercarte o alejarte. Si se prueba WebXR nativo, registra ese recorrido por separado. ## Repetir la comparación en teléfonos Usa la misma compilación, marcador, modelo, luz y recorrido de cámara. En la primera apertura del ejemplo 01 mide la carga inicial y en la segunda verifica el estado `target-cache-hit`. En el 14 apunta a ambos marcadores y observa matching, poses por ID y pérdida/recuperación. En el 07 anota si se detecta y confirma la superficie, y vuelve a una zona conocida tras una pérdida. Usa el 21 para comprobar la conservación explícita de la posición y el 22 para comprobar qué backend obtuvo la sesión. Compara `auto` y `battery` en el 23 con el mismo encuadre y duración. Las pruebas físicas no requieren repetir las 25 páginas. ## Inicio y carga lenta El componente solicita cámara antes de descargar/decodificar los modelos; ar-ready sigue significando que contenido/targets y modo están preparados, no que haya pose ni superficie colocada. Mientras carga un GLB, la vista previa puede estar activa y el estado indica Cámara activa. Cargando contenido. Si falla el contenido, se cierra la cámara y se emite ar-error. Las validaciones estructurales y el rechazo de file:// ocurren antes de pedir permisos. La herramienta muestra por separado camera-requesting (solicitud enviada), camera-granted (stream recibido) y camera-playing (esperando play). El autoenfoque continuo se intenta después de reproducir y no bloquea el inicio. Si play no termina en 15 segundos, la sesión falla con un mensaje específico y libera cámara. Mientras el permiso sigue pendiente, Detener permite cancelar sin dejar abierto un stream concedido posteriormente. Estas etapas permiten localizar la espera sin asumir su causa a partir de que el botón se ve activo. La herramienta no captura el click del botón Iniciar AR dentro de la escena. Los botones superiores llaman a scene.start directamente con configuración lista. La página muestra el mensaje de progreso/error inmediatamente debajo de esos botones. Antes de cargar el SDK, un toque informa de la carga; si el script falta, el botón muestra la causa en lugar de quedar gris y sin respuesta. Esa respuesta de error no significa que se haya concedido cámara ni que AR esté funcionando sin SDK. --- Fuente: docs/odometry-validation.md # Validación de odometría, relocalización y deriva La odometría sigue siendo monocular y de mapa local. Esta revisión añade validaciones, no SLAM global ni fusión IMU. No se atribuye a una cámara física lo medido con imágenes sintéticas. ## Cambios implementados - El seguimiento de puntos ambientales exige distribución espacial, además del número y proporción de inliers. La semilla planar de marcador tiene su propia excepción durante inicialización; no se le exige cubrir todo el frame. - La relocalización verifica reproyección, proporción de inliers y distribución en una rejilla. Las hipótesis débiles necesitan dos observaciones consecutivas con poses compatibles. Las fuertes (al menos 30 inliers, ratio >=0.65, error <1.5 px y seis celdas) pueden aceptarse directamente. - Se conserva la revisión del mapa al recuperar una pose; no se publica una identidad ni el anchor guardado como sustituto de una medición. - Los resultados ópticos incluyen relocalized y keyframeCorrected. El Worker los valida y la fachada los transmite en pose.diagnostics. En HTML se consultan en event.detail.diagnostics de ar-pose. Las pruebas incluyen mapa importado, ausencia de imagen, escena distinta, confirmación temporal débil, continuidad tras retirar marcador y un plano triangulado. Consulta [verificación](verification.md) y [protocolo físico](device-validation.md). ## Corrección local experimental `keyframe-correction` en webar-scene o `keyframeCorrection:true` en EngineOptions/OdometryConfig habilita una reobservación acotada del keyframe antiguo. Está **desactivada por defecto**. Solo se acepta una corrección con soporte suficiente, movimiento/rotación acotados y mejora de reproyección sin degradar demasiado los puntos LK. No cambia el origen del mapa ni constituye cierre de bucle global. Puede añadir coste, errores y saltos; el filtrado visual no demuestra que la geometría haya mejorado. ```html ``` No actives ese atributo suponiendo mayor precisión. Es una opción para experimentar con un protocolo propio y comparar resultados; cambiarla requiere reiniciar el backend. ## Comparación registrada Protocolo: 630 imágenes deterministas de 640×480, trayectoria sinusoidal de traslación, OpenCV/WASM real en Node. Se alinea una escala de referencia para medir error en unidades de la escena de prueba; no son errores físicos medidos en metros. Son ejecuciones individuales y no un estudio de rendimiento móvil. | Medida | Antes | Actual por defecto | Corrección experimental | | --- | --- | --- | --- | | Poses / frames | 580 / 630 | 580 / 630 | 580 / 630 | | Error de trayectoria p50 | 0.005136 | 0.005136 | 0.004975 | | Error de trayectoria p95 | 0.011150 | 0.011150 | 0.013269 | | Error máximo | 0.012635 | 0.012635 | 0.014603 | | Procesamiento p50 | 41.12 ms | 41.14 ms | 40.37 ms | | Procesamiento p95 | 77.02 ms | 79.12 ms | 81.18 ms | | Heap WASM caliente/final | 32 / 32 MiB | 32 / 32 MiB | 32 / 32 MiB | **El error de la configuración predeterminada no mejoró en esta trayectoria.** La corrección experimental reduce la mediana, pero empeora p95 y máximo, por eso no se activa automáticamente. Las diferencias de tiempo no prueban una mejora de velocidad. El rechazo de falsas relocalizaciones y la recuperación se verifican con pruebas específicas, no con esta tabla de deriva. Informe completo con las tres ejecuciones (`benchmarks/odometry-comparison.json` en el repositorio). Los archivos baseline-2026-11-09.json y experimental-2026-11-09.json conservan las observaciones iniciales; latest.json es el resultado predeterminado más reciente. ## Repetir el protocolo ```sh node scripts/benchmark.mjs 630 node scripts/benchmark.mjs 630 experimental node scripts/compare-odometry.mjs benchmarks/baseline-2026-11-09.json benchmarks/latest.json benchmarks/experimental-latest.json ``` Ejecuta secuencialmente y registra hardware/carga. Se puede usar un baseline histórico para referencia, pero para evaluar otra modificación conviene medir antes/después en el mismo entorno. El benchmark no incluye compositor, GPU móvil, sensor, rolling shutter, temperatura ni consumo energético. ## Lo que sigue pendiente Validar trayectorias con rotación y cambios de iluminación, recorridos más largos, oclusión prolongada y dispositivos reales; medir error relativo y absoluto con referencia externa. Ajustar mapas o resolver un cierre de bucle global necesita más trabajo y no se considera terminado por añadir la corrección local experimental. --- Fuente: docs/installation.md # Instalación y distribución ## HTML directo: opción recomendada Sigue [inicio con HTML](getting-started.md). Copia solo `webar-facil.min.js` y úsalo con `webar-scene`, `webar-target` y `webar-model`. El archivo compilado incluye OpenCV/WASM, Workers, Draco, Meshopt y Three.js. El paquete npm se llama `webar-facil`. En código nuevo, usa `WebARFacil` al cargar el script clásico. Los scripts clásicos también conservan el alias anterior `MiLibreriaAR` para integraciones existentes que ya lo usan. Si alojas el SDK en otro dominio HTTPS, apunta el script a ese servidor. En la capa declarativa OpenCV/Draco se localizan junto al script, mientras las URLs de imagen/modelo se interpretan respecto al HTML. El servidor de recursos debe permitir CORS; [referencia declarativa](declarative.md) y [despliegue](deployment.md) detallan rutas y actualización de caché. La biblioteca no publica por sí sola una URL CDN. ## Formatos disponibles | Archivo / import | Uso | Three.js | | --- | --- | --- | | `webar-facil.min.js` | ` ``` El contenedor `#view` debe existir y tener altura. Los ejemplos ESM del repositorio usan las rutas adecuadas a su subcarpeta. No uses `require()` para estos exports ESM. ## Recursos y licencias `vision/opencv.js` contiene el runtime OpenCV/WASM de esta distribución; conserva su archivo de licencias. Draco necesita `draco_wasm_wrapper.js` y `draco_decoder.wasm` en la ruta configurada; se distribuye también el decoder JS. Meshopt está integrado en el cargador. No hace falta un servicio remoto de compilación ni de tracking. Al redistribuir conserva `LICENSE`, `THIRD_PARTY_NOTICES.md` y los avisos de `vision/`, `decoders/` y `vendor/`. Los modelos y marcadores del ejemplo pueden sustituirse sin cambiar la API. --- Fuente: docs/local-release.md # Identificación y distribución local El alojamiento público y la web del proyecto se posponen. Esta revisión no publica un paquete ni crea un CDN. Los artefactos locales permiten identificar exactamente qué código probó cada dispositivo. ```sh npm run build npm run check:release npm run check:package ``` El build genera LIBRARY_VERSION desde package.json y LIBRARY_BUILD como hash determinista del código/configuración del runtime. Ambos se exportan desde core/standalone y se incluyen en diagnósticos. No equivale a una firma criptográfica ni certificación. Dist/release.json enumera archivos, tamaños y SHA-256; check:release comprueba su integridad. Cambiar archivos después de generarlo exige regenerar el manifest. Si solo regeneras documentación, ejecuta `node scripts/release-manifest.mjs` al terminar. El tarball local queda en artifacts con la versión del paquete. Dos builds de desarrollo pueden conservar la misma versión y tener build ID distinto. Para comparar informes usa ambos identificadores. Cuando se publique una versión estable, habrá que definir releases inmutables y la política de cache junto al alojamiento elegido. La distribución incluye Workers, OpenCV/WASM, decoders, medios originales, ejemplos y documentación. No basta copiar el JS suelto si los recursos no están alojados donde espera el SDK. [Instalación](installation.md) y [despliegue](deployment.md) describen su disposición; un servidor HTTP local no aporta por sí solo HTTPS para el teléfono. Los HTML empaquetados que cargan el standalone llevan ?build=... para renovar la URL del recurso con cada compilación. La herramienta device-tests además compara el build esperado del HTML con el del SDK cargado y lo muestra antes de iniciar. Vite expande la misma marca en el HTML fuente. Copia el conjunto dist actualizado: cambiar solo el HTML no actualiza un JS antiguo alojado en otra carpeta. La comprobación de build de device-tests es informativa cuando el SDK cargado sigue siendo compatible. La advertencia persiste durante la prueba y no bloquea la cámara; el informe conserva su build real. Un SDK no cargado o sin las APIs requeridas se distingue de una discrepancia de identificadores. --- Fuente: docs/alpha-release.md # Preparar la alfa pública La distribución ofrece seguimiento de marcadores, GLB, vídeo con chroma y colocación por cámara. La odometría monocular se considera experimental y puede acumular deriva. WebXR nativo se prueba aparte porque la disponibilidad depende del navegador y del dispositivo. ## Probar la distribución Genera y verifica el paquete desde la carpeta del proyecto: ```sh npm ci npm run build npm test npm run docs:check npm run check:release python scripts/package-alpha.py ``` En Windows también puedes ejecutar el último comando con `py -3`. El ZIP y su SHA-256 quedan en `artifacts/`. Descomprime el ZIP y sirve `dist/` mediante HTTP en localhost o HTTPS en el teléfono. Abre `examples/alpha.html` desde ese servidor para elegir una de las tres demos. El ZIP contiene los modelos, los marcadores, el vídeo, OpenCV, los decodificadores y las licencias. ## Antes de etiquetar en GitHub 1. Repite la [comparación móvil](diagnostics.md) con el build candidato. Registra iPhone 17 Pro como referencia de gama alta y el Android actual como prueba de estrés. Registra Android de gama media cuando haya evidencia de ese segmento. 2. Configura la identidad Git local y prepara el primer commit con el código del proyecto. Verifica que el índice excluya `reports/local-devices/`, `artifacts/`, `dist/` y configuraciones o recursos del sitio XAMPP. La distribución se adjunta al release como ZIP. 3. Verifica el build y CI desde un clon limpio. Los medios de demo y las dependencias tienen su procedencia en los avisos de terceros y en la guía de recursos de los ejemplos. 4. Una vez elegido el repositorio remoto, publica el código, etiqueta la versión de publicación y adjunta el ZIP y su SHA-256 a una release preliminar. Enlaza las tres demos cuando estén servidas por HTTPS. --- Fuente: docs/camera-performance.md # Cámara, foco y rendimiento ## Configuración declarativa ```html ``` Estos son los valores predeterminados de la capa HTML. Los controles solicitan la cámara automáticamente al pulsar Iniciar AR. No implementes otro CameraManager para esta escena. Las opciones de escena se leen al iniciar; para cambiarlas, detén, modifica atributos y reinicia. ## Perfiles sin listas de dispositivos El motor no clasifica iPhone/Android ni usa el tamaño del GLB para elegir la cámara. Parte de un presupuesto y adapta el análisis a los tiempos medidos y a la calidad del marcador. Ningún perfil garantiza temperatura, autonomía o precisión física. | performance | Captura solicitada | FPS objetivo | Ancho máximo de análisis | Píxeles máximos de análisis | | --- | --- | --- | --- | --- | | auto (predeterminado) | 960×540 | 30 | 640 | 307200 | | balanced | 1280×720 | 30 | 640 | 307200 | | battery | 640×480 | 20 | 480 | 172800 | | quality | 1920×1080 | 30 | 960 | 921600 | Los tamaños de captura son preferencias: getSettings informa el modo realmente concedido. El presupuesto de píxeles escala toda la imagen conservando su proporción, sin recortar ni estirar. En auto, el análisis empieza con un máximo de 172 800 píxeles. Para una entrada vertical 720×1280 equivale aproximadamente a 311×553. Si el coste lo permite o falta detalle en un marcador visible, el presupuesto puede subir hasta 307 200 píxeles (aproximadamente 415×738). El máximo controla trabajo, no fija un rectángulo idéntico para todas las proporciones. Puedes probar ahorro con performance="battery". Si el marcador es pequeño, más detalle puede resultar necesario: la resolución mínima no es siempre la que reconoce mejor. El render es independiente; pixel-ratio="1" y antialias="false" reducen coste gráfico sin cambiar la imagen analizada. Las opciones explícitas tienen prioridad: camera-width/camera-height modifican la solicitud de captura; analysis-width, analysis-pixels y fps modifican los límites del tracker. En TypeScript sus equivalentes son camera.width, camera.height, camera.maxFrameWidth, camera.maxFramePixels y camera.fps. adaptive:false desactiva la adaptación temporal, no los límites estáticos del perfil. La captura negociada no cambia automáticamente durante la sesión. En imagen se ajusta el ancho de análisis; en odometría se conserva la resolución con la que se construye el mapa y solo se adapta la frecuencia. Cambiar de perfil declarativo exige detener y reiniciar; la herramienta de pruebas lo hace al seleccionar el perfil. No conserva un mapa activo a través de ese reinicio. ## Configuración programática avanzada El `CameraManager` admite objetivo de hasta 60 FPS; auto y la capa declarativa empiezan con 30. Con la [API programática](programmatic.md) puedes configurar dispositivo, resolución de captura y foco: ```ts const engine=new WebAREngine({ camera:{performance:'auto',maxFramePixels:307200,adaptive:true}, }); await engine.startCamera(); console.log(engine.cameraSettings); const supported=await engine.setFocus('continuous'); ``` La resolución pedida no es una garantía de resolución real: el navegador negocia las constraints. La cámara trasera se solicita como preferencia cuando no hay `deviceId`, lo que mantiene disponible la webcam de escritorio. Para elegir un dispositivo concreto usa `camera.deviceId`; la lista de cámaras se obtiene con `navigator.mediaDevices.enumerateDevices()` y puede necesitar permiso previo para mostrar etiquetas. `fps` tiene máximo 60 y controla el objetivo de captura/análisis, no una garantía de que el dispositivo pueda ejecutar tracking a esa velocidad. `width` y `height` describen captura; `maxFrameWidth` limita la imagen entregada al analizador. `focusMode` puede ser `continuous`, `manual` o `none`; para foco manual `setFocus('manual',distance)` depende del soporte y las unidades del dispositivo. La ausencia de control de foco no impide seguir con foco automático del navegador. ## Mantener la entrada de cámara La captura web abre una pista de vídeo, con preferencia por la cámara trasera. El rastreo de imagen y la odometría óptica reutilizan la pista durante la sesión. Si WebXR nativo obliga a liberar la captura y después se vuelve a vídeo web, el SDK solicita el mismo `deviceId` de forma exacta cuando el navegador lo expuso. Si esa entrada ya no está disponible, Detener e Iniciar permite volver a negociar la selección automática. Para elegir una entrada que el navegador expone por separado, usa `camera.deviceId` en la API o `camera-device-id="ID_DEL_DISPOSITIVO"` en ``. El atributo se lee al iniciar. El número de lentes físicas no determina cuántas entradas de vídeo enumera el navegador. El SDK puede fijar la entrada expuesta; si el sistema agrupa lentes en una cámara virtual, los cambios internos de esa cámara dependen de la plataforma. En WebXR nativo la entrada la gestiona la sesión XR. ## Apertura y esperas El vídeo comienza a reproducirse antes de configurar el autoenfoque continuo. Esa mejora se intenta sin bloquear startCamera: un controlador con applyConstraints pendiente no debe impedir la cámara. Si solicitas focusMode manual, se espera su configuración con un límite de 8 segundos; Detener cancela esa espera. Si play no responde después de recibir el stream, el inicio falla a los 15 segundos y libera los tracks. Los estados camera-requesting, camera-granted y camera-playing se reciben mediante engine.onStatus o el evento declarativo ar-status (detail.message). Distinguen solicitud al navegador, stream concedido e intento de reproducción; camera-ready confirma que play terminó. CameraManager también admite onStatus para esas tres etapas. Un permiso pendiente puede no resolverse hasta que el usuario responda: no se interpreta automáticamente como denegado. Detener cancela la espera local y cierra cualquier stream que llegue tarde. ## Adaptación En auto el mínimo adaptativo de ancho es 240 px por defecto, acotado por el máximo y la resolución de origen. Los otros perfiles conservan su mínimo adaptativo anterior de hasta 320 px. `minFrameWidth` debe ser al menos 64 y no superar el máximo. Al detectar targets pequeños, pocos matches o desenfoque, la política protege detalle antes de bajar resolución; también ajusta frecuencia según carga y movimiento. La adaptación no puede reconstruir detalle ausente ni exceder el límite configurado. Si un target ocupa muy pocos píxeles, acerca la cámara, mejora la captura o aumenta el máximo de análisis. Reducir siempre resolución para ahorrar CPU puede empeorar tanto el reconocimiento que aumente el coste de recuperación. Las decisiones requieren al menos ocho muestras y 1.5 segundos desde la decisión anterior. El promedio suavizado de lectura y el de procesamiento evitan reaccionar a un solo frame caro. Si un marcador visible es pequeño, tiene soporte escaso o aparece desenfocado, se protege el detalle. Tras varios segundos sin detección, auto permite una ventana breve de búsqueda a mayor resolución. La frecuencia sigue limitada por el trabajo real: el objetivo solicitado no implica que el dispositivo lo alcance. setAnalysisWidth es una operación avanzada para semillas/mapas y fuerza el ancho solicitado por encima del presupuesto de píxeles. Se conserva así la proyección de un mapa existente. Volver a setAdaptiveResolution(true) libera esa fijación. No uses estos métodos para cambiar dimensiones arbitrariamente dentro de una odometría activa. ## Métricas En HTML, usa `scene.engine.onMetrics(listener)` después de `ar-ready`; la propiedad es `null` cuando la escena está parada. El siguiente fragmento utiliza un `engine` programático: ```ts const off=engine.onMetrics(metrics=>{ console.table({ analysisWidth:metrics.analysisWidth, analysisHeight:metrics.analysisHeight, targetAnalysisFPS:metrics.targetAnalysisFPS, readbackMs:metrics.readbackMs, processingMs:metrics.processingMs, consumerMs:metrics.consumerMs, averageProcessingMs:metrics.averageProcessingMs, skippedFrames:metrics.skippedFrames, }); }); ``` `processedFrames` cuenta procesados y `skippedFrames` incluye duplicados y frames omitidos por backpressure; no significa “marcador perdido”. `targetAnalysisFPS` es el objetivo del planificador, no una medición de FPS del renderer. Mide render por separado. Un solo frame está en vuelo por consumidor; los nuevos se omiten mientras el Worker está ocupado. El transporte de `ArrayBuffer` evita copia entre hilos, pero lectura del canvas y copia al heap WASM siguen teniendo coste. No se promete un pipeline completamente sin copias. ## Recomendaciones operativas 1. Comienza con una imagen rica en textura, un GLB pequeño y 640 px de análisis. 2. Revisa `trackingQuality` para distinguir tamaño/nitidez de carga. 3. Reduce geometría, texturas y pixel ratio del renderer antes de sacrificar detalle necesario del target. 4. Añade targets progresivamente y mide el teléfono más limitado de tu público. 5. Detén cámara y libera medios al salir; mide temperatura y batería en sesiones largas. La captura se suspende al ocultar la página; no asumas permiso o frames continuos al cambiar de app. El protocolo y los resultados publicados están en [verificación](verification.md) y [validación física](device-validation.md). ## Calibración opcional No es un paso obligatorio del inicio. El motor usa intrínsecos estimados y refinamiento de focal a partir de observaciones consistentes. Para aplicaciones con exigencias de geometría o distorsión conocidas puedes proporcionar `CameraIntrinsics` o usar la herramienta de calibración (`tools/calibrate.html` en el repositorio). El tablero ChArUco incluido es de 7×5 cuadrados de 30 mm, marcadores de 22 mm; imprime sin reescalado. Se requieren al menos 10 vistas diversas aceptadas; la herramienta recomienda 15–25. No reutilices una calibración de otra lente, recorte o zoom. La calibración opcional no resuelve oclusión ni falta de textura. ## Modelos grandes: separar renderizado y tracking El peso GLB en MB no mide su coste por fotograma. Draco/Meshopt reducen transferencia; no reducen por sí solos la cantidad de triángulos dibujados tras decodificar. Texturas, materiales, transparencias, esqueletos y animaciones también influyen. No hay un límite universal de MB o polígonos que garantice fluidez. Para comparar en el mismo teléfono, entorno y contenido: ```html ``` Estos atributos están disponibles en el standalone actualizado. En la API Three.js: `new ThreeARScene({container, maxPixelRatio:1, antialias:false})`. No cambian los 640 px/30 Hz iniciales del análisis. Reducir de ratio 2 a 1 divide por cuatro el número de píxeles del buffer a igual tamaño CSS; puede mejorar el rendimiento con una imagen menos nítida. No garantiza 60 FPS ni modifica la resolución de una sesión WebXR inmersiva. 1. Compara `pixel-ratio="2"` y `"1"` manteniendo el resto igual; reinicia la escena entre configuraciones. 2. Compara antialias true/false. Primero cambia una sola variable; puedes combinar ambas después. 3. Compara el mismo modelo con `animation="none"`, y después un modelo simple de demostración, sin cambiar el modo de tracking. 4. Exporta diagnósticos de cada sesión y anota si la pausa es solo al cargar, constante o aparece después de varios minutos. `tools/device-tests.html` permite elegir el ratio y antialias antes de Aplicar configuración. El informe guarda los ajustes efectivos y contadores del renderer. Si reducir los píxeles mejora la fluidez, indica sensibilidad al coste de renderizar, pero no identifica por sí solo un cuello GPU. Si la pausa sigue el modelo y no aparece con contenido simple, revisa geometría, llamadas de dibujo, materiales, texturas y animaciones en la herramienta de autoría. Optimiza una copia y conserva el original. Los contadores `renderer.latest` corresponden al render anterior: drawCalls y triangles incluyen fondo y ayudas; textures/geometries cuentan recursos, no bytes de memoria GPU. Los picos permiten ver la carga aunque el contenido desaparezca después. `renderLoopMs` mide intervalos de callbacks, no duración de GPU. Distingue un render fluido con poses espaciadas de un render entero entrecortado. Una frecuencia de tracking inferior a la del render no demuestra que el GLB sea pesado. ## Redetección con referencias verificadas El tracker conserva LK bidireccional y homografía RANSAC entre redetecciones. La decisión usa soporte geométrico en el plano del marcador: mínimo 12 inliers, error <=1.8 px, retención >=55 %, envolvente convexa >=8 % del marcador, al menos cuatro celdas y puntos no colineales en plano e imagen. Tener 12–23 puntos no obliga por sí solo a redetectar; si son fiables se mantiene el seguimiento. La caché se comprueba cada 4 u 8 frames si existe: 4 cuando la envolvente del soporte cubre menos de la mitad del marcador o su condición del plano es menor de 0.15; 8 con soporte más amplio. Tras un intento global se verifica también durante los dos frames siguientes para confirmar la recuperación. Una confirmación de caché exige soporte distribuido, envolvente >=15 %, condición del plano >=0.08, error <=1.5 px y ratio de inliers >=60 %. Ya no exige 30 inliers: conserva el mínimo de 12 acompañado por esas verificaciones. Guarda hasta 256 descriptores exactos de una detección global válida, con coordenadas del marcador original y sin aprenderlas de LK. Si la pose de caché discrepa de un LK fiable en las esquinas más de un límite de 2–4 px según el tamaño proyectado, se recurre al matching global. La búsqueda completa se solicita al perder soporte fiable o alcanzar 24 frames o el plazo temporal. El plazo parte de 1 s y puede llegar a 2 s si los frames llegan despacio y LK conserva soporte fiable. Con varios marcadores se ejecuta como máximo una búsqueda global por frame y se rota la prioridad. La verificación con caché usa la región alrededor del soporte LK fiable cuando hay suficientes descriptores; las búsquedas globales usan el frame completo. Una comprobación de caché fallida puede conservar un LK fiable hasta esa revisión, en vez de forzar búsqueda completa en cada frame. Sin referencia de caché y con LK fiable tampoco es necesario extraer ORB en cada frame. Las revisiones son intentos de validación; no garantizan ausencia de deriva. Los criterios anteriores deciden cuándo redetectar. Se mantienen los filtros forward/backward, consistencia RANSAC y validación de pose del motor. Cuando una revisión no consigue redetectar, se conserva la política previa de aceptar una pose LK que haya pasado las verificaciones originales; no se inventa una pose nueva ni se confunde retención visual con seguimiento. Cada target tiene su propio estado y caché, reiniciados al cambiar sesión/resolución. El formato WART se conserva. Al cargar JPG/PNG, el motor guarda hasta 12 marcadores compilados por origen en Cache Storage. La clave incluye el contenido, la escala física, el tamaño de compilación y el runtime. El SDK vuelve a compilar si se borra la caché o el almacenamiento no está disponible. La aplicación sigue indicando el JPG/PNG como fuente. Esto reduce los picos de matching de redetecciones periódicas. La primera detección, escenas difíciles y búsquedas fallidas aún pueden necesitar el pool completo; no se garantiza 60 FPS. Esta mejora corresponde al seguimiento de imágenes, no al coste de la odometría SLAM sin marcador. La primera optimización de caché, anterior a la decisión geométrica actual, está conservada en la comparación histórica (`benchmarks/image-tracking-comparison.json` en el repositorio) usa Workers reales y OpenCV WASM con 320 imágenes sintéticas: 316 con marcador y 4 de oclusión. Se excluyen cuatro frames iniciales de los tiempos de cada caso. Ambos motores detectaron los mismos 316 frames y no produjeron pose en la oclusión; recuperaron el marcador al reaparecer. El error máximo de esquinas del motor nuevo fue menor de 1.6 px en estos casos (en movimiento subió de 1.24 a 1.59 px). No demuestra precisión física general ni rendimiento de iPhone/Android. | Caso | Media Worker antes → después | p95 Worker antes → después | | --- | --- | --- | | Movimiento suave, marcas de tiempo a 33 ms | 19.56 → 9.37 ms | 118.73 → 26.14 ms | | Movimiento suave, marcas de tiempo a 140 ms | 37.50 → 11.53 ms | 223.39 → 28.35 ms | | Movimiento | 37.05 → 20.29 ms | 212.35 → 56.54 ms | | Oclusión y recuperación | 37.61 → 18.89 ms | 212.97 → 51.46 ms | Son medidas de una ejecución Node/Windows, sin cámara, compositor, GPU móvil ni control térmico. Los intervalos 33/140 ms son timestamps de entrada, no esperas de tiempo real del benchmark. Las colas de alta latencia aún pueden contener búsquedas completas. Repite la captura física con el mismo marcador, iluminación, modelo y ajustes, y compara los nuevos campos tracking.stages y tracking.paths. Para reproducir la versión actual: `npm run build` y `npm run benchmark:image`. Los informes históricos antes/después están conservados en benchmarks; ejecuta `node scripts/compare-image-tracking.mjs` para validar sus casos y generar la comparación. La comparación comprueba detecciones, recuperación y error geométrico, no impone un umbral de velocidad dependiente del ordenador. ## Comparación con MindAR y AR.js NFT MindAR separa el matching de targets que todavía no sigue del seguimiento de targets activos. Su tracker utiliza la pose anterior para proyectar la imagen y buscar correspondencias locales de plantillas mediante TensorFlow.js; el controlador vuelve al matching al perder seguimiento y gestiona tolerancias de aparición/pérdida y filtrado. Esto se observa en su [controlador](https://github.com/hiukim/mind-ar-js/blob/master/src/image-target/controller.js) y [tracker](https://github.com/hiukim/mind-ar-js/blob/master/src/image-target/tracker/tracker.js). [AR.js usa jsartoolkit5 para tracking](https://ar-js-org.github.io/AR.js-Docs/). En su ruta NFT, ARToolKit expone por separado inicialización y tracking de textura, con búsqueda local alrededor de la posición anterior y umbral de error de pose. La transformación inicial puede venir de KPM y debe restablecerse tras perder tracking. Ver la [API original ARToolKit NFT](https://www.artoolkitx.org/artoolkit5/doc/apiref/tracking_h/index.html). Los marcadores cuadrados de AR.js siguen otra ruta y no son equivalentes a NFT. La decisión de esta biblioteca es propia: mantener adquisición, seguimiento y recuperación separados, conservar LK cuando su evidencia es suficiente y reservar búsquedas caras para degradación o validación acotada. No trasladar umbrales de MindAR/ARToolKit directamente a ORB/LK: usan estimadores y soportes diferentes. Para medir soporte escaso: `npm run benchmark:image -- benchmarks/image-support-latest.json sparse`. La fixture proyecta el marcador a distancia mayor y usa LK real con 12–23 puntos en muchos frames. Los informes históricos de esta decisión se comparan con `node scripts/compare-image-tracking.mjs image-support`; ver comparación (`benchmarks/image-support-comparison.json` en el repositorio). La versión anterior se capturó desde el tarball local con build aef4ddad324066a2. Las imágenes son sintéticas, no registros de un teléfono; revisa tiempos, precisión, pérdidas y recuperación juntos. Puede haber picos mayores al ejecutar revisiones globales periódicas aunque baje la media. No extrapolar a 60 FPS ni a SLAM sin marcador. ### Resultado local de la política geométrica Comparación conservada entre aef4ddad324066a2 y 7e52a3fa6b95a911 (80 frames por caso, cuatro iniciales excluidos de los tiempos): | Caso de soporte escaso | Media Worker antes → después | p95 Worker antes → después | Error máximo de esquinas antes → después | | --- | --- | --- | --- | | Estático | 18.31 → 10.91 ms | 18.76 → 19.33 ms | 0.97 → 1.33 px | | Timestamps separados 140 ms | 31.37 → 23.65 ms | 61.58 → 80.30 ms | 0.97 → 1.33 px | | Movimiento | 34.24 → 21.37 ms | 48.97 → 46.38 ms | 2.46 → 3.52 px | | Oclusión y recuperación | 31.08 → 24.83 ms | 38.91 → 140.57 ms | 2.20 → 4.19 px | Los 316 frames con marcador se detectaron en ambas versiones y la recuperación se mantuvo. No hubo pose en los cuatro frames de oclusión. En el caso estático, ORB pasó de 76 a 25 extracciones en la ventana medida. Un test adicional con referencia de 23 descriptores verifica 20 poses con una sola búsqueda global; otro comprueba 60 frames con marcador pequeño y recuperación tras ocultarlo. El cambio reduce trabajo repetido y la media en estos casos, pero no mejora todas las métricas: aumentan los picos de revisiones completas en algunos casos y el error máximo crece, aunque permanece bajo el límite original de 8 px. La comparación no demuestra precisión idéntica ni eliminación de jitter. Las decisiones de conflicto/recuperación conservan una vía de búsqueda cara para corregir estimaciones incompatibles. Debe repetirse en Windows/iPhone/Android con la misma imagen y condiciones; esta entrega no certifica una mejora física general. ## Matching ORB exacto especializado en WASM El SDK utiliza automáticamente un kernel WASM propio para descriptores ORB de 32 bytes (WTA_K=2). Se conserva la lista de matches aceptados: distancia Hamming máxima 64, ratio estricto de 0.75, orden estable y unicidad del descriptor de destino. No se reducen los descriptores, la resolución ni los umbrales geométricos. El kernel se utiliza tanto en las verificaciones de caché como en las búsquedas completas. El matcher también se reutiliza en las rutas de odometría que llaman al matcher ORB compartido; los contadores por etapa siguen perteneciendo al tracker de imágenes. El kernel calcula XOR/popcount con instrucciones WASM estándar. Omite el resto de una distancia solo cuando ya no puede modificar la aceptación. Acota la búsqueda interna porque el resultado público solo incluye matches con distancia <=64 y ratio <0.75: si la mejor distancia es <=64 y la segunda es >=87, el ratio estricto se cumple. Una distancia parcial solo aumenta; cuando ya no puede mejorar el segundo mejor valor acotado, se omiten sus palabras restantes sin cambiar el resultado aceptado. No es un índice aproximado ni una reducción de candidatos arbitraria. Las pruebas contrastan los matches ordenados contra BFMatcher de OpenCV, incluidas las fronteras 64/65, ratios exactos, empates y duplicados. El módulo compilado ocupa 427 bytes y va embebido en los bundles/Workers. No requiere otro script, descarga ni cambio de HTML. Usa memoria independiente de OpenCV: 256 KiB en la fixture de 4538×1500 descriptores, crecimiento acotado por un máximo declarado de 16 MiB. El límite de entrada permite el pool máximo del compilador más apariencias verificadas. La memoria se reutiliza por Worker y se libera al terminarlo. No requiere SIMD, threads ni SharedArrayBuffer. Si la compilación del kernel no está disponible, se conserva la implementación OpenCV y el diagnóstico identifica esa alternativa. Los errores de ejecución/entrada no se ocultan como si fueran una compilación no soportada. La fuente MIT está en src/vision/wasm/hamming.wat y se incluye en la distribución como [vision/hamming.wat](../vision/hamming.wat). `npm run build:hamming` la compila con WABT de desarrollo, fijado en el lockfile. El runtime de WABT no se distribuye dentro del SDK. `npm run build` regenera el kernel antes de calcular el identificador de build. Las pruebas verifican que el binario generado coincide con el WAT. ### Medición del matcher aislado Informe reproducible (`benchmarks/matching-exact.json` en el repositorio), Node/Windows local, tres calentamientos y siete muestras por caso. Incluye subida y lectura de descriptores, no cámara/render: | Caso | Descriptores query × train | Media OpenCV → kernel exacto | Matches aceptados | | --- | --- | --- | --- | | Marcador contra imagen redimensionada | 4538 × 1500 | 92.89 → 23.07 ms | 949, lista idéntica | | Marcador contra descriptores aleatorios | 4538 × 1500 | 93.09 → 13.14 ms | 0, lista idéntica | | Referencia pequeña | 23 × 1500 | 0.496 → 0.112 ms | 8, lista idéntica | Ejecuta `npm run benchmark:matching` después de construir para reproducir la comparación. El caso aleatorio es sintético; la primera compilación/JIT no está incluida en esos tiempos calentados. No equivalen a FPS del tracker ni a rendimiento certificado de un teléfono. ### Comprobación del tracking completo La comparación con la versión anterior (`benchmarks/image-matching-comparison.json` en el repositorio) conserva 316/316 poses con marcador, ningún positivo durante cuatro frames de oclusión y recuperación al reaparecer. Los errores máximos de esquinas son idénticos por caso a los del build anterior 7e52a3fa6b95a911. En la ejecución registrada, el p95 Worker pasó de 75.65 a 30.74 ms en la secuencia con timestamps separados 140 ms y de 76.61 a 30.36 ms con oclusión. Las rutas de redetección no cambiaron. Esto verifica la equivalencia en esas fixtures; no garantiza precisión física idéntica ni 60 FPS en Windows/iPhone/Android. Para reproducir la fixture: `npm run benchmark:image -- benchmarks/image-matching-latest.json sparse`. Para comparar las capturas conservadas: `node scripts/compare-image-tracking.mjs image-matching`. En dispositivos reales, revisa matchingMs y workerMs junto con las pérdidas y la calidad; usa el mismo marcador, modelo y configuración. La lectura de cámara, ORB, RANSAC y el render conservan su propio coste. --- Fuente: docs/deployment.md # Despliegue y seguridad del navegador ## Publicación estática ```sh npm ci npm run build ``` Publica tu HTML, tus recursos y `webar-facil.min.js` en un servidor HTTP/HTTPS. El standalone incluye el motor y sus dependencias. Publica el contenido de `dist/` completo si también quieres servir los ejemplos, el manual o las variantes con recursos separados. Puedes alojar el standalone en otro origen HTTPS y referenciar la misma URL desde distintos sitios. Las imágenes y los modelos se resuelven respecto al HTML. Las variantes ESM y core siguen usando recursos separados. En ellas, las opciones `opencvURL` y `dracoDecoderPath` permiten configurar sus rutas. El [inicio rápido](getting-started.md) utiliza un HTML junto al bundle. Los archivos de `examples/` del repositorio emplean `../dist/`; no intercambies esas rutas sin cambiar la ubicación del HTML. Comprueba el standalone actualizado, no solo su nombre de archivo: un bundle antiguo no registra los nuevos componentes. ## HTTPS y teléfono `getUserMedia` requiere contexto seguro: HTTPS o localhost en desarrollo. Una página `http://192.168.x.x` normalmente no es un contexto seguro. En un teléfono `localhost` apunta al teléfono. Utiliza un dominio HTTPS accesible, un túnel HTTPS configurado por ti o una red local con certificado confiable para ese dispositivo. Para exponer el servidor de desarrollo en red puedes usar `npm run dev -- --host 0.0.0.0`, pero eso por sí solo no añade HTTPS. Revisa firewall y acceso de red. No desactives la seguridad del navegador para simular un despliegue correcto. En iframe el sitio superior debe conceder los permisos pertinentes, por ejemplo `allow="camera; xr-spatial-tracking"`, y su Permissions-Policy no debe bloquearlos. Un rechazo de permisos requiere una acción clara del usuario o cambios de configuración, no reintentos infinitos. ## MIME, rutas y cache El servidor debe entregar JS como JavaScript, WASM como `application/wasm`, JSON como JSON y medios con su MIME. No redirijas una ruta JS que falta a `index.html`: producirá errores de sintaxis o MIME en lugar de cargar el Worker. Comprueba estado HTTP, URL y contenido en Network. Actualiza bundles y recursos como una sola versión. Los nombres actuales no incluyen un hash de contenido en cada archivo; configura cache acorde y evita mezclar un Worker viejo con un binario de otro build. Para operación sin conexión, descarga previamente todos los recursos y gestiona tú el service worker; la librería no registra uno automáticamente. ## CSP Punto de partida para una app con todo en mismo origen y su código en un JS externo: ```text default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:; connect-src 'self'; img-src 'self' data: blob:; media-src 'self' blob:; style-src 'self'; object-src 'none'; base-uri 'self' ``` Adapta la política a tu navegador, host y dependencias. Los ejemplos didácticos tienen scripts y estilos inline. La capa declarativa también crea estilos embebidos en shadow DOM: una política `style-src 'self'` los bloquea si no se autorizan, aunque el JS haya cargado. Configura los hashes o la política de estilos correspondientes; esta versión no expone un atributo de nonce para ese estilo interno. Draco puede crear un Worker con blob URL. No hay requisito general de COOP/COEP ni `SharedArrayBuffer` para este pipeline. ## CORS y datos Prefiere recursos de mismo origen. Para imágenes, GLB o vídeos remotos el servidor remoto debe autorizar las peticiones y el uso en WebGL con CORS; una URL que se reproduce como enlace no garantiza que pueda subirse como textura. El prefijo `file://` y origen `null` no son un modo de despliegue soportado. Por defecto captura y compilación se procesan localmente. Tu aplicación sigue siendo responsable de sus propias descargas, analytics, almacenamiento, permisos y tratamiento de datos. No cargues un módulo de odometría arbitrario indicado por un archivo de marcador: los plugins de código deben proceder de una configuración confiable. --- Fuente: docs/troubleshooting.md # Problemas frecuentes ## HTML dentro de Examples y recursos dentro de Examples/assets Para una instalación estática con Examples, dist y recursos propios, las rutas dependen del directorio del HTML. Con esta estructura: ```text mi-sitio/ dist/webar-facil.min.js Examples/index.html Examples/01-modelo-marcador.html Examples/assets/marcador.jpg Examples/assets/robot.glb ``` usa `../dist/webar-facil.min.js` para el SDK y `./assets/marcador.jpg` para la imagen. `../assets` apuntaría a `mi-sitio/assets`, otra carpeta. Si un standalone antiguo solicita `vision/opencv.js` y falla, sustituye el JS por la versión que incorpora OpenCV y Draco. Revisa también si el HTML define una ruta externa mediante `asset-base`, `opencv-url` o `draco-path`. La herramienta scripts/configure-examples-paths.mjs prepara los 25 ejemplos de una instalación existente, conserva opciones/código y recursos del usuario, repone solo recursos de ejemplo ausentes y verifica enlaces antes de aplicar. --apply guarda los HTML anteriores en _webar-backups dentro del sitio. La copia preparada y el plan quedan en artifacts/examples-configured del repositorio. No es una exportación completa de imágenes/modelos privados. ```sh node scripts/configure-examples-paths.mjs --site ./mi-sitio node scripts/configure-examples-paths.mjs --site ./mi-sitio --apply ``` scripts/verify-examples-installation.mjs permite comprobar el servidor HTTP real con cámara sintética, recursos, inicio y cierre. Esa comprobación no certifica cámara física, precisión o rendimiento de teléfono. El informe local queda en artifacts/examples-configured/verification.json. | Síntoma | Comprobación y solución | | --- | --- | | CORS con `file:///...` y origen `null` | Inicia `npm run dev` y abre la URL HTTP; no abras el archivo con doble click | | “Ejecuta npm run build” / bundle 404 | Ejecuta el build y verifica la ruta exacta del script; `standalone` y bundle peer son archivos diferentes | | `Unexpected token '<'` en un Worker o JS | El servidor entregó HTML para una ruta inexistente; publica los Workers y corrige el fallback del servidor | | `WebARFacil` no existe | El script no se descargó, se ejecutó después de tu código o cargaste un ESM como script clásico | | `THREE is not defined` | Usa standalone o carga un global Three compatible antes del IIFE sin Three | | No resuelve `three` en ESM | Instala el peer en NPM o configura un importmap; no mezcles copias incompatibles | | Cámara no disponible | Revisa HTTPS, permisos del sistema/navegador, iframe y si otra app ocupa la cámara | | Funciona en PC pero no al abrir una IP desde teléfono | Necesitas HTTPS confiable; el localhost del PC no es el del teléfono | | Pantalla vacía sin errores | Da dimensiones al contenedor, pulsa Iniciar y comprueba iluminación y la visibilidad del target | | HTML declarativo vacío, sin botón Iniciar AR | Comprueba la ruta del standalone, actualiza el JS del sitio y verifica `customElements.get('webar-scene')`; un bundle antiguo no registra estas etiquetas. Conserva el párrafo de respaldo del ejemplo | | Reconoce pero el objeto tiembla o parpadea | Revisa calidad, tamaño/nitidez, tolerancia y filtrado; no alternes `visible` cada vez que no llegue una pose | | El GLB se reinicia al detectar | Carga una sola vez; no llames `load()` o `playAnimation()` desde cada `onPose` | | Se queda en pantalla al retirar marcador | Desactiva `setKeepContentOnLost(true)` si no deseas retención; retención no es SLAM | | GLB Draco no aparece | Verifica ruta `dracoDecoderPath` y descarga del decoder JS/WASM; observa el rechazo de `load()` | | Modelo gigante, pequeño o fuera de plano | Revisa `physicalWidthMeters`, `fitSize`, pivot y escala interna del GLB | | KTX2 o recursos GLTF externos rechazados | Usa GLB autocontenido con texturas compatibles; ese cargador no implementa KTX2 | | Vídeo no reproduce | Espera metadata, usa un botón para `play()`, captura su rechazo y prueba el codec real | | El verde no se elimina bien | Ajusta color, similarity, smoothness y spill; usa iluminación uniforme del fondo | | Segundo marcador reemplaza al primero | Usa `addImageTarget(id,...)`; `loadImageTarget` reemplaza todos por `default` | | Error de operación concurrente | Espera cada configuración del motor en orden; no uses Promise.all en cargas/configuración del motor | | Después de stop no rastrea | `stop()` borra targets; abre cámara y vuelve a registrarlos | | Matrices parecen invertidas | Usa el contrato ModelView/mundo/vista; no apliques dos veces conversiones OpenCV ni la matriz de vista | | “SLAM activado” pero no hay objeto | Inspecciona `result.placement`: puede pedir escaneo, selección o suelo con paralaje | | Al retirar el marcador falla el fallback | Necesita estructura 3D del entorno; textura solo en un plano o rotación sin traslación no bastan | | Mapas restaurados sin pose | Espera relocalización y comprueba proyección/cámara compatibles; no uses la pose almacenada como medición | | Favicon 404 | Añade un icono o ``; ese 404 no explica un fallo de tracking | ## Qué adjuntar al reportar un problema Versión de la librería, navegador y SO, modelo de dispositivo, protocolo/URL de alojamiento, backend seleccionado, pasos mínimos, errors de consola y rutas HTTP fallidas. Añade resolución efectiva de cámara, opciones de análisis, `trackingQuality` y métricas de procesamiento. Incluye una imagen de referencia y un GLB de prueba redistribuibles cuando sea posible. Separa “no se detecta”, “pose inestable”, “render incorrecto” y “medio no carga”. Cada caso tiene fuentes distintas. Evita enviar frames o mapas sensibles sin consentimiento. Usa [verificación](verification.md) para distinguir una prueba sintética de una limitación confirmada en hardware real. --- Fuente: docs/concepts.md # Conceptos y coordenadas ## Lo que declaras en HTML `webar-scene` describe una aplicación y administra una sola cámara/renderer. `webar-target` describe una imagen; sus hijos `webar-model` son el contenido asociado. En modo SLAM sin imagen los modelos van directamente dentro de la escena. Es la ruta de [inicio recomendada](getting-started.md). Las clases y matrices de las siguientes secciones explican lo que la capa HTML gestiona internamente y permiten crear integraciones avanzadas. No tienes que crearlas manualmente para usar las etiquetas. ## Motor, target y contenido Un `WebAREngine` administra una cámara y el modo de tracking. Un **target** es una imagen conocida y sus descriptores, identificada por un ID estable. El **contenido** es un objeto Three.js, modelo, vídeo o tu propia escena, asociado al ID por un wrapper. No es necesario crear un motor por objeto o por marcador. `ThreeARScene` combina el motor con un renderer Three.js y gestiona fondo de cámara, proyección, grupos por ID, luces y actualización de medios. `WebAREngine` por sí solo no dibuja nada. `ThreeARGroup` permite usar un renderer propio. La misma imagen repetida físicamente no genera varias instancias independientes. Utiliza imágenes diferentes e IDs diferentes para contenido independiente; el máximo actual es 8 targets. ## Estados de modo y estados de visibilidad `EngineState` contiene `Idle`, `Camera`, `TrackingImage`, `TrackingSurface`, `Disposed`. Describe el modo de la sesión; `TrackingImage` no significa que el marcador esté visible en ese instante. Para eso existen `targetFound`, `targetLost`, `targetVisibility` y `trackingQuality`. ```text Idle -> startCamera -> Camera -> cargar target -> TrackingImage TrackingImage / Camera -> enableSLAM -> TrackingSurface cualquier modo activo -> stop -> Idle dispose -> Disposed (final) ``` El cambio a seguimiento espacial puede requerir escanear o seleccionar una superficie antes de entregar poses. No emitas tu propia pose identidad para simular que ya quedó anclado. ## Contrato de matrices Todas son `Float32Array(16)`, columna mayor, sistema diestro normalizado para WebGL. La conversión desde coordenadas de OpenCV se aplica en el motor. **No vuelvas a invertir ejes** en el wrapper. | Campo de `ARPose` | Significado | | --- | --- | | `matrix` | Objeto a cámara: `T_camera_object` (ModelView) | | `worldMatrix` | Objeto a mundo: `T_world_object` | | `viewMatrix` | Mundo a cámara: `T_camera_world` | | `projectionMatrix` | Proyección de cámara, o `null` si no está disponible | Relación: `matrix = viewMatrix * worldMatrix`. Para un objeto en una escena con cámara móvil usa `worldMatrix` y la inversa de `viewMatrix` para la cámara. Inyectar `matrix` en el objeto y además mover la cámara aplica la vista dos veces. En image tracking el mundo se toma en el espacio de la cámara: `viewMatrix` es identidad. El plano del marcador sigue la convención de los wrappers: contenido plano sobre XZ y altura sobre Y. Para personalizar el pivot, añade un hijo al grupo de tracking; no escribas la posición local del grupo que recibe la matriz. `ARPose.backend` es `image`, `webxr` u `optical-flow`. `scale` es `metric` o `relative`; la precisión de una escala métrica basada en marcador depende de que su tamaño físico sea correcto. `projectionSource` permite distinguir `estimated`, `calibrated` y `webxr`. El campo `engine.calibration` devuelve la calibración configurada, no todas las estimaciones internas por frame. ## Tiempo, render y propiedad de memoria `onPose` se llama al tener una observación aceptada, no a 60 Hz fijos. Mantén el bucle de render y las animaciones separados del detector. Durante una pérdida tolerada se conserva la última transformación sin inventar observaciones nuevas. `timestampMs` corresponde al reloj monotónico de captura de la aplicación, no al reloj de exposición físico del sensor. Los buffers enviados con `postMessage(..., [buffer])` se transfieren: quedan separados del hilo emisor. En la API de bajo nivel no reutilices el `ImageData` transferido. ## Limpieza En la capa HTML, `await scene.stop()` destruye la escena interna y sus recursos. El siguiente `scene.start()` vuelve a construirla leyendo el HTML, incluidos los targets. Retirar la etiqueta de la página también la detiene. `scene.app` y `scene.engine` son nulos antes de iniciar y tras parar; registra callbacks de motor dentro de cada `ar-ready`, porque la instancia cambia al reiniciar. En la API programática, el contrato es diferente: `await engine.stop()` termina captura, tracking y XR y elimina targets, pero conserva la instancia y sus callbacks. Para reiniciar debes volver a añadir las imágenes. `engine.dispose()` es final. `await app.dispose()` libera también recursos gestionados por `ThreeARScene`. Los listeners retornan una función para desuscribirse. Consulta [recetas programáticas](recipes-programmatic.md) para navegación entre pantallas con esas clases. --- Fuente: docs/programmatic.md # Integración programática avanzada Esta alternativa permite controlar explícitamente el motor, los modelos y el ciclo de vida mediante JavaScript. Para una primera aplicación con imagen y GLB sigue [inicio con HTML](getting-started.md). No tienes que reproducir este código si utilizas `webar-scene`. ## Ejecutar este repositorio Requisitos: Node.js **22.18 o posterior**, NPM, navegador con WebGL y cámara. Desde la carpeta del proyecto: ```sh npm ci npm run dev ``` `predev` compila la librería, prepara OpenCV, los modelos de ejemplo, Workers, tipos y documentación. La primera ejecución tarda más que las siguientes. Abre `http://127.0.0.1:5173/examples/documented-app.html`, pulsa **Iniciar cámara** y concede el permiso. Muestra `http://127.0.0.1:5173/marker.png` en otra pantalla o imprímelo. No intentes escanear una miniatura demasiado pequeña. En un computador se utiliza su webcam disponible; en móviles se solicita preferentemente la trasera. El tracking de imágenes no requiere `immersive-ar`. ## Mi propio index.html Ejecuta `npm run build` y copia **todo el contenido de `dist/`** a la carpeta de tu sitio. Puedes cambiar su `index.html` por tu aplicación. No copies únicamente el JS: hacen falta los Workers, OpenCV y, para modelos Draco, sus decoders. ```text mi-sitio/ index.html webar-facil.min.js workers/ # copiar completo desde dist vision/opencv.js decoders/draco/ # conservar nombres de archivos imagen.jpg modelo.glb ``` En el HTML completo de abajo las rutas corresponden a `examples/documented-app.html` de este repositorio. Para usarlo como `mi-sitio/index.html`, sustituye: | Ruta del ejemplo | Ruta de tu sitio | | --- | --- | | `../dist/webar-facil.min.js` | `./webar-facil.min.js` | | `../vision/opencv.js` | `./vision/opencv.js` | | `../decoders/draco/` | `./decoders/draco/` | | `../marker.png` | `./imagen.jpg` | | `../models/demo-draco.glb` | `./modelo.glb` | ```html Mi aplicación WebAR

Apunta al marcador de ejemplo.

``` `addContent('poster', model)` y `addImageTarget('poster', imagen)` deben utilizar el mismo ID. La imagen se convierte internamente en descriptores: no tienes que cargar `.dat` ni JSON. La anchura física de `0.2` significa 20 cm; ajustarla da una escala coherente con el marcador real. Si desconoces su tamaño, puedes usarla como referencia de escala, sin atribuir exactitud métrica a la escena. ## Qué modificar después - Sustituye las rutas de imagen y GLB o usa archivos de un formulario: [recetas programáticas](recipes-programmatic.md). - Usa un MP4 en lugar del modelo: [vídeo](video.md). - Añade un segundo marcador con otro ID: [multimarcador](multiple-targets.md). - Activa retención visual con `app.engine.setKeepContentOnLost(true)`. - Añade SLAM siguiendo [la guía espacial](spatial-tracking.md); la disponibilidad y la continuidad al cambiar de modo son diferentes del tracking de imágenes. Para publicar o probar desde un teléfono utiliza HTTPS. `localhost` de un teléfono es el propio teléfono, no tu computador. Consulta [despliegue](deployment.md). --- Fuente: docs/recipes-programmatic.md # Recetas programáticas avanzadas Estos fragmentos se insertan en la [aplicación programática completa](programmatic.md). `app` es un `ThreeARScene` existente; en standalone las clases se obtienen de `WebARFacil`, y con NPM se importan del paquete. Para etiquetas HTML utiliza [recetas declarativas](recipes.md); no mezcles un `app` inexistente con una escena declarativa. ## Imagen y modelo elegidos por el usuario ```html ``` ```js const content=new ThreeARModel(); app.addContent('usuario',content); document.querySelector('#apply').onclick=async event=>{ const button=event.currentTarget; const image=document.querySelector('#marker').files[0]; const file=document.querySelector('#model').files[0]; if(!image||!file) return; button.disabled=true; try{ await content.load(file); await app.engine.startCamera(); const result=await app.engine.addImageTarget('usuario',image); console.log(result.metadata.quality.warnings); }catch(error){console.error(error);} finally{button.disabled=false;} }; ``` Para un flujo de reemplazo con cámara ya activa, no vuelvas a invocar `startCamera()` sin gestionar el estado; sustituye solo el modelo y el target. Bloquea también los controles de configuración incompatibles mientras haya una operación en curso. ## Descargar .dat y JSON opcionalmente ```js function download(blob,name){ const url=URL.createObjectURL(blob),link=document.createElement('a'); link.href=url;link.download=name;link.click(); setTimeout(()=>URL.revokeObjectURL(url),1000); } const compiled=await app.engine.addImageTarget('poster',file); download(new Blob([compiled.buffer],{type:'application/octet-stream'}),'poster.dat'); download(new Blob([JSON.stringify(compiled.metadata,null,2)],{type:'application/json'}),'poster.dat.json'); ``` `file` debe ser un `File` JPG/PNG elegido previamente. Para iniciar tracking basta con la llamada a `addImageTarget`; las descargas son una función optativa de tu aplicación. ## Contenido compuesto e interacción ```js const {THREE}=WebARFacil; const contentRoot=new THREE.Group(); const model=new ThreeARModel(); contentRoot.add(model); app.addContent('poster',contentRoot); await model.load('./modelo.glb'); const off=app.onRender(dt=>{contentRoot.rotation.y+=dt*0.2;}); ``` Modifica hijos del anchor, no el anchor que recibe matrices. Para picking, crea un `THREE.Raycaster`, calcula NDC respecto a `app.renderer.domElement.getBoundingClientRect()` y utiliza `app.camera`; el vídeo puede tener letterbox. Tu aplicación define qué hacer al pulsar el modelo. ## Checkbox de retención ```html ``` ```js document.querySelector('#retain').onchange=event=>{ app.engine.setKeepContentOnLost(event.currentTarget.checked); }; ``` ## Persistir mapas en IndexedDB Adaptador mínimo para `SLAMMapSnapshot`, con clones que conservan typed arrays: ```js async function openMaps(){ return new Promise((resolve,reject)=>{ const request=indexedDB.open('webar-local',1); request.onupgradeneeded=()=>request.result.createObjectStore('maps'); request.onsuccess=()=>resolve(request.result); request.onerror=()=>reject(request.error); }); } async function mapStore(mode,operation){ const db=await openMaps(); try{ return await new Promise((resolve,reject)=>{ const tx=db.transaction('maps',mode); const request=operation(tx.objectStore('maps')); let value; request.onsuccess=()=>{value=request.result;}; tx.oncomplete=()=>resolve(value); tx.onabort=()=>reject(tx.error??new Error('Transacción abortada')); tx.onerror=()=>reject(tx.error??request.error); }); }finally{db.close();} } // Con odometría activa y un mapa válido: const snapshot=await app.engine.exportSLAMMap(); await mapStore('readwrite',store=>store.put(snapshot,'mi-escena')); const restored=await mapStore('readonly',store=>store.get('mi-escena')); if(restored) await app.engine.importSLAMMap(restored); // Espera relocalización antes de mostrarlo como posición válida. ``` Es JavaScript de aplicación, no una API de la librería. Gestiona errores de cuota, contenido obsoleto y permisos de almacenamiento en tu UI. Lee [compatibilidad de mapas](maps.md) antes de restaurar otra sesión. ## SPA o cambio de pantalla ```js const subscriptions=[ app.engine.onError(console.error), app.engine.on('targetFound',event=>console.log(event.targetId)), ]; async function unmount(){ subscriptions.forEach(off=>off()); await app.dispose(); // Libera aquí mallas propias, listeners DOM y URLs que creó tu aplicación. } ``` No confíes únicamente en `pagehide` en una SPA: llama a tu limpieza al desmontar. Para pausar conservando la escena usa `await app.engine.stop()`; al volver, reinicia cámara y targets. El renderizador y los medios tienen ciclo de vida propio y no equivalen a la captura de cámara. ## Texto editable en realidad aumentada Dibuja el texto en un canvas y úsalo como `THREE.CanvasTexture` en un `THREE.Sprite`. Cuando cambia el campo de texto, vuelve a dibujar y marca la textura con `needsUpdate = true`. Añade el sprite al ancla del marcador o al ancla espacial después de `ar-ready`. [Ejemplo 4](../examples/PRUEBAS/04-texto-marcador.html) · [Ejemplo 10](../examples/PRUEBAS/10-texto-slam.html) ## Controlar la iluminación En `ar-ready`, obtén la escena de Three.js desde `scene.app.scene`. Busca la luz direccional y usa el valor del deslizador para cambiar su posición. Aplica el valor inicial al cargar la escena y vuelve a aplicarlo en el evento `input`. [Ejemplo 6](../examples/PRUEBAS/06-iluminacion-marcador.html) · [Ejemplo 12](../examples/PRUEBAS/12-iluminacion-slam.html) ## Seleccionar partes del modelo Convierte la posición del puntero a coordenadas normalizadas del canvas y usa `THREE.Raycaster` con la cámara de la escena. Busca la primera malla intersectada y resalta su material. Guarda el estado original para restaurarlo al limpiar la selección. [Ejemplo 19](../examples/PRUEBAS/19-seleccionar-partes.html) ## Cambiar colores del catálogo Actualiza el atributo `src` del modelo y espera `updateComplete`. Después recorre el GLB para buscar las mallas que quieres teñir y aplica `material.color.set(...)`. Repite el ajuste al cambiar el selector de color. [Ejemplo 20](../examples/PRUEBAS/20-catalogo-colores.html) ## Combinar modelo texto y vídeo Coloca el GLB y el MP4 dentro del mismo `webar-target`. Añade el sprite de texto a esa misma ancla. Cada contenido conserva su propio control de visibilidad o reproducción. [Ejemplo 24](../examples/PRUEBAS/24-contenido-compuesto.html) --- Fuente: docs/video.md # Vídeo MP4 y chroma key ## Vídeo declarativo recomendado ```html ``` Carga el standalone del [inicio](getting-started.md). La biblioteca gestiona reproducción y muestra un botón si el navegador la bloquea. Width, orientación y chroma se actualizan sin recargar; src puede reemplazarse esperando element.updateComplete sin reabrir cámara. Referencia de todos los atributos en [interacción](interaction.md); [ejemplo completo](../examples/declarative-video.html). ## API programática avanzada Para los fragmentos siguientes parte de [integración programática](programmatic.md); `app` es un `ThreeARScene` creado por tu código. También existen componentes A-Frame. En esta API de bajo nivel setSource no reproduce automáticamente, a diferencia de la política predeterminada de la etiqueta. ```ts const clip=new ThreeARVideo({ width:0.2, orientation:'flat', muted:true, loop:true, chromaKey:{color:'#00ff00',similarity:0.12,smoothness:0.08,spill:0.3}, }); app.addContent('poster',clip); await clip.setSource('./presentacion.mp4'); document.querySelector('#play').onclick=()=>{ void clip.play().catch(error=>console.error('No se pudo reproducir',error)); }; ``` El ejemplo es un fragmento que supone `app`, un target `poster` y un botón `#play` existentes. Ejemplo completo con selección de archivo: [vídeo y chroma](../examples/video.html). ## Reproducción `setSource(Blob | string | URL)` espera metadata y establece el aspecto, pero **no inicia reproducción**. Usa `play()` desde una interacción; devuelve una promesa y puede ser rechazada por la política del navegador. `muted:true` facilita reproducción, sin eliminar la necesidad de manejar errores. `pause()` pausa sin destruir. `videoElement` permite controlar tiempo, volumen y eventos HTMLVideoElement. No todos los MP4 tienen el mismo codec: la compatibilidad depende del navegador. Usa un perfil H.264 compatible con tu público y prueba el archivo real en los teléfonos objetivo. No existe una conversión automática de codec. El flujo nativo del elemento de vídeo y su textura sigue avanzando aunque falte tracking. No llames `setSource`, `play` o `currentTime=0` desde `onPose`/`targetFound` para cada frame. Ocultar el contenido no pausa automáticamente el MP4: si quieres ahorrar batería al perder el target, decide esa política en tu aplicación y acepta sus efectos sobre continuidad. ## Geometría y opciones `width` debe ser positivo. La altura se obtiene de la relación de aspecto del vídeo. `orientation:'flat'` coloca la pantalla en XZ sobre la imagen; `'upright'` la coloca vertical, con base Y=0. `setWidth(number)` cambia el tamaño. `mesh` expone la superficie renderizada. ```ts clip.setChromaKey(false); clip.setChromaKey({color:'#0000ff',similarity:0.15,smoothness:0.06,spill:0.2}); ``` El chroma se calcula en GPU. `similarity` y `spill` van de 0 a 1; `smoothness` debe ser >0 y <=1. No elimina automáticamente cualquier fondo: necesita un fondo de color suficientemente uniforme y ajustes según iluminación. Eliminar verde no convierte un MP4 en un archivo con canal alfa ni resuelve oclusión con objetos del mundo. Para reproducir archivos locales puedes pasar directamente el `File`. El wrapper gestiona sus blob URLs y las revoca al sustituir o disponer. Para URL remota el servidor debe permitir el uso de medios en WebGL mediante CORS; se recomienda mismo origen. `dispose()` es final: pausa, libera textura/material/geometría y las URLs propias. Si usas `ThreeARScene`, registra el clip con `addContent` para que la escena gestione su ciclo de vida. --- Fuente: docs/threejs.md # Integración con Three.js Guía avanzada para una aplicación que ya necesita código Three.js o un renderer propio. Para imagen/GLB y SLAM básicos utiliza [HTML declarativo](getting-started.md), que ya configura estas clases. No crees otro renderer sobre una escena declarativa. ## Escena gestionada ```ts const app=new ThreeARScene({ container:document.querySelector('#view'), maxPixelRatio:2, lighting:true, engineOptions:{opencvURL:new URL('./vision/opencv.js',document.baseURI)}, }); const model=new ThreeARModel(); app.addContent('poster',model); await model.load('./modelo.glb'); await app.engine.startCamera(); await app.engine.addImageTarget('poster','./poster.png'); ``` En TypeScript valida que `querySelector` devuelve un `HTMLElement` antes de pasarlo al constructor. El ejemplo es un fragmento para insertar después de importar las clases. El HTML completo está en [integración programática](programmatic.md). `app.engine`, `app.scene`, `app.camera` y `app.renderer` son públicos. `getAnchor(id)` devuelve un `ThreeARGroup` enlazado a ese ID. `addContent(id,object)` añade contenido y registra modelos/vídeos para su gestión; `removeContent(object)` lo separa sin destruirlo. `onRender(callback)` ejecuta lógica independiente del tracker y devuelve una función de retirada. ```js const {THREE}=WebARFacil; // Con standalone; en ESM: import * as THREE from 'three'. const box=new THREE.Mesh(new THREE.BoxGeometry(.05,.05,.05),new THREE.MeshNormalMaterial()); box.position.y=.025; app.addContent('poster',box); const off=app.onRender(dt=>{box.rotation.y+=dt;}); // Al retirar contenido de creación propia: off();app.removeContent(box);box.geometry.dispose();box.material.dispose(); ``` `ThreeARScene.dispose()` libera sus modelos y vídeos registrados, pero los recursos de mallas propias pertenecen a la aplicación. No instales un segundo `setAnimationLoop` en su renderer: usa `onRender`. ## Renderer existente [Ejemplo 29 · Three.js con renderer propio](../examples/PRUEBAS/29-threejs.html) muestra un GLB animado sobre un marcador con la cámara, la proyección y el bucle de render controlados desde JavaScript. Usa `WebAREngine`, `ThreeARGroup` y `ThreeCameraBackground`. [custom-app.html](../examples/custom-app.html) es una aplicación completa con control manual del viewport, fondo, matrices, animación y limpieza. ```ts const tracked=new ThreeARGroup().bind(engine,'poster'); scene.add(tracked); camera.matrixAutoUpdate=false; const offPose=engine.onPose((_matrix,pose)=>{ if(pose.backend==='webxr') return; // En XR el renderer XR controla las cámaras. camera.matrix.fromArray(pose.viewMatrix).invert(); camera.updateMatrixWorld(true); if(pose.projectionMatrix){ camera.projectionMatrix.fromArray(pose.projectionMatrix); camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert(); } }); ``` `bind` sincroniza `worldMatrix`, visibilidad, tolerancia y estado del motor. `unbind()` retira los listeners. `dispose()` desuscribe el grupo, pero no destruye sus hijos. `update(matrix,pose?)` permite integración manual: con metadata usa la matriz de mundo; sin metadata solo dispones de ModelView y debes mantener la cámara identidad. El fondo y la proyección deben representar el mismo recorte/aspecto del vídeo. No estires el feed con CSS mientras mantienes otra proyección. `ThreeARScene` hace letterbox con la proporción de la cámara. `ThreeCameraBackground` permite rectificación cuando se proporciona calibración con distorsión; no exige un proceso manual para tracking normal. ## XR y fallos de contexto El wrapper gestionado configura el renderer XR. Un renderer propio debe suministrar `attachXRSession`, `getXRContext` y su bucle XR antes de usar el backend nativo. No dibujes el vídeo getUserMedia como fondo durante una sesión inmersiva: el compositor XR aporta la cámara real. Al perderse el contexto WebGL, `ThreeARScene` detiene el motor. Tras restaurarlo vuelve el render, pero tu aplicación debe permitir reiniciar cámara y cargar targets otra vez. No se oculta este fallo con una pose artificial. --- Fuente: docs/aframe.md # Integración con A-Frame Integración opcional para proyectos que ya usan A-Frame. Las etiquetas `webar-scene`, `webar-target` y `webar-model` de [inicio](getting-started.md) son Web Components propios y no requieren este framework. No mezcles sus schemas ni crees dos motores para el mismo contenido. Los wrappers registran tres componentes: `my-ar-tracker`, `my-ar-model` y `my-ar-video`. A-Frame es una dependencia opcional para estas aplicaciones; el ejemplo 25 incluye una copia local. Utiliza una versión compatible y prueba tu combinación concreta. Se verifica el contrato de componentes y la combinación concreta del ejemplo, no todas las versiones del framework. La [receta completa 25](../examples/PRUEBAS/25-aframe.html) incorpora ahora una copia local opcional de A-Frame 1.8.0 en examples/assets y utiliza el standalone solicitado. Su prueba ejecuta el framework real, los wrappers de tracking/modelo, renderizado, cierre de tracks y reinicio con cámara sintética. No añade look-controls: la cámara recibe matrices del tracker. Esta combinación concreta no implica compatibilidad con todas las versiones ni certificación de cámara física. La variante siguiente con el bundle externo permite compartir AFRAME.THREE en integraciones avanzadas. ## Configuración de una página Ejemplo de integración de imagen. Copia la distribución a la raíz del sitio, añade tu propia copia compatible de `vendor/aframe.min.js`, `poster.jpg` y `modelo.glb`. Se reutiliza `AFRAME.THREE` para que renderer y wrappers compartan la misma copia de Three.js. ```html

``` El ejemplo gestiona imágenes. Para WebXR hace falta integrar explícitamente la sesión con el renderer A-Frame: el registro de componentes no instala por sí solo los hooks XR del motor. Para una escena XR gestionada usa `ThreeARScene` o implementa esos hooks de acuerdo con tu versión de A-Frame. ## Propiedades exactas | Componente | Propiedades y valores iniciales | | --- | --- | | `my-ar-tracker` | `target: ''`, `targetId: 'default'`, `physicalWidth: 0.2` | | `my-ar-model` | `src: ''`, `fitSize: 0.16`, `autoplay: true` | | `my-ar-video` | `src: ''`, `width: 0.2`, `chromaKey: false`, `keyColor: '#00ff00'`, `similarity: 0.12`, `smoothness: 0.08`, `spill: 0.3`, `muted: true`, `loop: true` | La propiedad A-Frame `physicalWidth` corresponde a `physicalWidthMeters` del motor. Respeta mayúsculas y nombres del schema: no existe una propiedad `marker`, `modelUrl` ni `orientation` en el schema del componente de vídeo. El tracker reconoce `.dat` por extensión y usa `addTarget`; otras rutas se compilan como imagen. Sus configuraciones se serializan entre entidades. No hagas cargas manuales simultáneas del motor mientras los componentes están configurándose. No cambies el `targetId` de una entidad activa para administrar propiedad de targets implícitamente: retira explícitamente el target anterior cuando corresponda. Al retirarse un componente se restauran sus matrices y listeners. La retirada de la entidad no elimina automáticamente el target del motor: si esa entidad era su propietaria, usa `await engine.removeTarget(id)`. ## Eventos y vídeo `ar-error`, `ar-model-error`, `ar-video-error` y `ar-video-playblocked` llevan el `Error` en `event.detail`. `ar-model-loaded` lleva `{animations: string[]}`; `ar-video-loaded` indica metadata lista. Son eventos DOM del componente, distintos de `engine.on(...)`. ```html ``` El componente intenta reproducir tras cargar; si la política lo bloquea, ofrece un botón que llame a `document.querySelector('#clip').components['my-ar-video'].video.play()` y gestione el rechazo. La instancia del componente de modelo expone `.model`; la de vídeo, `.video`. --- Fuente: docs/maps.md # Persistencia de mapas locales ## Qué es un mapa en esta biblioteca Al mover el teléfono, el backend óptico sigue detalles de la imagen, estima puntos 3D y conserva algunas vistas de referencia (keyframes). Es una memoria local y dispersa del entorno: permite relacionar observaciones nuevas con lugares ya vistos y estimar la cámara respecto a ellos. No es una fotografía, un marcador compilado ni una malla completa de la habitación. El ancla describe dónde se colocó el contenido dentro de ese sistema de coordenadas. Si se pierde el seguimiento, la relocalización intenta reconocer las referencias guardadas y comprobar geométricamente una pose. Guardar un ancla por sí sola no permite saber dónde está ahora el teléfono. Durante una sesión el mapa se mantiene en memoria automáticamente. Persistirlo entre sesiones es optativo mediante exportSLAMMap/importSLAMMap y almacenamiento de la aplicación. El mapa conserva hasta 50 fotogramas clave y usa referencias locales para relocalizarse. Su escala es relativa al iniciar sin referencia métrica; una semilla válida de marcador con tamaño físico conocido puede establecer escala métrica. Por ejemplo: colocas un modelo sobre una mesa, mueves el teléfono y vuelves a mirar la mesa. Las referencias del mapa pueden ayudar a recuperar el seguimiento. Si cambió la apariencia, falta textura o la cámara/proyección no es compatible, esa recuperación puede fallar. No se garantiza reconocer la misma habitación en otro móvil o al día siguiente. ## Guardar de forma optativa El [HTML completo 13-mapas-locales](../examples/PRUEBAS/13-mapas-locales.html) muestra guardar, recuperar y borrar mediante IndexedDB. Usa el backend óptico y colocación manual; oculta el contenido recuperado hasta recibir una relocalización real. Permite detener/reiniciar con cámara y configuración compatibles. Eliminar guardado borra la copia persistida, sin destruir el mapa activo en memoria. Función avanzada con código de aplicación. Si iniciaste mediante etiquetas, accede a la instancia actual con `scene.engine` después de `ar-ready`; espera además a disponer de un mapa exportable antes de guardarlo. `ar-ready` no garantiza geometría suficiente. El motor interno cambia en cada reinicio declarativo. `exportSLAMMap()` e `importSLAMMap()` operan únicamente con el backend óptico activo. No son exportaciones de mapas ARCore/ARKit. El seguimiento debe haber construido un mapa local exportable con keyframes válidos. ```ts const snapshot=await engine.exportSLAMMap(); // snapshot contiene typed arrays. IndexedDB conserva esos tipos por structured clone. await saveToIndexedDB('escena-1',snapshot); // Adaptador de tu aplicación. ``` `saveToIndexedDB` no es un método de la biblioteca. Implementa almacenamiento, consentimiento, cuota y borrado según tu producto; [recetas programáticas](recipes-programmatic.md) incluye un adaptador IndexedDB completo. No conviertas directamente `Float32Array` y `Uint8Array` mediante JSON y esperes recibir los mismos tipos al cargar. ## Contrato del snapshot `SLAMMapSnapshot` incluye `version:1`, `map`, `anchor:Float32Array | null` y `targetId`. El mapa incluye intrínsecos, `scale`, `mapId`, keyframes con descriptores, puntos 3D y matrices de vista, y opcionalmente un plano verificado. No almacena imágenes de cámara en el snapshot, pero la geometría y descriptores pueden seguir siendo información sensible del lugar. No los publiques por defecto. La biblioteca no sube mapas a un servidor. ## Restaurar ```ts // Primero inicia cámara y backend óptico con configuración compatible. await engine.importSLAMMap(snapshot); // A continuación debe observar el entorno y relocalizarse. // No publiques snapshot.anchor como pose actual de cámara. ``` La importación valida dimensiones, versión, geometría, IDs y proyección, restablece el estado y espera relocalización. No se emite una pose guardada como si fuese una observación nueva. El entorno tiene que conservar suficiente apariencia y geometría. Utiliza la misma cámara, lente, modo de captura y proyección compatibles. En una instancia nueva puedes configurar `calibration: snapshot.map.intrinsics` para conservar la proyección de ese mapa, con `camera.maxFrameWidth` suficiente para sus dimensiones. Esto no convierte unos intrínsecos estimados en calibración física medida y no garantiza portabilidad entre dispositivos. Si el mapa es incompatible, recházalo y vuelve a mapear. Conserva también el perfil y los límites de análisis compatibles. El motor fuerza el ancho del mapa importado y no lo reduce para satisfacer un presupuesto menor: hacerlo invalidaría sus referencias. Cambiar perfil durante la sesión declarativa reinicia el motor; no transforma automáticamente un mapa entre resoluciones, cámaras o lentes. El mapa es local y acotado; no ofrece cierre de bucle global, fusión IMU ni anclas compartidas multijugador. Guardar y recuperar un mapa no elimina por sí mismo la deriva. Consulta [API avanzada](advanced.md) para extender el backend. --- Fuente: docs/api-engine.md # WebAREngine Referencia práctica. Las firmas completas y tipos de opciones están en [API generada](api-generated.md). Esta es la API programática avanzada. Para aplicaciones básicas empieza con [HTML declarativo](getting-started.md). Una escena HTML expone su motor como `scene.engine` tras `ar-ready`, sin que debas construirlo. Recuerda que el motor se reemplaza al parar/reiniciar la escena declarativa. ```ts const engine=new WebAREngine({ opencvURL:new URL('./vision/opencv.js',document.baseURI), imageLossGraceMs:500, keepContentOnLost:false, imageSmoothingMs:65, camera:{width:1280,height:720,fps:60,maxFrameWidth:640}, }); ``` ## Opciones principales | Opción | Valor inicial / contrato | | --- | --- | | `opencvURL` | `./vision/opencv.js`, relativo a `document.baseURI`; configurar si el HTML está en subcarpeta | | `camera` | Opciones de `CameraManager` salvo callbacks `onFrame` y `onError` | | `tracker` | Opciones de `Tracker` | | `imageLossGraceMs` | 500 ms; entre 0 y 10000; 0 confirma pérdida inmediatamente | | `keepContentOnLost` | `false`; permite conservar visualmente la última transformación | | `imageSmoothingMs` | 65 ms; entre 0 y 500; 0 desactiva filtrado temporal | | `calibration` | Intrínsecos opcionales; no se exigen para uso normal | | `fallback` | Opciones de odometría o `false`; por defecto se usa el backend incluido | | `keyframeCorrection` | false; corrección local experimental, no cierre de bucle global | | `attachXRSession`, `getXRContext`, `overlayRoot` | Hooks para integrar XR con un renderer propio | `ThreeARScene` conecta los hooks XR automáticamente. El núcleo sin renderer requiere que la aplicación los suministre para WebXR. Si proporcionas opciones explícitas de fallback respeta el contrato `OpticalFlowOptions`, incluidos `intrinsics` y `floorROI`; no son simples flags de calidad. ## Ciclo de vida y targets | Método | Resultado y comportamiento | | --- | --- | | `startCamera()` | `Promise`; abre la cámara | | `loadImageTarget(source, options?)` | Compila JPG/PNG y **reemplaza todos** los targets por `default` | | `addImageTarget(id, source, options?)` | Compila y añade/reemplaza solo ese ID | | `loadTarget(url)` | Carga `.dat` y reemplaza todos por `default` | | `addTarget(id, url)` | Añade/reemplaza un `.dat` por ID | | `removeTarget(id)` | Elimina tracking y visibilidad de ese target | | `setKeepContentOnLost(enabled, targetId?)` | Cambia política global o de un target existente | | `setFocus(mode, distance?)` | `Promise`; `false` si no existe soporte | | `stop()` | `Promise`; cierra sesión y elimina targets; instancia reutilizable | | `dispose()` | `void`; liberación definitiva | `source` acepta `Blob`/`File`, string URL o `URL`. `ImageTargetOptions` tiene `physicalWidthMeters` (por defecto 0.2) y `maxDimension` (por defecto 1024). Los métodos de imagen devuelven `{buffer, metadata}`. El JSON no es un argumento requerido del tracker. Los IDs deben cumplir `^[a-zA-Z0-9_-]{1,64}$`. Se admiten hasta 8. Espera las operaciones de configuración en orden; si ejecutas `startCamera()`, carga, retirada o transición simultáneamente, el motor puede rechazar la operación concurrente. ```ts await engine.startCamera(); const result=await engine.addImageTarget('producto','./producto.jpg',{ physicalWidthMeters:0.15, }); console.log(result.metadata.quality); // No usar Promise.all para estas operaciones de configuración del motor. ``` ## Propiedades de consulta `state`, `videoElement`, `cameraSettings`, `calibration` y `targetIds` son de lectura. `targetIds` permite comprobar qué IDs están registrados; no es una lista de marcadores visibles. `cameraSettings` puede ser `null` sin captura activa. ## Observación y eventos ```ts const unsubscribe=engine.onPose((matrix,pose)=>{ // matrix === pose.matrix; usar el contrato completo si la cámara se mueve. console.log(pose.targetId,pose.backend,pose.scale); }); const unsubscribeError=engine.onError(error=>console.error(error)); // Al abandonar esta pantalla: unsubscribe();unsubscribeError(); ``` `onStatus(listener)`, `onSurface(listener)` y `onMetrics(listener)` también devuelven una función de retirada. `onSurface` recibe una matriz de la superficie propuesta; no implica que un objeto ya esté colocado. `on(name,listener)` acepta únicamente `targetFound`, `targetLost`, `trackingQuality` y `targetVisibility`; existen los atajos `onTargetFound`, `onTargetLost`, `onTrackingQuality` y `onTargetVisibility`. No existe un evento genérico `pose` en `on`: usa `onPose`. Las cadenas de `onStatus` son diagnósticos abiertos; no sustituyen los eventos tipados de detección. ## Seguimiento espacial `enableSLAM(options?)` devuelve `Promise`. Opciones: `targetId`, `backend: 'auto' | 'webxr' | 'optical-flow'`, `placement: 'auto' | 'preserve' | 'hit-test'`, `initializationTimeoutMs` (12000 por defecto, >0 y <=120000). Resultados `backend: 'webxr' | 'optical-flow'` y `placement: 'preserved' | 'scan-marker' | 'select-surface' | 'scan-floor'`. Solo `preserved` confirma continuidad ya conseguida. Lee [transiciones y límites](spatial-tracking.md) antes de conectar el botón. `exportSLAMMap()` e `importSLAMMap(snapshot)` operan sobre la odometría activa; consulta [persistencia](maps.md). No existe un método público del motor llamado `switchToSLAM`: esa función pertenece a la clase avanzada `SLAMManager`, con otro contrato. --- Fuente: docs/api-generated.md # API pública generada Generada desde los exports fuente y las declaraciones TypeScript del build. No editar a mano. Los imports relativos se muestran en su contexto original. Estos bloques describen contratos, no son implementaciones para pegar en una app. Las interfaces auxiliares describen parámetros y resultados; solo los nombres de la tabla son exports del entrypoint público. | Export | Clase de export | Entrypoints | | --- | --- | --- | | `ARAnimationOptions` | Tipo | `webar-facil` | | `ARComponentsOptions` | Tipo | `webar-facil` | | `ARDiagnostics` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `AREventMap` | Tipo | `webar-facil/core`, `webar-facil` | | `ARModelElement` | Tipo | `webar-facil` | | `ARModelLoadOptions` | Tipo | `webar-facil` | | `ARModelOptions` | Tipo | `webar-facil` | | `ARPose` | Tipo | `webar-facil/core`, `webar-facil` | | `ARSceneElement` | Tipo | `webar-facil` | | `ARSceneOptions` | Tipo | `webar-facil` | | `ARVideoElement` | Tipo | `webar-facil` | | `ARVideoOptions` | Tipo | `webar-facil` | | `CalibrationReport` | Tipo | `webar-facil/core`, `webar-facil` | | `CalibrationSession` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `CameraFrame` | Tipo | `webar-facil/core`, `webar-facil` | | `CameraIntrinsics` | Tipo | `webar-facil/core`, `webar-facil` | | `CameraManager` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `CameraManagerOptions` | Tipo | `webar-facil/core`, `webar-facil` | | `CameraMetrics` | Tipo | `webar-facil/core`, `webar-facil` | | `ChromaKeyOptions` | Tipo | `webar-facil` | | `CompilerOptions` | Tipo | `webar-facil/core`, `webar-facil` | | `DEVICE_TEST_PROTOCOL` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `DeviceTestCase` | Tipo | `webar-facil/core`, `webar-facil` | | `DeviceTestResult` | Tipo | `webar-facil/core`, `webar-facil` | | `DiagnosticConfiguration` | Tipo | `webar-facil/core`, `webar-facil` | | `DiagnosticContext` | Tipo | `webar-facil/core`, `webar-facil` | | `DiagnosticOptions` | Tipo | `webar-facil/core`, `webar-facil` | | `DiagnosticRendererMetrics` | Tipo | `webar-facil/core`, `webar-facil` | | `DiagnosticReport` | Tipo | `webar-facil/core`, `webar-facil` | | `DiagnosticStats` | Tipo | `webar-facil/core`, `webar-facil` | | `EngineOptions` | Tipo | `webar-facil/core`, `webar-facil` | | `EngineState` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `ExperimentalMetricScale` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `FrameQuality` | Tipo | `webar-facil/core`, `webar-facil` | | `ImageCompiler` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `ImageObservation` | Tipo | `webar-facil/core`, `webar-facil` | | `ImageTargetMetadata` | Tipo | `webar-facil/core`, `webar-facil` | | `ImageTargetOptions` | Tipo | `webar-facil/core`, `webar-facil` | | `ImageTargetResult` | Tipo | `webar-facil/core`, `webar-facil` | | `ImageTrackingProfile` | Tipo | `webar-facil/core`, `webar-facil` | | `LIBRARY_BUILD` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `LIBRARY_VERSION` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `MarkerCompiler` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `MarkerFeatures` | Tipo | `webar-facil/core`, `webar-facil` | | `MarkerQuality` | Tipo | `webar-facil/core`, `webar-facil` | | `MetricScaleDiagnostics` | Tipo | `webar-facil/core`, `webar-facil` | | `OdometryBackend` | Tipo | `webar-facil/core`, `webar-facil` | | `OdometryConfig` | Tipo | `webar-facil/core`, `webar-facil` | | `OdometryEstimate` | Tipo | `webar-facil/core`, `webar-facil` | | `OdometryMapSnapshot` | Tipo | `webar-facil/core`, `webar-facil` | | `OdometryModule` | Tipo | `webar-facil/core`, `webar-facil` | | `OdometryScanHint` | Tipo | `webar-facil/core`, `webar-facil` | | `OpticalFlowOptions` | Tipo | `webar-facil/core`, `webar-facil` | | `OpticalFlowSLAM` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `PlacementState` | Tipo | `webar-facil` | | `projectionFromIntrinsics` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `registerAFrameComponent` | Valor y/o tipo | `webar-facil` | | `registerAFrameModelComponent` | Valor y/o tipo | `webar-facil` | | `registerAFrameVideoComponent` | Valor y/o tipo | `webar-facil` | | `registerARComponents` | Valor y/o tipo | `webar-facil` | | `rotateIntrinsics` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `scaleIntrinsics` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `SLAMManager` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `SLAMMapSnapshot` | Tipo | `webar-facil/core`, `webar-facil` | | `SLAMOptions` | Tipo | `webar-facil/core`, `webar-facil` | | `SurfaceTransitionOptions` | Tipo | `webar-facil/core`, `webar-facil` | | `SurfaceTransitionResult` | Tipo | `webar-facil/core`, `webar-facil` | | `TargetFoundEvent` | Tipo | `webar-facil/core`, `webar-facil` | | `TargetLostEvent` | Tipo | `webar-facil/core`, `webar-facil` | | `TargetVisibilityEvent` | Tipo | `webar-facil/core`, `webar-facil` | | `ThreeARGroup` | Valor y/o tipo | `webar-facil` | | `ThreeARModel` | Valor y/o tipo | `webar-facil` | | `ThreeARScene` | Valor y/o tipo | `webar-facil` | | `ThreeARVideo` | Valor y/o tipo | `webar-facil` | | `ThreeCameraBackground` | Valor y/o tipo | `webar-facil` | | `Tracker` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `TrackerOptions` | Tipo | `webar-facil/core`, `webar-facil` | | `TrackingIssue` | Tipo | `webar-facil/core`, `webar-facil` | | `TrackingQualityEvent` | Tipo | `webar-facil/core`, `webar-facil` | | `TrackingResult` | Tipo | `webar-facil/core`, `webar-facil` | | `validateIntrinsics` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | | `VisualReferenceResult` | Tipo | `webar-facil/core`, `webar-facil` | | `WebAREngine` | Valor y/o tipo | `webar-facil/core`, `webar-facil` | El bundle standalone añade el namespace `THREE`. Consulta [API del motor](api-engine.md), [Three.js](threejs.md) y [API avanzada](advanced.md) para ejemplos y valores iniciales. ## Pose.d.ts ```ts /** Todos los backends entregan el mismo contrato, sin tipos del renderer. */ export interface ARPose { targetId?: string; backend: 'image' | 'webxr' | 'optical-flow'; timestampMs: number; scale: 'metric' | 'relative'; /** Session-only metric estimate from the experimental optical/inertial mode. */ scaleSource?: 'experimental-inertial'; /** T_camera_object: column-major WebGL. */ matrix: Float32Array; /** T_world_object. En imagen, el mundo de render es el espacio de cámara. */ worldMatrix: Float32Array; /** T_camera_world. */ viewMatrix: Float32Array; /** null si el backend no dispone de calibración; el renderer decide su FOV. */ projectionMatrix: Float32Array | null; projectionSource?: 'calibrated' | 'estimated' | 'webxr'; /** El ancla óptica coincide con el plano verificado por el estimador. */ surfaceDetected?: boolean; diagnostics?: { processingMs: number; inliers: number; reprojectionErrorPx: number; relocalized?: boolean; keyframeCorrected?: boolean; }; } export interface CameraIntrinsics { width: number; height: number; fx: number; fy: number; cx: number; cy: number; distortion?: readonly number[]; } ``` ## WebAREngine.d.ts ```ts import { type CameraManagerOptions, type CameraMetrics } from './camera/CameraManager.js'; import { type TrackerOptions } from './tracking/Tracker.js'; import { type MarkerQuality } from './tracking/ImageCompiler.js'; import { type OpticalFlowOptions } from './slam/OpticalFlowSLAM.js'; import type { ExperimentalMetricScale } from './slam/ExperimentalMetricScale.js'; import type { ARPose, CameraIntrinsics } from './Pose.js'; import type { OdometryMapSnapshot, VisualReferenceResult } from './slam/OdometryBackend.js'; import type { AREventMap } from './tracking/events.js'; export declare enum EngineState { Idle = "Idle", Camera = "Camera", TrackingImage = "TrackingImage", TrackingSurface = "TrackingSurface", Disposed = "Disposed" } export interface EngineOptions { /** Optional session-only conversion of relative optical poses into estimated meters. */ experimentalMetricScale?: ExperimentalMetricScale; /** Corrección local experimental; desactivada por defecto. No es cierre de bucle global. */ keyframeCorrection?: boolean; /** Ocultar tras una pérdida sostenida; sin publicar poses durante la incertidumbre. 0 desactiva la tolerancia. */ imageLossGraceMs?: number; keepContentOnLost?: boolean; imageSmoothingMs?: number; camera?: Omit; tracker?: TrackerOptions; opencvURL?: string | URL; calibration?: CameraIntrinsics; /** false deshabilita el fallback. Por defecto se usa el backend local integrado. */ fallback?: OpticalFlowOptions | false; attachXRSession?: (session: XRSession) => Promise; getXRContext?: () => WebGL2RenderingContext; overlayRoot?: HTMLElement; } export interface ImageTargetOptions { /** Opcional: escala de referencia de 0.2 m por defecto; no requiere medir el marcador. */ physicalWidthMeters?: number; maxDimension?: number; } export interface ImageTargetMetadata { format: 'WART'; version: 2; width: number; height: number; physicalWidthMeters: number; extractorId: number; quality: MarkerQuality; } export interface ImageTargetResult { /** Copia independiente: puede guardarse como .dat sin afectar al motor. */ buffer: ArrayBuffer; /** Datos listos para JSON.stringify; no se necesita cargar ningún JSON. */ metadata: ImageTargetMetadata; } export interface SurfaceTransitionOptions { targetId?: string; backend?: 'auto' | 'webxr' | 'optical-flow'; placement?: 'auto' | 'preserve' | 'hit-test'; initializationTimeoutMs?: number; } export interface SurfaceTransitionResult { backend: 'webxr' | 'optical-flow'; placement: 'preserved' | 'scan-marker' | 'select-surface' | 'scan-floor'; } export interface SLAMMapSnapshot { version: 1; map: OdometryMapSnapshot; anchor: Float32Array | null; targetId: string; } /** Fachada: cámara, tracking de imagen y espacios nativos/locales comparten ARPose. */ export declare class WebAREngine { constructor(options?: EngineOptions); get state(): EngineState; get videoElement(): HTMLVideoElement; get cameraSettings(): MediaTrackSettings | null; get calibration(): CameraIntrinsics | undefined; setFocus(mode: 'continuous' | 'manual', distance?: number): Promise; onPose(fn: (matrix: Float32Array, pose: ARPose) => void): () => void; onStatus(fn: (message: string) => void): () => void; onError(fn: (error: Error) => void): () => void; onSurface(fn: (matrix: Float32Array) => void): () => void; onMetrics(fn: (metrics: CameraMetrics) => void): () => void; get targetIds(): readonly string[]; on(name: K, listener: (event: AREventMap[K]) => void): () => void; onTargetFound(listener: (event: AREventMap['targetFound']) => void): () => void; onTargetLost(listener: (event: AREventMap['targetLost']) => void): () => void; onTrackingQuality(listener: (event: AREventMap['trackingQuality']) => void): () => void; onTargetVisibility(listener: (event: AREventMap['targetVisibility']) => void): () => void; /** Retener es congelar la ultima transformacion, no crear un anclaje SLAM. */ setKeepContentOnLost(value: boolean, targetId?: string): void; /** Compare a fresh optical frame against the pinned reference without correcting the live pose. */ checkSLAMVisualReference(action: 'capture' | 'verify', id: string): Promise; exportSLAMMap({ includeCurrentView }?: { includeCurrentView?: boolean; }): Promise; importSLAMMap(snapshot: SLAMMapSnapshot): Promise; startCamera(): Promise; loadTarget(url: string | URL): Promise; addTarget(id: string, url: string | URL): Promise; /** Compila JPG/PNG en Worker y activa el marcador sin archivos intermedios. */ loadImageTarget(source: Blob | string | URL, options?: ImageTargetOptions): Promise; addImageTarget(id: string, source: Blob | string | URL, options?: ImageTargetOptions): Promise; removeTarget(id: string): Promise; /** Llamar desde un clic. preserve exige una observación reciente y una alineación verificable. */ enableSLAM(options?: SurfaceTransitionOptions): Promise; stop(): Promise; dispose(): void; } ``` ## build-info.d.ts ```ts export declare const LIBRARY_VERSION: string; export declare const LIBRARY_BUILD = "1c906eb944d9f8f4"; ``` ## calibration/CalibrationSession.d.ts ```ts import type { CalibrationCapture, CalibrationReport } from './types.js'; /** API de calibración sin cómputo WASM en el hilo de UI. */ export declare class CalibrationSession { constructor(options?: { workerFactory?: () => Worker; }); initialize(opencvURL: string, signal?: AbortSignal): Promise; addFrame(frame: ImageData, signal?: AbortSignal): Promise; board(): Promise<{ data: Uint8ClampedArray; width: number; height: number; }>; solve(): Promise; reset(): Promise; dispose(): void; } ``` ## calibration/types.d.ts ```ts import type { CameraIntrinsics } from '../Pose.js'; export interface CalibrationObservation { width: number; height: number; objectPoints: Float32Array; imagePoints: Float32Array; } export interface CalibrationReport { calibration: CameraIntrinsics; rmsPx: number; perViewErrorsPx: number[]; views: number; coverage: number; warnings: string[]; } export interface CalibrationCapture { accepted: boolean; corners: Float32Array; views: number; reason: string; } ``` ## camera/CameraManager.d.ts ```ts import type { ImageTrackingProfile } from '../tracking/profile.js'; import type { TrackingQualityEvent } from '../tracking/events.js'; export interface CameraFrame { /** RGBA; el consumidor puede transferir imageData.data.buffer al Worker. */ imageData: ImageData; /** Tiempo de extracción, en el reloj performance.now(); no es tiempo del sensor. */ timestampMs: number; mediaTimeSeconds: number; sessionId: number; sourceWidth: number; sourceHeight: number; } export interface CameraManagerOptions { /** Resolver al terminar de procesar el frame, no solamente al enviarlo. */ onFrame: (frame: CameraFrame, signal: AbortSignal) => void | Promise; onError?: (error: Error) => void; /** Etapas reales de apertura; no confundir permiso concedido con vídeo reproduciéndose. */ onStatus?: (status: 'camera-requesting' | 'camera-granted' | 'camera-playing') => void; /** Visor opcional para montar el vídeo debajo del renderer durante la captura. */ playbackContainer?: HTMLElement; /** Presupuesto inicial; las opciones explícitas tienen prioridad. */ performance?: 'auto' | 'balanced' | 'battery' | 'quality'; width?: number; height?: number; /** Preferencia, no requisito: permite webcams sin cámara trasera. */ facingMode?: 'environment' | 'user'; /** Selección explícita; tiene prioridad sobre facingMode. */ deviceId?: string; /** Objetivo de captura, hasta 60; limitado por cámara, pantalla y CPU. */ fps?: number; /** Limita el coste de lectura RGBA conservando la relación de aspecto. */ maxFrameWidth?: number; /** Límite de píxeles analizados en cualquier orientación; no recorta la imagen. */ maxFramePixels?: number; focusMode?: 'continuous' | 'manual' | 'none'; focusDistance?: number; adaptive?: boolean; minFrameWidth?: number; onMetrics?: (metrics: CameraMetrics) => void; onSuspend?: () => void; onResume?: () => void; } export interface CameraMetrics { tracking?: ImageTrackingProfile; odometry?: import('../slam/OdometryBackend.js').OdometryTimings; consumerMs?: number; processedFrames: number; skippedFrames: number; readbackMs: number; processingMs: number; averageProcessingMs: number; analysisWidth: number; targetAnalysisFPS: number; analysisHeight?: number; performance?: CameraManagerOptions['performance']; } /** Webcam o cámara móvil. Usar con HTTPS o localhost. */ export declare class CameraManager { constructor(options: CameraManagerOptions); get state(): 'idle' | 'starting' | 'running' | 'paused' | 'disposed'; /** Vista previa del stream; la propiedad srcObject pertenece al gestor. */ get videoElement(): HTMLVideoElement; get settings(): MediaTrackSettings | null; /** Fijar resolución durante VO: cambiarla invalidaría el mapa. La frecuencia sí se adapta. */ setAdaptiveResolution(enabled: boolean): void; /** Priorizar detalle si bajar resolución puede hacer perder el marcador. */ setTrackingFeedback(qualities: readonly TrackingQualityEvent[], frameWidth?: number): void; /** Ancho explícito para semillas/mapas: prima sobre el presupuesto de píxeles. */ setAnalysisWidth(width: number): void; setFocus(mode: 'continuous' | 'manual', distance?: number): Promise; /** Idempotente durante una sesión. Los errores de arranque rechazan la promesa. */ start(): Promise; /** Cancela y libera la captura. retainDevice conserva la entrada para una transición interna. */ stop({ retainDevice }?: { retainDevice?: boolean; }): void; dispose(): void; } ``` ## diagnostics/ARDiagnostics.d.ts ```ts import { TRACKING_STAGES, TRACKING_COUNTS } from '../tracking/profile.js'; import type { WebAREngine } from '../WebAREngine.js'; import type { CameraMetrics } from '../camera/CameraManager.js'; import { ODOMETRY_STAGES } from '../slam/OdometryBackend.js'; export type DeviceTestResult = 'pending' | 'pass' | 'fail' | 'not-supported'; export interface DiagnosticContext { platform?: 'ios' | 'android' | 'desktop' | 'other'; execution?: 'unspecified' | 'automated' | 'physical-user-reported'; deviceModel?: string; osVersion?: string; browserVersion?: string; scenario?: string; } export interface DiagnosticConfiguration { mode?: string; backend?: string; performance?: string; maxFramePixels?: number; analysisWidth?: number; requestedFPS?: number; lossGraceMs?: number; keepContentOnLost?: boolean; manualPlacement?: boolean; keyframeCorrection?: boolean; renderPixelRatio?: number; antialias?: boolean; } export interface DiagnosticOptions { context?: DiagnosticContext; configuration?: DiagnosticConfiguration; sampleLimit?: number; now?: () => number; } export interface DiagnosticStats { count: number; mean: number | null; min: number | null; max: number | null; p50: number | null; p95: number | null; quantileSamples: number; } /** Contadores del renderer, no tiempo GPU ni bytes de VRAM. */ export interface DiagnosticRendererMetrics { drawCalls: number; triangles: number; geometries: number; textures: number; drawingBufferWidth: number; drawingBufferHeight: number; pixelRatio: number; } export interface DiagnosticReport { schemaVersion: 1; kind: 'webar-diagnostics'; sdk: { version: string; build: string; }; createdAt: string; durationMs: number; recording: boolean; context: Required; configuration: DiagnosticConfiguration; environment: { userAgent: string; secureContext: boolean; cameraAPI: boolean; webgl2API: boolean; xrAPI: boolean; immersiveAR: boolean | null; }; camera: Record; state: string; targets: Record; poses: { count: number; backends: Record; scales: Record; projectionSources: Record; relocalized: number; keyframeCorrected: number; firstPoseMs: number | null; }; quality: { samples: Record; issues: Record; diagnosis: 'heuristic'; }; metrics: { processingMs: DiagnosticStats; readbackMs: DiagnosticStats; consumerMs: DiagnosticStats; renderLoopMs: DiagnosticStats; poseStepTranslation: DiagnosticStats; poseStepRotationDegrees: DiagnosticStats; reprojectionErrorPx: DiagnosticStats; latestCamera: CameraMetrics | null; }; odometry: { frames: number; stages: Record; }; tracking: { frames: number; stages: Record; paths: Record; }; renderer: { latest: DiagnosticRendererMetrics | null; peakDrawCalls: number; peakTriangles: number; }; timeline: { atMs: number; event: string; detail: string; }[]; observations: { atMs: number; testId: string; result: DeviceTestResult; notes: string; }[]; limits: { sampleLimit: number; timelineLimit: number; observationLimit: number; quantiles: 'latest bounded samples'; poseSteps: 'not physical jitter or drift'; renderLoop: 'callback interval, not completed GPU frames'; physicalValidation: 'operator-declared, not independently certified'; }; } type Source = Pick; /** Telemetría local opt-in y acotada. No persiste frames/mapas ni exporta matrices/URLs de contenido. */ export declare class ARDiagnostics { constructor(source: Source, options?: DiagnosticOptions); setContext(context: DiagnosticContext): void; mark(event: string, detail?: string): void; observe(testId: string, result: DeviceTestResult, notes?: string): void; recordError(error: Error): void; recordRenderer(input: DiagnosticRendererMetrics): void; recordRenderFrame(): void; probeCapabilities(): Promise; stop(): void; snapshot(): DiagnosticReport; download(filename?: string): void; } export {}; ``` ## diagnostics/device-protocol.d.ts ```ts export interface DeviceTestCase { id: string; title: string; instructions: string; seconds: number; modes: readonly ('image' | 'flow' | 'transition' | 'webxr')[]; } /** Pasos manuales comunes a iOS y Android. Ninguno se aprueba automáticamente. */ export declare const DEVICE_TEST_PROTOCOL: readonly DeviceTestCase[]; ``` ## slam/ExperimentalMetricScale.d.ts ```ts type WindowRejection = 'visualCoverage' | 'sensorCoverage' | 'sensorGap' | 'weakVisualMotion' | 'weakInertialMotion'; type Interruption = 'poseGap' | 'trackingLost' | 'relocalized' | 'keyframeCorrection' | 'mapReset'; type AxisValues = { x: number; y: number; z: number; }; interface MotionSignal { windows: number; deviceAccelerationRmsMps2: AxisValues; worldAccelerationRmsMps2: AxisValues; integratedRmsMeters: AxisValues; meanAbsoluteIntegralMeters: AxisValues; } interface GuidedPhaseDiagnostics { visualPoses: number; visualDisplacementUnits: number | null; visualDisplacementMeters: number | null; visualCoverageMs: number; stationaryPoses: number; maxExcursionUnits: number | null; durationMs: number; samples: number; usableSamples: number; accelerationPeakMps2: AxisValues | null; accelerationRmsMps2: AxisValues | null; accelerationStdDevMps2: AxisValues | null; integratedWindows: number; weakWindows: number; } export interface MetricScaleDiagnostics { permission: 'idle' | 'listening' | 'denied' | 'unavailable'; motionEvents: number; samples: number; usableSamples: number; medianIntervalMs: number | null; largestGapMs: number; visualFrames: number; medianVisualIntervalMs: number | null; pairedWindows: number; relativeResidual: number | null; metersPerUnit: number | null; windowsEvaluated: number; windowsAccepted: number; rejectedWindows: Record; interruptions: Record; fitState: 'waiting' | 'inconsistent' | 'accepted'; fitAttempts: number; timing: { comparableWindows: number; estimatedOffsetMs: number | null; zeroOffsetResidual: number | null; bestResidual: number | null; }; lastTimingFit: MetricScaleDiagnostics['timing'] | null; motionSignal: { all: MotionSignal | null; weak: MotionSignal | null; guided: MotionSignal | null; guidedWeak: MotionSignal | null; }; guidedPhases: Record; } /** Experimental alignment of metric inertial and relative visual displacements. */ export declare class ExperimentalMetricScale { /** Call directly from a user gesture, before awaiting camera startup. */ start(): Promise; /** Keep the last diagnostic available after stopping. */ stop(): void; /** Aggregate the windows entirely contained in the current guided exercise. */ setGuidedMotionCapture(active: boolean): void; /** True after a continuous interval of fresh optical poses, including poses before plane detection. */ hasStableVisualTracking(now?: number): boolean; pauseGuidedMotionPhase(): void; /** Mark the start of a named exercise phase on the same clock as sensor delivery. */ setGuidedMotionPhase(id: string): void; diagnostics(): MetricScaleDiagnostics; /** Compare the observed positions at the beginning and end of a completed round trip. */ guidedReturnError(outboundId: string, inboundId: string): number | null; resetMap(): void; /** Exclude windows that cross a lost pose, correction, or relocalization. */ interrupt(reason?: Interruption): void; /** Receives unfiltered T_world_camera and the frame read time on the same page clock. */ observe(time: number, worldFromCamera: Float32Array, stationary?: boolean): number | null; } export {}; ``` ## slam/OdometryBackend.d.ts ```ts import type { CameraIntrinsics } from '../Pose.js'; import type { ImageObservation } from '../tracking/types.js'; export interface OdometryConfig { intrinsics: CameraIntrinsics; opencvURL?: string; seed?: ImageObservation; /** Experimental, desactivada por defecto: reobservación de keyframes locales. */ keyframeCorrection?: boolean; /** ROI normalizada para seleccionar esquinas candidatas del suelo. No prueba un plano. */ floorROI: { x: number; y: number; width: number; height: number; }; } /** Snapshot local: no imágenes de cámara; solo descriptores y geometría verificada. */ export interface OdometryMapSnapshot { version: 1; intrinsics: CameraIntrinsics; scale: 'relative' | 'metric'; mapId: string; keyframes: { descriptors: Uint8Array; points: Float32Array; view: Float32Array; }[]; groundPlane?: { worldFromPlane: Float32Array; inliers: number; }; /** La exportación añadió un fotograma clave de la vista actual. */ currentViewCaptured?: boolean; } export declare const ODOMETRY_STAGES: readonly [ "grayMs", "cornersMs", "flowMs", "flowNormalMs", "flowWideMs", "flowRetryMs", "initializationMs", "poseMs", "mapUpdateMs", "keyframesMs", "planeMs", "planeFitMs", "planeConsensusMs", "relocalizationMs", "workerMs" ]; export type OdometryTimings = Partial>; export declare const ODOMETRY_SCAN_HINTS: readonly [ "low-texture", "move-sideways", "map-initializing", "plane-unverified", "return-to-map", "map-matches-insufficient", "map-pose-unverified", "map-pose-confirming", "surface-ready" ]; /** Indicio de escaneo derivado del estimador local. */ export type OdometryScanHint = typeof ODOMETRY_SCAN_HINTS[number]; export type OdometryEstimate = ({ status: 'initializing' | 'lost'; mapId: string; } | { status: 'tracked'; mapId: string; /** Pose acumulada T_world_camera, NO delta entre frames. Convención WebGL. */ worldFromCamera: Float32Array; projectionMatrix: Float32Array; scale: 'relative' | 'metric'; trackedPoints: number; environmentReady?: boolean; processingMs?: number; reprojectionErrorPx?: number; relocalized?: boolean; keyframeCorrected?: boolean; /** Pose held against distributed, near-static visual correspondences. */ stationary?: boolean; /** Solo emitir tras triangulación y verificación geométrica de un plano. */ groundPlane?: { worldFromPlane: Float32Array; inliers: number; }; }) & { timings?: OdometryTimings; scanHint?: OdometryScanHint; }; /** ABI JavaScript del módulo local que encapsula el WASM del usuario. */ export interface OdometryBackend { /** * Pipeline esperado en WASM: gris -> Shi-Tomasi/FAST -> Lucas-Kanade piramidal * -> comprobación forward/backward -> geometría robusta -> keyframes/mapa -> pose. * Reponer puntos, detectar falta de paralaje y declarar lost ante degeneración. * Intrínsecos se escalan al tamaño RGBA recibido. No inferir traslación 3D * simplemente sumando desplazamientos de píxeles. */ processFrame(frame: { rgba: Uint8ClampedArray; width: number; height: number; timestampMs: number; }): OdometryEstimate | Promise; reset(): void | Promise; exportMap?(includeCurrentView?: boolean): OdometryMapSnapshot; importMap?(snapshot: OdometryMapSnapshot): void; checkVisualReference?(action: 'capture' | 'verify', id: string, timestampMs: number): VisualReferenceResult; } export type VisualReferenceResult = { status: 'captured'; points: number; } | { status: 'verified'; inliers: number; reprojectionErrorPx: number; medianImageShiftPx: number; directReturnUnits: number; trajectoryDeltaUnits: number; } | { status: 'unavailable' | 'insufficient-support' | 'missing-reference'; }; export interface OdometryModule { createOdometryBackend(config: OdometryConfig): Promise; } ``` ## slam/OpticalFlowSLAM.d.ts ```ts import type { CameraFrame } from '../camera/CameraManager.js'; import type { OdometryConfig, OdometryEstimate, OdometryMapSnapshot, VisualReferenceResult } from './OdometryBackend.js'; export interface OpticalFlowOptions extends OdometryConfig { /** Módulo ESM local que exporta createOdometryBackend; carga su propio .wasm. */ moduleURL?: URL; workerFactory?: () => Worker; } /** Cliente del backend WASM. No simula una pose si el backend no está disponible. */ export declare class OpticalFlowSLAM { constructor(options: OpticalFlowOptions); start(signal?: AbortSignal): Promise; processFrame(frame: CameraFrame, signal?: AbortSignal): Promise; checkVisualReference(action: 'capture' | 'verify', referenceId: string, signal?: AbortSignal): Promise; exportMap(signal?: AbortSignal, includeCurrentView?: boolean): Promise; importMap(snapshot: OdometryMapSnapshot, signal?: AbortSignal): Promise; stop(): void; dispose(): void; } ``` ## slam/SLAMManager.d.ts ```ts import type { ARPose } from '../Pose.js'; import type { CameraFrame } from '../camera/CameraManager.js'; import type { CameraIntrinsics } from '../Pose.js'; export interface SLAMOptions { /** El renderer configura XRWebGLLayer y su propio bucle de renderizado. */ attachSession: (session: XRSession) => Promise; overlayRoot?: HTMLElement; onPose: (pose: ARPose) => void; onSurface?: (matrix: Float32Array) => void; onLost?: () => void; onEnd?: () => void; onError?: (error: Error) => void; getGLContext?: () => WebGL2RenderingContext; requireCameraAccess?: boolean; onCameraFrame?: (frame: CameraFrame, worldFromCamera: Float32Array, intrinsics: CameraIntrinsics, referenceEpoch: number) => Promise; onAlignmentUnavailable?: (error: Error) => void; } /** Tracking nativo; las poses de hit-test no se presentan como una malla completa. */ export declare class SLAMManager { get imageAlignmentAvailable(): boolean; get hasPlacement(): boolean; get referenceEpoch(): number; constructor(options: SLAMOptions); static isSupported(): Promise; /** Llamar desde un gesto de usuario; requestSession se invoca antes del primer await. */ start(signal?: AbortSignal): Promise; /** * Solo preservar pose con T_world_camera alineada al MISMO frame del marcador. * Sin esa observación, habilitar hit-test y colocar mediante un gesto select. */ switchToSLAM(currentMatrix: Float32Array, alignedWorldFromCamera?: Float32Array): void; stop(): Promise; } ``` ## tools/MarkerCompiler.d.ts ```ts import type { MarkerQuality } from '../tracking/ImageCompiler.js'; export declare class MarkerCompiler { constructor(options?: { workerFactory?: () => Worker; }); /** JPG/PNG original; decodificación EXIF, ORB y binario se calculan en Worker. */ compile(file: Blob, options: { physicalWidthMeters: number; opencvURL: string; maxDimension?: number; }, signal?: AbortSignal): Promise<{ buffer: ArrayBuffer; quality: MarkerQuality; }>; dispose(): void; } ``` ## tracking/ImageCompiler.d.ts ```ts export declare const TARGET_HEADER_BYTES = 48; export declare const TARGET_MAX_BYTES: number; export declare const TARGET_MAX_FEATURES = 100000; export declare const ORB_DESCRIPTOR_BYTES = 32; export interface MarkerFeatures { width: number; height: number; /** x,y,ángulo en radianes,respuesta por punto; descriptores ORB de 32 bytes. */ keypoints: Float32Array; descriptors: Uint8Array; } export interface CompiledMarker extends MarkerFeatures { simulated: boolean; physicalWidthMeters: number; extractorId: number; } export interface DecodedImage { data: Uint8ClampedArray; width: number; height: number; } export interface CompilerOptions { /** Precalcular vistas acortadas para inclinaciones fuertes. true por defecto. */ affineViews?: boolean; physicalWidthMeters: number; opencvURL?: string; extractorId?: number; maxDimension?: number; /** Solo fixtures o compatibilidad: nunca se habilita implícitamente. */ simulate?: boolean; extract?: (encoded: Uint8Array) => Promise; decode?: (encoded: Uint8Array) => Promise; } export interface MarkerQuality { featureCount: number; coverage: number; duplicateRatio: number; usable: boolean; warnings: string[]; } /** CRC32 IEEE: corrupción de almacenamiento/transporte, no autenticación. */ export declare function crc32(bytes: Uint8Array): number; export declare function analyzeFeatures(features: MarkerFeatures): MarkerQuality; /** Compilador real; usar en un Worker de preprocesamiento o con el CLI de Node. */ export declare class ImageCompiler { compile(source: Uint8Array | Blob, options: CompilerOptions): Promise; compileRGBA(image: DecodedImage, options: CompilerOptions): Promise; } export declare function decodeImageBrowser(encoded: Uint8Array, max: number): Promise; export declare function encodeMarker(features: MarkerFeatures, options: CompilerOptions, simulated?: boolean): ArrayBuffer; /** Valida antes de reservar arrays. El llamador conserva el buffer de entrada. */ export declare function decodeMarker(buffer: ArrayBuffer): CompiledMarker; ``` ## tracking/Tracker.d.ts ```ts import { type ImageTrackingProfile } from './profile.js'; import type { CameraFrame } from '../camera/CameraManager.js'; import type { CameraIntrinsics } from '../Pose.js'; import type { TrackingResult, TrackingBatchResult } from './types.js'; export type { TrackingResult, ImageObservation } from './types.js'; export interface TrackerOptions { opencvURL?: string | URL; calibration?: CameraIntrinsics; allowEstimatedCalibration?: boolean; timeoutMs?: number; workerFactory?: () => Worker; } export declare class Tracker { get latestProfile(): ImageTrackingProfile | null; constructor(options?: TrackerOptions); get state(): 'idle' | 'busy' | 'disposed'; get hasTarget(): boolean; loadTarget(buffer: ArrayBuffer, signal?: AbortSignal, targetId?: string): Promise; get targetIds(): readonly string[]; loadTargets(targets: readonly { id: string; buffer: ArrayBuffer; }[], signal?: AbortSignal): Promise; processFrame(frame: CameraFrame, signal?: AbortSignal, calibrationOverride?: CameraIntrinsics): Promise; /** Un RGBA y un Worker para todos los marcadores. No duplicar el frame por target. */ processTargetsFrame(frame: CameraFrame, signal?: AbortSignal): Promise; dispose(): void; } ``` ## tracking/events.d.ts ```ts import type { ARPose } from '../Pose.js'; import type { OdometryScanHint } from '../slam/OdometryBackend.js'; export type TrackingIssue = 'low-texture' | 'small-target' | 'blur' | 'high-motion' | 'insufficient-matches' | 'occlusion-or-out-of-view' | 'device-load'; /** Indicadores heurísticos: una imagen sin rasgos no permite distinguir siempre blur y poca textura. */ export interface FrameQuality { gradient: number; laplacianVariance: number; motionPixels: number; targetSizePx: number; featureCount: number; inliers: number; reprojectionErrorPx: number; } export interface TrackingQualityEvent extends FrameQuality { targetId: string; timestampMs: number; state: 'tracking' | 'recovering' | 'lost'; score: number; processingMs: number; issues: TrackingIssue[]; diagnosis: 'heuristic'; } export interface TargetFoundEvent { targetId: string; timestampMs: number; pose: ARPose; } export interface TargetLostEvent { targetId: string; timestampMs: number; retained: boolean; reason: 'lost' | 'removed' | 'stopped'; } export interface TargetVisibilityEvent { targetId: string; visible: boolean; reason: 'tracked' | 'recovering' | 'lost' | 'removed' | 'stopped' | 'policy'; } export interface AREventMap { scanGuidance: { hint: OdometryScanHint; timestampMs: number; }; targetFound: TargetFoundEvent; targetLost: TargetLostEvent; trackingQuality: TrackingQualityEvent; targetVisibility: TargetVisibilityEvent; } ``` ## tracking/profile.d.ts ```ts /** Tiempos monotónicos por frame; el total Worker incluye las etapas y su preparación. */ export declare const TRACKING_STAGES: readonly [ "grayMs", "qualityMs", "supportMs", "opticalFlowMs", "flowValidationMs", "orbMs", "matchingMs", "homographyMs", "poseMs", "workerMs", "roundTripMs", "transportAndSchedulingMs" ]; export declare const TRACKING_COUNTS: readonly [ "flowAttempts", "flowAccepted", "redetections", "orbExtractions", "flowInputPoints", "cachedAttempts", "fullDetections", "matchingQueryDescriptors", "flowReliable", "redetectMissing", "redetectWeakSupport", "redetectDeadline", "redetectVerification", "globalDeferred", "flowSparseReliable", "weakDistribution", "weakReprojection", "weakRetention", "cacheConflicts", "exactMatchingCalls", "opencvMatchingCalls", "emptyMatchingCalls" ]; export interface ImageTrackingProfile { timings: Record; counts: Record; } export declare function createTrackingProfile(): ImageTrackingProfile; export declare function copyTrackingProfile(value: unknown): ImageTrackingProfile | null; export declare function measureTracking(profile: ImageTrackingProfile, key: typeof TRACKING_STAGES[number], run: () => T): T; ``` ## tracking/types.d.ts ```ts import type { CameraIntrinsics } from '../Pose.js'; import type { ImageTrackingProfile } from './profile.js'; import type { FrameQuality } from './events.js'; export interface ImageObservation { rgba: ArrayBuffer; width: number; height: number; timestampMs: number; sessionId: number; intrinsics: CameraIntrinsics; projectionSource?: 'calibrated' | 'estimated'; cameraFromMarker: Float32Array; imagePoints: Float32Array; objectPoints: Float32Array; markerCorners: Float32Array; } export type TrackingResult = { sessionId: number; timestampMs: number; processingMs: number; quality?: FrameQuality; targetId?: string; profile?: ImageTrackingProfile; } & ({ status: 'not-found'; matrix: null; } | { status: 'tracked'; matrix: Float32Array; projectionMatrix: Float32Array; projectionSource: 'calibrated' | 'estimated'; inliers: number; reprojectionErrorPx: number; reference: ImageObservation; }); export interface TrackingFrameRequest { type: 'FRAME' | 'FRAME_BATCH'; id: number; buffer: ArrayBuffer; width: number; height: number; sessionId: number; timestampMs: number; intrinsics: CameraIntrinsics; projectionSource: 'calibrated' | 'estimated'; } export interface TrackingBatchResult { results: TrackingResult[]; processingMs: number; profile?: ImageTrackingProfile; } ``` ## vision/calibration.d.ts ```ts import type { CameraIntrinsics } from '../Pose.js'; export declare function validateIntrinsics(k: CameraIntrinsics): void; /** No corregir silenciosamente un recorte o una rotación no observados. */ export declare function scaleIntrinsics(k: CameraIntrinsics, width: number, height: number): CameraIntrinsics; export declare function estimateIntrinsics(width: number, height: number, verticalFovDegrees?: number): CameraIntrinsics; /** Proyección OpenGL/WebGL, píxeles sin distorsión, origen superior izquierdo. */ export declare function projectionFromIntrinsics(k: CameraIntrinsics, near?: number, far?: number): Float32Array; export declare function rotateIntrinsics(k: CameraIntrinsics, degrees: 0 | 90 | 180 | 270): CameraIntrinsics; export declare function normalizedPixel(x: number, y: number, k: CameraIntrinsics): [ number, number ]; ``` ## wrappers/SurfacePlacement.d.ts ```ts import { RingGeometry, MeshBasicMaterial, Mesh } from 'three'; import type { ARPose } from '../Pose.js'; export type PlacementState = 'idle' | 'scanning' | 'surface-found' | 'placed' | 'lost'; /** Guía de presentación; nunca genera una pose ni supone que un hit-test sea suelo semántico. */ export declare class SurfacePlacement { readonly reticle: Mesh; constructor(manual: boolean, changed: (state: PlacementState) => void, now?: () => number, onStale?: () => void, showGrid?: boolean); get state(): PlacementState; get canConfirm(): boolean; begin(): void; status(message: string): void; observeSurface(matrix: Float32Array): void; observePose(pose: ARPose): void; confirm(): void; tick(): void; dispose(): void; } ``` ## wrappers/ThreeARGroup.d.ts ```ts import { Group } from 'three'; import type { ARPose } from '../Pose.js'; import type { WebAREngine } from '../WebAREngine.js'; export declare class ThreeARGroup extends Group { constructor(); /** Contenido independiente por marcador; no reinicia animaciones al perderlo. */ bind(engine: WebAREngine, targetId?: string): this; unbind(): void; dispose(): void; /** * Con metadata, usar pose de mundo: sirve con cámara normal y cámara XR. * Sin metadata, matrix es modelView y requiere una cámara identidad. * El core ya normalizó OpenCV -> WebGL; no repetir la conversión aquí. */ update(matrix: Float32Array, pose?: ARPose): void; } ``` ## wrappers/ThreeARModel.d.ts ```ts import { Group } from 'three'; export interface ARModelOptions { /** Carpeta local con draco_wasm_wrapper.js, draco_decoder.wasm y draco_decoder.js. */ dracoDecoderPath?: string | URL; maxBytes?: number; } export interface ARModelLoadOptions { /** Ajustar dimensión máxima, centrar X/Z y apoyar base en Y=0. false conserva unidades originales. */ fitSize?: number | false; autoplay?: boolean; signal?: AbortSignal; } export interface ARAnimationOptions { loop?: 'repeat' | 'once' | 'pingpong'; repetitions?: number; speed?: number; crossFadeSeconds?: number; clampWhenFinished?: boolean; } /** GLB autocontenido, sin CDN. Añadirlo a ThreeARGroup o a una entidad A-Frame. */ export declare class ThreeARModel extends Group { constructor(options?: ARModelOptions); get model(): Group | null; /** Una carga nueva cancela la anterior. El modelo previo se mantiene si la nueva falla. */ load(source: Blob | string | URL, options?: ARModelLoadOptions): Promise; /** Llamar con delta en segundos desde el bucle del renderer. */ update(deltaSeconds: number): void; setSize(size: number | false): void; playAnimation(nameOrIndex?: string | number, options?: ARAnimationOptions): void; configureAnimation(options: ARAnimationOptions): void; pauseAnimations(): void; resumeAnimations(): void; get animationState(): { name: string; time: number; paused: boolean; speed: number; running: boolean; } | null; onAnimationFinished(listener: (name: string) => void): () => void; stopAnimations(): void; unload(): void; dispose(): void; } ``` ## wrappers/ThreeARScene.d.ts ```ts import { Scene, PerspectiveCamera, WebGLRenderer, type Object3D } from 'three'; import { WebAREngine, type EngineOptions } from '../WebAREngine.js'; import { ThreeARGroup } from './ThreeARGroup.js'; export interface ARSceneOptions { container: HTMLElement; engineOptions?: EngineOptions; maxPixelRatio?: number; lighting?: boolean; fit?: 'contain' | 'cover'; /** MSAA al crear el contexto; cambiar exige recrear la escena. */ antialias?: boolean; } /** Wrapper opcional: script clásico + API propia, sin editor ni framework obligatorio. */ export declare class ThreeARScene { readonly engine: WebAREngine; readonly scene: Scene; readonly camera: PerspectiveCamera; readonly renderer: WebGLRenderer; constructor(options: ARSceneOptions); getAnchor(targetId?: string): ThreeARGroup; addContent(targetId: string, content: Object3D): void; removeContent(content: Object3D): void; onRender(callback: (deltaSeconds: number) => void): () => void; onAfterRender(callback: () => void): () => void; dispose(): Promise; } ``` ## wrappers/ThreeARVideo.d.ts ```ts import { Group, Mesh, PlaneGeometry, ShaderMaterial, type ColorRepresentation } from 'three'; export interface ChromaKeyOptions { color?: ColorRepresentation; similarity?: number; smoothness?: number; spill?: number; } export interface ARVideoOptions { width?: number; orientation?: 'flat' | 'upright'; muted?: boolean; loop?: boolean; chromaKey?: ChromaKeyOptions | false; } /** MP4 a través del decoder nativo. Reproducción independiente del estado del marcador. */ export declare class ThreeARVideo extends Group { readonly videoElement: HTMLVideoElement; readonly mesh: Mesh; constructor(options?: ARVideoOptions); setChromaKey(options: ChromaKeyOptions | false): void; setWidth(width: number): void; setOrientation(orientation: 'flat' | 'upright'): void; /** No reproduce ni rebobina automáticamente al recuperar un marcador. */ setSource(source: Blob | string | URL): Promise; /** Invocar desde un gesto del usuario; propaga bloqueos de autoplay para mostrar un botón. */ play(): Promise; pause(): void; dispose(): void; } ``` ## wrappers/ThreeCameraBackground.d.ts ```ts import { Mesh } from 'three'; import type { CameraIntrinsics } from '../Pose.js'; /** Fondo de cámara rectificado en GPU: la proyección K debe coincidir con la escena 3D. */ export declare class ThreeCameraBackground { readonly object3D: Mesh; constructor(video: HTMLVideoElement, calibration?: CameraIntrinsics); setCalibration(calibration?: CameraIntrinsics): void; dispose(): void; } ``` ## wrappers/aframe-component.d.ts ```ts import type { Object3D, Camera } from 'three'; import type { WebAREngine } from '../WebAREngine.js'; import { ThreeCameraBackground } from './ThreeCameraBackground.js'; interface ComponentInstance { data: { target: string; physicalWidth: number; targetId: string; }; el: HTMLElement & { object3D: Object3D; sceneEl?: { camera?: Camera; object3D?: Object3D; }; }; unsubscribe?: () => void; originalAutoUpdate?: boolean; originalVisible?: boolean; removed?: boolean; camera?: Camera; cameraAutoUpdate?: boolean; background?: ThreeCameraBackground; releaseBackground?: () => void; releaseCamera?: () => void; } interface AFrameRegistry { registerComponent(name: string, definition: { schema: { target: { type: 'string'; default: string; }; physicalWidth: { type: 'number'; default: number; }; targetId: { type: 'string'; default: string; }; }; init(this: ComponentInstance): void; update(this: ComponentInstance, previous: { target?: string; physicalWidth?: number; targetId?: string; }): void; remove(this: ComponentInstance): void; }): unknown; } /** Una instancia de motor por escena; configurar su cámara/renderer al crearla. */ export declare function registerAFrameComponent(AFRAME: AFrameRegistry, engine: WebAREngine): void; export {}; ``` ## wrappers/aframe-model.d.ts ```ts import type { Object3D } from 'three'; import { ThreeARModel, type ARModelOptions } from './ThreeARModel.js'; interface Instance { data: { src: string; fitSize: number; autoplay: boolean; }; el: HTMLElement & { object3D: Object3D; }; model?: ThreeARModel; generation?: number; } interface Registry { registerComponent(name: string, definition: { schema: { src: { type: 'string'; default: string; }; fitSize: { type: 'number'; default: number; }; autoplay: { type: 'boolean'; default: boolean; }; }; init(this: Instance): void; update(this: Instance, previous: Partial): void; tick(this: Instance, time: number, delta: number): void; remove(this: Instance): void; }): unknown; } /** Usar en un hijo de la entidad my-ar-tracker; mantiene separada la pose del contenido. */ export declare function registerAFrameModelComponent(AFRAME: Registry, options?: ARModelOptions): void; export {}; ``` ## wrappers/aframe-video.d.ts ```ts import type { Object3D } from 'three'; import { ThreeARVideo } from './ThreeARVideo.js'; interface Instance { data: { src: string; width: number; chromaKey: boolean; keyColor: string; similarity: number; smoothness: number; spill: number; muted: boolean; loop: boolean; }; el: HTMLElement & { object3D: Object3D; }; video?: ThreeARVideo; generation?: number; } interface Registry { registerComponent(name: string, definition: { schema: Record; init(this: Instance): void; update(this: Instance, previous: Partial): void; remove(this: Instance): void; }): unknown; } export declare function registerAFrameVideoComponent(AFRAME: Registry): void; export {}; ``` ## wrappers/web-components.d.ts ```ts import { ThreeARScene } from './ThreeARScene.js'; import { ThreeARModel, type ARAnimationOptions } from './ThreeARModel.js'; import { ThreeARVideo } from './ThreeARVideo.js'; import { type PlacementState } from './SurfacePlacement.js'; import { ARDiagnostics, type DiagnosticReport } from '../diagnostics/ARDiagnostics.js'; import type { WebAREngine, SurfaceTransitionOptions, SurfaceTransitionResult } from '../WebAREngine.js'; export interface ARComponentsOptions { assetBase?: string | URL; } export interface ARSceneElement extends HTMLElement { readonly app: ThreeARScene | null; readonly engine: WebAREngine | null; readonly updateComplete: Promise; readonly diagnostics: ARDiagnostics | null; readonly placementState: PlacementState; confirmPlacement(): void; getDiagnosticsReport(): DiagnosticReport | null; downloadDiagnostics(): void; start(): Promise; stop(): Promise; refresh(): Promise; enableSLAM(options?: SurfaceTransitionOptions): Promise; } export interface ARModelElement extends HTMLElement { readonly model: ThreeARModel | null; readonly updateComplete: Promise; reload(): Promise; playAnimation(nameOrIndex: string | number, options?: ARAnimationOptions): void; pauseAnimations(): void; resumeAnimations(): void; stopAnimations(): void; } export interface ARVideoElement extends HTMLElement { readonly video: ThreeARVideo | null; readonly updateComplete: Promise; reload(): Promise; play(): Promise; pause(): void; } /** Opt-in en ESM; standalone registra automáticamente. No toca DOM al importar en Node. */ export declare function registerARComponents(options?: ARComponentsOptions): void; ``` --- Fuente: docs/advanced.md # API avanzada y extensiones Para una aplicación normal utiliza [HTML declarativo](getting-started.md). Esta guía es para sustituir piezas del pipeline o construir una integración propia sobre `WebAREngine` y sus wrappers. Las firmas y todos los tipos públicos están en [la referencia generada](api-generated.md). ## CameraManager + Tracker Ejemplo de composición de bajo nivel; sustituye el registro en consola por tu consumidor de poses: ```ts import {CameraManager,Tracker} from 'webar-facil/core'; const tracker=new Tracker({opencvURL:new URL('./vision/opencv.js',document.baseURI)}); const response=await fetch('./target.dat'); if(!response.ok) throw new Error(`HTTP ${response.status}`); await tracker.loadTarget(await response.arrayBuffer()); const camera=new CameraManager({ maxFrameWidth:640, onFrame:async(frame,signal)=>{ const result=await tracker.processFrame(frame,signal); if(result?.status==='tracked') console.log(result.matrix,result.projectionMatrix); }, onError:console.error, }); // Llamar desde el botón de inicio de tu aplicación: await camera.start(); // Al terminar: camera.dispose();tracker.dispose(); ``` La última línea ilustra la limpieza al desmontar, no debe ejecutarse inmediatamente tras iniciar si quieres mantener la sesión. El callback `onFrame` debe resolver **cuando termine** el procesamiento, no cuando se haya enviado un mensaje. Eso proporciona backpressure. `Tracker.state` puede ser `idle`, `busy` o `disposed`. `processFrame` devuelve `null` si omite un frame por estar ocupado, o un resultado `not-found`/`tracked`; no conviertas `null` en una pérdida real. `loadTarget(buffer,signal?,id?)` y `loadTargets([{id,buffer}],signal?)` transfieren los binarios. Para varios targets usa `processTargetsFrame`: una sola transferencia RGBA y un lote de resultados. El método singular no es un pipeline de multimarcador. En el nivel bajo el buffer `frame.imageData.data.buffer` se transfiere al Worker y queda separado del emisor. Si un consumidor adicional necesita los píxeles, coordina propiedad explícitamente. Duplicar el frame por cada target anula el beneficio del pipeline compartido. La `ImageObservation` de un resultado aceptado incluye correspondencias, intrínsecos y la referencia RGBA para sembrar odometría. El RGBA puede compartirse en el lote: no intentes transferir el mismo buffer varias veces. ## Compilación y calibración `MarkerCompiler.compile(file,{physicalWidthMeters,opencvURL,maxDimension?},signal?)` gestiona la compilación de imagen en Worker y devuelve `{buffer,quality}`. `dispose()` libera ese Worker. `ImageCompiler` expone métodos estáticos con adaptadores `decode` y `extract` para herramientas propias/Node. `CalibrationSession` permite `initialize`, `board`, `addFrame`, `solve`, `reset` y `dispose`. `addFrame` transfiere el ImageData. `solve()` devuelve `CalibrationReport`: `calibration`, `rmsPx`, `perViewErrorsPx`, `views`, `coverage`, `warnings`. Es una herramienta opcional de precisión, no un requisito para detectar imágenes. Las utilidades públicas `projectionFromIntrinsics`, `scaleIntrinsics`, `rotateIntrinsics` y `validateIntrinsics` validan y convierten la cámara. `scaleIntrinsics` conserva la proporción y rechaza cambios de aspecto incompatibles; no la uses para justificar un crop distinto sin ajustar el modelo de cámara. `estimateIntrinsics` es interno, no un export público. ## SLAMManager nativo `SLAMManager.isSupported()` comprueba soporte. El constructor exige `attachSession` y `onPose`; admite hooks de superficie, pérdida, finalización, errores y alineación de cámara. `start(signal?)` se invoca desde una interacción y `stop()` termina la sesión. `switchToSLAM(matrix,alignedWorldFromCamera?)` es un método de bajo nivel que requiere un marco de cámara alineado. No acepta una ModelView de getUserMedia como sustituto de un marco XR común. Los getters `imageAlignmentAvailable`, `hasPlacement` y `referenceEpoch` informan del estado de alineación y colocación. No existe `SLAMManager.dispose()` ni un método `enableSurfaceTracking()` en esta versión. ## OpticalFlowSLAM y un backend propio `OpticalFlowSLAM` requiere `intrinsics` y `floorROI` normalizado; admite una semilla `ImageObservation`. Gestiona `start`, `processFrame`, `exportMap`, `importMap`, `stop` y `dispose`. Puede recibir `moduleURL` para un módulo JS confiable que encapsule tu WASM. El módulo implementa `OdometryModule.createOdometryBackend(config)`. El backend debe implementar `processFrame` y `reset`; puede ofrecer `exportMap`/`importMap`. El resultado es la unión `OdometryEstimate`: - `initializing` o `lost`, con `mapId`. - `tracked`, con `worldFromCamera`, `projectionMatrix`, `scale`, `trackedPoints` y métricas opcionales. `worldFromCamera` es una pose acumulada, **no un delta** de movimiento. Solo comunica `groundPlane` cuando esté verificado geométricamente. No estimes una traslación 3D sumando desplazamientos de píxeles. Debes declarar pérdida ante degeneración, reinicio de mapa o falta de paralaje. El ABI no obliga a un renderer ni a una biblioteca C++ concreta. Mantén validación, límites de recursos, protocolo de cancelación y convenciones de matrices para que los wrappers puedan consumir las poses de forma transparente. --- Fuente: docs/architecture.md # Arquitectura implementada El núcleo es agnóstico al renderizador. Los wrappers son una capa opcional. No hay servicios externos de compilación o tracking. La entrada recomendada es la capa webar-scene/target/model/video de `src/wrappers/web-components.ts`. El standalone la registra automáticamente; configura recursos, compila imágenes, selecciona clips y libera recursos. Un observador y una cola sincronizan cambios de targets/jerarquía; cada medio administra sus cargas cancelables. GestureController gestiona interacción opt-in sobre contenido visible sin modificar el anclaje. El núcleo y los wrappers programáticos se conservan para extensiones. ```text Aplicación HTML / TypeScript / A-Frame | ThreeARScene / ThreeARGroup / componentes (opcionales) | WebAREngine / | \ CameraManager Tracker SLAMManager (WebXR) | | | | Worker ORB/LK compositor y hit-test nativos | OpticalFlowSLAM -> Worker de odometría -> OpenCV WASM JPG/PNG -> MarkerCompiler -> Worker -> WART v2 -> Tracker ``` ## Carpetas fuente | Ruta | Responsabilidad | | --- | --- | | `src/WebAREngine.ts` | Fachada, estados, eventos y transición espacial | | `src/Pose.ts`, `src/core.ts`, `src/index.ts`, `src/standalone.ts` | Contratos y exports | | `src/camera/` | Captura, lectura RGBA, negociación y adaptación | | `src/tracking/` | Controlador, Worker, extracción, matching, pose y eventos | | `src/tools/` | Compilador de marcador en Worker | | `src/calibration/` | Captura ChArUco y solución opcional de cámara | | `src/slam/` | WebXR, captura alineada, odometría y snapshots | | `src/spatial/` | Matrices, filtrado y presentación temporal | | `src/vision/` | Runtime CV, intrínsecos y algoritmos auxiliares | | `src/wrappers/` | Scene, Group, Model, Video, fondo y A-Frame | | `scripts/` | CLI, benchmark, empaquetado y manual | | `public/` | Recursos estáticos que se copian al build | | `examples/`, `tools/` | Aplicaciones y herramientas | | `tests/` | Pruebas de CV, contratos y navegadores | | `dist/` | Distribución generada; no editar como fuente | El diseño inicial archivado propuso paquetes separados; la distribución vigente usa los entrypoints `webar-facil`, `webar-facil/core`, `webar-facil/browser` y `webar-facil/standalone`. No importes nombres de paquetes del diseño histórico. ## Flujo y concurrencia La cámara negocia un MediaStream y entrega frames limitados por resolución/frecuencia. Cada frame tiene un identificador de sesión; respuestas tardías de una sesión cancelada no deben reaparecer como observaciones actuales. El tracker transfiere un único RGBA por lote. El Worker comparte gris y extracción entre targets y conserva estados/descriptores para seguimiento y recuperación por ID. Las matrices aceptadas pasan a filtrado, tolerancia, eventos y visibilidad. Animación y render son independientes. La fachada serializa configuración y utiliza backpressure para frames. Cancelar o detener cierra recursos y descarta mensajes tardíos. Los Workers no acceden al renderer ni escriben DOM. ## Marcos y extensiones WebXR usa su propio origen. La odometría mantiene un mapa local. La transición requiere una transformación de cámara común y una observación reciente o una recolocación explícita. Conservar una matriz camera-space no demuestra anclaje world-space. Un renderer nuevo consume `ARPose` y eventos de visibilidad. Un backend implementa `OdometryModule`. Un compilador alternativo debe producir descriptores compatibles e identificador/versionado propios si cambia el algoritmo. Preserva las verificaciones geométricas y declara pérdida cuando no haya evidencia. El módulo NFT perfila etapas por frame y mantiene una referencia de descriptores ya verificados para redetección rápida. Recurre al pool global si falla esa ruta y periódicamente; no elimina RANSAC ni sustituye mediciones por poses inventadas. Las estadísticas llegan por CameraMetrics.tracking y ARDiagnostics; no dependen de Three.js. Ver [rendimiento y benchmark](camera-performance.md). La redetección NFT considera cobertura convexa, distribución, error y retención del soporte. Un mínimo escaso pero bien distribuido no fuerza matching global por cantidad. Revisiones acotadas y desacuerdos entre estimaciones mantienen una vía de corrección independiente; ver política y límites en [rendimiento](camera-performance.md). El matching ORB compartido usa un kernel Hamming WASM especializado con resultado equivalente al BFMatcher previo y alternativa OpenCV si no puede compilarse. El kernel está embebido, sus buffers son reutilizables/acotados y la fuente WAT se distribuye con el SDK. No altera los criterios geométricos, los datos WART ni la API declarativa. --- Fuente: docs/contributing.md # Desarrollo, pruebas y contribuciones ## Entorno Node.js >=22.18, `npm ci` y `npm run build`. TypeScript estricto y Vite generan ESM, IIFE, standalone, core sin renderer, Workers estáticos y declaraciones. No edites `dist/` como fuente. ## Comandos | Comando | Propósito | | --- | --- | | `npm run dev` | Build previo y servidor en 127.0.0.1:5173 | | `npm run check` | TypeScript sin emitir | | `npm run build` | Preparar recursos y generar distribución y documentación | | `npm test` | Pruebas Node con CV real y contratos de pipeline | | `npm run test:browser` | Pruebas de navegador con Playwright | | `npm run check:package` | Instalar tarball en consumidor independiente y validar tipos/exports | | `npm run check:release` | Comprobar archivos, tamaños y SHA-256 de la distribución local | | `npm run reports:summarize -- ios.json android.json` | Resumir evidencia declarada sin certificar dispositivos | | `npm run benchmark` | Benchmark sintético de odometría | | `npm run docs:build` | Regenerar sitio, API y textos para IA; requiere declaraciones del build | | `npm run docs:check` | Comprobar links, exports, ejemplos y documentación generada | | `npm run docs:test-browser` | Probar lector del manual y aplicación documentada en Chrome | | `npm run compile:marker -- input.jpg public/target.dat 0.2` | Precompilar un marcador | | `npm run generate:demo` | Regenerar referencia y binario de ejemplo | | `npm run generate:video` | Regenerar el MP4 original de chroma; requiere Chrome/Playwright con encoder H.264 | Las pruebas de navegador necesitan Playwright y sus navegadores instalados (`npx playwright install chrome webkit`; en Linux puede requerir `--with-deps`). En PowerShell, para la matriz ampliada: ```powershell $env:TEST_WEBKIT='1' $env:PLAYWRIGHT_CHANNEL='chrome' npm run test:browser ``` No atribuyas a una prueba sintética una certificación de cámara física. Los resultados y el protocolo están en [verificación](verification.md) y [dispositivos](device-validation.md). ## Cambiar documentación Edita los `.md` de `docs/`, el HTML de `examples/` o este README del proyecto. El inicio, los capítulos de funcionalidad y la guía para IA deben presentar primero HTML declarativo; el código de motor/renderer se identifica como avanzado. La referencia nativa incluye webar-video, gestos y actualizaciones en vivo; no publiques atributos que no estén implementados. `docs/api-generated.md`, `docs/api.json`, `llms.txt`, `llms-full.txt`, `public/docs/` y `dist/docs/` se generan con `scripts/build-docs.mjs`. No corrijas una firma únicamente en el archivo generado: modifica el código/tipos y reconstruye. El índice de capítulos del generador define qué se publica y qué recibe una IA. Para añadir una guía, regístrala allí y enlázala desde el manual. Los delimitadores `` y `` mantienen el HTML real entre ambos en el Markdown fuente, el sitio y el texto para IA, evitando una copia divergente del ejemplo ejecutable. Modifica el HTML original y regenera. ## Cambios de API Actualiza los exports de `src/core.ts` o `src/index.ts`, sus declaraciones y guías. El core debe seguir importándose sin Three.js. Conserva IDs, propiedad de buffers, cancelación y contratos de matrices; no presentes una pose inventada como recuperación. Añade pruebas que reproduzcan el fallo y validación apropiada al cambio. Antes de distribuir, ejecuta los checks relevantes y `npm run check:package`. Incluye licencias de dependencias y no incorpores assets sin derechos de redistribución. Los cambios de tracking requieren medir calidad geométrica además de contar poses. La prueba `tests/browser/declarative.test.mjs` valida la capa HTML y el servidor de recursos en otro origen. El registro automático se limita a `src/standalone.ts`; `registerARComponents` es opt-in en ESM y no debe tocar DOM al importar el paquete en Node. `docs:test-browser` ejecuta el primer HTML completo de la guía de inicio como una aplicación servida junto a la distribución, además de comprobar el lector y las alternativas programáticas. No mantengas un ejemplo de inicio distinto del que consume esta comprobación. El build genera src/build-info.ts desde las fuentes y package.json. No lo edites a mano. Las pruebas de informes escriben sus fixtures solo en tests/generated, no en la carpeta de resultados físicos del operador. La emulación móvil no aprueba hardware; en WebKit sin API de cámara se comprueba la exportación del error, no seguimiento físico. Para medir NFT con etapas y precisión: `npm run benchmark:image` genera benchmarks/image-tracking-latest.json sin sobrescribir las capturas históricas before/after. Usa `node scripts/compare-image-tracking.mjs` para comparar las capturas conservadas. Mantén la misma fixture y entorno, y revisa detecciones, falsos positivos, recuperación y error de esquinas junto a los tiempos. Los números no certifican teléfonos. La suite de soporte escaso se ejecuta con `npm run benchmark:image -- benchmarks/image-support-latest.json sparse`; el tercer argumento opcional del script Node permite cargar un módulo core de baseline local. Los informes históricos image-support-before/after conservan build, fixture y entorno; `node scripts/compare-image-tracking.mjs image-support` verifica compatibilidad y precisión acotada. Una media menor con p95 o error mayores debe documentarse como compromiso, no como mejora uniforme. El kernel Hamming tiene fuente propia MIT en src/vision/wasm/hamming.wat. Tras editarlo ejecuta `npm run build:hamming`; `node scripts/build-hamming.mjs --check` comprueba el generado sin modificarlo. WABT es solo herramienta dev, no dependencia de ejecución. `npm run benchmark:matching` contrasta la lista completa de matches contra OpenCV y mide por separado el matcher aislado. El build regenera antes del hash; no edites manualmente hamming-binary.ts. --- Fuente: docs/verification.md # Verificación de la implementación Resultados locales de la entrega actual: El recolector XAMPP opcional tiene seis verificaciones explícitas en tests/integration/local-diagnostics.test.mjs: validación/token/origen y privacidad, secuencias fuera de orden, envío periódico y panel, cola sin red entre páginas, A-Frame por HTTPS, avance al siguiente ejemplo y captura de errores de permiso en WebKit. Se ejecutan contra Apache/PHP real con cámara sintética o permiso denegado simulado; los datos quedan etiquetados como automatizados. No forman parte de npm test porque requieren una instalación XAMPP activa. - `npm run build`: TypeScript estricto, ES/IIFE, script standalone con Three.js, core sin renderer, Workers estáticos y declaraciones ESM. - `npm test`: **68 pruebas aprobadas**, con visión real, cámara/XR simulados, perfiles de captura, presupuesto vertical/horizontal, adaptación por carga sin degradar marcadores débiles ni cambiar dimensiones del mapa, autoenfoque pendiente sin bloqueo, cancelación de play/foco, diagnóstico acotado, privacidad del informe, colocación fresca y clasificación de evidencia declarada. - `TEST_WEBKIT=1 node --test --test-skip-pattern="Abrir con file" --test-concurrency=1 tests/browser/browser.test.mjs tests/browser/declarative.test.mjs`: **36 pruebas HTTP aprobadas**, con regresiones Chrome/WebKit y herramientas móviles; el caso de navegación local se omite en esta ejecución. Android/Chrome con viewport/tacto móvil registra cámara sintética y exporta. Se comprueba reconocimiento y diagnóstico después de cambiar entre ahorro y auto. Otra prueba usa getUserMedia y play nativos de Chromium con su dispositivo virtual, sin reemplazarlos en JavaScript: verifica vídeo, estados, ambos botones, cierre de tracks y reinicio. En este WebKit de escritorio faltan getUserMedia/captureStream: se verifica el informe del error, no seguimiento iPhone. No se simula el SO ni se certifica hardware físico. - `node --test tests/browser/pruebas.test.mjs`: **4 pruebas de integración aprobadas** para los 13 HTML fuente y empaquetados, inicio visible en móvil, recursos locales, GLB originales, traslación sobre plano, giro/pellizco, sustitución sin reiniciar, CanvasTexture reutilizada, MP4, iluminación y mapa exportado/recuperado tras reiniciar con relocalización real. Usa OpenCV real y cámara sintética con una superficie geométrica; no certifica hardware físico. - `node --test tests/browser/pruebas-extra.test.mjs`: **4 pruebas de integración aprobadas** para 14–25: rutas y recursos fuente/distribución, multimarcador, clips reales, chroma vivo, archivos locales, retención, picking táctil, catálogo, transición óptica preservada, petición XR con rechazo simulado (sin certificar sesión física), comparaciones descargables y composición. A-Frame 1.8.0 se ejecuta realmente con wrappers, render y reinicio, sin pedir orientación; cámara sintética. Las 8 pruebas de ambas colecciones se ejecutaron juntas, conservando 01–13. - `npm run check:package`: tarball instalado en un consumidor independiente; tipos NodeNext estrictos con `skipLibCheck: false`, core y wrappers; API tipada de eventos, vídeo, escena, GLB y multimarcador; importación del core en Node. - `npm run benchmark`: 630 frames sintéticos con OpenCV real, 580 poses tras inicialización; mediana aproximada **41.1ms**, p95 **79.1ms**, heap WASM estable en **32MiB**. [Comparación de trayectoria y opción experimental](odometry-validation.md). No demuestra mejora general de deriva ni rendimiento móvil. ## Continuidad, calidad y marcadores La regresión sostenida exige al menos 42/44 observaciones visibles aceptadas, error de registro de las cuatro esquinas <8px en las poses brutas aceptadas y recuperación tras desaparición total. Hay pruebas adicionales de inclinación cercana a 60°, cambios bruscos de posición y compilación de vistas acortadas, sin aportar intrínsecos manuales. `targetFound`/`targetLost` no se repiten por cada frame. Durante incertidumbre o retención no se emiten poses inventadas. La prueba de navegador comprueba que el contador de poses se detiene mientras la transformación visible se conserva y la animación GLB continúa. Cambiar `keepContentOnLost` modifica visibilidad sin recargar contenido; detener la cámara oculta. La prueba multimarcador transfiere un solo RGBA, obtiene dos poses con la misma proyección, verifica posiciones independientes y pierde cada ID por separado. La API admite hasta 8; no se ha certificado rendimiento con 8 en móviles. Los diagnósticos de blur/textura/tamaño/movimiento/carga son heurísticos, no una clasificación infalible. ## Vídeo y modelos El MP4 H.264 de prueba es original, generado mediante MediaRecorder a partir de un canvas. Se decodifica en Chrome y WebKit. La prueba GPU comprueba píxeles: el verde se elimina, el sujeto rojo se conserva y desactivar chroma restaura el fondo. Otra prueba comprueba que durante pérdida del marcador no hay recarga ni pausa y siguen aumentando los frames de vídeo. Los GLB normal, Draco y Meshopt incluyen textura y animación reales. Se comprueban tamaño, base, transformación animada, textura, disposición de geometría, sustitución en escena y cancelación. Los componentes A-Frame se verifican mediante su contrato de registro/ciclo de vida y objetos Three.js; no se presenta esto como una certificación de todas las versiones de A-Frame. ## Mapas y límites físicos Los snapshots se prueban tanto en el backend como a través del Worker: no hay pose guardada publicada al importar, el mapa debe relocalizarse, mantiene geometría/escala y rechaza datos corruptos. La exportación contiene geometría y descriptores, no frames de cámara. No se midió temperatura ni se utilizaron cámaras físicas en estas verificaciones. WebKit de Playwright no equivale a certificar Safari en un iPhone. Raw Camera Access, WebXR y anclajes nativos requieren hardware real. El fallback es odometría con mapa local, sin cierre de bucle global ni fusión inercial. El protocolo pendiente está en `device-validation.md`. La herramienta tools/device-tests.html usa el mismo protocolo para iOS y Android y no aprueba por temporizador. El resumen distingue datos automáticos de declaraciones físicas, mantiene pendiente la plataforma sin evidencia y no genera certificaciones. Los fixtures de validación se guardan solo en tests/generated para no confundirse con resultados del operador. ## Documentación La documentación tiene una comprobación independiente de enlaces locales, sintaxis de bloques, correspondencia entre exports documentados y declaraciones reales, y un consumidor TypeScript estricto de las recetas. `npm run docs:test-browser` prueba el lector en desktop/móvil, búsqueda, ausencia de desbordamiento y el primer HTML literal de la guía de inicio: compila una imagen, sigue el marcador mediante OpenCV real con cámara sintética, carga un GLB animado y detiene la sesión declarativa. Conserva además las alternativas programáticas como regresiones. Esto tampoco certifica una cámara física. El empaquetado verifica que el tarball incluya manual, contrato JSON, textos para IA, sitio estático y el ejemplo documentado. Estas comprobaciones no sustituyen las pruebas de tracking y hardware descritas arriba. ## HTML declarativo Las pruebas específicas sirven el HTML y el SDK desde dos servidores HTTP de distinto origen, manteniendo CORS del navegador. Verifican compilación/detección real, descarga de OpenCV y Draco respecto al script remoto, retención, transformaciones, selección de animación, reinicio, disposición al retirar la etiqueta, cancelación de arranque y errores de configuración. Otra prueba inicia el modo óptico sin targets y comprueba su parada; esto no demuestra estabilidad de un plano físico sin marcador ni una certificación de iPhone. La suite declarativa específica también comprueba los cinco HTML reales del repositorio: los controles aparecen con el bundle actual y el texto de respaldo permanece visible si se sirve un SDK antiguo o un 404. Abrir con file:// presenta instrucciones y no crea una cámara al pulsar Iniciar. La suite declarativa incluye además visibilidad de inicio en viewport móvil y entrada táctil nativa de Chrome: selección, arrastre, pellizco, bloqueo de scroll/zoom dentro del visor y restauración al detener. Las nuevas regresiones verifican que GLB, tamaño, jerarquía y targets cambien conservando la misma instancia de motor y MediaStream; que los archivos fallidos conserven contenido anterior; y que vídeo/chroma no se recarguen al modificar parámetros. Se verifican pausa, velocidad, finalización y crossfade con dos clips, y raycast/arrastre/pellizco acotado mediante Pointer Events. El MP4 original incluido puede regenerarse con generate:video. ## Perfil y caché NFT Se verifican perfiles numéricos, copias de diagnóstico sin memoria compartida, uso de caché/global en Worker real, rechazo de textura ajena y reinicio de caché entre sesiones. El benchmark de imagen conserva 316/316 poses esperadas con recuperación tras oclusión; ver [resultados y límites](camera-performance.md). La decisión geométrica se comprueba con soportes repartidos, concentrados y colineales; error, retención, plazos y conflicto de poses. Workers WASM reales verifican un marcador pequeño con 16 puntos y una referencia de 23 descriptores, manteniendo el límite original de 8 px de error de esquinas, pérdida real y recuperación. ## Matching Hamming exacto Pruebas diferenciales contra OpenCV verifican listas ordenadas, aleatorios/perturbados, límites de ratio y distancia, empates, unicidad, vistas con offset, crecimiento y límites de memoria, compilación no disponible y reproducibilidad WAT. Chrome y WebKit ejecutan un Worker NFT real con el kernel nuevo a partir de un canvas, sin exigir getUserMedia para esa prueba. El benchmark completo conserva detecciones, recuperación y los errores de esquinas de la versión anterior en las fixtures comparadas. La herramienta móvil aplica selectores antes de ambos botones de inicio, arranca óptico sin targets/peticiones de marcador, vuelve a imagen y pide immersive-ar con hit-test desde un gesto dedicado. Un test móvil de distribución verifica script versionado y advertencia persistente ante builds distintos, permitiendo getUserMedia tras un toque cuando el SDK sigue siendo compatible. Chrome/WebKit verifican que un rechazo de permisos no deja bloqueados los botones de reintento. La petición XR se simula y rechaza expresamente: no se certifica una sesión nativa en hardware. Arranque móvil: se retiene una descarga GLB, se observa cámara/vídeo antes de liberarla, se comprueba activación del usuario en las dos solicitudes legítimas y se evitan duplicados. Un fallo de contenido posterior cierra el stream. Se prueban respuesta de botones con SDK ausente, aviso de builds distintos y reintento tras rechazo de permisos. En esta revisión se ejecuta la regresión HTTP; la prueba histórica de file:// se conserva aparte. --- Fuente: docs/device-validation.md # Aceptación en dispositivos físicos Para probar la integración recomendada utiliza `examples/declarative.html` y `examples/declarative-slam.html` mediante HTTP/HTTPS. `index.html` conserva controles programáticos de diagnóstico; sus campos no son pasos obligatorios para una aplicación declarativa. El protocolo físico siguiente aplica al mismo motor de visión en ambas integraciones. Estado: el agente no ejecutó el protocolo en hardware físico. Se recibieron informes de imagen declarados por el usuario para iPhone y Android con un SDK anterior; ver evidencia parcial al final. Las pruebas automáticas y la emulación no sustituyen estos resultados. No publicar una certificación de 60FPS o precisión métrica universal con datos de escritorio. ## Matriz mínima Registrar modelo, SO, versión exacta de navegador, orientación, lente usada, resolución real y calibración. Cubrir Android con WebXR, Android sin WebXR, iPhone/iPad con fallback y al menos un móvil de prestaciones limitadas. Probar también Chrome/Edge de escritorio donde haya cámara trasera seleccionable. Detectar capacidades en ejecución; no decidir por user-agent. | Prueba | Procedimiento | Evidencia requerida | | --- | --- | --- | | NFT | Tres imágenes distintas, ancho físico medido, marcador >=150px de ancho en análisis; varias distancias e inclinaciones | Tiempo de primera detección, tasa de detección, inliers y error de reproyección | | Escala | Cubo con dimensiones conocidas sobre imagen impresa sin reescalar | Diferencia de tamaño y posición frente a referencia física | | Estabilidad | Cámara y marcador inmóviles durante 60s | Jitter en píxeles y orientación; guardar vídeo sin datos sensibles | | Recuperación | Ocluir marcador, salir del encuadre y volver; movimiento rápido y blur | Tiempo y tasa de recuperación; ausencia de poses falsas | | Luz | Interior iluminado, penumbra, reflejos y luz variable | Intervalos de tracking/lost, degradación explicable | | Fallback | Traslación lateral con textura; rotación pura; superficie uniforme; personas en movimiento | Inicialización solo con evidencia suficiente; estado initializing/lost en degeneración | | Transición local | Marcador inmóvil, mover cámara, esperar mapa listo y retirar marcador | Continuidad de pose y estabilidad sobre puntos del entorno | | Transición XR | Dispositivo con camera-access, luego sin esa feature | Preservación con observación sincronizada o recolocación explícita; nunca conservar pose ficticia | | Lifecycle | Denegar permisos, revocarlos, ocultar página, bloquear pantalla, rotar, salir de XR y reiniciar | Cámara liberada; callbacks/Workers detenidos o suspendidos; recuperación controlada | | GPU | Forzar pérdida de contexto si es posible | Restauración y reinicio sin escena ni stream huérfanos | | Sostenido | 2–5 minutos por dispositivo y configuración | Percentiles de latencia, FPS de poses/rAF/render por separado, memoria disponible y degradación temporal | | Persistencia | Ocluir 200ms, luego 2s; alternar `keepContentOnLost` durante la pérdida | Found/lost una sola vez por transición; última pose retenida sin simular tracking; stop siempre oculta | | Animación y vídeo | GLB animado y MP4 en bucle, con el marcador visible y oculto | La reproducción avanza sin recarga, pausa ni rebobinado causados por tracking | | Chroma key | MP4 verde/azul, ajustar color/tolerancia, sujeto con bordes finos | Transparencia y spill, sujeto preservado, codec y gesto de reproducción en Safari | | Multimarcador | Dos imágenes distintas simultáneas, ocultar cada una por separado; subir progresivamente hasta 8 | ID y contenido correctos; sin visibilidad cruzada; latencia y calidad por marcador | | Adaptación | Alejar el marcador hasta ocupar pocos píxeles y aumentar carga | Conservar detalle útil; evitar oscilaciones de resolución; informar baja calidad/carga sin inventar causas | | Mapas | Exportar snapshot, reiniciar visión local, importar y volver a observar el entorno | Sin pose hasta relocalizar; revisión de mapa nueva; continuidad del espacio y del contenido tras reconocerlo | ## Criterios Elegir umbrales antes de medir en los dispositivos objetivo. Como objetivos iniciales de producto: detección en menos de 1s en condiciones favorables, reproyección <3px, recuperación <2s y tracking sostenido sin crecimiento de recursos. Son objetivos pendientes de aceptación física, no resultados ya obtenidos. La estabilidad métrica debe evaluarse con calibración y marcador medido. Si se usa proyección estimada, registrar esa limitación. La escala relativa de VO sin referencia física no se puede comparar directamente con metros. Para anclaje ambiental, comprobar deriva tras un recorrido y al regresar a una zona conocida. ## Herramientas - `tools/calibrate.html`: tablero y captura ChArUco, JSON de intrínsecos y errores. - `tools/compile.html`: marcadores locales y calidad de características. - `tools/benchmark.html`: informe local, sin subir datos a servidores. - `benchmarks/latest.json`: referencia de escritorio con entrada sintética y cálculo real. - `tools/device-tests.html`: protocolo guiado común a iPhone y Android, registro por escenario y exportación local. [Uso y campos](diagnostics.md). - `examples/declarative-placement.html`: anillo válido y confirmación manual en odometría. - `npm run reports:summarize -- iphone.json android.json`: resumen por dispositivo/build, sin certificar automáticamente. El navegador no proporciona una lectura portátil de temperatura ni una medida universal de memoria del proceso. Registrar calentamiento o throttling mediante herramientas del dispositivo, anotando el método. Separar cambios térmicos de diferencias de batería, ahorro de energía o carga de otras aplicaciones. ## Límites conocidos Objetivos planos y rígidos; una imagen uniforme no es rastreable. VO local depende de paralaje, textura y una escena mayoritariamente estática. No hay cierre de bucle global; existen snapshots locales que necesitan relocalización, no anclas universales persistentes. Un plano visual inferior no garantiza suelo horizontal sin gravedad. Intrínsecos, lente y recorte deben corresponder a la captura. Raw Camera Access, anchors y WebXR requieren soporte y permisos específicos. ## Cobertura pendiente de hardware | Plataforma física | Cobertura mínima | Estado inicial | | --- | --- | --- | | iPhone/iPad | Safari y navegador habitual; imagen, vídeo/gestos, fallback, transición, recuperación y sesión prolongada | Informe parcial de imagen del usuario con build anterior; SDK actual y SLAM pendientes | | Android gama media | Navegador habitual; los mismos casos y native hit-test si se concede | Pendiente de informes físicos | | Android de prestaciones limitadas | Imagen pequeña, carga, adaptación, vídeo y sesión prolongada | Informe parcial de imagen del usuario con build anterior; falta modelo exacto y SDK actual/SLAM | No se elige backend por esa tabla: se consulta soporte real. El caso WebXR no compatible debe marcarse explícitamente, sin convertirlo en éxito. La herramienta no inventa modelo exacto ni versión del navegador a partir del user-agent; anótalos. Mantén separados navegador/SO/build del SDK y los resultados de emulación. La casilla físico es una declaración del operador, no una verificación independiente. Marcar cada dispositivo como `pendiente`, `aprobado con límites` o `falló`, vinculando su informe. La ausencia de un dispositivo nunca cuenta como aprobación. En `tools/device-tests.html`, el botón **Iniciar cámara** aparece antes del formulario y lleva al visor. También puedes iniciar desde **Iniciar AR**, en la barra superior de la escena. Con `gestures`, los toques dentro del visor manipulan contenido sin desplazar la página; fuera del visor puedes seguir desplazando el formulario. ## Informes aportados el 10 de noviembre de 2026 Se revisaron dos registros declarados por el operador como físicos: iPhone 17 Pro/Safari (7.51 s, 40 frames perfilados) y Android descrito como de prestaciones bajas (64.76 s, 35 frames perfilados, sin modelo exacto ni versiones declaradas). Ambos usaban build 7e52a3fa6b95a911, modo image y backend auto, con hasta 490102 triángulos. No ejecutaban el matcher WASM exacto introducido después. No se conservan aquí las capturas crudas ni identificadores privados. | Medida en la captura anterior | iPhone | Android | | --- | --- | --- | | Matching medio | 92.33 ms | 993.52 ms | | Worker medio | 133.80 ms | 1339.56 ms | | Lectura de cámara media | 6.80 ms | 74.30 ms | | Consulta immersive-ar | false | true | En Android, los 35 frames hicieron búsqueda completa y los 35 vencieron el plazo de revisión: el procesamiento superaba 1 s incluso en su mínimo. Esto explica una realimentación de coste en esa versión; se necesita repetir con el matcher actualizado antes de atribuir mejoras al cambio. También hay costes de ORB (177.97 ms de media) y lectura. La captura de iPhone es demasiado corta para aceptación sostenida. Los flags de desenfoque/movimiento son heurísticos, no causas confirmadas. El usuario no pudo probar SLAM desde la herramienta. Se corrigió la selección para aplicarse automáticamente y se añadió Probar SLAM sin marcador, guía por modo y comprobación del build antes de pedir cámara. La regresión automatizada verifica arranque óptico sin targets ni peticiones de marcador, vuelta a imagen y solicitud nativa hit-test con gesto, simulando el rechazo de la sesión. No sustituye una sesión XR física ni valida la calidad del anclaje. Repetir 30–60 s con SDK actual, mismo modelo/marcador, anotando dispositivo y versiones. Probar por separado imagen, óptico sin marcador y WebXR si se concede. En estos registros, el iPhone declaró que no tenía XR; el Android declaró immersive-ar compatible, pero todavía debe verificarse la sesión real. No elegir backend basándose solo en el nombre del teléfono. Corrección de acceso a cámara: una diferencia entre el build esperado por la herramienta y el del SDK compatible genera aviso, sin impedir iniciar. En pruebas Chrome/WebKit con viewport móvil se verifican solicitud solo tras toque y recuperación de botones después de rechazar permisos. Los permisos se simulan en esa regresión; no representa una concesión real en un teléfono. La regresión de acceso a cámara incluye una descarga GLB retenida: se observa getUserMedia y vídeo activo antes de liberar el modelo, sin solicitudes duplicadas. Se prueban el botón superior y el nativo del componente. Los mensajes de inicio/error se muestran antes del formulario. La prueba usa cámara sintética y no permite afirmar que un problema en un teléfono físico esté resuelto sin repetirlo allí. --- Fuente: docs/glossary.md # Glosario | Término | Significado en esta librería | | --- | --- | | HTML declarativo | Etiquetas webar-scene/target/model que configuran la aplicación sin montar manualmente el motor | | Web Component | Elemento HTML registrado por el SDK; no requiere A-Frame | | API programática | Clases y callbacks para control explícito; alternativa avanzada | | Target / marcador | Imagen de referencia conocida; no requiere borde negro | | NFT | Seguimiento mediante características naturales de una imagen | | ORB / FAST / descriptor | Detección y representación compacta de características visuales | | LK / optical flow | Seguimiento de puntos entre frames mediante Lucas–Kanade | | WASM | Runtime de cálculo compilado a WebAssembly | | Worker | Hilo de procesamiento independiente de la UI | | WART / .dat | Formato binario versionado de marcador de esta biblioteca | | Pose | Transformación aceptada, con backend, tiempo, escala y matrices | | ModelView | Transformación de objeto al espacio de cámara | | Intrínsecos | Resolución de referencia, focal y centro óptico; distorsión opcional | | Backpressure | No enviar otro frame mientras el consumidor sigue ocupado | | Tolerancia de pérdida | Intervalo antes de confirmar la desaparición del target | | Retención | Mantener visible la última transformación sin nueva medición | | Hit-test | Consulta de intersección del sistema XR con superficies del entorno | | Ancla | Transformación situada en un marco espacial; no equivale a retención | | VO / odometría visual | Estimación de movimiento a partir de imágenes | | SLAM | Localización y construcción de mapa; aquí el fallback ofrece un mapa local | | Paralaje | Cambio de posición aparente por traslación de cámara que aporta profundidad | | Keyframe | Observación seleccionada para mantener geometría y descriptores del mapa | | Relocalización | Reconocer el lugar en un mapa para recuperar una pose válida | | Deriva | Error acumulado de una trayectoria estimada | | GLB | Contenedor binario de glTF; el wrapper requiere recursos autocontenidos | | Draco / Meshopt | Compresión geométrica soportada por el cargador de modelos | | Chroma key | Transparencia por comparación de color, calculada en GPU | | Standalone | Bundle clásico que incluye Three.js y wrappers | ## English documentation --- Source: docs/en/README.md # WebAR Fácil documentation **[Español](../README.md)** · **English** WebAR Fácil is an MIT-licensed TypeScript library for building browser-based augmented reality. The standalone build includes the renderer, vision runtime, workers and model decoders. Start with HTML or use the JavaScript API to control the camera, tracking and rendering yourself. ## Start here 1. [Your first HTML app](getting-started.md) gets a JPG or PNG marker and a GLB on screen. 2. [Declarative HTML](declarative.md) describes scene attributes, content and events. 3. [Declarative recipes](recipes.md) adds transforms, animations and transitions. 4. [Installation and distribution](installation.md) explains the standalone, ESM and core builds. | Goal | Where to start | | --- | --- | | Add a model, video, gestures or several image targets | [Declarative HTML](declarative.md), [Interaction](interaction.md), [Multiple markers](multiple-targets.md) | | Show a model when the camera detects a phone | [Phone recognition](phone-detection.md) | | Control rendering and the engine in JavaScript | [Programmatic integration](programmatic.md), [WebAREngine](api-engine.md), [Three.js](threejs.md), [A-Frame](aframe.md) | | Place content in space and recover a saved map | [Spatial tracking](spatial-tracking.md), [Placement](placement.md), [Local maps](maps.md) | | Tune capture and diagnose tracking | [Camera and performance](camera-performance.md), [Events](events.md), [Diagnostics](diagnostics.md) | | Check what is implemented and tested | [Generated API](api-generated.md), [Verification](verification.md), [Physical-device acceptance](device-validation.md) | | Generate a working example with an AI assistant | [AI integration contract](ai-guide.md), [JSON API contract](../api.json) | ## Run an example The [gallery of 31 examples](../../examples/PRUEBAS/index.html) covers markers, gestures, 3D models, video, surfaces and local maps. Use the [image example](../../examples/declarative.html) for a first test, the [video example](../../examples/declarative-video.html) for chroma key, or the [placement example](../../examples/declarative-placement.html) for a surface. Image tracking and optical odometry work with ordinary camera capture. Native immersive WebXR depends on the browser and device. Optical placement without a metric reference uses relative scale. [Tracking modes](spatial-tracking.md) --- Source: docs/en/getting-started.md # Your first HTML application ## Put three files next to your HTML ```text my-webar-app/ index.html webar-facil.min.js poster.jpg model.glb ``` Copy the compiled `webar-facil.min.js` from the distribution. The script contains Three.js, OpenCV/WASM, workers, Draco and Meshopt. Alternatively, point the script tag at a hosted copy of the SDK. Marker and model paths are still relative to your HTML. ```html My first WebAR app

Loading WebAR Fácil…

``` ## Open the camera and find the marker 1. Serve the directory over localhost on your computer or trusted HTTPS on your phone. 2. Press the scene's camera start button. 3. Print the exact image or show it on another screen and point the camera at it. 4. Stop the scene when finished. JPG and PNG targets compile on demand. The default marker width is 20 cm. Use the actual printed width in meters when you need scale relative to the marker. A large, sharp and well-lit image is easier to recognize. ## Run the repository Use Node.js 22.18 or newer and run `npm ci`, then `npm run dev`. Open the [declarative example](../../examples/declarative.html) or the [gallery](../../examples/PRUEBAS/index.html). You only need a static server for a separate project with the compiled SDK. | Where the HTML lives | Example script path | | --- | --- | | Next to the SDK | `./webar-facil.min.js` | | Repository root | `./dist/webar-facil.min.js` | | Inside the repository's `examples/` directory | `../dist/webar-facil.min.js` | `webar-facil.min.js` remains a compatible filename. Both `WebARFacil` and `WebARFacil` expose the same standalone API. If the loading message stays on screen, check the script path and the browser console. [Troubleshooting](troubleshooting.md) --- Source: docs/en/phone-detection.md # Phone recognition [Example 31](../../examples/PRUEBAS/31-reconocer-celular.html) uses an optional object detector. Point at another phone and enable «Mostrar detección y confianza» to see the detected box and confidence. Kai follows the box on screen. Inference runs locally in a worker. ## Install the phone detector (example 31) The detector is optional and distributed separately from `webar-facil.min.js`. To add it to your project, copy `31-reconocer-celular.html`, the entire `phone-detector/` folder alongside the HTML, and the GLB model you want to show. - Keep `demo.js`, `worker.js`, `tf.min.js`, `coco-ssd.min.js`, `model.json`, and all five files from `group1-shard1of5` through `group1-shard5of5` together. The weights manifest uses those exact names. Include `LICENSE.txt` and keep `README.md` for reference. - In the HTML, point the first ``. If you change the GLB or folder layout, update the path in `demo.js`. The model path resolves relative to `demo.js`, not the HTML. - Serve the files over HTTPS or use `localhost` during development. The worker loads the weights from its own folder and processes camera frames locally. Apps that only use markers or SLAM can use the SDK without copying the detector. --- Source: docs/en/declarative.md # Applications with declarative HTML Use `webar-scene` for the camera, renderer and controls. Put each model or video inside its `webar-target` in image mode. In surface mode, put the content directly in the scene. See the [complete first app](getting-started.md). ## Scene attributes | Attribute | Default or effect | | --- | --- | | `mode` | `image`; use `slam` for markerless placement | | `backend` | `auto` in image mode; `optical-flow` in SLAM mode | | `slam` | Adds the transition control to an image scene | | `keep-content-on-lost` | Holds the last displayed transform when enabled | | `loss-grace` | 500 ms | | `performance` | `auto`, `balanced`, `battery` or `quality` | | `camera-width`, `camera-height`, `fps` | Requested capture settings; the browser negotiates the stream | | `analysis-width`, `analysis-pixels` | Analysis limits; auto/balanced cap at 640 px wide and 307,200 pixels | | `pixel-ratio`, `antialias` | Renderer controls; default maximum pixel ratio 2 and MSAA enabled | | `asset-base`, `opencv-url`, `draco-path` | Optional explicit paths for external runtime and decoder assets | | `gestures`, `min-scale`, `max-scale` | Opt-in selection, rotation and scaling; initial bounds 0.1 and 100 | | `scan-guide`, `surface-grid` | Marker scan guide and surface grid, enabled by default | | `placement` | `auto` or `manual`; manual optical placement needs a recent proposal | | `media-menu`, `fullscreen-view` | Optional capture menu and full-viewport session controls | | `diagnostics`, `keyframe-correction` | Optional local reports and experimental optical keyframe correction | Boolean attributes accept an empty value, `true` or `false`. Use CSS to override the default `100dvh` height. Camera/backend configuration is read at startup. Stop, apply the new values and start again when changing those settings. [Camera details](camera-performance.md) ## Image and content attributes `webar-target` takes a JPG, PNG or compiled WART file through `src`, a `target-id`, and `physical-width` in meters. Omitted IDs become `target-0`, `target-1` and so on. The default physical width is 0.2 m. Use up to eight different images. [Multiple markers](multiple-targets.md) `webar-model` loads a self-contained GLB, including Draco and Meshopt. Use `size` for the largest dimension (default 0.16), `scale` for uniform or XYZ scaling, `rotation` for XYZ degrees, and `position` for local XYZ coordinates. `animation` accepts an exact name, zero-based index or `none`. By default the first available clip plays. `webar-video` accepts MP4, `width`, `orientation`, `autoplay`, `muted`, `loop` and chroma options. [Interaction reference](interaction.md) ## Camera and media controls Use `media-menu` for camera presets, photo and microphone video. Saving and sharing use browser capabilities. The composited recording path applies to web camera scenes. Native immersive WebXR camera imagery is outside that path. Use `fullscreen-view` for full-viewport viewing and a session dialog. ## Use JavaScript with HTML elements ```js const scene = document.querySelector('webar-scene'); scene.addEventListener('targetFound', event => console.log(event.detail.targetId)); scene.addEventListener('trackingQuality', event => console.log(event.detail.issues)); scene.addEventListener('ar-error', event => console.error(event.detail)); ``` | Event | Payload or meaning | | --- | --- | | `ar-ready`, `ar-stopped` | Prepared `{app}`, or `null` after stopping | | `ar-error`, `ar-status` | Error, or `{message}` | | `ar-pose`, `ar-slam` | Accepted `ARPose`, or transition result | | `modelLoaded`, `animationFinished` | `{animations}`, or `{name}` | | `videoLoaded`, `videoPlayBlocked` | `{width,height}`, or playback error | | `ar-select`, `ar-gesture` | Pointer selection and transforms | | `contentUpdated`, `ar-placement` | Synchronized content, or placement `{state}` | | `targetFound`, `targetLost`, `targetVisibility`, `trackingQuality` | Same payloads as the [engine events](events.md) | `start()`, `stop()`, `refresh()` and `enableSLAM(options?)` return promises. Await `model.updateComplete` after changing a model source. Use `scene.refresh()` after changing the hierarchy. `scene.app` and `scene.engine` are available during the session. Scene `stop()` disposes the managed renderer and resources. Removing the element in an SPA also frees resources. --- Source: docs/en/interaction.md # Video, gestures, animation and live content ## Play MP4 and remove a color ```html ``` Use `orientation="flat"` for a plane lying on its anchor and `upright` for a vertical plane. The color, similarity, smoothness and spill controls affect the shader. Try a well-lit, even background. Muted video is more likely to autoplay. When the browser blocks playback, use the scene's play control or start playback from a click. [Video reference](video.md) ## Enable touch gestures Add `gestures` to the scene or content. Tap a model to select it, drag to rotate it, and pinch to change scale. `min-scale` and `max-scale` bound the multiplier. The gesture transform is local to the content and does not modify the tracked camera pose. Use `ar-select` and `ar-gesture` for custom interaction. ## Choose and control animations ```html ``` Loop modes are `repeat`, `once` and `pingpong`. `animation-repetitions`, `animation-paused`, and `animation-fade` control repeat count, pause and blending. Listen to `modelLoaded` for clip names and `animationFinished` for completion. The element also exposes `playAnimation(nameOrIndex)` and `stopAnimations()`. ## Replace assets without closing the camera ```js const model = document.querySelector('webar-model'); model.setAttribute('src', './replacement.glb'); await model.updateComplete; ``` Transforms and playback settings can be updated live. A failed load can be handled with the content error event. When replacing the hierarchy or editing targets, update the DOM and await `scene.refresh()`. Replacing a target can trigger image compilation and change visibility. Changing capture settings or tracking backend follows the session lifecycle. Try the [live content example](../../examples/declarative-interactive.html), the [video example](../../examples/declarative-video.html), and the [declarative recipes](recipes.md). --- Source: docs/en/image-targets.md # Image markers and compilation ## Choose an image Use a sharp JPG or PNG with distinctive features distributed across the image. A planar print or a static screen works best. Large blank areas, repeating motifs, glare, blur and a very small image in the camera all make matching harder. ```html ``` The scene compiles the target automatically. Use the actual width in meters for physical scale. The tracking transform applies to the image plane and content can be repositioned locally. ## Use the engine compiler ```js const result = await engine.addImageTarget('poster', './poster.jpg', { physicalWidthMeters: 0.2, maxDimension: 1024, }); console.log(result.metadata.quality); ``` The source can be a URL or a Blob/File. The compiler checks input size and format, constrains the working resolution, and extracts features in a worker. The result includes a WART binary buffer and metadata. Quality warnings describe properties such as low texture or weak feature distribution. They are indicators, not a guarantee of real-world detection. ## Compile once and reuse For a deployment that needs deterministic precompiled assets, use the repository's compiler script or `MarkerCompiler`. Save the binary as `.dat` and load it through `addTarget()` or a declarative target source. The JSON metadata is optional. Keep marker IDs and scale consistent when replacing assets. Use a new URL for a changed image when cache behavior matters. See the [API reference](api-generated.md) for compiler options, the [quality events](events.md) for live detection, and [multiple markers](multiple-targets.md) for independent targets. --- Source: docs/en/models.md # GLB models and animations ## Configure a model ```html ``` Provide a self-contained GLB. `size` fits its largest dimension and the wrapper positions its base and center. `scale` multiplies that result, with a single factor or XYZ factors. Rotation uses XYZ degrees in HTML. Position is local to the tracked group. Set size relative to your marker or placement backend and inspect it on the target device. ## Load and animate with JavaScript ```js const model = new WebARFacil.ThreeARModel(); await model.load('./model.glb', { fitSize: 0.3, autoplay: false }); model.playAnimation('Walk', { loop: 'repeat', speed: 1 }); ``` Use `fitSize:false` to preserve model units. The renderer should update animation every frame even if no fresh tracking pose arrived. Stop clips when required and dispose the model and scene when leaving a page or route. ## Compression and rendering cost The standalone includes Draco and Meshopt. For custom builds, configure Draco decoder paths if external assets are used. Compression primarily reduces transfer size. Inspect triangles, texture sizes, materials, draw calls and animation complexity when a scene is slow. Compare the same experience with a lightweight GLB to isolate render cost. [Camera and performance](camera-performance.md) Change `src` to replace content and await the element's `updateComplete`. Use the [animation example](../../examples/PRUEBAS/15-animaciones-glb.html) to inspect clips and the [interaction guide](interaction.md) for playback controls. --- Source: docs/en/multiple-targets.md # Multiple image markers Use distinct target images and IDs. The same session can track up to eight targets. Different instances of the exact same print are not independent image IDs. ```html ``` With JavaScript, bind groups to their IDs or add the models through `app.addContent(id, model)`. Then register targets using `engine.addImageTarget(id, source, options)`. Subscribe to `targetFound`, `targetLost` and `targetVisibility` and route each event using `targetId`. When transitioning from an image to a surface, explicitly choose the target you intend to preserve. Large images and multiple simultaneous targets can increase matching cost. Check quality per target, choose visually distinct artwork and test at the distance visitors will use. [Multiple-marker example](../../examples/PRUEBAS/14-varios-marcadores.html) --- Source: docs/en/events.md # Events, tracking continuity and diagnostics ## Track lifecycle and visibility | Event | Main fields | Use | | --- | --- | --- | | `targetFound` | `targetId`, `timestampMs`, `pose` | First detection or reacquisition | | `targetLost` | `targetId`, `timestampMs`, `retained`, `reason` | Confirmed loss, removal or stop | | `targetVisibility` | `targetId`, `visible`, `reason` | Control visibility in a custom renderer | | `trackingQuality` | `targetId`, `timestampMs`, `state`, `score`, `processingMs`, `issues`, `diagnosis` | Guide users and diagnose tracking | Listen through `scene.addEventListener(name, callback)` and read `event.detail`, or subscribe via `engine.on(name, callback)`. The engine returns an unsubscribe function. `ar-ready` means the scene is prepared. Use target events to determine whether a marker is visible. ## Avoid flicker and restarts The loss grace period absorbs brief interruptions. The target states are `tracking`, `recovering` and `lost`. Found/lost events occur on transitions, so load assets once and keep playback independent of these events. Enable `keep-content-on-lost` to retain the last displayed transform after confirmed loss. In a running scene, call `scene.engine.setKeepContentOnLost(enabled)` and update the attribute for the next session. ```js const scene = document.querySelector('webar-scene'); scene.addEventListener('trackingQuality', ({detail}) => { if (detail.issues.includes('small-target')) console.log('Move closer'); if (detail.issues.includes('blur')) console.log('Hold the camera steady'); }); ``` Quality scores range from 0 to 1 and are heuristic. Use motion, inlier count and measured timings with the issue labels. [Diagnostics](diagnostics.md) --- Source: docs/en/recipes.md # Declarative HTML recipes ## Size, rotation and animation ```html ``` ```js const avatar = document.querySelector('#avatar'); avatar.setAttribute('size', '0.45'); avatar.setAttribute('rotation', '0 90 0'); avatar.addEventListener('modelLoaded', ({detail}) => console.log(detail.animations)); ``` To retain content after the marker disappears, enable `keep-content-on-lost` on the scene. During a running session use the engine method and keep the markup in sync if the choice should survive restart. A retained object stays on the last known transform. ## Change a model from a button ```js document.querySelector('#change-model').onclick = async () => { const avatar = document.querySelector('#avatar'); avatar.setAttribute('src', './second.glb'); await avatar.updateComplete; }; ``` Use a different target ID when the physical image changes. Use `scene.refresh()` after DOM hierarchy changes. When you only update a model source, retain the original target and camera. ## Place content on a surface ```html ``` To transition from image mode, add `slam` to the scene. Start on the marker, then use the transition control. Confirm the proposed location if the chosen placement mode requires confirmation. [Spatial transitions](spatial-tracking.md) For hands-on examples, see the [gallery](../../examples/PRUEBAS/index.html) and the [advanced recipes](recipes-programmatic.md). --- Source: docs/en/spatial-tracking.md # Spatial tracking with WebXR and optical fallback WebXR and optical tracking both expose placement poses. Select the backend with `backend="auto"`, `backend="webxr"`, or `backend="optical-flow"`. Auto can use WebXR when immersive AR and hit testing are available. Use the optical backend to test camera-based tracking explicitly, including on iOS browsers where immersive AR is unavailable. A declarative SLAM scene defaults to optical tracking. ```html ``` 1. Open the page over HTTPS and start AR with the scene button. 2. Aim at a textured surface and move the phone slowly to produce parallax. 3. Wait for a placement guide and confirm the position. 4. Move sideways and return to the initial view to inspect drift. In native WebXR, accept the browser’s immersive prompt and confirm placement inside the session. Optical tracking estimates camera motion and a local surface from frames. Its scale is relative when no metric reference is available. A marker with a known physical width can seed metric scale under suitable conditions. Markerless metric tracking remains experimental. Use [device validation](device-validation.md) to compare backends. --- Source: docs/en/placement.md # Visual guidance and placement confirmation After placing content in example 30, walk normally. The map automatically collects viewpoints along the route. If tracking is lost, the example attempts to recover the original placement. «Otra zona» starts a new map. In [example 30](../../examples/PRUEBAS/30-explorar-superficies.html), tap to choose an area of the optical SLAM plane to inspect. Blue dots highlight contrast in the camera feed and yellow dots show map points near the plane. The green area marks the extent supported by those points. Tap the area or select «Usar zona» to preview a GLB or MP4. Pinch to resize the grid within the detected area or to scale the content. The image scan guide tells users where to aim. The surface guide previews where spatial content will be placed. With `placement="manual"` the user confirms before the model becomes anchored. | State | What to do | | --- | --- | | Scanning for image | Put the whole marker in view and avoid glare. | | Searching for a surface | Aim at a textured area and move sideways gently. | | Surface proposed | Inspect the grid and confirm placement. | | Content anchored | Walk around the model and verify that it stays near the selected point. | The optical grid reflects a local estimate. If it straddles a tabletop and an object, move the view until the intended area fills the center and reacquire a proposal. Use the [spatial tracking chapter](spatial-tracking.md) to choose the backend. ## Matches and recovery in example 30 Optical SLAM compares each recovery frame against up to **50 saved views**. It evaluates each view separately. A visual match pairs similar ORB descriptors. A geometrically consistent point (*inlier*) is a match that also fits the estimated camera position and orientation. | Message in the example | Meaning | | --- | --- | | No matches found | None of the saved views produced any matches. | | Few matches found | The best view produced 1 to 14 matches. The engine needs 15 before attempting pose estimation. | | Matches found, but insufficient to confirm | At least one view produced 15 or more matches, but none passed geometric validation. | | Candidate pose found | The estimate passed basic validation and needs a second compatible observation. | **Basic validation** requires at least 15 geometrically consistent points, at least 40% of that view's matches, and coverage across at least 4 image cells. The bounding rectangle of those points must cover at least 8% of the analyzed frame. **Single-frame confirmation** requires at least 30 inliers, a 65% inlier ratio, coverage across 6 cells, and reprojection error below 1.5 pixels. If the estimate only passes basic validation, the engine requires two compatible observations. If a tabletop has little texture, frame its edges and surroundings to give the engine more visual references. --- Source: docs/en/diagnostics.md # Local diagnostics for iPhone and Android Use diagnostics in development to reproduce camera, rendering and tracking problems. For a controlled comparison, use the same marker or surface, camera distance, scene and sequence of movements on both phones. Record the browser, backend and performance preset with each run. 1. Open the development diagnostics tool over HTTPS. 2. Start one scene and let the camera settle before collecting a baseline. 3. Repeat the action that causes the issue, such as covering the marker or moving the phone sideways. 4. Export the report and identify the run by device and scenario. | Observation | Signals to compare | | --- | --- | | Model stutters while marker stays visible | Render cadence, frame time, model complexity and device load. | | Model jumps after occlusion | Target loss and recovery events, pose quality and marker size in frame. | | Optical anchor moves with camera motion | Backend, initialization, parallax and relocalization. | Development diagnostics use structured measurements. The public gallery omits the report controls. See [camera and performance](camera-performance.md) and [odometry validation](odometry-validation.md). --- Source: docs/en/odometry-validation.md # Validating odometry, relocalization and drift Treat acquisition, tracking and recovery as separate measurements. Repeat each test several times on the same textured surface, with the same route and lighting, and record conditions such as glare and autofocus hunting. | Test | Procedure | Record | | --- | --- | --- | | Initialization | Move laterally until a surface can be selected. | Time to first valid placement. | | Drift | Place an object, walk a short loop and return to the starting view. | Offset from the original placement and tracking interruptions. | | Recovery | Briefly cover the camera or look away, then return to the mapped view. | Whether the pose recovers and how long recovery takes. | | Reload | Save and restore an optical map, then revisit the original viewpoint. | Whether the environment relocalizes and the anchor returns. | For optical sessions initialized without metric scale, compare alignment visually or in relative units. For measurements in meters, use a known-size reference and check the result against a physical measurement. Compare like-for-like backends and keep failed runs in the record. See [local maps](maps.md) and [physical-device acceptance](device-validation.md). --- Source: docs/en/installation.md # Installation and distribution The hosted standalone script registers custom elements and includes Three.js. Place it in the page head with `defer`, then declare the scene in the body. The npm package is named `webar-facil`. Use `WebARFacil` in new classic-script integrations. Classic builds also expose the legacy `MiLibreriaAR` alias for existing integrations. ```html ``` Choose the distribution based on how the application is built. | Application | Use | | --- | --- | | Plain HTML | Hosted standalone script and declarative elements. | | Existing JavaScript application | The standalone global or the package exports in a bundler. | | Source checkout or GitHub ZIP | Run `npm ci` and `npm run build`, then inspect `dist/`. | A model, marker or video URL in your HTML resolves against your page. Put assets beside your page or use absolute URLs served with compatible CORS headers. Use HTTPS for device testing. See [local release](local-release.md) and [deployment](deployment.md). --- Source: docs/en/local-release.md # Identifying and serving a local build From a source checkout, build the distributable and serve it through HTTP on localhost or HTTPS on another device. ```sh npm ci npm run build npm run docs:check npm run dev ``` The build creates `dist/webar-facil.min.js`, the documentation and the example gallery. Use the local server URL printed by the development command and follow the example’s paths. To verify what a browser loaded, inspect the SDK script in the Network panel and use the exported `LIBRARY_VERSION` and `LIBRARY_BUILD` from the loaded SDK when troubleshooting a stale copy. The hosted script URL is documented in [installation](installation.md). For a portable release, review [release preparation](alpha-release.md) and [implementation verification](verification.md). --- Source: docs/en/alpha-release.md # Preparing the public alpha Before distributing a GitHub archive or updating the hosted bundle, verify it from a clean extraction. Include the MIT license, dependency notices, source, lockfile, documentation and runnable examples. Keep development reports, environment settings and installed dependencies out of the archive. - Build and run the documentation checks from the extracted copy. - Start with the declarative marker example, then test video and spatial placement. - Check that the English and Spanish manuals contain the same chapters and link to each other. - Record results separately for desktop, iOS and Android. Record backend and browser for spatial tests. - Keep experimental markerless metric placement visibly identified in its example. See [contributing](contributing.md) for commands and [device validation](device-validation.md) for acceptance scenarios. --- Source: docs/en/camera-performance.md # Camera, focus and performance Set `performance` on the scene. The preset influences capture and processing budgets. Actual video dimensions, frame rate and focus capabilities depend on the camera track and browser. ```html ``` | Profile | Typical use | | --- | --- | | `auto` | Start here when the device capabilities are unknown. | | `battery` | Reduce processing load for long sessions or constrained hardware. | | `balanced` | Compare tracking and render cadence under a consistent workload. | | `quality` | Test whether additional detail helps a small or distant marker. | Compare a lightweight model and the production GLB with the same marker and profile. If tracking remains steady while rendering slows, inspect geometry, texture sizes and draw calls. If detection worsens as resolution falls, let the marker occupy more of the image. Test camera switching and autofocus on the physical device before drawing conclusions from a desktop webcam. See [diagnostics](diagnostics.md). --- Source: docs/en/deployment.md # Deployment and browser security Serve production pages and device previews over HTTPS. Desktop loopback URLs can use a secure-context exception. For a phone on your LAN, use an HTTPS host that the phone trusts. Open the page from its served URL so that scripts, models, images and media resolve over HTTP or HTTPS. | Resource | Deployment check | | --- | --- | | SDK | Script returns JavaScript and loads before AR initialization. | | Marker and model | URLs resolve relative to the document, with exact filename casing. | | Video | Browser can decode the container and codec. Cross-origin hosting permits access when used as a texture. | | Camera or microphone | Permission granted by the browser and any enclosing iframe policy. | If the page is embedded, the host page must allow the required features through its permissions policy and iframe configuration. Open the page directly to isolate iframe restrictions. Use the [troubleshooting guide](troubleshooting.md) to separate network and camera failures. --- Source: docs/en/troubleshooting.md # Troubleshooting | Symptom | Check | Action | | --- | --- | --- | | Page shows “Loading SDK” | Network panel, SDK response and console. | Correct the script URL and confirm the response contains JavaScript. | | Camera button does nothing | Secure context, browser permissions and other apps using the camera. | Open the HTTPS page directly and retry from a fresh tab. | | Module fetch fails | Exact URL, response status and content type. | Serve the current standalone build or copy the runtime files required by an older build. | | Model never appears | Marker detected event, model URL and size. | Verify the original marker, then test the GLB in the simplest example. | | Video appears black | Codec support, autoplay policy and CORS. | Use a supported MP4 and start it from a user interaction if required. | | SLAM guide appears but content does not | Placement confirmation. | Use the scene’s placement button or select within native WebXR. | | Content jumps or drifts | Backend, texture, motion and scale. | Test a textured area and follow the [odometry procedure](odometry-validation.md). | For reproducible reports, include the browser, device, exact page, backend and steps to trigger the issue. Capture the first console error and check its URL before chasing subsequent failures. --- Source: docs/en/concepts.md # Concepts and coordinate systems | Term | Meaning in WebAR Fácil | | --- | --- | | Target | Reference image and its precomputed features. | | Model-view matrix | Target or anchor transform expressed in camera coordinates. | | Projection matrix | Camera intrinsics expressed for the renderer, including the near and far planes. | | Anchor | A content transform retained relative to a spatial reference. | | Content transform | The model's local position, scale and rotation relative to the tracked parent. | Pose matrices have 16 elements in column-major order. Treat the projection and model-view as a pair and let the wrapper apply the conventions it expects. With a custom renderer, align the projected viewport with the displayed video crop. Apply visual offsets to a child of the tracked group so a new pose can update the parent independently. The optical backend uses relative units until initialized with a metric reference. --- Source: docs/en/programmatic.md # Advanced programmatic integration Use `ThreeARScene` when the application needs buttons, application state or runtime content changes while delegating rendering and camera composition to the library. The scene owns a renderer, camera, lights and animation loop. Load your assets, start the camera from a user action and register the image target. ```js const { ThreeARScene, ThreeARModel } = window.WebARFacil; const app = new ThreeARScene({ container: document.querySelector('#view') }); const model = new ThreeARModel(); app.addContent('poster', model); document.querySelector('#start').addEventListener('click', async () => { try { await app.engine.startCamera(); await app.engine.addImageTarget('poster', './poster.jpg', { physicalWidthMeters: 0.2 }); await model.load('./robot.glb'); } catch (error) { console.error(error); await app.engine.stop(); } }); ``` Match the target ID used by `addContent` and `addImageTarget`. Call `app.dispose()` on unmount. If you stop and restart the engine, register your targets again. For the declarative equivalent see [first application](getting-started.md). For an existing renderer see [Three.js](threejs.md). --- Source: docs/en/recipes-programmatic.md # Advanced programmatic recipes ## Use files picked by the user Pass an image File to `addImageTarget`. For a GLB, create an object URL and release it when loading has finished. Load a replacement on the existing model so it stays associated with the target ID. ```js await app.engine.addImageTarget('poster', imageInput.files[0], { physicalWidthMeters: 0.2 }); const url = URL.createObjectURL(modelInput.files[0]); try { await model.load(url); } finally { URL.revokeObjectURL(url); } ``` In production, check that the inputs contain files and surface load failures near the corresponding control. Use a single controller to serialize start, stop and content changes. ## Export descriptors or preserve a local map The [marker chapter](image-targets.md) describes how to export precomputed targets. When using optical SLAM, persist the structured snapshot described in [local maps](maps.md). Store typed arrays using IndexedDB's structured clone support. ## Unmount an experience Remove listeners created by your page, revoke object URLs you still own, then await `app.dispose()` so the camera, renderer and owned content are released. When mounting another route, create a fresh scene. ## Editable text in AR Draw the text on a canvas and use it as a `THREE.CanvasTexture` on a `THREE.Sprite`. Redraw on input and set `needsUpdate = true`. Add the sprite to the image or spatial anchor after `ar-ready`. [Example 4](../../examples/PRUEBAS/04-texto-marcador.html) · [Example 10](../../examples/PRUEBAS/10-texto-slam.html) ## Control scene lighting On `ar-ready`, get the Three.js scene from `scene.app.scene`. Find the directional light and use the slider value to update its position. Apply the initial value when the scene is ready and update it on `input`. [Example 6](../../examples/PRUEBAS/06-iluminacion-marcador.html) · [Example 12](../../examples/PRUEBAS/12-iluminacion-slam.html) ## Select model parts Convert the pointer position to normalized canvas coordinates and use `THREE.Raycaster` with the scene camera. Highlight the first intersected mesh and save its original material state so you can clear the selection. [Example 19](../../examples/PRUEBAS/19-seleccionar-partes.html) ## Change catalog colors Update the model's `src` and await `updateComplete`. Then traverse the GLB, find the meshes you want to tint and call `material.color.set(...)`. Reapply the tint when the color input changes. [Example 20](../../examples/PRUEBAS/20-catalogo-colores.html) ## Combine model text and video Put the GLB and MP4 in the same `webar-target`. Add the text sprite to the same anchor. Each item has its own visibility or playback control. [Example 24](../../examples/PRUEBAS/24-contenido-compuesto.html) --- Source: docs/en/video.md # MP4 video and chroma key ```html ``` Set `orientation="upright"` to stand the video up or `orientation="flat"` to lay it along the marker. Use `chroma-key` for a uniform key background and tune `key-color`, `similarity`, `smoothness` and `spill` using the actual footage. A compatible MP4 with a flat, evenly lit background produces a cleaner cutout. Muted playback works with more autoplay policies. For audio, provide a user action that starts playback and handle browser rejection. In a custom Three.js scene, `ThreeARVideo` provides the equivalent programmatic content. Manage its playback and disposal with the scene lifecycle. If a remote video plays as a plain HTML element but fails as a texture, check its CORS response headers. --- Source: docs/en/threejs.md # Three.js integration Use the standalone bundle's `THREE` namespace together with its wrappers, or import the module build and Three.js through the same bundler setup. Keep objects and loaders on a compatible Three.js instance. | Wrapper | Responsibility | | --- | --- | | `ThreeARScene` | Managed camera, renderer, render loop and pose application. | | `ThreeARGroup` | Tracked transform for an existing scene graph. | | `ThreeARModel` | GLB loading, model normalization and animation. | | `ThreeARVideo` | Video texture and optional chroma material. | ## Use an existing renderer [Example 29 · Three.js with a custom renderer](../../examples/PRUEBAS/29-threejs.html) shows an animated GLB on an image target with the camera, projection and render loop managed in JavaScript. Bind the tracked group to the target ID. The group applies the world matrix and follows engine visibility. Derive the camera transform from the inverse view matrix and apply the matching projection. ```js const tracked = new ThreeARGroup().bind(engine, 'poster'); scene.add(tracked); camera.matrixAutoUpdate = false; const unsubscribe = engine.onPose((_matrix, pose) => { if (pose.backend === 'webxr') return; camera.matrix.fromArray(pose.viewMatrix).invert(); camera.updateMatrixWorld(true); if (pose.projectionMatrix) { camera.projectionMatrix.fromArray(pose.projectionMatrix); camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert(); } }); ``` Use a child for local transforms. Keep the viewport and video crop aligned. If you only have model-view and update the group manually, use an identity camera. In immersive WebXR the renderer owns the XR camera and frame loop. On teardown, call `unsubscribe()` and `tracked.dispose()`, then release your own scene objects. See [the complete custom app](../../examples/custom-app.html) and [coordinates](concepts.md). --- Source: docs/en/aframe.md # A-Frame integration Load A-Frame first, create an engine and register the official components before A-Frame initializes the associated scene. The tracker component binds the entity to a target ID, while model and video components provide content helpers. ```js const { WebAREngine, registerAFrameComponent, registerAFrameModelComponent, registerAFrameVideoComponent } = window.WebARFacil; const engine = new WebAREngine(); registerAFrameComponent(AFRAME, engine); registerAFrameModelComponent(AFRAME); registerAFrameVideoComponent(AFRAME); ``` ```html ``` Start the engine from a button and register the same `poster` ID. Put authored transforms on a child entity, because tracking controls the parent's matrix. If another A-Frame component also drives the camera, resolve that ownership before starting AR. See [engine API](api-engine.md). | Component | Properties | | --- | --- | | `my-ar-tracker` | `target`, `targetId`, `physicalWidth` | | `my-ar-model` | `src`, `fitSize`, `autoplay` | | `my-ar-video` | `src`, `width`, `chromaKey`, `keyColor`, `similarity`, `smoothness`, `spill`, `muted`, `loop` | Set the tracker component's `target` after the camera starts if you want it to load the reference. A `.dat` file goes through compiled target loading. An image URL is compiled by the engine. Listen for `ar-error` on the tracker and `ar-model-error` on the model. If you remove the entity that owns a marker, remove its target explicitly with `engine.removeTarget(id)`. See the [complete A-Frame example](../../examples/PRUEBAS/25-aframe.html). --- Source: docs/en/maps.md # Persisting local optical maps An optical map stores a bounded set of local keyframes used for relocalization. The engine keeps it in memory during the session. Applications may save it explicitly and restore it into a later optical session. Use the same physical space and revisit a mapped viewpoint under similar lighting. ```js const snapshot = await engine.exportSLAMMap(); // Store snapshot with IndexedDB's structured clone support. const restored = await readSavedMap(); await engine.importSLAMMap(restored); ``` The snapshot carries a schema version, an optical map, a target ID and an optional anchor transform. The map includes camera intrinsics, a scale, a map ID and keyframes with descriptors, points and view matrices. Preserve typed arrays and reject incompatible or corrupt snapshots. Snapshots contain scene geometry and descriptors. Keep them private by default and give users a way to delete them from application storage. Use compatible camera, lens, capture dimensions and projection on restore. An imported anchor is eligible for display after the camera relocalizes against the environment. Native WebXR maps are managed by the platform and are outside this API. Unseeded optical maps use relative scale and may drift. See the [IndexedDB map example](../../examples/PRUEBAS/13-mapas-locales.html) and [odometry tests](odometry-validation.md). --- Source: docs/en/api-engine.md # WebAREngine `WebAREngine` manages camera access, image tracking, spatial tracking and event subscriptions. Create a scene wrapper for a managed renderer, or supply your own renderer and consume the engine's poses. | Task | Public method | | --- | --- | | Start camera | `startCamera()` | | Register an image | `addImageTarget(id, source, options)` | | Load a compiled target | `loadTarget(url)` | | Start spatial tracking | `enableSLAM(options)` | | Listen for transforms | `onPose(callback)` | | Listen for visibility or quality | `on('targetFound', callback)`, `on('targetLost', callback)`, `on('trackingQuality', callback)` | | Persist an optical map | `exportSLAMMap()`, `importSLAMMap(snapshot)` | | Release the session | `stop()` | Pass a measured `physicalWidthMeters` when size matters. Set `imageLossGraceMs` and `keepContentOnLost` to control temporary loss and final visibility. Await operations that change tracking state and dispose of application listeners on unmount. See the [generated reference](api-generated.md) for exact types. --- Source: docs/en/api-generated.md # Generated public API Generated from the public exports and TypeScript declarations. Relative imports retain their original context. The standalone script also exposes the THREE namespace. | Export | Kind | Entry points | | --- | --- | --- | | `ARAnimationOptions` | Type | `webar-facil` | | `ARComponentsOptions` | Type | `webar-facil` | | `ARDiagnostics` | Value and/or type | `webar-facil/core`, `webar-facil` | | `AREventMap` | Type | `webar-facil/core`, `webar-facil` | | `ARModelElement` | Type | `webar-facil` | | `ARModelLoadOptions` | Type | `webar-facil` | | `ARModelOptions` | Type | `webar-facil` | | `ARPose` | Type | `webar-facil/core`, `webar-facil` | | `ARSceneElement` | Type | `webar-facil` | | `ARSceneOptions` | Type | `webar-facil` | | `ARVideoElement` | Type | `webar-facil` | | `ARVideoOptions` | Type | `webar-facil` | | `CalibrationReport` | Type | `webar-facil/core`, `webar-facil` | | `CalibrationSession` | Value and/or type | `webar-facil/core`, `webar-facil` | | `CameraFrame` | Type | `webar-facil/core`, `webar-facil` | | `CameraIntrinsics` | Type | `webar-facil/core`, `webar-facil` | | `CameraManager` | Value and/or type | `webar-facil/core`, `webar-facil` | | `CameraManagerOptions` | Type | `webar-facil/core`, `webar-facil` | | `CameraMetrics` | Type | `webar-facil/core`, `webar-facil` | | `ChromaKeyOptions` | Type | `webar-facil` | | `CompilerOptions` | Type | `webar-facil/core`, `webar-facil` | | `DEVICE_TEST_PROTOCOL` | Value and/or type | `webar-facil/core`, `webar-facil` | | `DeviceTestCase` | Type | `webar-facil/core`, `webar-facil` | | `DeviceTestResult` | Type | `webar-facil/core`, `webar-facil` | | `DiagnosticConfiguration` | Type | `webar-facil/core`, `webar-facil` | | `DiagnosticContext` | Type | `webar-facil/core`, `webar-facil` | | `DiagnosticOptions` | Type | `webar-facil/core`, `webar-facil` | | `DiagnosticRendererMetrics` | Type | `webar-facil/core`, `webar-facil` | | `DiagnosticReport` | Type | `webar-facil/core`, `webar-facil` | | `DiagnosticStats` | Type | `webar-facil/core`, `webar-facil` | | `EngineOptions` | Type | `webar-facil/core`, `webar-facil` | | `EngineState` | Value and/or type | `webar-facil/core`, `webar-facil` | | `ExperimentalMetricScale` | Value and/or type | `webar-facil/core`, `webar-facil` | | `FrameQuality` | Type | `webar-facil/core`, `webar-facil` | | `ImageCompiler` | Value and/or type | `webar-facil/core`, `webar-facil` | | `ImageObservation` | Type | `webar-facil/core`, `webar-facil` | | `ImageTargetMetadata` | Type | `webar-facil/core`, `webar-facil` | | `ImageTargetOptions` | Type | `webar-facil/core`, `webar-facil` | | `ImageTargetResult` | Type | `webar-facil/core`, `webar-facil` | | `ImageTrackingProfile` | Type | `webar-facil/core`, `webar-facil` | | `LIBRARY_BUILD` | Value and/or type | `webar-facil/core`, `webar-facil` | | `LIBRARY_VERSION` | Value and/or type | `webar-facil/core`, `webar-facil` | | `MarkerCompiler` | Value and/or type | `webar-facil/core`, `webar-facil` | | `MarkerFeatures` | Type | `webar-facil/core`, `webar-facil` | | `MarkerQuality` | Type | `webar-facil/core`, `webar-facil` | | `MetricScaleDiagnostics` | Type | `webar-facil/core`, `webar-facil` | | `OdometryBackend` | Type | `webar-facil/core`, `webar-facil` | | `OdometryConfig` | Type | `webar-facil/core`, `webar-facil` | | `OdometryEstimate` | Type | `webar-facil/core`, `webar-facil` | | `OdometryMapSnapshot` | Type | `webar-facil/core`, `webar-facil` | | `OdometryModule` | Type | `webar-facil/core`, `webar-facil` | | `OdometryScanHint` | Type | `webar-facil/core`, `webar-facil` | | `OpticalFlowOptions` | Type | `webar-facil/core`, `webar-facil` | | `OpticalFlowSLAM` | Value and/or type | `webar-facil/core`, `webar-facil` | | `PlacementState` | Type | `webar-facil` | | `projectionFromIntrinsics` | Value and/or type | `webar-facil/core`, `webar-facil` | | `registerAFrameComponent` | Value and/or type | `webar-facil` | | `registerAFrameModelComponent` | Value and/or type | `webar-facil` | | `registerAFrameVideoComponent` | Value and/or type | `webar-facil` | | `registerARComponents` | Value and/or type | `webar-facil` | | `rotateIntrinsics` | Value and/or type | `webar-facil/core`, `webar-facil` | | `scaleIntrinsics` | Value and/or type | `webar-facil/core`, `webar-facil` | | `SLAMManager` | Value and/or type | `webar-facil/core`, `webar-facil` | | `SLAMMapSnapshot` | Type | `webar-facil/core`, `webar-facil` | | `SLAMOptions` | Type | `webar-facil/core`, `webar-facil` | | `SurfaceTransitionOptions` | Type | `webar-facil/core`, `webar-facil` | | `SurfaceTransitionResult` | Type | `webar-facil/core`, `webar-facil` | | `TargetFoundEvent` | Type | `webar-facil/core`, `webar-facil` | | `TargetLostEvent` | Type | `webar-facil/core`, `webar-facil` | | `TargetVisibilityEvent` | Type | `webar-facil/core`, `webar-facil` | | `ThreeARGroup` | Value and/or type | `webar-facil` | | `ThreeARModel` | Value and/or type | `webar-facil` | | `ThreeARScene` | Value and/or type | `webar-facil` | | `ThreeARVideo` | Value and/or type | `webar-facil` | | `ThreeCameraBackground` | Value and/or type | `webar-facil` | | `Tracker` | Value and/or type | `webar-facil/core`, `webar-facil` | | `TrackerOptions` | Type | `webar-facil/core`, `webar-facil` | | `TrackingIssue` | Type | `webar-facil/core`, `webar-facil` | | `TrackingQualityEvent` | Type | `webar-facil/core`, `webar-facil` | | `TrackingResult` | Type | `webar-facil/core`, `webar-facil` | | `validateIntrinsics` | Value and/or type | `webar-facil/core`, `webar-facil` | | `VisualReferenceResult` | Type | `webar-facil/core`, `webar-facil` | | `WebAREngine` | Value and/or type | `webar-facil/core`, `webar-facil` | See [WebAREngine](api-engine.md) and [the advanced API](advanced.md) for integration guidance. ## Pose.d.ts ```ts export interface ARPose { targetId?: string; backend: 'image' | 'webxr' | 'optical-flow'; timestampMs: number; scale: 'metric' | 'relative'; scaleSource?: 'experimental-inertial'; matrix: Float32Array; worldMatrix: Float32Array; viewMatrix: Float32Array; projectionMatrix: Float32Array | null; projectionSource?: 'calibrated' | 'estimated' | 'webxr'; surfaceDetected?: boolean; diagnostics?: { processingMs: number; inliers: number; reprojectionErrorPx: number; relocalized?: boolean; keyframeCorrected?: boolean; }; } export interface CameraIntrinsics { width: number; height: number; fx: number; fy: number; cx: number; cy: number; distortion?: readonly number[]; } ``` ## WebAREngine.d.ts ```ts import { type CameraManagerOptions, type CameraMetrics } from './camera/CameraManager.js'; import { type TrackerOptions } from './tracking/Tracker.js'; import { type MarkerQuality } from './tracking/ImageCompiler.js'; import { type OpticalFlowOptions } from './slam/OpticalFlowSLAM.js'; import type { ExperimentalMetricScale } from './slam/ExperimentalMetricScale.js'; import type { ARPose, CameraIntrinsics } from './Pose.js'; import type { OdometryMapSnapshot, VisualReferenceResult } from './slam/OdometryBackend.js'; import type { AREventMap } from './tracking/events.js'; export declare enum EngineState { Idle = "Idle", Camera = "Camera", TrackingImage = "TrackingImage", TrackingSurface = "TrackingSurface", Disposed = "Disposed" } export interface EngineOptions { experimentalMetricScale?: ExperimentalMetricScale; keyframeCorrection?: boolean; imageLossGraceMs?: number; keepContentOnLost?: boolean; imageSmoothingMs?: number; camera?: Omit; tracker?: TrackerOptions; opencvURL?: string | URL; calibration?: CameraIntrinsics; fallback?: OpticalFlowOptions | false; attachXRSession?: (session: XRSession) => Promise; getXRContext?: () => WebGL2RenderingContext; overlayRoot?: HTMLElement; } export interface ImageTargetOptions { physicalWidthMeters?: number; maxDimension?: number; } export interface ImageTargetMetadata { format: 'WART'; version: 2; width: number; height: number; physicalWidthMeters: number; extractorId: number; quality: MarkerQuality; } export interface ImageTargetResult { buffer: ArrayBuffer; metadata: ImageTargetMetadata; } export interface SurfaceTransitionOptions { targetId?: string; backend?: 'auto' | 'webxr' | 'optical-flow'; placement?: 'auto' | 'preserve' | 'hit-test'; initializationTimeoutMs?: number; } export interface SurfaceTransitionResult { backend: 'webxr' | 'optical-flow'; placement: 'preserved' | 'scan-marker' | 'select-surface' | 'scan-floor'; } export interface SLAMMapSnapshot { version: 1; map: OdometryMapSnapshot; anchor: Float32Array | null; targetId: string; } export declare class WebAREngine { constructor(options?: EngineOptions); get state(): EngineState; get videoElement(): HTMLVideoElement; get cameraSettings(): MediaTrackSettings | null; get calibration(): CameraIntrinsics | undefined; setFocus(mode: 'continuous' | 'manual', distance?: number): Promise; onPose(fn: (matrix: Float32Array, pose: ARPose) => void): () => void; onStatus(fn: (message: string) => void): () => void; onError(fn: (error: Error) => void): () => void; onSurface(fn: (matrix: Float32Array) => void): () => void; onMetrics(fn: (metrics: CameraMetrics) => void): () => void; get targetIds(): readonly string[]; on(name: K, listener: (event: AREventMap[K]) => void): () => void; onTargetFound(listener: (event: AREventMap['targetFound']) => void): () => void; onTargetLost(listener: (event: AREventMap['targetLost']) => void): () => void; onTrackingQuality(listener: (event: AREventMap['trackingQuality']) => void): () => void; onTargetVisibility(listener: (event: AREventMap['targetVisibility']) => void): () => void; setKeepContentOnLost(value: boolean, targetId?: string): void; checkSLAMVisualReference(action: 'capture' | 'verify', id: string): Promise; exportSLAMMap({ includeCurrentView }?: { includeCurrentView?: boolean; }): Promise; importSLAMMap(snapshot: SLAMMapSnapshot): Promise; startCamera(): Promise; loadTarget(url: string | URL): Promise; addTarget(id: string, url: string | URL): Promise; loadImageTarget(source: Blob | string | URL, options?: ImageTargetOptions): Promise; addImageTarget(id: string, source: Blob | string | URL, options?: ImageTargetOptions): Promise; removeTarget(id: string): Promise; enableSLAM(options?: SurfaceTransitionOptions): Promise; stop(): Promise; dispose(): void; } ``` ## build-info.d.ts ```ts export declare const LIBRARY_VERSION: string; export declare const LIBRARY_BUILD = "1c906eb944d9f8f4"; ``` ## calibration/CalibrationSession.d.ts ```ts import type { CalibrationCapture, CalibrationReport } from './types.js'; export declare class CalibrationSession { constructor(options?: { workerFactory?: () => Worker; }); initialize(opencvURL: string, signal?: AbortSignal): Promise; addFrame(frame: ImageData, signal?: AbortSignal): Promise; board(): Promise<{ data: Uint8ClampedArray; width: number; height: number; }>; solve(): Promise; reset(): Promise; dispose(): void; } ``` ## calibration/types.d.ts ```ts import type { CameraIntrinsics } from '../Pose.js'; export interface CalibrationObservation { width: number; height: number; objectPoints: Float32Array; imagePoints: Float32Array; } export interface CalibrationReport { calibration: CameraIntrinsics; rmsPx: number; perViewErrorsPx: number[]; views: number; coverage: number; warnings: string[]; } export interface CalibrationCapture { accepted: boolean; corners: Float32Array; views: number; reason: string; } ``` ## camera/CameraManager.d.ts ```ts import type { ImageTrackingProfile } from '../tracking/profile.js'; import type { TrackingQualityEvent } from '../tracking/events.js'; export interface CameraFrame { imageData: ImageData; timestampMs: number; mediaTimeSeconds: number; sessionId: number; sourceWidth: number; sourceHeight: number; } export interface CameraManagerOptions { onFrame: (frame: CameraFrame, signal: AbortSignal) => void | Promise; onError?: (error: Error) => void; onStatus?: (status: 'camera-requesting' | 'camera-granted' | 'camera-playing') => void; playbackContainer?: HTMLElement; performance?: 'auto' | 'balanced' | 'battery' | 'quality'; width?: number; height?: number; facingMode?: 'environment' | 'user'; deviceId?: string; fps?: number; maxFrameWidth?: number; maxFramePixels?: number; focusMode?: 'continuous' | 'manual' | 'none'; focusDistance?: number; adaptive?: boolean; minFrameWidth?: number; onMetrics?: (metrics: CameraMetrics) => void; onSuspend?: () => void; onResume?: () => void; } export interface CameraMetrics { tracking?: ImageTrackingProfile; odometry?: import('../slam/OdometryBackend.js').OdometryTimings; consumerMs?: number; processedFrames: number; skippedFrames: number; readbackMs: number; processingMs: number; averageProcessingMs: number; analysisWidth: number; targetAnalysisFPS: number; analysisHeight?: number; performance?: CameraManagerOptions['performance']; } export declare class CameraManager { constructor(options: CameraManagerOptions); get state(): 'idle' | 'starting' | 'running' | 'paused' | 'disposed'; get videoElement(): HTMLVideoElement; get settings(): MediaTrackSettings | null; setAdaptiveResolution(enabled: boolean): void; setTrackingFeedback(qualities: readonly TrackingQualityEvent[], frameWidth?: number): void; setAnalysisWidth(width: number): void; setFocus(mode: 'continuous' | 'manual', distance?: number): Promise; start(): Promise; stop({ retainDevice }?: { retainDevice?: boolean; }): void; dispose(): void; } ``` ## diagnostics/ARDiagnostics.d.ts ```ts import { TRACKING_STAGES, TRACKING_COUNTS } from '../tracking/profile.js'; import type { WebAREngine } from '../WebAREngine.js'; import type { CameraMetrics } from '../camera/CameraManager.js'; import { ODOMETRY_STAGES } from '../slam/OdometryBackend.js'; export type DeviceTestResult = 'pending' | 'pass' | 'fail' | 'not-supported'; export interface DiagnosticContext { platform?: 'ios' | 'android' | 'desktop' | 'other'; execution?: 'unspecified' | 'automated' | 'physical-user-reported'; deviceModel?: string; osVersion?: string; browserVersion?: string; scenario?: string; } export interface DiagnosticConfiguration { mode?: string; backend?: string; performance?: string; maxFramePixels?: number; analysisWidth?: number; requestedFPS?: number; lossGraceMs?: number; keepContentOnLost?: boolean; manualPlacement?: boolean; keyframeCorrection?: boolean; renderPixelRatio?: number; antialias?: boolean; } export interface DiagnosticOptions { context?: DiagnosticContext; configuration?: DiagnosticConfiguration; sampleLimit?: number; now?: () => number; } export interface DiagnosticStats { count: number; mean: number | null; min: number | null; max: number | null; p50: number | null; p95: number | null; quantileSamples: number; } export interface DiagnosticRendererMetrics { drawCalls: number; triangles: number; geometries: number; textures: number; drawingBufferWidth: number; drawingBufferHeight: number; pixelRatio: number; } export interface DiagnosticReport { schemaVersion: 1; kind: 'webar-diagnostics'; sdk: { version: string; build: string; }; createdAt: string; durationMs: number; recording: boolean; context: Required; configuration: DiagnosticConfiguration; environment: { userAgent: string; secureContext: boolean; cameraAPI: boolean; webgl2API: boolean; xrAPI: boolean; immersiveAR: boolean | null; }; camera: Record; state: string; targets: Record; poses: { count: number; backends: Record; scales: Record; projectionSources: Record; relocalized: number; keyframeCorrected: number; firstPoseMs: number | null; }; quality: { samples: Record; issues: Record; diagnosis: 'heuristic'; }; metrics: { processingMs: DiagnosticStats; readbackMs: DiagnosticStats; consumerMs: DiagnosticStats; renderLoopMs: DiagnosticStats; poseStepTranslation: DiagnosticStats; poseStepRotationDegrees: DiagnosticStats; reprojectionErrorPx: DiagnosticStats; latestCamera: CameraMetrics | null; }; odometry: { frames: number; stages: Record; }; tracking: { frames: number; stages: Record; paths: Record; }; renderer: { latest: DiagnosticRendererMetrics | null; peakDrawCalls: number; peakTriangles: number; }; timeline: { atMs: number; event: string; detail: string; }[]; observations: { atMs: number; testId: string; result: DeviceTestResult; notes: string; }[]; limits: { sampleLimit: number; timelineLimit: number; observationLimit: number; quantiles: 'latest bounded samples'; poseSteps: 'not physical jitter or drift'; renderLoop: 'callback interval, not completed GPU frames'; physicalValidation: 'operator-declared, not independently certified'; }; } type Source = Pick; export declare class ARDiagnostics { constructor(source: Source, options?: DiagnosticOptions); setContext(context: DiagnosticContext): void; mark(event: string, detail?: string): void; observe(testId: string, result: DeviceTestResult, notes?: string): void; recordError(error: Error): void; recordRenderer(input: DiagnosticRendererMetrics): void; recordRenderFrame(): void; probeCapabilities(): Promise; stop(): void; snapshot(): DiagnosticReport; download(filename?: string): void; } export {}; ``` ## diagnostics/device-protocol.d.ts ```ts export interface DeviceTestCase { id: string; title: string; instructions: string; seconds: number; modes: readonly ('image' | 'flow' | 'transition' | 'webxr')[]; } export declare const DEVICE_TEST_PROTOCOL: readonly DeviceTestCase[]; ``` ## slam/ExperimentalMetricScale.d.ts ```ts type WindowRejection = 'visualCoverage' | 'sensorCoverage' | 'sensorGap' | 'weakVisualMotion' | 'weakInertialMotion'; type Interruption = 'poseGap' | 'trackingLost' | 'relocalized' | 'keyframeCorrection' | 'mapReset'; type AxisValues = { x: number; y: number; z: number; }; interface MotionSignal { windows: number; deviceAccelerationRmsMps2: AxisValues; worldAccelerationRmsMps2: AxisValues; integratedRmsMeters: AxisValues; meanAbsoluteIntegralMeters: AxisValues; } interface GuidedPhaseDiagnostics { visualPoses: number; visualDisplacementUnits: number | null; visualDisplacementMeters: number | null; visualCoverageMs: number; stationaryPoses: number; maxExcursionUnits: number | null; durationMs: number; samples: number; usableSamples: number; accelerationPeakMps2: AxisValues | null; accelerationRmsMps2: AxisValues | null; accelerationStdDevMps2: AxisValues | null; integratedWindows: number; weakWindows: number; } export interface MetricScaleDiagnostics { permission: 'idle' | 'listening' | 'denied' | 'unavailable'; motionEvents: number; samples: number; usableSamples: number; medianIntervalMs: number | null; largestGapMs: number; visualFrames: number; medianVisualIntervalMs: number | null; pairedWindows: number; relativeResidual: number | null; metersPerUnit: number | null; windowsEvaluated: number; windowsAccepted: number; rejectedWindows: Record; interruptions: Record; fitState: 'waiting' | 'inconsistent' | 'accepted'; fitAttempts: number; timing: { comparableWindows: number; estimatedOffsetMs: number | null; zeroOffsetResidual: number | null; bestResidual: number | null; }; lastTimingFit: MetricScaleDiagnostics['timing'] | null; motionSignal: { all: MotionSignal | null; weak: MotionSignal | null; guided: MotionSignal | null; guidedWeak: MotionSignal | null; }; guidedPhases: Record; } export declare class ExperimentalMetricScale { start(): Promise; stop(): void; setGuidedMotionCapture(active: boolean): void; hasStableVisualTracking(now?: number): boolean; pauseGuidedMotionPhase(): void; setGuidedMotionPhase(id: string): void; diagnostics(): MetricScaleDiagnostics; guidedReturnError(outboundId: string, inboundId: string): number | null; resetMap(): void; interrupt(reason?: Interruption): void; observe(time: number, worldFromCamera: Float32Array, stationary?: boolean): number | null; } export {}; ``` ## slam/OdometryBackend.d.ts ```ts import type { CameraIntrinsics } from '../Pose.js'; import type { ImageObservation } from '../tracking/types.js'; export interface OdometryConfig { intrinsics: CameraIntrinsics; opencvURL?: string; seed?: ImageObservation; keyframeCorrection?: boolean; floorROI: { x: number; y: number; width: number; height: number; }; } export interface OdometryMapSnapshot { version: 1; intrinsics: CameraIntrinsics; scale: 'relative' | 'metric'; mapId: string; keyframes: { descriptors: Uint8Array; points: Float32Array; view: Float32Array; }[]; groundPlane?: { worldFromPlane: Float32Array; inliers: number; }; currentViewCaptured?: boolean; } export declare const ODOMETRY_STAGES: readonly [ "grayMs", "cornersMs", "flowMs", "flowNormalMs", "flowWideMs", "flowRetryMs", "initializationMs", "poseMs", "mapUpdateMs", "keyframesMs", "planeMs", "planeFitMs", "planeConsensusMs", "relocalizationMs", "workerMs" ]; export type OdometryTimings = Partial>; export declare const ODOMETRY_SCAN_HINTS: readonly [ "low-texture", "move-sideways", "map-initializing", "plane-unverified", "return-to-map", "map-matches-insufficient", "map-pose-unverified", "map-pose-confirming", "surface-ready" ]; export type OdometryScanHint = typeof ODOMETRY_SCAN_HINTS[number]; export type OdometryEstimate = ({ status: 'initializing' | 'lost'; mapId: string; } | { status: 'tracked'; mapId: string; worldFromCamera: Float32Array; projectionMatrix: Float32Array; scale: 'relative' | 'metric'; trackedPoints: number; environmentReady?: boolean; processingMs?: number; reprojectionErrorPx?: number; relocalized?: boolean; keyframeCorrected?: boolean; stationary?: boolean; groundPlane?: { worldFromPlane: Float32Array; inliers: number; }; }) & { timings?: OdometryTimings; scanHint?: OdometryScanHint; }; export interface OdometryBackend { processFrame(frame: { rgba: Uint8ClampedArray; width: number; height: number; timestampMs: number; }): OdometryEstimate | Promise; reset(): void | Promise; exportMap?(includeCurrentView?: boolean): OdometryMapSnapshot; importMap?(snapshot: OdometryMapSnapshot): void; checkVisualReference?(action: 'capture' | 'verify', id: string, timestampMs: number): VisualReferenceResult; } export type VisualReferenceResult = { status: 'captured'; points: number; } | { status: 'verified'; inliers: number; reprojectionErrorPx: number; medianImageShiftPx: number; directReturnUnits: number; trajectoryDeltaUnits: number; } | { status: 'unavailable' | 'insufficient-support' | 'missing-reference'; }; export interface OdometryModule { createOdometryBackend(config: OdometryConfig): Promise; } ``` ## slam/OpticalFlowSLAM.d.ts ```ts import type { CameraFrame } from '../camera/CameraManager.js'; import type { OdometryConfig, OdometryEstimate, OdometryMapSnapshot, VisualReferenceResult } from './OdometryBackend.js'; export interface OpticalFlowOptions extends OdometryConfig { moduleURL?: URL; workerFactory?: () => Worker; } export declare class OpticalFlowSLAM { constructor(options: OpticalFlowOptions); start(signal?: AbortSignal): Promise; processFrame(frame: CameraFrame, signal?: AbortSignal): Promise; checkVisualReference(action: 'capture' | 'verify', referenceId: string, signal?: AbortSignal): Promise; exportMap(signal?: AbortSignal, includeCurrentView?: boolean): Promise; importMap(snapshot: OdometryMapSnapshot, signal?: AbortSignal): Promise; stop(): void; dispose(): void; } ``` ## slam/SLAMManager.d.ts ```ts import type { ARPose } from '../Pose.js'; import type { CameraFrame } from '../camera/CameraManager.js'; import type { CameraIntrinsics } from '../Pose.js'; export interface SLAMOptions { attachSession: (session: XRSession) => Promise; overlayRoot?: HTMLElement; onPose: (pose: ARPose) => void; onSurface?: (matrix: Float32Array) => void; onLost?: () => void; onEnd?: () => void; onError?: (error: Error) => void; getGLContext?: () => WebGL2RenderingContext; requireCameraAccess?: boolean; onCameraFrame?: (frame: CameraFrame, worldFromCamera: Float32Array, intrinsics: CameraIntrinsics, referenceEpoch: number) => Promise; onAlignmentUnavailable?: (error: Error) => void; } export declare class SLAMManager { get imageAlignmentAvailable(): boolean; get hasPlacement(): boolean; get referenceEpoch(): number; constructor(options: SLAMOptions); static isSupported(): Promise; start(signal?: AbortSignal): Promise; switchToSLAM(currentMatrix: Float32Array, alignedWorldFromCamera?: Float32Array): void; stop(): Promise; } ``` ## tools/MarkerCompiler.d.ts ```ts import type { MarkerQuality } from '../tracking/ImageCompiler.js'; export declare class MarkerCompiler { constructor(options?: { workerFactory?: () => Worker; }); compile(file: Blob, options: { physicalWidthMeters: number; opencvURL: string; maxDimension?: number; }, signal?: AbortSignal): Promise<{ buffer: ArrayBuffer; quality: MarkerQuality; }>; dispose(): void; } ``` ## tracking/ImageCompiler.d.ts ```ts export declare const TARGET_HEADER_BYTES = 48; export declare const TARGET_MAX_BYTES: number; export declare const TARGET_MAX_FEATURES = 100000; export declare const ORB_DESCRIPTOR_BYTES = 32; export interface MarkerFeatures { width: number; height: number; keypoints: Float32Array; descriptors: Uint8Array; } export interface CompiledMarker extends MarkerFeatures { simulated: boolean; physicalWidthMeters: number; extractorId: number; } export interface DecodedImage { data: Uint8ClampedArray; width: number; height: number; } export interface CompilerOptions { affineViews?: boolean; physicalWidthMeters: number; opencvURL?: string; extractorId?: number; maxDimension?: number; simulate?: boolean; extract?: (encoded: Uint8Array) => Promise; decode?: (encoded: Uint8Array) => Promise; } export interface MarkerQuality { featureCount: number; coverage: number; duplicateRatio: number; usable: boolean; warnings: string[]; } export declare function crc32(bytes: Uint8Array): number; export declare function analyzeFeatures(features: MarkerFeatures): MarkerQuality; export declare class ImageCompiler { compile(source: Uint8Array | Blob, options: CompilerOptions): Promise; compileRGBA(image: DecodedImage, options: CompilerOptions): Promise; } export declare function decodeImageBrowser(encoded: Uint8Array, max: number): Promise; export declare function encodeMarker(features: MarkerFeatures, options: CompilerOptions, simulated?: boolean): ArrayBuffer; export declare function decodeMarker(buffer: ArrayBuffer): CompiledMarker; ``` ## tracking/Tracker.d.ts ```ts import { type ImageTrackingProfile } from './profile.js'; import type { CameraFrame } from '../camera/CameraManager.js'; import type { CameraIntrinsics } from '../Pose.js'; import type { TrackingResult, TrackingBatchResult } from './types.js'; export type { TrackingResult, ImageObservation } from './types.js'; export interface TrackerOptions { opencvURL?: string | URL; calibration?: CameraIntrinsics; allowEstimatedCalibration?: boolean; timeoutMs?: number; workerFactory?: () => Worker; } export declare class Tracker { get latestProfile(): ImageTrackingProfile | null; constructor(options?: TrackerOptions); get state(): 'idle' | 'busy' | 'disposed'; get hasTarget(): boolean; loadTarget(buffer: ArrayBuffer, signal?: AbortSignal, targetId?: string): Promise; get targetIds(): readonly string[]; loadTargets(targets: readonly { id: string; buffer: ArrayBuffer; }[], signal?: AbortSignal): Promise; processFrame(frame: CameraFrame, signal?: AbortSignal, calibrationOverride?: CameraIntrinsics): Promise; processTargetsFrame(frame: CameraFrame, signal?: AbortSignal): Promise; dispose(): void; } ``` ## tracking/events.d.ts ```ts import type { ARPose } from '../Pose.js'; import type { OdometryScanHint } from '../slam/OdometryBackend.js'; export type TrackingIssue = 'low-texture' | 'small-target' | 'blur' | 'high-motion' | 'insufficient-matches' | 'occlusion-or-out-of-view' | 'device-load'; export interface FrameQuality { gradient: number; laplacianVariance: number; motionPixels: number; targetSizePx: number; featureCount: number; inliers: number; reprojectionErrorPx: number; } export interface TrackingQualityEvent extends FrameQuality { targetId: string; timestampMs: number; state: 'tracking' | 'recovering' | 'lost'; score: number; processingMs: number; issues: TrackingIssue[]; diagnosis: 'heuristic'; } export interface TargetFoundEvent { targetId: string; timestampMs: number; pose: ARPose; } export interface TargetLostEvent { targetId: string; timestampMs: number; retained: boolean; reason: 'lost' | 'removed' | 'stopped'; } export interface TargetVisibilityEvent { targetId: string; visible: boolean; reason: 'tracked' | 'recovering' | 'lost' | 'removed' | 'stopped' | 'policy'; } export interface AREventMap { scanGuidance: { hint: OdometryScanHint; timestampMs: number; }; targetFound: TargetFoundEvent; targetLost: TargetLostEvent; trackingQuality: TrackingQualityEvent; targetVisibility: TargetVisibilityEvent; } ``` ## tracking/profile.d.ts ```ts export declare const TRACKING_STAGES: readonly [ "grayMs", "qualityMs", "supportMs", "opticalFlowMs", "flowValidationMs", "orbMs", "matchingMs", "homographyMs", "poseMs", "workerMs", "roundTripMs", "transportAndSchedulingMs" ]; export declare const TRACKING_COUNTS: readonly [ "flowAttempts", "flowAccepted", "redetections", "orbExtractions", "flowInputPoints", "cachedAttempts", "fullDetections", "matchingQueryDescriptors", "flowReliable", "redetectMissing", "redetectWeakSupport", "redetectDeadline", "redetectVerification", "globalDeferred", "flowSparseReliable", "weakDistribution", "weakReprojection", "weakRetention", "cacheConflicts", "exactMatchingCalls", "opencvMatchingCalls", "emptyMatchingCalls" ]; export interface ImageTrackingProfile { timings: Record; counts: Record; } export declare function createTrackingProfile(): ImageTrackingProfile; export declare function copyTrackingProfile(value: unknown): ImageTrackingProfile | null; export declare function measureTracking(profile: ImageTrackingProfile, key: typeof TRACKING_STAGES[number], run: () => T): T; ``` ## tracking/types.d.ts ```ts import type { CameraIntrinsics } from '../Pose.js'; import type { ImageTrackingProfile } from './profile.js'; import type { FrameQuality } from './events.js'; export interface ImageObservation { rgba: ArrayBuffer; width: number; height: number; timestampMs: number; sessionId: number; intrinsics: CameraIntrinsics; projectionSource?: 'calibrated' | 'estimated'; cameraFromMarker: Float32Array; imagePoints: Float32Array; objectPoints: Float32Array; markerCorners: Float32Array; } export type TrackingResult = { sessionId: number; timestampMs: number; processingMs: number; quality?: FrameQuality; targetId?: string; profile?: ImageTrackingProfile; } & ({ status: 'not-found'; matrix: null; } | { status: 'tracked'; matrix: Float32Array; projectionMatrix: Float32Array; projectionSource: 'calibrated' | 'estimated'; inliers: number; reprojectionErrorPx: number; reference: ImageObservation; }); export interface TrackingFrameRequest { type: 'FRAME' | 'FRAME_BATCH'; id: number; buffer: ArrayBuffer; width: number; height: number; sessionId: number; timestampMs: number; intrinsics: CameraIntrinsics; projectionSource: 'calibrated' | 'estimated'; } export interface TrackingBatchResult { results: TrackingResult[]; processingMs: number; profile?: ImageTrackingProfile; } ``` ## vision/calibration.d.ts ```ts import type { CameraIntrinsics } from '../Pose.js'; export declare function validateIntrinsics(k: CameraIntrinsics): void; export declare function scaleIntrinsics(k: CameraIntrinsics, width: number, height: number): CameraIntrinsics; export declare function estimateIntrinsics(width: number, height: number, verticalFovDegrees?: number): CameraIntrinsics; export declare function projectionFromIntrinsics(k: CameraIntrinsics, near?: number, far?: number): Float32Array; export declare function rotateIntrinsics(k: CameraIntrinsics, degrees: 0 | 90 | 180 | 270): CameraIntrinsics; export declare function normalizedPixel(x: number, y: number, k: CameraIntrinsics): [ number, number ]; ``` ## wrappers/SurfacePlacement.d.ts ```ts import { RingGeometry, MeshBasicMaterial, Mesh } from 'three'; import type { ARPose } from '../Pose.js'; export type PlacementState = 'idle' | 'scanning' | 'surface-found' | 'placed' | 'lost'; export declare class SurfacePlacement { readonly reticle: Mesh; constructor(manual: boolean, changed: (state: PlacementState) => void, now?: () => number, onStale?: () => void, showGrid?: boolean); get state(): PlacementState; get canConfirm(): boolean; begin(): void; status(message: string): void; observeSurface(matrix: Float32Array): void; observePose(pose: ARPose): void; confirm(): void; tick(): void; dispose(): void; } ``` ## wrappers/ThreeARGroup.d.ts ```ts import { Group } from 'three'; import type { ARPose } from '../Pose.js'; import type { WebAREngine } from '../WebAREngine.js'; export declare class ThreeARGroup extends Group { constructor(); bind(engine: WebAREngine, targetId?: string): this; unbind(): void; dispose(): void; update(matrix: Float32Array, pose?: ARPose): void; } ``` ## wrappers/ThreeARModel.d.ts ```ts import { Group } from 'three'; export interface ARModelOptions { dracoDecoderPath?: string | URL; maxBytes?: number; } export interface ARModelLoadOptions { fitSize?: number | false; autoplay?: boolean; signal?: AbortSignal; } export interface ARAnimationOptions { loop?: 'repeat' | 'once' | 'pingpong'; repetitions?: number; speed?: number; crossFadeSeconds?: number; clampWhenFinished?: boolean; } export declare class ThreeARModel extends Group { constructor(options?: ARModelOptions); get model(): Group | null; load(source: Blob | string | URL, options?: ARModelLoadOptions): Promise; update(deltaSeconds: number): void; setSize(size: number | false): void; playAnimation(nameOrIndex?: string | number, options?: ARAnimationOptions): void; configureAnimation(options: ARAnimationOptions): void; pauseAnimations(): void; resumeAnimations(): void; get animationState(): { name: string; time: number; paused: boolean; speed: number; running: boolean; } | null; onAnimationFinished(listener: (name: string) => void): () => void; stopAnimations(): void; unload(): void; dispose(): void; } ``` ## wrappers/ThreeARScene.d.ts ```ts import { Scene, PerspectiveCamera, WebGLRenderer, type Object3D } from 'three'; import { WebAREngine, type EngineOptions } from '../WebAREngine.js'; import { ThreeARGroup } from './ThreeARGroup.js'; export interface ARSceneOptions { container: HTMLElement; engineOptions?: EngineOptions; maxPixelRatio?: number; lighting?: boolean; fit?: 'contain' | 'cover'; antialias?: boolean; } export declare class ThreeARScene { readonly engine: WebAREngine; readonly scene: Scene; readonly camera: PerspectiveCamera; readonly renderer: WebGLRenderer; constructor(options: ARSceneOptions); getAnchor(targetId?: string): ThreeARGroup; addContent(targetId: string, content: Object3D): void; removeContent(content: Object3D): void; onRender(callback: (deltaSeconds: number) => void): () => void; onAfterRender(callback: () => void): () => void; dispose(): Promise; } ``` ## wrappers/ThreeARVideo.d.ts ```ts import { Group, Mesh, PlaneGeometry, ShaderMaterial, type ColorRepresentation } from 'three'; export interface ChromaKeyOptions { color?: ColorRepresentation; similarity?: number; smoothness?: number; spill?: number; } export interface ARVideoOptions { width?: number; orientation?: 'flat' | 'upright'; muted?: boolean; loop?: boolean; chromaKey?: ChromaKeyOptions | false; } export declare class ThreeARVideo extends Group { readonly videoElement: HTMLVideoElement; readonly mesh: Mesh; constructor(options?: ARVideoOptions); setChromaKey(options: ChromaKeyOptions | false): void; setWidth(width: number): void; setOrientation(orientation: 'flat' | 'upright'): void; setSource(source: Blob | string | URL): Promise; play(): Promise; pause(): void; dispose(): void; } ``` ## wrappers/ThreeCameraBackground.d.ts ```ts import { Mesh } from 'three'; import type { CameraIntrinsics } from '../Pose.js'; export declare class ThreeCameraBackground { readonly object3D: Mesh; constructor(video: HTMLVideoElement, calibration?: CameraIntrinsics); setCalibration(calibration?: CameraIntrinsics): void; dispose(): void; } ``` ## wrappers/aframe-component.d.ts ```ts import type { Object3D, Camera } from 'three'; import type { WebAREngine } from '../WebAREngine.js'; import { ThreeCameraBackground } from './ThreeCameraBackground.js'; interface ComponentInstance { data: { target: string; physicalWidth: number; targetId: string; }; el: HTMLElement & { object3D: Object3D; sceneEl?: { camera?: Camera; object3D?: Object3D; }; }; unsubscribe?: () => void; originalAutoUpdate?: boolean; originalVisible?: boolean; removed?: boolean; camera?: Camera; cameraAutoUpdate?: boolean; background?: ThreeCameraBackground; releaseBackground?: () => void; releaseCamera?: () => void; } interface AFrameRegistry { registerComponent(name: string, definition: { schema: { target: { type: 'string'; default: string; }; physicalWidth: { type: 'number'; default: number; }; targetId: { type: 'string'; default: string; }; }; init(this: ComponentInstance): void; update(this: ComponentInstance, previous: { target?: string; physicalWidth?: number; targetId?: string; }): void; remove(this: ComponentInstance): void; }): unknown; } export declare function registerAFrameComponent(AFRAME: AFrameRegistry, engine: WebAREngine): void; export {}; ``` ## wrappers/aframe-model.d.ts ```ts import type { Object3D } from 'three'; import { ThreeARModel, type ARModelOptions } from './ThreeARModel.js'; interface Instance { data: { src: string; fitSize: number; autoplay: boolean; }; el: HTMLElement & { object3D: Object3D; }; model?: ThreeARModel; generation?: number; } interface Registry { registerComponent(name: string, definition: { schema: { src: { type: 'string'; default: string; }; fitSize: { type: 'number'; default: number; }; autoplay: { type: 'boolean'; default: boolean; }; }; init(this: Instance): void; update(this: Instance, previous: Partial): void; tick(this: Instance, time: number, delta: number): void; remove(this: Instance): void; }): unknown; } export declare function registerAFrameModelComponent(AFRAME: Registry, options?: ARModelOptions): void; export {}; ``` ## wrappers/aframe-video.d.ts ```ts import type { Object3D } from 'three'; import { ThreeARVideo } from './ThreeARVideo.js'; interface Instance { data: { src: string; width: number; chromaKey: boolean; keyColor: string; similarity: number; smoothness: number; spill: number; muted: boolean; loop: boolean; }; el: HTMLElement & { object3D: Object3D; }; video?: ThreeARVideo; generation?: number; } interface Registry { registerComponent(name: string, definition: { schema: Record; init(this: Instance): void; update(this: Instance, previous: Partial): void; remove(this: Instance): void; }): unknown; } export declare function registerAFrameVideoComponent(AFRAME: Registry): void; export {}; ``` ## wrappers/web-components.d.ts ```ts import { ThreeARScene } from './ThreeARScene.js'; import { ThreeARModel, type ARAnimationOptions } from './ThreeARModel.js'; import { ThreeARVideo } from './ThreeARVideo.js'; import { type PlacementState } from './SurfacePlacement.js'; import { ARDiagnostics, type DiagnosticReport } from '../diagnostics/ARDiagnostics.js'; import type { WebAREngine, SurfaceTransitionOptions, SurfaceTransitionResult } from '../WebAREngine.js'; export interface ARComponentsOptions { assetBase?: string | URL; } export interface ARSceneElement extends HTMLElement { readonly app: ThreeARScene | null; readonly engine: WebAREngine | null; readonly updateComplete: Promise; readonly diagnostics: ARDiagnostics | null; readonly placementState: PlacementState; confirmPlacement(): void; getDiagnosticsReport(): DiagnosticReport | null; downloadDiagnostics(): void; start(): Promise; stop(): Promise; refresh(): Promise; enableSLAM(options?: SurfaceTransitionOptions): Promise; } export interface ARModelElement extends HTMLElement { readonly model: ThreeARModel | null; readonly updateComplete: Promise; reload(): Promise; playAnimation(nameOrIndex: string | number, options?: ARAnimationOptions): void; pauseAnimations(): void; resumeAnimations(): void; stopAnimations(): void; } export interface ARVideoElement extends HTMLElement { readonly video: ThreeARVideo | null; readonly updateComplete: Promise; reload(): Promise; play(): Promise; pause(): void; } export declare function registerARComponents(options?: ARComponentsOptions): void; ``` --- Source: docs/en/advanced.md # Advanced API and extensions Choose the level of integration that matches the application. | Need | Entry point | | --- | --- | | Camera stream and frames only | `CameraManager` | | Image processing and target matching | `Tracker` | | Precompile an image target | `ImageCompiler` | | Native immersive AR and hit testing | `SLAMManager` | | Optical odometry and local relocalization | `OpticalFlowSLAM` | | Coordinate camera, targets and backend switching | `WebAREngine` | When driving `Tracker` directly, respect frame ownership for transferred pixel buffers and the worker's backpressure. Stop frame production and dispose of workers when the application ends. Preserve camera intrinsics when adjusting the frame dimensions. For custom pose consumption, start with [coordinate conventions](concepts.md), then read the signatures in the [generated API](api-generated.md). --- Source: docs/en/architecture.md # Implemented architecture | Layer | Owns | | --- | --- | | Camera | Permission, video track, frame scheduling and capture settings. | | Workers and WASM | Feature extraction, matching and optical processing away from the UI thread. | | Image tracker | Marker compilation, frame-to-frame tracking, geometric checks and redetection. | | Spatial tracking | Native XR hit testing or optical motion and surface estimates. | | Engine | Lifecycle, target events, pose delivery and backend coordination. | | Wrappers | Three.js and A-Frame scene updates, models, video and declarative elements. | Workers report transforms and diagnostics to the engine. Rendering may continue between tracking updates, which allows animations to remain smooth while pose processing runs at a different cadence. The public API can be integrated without the declarative layer. See [advanced API](advanced.md) and [implementation checks](verification.md). --- Source: docs/en/contributing.md # Development, tests and contributions Install the Node.js version specified by the repository and restore the locked dependencies with `npm ci`. Run the checks relevant to the change from the repository root. ```sh npm run check npm test npm run build npm run docs:check npm run test:browser ``` For tracking changes, report the device, browser, backend, performance preset and test scene. Compare acquisition, steady tracking and recovery independently. For public API changes, update the examples, both manuals, and the declaration-derived reference in the same change. See [implementation verification](verification.md) and [physical-device acceptance](device-validation.md). --- Source: docs/en/verification.md # Implementation verification Use separate checks for the behavior and environment each layer needs. | Check | What it covers | | --- | --- | | `npm run check` and `npm test` | Type contracts and unit regressions. | | `npm run build` and `npm run check:package` | Compiled entry points, declarations and distribution artifacts. | | `npm run docs:check` and `npm run docs:test-browser` | Documentation references, rendered navigation and browser smoke checks. | | `npm run test:browser` | Browser integration and repeatable automation scenarios. | | Physical-device run | Real camera permissions, focus, video decoding, gestures, heat and spatial behavior. | Browser automation and prerecorded inputs make regressions repeatable. Collect live results separately for native XR and optical tracking. See [the device checklist](device-validation.md). --- Source: docs/en/device-validation.md # Acceptance on physical devices Run the same scenarios on desktop, iPhone and Android where supported. Keep camera/browser versions, tested backend and assets with each result. For weaker devices, repeat a run with `performance="battery"` and compare both the render experience and detection reliability. - **Camera.** Grant and deny permission, stop, restart, and return from a background tab. - **Images.** Acquire both a large and a small marker, tilt it, obscure it and bring it back. - **Interaction.** Select a model, drag and pinch while keeping the page controls usable. - **Content.** Play an animated GLB and a video, then swap content during the session. - **Spatial.** Place on a textured surface, walk around it, lose tracking and try to recover. - **Maps.** Save a local optical map, restart and revisit the same viewpoint. - **Capture.** Try saving a photo and a recording when the device and browser offer capture. Record unavailable native features separately from failed tests. The optical backend and WebXR may produce different scale and recovery results. See [diagnostics](diagnostics.md) for collecting reproducible evidence. --- Source: docs/en/ai-guide.md # Integration contract for AI assistants Start with [the first HTML application](getting-started.md) and the [declarative reference](declarative.md). Use the [generated TypeScript reference](api-generated.md) and [machine-readable API](../api.json) for exact names and signatures. 1. Prefer the hosted standalone script and custom elements for plain HTML. 2. Use image URLs directly. Precompiled targets are optional for normal JPG/PNG setup. 3. Keep target IDs identical across registration, pose events and rendering. 4. Ask the user for the real image width if physical size matters. 5. Use a user action to start the camera. Serve on localhost for desktop development and trusted HTTPS on phones. 6. Name the spatial backend and explain its scale when showing placement. 7. For dynamic apps, preserve the current session when changing content and release it when the page unmounts. 8. Check API spelling and attribute defaults against the reference. Use tested examples for advanced integrations. See [recipes](recipes.md) for complete patterns and [troubleshooting](troubleshooting.md) for failure messages. --- Source: docs/en/glossary.md # Glossary | Term | Definition | | --- | --- | | Anchor | Stored transform used to keep virtual content at a placement. | | Chroma key | Technique that removes a selected background color from video. | | Descriptor | Compact representation of a local image feature for matching. | | Drift | Accumulated spatial pose error over time. | | GLB | Binary glTF container for 3D assets. | | Hit test | Query for a surface intersection from a ray in an XR session. | | Keyframe | Selected frame whose features and pose help describe a local map. | | Optical flow | Estimated movement of image points between frames. | | Parallax | Relative image motion caused by changing viewpoint. | | Pose | Position and orientation in a reference coordinate system. | | Redetection | Searching for a known image again when tracking needs recovery. | | Relocalization | Recovering a camera pose relative to a previously observed map. | | SLAM | Simultaneous localization and mapping. | | WASM | WebAssembly, used for computational workloads in the vision workers. | | WebXR | Browser API for immersive XR sessions on supported devices. | ## Autoría / Credits **Creador / Creator:** IngCarlosReina. - Realidad Aumentada Empezando Desde Cero - [Web](https://realidad-aumentada.com.co) - [Blog](https://blog.realidad-aumentada.com.co) - [YouTube](https://www.youtube.com/@carlosreinamcr)