Per sviluppatori

Embed API

Fai parlare il tuo viewer Luxa con la pagina del tuo shop. Ascolta i cambi di materiale e di variante, guida il viewer dai tuoi pulsanti e reagisci ai clic sugli hotspot. Tutto con il postMessage del browser: niente server, niente SDK, niente build.

Piano CustomProtocollo luxa:v1

Panoramica

Il viewer vive in un iframe dentro la tua pagina prodotto. Di base l'iframe e la pagina che lo contiene non possono parlarsi (il browser li tiene separati). L'Embed API apre un canale nei due sensi con lo standard window.postMessage:

  • Gli eventi escono: il viewer avvisa la tua pagina quando il cliente sceglie un materiale, apre un hotspot o quando il modello ha finito di caricare.
  • I comandi entrano: la tua pagina chiede al viewer di cambiare materiale o variante, spostare la camera o avviare un'animazione.

Entrambi passano per identificatori stabili, i publicName che imposti su widget e voci nell'editor di Luxa, mai per ID interni del database che potrebbero cambiare. Ogni viewer ha la sua lista delle pagine autorizzate a parlargli; i messaggi da qualsiasi altra origine vengono ignorati senza avvisi.

Avvio rapido

L'embed minimo che funziona. Mettilo in una qualsiasi pagina HTML, sostituisci YOUR_VIEWER_ID e vedrai luxa:material-changed nella console ogni volta che si clicca un campione dentro il viewer.

html
<iframe
  id="luxa"
  src="https://luxa3d.com/viewer/YOUR_VIEWER_ID"
  style="width:100%;aspect-ratio:4/5;border:0"
  allow="camera; xr-spatial-tracking; fullscreen"
></iframe>

<script>
  window.addEventListener('message', (e) => {
    if (e.origin !== 'https://luxa3d.com') return;
    if (e.data.type === 'luxa:material-changed') {
      console.log('User picked', e.data.item.publicName, e.data.item.metadata);
    }
  });
</script>

Oppure salta il codice. Con il piano Custom, Embed → Open snippet builder nell'editor scrive l'integrazione completa per te, già collegata ai tuoi widget: Shopify Liquid, Wix, Squarespace, WordPress o HTML semplice. Pulsanti di acquisto, campioni esterni, WhatsApp, aggiungi al carrello: scegli ciò che ti serve, copia, incolla.

Configurazione in 3 passi

  1. 1. Dai un publicName a ogni widget e voce che vuoi esporre

    Nel pannello di destra dell'editor apri il blocco "Embed API" sotto un widget o una voce (campione, variante, hotspot) e scrivi un nome stabile: lettere minuscole, numeri e trattini, come color-picker per il widget e leather-strap per uno dei suoi campioni. Ciò che non ha un publicName resta invisibile all'API.

  2. 2. Autorizza l'origine della tua pagina

    Apri la finestra Embed dalla barra in alto dell'editor. In Allowed parent origins aggiungi l'indirizzo di ogni sito che ospiterà il viewer, per esempio https://yourbrand.com. Finché non c'è almeno un'origine l'API è spenta: non escono eventi e non si accettano comandi.

  3. 3. Mettiti in ascolto nella tua pagina

    Aggiungi window.addEventListener('message', …), controlla event.origin === 'https://luxa3d.com' e gestisci i messaggi. Gli eventi e i due esempi completi qui sotto mostrano come.

Eventi

Dal viewer alla tua pagina. Ogni evento è un oggetto con un type nello spazio "luxa:<name>" e un contenuto suo. Controlla sempre event.origin prima di fidarti.

luxa:ready

Inviato una volta, quando il modello 3D è caricato e il viewer si può usare. Il momento giusto per attivare un pulsante di acquisto o togliere un segnaposto. Elenca ogni widget con un nome e, in initialSelection, il campione da cui parte ognuno.

json
{
  "type": "luxa:ready",
  "widgets": [
{
  "publicName": "color-picker",
  "type": "material_switcher",
  "metadata": null,
  "items": [
    { "publicName": "leather-strap", "label": "Leather", "metadata": { "price": 199, "sku": "LTH-01" } },
    { "publicName": "metal-strap",   "label": "Metal",   "metadata": { "price": 249, "sku": "MTL-01" } }
  ]
}
  ],
  "initialSelection": {
"color-picker": { "publicName": "leather-strap", "label": "Leather", "metadata": { "price": 199, "sku": "LTH-01" } }
  }
}

luxa:material-changed

Inviato quando il cliente sceglie un campione in un widget material_switcher. Solo per widget o campioni che hanno un publicName.

json
{
  "type": "luxa:material-changed",
  "widget": { "publicName": "color-picker", "metadata": null },
  "item":   { "publicName": "leather-strap", "label": "Leather", "metadata": { "price": 199, "sku": "LTH-01" } }
}

luxa:variant-changed

Inviato quando il cliente sceglie una variante del modello (una variazione di scena salvata). Stessa struttura di material-changed.

json
{
  "type": "luxa:variant-changed",
  "widget": { "publicName": "configurator", "metadata": null },
  "item":   { "publicName": "ultra-titanium", "label": "Ultra · Titanium", "metadata": { "price": 899 } }
}

luxa:hotspot-clicked

Inviato quando il cliente clicca un hotspot di annotazione. Contiene la posizione dell'hotspot nella scena 3D, se vuoi mettere un'interfaccia tua accanto.

json
{
  "type": "luxa:hotspot-clicked",
  "widget": { "publicName": "info-pins", "metadata": null },
  "item":   {
"publicName": "crown-detail",
"label": "Crown",
"metadata": { "section": "specs" },
"position": [0.12, 0.05, -0.08]
  }
}

Comandi

Dalla tua pagina al viewer, con iframe.contentWindow.postMessage(message, 'https://luxa3d.com'). Passa sempre l'origine di Luxa come secondo argomento, mai "*", così il messaggio non va da nessun'altra parte. I comandi trovano il loro bersaglio tramite il publicName.

luxa:set-material

Cambia il campione attivo di un widget material_switcher: il widget per il suo publicName, il campione per il proprio.

json
{
  "type": "luxa:set-material",
  "widget": "color-picker",
  "item":   "leather-strap"
}

luxa:set-variant

Cambia la variante attiva di un widget model_variant.

json
{
  "type": "luxa:set-variant",
  "widget": "configurator",
  "item":   "ultra-titanium"
}

luxa:set-camera

Passa a una delle viste di un widget camera_tour.

json
{
  "type": "luxa:set-camera",
  "widget": "tour",
  "item":   "front-three-quarter"
}

luxa:play-animation

Avvia una clip di animazione. 'modelId::clipName' sceglie un modello in una scena che ne ha più di uno; un 'clipName' da solo la avvia su quello principale.

json
{
  "type": "luxa:play-animation",
  "clipName": "open-lid"
}

luxa:dress

Dà a tutto il modello un materiale definito da te, invece di uno configurato nell'editor: colore, metallicità, rugosità e opacità, più eventuali texture tramite URL. Invia null per toglierlo.

json
{
  "type": "luxa:dress",
  "material": { "color": "#8a5a3c", "metalness": 0, "roughness": 0.6, "opacity": 1 }
}

luxa:pause / luxa:resume

Ferma il rendering del viewer e lo fa ripartire. L'ultimo fotogramma resta a schermo. Per una pagina che mostra il viewer insieme ad altre cose e lo vuole fermo quando nessuno lo sta guardando.

json
{ "type": "luxa:pause" }
{ "type": "luxa:resume" }

Sicurezza

Una lista per ogni viewer. Ogni viewer ha la sua lista delle pagine autorizzate a parlargli, gestita nella finestra Embed dell'editor. Gli eventi vanno solo a quelle origini, e i comandi sono accettati solo quando event.origin è nella lista. Lista vuota significa API spenta, senza eccezioni.

Solo la pagina che lo contiene. I comandi devono anche arrivare da window.parent. Altri iframe, worker e schede non collegate non possono guidare il tuo viewer, anche con l'origine giusta.

Controlla event.origin anche dalla tua parte. Il viewer parla sempre da https://luxa3d.com, o dal tuo dominio se ne hai impostato uno; scarta ogni altra origine nel tuo gestore.

Niente sui nostri server. postMessage avviene tutto nel browser: nessuna chiamata a Luxa, nessun limite di frequenza, nessun costo per evento. L'accesso dipende dal piano, nell'editor; una volta attivo, usarlo non costa nulla.

Esempio Shopify

Uno snippet per una pagina prodotto Shopify. Scrive sul pulsante di acquisto il prezzo del materiale scelto e manda "Aggiungi al carrello" alla variante Shopify corrispondente tramite il normale /cart/add.js.

liquid
{%- comment -%}
  Drop this inside your product.liquid (or a section snippet).
  Replace VIEWER_ID with the published Luxa viewer ID.
  Tested on Dawn 14.x; works on most Shopify 2.0 themes.
{%- endcomment -%}

<div class="luxa-product-3d">
  <iframe
    id="luxa-viewer"
    src="https://luxa3d.com/viewer/VIEWER_ID"
    style="width: 100%; aspect-ratio: 4/5; border: 0;"
    allow="camera; xr-spatial-tracking; fullscreen"
    loading="lazy"
  ></iframe>

  <button id="luxa-buy" class="button button--primary" disabled>
    Loading…
  </button>
</div>

<script>
(function () {
  const LUXA_ORIGIN = 'https://luxa3d.com';
  const buyBtn = document.getElementById('luxa-buy');
  const viewer = document.getElementById('luxa-viewer');

  // Variant ID lookup: map publicName from Luxa → Shopify variant ID.
  // The merchant maintains this on their side once; everything else is automatic.
  const VARIANT_ID_BY_PUBLIC_NAME = {
    'leather-strap': {{ product.variants[0].id }},
    'metal-strap':   {{ product.variants[1].id }},
    'fabric-strap':  {{ product.variants[2].id }},
  };

  let currentVariantId = null;

  window.addEventListener('message', function (event) {
    if (event.origin !== LUXA_ORIGIN) return;
    const msg = event.data;
    if (!msg || typeof msg.type !== 'string') return;

    if (msg.type === 'luxa:ready') {
      buyBtn.disabled = false;
      buyBtn.textContent = 'Add to cart';
    }

    if (msg.type === 'luxa:material-changed' && msg.item) {
      const meta = msg.item.metadata || {};
      const price = meta.price ? '€' + meta.price : '';
      buyBtn.textContent = 'Add to cart' + (price ? ' · ' + price : '');
      currentVariantId = VARIANT_ID_BY_PUBLIC_NAME[msg.item.publicName] || null;
    }
  });

  buyBtn.addEventListener('click', function () {
    if (!currentVariantId) return;
    fetch('/cart/add.js', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id: currentVariantId, quantity: 1 }),
    }).then(function () { window.location.href = '/cart'; });
  });
})();
</script>

Esempio HTML

Una pagina HTML completa. Ascolta material-changed per aggiornare prezzo e SKU, e ha tre pulsanti campione propri che guidano il viewer con set-material. Un punto di partenza per WordPress, Webflow o un sito statico.

html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>My Product · 3D Viewer</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
    .stage { position: relative; aspect-ratio: 4/5; border-radius: 12px; overflow: hidden; }
    .stage iframe { width: 100%; height: 100%; border: 0; }
    .row { display: flex; gap: .5rem; align-items: center; margin-top: 1rem; }
    button { padding: .75rem 1.25rem; border-radius: 8px; border: 0; background: #111; color: #fff; cursor: pointer; }
    button:disabled { opacity: .4; cursor: not-allowed; }
    .price { font-weight: 600; font-size: 1.25rem; }
    .swatch { padding: .4rem .75rem; border-radius: 999px; border: 1px solid #ccc; background: #fff; cursor: pointer; }
  </style>
</head>
<body>
  <h1>Wristwatch in 3D</h1>

  <div class="stage">
    <iframe id="viewer" src="https://luxa3d.com/viewer/VIEWER_ID"
            allow="camera; xr-spatial-tracking; fullscreen"></iframe>
  </div>

  <div class="row">
    <span class="price" id="price">€…</span>
    <span id="sku" style="color:#888"></span>
  </div>
  <div class="row">
    <button class="swatch" data-pn="leather-strap">Leather</button>
    <button class="swatch" data-pn="metal-strap">Metal</button>
    <button class="swatch" data-pn="fabric-strap">Fabric</button>
  </div>
  <div class="row">
    <button id="buy" disabled>Add to cart</button>
  </div>

  <script>
    const LUXA_ORIGIN = 'https://luxa3d.com';
    const viewer = document.getElementById('viewer');
    const priceEl = document.getElementById('price');
    const skuEl = document.getElementById('sku');
    const buyBtn = document.getElementById('buy');

    // Listen for events from the viewer.
    window.addEventListener('message', (event) => {
      if (event.origin !== LUXA_ORIGIN) return;
      const msg = event.data;
      if (!msg?.type?.startsWith?.('luxa:')) return;

      if (msg.type === 'luxa:ready') {
        buyBtn.disabled = false;
      }
      if (msg.type === 'luxa:material-changed' && msg.item?.metadata) {
        priceEl.textContent = '€' + msg.item.metadata.price;
        skuEl.textContent   = '· ' + msg.item.metadata.sku;
      }
    });

    // Drive the viewer from outside buttons.
    document.querySelectorAll('.swatch').forEach((btn) => {
      btn.addEventListener('click', () => {
        viewer.contentWindow.postMessage({
          type: 'luxa:set-material',
          widget: 'color-picker',
          item: btn.dataset.pn,
        }, LUXA_ORIGIN);
      });
    });

    buyBtn.addEventListener('click', () => {
      // Plug in your cart logic here.
      alert('Buy: ' + skuEl.textContent);
    });
  </script>
</body>
</html>

Prossimi passi

  • luxa:open-ar: avviare la realtà aumentata da un pulsante della tua pagina.
  • Origini con caratteri jolly, come https://*.yourbrand.com.
  • Webhook: gli stessi eventi inviati al tuo server, per analisi e ordini.
  • Un piccolo SDK su CDN: Luxa.on('material:changed', …).
  • luxa:viewer-error, perché la tua pagina possa ripiegare con eleganza su una connessione lenta.

Ti serve un altro comando o evento?

Con il piano Custom le richieste di funzionalità fanno parte del contratto.