@fmsim/machine 2.0.0-beta.98 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/carrier-manager.js +9 -1
  2. package/dist/carrier-manager.js.map +1 -1
  3. package/dist/carrier.js +69 -52
  4. package/dist/carrier.js.map +1 -1
  5. package/dist/crane.js +15 -6
  6. package/dist/crane.js.map +1 -1
  7. package/dist/factories/agv-3d.js +9 -27
  8. package/dist/factories/agv-3d.js.map +1 -1
  9. package/dist/factories/conveyor-3d.js +11 -19
  10. package/dist/factories/conveyor-3d.js.map +1 -1
  11. package/dist/factories/crane-3d.js +8 -17
  12. package/dist/factories/crane-3d.js.map +1 -1
  13. package/dist/factories/crane-rail-3d.js +8 -12
  14. package/dist/factories/crane-rail-3d.js.map +1 -1
  15. package/dist/factories/machine-3d.js +100 -63
  16. package/dist/factories/machine-3d.js.map +1 -1
  17. package/dist/factories/oht-3d.js +9 -27
  18. package/dist/factories/oht-3d.js.map +1 -1
  19. package/dist/factories/shuttle-3d.js +11 -27
  20. package/dist/factories/shuttle-3d.js.map +1 -1
  21. package/dist/features/capacity-gauge-mixin.js +520 -0
  22. package/dist/features/capacity-gauge-mixin.js.map +1 -0
  23. package/dist/features/mcs-status-default.js +13 -0
  24. package/dist/features/mcs-status-default.js.map +1 -1
  25. package/dist/features/parent-object-mixin.js +5 -1
  26. package/dist/features/parent-object-mixin.js.map +1 -1
  27. package/dist/mcs-machine.js +4 -23
  28. package/dist/mcs-machine.js.map +1 -1
  29. package/dist/mcs-unit.js +2 -5
  30. package/dist/mcs-unit.js.map +1 -1
  31. package/dist/mcs-vehicle.js +3 -1
  32. package/dist/mcs-vehicle.js.map +1 -1
  33. package/dist/scene/root-container-override.js +5 -2
  34. package/dist/scene/root-container-override.js.map +1 -1
  35. package/dist/stocker-capacity-bar.js +11 -372
  36. package/dist/stocker-capacity-bar.js.map +1 -1
  37. package/dist/stocker.js +29 -1
  38. package/dist/stocker.js.map +1 -1
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/dist/utils/crane-position.js +23 -14
  41. package/dist/utils/crane-position.js.map +1 -1
  42. package/dist/utils/stocker-gauge.js +48 -10
  43. package/dist/utils/stocker-gauge.js.map +1 -1
  44. package/dist/zone-capacity-bar.js +13 -213
  45. package/dist/zone-capacity-bar.js.map +1 -1
  46. package/package.json +4 -4
  47. package/src/carrier-manager.ts +9 -1
  48. package/src/carrier.ts +73 -52
  49. package/src/crane.ts +14 -5
  50. package/src/factories/agv-3d.ts +10 -26
  51. package/src/factories/conveyor-3d.ts +12 -19
  52. package/src/factories/crane-3d.ts +9 -17
  53. package/src/factories/crane-rail-3d.ts +9 -13
  54. package/src/factories/machine-3d.ts +125 -66
  55. package/src/factories/oht-3d.ts +10 -26
  56. package/src/factories/shuttle-3d.ts +12 -26
  57. package/src/features/capacity-gauge-mixin.ts +642 -0
  58. package/src/features/mcs-status-default.ts +13 -0
  59. package/src/features/parent-object-mixin.ts +5 -1
  60. package/src/mcs-machine.ts +4 -23
  61. package/src/mcs-unit.ts +2 -5
  62. package/src/mcs-vehicle.ts +3 -1
  63. package/src/scene/root-container-override.ts +5 -2
  64. package/src/stocker-capacity-bar.ts +15 -476
  65. package/src/stocker.ts +29 -1
  66. package/src/utils/crane-position.ts +46 -14
  67. package/src/utils/stocker-gauge.ts +63 -10
  68. package/src/zone-capacity-bar.ts +17 -282
  69. package/translations/en.json +4 -1
  70. package/translations/ja.json +4 -1
  71. package/translations/ko.json +4 -1
  72. package/translations/ms.json +4 -1
  73. package/translations/zh.json +4 -1
@@ -10,15 +10,15 @@
10
10
 
11
11
  import * as THREE from 'three'
12
12
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
13
- import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene'
13
+ import { RealObjectGroup, registerRealObjectFactory, disposeObject3D, MaterialBank } from '@hatiolab/things-scene'
14
14
  import type { Component } from '@hatiolab/things-scene'
15
- import { LEGEND_CAPACITY } from '../features/mcs-status-default'
15
+ import { LEGEND_CAPACITY, resolveCarrierLegend } from '../features/mcs-status-default'
16
16
  import { getValueOnRanges } from '../utils/get-value-on-ranges'
17
17
  import { fitGaugeScale } from '../utils/fit-gauge-scale'
18
18
  import {
19
19
  carrierSegmentsFromData,
20
20
  abnormalSegmentsFromData,
21
- CARRIER_COLORS,
21
+ carrierColorsFromLegend,
22
22
  ABNORMAL_COLORS
23
23
  } from '../utils/stocker-gauge'
24
24
  import { Z_FMSIM } from './z-priority'
@@ -312,23 +312,30 @@ class MCSRealObject3D extends RealObjectGroup {
312
312
  : this.buildBoxGeometry(w, depth, h)
313
313
 
314
314
  const isPlane = shape === 'plane'
315
- const material = isPlane
315
+ // Standard body 는 MaterialBank 공유(색 변형) — 인스턴스 dedup + faint 안전(코어가 공유 base 의
316
+ // 변형만 스왑, 원본 base 불변). plane 은 unlit MeshBasicMaterial 이라 bank(Standard 전용) 비대상 → raw.
317
+ const material: THREE.Material = isPlane
316
318
  ? new THREE.MeshBasicMaterial({
317
319
  color,
318
320
  opacity,
319
321
  transparent: opacity < 1,
320
322
  side: THREE.DoubleSide,
321
323
  })
322
- : new THREE.MeshStandardMaterial({
324
+ : MaterialBank.material(
325
+ 'mcs|body',
326
+ {
327
+ metalness,
328
+ roughness,
329
+ opacity,
330
+ side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide,
331
+ ...(this._config.emissiveIntensity
332
+ ? { emissive: color, emissiveIntensity: this._config.emissiveIntensity }
333
+ : {}),
334
+ },
323
335
  color,
324
- metalness,
325
- roughness,
326
- opacity,
327
- transparent: opacity < 1,
328
- side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide,
329
- ...(this._config.emissiveIntensity ? { emissive: color, emissiveIntensity: this._config.emissiveIntensity } : {}),
330
- })
331
- // z-fighting 방지: 바닥면(floor grid)보다 항상 앞에 렌더링
336
+ )
337
+ // z-fighting 방지: 바닥면(floor grid)보다 앞. bank body 는 공유 base 라 polygonOffset 은 모든 mcs
338
+ // body 공통 상수 — 반환 material 에 세팅(같은 값이라 멱등, 인스턴스별 발산 없음).
332
339
  material.polygonOffset = true
333
340
  material.polygonOffsetFactor = -1
334
341
  material.polygonOffsetUnit = -1
@@ -360,6 +367,11 @@ class MCSRealObject3D extends RealObjectGroup {
360
367
  this._ownMesh.castShadow = !isPlane
361
368
  this._ownMesh.receiveShadow = true
362
369
  this.components3D.add(this._ownMesh)
370
+
371
+ // mesh 는 super.build() *이후* 생성되므로, 코어의 build-시점 faint 적용이 이 mesh 에 못 닿는다.
372
+ // mesh 생성 직후 명시 적용 — 코어 applyMaterialOpacity 가 content-alpha × faint 를 일괄 처리.
373
+ // view 모드에선 factor=1(=state.alpha 없음) → canonical(config.opacity) 그대로, 고객 화면 불변.
374
+ this.updateAlpha()
363
375
  }
364
376
 
365
377
  // ── Change handling ──
@@ -375,7 +387,8 @@ class MCSRealObject3D extends RealObjectGroup {
375
387
  'strokeStyle' in after ||
376
388
  'id' in after ||
377
389
  'EMPTYTYPE' in after ||
378
- 'CARRIERSTATUS' in after
390
+ 'CARRIERSTATUS' in after ||
391
+ 'showText' in after
379
392
  ) {
380
393
  // 연속 setState에 의한 다중 rebuild 방지: 한 프레임에 한 번만 실행
381
394
  if (!this._updatePending) {
@@ -415,8 +428,9 @@ class MCSRealObject3D extends RealObjectGroup {
415
428
  ctx.fillStyle = statusColor
416
429
  ctx.fillRect(0, 0, usedW, usedH)
417
430
 
418
- // 텍스트 (사용 영역 중앙)
419
- if (text) {
431
+ // 텍스트 (사용 영역 중앙) — showText(= 현재 홀더의 showCarrierText) off 면 미표시. 2D 와 동일.
432
+ const showText = !!(this.component.state as any)?.showText
433
+ if (text && showText) {
420
434
  const fontSize = Math.round(Math.min(usedW, usedH) * 0.7)
421
435
  ctx.font = `bold ${fontSize}px Arial`
422
436
  ctx.textAlign = 'center'
@@ -468,22 +482,10 @@ class MCSRealObject3D extends RealObjectGroup {
468
482
  // RealObjectGroup 기본 동작 비활성화
469
483
  updateDimension() {}
470
484
 
471
- updateAlpha() {
472
- const state = this.component.state
473
- // alpha가 명시적으로 설정되어 있으면 그 값을 사용, 없으면 config 기본값 적용
474
- const effective = ('alpha' in state) ? (state.alpha as number) : this._config.opacity
475
-
476
- this.object3d.traverse((o: any) => {
477
- if (!o.material) return
478
- const materials = Array.isArray(o.material) ? o.material : [o.material]
479
- for (const m of materials) {
480
- if (!m.visible) continue // openTop invisible face 무시
481
- if (m.map) continue // 텍스처 material은 자체 transparent 유지
482
- m.opacity = effective
483
- m.transparent = effective < 1
484
- }
485
- })
486
- }
485
+ // updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴.
486
+ // 코어가 canonical(=build 시 config.opacity 로 만든 material) 위에 factor = state.alpha × faint 를
487
+ // 공유 변형 스왑으로 일괄 적용한다(원본 base 불변). 기존 in-place 변이는 (1) faint 미적용,
488
+ // (2) 코어 applyFocusDim 과 이중 적용 소지가 있어 방언이었다. config.opacity 는 canonical 로 보존.
487
489
  }
488
490
 
489
491
  // ── 헬퍼 ────────────────────────────────────────────────
@@ -573,11 +575,9 @@ class OHTLineRealObject3D extends RealObjectGroup {
573
575
  const webH = beamH - flangeH * 2 // 웹 높이
574
576
  const webW = Math.max(flangeW * 0.25, 1) // 웹 두께
575
577
 
576
- const mat = new THREE.MeshStandardMaterial({
577
- color: OHT_LINE_RAIL_COLOR,
578
- metalness: 0.5,
579
- roughness: 0.4,
580
- })
578
+ // 공유 base(MaterialBank) — OHTLine rail 은 정적 색이라 전 인스턴스 dedup. faint 는 코어가
579
+ // 공유 base 의 변형을 스왑(원본 불변). dispose 는 bank-safe(update 에서 material.dispose 안 함).
580
+ const mat = MaterialBank.material('oht-line|rail', { metalness: 0.5, roughness: 0.4 }, OHT_LINE_RAIL_COLOR)
581
581
 
582
582
  const geoms: THREE.BufferGeometry[] = []
583
583
 
@@ -605,13 +605,16 @@ class OHTLineRealObject3D extends RealObjectGroup {
605
605
  this._mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(geoms), mat)
606
606
  this._mesh.castShadow = true
607
607
  this.object3d.add(this._mesh)
608
+
609
+ // mesh 가 super.build() 이후 생성 → 코어 faint 를 명시 적용(편집 흐림이 mesh 에 닿게).
610
+ this.updateAlpha()
608
611
  }
609
612
 
610
613
  update() {
611
614
  if (this._mesh) {
612
615
  this.object3d.remove(this._mesh)
613
616
  this._mesh.geometry.dispose()
614
- ;(this._mesh.material as THREE.Material).dispose()
617
+ // material 은 MaterialBank 공유(앱 수명) — 여기서 dispose 하면 다른 인스턴스가 깨진다.
615
618
  this._mesh = undefined
616
619
  }
617
620
  this.build()
@@ -628,8 +631,8 @@ class OHTLineRealObject3D extends RealObjectGroup {
628
631
  super.onchange(after, before)
629
632
  }
630
633
 
634
+ // updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴(faint).
631
635
  updateDimension() {}
632
- updateAlpha() {}
633
636
  }
634
637
 
635
638
  registerRealObjectFactory('OHTLine', (component: Component) => new OHTLineRealObject3D(component))
@@ -702,24 +705,32 @@ class StockerRealObject3D extends MCSRealObject3D {
702
705
  ctx.lineWidth = 12
703
706
  ctx.strokeRect(6, 6, canvasW - 12, canvasH - 12)
704
707
 
705
- // 세그먼트 채움 (왼→오)
708
+ // 세그먼트 채움 (왼→오) + carrier/abnormal 은 세그먼트별 숫자(%) 표기 (2D 와 동일)
706
709
  const innerW = canvasW - pad * 2
707
710
  const innerH = canvasH - pad * 2
711
+ // 컴포넌트 폰트 설정(fontFamily/bold/italic) 을 그대로, 크기만 텍스처 scale 배 (2D 와 상대 크기 일치).
712
+ const fontSize = (Number((this.component as any).state?.fontSize) || 15) * scale
713
+ const font = this.gaugeFont(fontSize)
708
714
  let x = pad
709
715
  for (const seg of gauge.segments) {
710
716
  const segW = Math.max(innerW * seg.ratio, seg.ratio > 0 ? 1 : 0)
711
717
  if (segW <= 0) continue
712
718
  ctx.fillStyle = seg.color
713
719
  ctx.fillRect(x, pad, segW, innerH)
720
+ if (gauge.perSegment) {
721
+ this.drawGaugeSegmentNumber(ctx, seg.ratio, x, pad, segW, innerH, font, fontSize)
722
+ }
714
723
  x += segW
715
724
  }
716
725
 
717
- // 텍스트
718
- ctx.font = `bold ${10 * scale}px sans-serif`
719
- ctx.textAlign = 'center'
720
- ctx.textBaseline = 'middle'
721
- ctx.fillStyle = '#333333'
722
- ctx.fillText(gauge.label, canvasW / 2, canvasH / 2)
726
+ // capacity: 전체 중앙에 usage% 한 개 (2D 와 동일). carrier/abnormal 은 위 세그먼트별 숫자 사용.
727
+ if (gauge.label) {
728
+ ctx.font = font
729
+ ctx.textAlign = 'center'
730
+ ctx.textBaseline = 'middle'
731
+ ctx.fillStyle = '#333333'
732
+ ctx.fillText(gauge.label, canvasW / 2, canvasH / 2)
733
+ }
723
734
 
724
735
  // 텍스처 → Plane
725
736
  const texture = new THREE.CanvasTexture(canvas)
@@ -744,34 +755,45 @@ class StockerRealObject3D extends MCSRealObject3D {
744
755
  this.components3D.add(this.gaugeMesh)
745
756
  }
746
757
 
747
- private resolveGauge(data: any, mode: string) {
758
+ private resolveGauge(
759
+ data: any,
760
+ mode: string
761
+ ): { segments: { ratio: number; color: string }[]; borderColor: string; label: string | null; perSegment: boolean } {
748
762
  switch (mode) {
749
763
  case 'carrier': {
750
- // 세그먼트 비율/색상은 2D 와 공유되는 단일 소스에서 계산 (필드/색상 drift 방지)
751
- const [f, e, ee] = carrierSegmentsFromData(data)
764
+ // 세그먼트 비율/색상은 2D 와 공유되는 단일 소스에서 계산 (필드/색상 drift 방지).
765
+ // 색상은 carrier 컴포넌트와 동일한 legend(테마)에서 취득.
766
+ const colors = carrierColorsFromLegend(resolveCarrierLegend(this.component))
767
+ const [f, e, ee] = carrierSegmentsFromData(data, colors)
752
768
  const total = f.count + e.count + ee.count
753
- const segments = total > 0
754
- ? [f, e, ee].map(s => ({ ratio: s.ratio, color: s.color }))
755
- : [{ ratio: 1, color: '#dddddd' }]
756
- return {
757
- segments,
758
- borderColor: CARRIER_COLORS.full,
759
- label: total > 0 ? `F${f.count} E${e.count} EE${ee.count}` : '',
760
- }
769
+ // 접두사 없는 세그먼트별 숫자(%)로 표기 (capacity·2D 와 동일). label(중앙 한 개) 미사용.
770
+ return total > 0
771
+ ? {
772
+ segments: [f, e, ee].map(s => ({ ratio: s.ratio, color: s.color })),
773
+ borderColor: colors.full,
774
+ label: null,
775
+ perSegment: true,
776
+ }
777
+ : { segments: [{ ratio: 1, color: '#dddddd' }], borderColor: colors.full, label: null, perSegment: false }
761
778
  }
762
779
  case 'abnormal': {
763
780
  const [n, a] = abnormalSegmentsFromData(data)
764
781
  const total = n.count + a.count
765
- const segments = total > 0
766
- ? [n, a].map(s => ({ ratio: s.ratio, color: s.color }))
767
- : [{ ratio: 1, color: '#dddddd' }]
768
- return {
769
- segments,
770
- borderColor: a.count > 0 ? ABNORMAL_COLORS.abnormal : ABNORMAL_COLORS.normal,
771
- label: total > 0 ? `N${n.count} A${a.count}` : '',
772
- }
782
+ return total > 0
783
+ ? {
784
+ segments: [n, a].map(s => ({ ratio: s.ratio, color: s.color })),
785
+ borderColor: a.count > 0 ? ABNORMAL_COLORS.abnormal : ABNORMAL_COLORS.normal,
786
+ label: null,
787
+ perSegment: true,
788
+ }
789
+ : {
790
+ segments: [{ ratio: 1, color: '#dddddd' }],
791
+ borderColor: ABNORMAL_COLORS.normal,
792
+ label: null,
793
+ perSegment: false,
794
+ }
773
795
  }
774
- default: { // capacity
796
+ default: { // capacity — 전체 중앙에 usage% 한 개
775
797
  const cur = Number(data.CURRENTCAPACITY) || 0
776
798
  const max = Number(data.MAXCAPACITY) || 100
777
799
  const fullup = data.FULLUP === 'T'
@@ -782,11 +804,48 @@ class StockerRealObject3D extends MCSRealObject3D {
782
804
  segments: [{ ratio: usage / 100, color }],
783
805
  borderColor: color,
784
806
  label: `${usage}%`,
807
+ perSegment: false,
785
808
  }
786
809
  }
787
810
  }
788
811
  }
789
812
 
813
+ /** 컴포넌트 폰트 설정(fontFamily/bold/italic)으로 CSS font 문자열 조합. size 는 텍스처 스케일 반영값. */
814
+ private gaugeFont(sizePx: number): string {
815
+ const st = (this.component as any).state || {}
816
+ const parts: string[] = []
817
+ if (st.bold) parts.push('bold')
818
+ if (st.italic) parts.push('italic')
819
+ parts.push(`${sizePx}px`)
820
+ parts.push(st.fontFamily || 'sans-serif')
821
+ return parts.join(' ')
822
+ }
823
+
824
+ /** 세그먼트 영역 중심에 숫자(%)만 표기. 좁으면 값% → 값 → 생략 (2D _drawSegmentLabel 과 동일 정책). */
825
+ private drawGaugeSegmentNumber(
826
+ ctx: CanvasRenderingContext2D,
827
+ ratio: number,
828
+ x: number,
829
+ y: number,
830
+ w: number,
831
+ h: number,
832
+ font: string,
833
+ fontSize: number
834
+ ) {
835
+ ctx.font = font
836
+ ctx.textAlign = 'center'
837
+ ctx.textBaseline = 'middle'
838
+ ctx.fillStyle = '#333333'
839
+ const value = `${Math.round(ratio * 100)}`
840
+ const cx = x + w / 2
841
+ const cy = y + h / 2
842
+ if (fontSize <= h && ctx.measureText(`${value}%`).width <= w) {
843
+ ctx.fillText(`${value}%`, cx, cy)
844
+ } else if (fontSize <= h && ctx.measureText(value).width <= w) {
845
+ ctx.fillText(value, cx, cy)
846
+ }
847
+ }
848
+
790
849
  update() {
791
850
  this.clearGauge()
792
851
  super.update()
@@ -12,7 +12,7 @@
12
12
  import * as THREE from 'three'
13
13
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
14
14
  import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js'
15
- import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene'
15
+ import { RealObjectGroup, registerRealObjectFactory, disposeObject3D, MaterialBank } from '@hatiolab/things-scene'
16
16
  import type { Component } from '@hatiolab/things-scene'
17
17
  import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
18
18
 
@@ -77,11 +77,8 @@ export class OHT3D extends RealObjectGroup {
77
77
  const armThickness = Math.min(w, h) * 0.12
78
78
  const bodyCornerR = Math.min(w, h, bodyH) * 0.1
79
79
 
80
- const frameMat = new THREE.MeshStandardMaterial({
81
- color: FRAME_COLOR,
82
- roughness: 0.5,
83
- metalness: 0.4,
84
- })
80
+ // MaterialBank 공유 — dedup + faint 안전(코어 변형 스왑, disposeObject3D 가 __bankOwned 스킵).
81
+ const frameMat = MaterialBank.material('oht|frame', { roughness: 0.5, metalness: 0.4 }, FRAME_COLOR)
85
82
 
86
83
  // ── 1. 하단 그리퍼 바 (캐리어를 집는 가로 바 — 이동 방향과 직교) ──
87
84
  const gripperBarW = w * 0.7
@@ -116,11 +113,7 @@ export class OHT3D extends RealObjectGroup {
116
113
  const bodyBaseY = armBaseY + armH
117
114
  const bodyColor = this.resolveColor()
118
115
  const bodyGeo = new RoundedBoxGeometry(w, bodyH, h, 4, bodyCornerR)
119
- const bodyMat = new THREE.MeshStandardMaterial({
120
- color: bodyColor,
121
- roughness: 0.3,
122
- metalness: 0.5,
123
- })
116
+ const bodyMat = MaterialBank.material('oht|body', { roughness: 0.3, metalness: 0.5 }, bodyColor)
124
117
  const bodyMesh = new THREE.Mesh(bodyGeo, bodyMat)
125
118
  bodyMesh.position.y = bodyBaseY + bodyH / 2
126
119
  bodyMesh.castShadow = true
@@ -131,11 +124,7 @@ export class OHT3D extends RealObjectGroup {
131
124
  const trolleyW = w * 0.6
132
125
  const trolleyD = h * 1.15
133
126
  const trolleyGeo = new THREE.BoxGeometry(trolleyW, trolleyH, trolleyD)
134
- const trolleyMat = new THREE.MeshStandardMaterial({
135
- color: TROLLEY_COLOR,
136
- roughness: 0.6,
137
- metalness: 0.3,
138
- })
127
+ const trolleyMat = MaterialBank.material('oht|trolley', { roughness: 0.6, metalness: 0.3 }, TROLLEY_COLOR)
139
128
  const trolleyMesh = new THREE.Mesh(trolleyGeo, trolleyMat)
140
129
  trolleyMesh.position.y = trolleyBaseY + trolleyH / 2
141
130
  trolleyMesh.castShadow = true
@@ -156,6 +145,9 @@ export class OHT3D extends RealObjectGroup {
156
145
 
157
146
  this._ownMeshes = [gripperMesh, armMesh, bodyMesh, trolleyMesh, ...wheelMeshes]
158
147
  for (const m of this._ownMeshes) this.components3D.add(m)
148
+
149
+ // mesh 는 super.build() 이후 생성 → 코어 faint 를 명시 적용(편집 흐림이 mesh 에 닿게).
150
+ this.updateAlpha()
159
151
  }
160
152
 
161
153
  updateDimension() {}
@@ -172,16 +164,8 @@ export class OHT3D extends RealObjectGroup {
172
164
  this.updateHidden()
173
165
  }
174
166
 
175
- updateAlpha() {
176
- const { alpha = 1 } = this.component.state
177
- for (const m of this._ownMeshes) {
178
- const materials = Array.isArray(m.material) ? m.material : [m.material]
179
- for (const mat of materials) {
180
- ;(mat as THREE.MeshStandardMaterial).opacity = alpha
181
- ;(mat as THREE.MeshStandardMaterial).transparent = alpha < 1
182
- }
183
- }
184
- }
167
+ // updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴.
168
+ // canonical(bank base opacity 1) × state.alpha × faint. view 모드 불변, 편집 모드 faint 추가.
185
169
 
186
170
  onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
187
171
  if ('width' in after || 'height' in after || 'depth' in after ||
@@ -12,7 +12,7 @@
12
12
 
13
13
  import * as THREE from 'three'
14
14
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
15
- import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene'
15
+ import { RealObjectGroup, registerRealObjectFactory, disposeObject3D, MaterialBank } from '@hatiolab/things-scene'
16
16
  import type { Component } from '@hatiolab/things-scene'
17
17
  import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
18
18
 
@@ -68,11 +68,9 @@ export class Shuttle3D extends RealObjectGroup {
68
68
  // ── 1. 하단 베이스 플레이트 (납작하고 넓은 주행부) ──
69
69
  const basePad = Math.min(w, h) * 0.05
70
70
  const baseGeo = new THREE.BoxGeometry(w + basePad * 2, baseH, h + basePad * 2)
71
- const baseMat = new THREE.MeshStandardMaterial({
72
- color: BASE_COLOR,
73
- roughness: 0.7,
74
- metalness: 0.3,
75
- })
71
+ // MaterialBank 공유 — 정적/상태색 dedup + faint 안전(코어가 공유 base 변형 스왑). dispose 는
72
+ // disposeObject3D 가 __bankOwned 를 건너뛰어 안전.
73
+ const baseMat = MaterialBank.material('shuttle|base', { roughness: 0.7, metalness: 0.3 }, BASE_COLOR)
76
74
  const baseMesh = new THREE.Mesh(baseGeo, baseMat)
77
75
  baseMesh.position.y = baseH / 2
78
76
  baseMesh.castShadow = true
@@ -81,11 +79,7 @@ export class Shuttle3D extends RealObjectGroup {
81
79
  // ── 2. 좌우 마스트 기둥 (statusColor 적용) ──
82
80
  const mastBaseY = baseH
83
81
  const bodyColor = this.resolveColor()
84
- const mastMat = new THREE.MeshStandardMaterial({
85
- color: bodyColor,
86
- roughness: 0.4,
87
- metalness: 0.5,
88
- })
82
+ const mastMat = MaterialBank.material('shuttle|mast', { roughness: 0.4, metalness: 0.5 }, bodyColor)
89
83
 
90
84
  const mastGeometries: THREE.BufferGeometry[] = []
91
85
 
@@ -108,11 +102,7 @@ export class Shuttle3D extends RealObjectGroup {
108
102
 
109
103
  // ── 3. 상단 포크 (2개의 평행 암 + 백 플레이트) ──
110
104
  const forkBaseY = mastBaseY + mastH
111
- const forkMat = new THREE.MeshStandardMaterial({
112
- color: FORK_COLOR,
113
- roughness: 0.4,
114
- metalness: 0.6,
115
- })
105
+ const forkMat = MaterialBank.material('shuttle|fork', { roughness: 0.4, metalness: 0.6 }, FORK_COLOR)
116
106
 
117
107
  const forkGeometries: THREE.BufferGeometry[] = []
118
108
 
@@ -139,6 +129,9 @@ export class Shuttle3D extends RealObjectGroup {
139
129
 
140
130
  this._ownMeshes = [baseMesh, mastMesh, forkMesh]
141
131
  for (const m of this._ownMeshes) this.components3D.add(m)
132
+
133
+ // mesh 는 super.build() 이후 생성 → 코어 faint 를 명시 적용(편집 흐림이 mesh 에 닿게).
134
+ this.updateAlpha()
142
135
  }
143
136
 
144
137
  updateDimension() {}
@@ -155,16 +148,9 @@ export class Shuttle3D extends RealObjectGroup {
155
148
  this.updateHidden()
156
149
  }
157
150
 
158
- updateAlpha() {
159
- const { alpha = 1 } = this.component.state
160
- for (const m of this._ownMeshes) {
161
- const materials = Array.isArray(m.material) ? m.material : [m.material]
162
- for (const mat of materials) {
163
- ;(mat as THREE.MeshStandardMaterial).opacity = alpha
164
- ;(mat as THREE.MeshStandardMaterial).transparent = alpha < 1
165
- }
166
- }
167
- }
151
+ // updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴.
152
+ // 코어가 canonical(=bank base, opacity 1) 위에 state.alpha × faint 를 일괄. view 모드는
153
+ // 1×state.alpha = 기존 in-place 결과와 동일 → 불변, 편집 모드만 faint 추가.
168
154
 
169
155
  onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
170
156
  if ('width' in after || 'height' in after || 'depth' in after ||