یک نمایشگر آجری با عنصر روشن بسازید

1. مقدمه

اجزای وب

اجزای وب مجموعه ای از استانداردهای وب هستند که به توسعه دهندگان اجازه می دهد HTML را با عناصر سفارشی گسترش دهند. در این نرم افزار کد، عنصر <brick-viewer> را تعریف می کنید که می تواند مدل های آجری را نمایش دهد!

عنصر روشن

برای کمک به ما در تعریف عنصر سفارشی <brick-viewer> ، از عنصر روشن استفاده می کنیم. lit-element یک کلاس پایه سبک وزن است که مقداری قند نحوی را به استاندارد اجزای وب اضافه می کند. این کار ما را آسان می کند تا با عنصر سفارشی خود راه اندازی و اجرا کنیم.

شروع کنید

ما در یک محیط آنلاین Stackblitz کدنویسی خواهیم کرد، بنابراین این پیوند را در یک پنجره جدید باز کنید:

stackblitz.com/edit/brick-viewer

بیایید شروع کنیم!

2. یک عنصر سفارشی را تعریف کنید

تعریف کلاس

برای تعریف یک عنصر سفارشی، کلاسی ایجاد کنید که LitElement گسترش دهد و آن را با @customElement تزئین کنید. آرگومان @customElement نام عنصر سفارشی خواهد بود.

در brick-viewer.ts قرار دهید:

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

اکنون عنصر <brick-viewer></brick-viewer> برای استفاده در HTML آماده است. اما، اگر آن را امتحان کنید، هیچ چیز ارائه نمی شود. بیایید آن را درست کنیم.

روش رندر

برای پیاده سازی نمای کامپوننت، متدی به نام render تعریف کنید. این روش باید یک الگوی تحت اللفظی برچسب‌گذاری شده با تابع html را برگرداند. هر HTML را که می خواهید در قالب تگ شده به معنای واقعی کلمه قرار دهید. وقتی از <brick-viewer> استفاده می‌کنید، این رندر می‌شود.

روش render را اضافه کنید:

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

3. مشخص کردن فایل LDraw

یک خاصیت را تعریف کنید

بسیار عالی خواهد بود اگر کاربر <brick-viewer> بتواند با استفاده از یک ویژگی، مدل آجری را مشخص کند که نشان دهد:

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

از آنجایی که ما در حال ساختن یک عنصر HTML هستیم، می‌توانیم از API اعلانی استفاده کنیم و یک ویژگی منبع را تعریف کنیم، درست مانند تگ <img> یا <video> . با lit-element، تزئین یک ویژگی کلاس با @property آسان است. گزینه type به شما امکان می دهد تعیین کنید که عنصر lit چگونه ویژگی را برای استفاده به عنوان یک ویژگی HTML تجزیه می کند.

ویژگی و ویژگی src را تعریف کنید:

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

<brick-viewer> اکنون یک ویژگی src دارد که می توانیم آن را در HTML تنظیم کنیم! مقدار آن در حال حاضر از داخل کلاس BrickViewer ما به لطف عنصر روشن قابل خواندن است.

نمایش مقادیر

ما می‌توانیم مقدار ویژگی src را با استفاده از آن در قالب literal متد render نمایش دهیم. با استفاده از نحو ${value} مقادیر را در کلمات الفبای الگو درون یابی کنید.

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

اکنون مقدار ویژگی src را در عنصر <brick-viewer> در پنجره می بینیم. این را امتحان کنید: ابزارهای توسعه دهنده مرورگر خود را باز کنید و ویژگی src را به صورت دستی تغییر دهید. برو امتحان کن...

... توجه کردید که متن در عنصر به طور خودکار به روز می شود؟ lit-element ویژگی های کلاس تزئین شده با @property را مشاهده می کند و نمای را دوباره برای شما رندر می کند! lit-element کارهای سنگین را انجام می دهد، بنابراین شما مجبور به انجام این کار نیستید.

4. صحنه را با Three.js تنظیم کنید

چراغ ها، دوربین، رندر!

عنصر سفارشی ما از three.js برای ارائه مدل های آجری سه بعدی ما استفاده می کند. برخی کارها وجود دارد که ما می خواهیم فقط یک بار برای هر نمونه از عنصر <brick-viewer> انجام دهیم، مانند تنظیم صحنه three.js، دوربین و نورپردازی. ما اینها را به سازنده کلاس BrickViewer اضافه می کنیم. برخی از اشیاء را به عنوان ویژگی های کلاس نگه می داریم تا بتوانیم بعداً از آنها استفاده کنیم: دوربین، صحنه، کنترل ها و رندر.

در تنظیمات صحنه 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);
  };
}

شی WebGLRenderer یک عنصر DOM را ارائه می دهد که صحنه رندر شده three.js را نمایش می دهد. از طریق ویژگی domElement قابل دسترسی است. می‌توانیم این مقدار را با استفاده از نحو ${value} در قالب لفظی رندر درون‌یابی کنیم.

پیام src را که در قالب داشتیم حذف کنید و عنصر DOM رندر کننده را وارد کنید:

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

برای اینکه اجازه دهیم عنصر dom رندر به طور کامل نشان داده شود، باید خود عنصر <brick-viewer> را نیز برای display: block . ما می‌توانیم استایل‌ها را در یک ویژگی ثابت به نام styles ارائه کنیم، که به معنای واقعی کلمه بر روی یک الگوی css تنظیم شده است.

این استایل را به کلاس اضافه کنید:

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

اکنون <brick-viewer> یک صحنه رندر شده three.js را نمایش می دهد:

یک عنصر بیننده آجری که صحنه ای رندر شده، اما خالی را نمایش می دهد.

اما... خالی است. بیایید یک مدل به آن ارائه دهیم.

آجر لودر

ویژگی src را که قبلاً تعریف کرده بودیم به LDrawLoader که با three.js ارسال می شود، منتقل می کنیم.

فایل‌های LDraw می‌توانند یک مدل Brick را به مراحل ساختمان جداگانه تقسیم کنند. تعداد کل مراحل و نمایان شدن تک تک آجرها از طریق API LDrawLoader قابل دسترسی است.

این ویژگی ها، متد جدید _loadModel و خط جدید را در سازنده کپی کنید:

@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();
        });
  }
}

چه زمانی باید _loadModel فراخوانی شود؟ باید هر بار که ویژگی src تغییر می کند فراخوانی شود. با تزئین ویژگی src با @property ، این ویژگی را در چرخه حیات به‌روزرسانی عنصر lit-element انتخاب کرده‌ایم. هرگاه مقدار یکی از این املاک تزئین شده تغییر کند، یک سری روش ها نامیده می شود که می تواند به مقادیر جدید و قدیمی املاک دسترسی پیدا کند. روش چرخه عمر مورد علاقه ما update نامیده می شود. متد update آرگومان PropertyValues ​​را می گیرد که حاوی اطلاعاتی در مورد ویژگی هایی است که به تازگی تغییر کرده اند. این مکان عالی برای فراخوانی _loadModel است.

روش update را اضافه کنید:

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

عنصر <brick-viewer> ما اکنون می تواند یک فایل آجری را که با ویژگی src مشخص شده است نمایش دهد.

یک عنصر آجر بیننده که مدلی از یک ماشین را نمایش می دهد.

5. نمایش مدل های جزئی

حالا بیایید مرحله ساخت و ساز فعلی را قابل تنظیم کنیم. ما می‌خواهیم بتوانیم <brick-viewer step="5"></brick-viewer> را مشخص کنیم و باید ببینیم که مدل آجری در مرحله پنجم ساخت‌وساز چگونه به نظر می‌رسد. برای انجام این کار، بیایید ویژگی step را با تزئین آن با @property به یک ویژگی مشاهده شده تبدیل کنیم.

تزیین ویژگی step :

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

اکنون، یک روش کمکی اضافه می کنیم که فقط آجرها را تا مرحله ساخت فعلی قابل مشاهده می کند. ما کمک کننده را در متد به روز رسانی فراخوانی می کنیم تا با هر بار تغییر ویژگی step اجرا شود.

روش update را به روز کنید و روش جدید _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);
  }
}

بسیار خوب، اکنون ابزارهای توسعه مرورگر خود را باز کنید و عنصر <brick-viewer> را بررسی کنید. یک ویژگی step به آن اضافه کنید، مانند این:

کد HTML یک عنصر brick-viewer، با ویژگی step روی 10 تنظیم شده است.

ببینید چه اتفاقی برای مدل رندر شده می افتد! ما می توانیم از ویژگی step برای کنترل میزان نمایش مدل استفاده کنیم. وقتی ویژگی step روی "10" تنظیم می شود، در اینجا باید به نظر برسد:

یک مدل آجری با تنها ده پله ساخت.

6. ناوبری مجموعه آجری

mwc-icon-button

کاربر نهایی <brick-viewer> ما نیز باید بتواند مراحل ساخت را از طریق UI پیمایش کند. بیایید دکمه هایی برای رفتن به مرحله بعد، مرحله قبل و مرحله اول اضافه کنیم. ما از کامپوننت وب دکمه Material Design برای آسان کردن آن استفاده خواهیم کرد. از آنجایی که @material/mwc-icon-button قبلاً وارد شده است، ما آماده ایم <mwc-icon-button></mwc-icon-button> را رها کنیم. می‌توانیم نمادی را که می‌خواهیم با ویژگی icon استفاده کنیم، مانند این مشخص کنیم: <mwc-icon-button icon="thumb_up"></mwc-icon-button> . تمام نمادهای ممکن را می توانید در اینجا پیدا کنید: material.io/resources/icons .

بیایید چند دکمه آیکون را به روش رندر اضافه کنیم:

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>
    `;
  }
}

استفاده از طراحی متریال در صفحه ما به لطف اجزای وب به همین راحتی است!

اتصالات رویداد

این دکمه ها در واقع باید کاری انجام دهند. دکمه "پاسخ" باید مرحله ساخت را به 1 بازنشانی کند. دکمه "navigate_before" باید مرحله ساخت را کاهش دهد و دکمه "navigate_next" باید آن را افزایش دهد. lit-element اضافه کردن این قابلیت را با اتصالات رویداد آسان می کند. در قالب html خود، از نحو @eventname=${eventHandler} به عنوان ویژگی عنصر استفاده کنید. eventHandler اکنون زمانی اجرا می شود که یک رویداد eventname در آن عنصر شناسایی شود! به عنوان مثال، اجازه دهید کنترل کننده رویداد کلیک را به سه دکمه نماد خود اضافه کنیم:

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>
    `;
  }
}

اکنون روی دکمه ها کلیک کنید. کار خوب!

سبک ها

دکمه ها کار می کنند، اما ظاهر خوبی ندارند. همه آنها در پایین جمع شده اند. بیایید آنها را به گونه‌ای طراحی کنیم که روی صحنه قرار بگیرند.

برای اعمال استایل ها به این دکمه ها، به ویژگی static styles باز می گردیم. این سبک‌ها دارای دامنه هستند، به این معنی که آنها فقط برای عناصر درون این مؤلفه وب اعمال می‌شوند. این یکی از لذت‌های نوشتن مؤلفه‌های وب است: انتخابگرها می‌توانند ساده‌تر باشند و CSS خواندن و نوشتن آسان‌تر خواهد بود. خداحافظ، BEM !

استایل ها را به روز کنید تا شبیه به این شوند:

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

یک عنصر نمایشگر آجری با دکمه‌های راه‌اندازی مجدد، عقب و جلو.

دکمه تنظیم مجدد دوربین

کاربران نهایی <brick-viewer> ما می توانند صحنه را با استفاده از کنترل های ماوس بچرخانند. در حالی که در حال اضافه کردن دکمه‌ها هستیم، بیایید یک دکمه برای تنظیم مجدد دوربین به موقعیت پیش‌فرض خود اضافه کنیم. یک <mwc-icon-button> دیگر با پیوند رویداد کلیک، کار را انجام می دهد.

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>
    `;
  }
}

ناوبری سریعتر

برخی از مجموعه های آجری دارای پله های زیادی هستند. ممکن است کاربر بخواهد به مرحله خاصی بپرد. افزودن یک نوار لغزنده با اعداد مرحله می تواند به ناوبری سریع کمک کند. برای این کار از عنصر <mwc-slider> استفاده خواهیم کرد.

mwc-slider

عنصر لغزنده به چند قطعه داده مهم مانند حداقل و حداکثر مقدار لغزنده نیاز دارد. حداقل مقدار لغزنده همیشه می تواند "1" باشد. حداکثر مقدار لغزنده باید this._numConstructionSteps باشد._numConstructionSteps، اگر مدل بارگذاری شده باشد. ما می توانیم به <mwc-slider> این مقادیر را از طریق ویژگی های آن بگوییم. اگر ویژگی _numConstructionSteps تعریف نشده باشد، می توانیم از دستورالعمل ifDefined lit-html برای جلوگیری از تنظیم ویژگی max استفاده کنیم.

یک <mwc-slider> بین دکمه های "back" و "forward" اضافه کنید:

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>
    `;
  }
}

داده ها "بالا"

هنگامی که کاربر نوار لغزنده را جابجا می کند، مرحله ساخت فعلی باید تغییر کند و دید مدل باید بر این اساس به روز شود. هر زمان که نوار لغزنده کشیده شود، عنصر لغزنده یک رویداد ورودی منتشر می کند. برای گرفتن این رویداد و تغییر مرحله ساخت، یک رویداد binding روی خود اسلایدر اضافه کنید.

پیوند رویداد را اضافه کنید:

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>
    `;
  }
}

وو ما می توانیم از نوار لغزنده برای تغییر مرحله نمایش داده شده استفاده کنیم.

داده ها "پایین"

یه چیز دیگه هم هست هنگامی که از دکمه های "بازگشت" و "بعدی" برای تغییر مرحله استفاده می شود، دسته لغزنده باید به روز شود. ویژگی مقدار <mwc-slider> را به this.step متصل کنید.

value binding را اضافه کنید:

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>
    `;
  }
}

ما تقریباً کار با نوار لغزنده را تمام کرده ایم. یک سبک انعطاف‌پذیر اضافه کنید تا با سایر کنترل‌ها به خوبی بازی کند:

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

همچنین، باید layout روی خود عنصر اسلایدر فراخوانی کنیم. ما این کار را در روش چرخه حیات firstUpdated انجام خواهیم داد، که پس از اولین بارگذاری DOM فراخوانی می شود. تزیین کننده query می تواند به ما کمک کند تا به عنصر لغزنده در الگو مراجعه کنیم.

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

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

در اینجا همه موارد اضافه شده اسلایدر با هم آمده است (با ویژگی های pin و markers اضافی روی نوار لغزنده برای اینکه ظاهر جالبی داشته باشد):

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>
   `;
 }
}

در اینجا محصول نهایی است!

پیمایش یک مدل آجر ماشین با عنصر آجر بیننده

7. نتیجه گیری

ما چیزهای زیادی در مورد نحوه استفاده از عنصر lit برای ساختن عنصر HTML خودمان یاد گرفتیم. ما یاد گرفتیم که چگونه:

  • یک عنصر سفارشی را تعریف کنید
  • یک ویژگی API را اعلام کنید
  • نمایش یک عنصر سفارشی را ارائه دهید
  • کپسوله کردن سبک ها
  • از رویدادها و ویژگی ها برای ارسال داده ها استفاده کنید

اگر می‌خواهید درباره عنصر روشن بیشتر بدانید، می‌توانید در سایت رسمی آن بیشتر بخوانید.

می‌توانید یک عنصر کامل‌شده آجری را در stackblitz.com/edit/brick-viewer-complete مشاهده کنید.

brick-viewer نیز در NPM ارسال می‌شود، و می‌توانید منبع را در اینجا مشاهده کنید: Github repo .