إنشاء عارض من الطوب مع عناصر مضاءة

1. مقدمة

مكونات الويب

مكوّنات الويب هي مجموعة من معايير الويب التي تسمح للمطوّرين بتوسيع HTML باستخدام عناصر مخصّصة. في هذا الدرس التطبيقي حول الترميز، ستحدِّد العنصر <brick-viewer> الذي سيتمكن من عرض نماذج من الطوب.

عنصر مضاء

لمساعدتنا في تحديد العنصر المخصص <brick-viewer>، سنستخدم عنصر مضاء. العنصر المضاء هي فئة أساسية خفيفة تضيف بعض السكر النحوي إلى معيار مكونات الويب. وهذا سيجعل من السهل علينا بدء استخدام العنصر المخصص.

البدء

سنقوم بالترميز في بيئة 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. ولكن إذا جربتها، فلن يتم عرض أي شيء. دعنا نصلح ذلك.

طريقة العرض

لتنفيذ طريقة عرض المكوِّن، حدد طريقة تُسمى العرض. من المفترض أن تعرض هذه الطريقة نموذجًا حرفيًا تم وضع علامة عليه باستخدام الدالة 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، يمكننا الاستفادة من واجهة برمجة التطبيقات التعريفية وتحديد سمة المصدر، تمامًا مثل العلامة <img> أو <video>. مع عنصر مضاء، الأمر سهل تمامًا كتزيين فندق صف باستخدام "@property". يتيح لك الخيار type تحديد كيفية تحليل العنصر المضيّ للسمة من أجل استخدامها كسمة HTML.

حدد خاصية وسمة src:

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

تشمل السمة <brick-viewer> الآن السمة src التي يمكننا ضبطها في HTML. ويمكن قراءة قيمتها بالفعل من داخل فئة BrickViewer بفضل العنصر المضاء.

القيم المعروضة

يمكننا عرض قيمة السمة src من خلال استخدامها في النموذج الحرفي لنموذج طريقة العرض. دمج القيم في قيم حرفية للنموذج باستخدام بنية ${value}

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

نرى الآن قيمة السمة src في العنصر <brick-viewer> في النافذة. جرِّب ما يلي: افتح أدوات مطوّري البرامج في متصفحك وغيِّر سمة src يدويًا. ابدأ التجربة...

...هل لاحظت أنّ النص في العنصر يتم تحديثه تلقائيًا؟ عنصر مضاء يراقب خصائص الفئة المزينة بـ @property وتعرض المنظر لك مجددًا العنصر المضاء بالمجهود الثقيل حتى لا تضطر إلى القيام بذلك.

4. تعيين المشهد باستخدام Three.js

استمتِع بتجربة العرض.

وسيستخدم العنصر المخصص 3.js لعرض نماذج الطوب الثلاثية الأبعاد. هناك بعض الإجراءات التي نريد تنفيذها مرة واحدة فقط لكل نسخة افتراضية من عنصر <brick-viewer>، مثل إعداد المشهد الثلاثة.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> الآن مشهدًا 3.js معروضًا:

عنصر عارض من الطوب يعرض مشهدًا معروضًا ولكنّه فارغ

لكنها فارغة. لنقدم نموذجًا.

عامل تحميل الطوب

سننقل السمة src التي حدّدناها سابقًا إلى LDrawLoader، والذي يتم شحنه باستخدام three.js.

يمكن لملفات LDraw فصل نموذج من الطوب إلى خطوات بناء منفصلة. يمكن الوصول إلى إجمالي عدد الخطوات ومستوى الرؤية الفردي للطوب من خلال واجهة برمجة تطبيقات 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، اخترنا تضمين العقار في دورة تحديث العناصر المضاءة. عندما يتعلق الأمر بأحد هذه الخصائص المزخرفة التغييرات في القيم، يتم استدعاء سلسلة من الطرق التي يمكنها الوصول إلى القيم الجديدة والقديمة للخصائص. وتُسمى طريقة دورة الحياة التي نحن مهتم بها 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، مع ضبط سمة الخطوة على 10

شاهِد ما يحدث للنموذج المعروض. ويمكننا استخدام السمة step للتحكّم في مقدار ما يظهر من النموذج. في ما يلي الشكل الذي ستظهر به عند ضبط السمة step على "10":

نموذج من الطوب مكون من عشر درجات بناء فقط.

6- التنقل في مجموعة الطوب

زر-رمز-mwc

يجب أيضًا أن يتمكّن مستخدم <brick-viewer> النهائي من الانتقال إلى خطوات التصميم من خلال واجهة المستخدم. لنضيف أزرارًا للانتقال إلى الخطوة التالية والخطوة السابقة والخطوة الأولى. سنستخدم مكون الويب لزر Material Design لتسهيل الأمر. بما أنّه تم استيراد @material/mwc-icon-button من قبل، نحن مستعدون للإسقاط في <mwc-icon-button></mwc-icon-button>. يمكننا تحديد الرمز الذي نريد استخدامه مع سمة الرمز، كما يلي: <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>
    `;
  }
}

يُعد استخدام Material Design في صفحتنا أمرًا سهلاً، وذلك بفضل مكونات الويب!

عمليات ربط الأحداث

يجب أن تفعل هذه الأزرار شيئًا في الواقع. "الرد" زر إعادة تعيين خطوة البناء إلى 1. أمر "navigate_before" يؤدي الزر إلى تقليل خطوة الإنشاء، بالإضافة إلى زر "navigate_next" الزر. تسهّل العناصر المضاءة إضافة هذه الوظيفة باستخدام عمليات ربط الأحداث. في الصيغة الحرفية لنموذج 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;
    }
  `;
}

عنصر أفقي مع أزرار &quot;إعادة التشغيل&quot; و&quot;للخلف&quot; و&quot;للأمام&quot;

زر إعادة ضبط الكاميرا

يمكن لمستخدمي <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

يحتاج عنصر شريط التمرير إلى بضع أجزاء من البيانات المهمة، مثل الحد الأدنى والحد الأقصى لقيمة شريط التمرير. يمكن أن يكون الحد الأدنى لقيمة شريط التمرير دائمًا "1". يجب أن تكون قيمة الحد الأقصى لشريط التمرير this._numConstructionSteps، إذا تم تحميل النموذج. ويمكننا تحديد <mwc-slider> لهذه القيم من خلال سماتها. يمكننا أيضًا استخدام توجيه lit-html ifDefined لتجنُّب ضبط السمة max إذا لم يتم تحديد السمة _numConstructionSteps.

إضافة <mwc-slider> بين علامتَي التبويب "رجوع" و"إعادة التوجيه" الأزرار:

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

بيانات "أعلى"

عندما يحرّك المستخدم شريط التمرير، من المفترض أن تتغير خطوة البناء الحالية، كما يجب تحديث مستوى رؤية النموذج وفقًا لذلك. سيصدر عنصر شريط التمرير حدث إدخال كلما تم سحب شريط التمرير. أضِف ربط حدث على شريط التمرير نفسه لتسجيل هذا الحدث وتغيير خطوة الإنشاء.

إضافة ربط الحدث:

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":

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. الخاتمة

لقد تعلمنا الكثير عن كيفية استخدام عنصر مضيء لإنشاء عنصر HTML الخاص بنا. لقد تعلمنا كيفية:

  • تحديد عنصر مخصّص
  • تعريف واجهة برمجة التطبيقات للسمة
  • عرض طريقة عرض لعنصر مخصّص
  • تضمين الأنماط
  • استخدام الأحداث والخصائص لتمرير البيانات

لمزيد من المعلومات حول العنصر المضيء، يمكنك قراءة المزيد من المعلومات على موقعه الرسمي.

يمكنك مشاهدة عنصر brick-viewer مكتمل على الرابط stackblitz.com/edit/brick-viewer-complete.

يتم شحن brick-viewer أيضًا على NPM، ويمكنك الاطّلاع على المصدر هنا: Github repo.