Documentación para desarrolladores

Una etiqueta incrusta el visor. Todo lo demás es opcional.

html
<script src="https://3d-lion.com/viewer/loader.js" defer></script>

<glb-viewer
  share-token="prj_your_token"
  style="display: block; width: 100%; height: min(78svh, 760px)">
</glb-viewer>

Inicio rápido

Un script y una etiqueta. No hay una segunda forma de hacerlo, así que aquí no se puede elegir mal.

La etiqueta <glb-viewer>

Un elemento corriente en tu página. Abre el modelo en un marco propio y te entrega la API en el propio elemento: tu página no carga con nada de 3D y aun así lo gobierna entero.

  • El motor corre en su propio marco: en el sitio real de un cliente, el mismo modelo ocupó el hilo principal de la página 444 ms dentro de ella y 2 ms en un marco
  • await viewer.ready te entrega toda la API — cada llamada devuelve una promesa
  • Casi todos los atributos son en vivo: cambias uno y el visor lo aplica sin recargar
html
<script src="https://3d-lion.com/viewer/loader.js" defer></script>

<glb-viewer
  share-token="prj_your_token"
  style="display: block; width: 100%; height: min(78svh, 760px)">
</glb-viewer>

Una línea en lugar de dos

El script escribe la misma etiqueta donde está su propia línea: entre párrafos, en una columna — allí donde la pongas. Todo lo demás lo dice el propio proyecto, así que la línea no lleva más que su token. Ser breve cuesta una cosa: un visor escrito así aparece después de analizar la página, y el modelo empieza a cargarse decenas de milisegundos más tarde que uno escrito a mano.

html
<script
  src="https://3d-lion.com/viewer/loader.js"
  data-token="prj_your_token" defer></script>

El tamaño y el título son data-height y data-title; la excepción para una sola página es data-tour. Una etiqueta en <head> no tiene sitio en la página: coloca un <div data-glb-viewer></div> vacío donde deba estar el modelo.

Listo. Las zonas de color, el tour de cámara y los controles ya funcionan — no escribes nada de JavaScript.

El token del proyecto (prj_…) está en la página del proyecto, dentro de tu panel. No hace falta nada más: tu dominio se verifica en el servidor.

Casos habituales

Lo que más se pide se activa con un solo atributo. Copia la línea dentro de la etiqueta.

Tu propia interfaz

Oculta el panel del configurador integrado para que dibujes tú las muestras y apliques los colores por la API.

ui="none"

Colores de inicio

El modelo aparece ya configurado, sin un destello de los colores originales.

colors="body:#111111,seats:#d8c9a6"

El tour se reproduce solo

La cámara recorre la ruta que grabaste en el editor en cuanto llega el modelo, como una película en un escaparate.

tour="auto"

El desplazamiento conduce el tour

El lector lleva la cámara por la ruta a su ritmo: adelante, atrás o detenida a mitad. El modelo se queda en el centro de la ventana mientras tanto y la página sigue desplazándose como siempre: continúa un poco más y sigue su camino. No hace falta reservarle una sección: el visor toma su propio espacio. En el móvil se ofrece el botón.

tour="scroll"

Caminar por dentro

Modo en primera persona — para interiores, habitáculos y estancias.

camera-mode="walk"

Giro lento

El modelo gira suavemente hasta que el visitante lo toca.

auto-rotate

La cámara no se mueve

Por defecto, elegir una zona vuela a su vista guardada. Así decides tú.

zone-focus="none"

Idioma de la interfaz

Los textos del visor en el idioma de tu web: en, ru, es.

lang="ru"

El modelo se queda en su sitio por sí solo

Cuando el lector llega a él, la página sujeta el modelo en el centro de la ventana durante un gesto de desplazamiento y luego lo suelta. No hay que pedirlo, y si tu maquetación prefiere que se desplace, dilo.

stage="none"

La pausa sobre el modelo mientras la página se desplaza

Solo la propia página puede mantener el modelo quieto: el motor está en un marco, y un marco no tiene permiso para tocar la página que lo rodea. Para eso está la etiqueta — se coloca en tu maquetación y sujeta el modelo por ti, así que en el marcado esto es un atributo y nada más. Cómo se ve el tour se elige normalmente en el panel; el atributo manda sobre ese ajuste en una página concreta.

html
<glb-viewer
  share-token="prj_your_token"
  tour="scroll">
</glb-viewer>

Lo que se sujeta es la caja que ocupa un sitio en la página. Un visor con position: absolute no ocupa ninguno propio, así que sujetamos la caja más cercana que sí lo hace: tu tarjeta, con su marco. Dale la altura a esa caja.

Una sección de la que el modelo no se va

Un bloque normal se desplaza con todo lo demás, así que el lector encuentra el modelo unas veces entero y otras a medias. Eso ya no ocurre: cuando el lector llega a él, la página sujeta el modelo en el centro de la ventana durante un gesto de desplazamiento y luego lo suelta, y no hay que pedir nada. Dale una sección más alta que la pantalla y se quedará durante toda esa sección, y se irá con ella. La altura sigue siendo tuya: el modelo se sujeta exactamente con la que le diste en tu maquetación, centrado en lo que la ventana deja libre. Si lo quieres a pantalla completa, di stage="screen".

html
<section style="height: 240vh">
  <glb-viewer share-token="prj_your_token"></glb-viewer>
</section>

La altura es tuya, incluida «la ventana menos la cabecera»

Tu maquetación decide cuánto mide el modelo, con media queries normales: una franja en pantalla ancha y toda la ventana en el móvil. Falta un número que el CSS no puede saber: cuánta ventana ocupa tu cabecera fija. Ya lo hemos medido, porque colocamos el modelo debajo de ella, y te lo devolvemos como --glb-stage-free: en el propio modelo y en la raíz de la página, para que llegue a la caja que decide la altura aunque esa caja sea un contenedor alrededor del modelo. Escríbelo con valor de reserva: en una página que ha rechazado la sujeción tampoco hay cabecera bajo la que ponerse.

css
.model-stage {
  height: var(--glb-stage-free, 100svh);
}

@media (min-width: 1024px) {
  .model-stage {
    height: min(var(--glb-stage-free, 100svh), 760px);
  }
}

.model-stage {
  --glb-stage-top: 72px;
}

La cabecera la buscamos nosotros y alguna vez podemos equivocarnos, por ejemplo si solo aparece al subir. Entonces di el número tú: --glb-stage-top en la misma caja cancela la búsqueda y se toma al pie de la letra.

Los marcados como «en vivo» en la tabla de arriba se pueden cambiar en cualquier momento con setAttribute — el visor aplica el cambio sin recargar el modelo.

Atributos

Defínelos en el HTML o cámbialos en tiempo real — todo lo marcado como «en vivo» se aplica al instante.

AtributoEn vivoQué hace
share-tokenObligatorioToken del proyecto — el modelo y sus ajustes vienen del proyecto
scenarioOpcionalQué porción del catálogo muestra esta incrustación, definida en el panel
pageOpcionalQué página de tu sitio es esta, para poder leer los informes página a página. Se rellena sola desde la dirección — decláralo solo si quieres otra etiqueta
uiOpcionalauto muestra el configurador integrado, none lo oculta para que uses el tuyo
langOpcionalIdioma de la interfaz: en, ru, es
layoutOpcionalLa forma en que se dibuja el panel cuando hay sitio para elegir
lead-captureOpcionalSi una configuración terminada se te puede enviar como solicitud
zone-focusOpcionalauto vuela a la vista guardada de la zona al seleccionarla, none te deja la cámara a ti
colorsOpcional✓Combinación de colores inicial, p. ej. body:#111111,seats:#d8c9a6
camera-modeOpcional✓orbit, free o walk
auto-rotateOpcional✓Girar el modelo lentamente mientras nadie interactúa
panel-sideOpcional✓De qué lado se coloca el panel del comprador: right, left o below
tourOpcional✓Cómo se ve el tour: button (por defecto), auto o scroll — lo conduce el desplazamiento de la página
stageOpcional✓la sujeción al desplazar está activada por sí sola: none la desactiva, screen es simplemente la altura de la pantalla
titleOpcional✓El título que se muestra sobre el modelo

Los que no llevan marca deciden QUÉ es el visor, no cómo se comporta: el catálogo llega del servidor ya cortado por scenario, la forma del panel queda fijada antes de mostrar el modelo, y un idioma es otro documento. Cambia uno de esos y el visor se vuelve a abrir en una dirección nueva, en lugar de ignorarte en silencio.

API de JavaScript

El elemento expone exactamente un objeto, congelado, con ocho espacios de nombres. Todo lo demás dentro del visor sigue siendo privado, así que tu integración sobrevive a las actualizaciones.

colors

Colores y zonas

Las zonas con sus paletas, la selección de zona, el cambio de color de una zona o de toda la combinación, y el reinicio al original.

options

Opciones sin color

Lo que el proyecto ofrece más allá del color — acabados, cantidades, texto libre — con la selección actual y lo que las reglas hayan tenido que ajustar.

tour

Tour de cámara

Reproducir, pausar y detener el recorrido, la velocidad, el progreso y desactivar el aviso integrado de «continuar».

camera

Cámara

Posición y objetivo, modo (órbita, vuelo libre, caminar), límites, giro automático y vuelta a la vista inicial.

animations

Animaciones

Las animaciones incluidas en el modelo: listar, reproducir y detener — puertas, capó, mecanismos.

build

La configuración terminada

Guardar lo que configuró el visitante y recibir un enlace propio, o enviártelo como solicitud con sus datos de contacto.

panel

El panel del comprador

El color, la transparencia, las esquinas y el lado del panel, la forma en que se dibujan las opciones y sobre qué está apoyado el panel.

scene

La escena

Lo que rodea al modelo: el fondo, y si se le mira desde fuera o se está dentro.

javascript
const viewer = document.querySelector('glb-viewer');
const { colors, options, tour, camera, animations } = await viewer.ready;

// Every call is a promise: the model runs in its own frame and answers from there.
await colors.getZones();                 // [{ id, label, group, palette, defaultColor, hasView }]
await colors.selectZone('seats');        // flies to the view the author saved for the zone
await colors.selectZone(null);           // visitor left the zone — flies back out
await colors.setColor('seats', '#d8c9a6');
await colors.setColor('seats', '#d8c9a6', { focus: false });  // recolour, camera stays put
await colors.setColors({ seats: '#d8c9a6' });  // whole colourway, camera untouched
await colors.reset();

await options.getGroups();               // the non-colour choices the project carries
await options.select(groupId, optionId);

await tour.play(); await tour.togglePause(); await tour.stop();
await tour.setSpeed(2.4);
await tour.setResumePrompt(false);       // you draw your own resume control

await camera.home();                     // back to the opening view
await camera.setAutoRotate(true);
await camera.setMode('orbit');           // 'orbit' | 'free' | 'walk'
await camera.setLimits({ minDistance: 2, maxPolarAngle: 1.5 });

await animations.getEntries();           // [{ id, label }]
await animations.play(id); await animations.stop(id);

Con await viewer.ready no hay nada que comprobar ni con qué competir: devuelve la misma API cuando la pidas, antes del arranque del visor o mucho después.

Todos los espacios de nombres están siempre, y cada llamada devuelve una promesa, porque la respuesta llega desde el propio marco del visor. Pide a un proyecto sin zonas que recoloree una y la promesa se rechaza: nunca devuelve undefined en silencio.

Un panel a tono con tu sitio

El panel del comprador se pinta con tu color y con ningún otro. Estar a tono con cualquier sitio es para lo que sirve la transparencia — y esa transparencia tiene un suelo calculado, porque el panel debe sostener su propio texto a 4.5:1 sobre aquello en lo que se apoya.

javascript
const { panel } = await viewer.ready;

panel.setSkin({
  surface: '#faf7f2',        // your colour — null follows the side your page is on
  surfaceOpacity: 0.7,       // a request: the floor below may raise it
  blur: 30,                  // 0…40 px, dropped once nothing shows through anyway
  accent: '#c07a3e',         // the chosen option and the primary button
  radius: 14,                // 0…32 px
  side: 'right',             // 'right' | 'left' | 'below'
});

// What the panel is standing on. Buys density, never colour.
panel.setBackdrop('#faf7f2');
panel.setBackdrop('#101010', { live: true });  // video, slideshow — counts as nothing said
panel.setBackdrop(null);                       // take the answer back

panel.setOptionDisplay('tiles');  // 'flat' | 'tiles'
panel.getSkin();

La opacidad que fijas es una petición, no un resultado. Sin un fondo declarado, la legibilidad hay que garantizarla sobre cualquier fondo que exista — así que el panel se asienta más denso de lo pedido. Declara el fondo y la petición se cumple.

setBackdrop compra densidad y nunca color. El panel en tu página y el panel en la de otro son del mismo color; conocer el fondo solo cambia cuánto se ve a través. A pantalla completa responde la escena: tu página ya no está detrás del panel allí.

Declara surface de forma explícita. El visor está en un marco propio, y nada se hereda a través del borde de un marco: lee su propio documento, no la página de debajo, así que no puede adivinar si tu sitio es claro u oscuro.

El acento nunca se adapta. Un botón que cambió de color por una fotografía de la página es un error a ojos de cualquier marca.

Leer el estado

Cada espacio de nombres responde a getState() con su instantánea actual. Úsalo para valores que cambian demasiado a menudo como para difundirlos — el progreso del tour, la posición de la cámara — o para ponerte al día si te suscribiste tarde.

javascript
const { colors, options, tour, camera, animations } = await viewer.ready;

await colors.getState();      // { zones, colors, selectedZoneId }
await options.getGroups();    // [{ id, label, options: [{ id, chosen, unmet }] }]
await tour.getState();        // { isPlaying, isPaused, progress, hasWaypoints }
await camera.getState();      // { position: [x, y, z], target: [x, y, z] }
await animations.getState();  // { entries: [{ id, label, status }] }

Eventos

El visor te cuenta lo que ha pasado, así que nunca lo consultas en bucle. Todo llega como un evento DOM corriente en el elemento: addEventListener, y los datos están en e.detail.

EventoDatosCuándo se dispara
glb-viewer:ready{ zones, tour, animations }Primera instantánea: zonas, tour y animaciones del proyecto
glb-viewer:model{ modelId, state, progress }La entrega del modelo: state es loading, loaded o failed, y progress cuenta de 0 a 1
glb-viewer:zones{ zones }La lista de zonas ha cambiado, o llega por primera vez
glb-viewer:zone{ zoneId }Un visitante ha elegido una zona; null significa que la deseleccionó
glb-viewer:color{ zoneId, hex }Una zona ha cambiado de color
glb-viewer:colors{ colors }Ha cambiado toda la combinación — por ejemplo, se aplicó un preajuste
glb-viewer:options{ selection, adjusted }Ha cambiado una opción sin color. En adjusted está lo que las reglas de compatibilidad han tenido que mover con ella
glb-viewer:tour:status{ isPlaying, isPaused, hasWaypoints }El tour se ha iniciado, pausado o detenido
glb-viewer:build{ publicId, url }Se ha guardado una configuración y ya tiene un enlace propio
glb-viewer:build-request{ publicId, url }Un visitante te ha enviado su configuración como solicitud
glb-viewer:error{ code, message }Ramifica según code — es estable. message está escrito para personas y puede cambiar.
javascript
const viewer = document.querySelector('glb-viewer');

viewer.addEventListener('glb-viewer:zones', (e) => e.detail.zones);
viewer.addEventListener('glb-viewer:zone', (e) => e.detail.zoneId);   // null = deselected
viewer.addEventListener('glb-viewer:color', (e) => e.detail);          // { zoneId, hex }
viewer.addEventListener('glb-viewer:colors', (e) => e.detail.colors);
viewer.addEventListener('glb-viewer:options', (e) => e.detail.selection);
viewer.addEventListener('glb-viewer:tour:status', (e) => e.detail.isPlaying);
viewer.addEventListener('glb-viewer:build', (e) => e.detail.url);
viewer.addEventListener('glb-viewer:error', (e) => e.detail.code);

Dominios y acceso

Tu clave está ligada a tus dominios, y la página del visor a esa misma lista a nivel de navegador. Un fragmento copiado en el sitio de otro no funciona allí.

La clave solo funciona en tus dominios

El servidor toma el dominio de las cabeceras de la petición, no de lo que envió el fragmento: no se puede falsificar desde el código de la página. Mientras tu lista de dominios esté vacía no se bloquea nada y los intentos quedan registrados, así que activar la comprobación no rompe ninguna integración existente. El valor * retira la comprobación de forma deliberada.

Un iframe copiado lo detiene el navegador

La página /v/ se sirve con una política frame-ancestors construida con esa misma lista de dominios. Un sitio que no figure en ella recibe un marco vacío: lo decide el navegador del visitante, antes de que se ejecute una sola línea de nuestro código.

Un proyecto privado se cierra en el servidor

Si el proyecto no es público, /init responde 403 PRIVATE_PROJECT a todos salvo a su propietario y a los usuarios con sesión iniciada. La comprobación vive en la API, así que no se sortea ni con un enlace directo ni con tu propia copia del visor.

Rechazos
POST /api/v1/viewer/auth/verify
403  { "error": "Domain not allowed" }   // the origin is not on the allowlist

GET /api/v1/viewer/prj_your_token
403  { "code": "PRIVATE_PROJECT" }       // the project is not shared publicly
404  { "code": "NOT_PUBLISHED" }         // the project has never been published

La lista de dominios la mantenemos nosotros: dinos qué direcciones añadir y la actualizamos. Todavía no hay gestión autónoma en el panel.

Qué guarda el widget

El widget funciona en tu página, así que es a ti a quien pregunta el visitante. Aquí está todo lo que escribimos en su dispositivo y guardamos en el nuestro, con las mismas palabras que puedes llevar a tu política.

ClavePara quéCuánto vive
glb-viewer:visitorUn identificador aleatorio del navegador. Responde a una sola pregunta: si esa persona ha vuelto o entra por primera vez. No contiene nombre, correo ni teléfono.Hasta que se borre el almacenamiento. Safari lo caduca a los 7 días
glb-viewer:sessionUna sesión de uso. Une las acciones dentro de una misma visita, para que «10 vistas» no sean una persona abriendo la página diez veces.30 minutos de silencio

Respetamos la negativa a ser reconocido

Si el navegador envía Global Privacy Control o Do Not Track, el identificador del visitante no se escribe ni se lee, ni siquiera el que ya estuviera en el dispositivo. La visita se sigue contando: una pestaña en una página es una página, no una persona.

No guardamos direcciones IP

De la dirección solo se toma el país; la dirección en sí no se guarda en ningún sitio. Por eso el mapa responde desde dónde miran, y nunca quién mira.

Borrado a petición

POST /viewer/forget con el identificador del visitante borra tanto los eventos como las visitas consolidadas. La solicitud ya enviada permanece: ya es tuya, y retirarla en tu nombre no nos corresponde.

Cuánto tiempo lo guardamos

Los eventos en bruto se borran automáticamente según el plazo de tu plan. Las visitas consolidadas —totales anónimos, sin el contenido de las acciones— se quedan, para que los informes alcancen años anteriores.

Esto describe cómo funciona el widget; no es un documento legal. Acuerda la redacción de tu política con tu propio abogado, sobre todo si tienes visitantes en la UE, donde escribir un identificador exige consentimiento.

React, Vue y los demás

No hay paquetes por framework ni hacen falta: <glb-viewer> es un elemento normal, así que funciona en cualquier sitio sin envoltorios.

React
import { useEffect, useRef } from 'react';

export function Configurator({ token }) {
  const ref = useRef(null);

  useEffect(() => {
    let cancelled = false;
    ref.current.ready.then(async ({ colors }) => {
      if (!cancelled) await colors.setColors({ seats: '#d8c9a6' });
    });
    return () => { cancelled = true; };
  }, []);

  return (
    <glb-viewer
      ref={ref}
      share-token={token}
      style={{ display: 'block', width: '100%', height: 600 }}
    />
  );
}
Vue / Nuxt
<template>
  <glb-viewer
    ref="viewer"
    :share-token="token"
    style="display: block; width: 100%; height: 600px"
  />
</template>

<script setup>
import { onMounted, ref } from 'vue';

const viewer = ref(null);

onMounted(async () => {
  const { colors } = await viewer.value.ready;
  await colors.setColors({ seats: '#d8c9a6' });
});
</script>

En Vue, añade glb-viewer a isCustomElement (vue.compilerOptions.isCustomElement en Nuxt) para que el compilador deje de buscar un componente con ese nombre.

Errores

Cada error lleva un code estable — construye tu lógica sobre él. El texto message está escrito para personas y puede cambiar.

CódigoQué ha pasado
PROJECT_NOT_FOUNDNo hay proyecto para este token, o aún no está publicado
NOT_PUBLISHEDEl proyecto nunca se ha publicado, así que no se entrega a los visitantes
PRIVATE_PROJECTEl proyecto no es público: solo su propietario y los usuarios con sesión tienen acceso
SUBSCRIPTION_INACTIVELa suscripción está inactiva, así que el visor está desactivado
AUTH_INVALID_KEYLa clave de acceso es incorrecta o ha sido revocada
AUTH_FORBIDDENTu dominio no está permitido para esta clave
MODEL_OR_TOKEN_REQUIREDNo se indicó ni token de proyecto ni URL de modelo
MODEL_URL_REQUIREDEl proyecto no tiene ningún modelo asociado
MODEL_LOAD_FAILEDEl archivo del modelo no se cargó — red o archivo dañado
WEBGL_NOT_SUPPORTEDEl navegador o el dispositivo no soporta WebGL
WEBGL_CONTEXT_LOSTEl navegador retiró el contexto WebGL, normalmente por falta de memoria de vídeo
INIT_NETWORK_ERRORNo se pudieron obtener los datos del proyecto — red o servidor
javascript
viewer.addEventListener('glb-viewer:error', ({ detail }) => {
  if (detail.code === 'WEBGL_NOT_SUPPORTED') showStaticGallery();
  else if (detail.code === 'PROJECT_NOT_FOUND') hideSection();
  else reportToMonitoring(detail.code, detail.message);
});

TypeScript

El paquete incluye declaraciones escritas a mano para toda la superficie pública: el elemento, la API y los datos de cada evento están tipados, sin que nada interno se filtre a tu compilación.

typescript
/// <reference types="@glb-viewer/cdn" />

const viewer = document.querySelector('glb-viewer')!;  // GLBViewerElement
const { colors } = await viewer.ready;                 // GLBViewerApi
await colors.setColor('seats', '#d8c9a6');

¿Dudas con la integración?

Escríbenos — te ayudamos a incrustar el configurador y a ajustar la experiencia a tu producto.