Crea un visualizzatore di mattoncini con lit-element

1. Introduzione

Componenti web

I componenti web sono una raccolta di standard web che consentono agli sviluppatori di estendere l'HTML con elementi personalizzati. In questo codelab, definirai l'elemento <brick-viewer>, che potrà mostrare i modelli di blocchi.

elemento-lit

Per aiutarci a definire il nostro elemento personalizzato <brick-viewer>, utilizzeremo lit-element. lit-element è una classe di base leggera che aggiunge alcune funzionalità di sintassi allo standard dei componenti web. In questo modo sarà più facile per noi iniziare a utilizzare il nostro elemento personalizzato.

Inizia

Faremo il codice in un ambiente Stackblitz online, quindi apri questo link in una nuova finestra:

stackblitz.com/edit/brick-viewer

Iniziamo.

2. Definisci un elemento personalizzato

Definizione del corso

Per definire un elemento personalizzato, crea una classe che amplia LitElement e decoralo con @customElement. L'argomento per @customElement sarà il nome dell'elemento personalizzato.

In Brick-viewer.ts, metti:

@customElement('brick-viewer')
export class BrickViewer extends LitElement {
}

Ora l'elemento <brick-viewer></brick-viewer> è pronto per essere utilizzato in HTML. Tuttavia, se provi, non verrà visualizzato nulla. Risolviamo il problema.

Metodo di rendering

Per implementare la visualizzazione del componente, definisci un metodo denominato render. Questo metodo dovrebbe restituire un valore letterale di modello con la funzione html. Inserisci il codice HTML che preferisci nel literal del modello taggato. Verrà visualizzata quando usi <brick-viewer>.

Aggiungi il metodo render:

export class BrickViewer extends LitElement {
  render() {
    return html`<div>Brick viewer</div>`;
  }
}

3. Specifica del file LDraw

Definire una proprietà

Sarebbe bello se un utente dell'<brick-viewer> potesse specificare il modello di mattoncino da mostrare utilizzando un attributo, ad esempio:

<brick-viewer src="path/to/model.ldraw"></brick-viewer>

Poiché stiamo creando un elemento HTML, possiamo sfruttare l'API dichiarativa e definire un attributo source, proprio come un tag <img> o <video>. Con l'elemento lit-element, è facile come decorare una proprietà di classe con @property. L'opzione type consente di specificare in che modo lit-element analizza la proprietà per l'utilizzo come attributo HTML.

Definisci la proprietà e l'attributo src:

export class BrickViewer extends LitElement {
  @property({type: String})
  src: string|null = null;
}

<brick-viewer> ora ha un attributo src che possiamo impostare in HTML. Il suo valore è già leggibile all'interno della nostra classe BrickViewer grazie a lit-element.

Visualizzazione dei valori

Possiamo visualizzare il valore dell'attributo src utilizzandolo nel valore letterale del modello del metodo di rendering. Interpola i valori nei valori letterali del modello utilizzando la sintassi ${value}.

export class BrickViewer extends LitElement {
  render() {
    return html`<div>Brick model: ${this.src}</div>`;
  }
}

Ora vediamo il valore dell'attributo src nell'elemento <brick-viewer> nella finestra. Prova a fare così: apri gli strumenti per sviluppatori del browser e modifica manualmente l'attributo src. Vai, prova…

...Hai notato che il testo dell'elemento si aggiorna automaticamente? lit-element osserva le proprietà della classe decorate con @property e visualizza di nuovo la vista al posto tuo! lit-element fa il resto, così non devi farlo tu.

4. Creare la scena con Three.js

Luci, fotocamera, rendering!

Il nostro elemento personalizzato utilizzerà tre.js per eseguire il rendering dei nostri modelli di mattoncini 3D. Ci sono alcune cose che vogliamo fare una sola volta per ogni istanza di un elemento <brick-viewer>, ad esempio configurare la scena terza.js, la fotocamera e l'illuminazione. Li aggiungeremo al costruttore della classe BrickViewer. Manterremo alcuni oggetti come proprietà di classe per poterli utilizzare in un secondo momento: camera, scene, controls e renderer.

Aggiungi la configurazione della scena three.js:

export class BrickViewer extends LitElement {

  private _camera: THREE.PerspectiveCamera;
  private _scene: THREE.Scene;
  private _controls: OrbitControls;
  private _renderer: THREE.WebGLRenderer;

  constructor() {
    super();

    this._camera = new THREE.PerspectiveCamera(45, this.clientWidth/this.clientHeight, 1, 10000);
    this._camera.position.set(150, 200, 250);

    this._scene = new THREE.Scene();
    this._scene.background = new THREE.Color(0xdeebed);

    const ambientLight = new THREE.AmbientLight(0xdeebed, 0.4);
    this._scene.add( ambientLight );

    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
    directionalLight.position.set(-1000, 1200, 1500);
    this._scene.add(directionalLight);

    this._renderer = new THREE.WebGLRenderer({antialias: true});
    this._renderer.setPixelRatio(window.devicePixelRatio);
    this._renderer.setSize(this.offsetWidth, this.offsetHeight);

    this._controls = new OrbitControls(this._camera, this._renderer.domElement);
    this._controls.addEventListener("change", () =>
      requestAnimationFrame(this._animate)
    );

    this._animate();

    const resizeObserver = new ResizeObserver(this._onResize);
    resizeObserver.observe(this);
  }

  private _onResize = (entries: ResizeObserverEntry[]) => {
    const { width, height } = entries[0].contentRect;
    this._renderer.setSize(width, height);
    this._camera.aspect = width / height;
    this._camera.updateProjectionMatrix();
    requestAnimationFrame(this._animate);
  };

  private _animate = () => {
    this._renderer.render(this._scene, this._camera);
  };
}

L'oggetto WebGLRenderer fornisce un elemento DOM che mostra la scena terza.js sottoposta a rendering. Si accede tramite la proprietà domElement. Possiamo interpolare questo valore nel valore letterale del modello di rendering, utilizzando la sintassi ${value}.

Rimuovi il messaggio src presente nel modello e inserisci l'elemento DOM del renderer:

export class BrickViewer extends LitElement {
  render() {
    return html`
      ${this._renderer.domElement}
    `;
  }
}

Per consentire la visualizzazione completa dell'elemento dom del renderer, è necessario impostare anche l'elemento <brick-viewer> su display: block. Possiamo fornire gli stili in una proprietà statica denominata styles, impostata su un valore letterale di modello css.

Aggiungi questo stile al corso:

export class BrickViewer extends LitElement {
  static styles = css`
    /* The :host selector styles the brick-viewer itself! */
    :host {
      display: block;
    }
  `;
}

Ora <brick-viewer> sta mostrando una scena terza.js sottoposta a rendering:

Un elemento visualizzatore di blocchi che mostra una scena visualizzata, ma vuota.

Ma... è vuota. Forniamo un modello.

Caricatore per mattoncini

Passeremo la proprietà src che abbiamo definito in precedenza a LDrawLoader, che viene fornito con tre.js.

I file LDraw possono separare un modello Brick in passaggi di costruzione distinti. Il numero totale di passaggi e la visibilità dei singoli mattoncini sono accessibili tramite l'API LDrawLoader.

Copia queste proprietà, il nuovo metodo _loadModel e la nuova riga nel costruttore:

@customElement('brick-viewer')
export class BrickViewer extends LitElement {
  private _loader = new LDrawLoader();
  private _model: any;
  private _numConstructionSteps?: number;
  step?: number;

  constructor() {
    // ...
    // Add this line right before this._animate();
    (this._loader as any).separateObjects = true;
    this._animate();
  }

  private _loadModel() {
    if (this.src === null) {
      return;
    }
    this._loader
        .setPath('')
        // Using our src property!
        .load(this.src, (newModel) => {

          if (this._model !== undefined) {
            this._scene.remove(this._model);
            this._model = undefined;
          }

          this._model = newModel;

          // Convert from LDraw coordinates: rotate 180 degrees around OX
          this._model.rotation.x = Math.PI;
          this._scene.add(this._model);

          this._numConstructionSteps = this._model.userData.numConstructionSteps;
          this.step = this._numConstructionSteps!;

          const bbox = new THREE.Box3().setFromObject(this._model);
          this._controls.target.copy(bbox.getCenter(new THREE.Vector3()));
          this._controls.update();
          this._controls.saveState();
        });
  }
}

Quando deve essere chiamato _loadModel? Deve essere invocato ogni volta che l'attributo src cambia. Decorando la proprietà src con @property, abbiamo attivato il ciclo di vita dell'aggiornamento degli elementi accesi. Ogni volta che il valore di una di queste proprietà decorate cambia, viene chiamata una serie di metodi che possono accedere ai valori nuovi e precedenti delle proprietà. Il metodo del ciclo di vita che ci interessa si chiama update. Il metodo update accetta un argomento PropertyValues, che conterrà informazioni su eventuali proprietà appena modificate. Questo è il posto perfetto per chiamare _loadModel.

Aggiungi il metodo update:

export class BrickViewer extends LitElement {
  update(changedProperties: PropertyValues) {
    if (changedProperties.has('src')) {
      this._loadModel();
    }
    super.update(changedProperties);
  }
}

Il nostro elemento <brick-viewer> ora può visualizzare un file brick, specificato con l'attributo src.

Un mattoncino che mostra il modello di un&#39;auto.

5. Visualizzazione di modelli parziali

Ora rendi configurabile il passaggio di costruzione corrente. Vorremmo essere in grado di specificare <brick-viewer step="5"></brick-viewer> e dovremmo vedere come appare il modello del mattone nel quinto passaggio della costruzione. A tale scopo, rendiamo la proprietà step una proprietà osservata decorandola con @property.

Decora la proprietà step:

export class BrickViewer extends LitElement {
  @property({type: Number})
  step?: number;
}

Ora aggiungeremo un metodo di supporto che rende visibili solo i blocchi fino al passaggio di compilazione corrente. Chiameremo l'helper nel metodo di aggiornamento in modo che venga eseguito ogni volta che la proprietà step viene modificata.

Aggiorna il metodo update e aggiungi il nuovo metodo _updateBricksVisibility:

export class BrickViewer extends LitElement {
  update(changedProperties: PropertyValues) {
    if (changedProperties.has('src')) {
      this._loadModel();
    }
    if (changedProperties.has('step')) {
      this._updateBricksVisibility();
    }
    super.update(changedProperties);
  }

  private _updateBricksVisibility() {
    this._model && this._model.traverse((c: any) => {
      if (c.isGroup && this.step) {
        c.visible = c.userData.constructionStep <= this.step;
      }
    });
    requestAnimationFrame(this._animate);
  }
}

Ora apri devtools del browser e ispeziona l'elemento <brick-viewer>. Aggiungi un attributo step, in questo modo:

Il codice HTML di un elemento Brick-viewer, con un attributo step impostato su 10.

Guarda cosa succede al modello sottoposto a rendering. Possiamo utilizzare l'attributo step per controllare la quantità di modello da mostrare. Ecco come dovrebbe apparire quando l'attributo step è impostato su "10":

Un modello in mattoni con solo dieci gradini di costruzione.

6. Navigazione sul set di mattoncini

pulsante-icona-mwc

L'utente finale del nostro <brick-viewer> deve anche essere in grado di navigare nei passaggi di compilazione tramite l'interfaccia utente. Aggiungiamo pulsanti per andare al passaggio successivo, al passaggio precedente e al primo passaggio. Per semplificare l'operazione, utilizzeremo il componente web dei pulsanti di Material Design. Poiché @material/mwc-icon-button è già importato, possiamo inserirlo in <mwc-icon-button></mwc-icon-button>. Possiamo specificare l'icona che vogliamo utilizzare con l'attributo icon, come segue: <mwc-icon-button icon="thumb_up"></mwc-icon-button>. Tutte le possibili icone sono disponibili qui: material.io/resources/icons.

Aggiungiamo alcuni pulsanti con icone al metodo di rendering:

export class BrickViewer extends LitElement {
  render() {
    return html`
      ${this._renderer.domElement}
      <div id="controls">
        <mwc-icon-button icon="replay"></mwc-icon-button>
        <mwc-icon-button icon="navigate_before"></mwc-icon-button>
        <mwc-icon-button icon="navigate_next"></mwc-icon-button>
      </div>
    `;
  }
}

Utilizzare Material Design nella nostra pagina è semplicissimo grazie ai componenti web.

Associazioni di eventi

Questi pulsanti dovrebbero fare qualcosa. Il pulsante "Rispondi" dovrebbe reimpostare il passaggio di creazione su 1. Il pulsante "navigate_before" deve decrementare il passaggio di costruzione e il pulsante "navigate_next" deve incrementarlo. lit-element semplifica l'aggiunta di questa funzionalità con le associazioni di eventi. Nel valore letterale del modello HTML, utilizza la sintassi @eventname=${eventHandler} come attributo dell'elemento. Ora eventHandler verrà eseguito quando verrà rilevato un evento eventname su quell'elemento. Ad esempio, aggiungiamo gestori di eventi di clic ai tre pulsanti con icone:

export class BrickViewer extends LitElement {
  private _restart() {
    this.step! = 1;
  }

  private _stepBack() {
    this.step! -= 1;
  }

  private _stepForward() {
    this.step! += 1;
  }

  render() {
    return html`
      ${this._renderer.domElement}
      <div id="controls">
        <mwc-icon-button @click=${this._restart} icon="replay"></mwc-icon-button>
        <mwc-icon-button @click=${this._stepBack} icon="navigate_before"></mwc-icon-button>
        <mwc-icon-button @click=${this._stepForward} icon="navigate_next"></mwc-icon-button>
      </div>
    `;
  }
}

Prova a fare clic sui pulsanti ora. Bene!

Stili

I pulsanti funzionano, ma non hanno un bell'aspetto. Sono tutti rannicchiati sul fondo. Scegli uno stile per sovrapporle alla scena.

Per applicare gli stili a questi pulsanti, torniamo alla proprietà static styles. Questi stili sono limitati, il che significa che verranno applicati solo agli elementi all'interno di questo componente web. Questa è una delle gioie della scrittura dei componenti web: i selettori possono essere più semplici e i CSS sono più facili da leggere e scrivere. Arrivederci, BEM.

Aggiorna gli stili in modo che abbiano il seguente aspetto:

export class BrickViewer extends LitElement {
  static styles = css`
    :host {
      display: block;
      position: relative;
    }
    #controls {
      position: absolute;
      bottom: 0;
      width: 100%;
      display: flex;
    }
  `;
}

Un elemento visualizzatore di mattoncini con pulsanti Riavvia, Indietro e Avanti.

Pulsante di reimpostazione della videocamera

Gli utenti finali di <brick-viewer> possono ruotare la scena utilizzando i controlli del mouse. Mentre aggiungiamo i pulsanti, aggiungine uno per reimpostare la posizione predefinita della fotocamera. Un altro <mwc-icon-button> con un'associazione di eventi di clic completerà il lavoro.

export class BrickViewer extends LitElement {
  private _resetCamera() {
    this._controls.reset();
  }

  render() {
    return html`
      <div id="controls">
        <!-- ... -->
        <!-- Add this button: -->
        <mwc-icon-button @click=${this._resetCamera} icon="center_focus_strong"></mwc-icon-button>
      </div>
    `;
  }
}

Navigazione più rapida

Alcuni mattoncini hanno molti gradini. Un utente potrebbe voler passare a un passaggio specifico. L'aggiunta di un cursore con numeri di passaggi può essere utile per la navigazione rapida. A tale scopo utilizzeremo l'elemento <mwc-slider>.

mwc-slider

L'elemento del dispositivo di scorrimento richiede alcuni dati importanti, come il valore minimo e massimo del dispositivo di scorrimento. Il valore minimo del cursore può sempre essere "1". Se il modello è stato caricato, il valore massimo del cursore deve essere this._numConstructionSteps. Possiamo indicare a <mwc-slider> questi valori tramite i suoi attributi. Possiamo anche utilizzare l'directive lit-html ifDefined per evitare di impostare l'attributo max se la proprietà _numConstructionSteps non è stata definita.

Aggiungi un <mwc-slider> tra i pulsanti "Indietro" e "Avanti":

export class BrickViewer extends LitElement {
  render() {
    return html`
      <div id="controls">
        <!-- ... backwards button -->
        <!-- Add this slider: -->
        <mwc-slider
            step="1"
            pin
            markers
            min="1"
            max=${ifDefined(this._numConstructionSteps)}
        ></mwc-slider>
        <!-- ... forwards button -->
      </div>
    `;
  }
}

Dati "up"

Quando un utente sposta il cursore, il passaggio di costruzione corrente deve cambiare e la visibilità del modello deve essere aggiornata di conseguenza. L'elemento del cursore emetterà un evento di input ogni volta che viene trascinato. Aggiungi un'associazione di eventi sul dispositivo di scorrimento per individuare l'evento e modificare il passaggio di costruzione.

Aggiungi l'associazione di eventi:

export class BrickViewer extends LitElement {
  render() {
    return html`
      <div id="controls">
        <!-- ...  -->
        <!-- Add the @input event binding: -->
        <mwc-slider
            ...
            @input=${(e: CustomEvent) => this.step = e.detail.value}
        ></mwc-slider>
        <!-- ... -->
      </div>
    `;
  }
}

Evviva! Possiamo usare il cursore per cambiare il passaggio visualizzato.

Dati "in calo"

C'è un'altra cosa. Quando vengono utilizzati i pulsanti "Indietro" e "Avanti" per modificare il passaggio, il punto di manipolazione del cursore deve essere aggiornato. Associa l'attributo del valore di <mwc-slider> a this.step.

Aggiungi l'associazione value:

export class BrickViewer extends LitElement {
  render() {
    return html`
      <div id="controls">
        <!-- ...  -->
        <!-- Add the value property binding: -->
        <mwc-slider
            ...
            value=${ifDefined(this.step)}
        ></mwc-slider>
        <!-- ... -->
      </div>
    `;
  }
}

Abbiamo quasi finito con il cursore. Aggiungi uno stile flex per integrarlo bene con gli altri controlli:

export class BrickViewer extends LitElement {
  static styles = css`
    /* ... */
    mwc-slider {
      flex-grow: 1;
    }
  `;
}

Inoltre, dobbiamo chiamare layout sull'elemento di scorrimento stesso. Lo faremo nel metodo del ciclo di vita firstUpdated, che viene chiamato dopo la prima impaginazione del DOM. Il decoratore query può aiutarci a ottenere un riferimento all'elemento del dispositivo di scorrimento nel modello.

export class BrickViewer extends LitElement {
  @query('mwc-slider')
  slider!: Slider|null;

  async firstUpdated() {
    if (this.slider) {
      await this.slider.updateComplete
      this.slider.layout();
    }
  }
}

Di seguito sono riportate tutte le aggiunte dei dispositivi di scorrimento (con attributi pin e markers aggiuntivi sul cursore per conferire un aspetto accattivante):

export class BrickViewer extends LitElement {
 @query('mwc-slider')
 slider!: Slider|null;

 static styles = css`
   /* ... */
   mwc-slider {
     flex-grow: 1;
   }
 `;

 async firstUpdated() {
   if (this.slider) {
     await this.slider.updateComplete
     this.slider.layout();
   }
 }

 render() {
   return html`
     ${this._renderer.domElement}
     <div id="controls">
       <mwc-icon-button @click=${this._restart} icon="replay"></mwc-icon-button>
       <mwc-icon-button @click=${this._stepBack} icon="navigate_before"></mwc-icon-button>
       <mwc-slider
         step="1"
         pin
         markers
         min="1"
         max=${ifDefined(this._numConstructionSteps)}
         ?disabled=${this._numConstructionSteps === undefined}
         value=${ifDefined(this.step)}
         @input=${(e: CustomEvent) => this.constructionStep = e.detail.value}
       ></mwc-slider>
       <mwc-icon-button @click=${this._stepForward} icon="navigate_next"></mwc-icon-button>
       <mwc-icon-button @click=${this._resetCamera} icon="center_focus_strong"></mwc-icon-button>
     </div>
   `;
 }
}

Ecco il prodotto finale.

Navigare nel modello di un mattoncino di un&#39;auto con l&#39;elemento mattoncino

7. Conclusione

Abbiamo imparato molto su come utilizzare lit-element per creare il nostro elemento HTML. Abbiamo imparato a:

  • Definisci un elemento personalizzato
  • Dichiarare un'API di attributi
  • Visualizzare una vista per un elemento personalizzato
  • Incapsulare gli stili
  • Utilizzare eventi e proprietà per trasmettere i dati

Per saperne di più su lit-element, puoi leggere il loro sito ufficiale.

Puoi vedere un elemento completo di Brick-viewer all'indirizzo stackblitz.com/edit/brick-viewer-complete.

Brick-viewer viene anche spedito su Gestione dei partner di rete e puoi visualizzare il codice qui: Repository GitHub.