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

1. مقدمة

مكونات الويب

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

عنصر مضاء

لمساعدتنا في تحديد العنصر المخصّص <brick-viewer>، سنستخدم lit-element.‏ 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. ولكن إذا جربتها، فلن يتم عرض أي شيء. دعنا نصلح ذلك.

طريقة العرض

لتنفيذ طريقة عرض المكوِّن، حدد طريقة تُسمى العرض. من المفترض أن تعرِض هذه الطريقة نموذجًا حرفيًا تم وضع علامة عليه باستخدام الدالة 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 تحديد كيفية تحليل lit-element للسمة لاستخدامها كسمة HTML.

حدِّد السمة src والموقع src:

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

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

عرض القيم

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

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

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

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

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

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

سيستخدم العنصر المخصّص مكتبة three.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 معروضًا:

عنصر &quot;عارض الطوب&quot; يعرض مشهدًا تم إنشاؤه، ولكنه فارغ

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

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

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

يمكن أن تفصل ملفات LDraw نموذجًا من الطوب إلى خطوات بناء منفصلة. يمكن الوصول إلى إجمالي عدد الخطوات ومستوى رؤية كلّ قطعة من الطوب من خلال LDrawLoader API.

انسخ هذه السمات وطريقة _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.

عنصر &quot;عارض الطوب&quot; يعرض نموذجًا لسيارة

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

حسنًا، افتح الآن devtools في المتصفّح، وفحص عنصر <brick-viewer>. أضِف سمة step إليها، على النحو التالي:

رمز HTML لعنصر &quot;عارض الطوب&quot;، مع ضبط سمة الخطوة على 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" إلى زيادة السرعة. ويسهل عنصر lit إضافة هذه الوظيفة باستخدام عمليات ربط الأحداث. في الصيغة الحرفية لنموذج 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، إذا تم تحميل النموذج. يمكننا إخبار <mwc-slider> بهذه القيم من خلال سمات المنتج. يمكننا أيضًا استخدام توجيه ifDefined lit-html لتجنُّب ضبط السمة 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 lifecycle، والتي يتمّ استدعاؤها بعد ترتيب 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 الخاص بنا. لقد تعلمنا كيفية:

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

إذا كنت تريد معرفة المزيد عن lit-element، يمكنك الاطّلاع على مزيد من المعلومات على الموقع الرسمي.

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

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