@fmsim/machine 2.0.0-beta.100 → 2.0.0-beta.104

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.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@fmsim/machine",
3
3
  "description": "Layout View를 구성하는 설비(machine)들과 유닛을 구현한 모듈입니다.",
4
4
  "author": "heartyoh",
5
- "version": "2.0.0-beta.100",
5
+ "version": "2.0.0-beta.104",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
8
8
  "things-scene": true,
@@ -55,5 +55,5 @@
55
55
  "prettier --write"
56
56
  ]
57
57
  },
58
- "gitHead": "332f5d01766a2621d1dc9ca745aa764acfd9c960"
58
+ "gitHead": "9fd007d34fca773bdf6aa56f06f2ff9b189133d8"
59
59
  }
@@ -15,7 +15,7 @@
15
15
 
16
16
  import * as THREE from 'three'
17
17
  import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js'
18
- import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene'
18
+ import { RealObjectGroup, registerRealObjectFactory, disposeObject3D, MaterialBank } from '@hatiolab/things-scene'
19
19
  import type { Component } from '@hatiolab/things-scene'
20
20
  import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
21
21
 
@@ -76,11 +76,8 @@ export class AGV3D extends RealObjectGroup {
76
76
  const bumperPad = Math.min(w, h) * 0.03
77
77
  const bumperCornerR = Math.min(w + bumperPad * 2, h + bumperPad * 2, bumperH) * 0.2
78
78
  const bumperGeo = new RoundedBoxGeometry(w + bumperPad * 2, bumperH, h + bumperPad * 2, 4, bumperCornerR)
79
- const bumperMat = new THREE.MeshStandardMaterial({
80
- color: BUMPER_COLOR,
81
- roughness: 0.8,
82
- metalness: 0.1,
83
- })
79
+ // MaterialBank 공유 — dedup + faint 안전(코어 변형 스왑, disposeObject3D 가 __bankOwned 스킵).
80
+ const bumperMat = MaterialBank.material('agv|bumper', { roughness: 0.8, metalness: 0.1 }, BUMPER_COLOR)
84
81
  const bumper = new THREE.Mesh(bumperGeo, bumperMat)
85
82
  bumper.position.y = bumperH / 2
86
83
  bumper.castShadow = true
@@ -89,11 +86,7 @@ export class AGV3D extends RealObjectGroup {
89
86
  // ── 본체 (둥근 모서리, statusColor 적용) ──
90
87
  const bodyColor = this.resolveColor()
91
88
  const bodyGeo = new RoundedBoxGeometry(w, bodyH, h, 4, cornerR)
92
- const bodyMat = new THREE.MeshStandardMaterial({
93
- color: bodyColor,
94
- roughness: 0.3,
95
- metalness: 0.5,
96
- })
89
+ const bodyMat = MaterialBank.material('agv|body', { roughness: 0.3, metalness: 0.5 }, bodyColor)
97
90
  const body = new THREE.Mesh(bodyGeo, bodyMat)
98
91
  body.position.y = bumperH + bodyH / 2
99
92
  body.castShadow = true
@@ -103,11 +96,7 @@ export class AGV3D extends RealObjectGroup {
103
96
  const platInset = Math.min(w, h) * 0.04
104
97
  const platCornerR = Math.min(w - platInset * 2, h - platInset * 2, platH) * 0.15
105
98
  const platGeo = new RoundedBoxGeometry(w - platInset * 2, platH, h - platInset * 2, 4, platCornerR)
106
- const platMat = new THREE.MeshStandardMaterial({
107
- color: PLATFORM_COLOR,
108
- roughness: 0.5,
109
- metalness: 0.15,
110
- })
99
+ const platMat = MaterialBank.material('agv|platform', { roughness: 0.5, metalness: 0.15 }, PLATFORM_COLOR)
111
100
  const plat = new THREE.Mesh(platGeo, platMat)
112
101
  plat.position.y = bumperH + bodyH + gapH + platH / 2
113
102
  plat.castShadow = true
@@ -116,6 +105,9 @@ export class AGV3D extends RealObjectGroup {
116
105
  this._ownMeshes = [bumper, body, plat]
117
106
  // ownGroup이 center-origin ↔ bottom-up 좌표계 변환을 자동 처리
118
107
  for (const m of this._ownMeshes) this.components3D.add(m)
108
+
109
+ // mesh 는 super.build() 이후 생성 → 코어 faint 를 명시 적용(편집 흐림이 mesh 에 닿게).
110
+ this.updateAlpha()
119
111
  }
120
112
 
121
113
  updateDimension() {}
@@ -134,16 +126,8 @@ export class AGV3D extends RealObjectGroup {
134
126
  this.updateHidden()
135
127
  }
136
128
 
137
- updateAlpha() {
138
- const { alpha = 1 } = this.component.state
139
- for (const m of this._ownMeshes) {
140
- const materials = Array.isArray(m.material) ? m.material : [m.material]
141
- for (const mat of materials) {
142
- ;(mat as THREE.MeshStandardMaterial).opacity = alpha
143
- ;(mat as THREE.MeshStandardMaterial).transparent = alpha < 1
144
- }
145
- }
146
- }
129
+ // updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴.
130
+ // canonical(bank base opacity 1) × state.alpha × faint. view 모드 불변, 편집 모드 faint 추가.
147
131
 
148
132
  onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
149
133
  if ('width' in after || 'height' in after || 'depth' in after ||
@@ -13,7 +13,7 @@
13
13
 
14
14
  import * as THREE from 'three'
15
15
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
16
- import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
16
+ import { RealObjectGroup, registerRealObjectFactory, MaterialBank } from '@hatiolab/things-scene'
17
17
  import type { Component } from '@hatiolab/things-scene'
18
18
  import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
19
19
  import { Z_FMSIM } from './z-priority'
@@ -85,11 +85,8 @@ export class Conveyor3D extends RealObjectGroup {
85
85
  const railW = Math.max(beltWidth * 0.06, 2)
86
86
  const legThickness = Math.max(railW * 0.8, 2)
87
87
 
88
- const frameMaterial = new THREE.MeshStandardMaterial({
89
- color: FRAME_COLOR,
90
- metalness: 0.85,
91
- roughness: 0.35,
92
- })
88
+ // MaterialBank 공유(dedup). 자체 update() 없이 base 생명주기(guarded disposeObject3D) 상속 → 안전.
89
+ const frameMaterial = MaterialBank.material('conveyor|frame', { metalness: 0.85, roughness: 0.35 }, FRAME_COLOR)
93
90
 
94
91
  // --- Frame: side rails + legs + cross-bracing ---
95
92
  // Y 좌표: y=0(바닥) ~ y=depth(상단). 레일 상단이 depth에 위치.
@@ -179,6 +176,9 @@ export class Conveyor3D extends RealObjectGroup {
179
176
  } else {
180
177
  this.buildRollers(width, height, depth, frameH, railW, rollWidth, isVertical)
181
178
  }
179
+
180
+ // mesh 는 super.build() 이후 생성 → 코어 faint 를 명시 적용(편집 흐림이 mesh 에 닿게).
181
+ this.updateAlpha()
182
182
  }
183
183
 
184
184
  private buildRollers(
@@ -231,7 +231,7 @@ export class Conveyor3D extends RealObjectGroup {
231
231
  if (rollerGeometries.length > 0) {
232
232
  const rollerMesh = new THREE.Mesh(
233
233
  BufferGeometryUtils.mergeGeometries(rollerGeometries),
234
- new THREE.MeshStandardMaterial({ color, metalness: 0.9, roughness: 0.2 })
234
+ MaterialBank.material('conveyor|roller', { metalness: 0.9, roughness: 0.2 }, color)
235
235
  )
236
236
  rollerMesh.castShadow = true
237
237
  this.components3D.add(rollerMesh)
@@ -268,18 +268,14 @@ export class Conveyor3D extends RealObjectGroup {
268
268
 
269
269
  const drumMesh = new THREE.Mesh(
270
270
  BufferGeometryUtils.mergeGeometries(drumGeometries),
271
- new THREE.MeshStandardMaterial({ color: ROLLER_COLOR, metalness: 0.9, roughness: 0.2 })
271
+ MaterialBank.material('conveyor|drum', { metalness: 0.9, roughness: 0.2 }, ROLLER_COLOR)
272
272
  )
273
273
  drumMesh.castShadow = true
274
274
  this.components3D.add(drumMesh)
275
275
 
276
276
  // Belt surface
277
277
  const beltThickness = drumRadius * 0.12
278
- const beltMaterial = new THREE.MeshStandardMaterial({
279
- color: color,
280
- metalness: 0.0,
281
- roughness: 0.9,
282
- })
278
+ const beltMaterial = MaterialBank.material('conveyor|belt', { metalness: 0.0, roughness: 0.9 }, color)
283
279
 
284
280
  // Flat belt top surface spanning between drums
285
281
  const flatLen = drum2 - drum1
@@ -347,11 +343,7 @@ export class Conveyor3D extends RealObjectGroup {
347
343
  // 두 레일 간격: 내부 폭의 60%
348
344
  const railSpacing = innerWidth * 0.6
349
345
 
350
- const railMaterial = new THREE.MeshStandardMaterial({
351
- color,
352
- metalness: 0.7,
353
- roughness: 0.3,
354
- })
346
+ const railMaterial = MaterialBank.material('conveyor|rail', { metalness: 0.7, roughness: 0.3 }, color)
355
347
 
356
348
  const railGeometries: THREE.BufferGeometry[] = []
357
349
 
@@ -376,7 +368,8 @@ export class Conveyor3D extends RealObjectGroup {
376
368
  }
377
369
 
378
370
  updateDimension() {}
379
- updateAlpha() {}
371
+ // updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴(faint).
372
+ // view 모드는 factor=1 → build-시 opacity 그대로(불변), 편집 모드만 faint 추가.
380
373
 
381
374
  onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
382
375
  if (
@@ -13,7 +13,7 @@
13
13
 
14
14
  import * as THREE from 'three'
15
15
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
16
- import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
16
+ import { RealObjectGroup, registerRealObjectFactory, MaterialBank } from '@hatiolab/things-scene'
17
17
  import type { Component } from '@hatiolab/things-scene'
18
18
  import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
19
19
 
@@ -84,11 +84,8 @@ export class Crane3D extends RealObjectGroup {
84
84
 
85
85
  const pillarThick = Math.min(w, h) * 0.12
86
86
 
87
- const clampMat = new THREE.MeshStandardMaterial({
88
- color: CLAMP_COLOR,
89
- roughness: 0.5,
90
- metalness: 0.5,
91
- })
87
+ // MaterialBank 공유(dedup). 자체 update() 없이 base 생명주기(guarded disposeObject3D) 상속 → 안전.
88
+ const clampMat = MaterialBank.material('crane|clamp', { roughness: 0.5, metalness: 0.5 }, CLAMP_COLOR)
92
89
 
93
90
  // ── 1. 하단 클램프 (바닥 레일에 물리는 바퀴부) ──
94
91
  const clampW = w * 0.7
@@ -102,11 +99,7 @@ export class Crane3D extends RealObjectGroup {
102
99
  // ── 2. 수직 마스트 (좌우 기둥 + 상·하 가로 연결 바) ──
103
100
  const mastBaseY = clampH
104
101
  const bodyColor = this.resolveColor()
105
- const mastMat = new THREE.MeshStandardMaterial({
106
- color: bodyColor,
107
- roughness: 0.3,
108
- metalness: 0.6,
109
- })
102
+ const mastMat = MaterialBank.material('crane|mast', { roughness: 0.3, metalness: 0.6 }, bodyColor)
110
103
 
111
104
  const mastGeometries: THREE.BufferGeometry[] = []
112
105
 
@@ -137,11 +130,7 @@ export class Crane3D extends RealObjectGroup {
137
130
  // 포크 geometry 는 *_mastBaseY 기준 local 0 에 둠* — 즉 group.position.y = 0
138
131
  // 일 때 mast 바닥 자리에. forkZ 0~100 에 따라 group.position.y 만 변경하면
139
132
  // mast 따라 위/아래 이동. mesh 재구축 불필요.
140
- const forkMat = new THREE.MeshStandardMaterial({
141
- color: FORK_COLOR,
142
- roughness: 0.4,
143
- metalness: 0.5,
144
- })
133
+ const forkMat = MaterialBank.material('crane|fork', { roughness: 0.4, metalness: 0.5 }, FORK_COLOR)
145
134
 
146
135
  const forkGeometries: THREE.BufferGeometry[] = []
147
136
 
@@ -188,10 +177,13 @@ export class Crane3D extends RealObjectGroup {
188
177
  topClamp.position.y = mastBaseY + mastH + clampH / 2
189
178
  topClamp.castShadow = true
190
179
  inner.add(topClamp)
180
+
181
+ // mesh 는 super.build() 이후 생성 → 코어 faint 를 명시 적용(편집 흐림이 mesh 에 닿게).
182
+ this.updateAlpha()
191
183
  }
192
184
 
193
185
  updateDimension() {}
194
- updateAlpha() {}
186
+ // updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴(faint).
195
187
 
196
188
  /** build() 직전 호출. forkGroup 참조를 깨끗이 비워둔다 — 새 build 가 다시 set. */
197
189
  clear() {
@@ -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 } from '@hatiolab/things-scene'
15
+ import { RealObjectGroup, registerRealObjectFactory, 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
  import { Z_FMSIM } from './z-priority'
@@ -46,17 +46,10 @@ export class CraneRail3D extends RealObjectGroup {
46
46
  const railW = Math.max(h * 0.06, 2) // 레일 폭
47
47
  const supportThick = Math.max(Math.min(w, h) * 0.05, 2)
48
48
 
49
- const railMat = new THREE.MeshStandardMaterial({
50
- color: RAIL_COLOR,
51
- roughness: 0.3,
52
- metalness: 0.7,
53
- })
54
-
55
- const supportMat = new THREE.MeshStandardMaterial({
56
- color: SUPPORT_COLOR,
57
- roughness: 0.5,
58
- metalness: 0.5,
59
- })
49
+ // MaterialBank 공유(정적 색 dedup). 자체 update() 없이 base 생명주기(guarded disposeObject3D)
50
+ // 상속 → 공유 base dispose 안전.
51
+ const railMat = MaterialBank.material('crane-rail|rail', { roughness: 0.3, metalness: 0.7 }, RAIL_COLOR)
52
+ const supportMat = MaterialBank.material('crane-rail|support', { roughness: 0.5, metalness: 0.5 }, SUPPORT_COLOR)
60
53
 
61
54
  // ── 바닥 레일 (2개 평행 트랙) ──
62
55
  const bottomGeometries: THREE.BufferGeometry[] = []
@@ -98,10 +91,13 @@ export class CraneRail3D extends RealObjectGroup {
98
91
  const supportMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(supportGeometries), supportMat)
99
92
  supportMesh.castShadow = true
100
93
  inner.add(supportMesh)
94
+
95
+ // mesh 는 super.build() 이후 생성 → 코어 faint 를 명시 적용(편집 흐림이 mesh 에 닿게).
96
+ this.updateAlpha()
101
97
  }
102
98
 
103
99
  updateDimension() {}
104
- updateAlpha() {}
100
+ // updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴(faint).
105
101
 
106
102
  onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
107
103
  if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) {
@@ -10,7 +10,7 @@
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
15
  import { LEGEND_CAPACITY, resolveCarrierLegend } from '../features/mcs-status-default'
16
16
  import { getValueOnRanges } from '../utils/get-value-on-ranges'
@@ -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 ──
@@ -468,22 +480,10 @@ class MCSRealObject3D extends RealObjectGroup {
468
480
  // RealObjectGroup 기본 동작 비활성화
469
481
  updateDimension() {}
470
482
 
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
- }
483
+ // updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴.
484
+ // 코어가 canonical(=build 시 config.opacity 로 만든 material) 위에 factor = state.alpha × faint 를
485
+ // 공유 변형 스왑으로 일괄 적용한다(원본 base 불변). 기존 in-place 변이는 (1) faint 미적용,
486
+ // (2) 코어 applyFocusDim 과 이중 적용 소지가 있어 방언이었다. config.opacity 는 canonical 로 보존.
487
487
  }
488
488
 
489
489
  // ── 헬퍼 ────────────────────────────────────────────────
@@ -573,11 +573,9 @@ class OHTLineRealObject3D extends RealObjectGroup {
573
573
  const webH = beamH - flangeH * 2 // 웹 높이
574
574
  const webW = Math.max(flangeW * 0.25, 1) // 웹 두께
575
575
 
576
- const mat = new THREE.MeshStandardMaterial({
577
- color: OHT_LINE_RAIL_COLOR,
578
- metalness: 0.5,
579
- roughness: 0.4,
580
- })
576
+ // 공유 base(MaterialBank) — OHTLine rail 은 정적 색이라 전 인스턴스 dedup. faint 는 코어가
577
+ // 공유 base 의 변형을 스왑(원본 불변). dispose 는 bank-safe(update 에서 material.dispose 안 함).
578
+ const mat = MaterialBank.material('oht-line|rail', { metalness: 0.5, roughness: 0.4 }, OHT_LINE_RAIL_COLOR)
581
579
 
582
580
  const geoms: THREE.BufferGeometry[] = []
583
581
 
@@ -605,13 +603,16 @@ class OHTLineRealObject3D extends RealObjectGroup {
605
603
  this._mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(geoms), mat)
606
604
  this._mesh.castShadow = true
607
605
  this.object3d.add(this._mesh)
606
+
607
+ // mesh 가 super.build() 이후 생성 → 코어 faint 를 명시 적용(편집 흐림이 mesh 에 닿게).
608
+ this.updateAlpha()
608
609
  }
609
610
 
610
611
  update() {
611
612
  if (this._mesh) {
612
613
  this.object3d.remove(this._mesh)
613
614
  this._mesh.geometry.dispose()
614
- ;(this._mesh.material as THREE.Material).dispose()
615
+ // material 은 MaterialBank 공유(앱 수명) — 여기서 dispose 하면 다른 인스턴스가 깨진다.
615
616
  this._mesh = undefined
616
617
  }
617
618
  this.build()
@@ -628,8 +629,8 @@ class OHTLineRealObject3D extends RealObjectGroup {
628
629
  super.onchange(after, before)
629
630
  }
630
631
 
632
+ // updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴(faint).
631
633
  updateDimension() {}
632
- updateAlpha() {}
633
634
  }
634
635
 
635
636
  registerRealObjectFactory('OHTLine', (component: Component) => new OHTLineRealObject3D(component))
@@ -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 ||
@@ -84,32 +84,13 @@ export default class MCSMachine extends MCSStatusMixin(ContainerAbstract) {
84
84
  }
85
85
 
86
86
  /**
87
- * In 2D, ignore `state.alpha`; preserve everything else.
88
- *
89
- * things-scene's base prerender (component-drawing.ts:31, 44) does:
90
- * const fade = (alpha || 1) - this._delta.fade
91
- * if (fade) context.globalAlpha = fade
92
- *
93
- * It *overwrites* `globalAlpha` (not multiplies), so the inherited
94
- * parent alpha is also lost — that's a base-layer quirk we match the
95
- * recovery for. Then at line 48 it applies editMode+hidden dimming
96
- * with a *= 0.5.
97
- *
98
- * Our recovery:
99
- * 1. Capture the inherited `globalAlpha` *before* super runs.
100
- * 2. Let super do its work (transforms / shadow / dim flags).
101
- * 3. Restore globalAlpha to inherited × editMode-hidden dim factor —
102
- * i.e., what super would have produced if our state.alpha were 1.
103
- *
104
- * 3D path is unaffected: 3D factories read `state.alpha` directly via
105
- * their `updateAlpha()` implementations and continue to honor it (with
106
- * per-config opacity fallback).
87
+ * 2D 에서는 `state.alpha`(3D material 전용)를 무시한다. hidden/모델링 dim 흐림은 **코어(draw)가
88
+ * 일괄 처리**하므로 여기서 손대지 않는다 (component-drawing.ts draw 가 prerender 직후 faint 적용).
89
+ * fade 애니메이션은 보존. 3D 는 factory `updateAlpha()` 가 state.alpha 를 별도로 존중.
107
90
  */
108
91
  prerender(ctx: any) {
109
- const inherited = ctx.globalAlpha
110
92
  super.prerender(ctx)
111
- const dim = this.app.isEditMode && this.hidden ? 0.5 : 1
112
- ctx.globalAlpha = inherited * dim
93
+ ctx.globalAlpha = 1 - this._delta.fade
113
94
  }
114
95
 
115
96
  render(ctx) {
package/src/mcs-unit.ts CHANGED
@@ -62,14 +62,11 @@ export default class MCSUnit extends MCSStatusMixin(ParentObjectMixin(ContainerA
62
62
  }
63
63
 
64
64
  /**
65
- * In 2D, ignore `state.alpha`; preserve inherited alpha + editMode-hidden dim.
66
- * See MCSMachine.prerender for the full rationale.
65
+ * 2D 에서는 state.alpha 무시. hidden/모델링 dim 흐림은 코어(draw)가 일괄 처리 (MCSMachine 동일).
67
66
  */
68
67
  prerender(ctx: any) {
69
- const inherited = ctx.globalAlpha
70
68
  super.prerender(ctx)
71
- const dim = this.app.isEditMode && this.hidden ? 0.5 : 1
72
- ctx.globalAlpha = inherited * dim
69
+ ctx.globalAlpha = 1 - this._delta.fade
73
70
  }
74
71
 
75
72
  contains(x, y) {
@@ -217,6 +217,7 @@ Object.defineProperty(ModelLayer.prototype, 'nature', {
217
217
  type: 'legend',
218
218
  label: '',
219
219
  name: 'MOOD',
220
+ requires3d: true,
220
221
  property: {
221
222
  label: 'MOOD'
222
223
  }
@@ -225,6 +226,7 @@ Object.defineProperty(ModelLayer.prototype, 'nature', {
225
226
  type: 'select',
226
227
  label: 'light-preset',
227
228
  name: 'lightPreset',
229
+ requires3d: true,
228
230
  property: {
229
231
  options: ['dark', 'medium', 'bright']
230
232
  }