@fmsim/machine 2.0.0-beta.5 → 2.0.0-beta.52
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/dist/agv.js +2 -2
- package/dist/agv.js.map +1 -1
- package/dist/carrier-manager.js +93 -0
- package/dist/carrier-manager.js.map +1 -0
- package/dist/carrier.js +51 -6
- package/dist/carrier.js.map +1 -1
- package/dist/conveyor-join.js +8 -2
- package/dist/conveyor-join.js.map +1 -1
- package/dist/conveyor.js +8 -0
- package/dist/conveyor.js.map +1 -1
- package/dist/crane.js.map +1 -1
- package/dist/factories/agv-3d.js +36 -8
- package/dist/factories/agv-3d.js.map +1 -1
- package/dist/factories/conveyor-3d.js +159 -69
- package/dist/factories/conveyor-3d.js.map +1 -1
- package/dist/factories/machine-3d.js +449 -62
- package/dist/factories/machine-3d.js.map +1 -1
- package/dist/factories/oht-3d.js +50 -24
- package/dist/factories/oht-3d.js.map +1 -1
- package/dist/factories/shuttle-3d.js +32 -6
- package/dist/factories/shuttle-3d.js.map +1 -1
- package/dist/index.js +50 -34
- package/dist/index.js.map +1 -1
- package/dist/mcs-carrier-holder.js +39 -22
- package/dist/mcs-carrier-holder.js.map +1 -1
- package/dist/mcs-vehicle.js +8 -3
- package/dist/mcs-vehicle.js.map +1 -1
- package/dist/node.js +1 -1
- package/dist/node.js.map +1 -1
- package/dist/oht.js.map +1 -1
- package/dist/scanner.js +30 -0
- package/dist/scanner.js.map +1 -0
- package/dist/scene/root-container-override.js +9 -0
- package/dist/scene/root-container-override.js.map +1 -1
- package/dist/stocker-capacity-bar.js +2 -2
- package/dist/stocker-capacity-bar.js.map +1 -1
- package/dist/templates/index.js +2 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/scanner.js +20 -0
- package/dist/templates/scanner.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/get-value-on-ranges.js +2 -4
- package/dist/utils/get-value-on-ranges.js.map +1 -1
- package/dist/zone-capacity-bar.js +2 -2
- package/dist/zone-capacity-bar.js.map +1 -1
- package/jest.config.js +10 -0
- package/package.json +4 -3
- package/src/agv.ts +2 -2
- package/src/carrier-manager.ts +117 -0
- package/src/carrier.ts +50 -4
- package/src/conveyor-join.ts +5 -2
- package/src/conveyor.ts +8 -0
- package/src/crane.ts +1 -1
- package/src/factories/agv-3d.ts +36 -8
- package/src/factories/conveyor-3d.ts +161 -71
- package/src/factories/machine-3d.ts +535 -56
- package/src/factories/oht-3d.ts +48 -24
- package/src/factories/shuttle-3d.ts +30 -6
- package/src/index.ts +56 -34
- package/src/mcs-carrier-holder.ts +38 -34
- package/src/mcs-vehicle.ts +9 -6
- package/src/node.ts +1 -1
- package/src/oht.ts +1 -1
- package/src/scanner.ts +29 -0
- package/src/scene/root-container-override.ts +12 -0
- package/src/stocker-capacity-bar.ts +2 -2
- package/src/templates/index.ts +2 -0
- package/src/templates/scanner.ts +20 -0
- package/src/utils/get-value-on-ranges.ts +2 -4
- package/src/zone-capacity-bar.ts +2 -2
- package/translations/en.json +2 -1
- package/translations/ja.json +2 -1
- package/translations/ko.json +2 -1
- package/translations/ms.json +2 -1
- package/translations/zh.json +2 -1
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import * as THREE from 'three'
|
|
12
|
-
import
|
|
12
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
13
|
+
import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene'
|
|
13
14
|
import type { Component } from '@hatiolab/things-scene'
|
|
14
15
|
import { LEGEND_CAPACITY } from '../features/mcs-status-default'
|
|
15
|
-
import {
|
|
16
|
+
import { getValueOnRanges } from '../utils/get-value-on-ranges'
|
|
16
17
|
|
|
17
18
|
// ── 설정 타입 ────────────────────────────────────────────
|
|
18
19
|
|
|
@@ -30,11 +31,23 @@ interface MCS3DConfig {
|
|
|
30
31
|
/** statusColor 미사용 시 기본 색상 */
|
|
31
32
|
defaultColor: number
|
|
32
33
|
/** 'box' | 'hexagon' */
|
|
33
|
-
shape: 'box' | 'hexagon'
|
|
34
|
+
shape: 'box' | 'hexagon' | 'plane'
|
|
34
35
|
/** mesh 생성 전 가시성 판정 — false 반환 시 mesh 미생성 */
|
|
35
36
|
isVisible?: (component: Component) => boolean
|
|
36
37
|
/** 부모 대비 크기 비율 (기본 1.0). < 1이면 부모 중심에 배치. */
|
|
37
38
|
sizeRatio?: number
|
|
39
|
+
/** 상부 면 제거 (5면 개방 박스) */
|
|
40
|
+
openTop?: boolean
|
|
41
|
+
/** 상판 무반사 (MeshBasicMaterial) */
|
|
42
|
+
matteTop?: boolean
|
|
43
|
+
/** 상판에 2D 렌더링 텍스처 적용 */
|
|
44
|
+
texturedTop?: boolean
|
|
45
|
+
/** 자체 발광 강도 (0~1). 조명 약한 면에서도 색상 유지 */
|
|
46
|
+
emissiveIntensity?: number
|
|
47
|
+
/** 바닥면 유지 (기본 false — 바닥면 제거) */
|
|
48
|
+
keepBottomFace?: boolean
|
|
49
|
+
/** syncZPosOffset 고정값 (설정 시 다른 로직 무시) */
|
|
50
|
+
zPosOverride?: number
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
/**
|
|
@@ -78,14 +91,22 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
protected get syncZPosOffset(): number {
|
|
94
|
+
// zPosOverride: 고정 높이 (OHTLine 등)
|
|
95
|
+
if (this._config.zPosOverride != null) {
|
|
96
|
+
return this._config.zPosOverride
|
|
97
|
+
}
|
|
81
98
|
// Port: 이송 표면 높이에 배치
|
|
82
99
|
if (this._config.defaultZPosFromParent) {
|
|
83
100
|
return TRANSPORT_SURFACE_HEIGHT
|
|
84
101
|
}
|
|
85
|
-
// Carrier:
|
|
102
|
+
// Carrier: 부모 유형에 따라 배치 위치 결정
|
|
86
103
|
if (this._config.sizeRatio && this._config.sizeRatio < 1) {
|
|
87
104
|
const parentType = this.component.parent?.state?.type
|
|
88
|
-
|
|
105
|
+
// OHT: 그리퍼 아래에 매달림 (캐리어 상단이 OHT 하단에 붙음)
|
|
106
|
+
if (parentType === 'OHT') {
|
|
107
|
+
return -this.effectiveDepth
|
|
108
|
+
}
|
|
109
|
+
if (parentType === 'Port' || parentType === 'Shelf' || parentType === 'Shuttle') {
|
|
89
110
|
return 0
|
|
90
111
|
}
|
|
91
112
|
return TRANSPORT_SURFACE_HEIGHT
|
|
@@ -94,21 +115,21 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
94
115
|
}
|
|
95
116
|
|
|
96
117
|
// 컴포넌트의 상태 색상(statusColor) 또는 fillStyle → Three.js 색상 변환
|
|
97
|
-
|
|
118
|
+
protected resolveColor(): number {
|
|
98
119
|
const comp = this.component as any
|
|
99
120
|
|
|
100
121
|
// 1) MCSStatusMixin.statusColor (legend 기반 상태 색상)
|
|
101
122
|
// '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
|
|
102
123
|
try {
|
|
103
124
|
const sc = comp.statusColor
|
|
104
|
-
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
|
|
125
|
+
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()
|
|
105
126
|
} catch {
|
|
106
127
|
/* statusColor 미구현 시 무시 */
|
|
107
128
|
}
|
|
108
129
|
|
|
109
130
|
// 2) state.fillStyle fallback
|
|
110
131
|
const { fillStyle } = this.component.state
|
|
111
|
-
if (fillStyle && typeof fillStyle === 'string') {
|
|
132
|
+
if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
|
|
112
133
|
try {
|
|
113
134
|
return new THREE.Color(fillStyle).getHex()
|
|
114
135
|
} catch {
|
|
@@ -157,7 +178,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
157
178
|
|
|
158
179
|
private buildBoxGeometry(w: number, d: number, h: number): THREE.BufferGeometry {
|
|
159
180
|
const geo = new THREE.BoxGeometry(w, d, h)
|
|
160
|
-
return this.removeBottomFace(geo)
|
|
181
|
+
return this._config.keepBottomFace ? geo : this.removeBottomFace(geo)
|
|
161
182
|
}
|
|
162
183
|
|
|
163
184
|
private buildHexagonGeometry(w: number, d: number, h: number): THREE.BufferGeometry {
|
|
@@ -188,8 +209,16 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
188
209
|
return this.removeBottomFace(geo)
|
|
189
210
|
}
|
|
190
211
|
|
|
212
|
+
private buildPlaneGeometry(w: number, h: number): THREE.BufferGeometry {
|
|
213
|
+
const geo = new THREE.PlaneGeometry(w, h)
|
|
214
|
+
geo.rotateX(-Math.PI / 2)
|
|
215
|
+
return geo
|
|
216
|
+
}
|
|
217
|
+
|
|
191
218
|
// ── Build ──
|
|
192
219
|
|
|
220
|
+
protected _ownMesh?: THREE.Mesh
|
|
221
|
+
|
|
193
222
|
build() {
|
|
194
223
|
super.build()
|
|
195
224
|
|
|
@@ -209,34 +238,64 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
209
238
|
const { opacity, metalness, roughness, shape } = this._config
|
|
210
239
|
|
|
211
240
|
const geometry =
|
|
212
|
-
shape === '
|
|
213
|
-
? this.
|
|
214
|
-
:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
241
|
+
shape === 'plane'
|
|
242
|
+
? this.buildPlaneGeometry(w, h)
|
|
243
|
+
: shape === 'hexagon'
|
|
244
|
+
? this.buildHexagonGeometry(w, depth, h)
|
|
245
|
+
: this.buildBoxGeometry(w, depth, h)
|
|
246
|
+
|
|
247
|
+
const isPlane = shape === 'plane'
|
|
248
|
+
const material = isPlane
|
|
249
|
+
? new THREE.MeshBasicMaterial({
|
|
250
|
+
color,
|
|
251
|
+
opacity,
|
|
252
|
+
transparent: opacity < 1,
|
|
253
|
+
side: THREE.DoubleSide,
|
|
254
|
+
})
|
|
255
|
+
: new THREE.MeshStandardMaterial({
|
|
256
|
+
color,
|
|
257
|
+
metalness,
|
|
258
|
+
roughness,
|
|
259
|
+
opacity,
|
|
260
|
+
transparent: opacity < 1,
|
|
261
|
+
side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide,
|
|
262
|
+
...(this._config.emissiveIntensity ? { emissive: color, emissiveIntensity: this._config.emissiveIntensity } : {}),
|
|
263
|
+
})
|
|
225
264
|
// z-fighting 방지: 바닥면(floor grid)보다 항상 앞에 렌더링
|
|
226
265
|
material.polygonOffset = true
|
|
227
266
|
material.polygonOffsetFactor = -1
|
|
228
267
|
material.polygonOffsetUnit = -1
|
|
229
268
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
269
|
+
// BoxGeometry material groups: +x(0), -x(1), +y(2), -y(3), +z(4), -z(5)
|
|
270
|
+
let meshMaterial: THREE.Material | THREE.Material[] = material
|
|
271
|
+
if (shape === 'box') {
|
|
272
|
+
if (this._config.openTop) {
|
|
273
|
+
// 상단면 제거 → 5면 개방 박스
|
|
274
|
+
const invisible = new THREE.MeshBasicMaterial({ visible: false })
|
|
275
|
+
meshMaterial = [material, material, invisible, material, material, material]
|
|
276
|
+
} else if (this._config.texturedTop) {
|
|
277
|
+
// 상판에 2D 렌더링 텍스처
|
|
278
|
+
const topMat = this.buildTopTexture(w, h)
|
|
279
|
+
meshMaterial = [material, material, topMat, material, material, material]
|
|
280
|
+
} else if (this._config.matteTop) {
|
|
281
|
+
// 상판만 무반사
|
|
282
|
+
const matte = new THREE.MeshBasicMaterial({ color, opacity, transparent: opacity < 1 })
|
|
283
|
+
meshMaterial = [material, material, matte, material, material, material]
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this._ownMesh = new THREE.Mesh(geometry, meshMaterial)
|
|
288
|
+
// sizeRatio < 1이면 부모 중심에 배치 (rawDepth/2), plane은 살짝 띄움
|
|
289
|
+
this._ownMesh.position.y = isPlane ? 1 : ratio < 1 ? rawDepth / 2 : depth / 2
|
|
290
|
+
this._ownMesh.castShadow = !isPlane
|
|
291
|
+
this._ownMesh.receiveShadow = true
|
|
292
|
+
this.object3d.add(this._ownMesh)
|
|
236
293
|
}
|
|
237
294
|
|
|
238
295
|
// ── Change handling ──
|
|
239
296
|
|
|
297
|
+
private _updatePending = false
|
|
298
|
+
|
|
240
299
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
241
300
|
if (
|
|
242
301
|
'width' in after ||
|
|
@@ -244,18 +303,117 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
244
303
|
'depth' in after ||
|
|
245
304
|
'fillStyle' in after ||
|
|
246
305
|
'strokeStyle' in after ||
|
|
247
|
-
'id' in after
|
|
306
|
+
'id' in after ||
|
|
307
|
+
'EMPTYTYPE' in after ||
|
|
308
|
+
'CARRIERSTATUS' in after
|
|
248
309
|
) {
|
|
249
|
-
|
|
310
|
+
// 연속 setState에 의한 다중 rebuild 방지: 한 프레임에 한 번만 실행
|
|
311
|
+
if (!this._updatePending) {
|
|
312
|
+
this._updatePending = true
|
|
313
|
+
queueMicrotask(() => {
|
|
314
|
+
this._updatePending = false
|
|
315
|
+
this.update()
|
|
316
|
+
})
|
|
317
|
+
}
|
|
250
318
|
return
|
|
251
319
|
}
|
|
252
320
|
|
|
253
321
|
super.onchange(after, before)
|
|
254
322
|
}
|
|
255
323
|
|
|
256
|
-
|
|
324
|
+
/**
|
|
325
|
+
* 상판 텍스처: 컴포넌트의 2D render()를 캔버스에 그려서 텍스처로 사용
|
|
326
|
+
*/
|
|
327
|
+
protected buildTopTexture(w: number, h: number): THREE.MeshBasicMaterial {
|
|
328
|
+
const scale = 4
|
|
329
|
+
const cw = this.nextPow2(w * scale)
|
|
330
|
+
const ch = this.nextPow2(h * scale)
|
|
331
|
+
const canvas = document.createElement('canvas')
|
|
332
|
+
canvas.width = cw
|
|
333
|
+
canvas.height = ch
|
|
334
|
+
const ctx = canvas.getContext('2d')!
|
|
335
|
+
|
|
336
|
+
const statusColor = '#' + this.resolveColor().toString(16).padStart(6, '0')
|
|
337
|
+
const emptyType = (this.component.state as any)?.EMPTYTYPE || ''
|
|
338
|
+
const text = emptyType === 'FULL' ? 'F' : emptyType === 'EMPTY' ? 'E' : emptyType === 'EMPTYEMPTY' ? 'X' : ''
|
|
339
|
+
|
|
340
|
+
// 실제 사용 영역 (nextPow2 이전 크기)
|
|
341
|
+
const usedW = w * scale
|
|
342
|
+
const usedH = h * scale
|
|
343
|
+
|
|
344
|
+
// 바탕: 캐리어 상태 색상 (사용 영역만)
|
|
345
|
+
ctx.fillStyle = statusColor
|
|
346
|
+
ctx.fillRect(0, 0, usedW, usedH)
|
|
347
|
+
|
|
348
|
+
// 텍스트 (사용 영역 중앙)
|
|
349
|
+
if (text) {
|
|
350
|
+
const fontSize = Math.round(Math.min(usedW, usedH) * 0.7)
|
|
351
|
+
ctx.font = `bold ${fontSize}px Arial`
|
|
352
|
+
ctx.textAlign = 'center'
|
|
353
|
+
ctx.textBaseline = 'middle'
|
|
354
|
+
ctx.fillStyle = '#000000'
|
|
355
|
+
ctx.fillText(text, usedW / 2, usedH / 2)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
359
|
+
texture.offset.set(0, 1 - usedH / ch)
|
|
360
|
+
texture.repeat.set(usedW / cw, usedH / ch)
|
|
361
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
362
|
+
|
|
363
|
+
return new THREE.MeshBasicMaterial({
|
|
364
|
+
map: texture,
|
|
365
|
+
transparent: true,
|
|
366
|
+
side: THREE.DoubleSide,
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private nextPow2(n: number): number {
|
|
371
|
+
if (!(n > 0)) return 1
|
|
372
|
+
let v = Math.ceil(n)
|
|
373
|
+
v--
|
|
374
|
+
v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16
|
|
375
|
+
return v + 1
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 자신의 mesh만 제거 후 rebuild.
|
|
380
|
+
* 기본 update()는 clear()로 object3d의 모든 children을 제거하는데,
|
|
381
|
+
* 컨테이너(Port 등)의 경우 자식 컴포넌트(Carrier 등)의 3D 오브젝트도 함께 제거됨.
|
|
382
|
+
* 이를 방지하기 위해 자신이 생성한 mesh만 제거한다.
|
|
383
|
+
*/
|
|
384
|
+
update() {
|
|
385
|
+
// 자신의 mesh만 제거 (자식 컴포넌트 3D 오브젝트 보존)
|
|
386
|
+
if (this._ownMesh) {
|
|
387
|
+
this.object3d.remove(this._ownMesh)
|
|
388
|
+
disposeObject3D(this._ownMesh)
|
|
389
|
+
this._ownMesh = undefined
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
this.build()
|
|
393
|
+
this.updateTransform()
|
|
394
|
+
this.updateAlpha()
|
|
395
|
+
this.updateHidden()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// RealObjectGroup 기본 동작 비활성화
|
|
257
399
|
updateDimension() {}
|
|
258
|
-
|
|
400
|
+
|
|
401
|
+
updateAlpha() {
|
|
402
|
+
const state = this.component.state
|
|
403
|
+
// alpha가 명시적으로 설정되어 있으면 그 값을 사용, 없으면 config 기본값 적용
|
|
404
|
+
const effective = ('alpha' in state) ? (state.alpha as number) : this._config.opacity
|
|
405
|
+
|
|
406
|
+
this.object3d.traverse((o: any) => {
|
|
407
|
+
if (!o.material) return
|
|
408
|
+
const materials = Array.isArray(o.material) ? o.material : [o.material]
|
|
409
|
+
for (const m of materials) {
|
|
410
|
+
if (!m.visible) continue // openTop invisible face 무시
|
|
411
|
+
if (m.map) continue // 텍스처 material은 자체 transparent 유지
|
|
412
|
+
m.opacity = effective
|
|
413
|
+
m.transparent = effective < 1
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
}
|
|
259
417
|
}
|
|
260
418
|
|
|
261
419
|
// ── 팩토리 등록 헬퍼 ─────────────────────────────────────
|
|
@@ -287,18 +445,106 @@ register('Buffer', {
|
|
|
287
445
|
})
|
|
288
446
|
|
|
289
447
|
register('AGVLine', {
|
|
290
|
-
depthRatio: 0
|
|
448
|
+
depthRatio: 0,
|
|
449
|
+
defaultDepth: 0.1,
|
|
291
450
|
metalness: 0.6,
|
|
292
451
|
roughness: 0.3,
|
|
293
452
|
defaultColor: 0x888899,
|
|
294
453
|
})
|
|
295
454
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
455
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
456
|
+
// OHTLine — 천장 I-beam 레일
|
|
457
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
458
|
+
|
|
459
|
+
const OHT_LINE_RAIL_COLOR = 0xcccccc
|
|
460
|
+
const OHT_LINE_HEIGHT = 7 * TRANSPORT_SURFACE_HEIGHT // OHT 상단과 일치
|
|
461
|
+
|
|
462
|
+
class OHTLineRealObject3D extends RealObjectGroup {
|
|
463
|
+
private _mesh?: THREE.Mesh
|
|
464
|
+
|
|
465
|
+
get effectiveDepth(): number { return 0 }
|
|
466
|
+
protected get syncZPosOffset(): number { return OHT_LINE_HEIGHT }
|
|
467
|
+
|
|
468
|
+
build() {
|
|
469
|
+
super.build()
|
|
470
|
+
|
|
471
|
+
const { width = 10, height = 10 } = this.component.state
|
|
472
|
+
const w = Math.abs(width)
|
|
473
|
+
const h = Math.abs(height)
|
|
474
|
+
if (!w || !h) return
|
|
475
|
+
|
|
476
|
+
// I-beam 치수: 이송 방향 = 긴 변, 폭은 2D의 1/2
|
|
477
|
+
const isVertical = w < h
|
|
478
|
+
const shortSide = isVertical ? w : h // 2D 원본 짧은 쪽 폭
|
|
479
|
+
const beltW = shortSide * 0.25 // I-beam 폭: 원본의 1/4
|
|
480
|
+
const beamH = Math.max(beltW * 0.5, 3) // I-beam 전체 높이
|
|
481
|
+
const inset = shortSide * 0.5 // 양끝 여백: 원본 폭의 1/2
|
|
482
|
+
const railLen = (isVertical ? h : w) - inset * 2
|
|
483
|
+
const flangeH = beamH * 0.2 // 플랜지 두께
|
|
484
|
+
const flangeW = beltW * 0.8 // 플랜지 폭
|
|
485
|
+
const webH = beamH - flangeH * 2 // 웹 높이
|
|
486
|
+
const webW = Math.max(flangeW * 0.25, 1) // 웹 두께
|
|
487
|
+
|
|
488
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
489
|
+
color: OHT_LINE_RAIL_COLOR,
|
|
490
|
+
metalness: 0.5,
|
|
491
|
+
roughness: 0.4,
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
const geoms: THREE.BufferGeometry[] = []
|
|
495
|
+
|
|
496
|
+
// 상부 플랜지
|
|
497
|
+
const topFlange = isVertical
|
|
498
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
499
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
500
|
+
topFlange.translate(0, -flangeH / 2, 0)
|
|
501
|
+
geoms.push(topFlange)
|
|
502
|
+
|
|
503
|
+
// 웹 (수직판)
|
|
504
|
+
const web = isVertical
|
|
505
|
+
? new THREE.BoxGeometry(webW, webH, railLen)
|
|
506
|
+
: new THREE.BoxGeometry(railLen, webH, webW)
|
|
507
|
+
web.translate(0, -flangeH - webH / 2, 0)
|
|
508
|
+
geoms.push(web)
|
|
509
|
+
|
|
510
|
+
// 하부 플랜지
|
|
511
|
+
const botFlange = isVertical
|
|
512
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
513
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
514
|
+
botFlange.translate(0, -flangeH - webH - flangeH / 2, 0)
|
|
515
|
+
geoms.push(botFlange)
|
|
516
|
+
|
|
517
|
+
this._mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(geoms), mat)
|
|
518
|
+
this._mesh.castShadow = true
|
|
519
|
+
this.object3d.add(this._mesh)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
update() {
|
|
523
|
+
if (this._mesh) {
|
|
524
|
+
this.object3d.remove(this._mesh)
|
|
525
|
+
this._mesh.geometry.dispose()
|
|
526
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
527
|
+
this._mesh = undefined
|
|
528
|
+
}
|
|
529
|
+
this.build()
|
|
530
|
+
this.updateTransform()
|
|
531
|
+
this.updateHidden()
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
535
|
+
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
536
|
+
'fillStyle' in after || 'strokeStyle' in after) {
|
|
537
|
+
this.update()
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
super.onchange(after, before)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
updateDimension() {}
|
|
544
|
+
updateAlpha() {}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
registerRealObjectFactory('OHTLine', (component: Component) => new OHTLineRealObject3D(component))
|
|
302
548
|
|
|
303
549
|
// STOCKER: 벽면에 용량 게이지 바를 포함하는 커스텀 3D 오브젝트
|
|
304
550
|
const STOCKER_CONFIG: MCS3DConfig = {
|
|
@@ -442,7 +688,7 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
442
688
|
const fullup = data.FULLUP === 'T'
|
|
443
689
|
const usage = Math.round((cur / max) * 100)
|
|
444
690
|
const legend = (this.component as any).root?.style?.LEGEND_CAPACITY || LEGEND_CAPACITY
|
|
445
|
-
const color = fullup ? 'red' : (
|
|
691
|
+
const color = fullup ? 'red' : (getValueOnRanges(usage, legend) || '#44aa44')
|
|
446
692
|
return {
|
|
447
693
|
segments: [{ ratio: usage / 100, color }],
|
|
448
694
|
borderColor: color,
|
|
@@ -452,6 +698,11 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
452
698
|
}
|
|
453
699
|
}
|
|
454
700
|
|
|
701
|
+
update() {
|
|
702
|
+
this.clearGauge()
|
|
703
|
+
super.update()
|
|
704
|
+
}
|
|
705
|
+
|
|
455
706
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
456
707
|
if ('data' in after) {
|
|
457
708
|
this.clearGauge()
|
|
@@ -483,14 +734,18 @@ registerRealObjectFactory('STOCKER', (component: Component) => new StockerRealOb
|
|
|
483
734
|
// Unit (이동체 · 홀더) — 낮은 depth, 불투명
|
|
484
735
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
485
736
|
|
|
486
|
-
|
|
487
|
-
|
|
737
|
+
// Port: 바닥판 (PlaneGeometry)
|
|
738
|
+
const PORT_CONFIG: MCS3DConfig = {
|
|
739
|
+
...DEFAULTS,
|
|
740
|
+
depthRatio: 0.01,
|
|
488
741
|
defaultZPosFromParent: true,
|
|
489
|
-
opacity: 0.
|
|
490
|
-
metalness: 0.
|
|
491
|
-
roughness: 0.
|
|
742
|
+
opacity: 0.1,
|
|
743
|
+
metalness: 0.3,
|
|
744
|
+
roughness: 0.8,
|
|
492
745
|
defaultColor: 0xb0a090,
|
|
493
|
-
|
|
746
|
+
shape: 'plane',
|
|
747
|
+
}
|
|
748
|
+
registerRealObjectFactory('Port', (component: Component) => new MCSRealObject3D(component, PORT_CONFIG))
|
|
494
749
|
|
|
495
750
|
register('Shelf', {
|
|
496
751
|
depthRatio: 0.25,
|
|
@@ -499,14 +754,25 @@ register('Shelf', {
|
|
|
499
754
|
defaultColor: 0xaabbcc,
|
|
500
755
|
})
|
|
501
756
|
|
|
502
|
-
|
|
757
|
+
// Carrier: CARRIERSTATUS에 따른 3D 애니메이션 지원
|
|
758
|
+
const CARRIER_CONFIG: MCS3DConfig = {
|
|
759
|
+
...DEFAULTS,
|
|
503
760
|
depthRatio: 0.5,
|
|
504
|
-
metalness: 0
|
|
505
|
-
roughness:
|
|
506
|
-
defaultColor:
|
|
761
|
+
metalness: 0,
|
|
762
|
+
roughness: 1,
|
|
763
|
+
defaultColor: 0xf0f0f0,
|
|
507
764
|
isVisible: (c: Component) => !!(c.state as any).id,
|
|
508
765
|
sizeRatio: 0.8,
|
|
509
|
-
|
|
766
|
+
texturedTop: true,
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
class CarrierRealObject3D extends MCSRealObject3D {
|
|
770
|
+
constructor(component: Component) {
|
|
771
|
+
super(component, CARRIER_CONFIG)
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
registerRealObjectFactory('Carrier', (component: Component) => new CarrierRealObject3D(component))
|
|
510
776
|
|
|
511
777
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
512
778
|
// Gauge (용량 게이지) — 얇은 바
|
|
@@ -516,8 +782,221 @@ register('Carrier', {
|
|
|
516
782
|
// Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
|
|
517
783
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
518
784
|
|
|
519
|
-
// Gauge
|
|
520
|
-
//
|
|
521
|
-
registerRealObjectFactory('StockerCapacityBar', () =>
|
|
522
|
-
registerRealObjectFactory('MCSGaugeCapacityBar', () =>
|
|
523
|
-
registerRealObjectFactory('ZoneCapacityBar', () =>
|
|
785
|
+
// Gauge 컴포넌트는 3D에서 표시하지 않음 (부모 STOCKER의 3D 모델에서 직접 게이지 렌더링)
|
|
786
|
+
// null 반환으로 3D 오브젝트 생성을 억제하고, default factory fallback도 방지
|
|
787
|
+
registerRealObjectFactory('StockerCapacityBar', () => undefined as any)
|
|
788
|
+
registerRealObjectFactory('MCSGaugeCapacityBar', () => undefined as any)
|
|
789
|
+
registerRealObjectFactory('ZoneCapacityBar', () => undefined as any)
|
|
790
|
+
|
|
791
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
792
|
+
// PortFlow — 2D render를 Plane 텍스처로 3D 표시
|
|
793
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
794
|
+
|
|
795
|
+
class PortFlowRealObject3D extends RealObjectGroup {
|
|
796
|
+
private _mesh?: THREE.Mesh
|
|
797
|
+
private _texture?: THREE.CanvasTexture
|
|
798
|
+
|
|
799
|
+
get effectiveDepth(): number { return 0 }
|
|
800
|
+
protected get syncZPosOffset(): number {
|
|
801
|
+
// 부모 Port/Conveyor의 zPos와 동일하게
|
|
802
|
+
const parent = (this.component as any).parentObject
|
|
803
|
+
if (parent?.state?.zPos != null) return parent.state.zPos
|
|
804
|
+
return TRANSPORT_SURFACE_HEIGHT
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
build() {
|
|
808
|
+
super.build()
|
|
809
|
+
|
|
810
|
+
const { width = 10, height = 10 } = this.component.state
|
|
811
|
+
const w = Math.abs(width)
|
|
812
|
+
const h = Math.abs(height)
|
|
813
|
+
if (!w || !h) return
|
|
814
|
+
|
|
815
|
+
// 2D render를 캔버스에 캡처
|
|
816
|
+
// 화살표 날개 + 텍스트 영역이 bounds 밖으로 나가므로 전체 영역 계산
|
|
817
|
+
const { left, top } = this.component.bounds
|
|
818
|
+
const { fontSize = 24, labelPosition = 'top' } = this.component.state
|
|
819
|
+
const arrowPad = Math.min(w, h) * 0.4
|
|
820
|
+
|
|
821
|
+
// 텍스트 영역 확장
|
|
822
|
+
let padLeft = arrowPad, padRight = arrowPad, padTop = arrowPad, padBottom = arrowPad
|
|
823
|
+
if ((this.component as any).hasTextProperty) {
|
|
824
|
+
switch (labelPosition) {
|
|
825
|
+
case 'top': padTop = Math.max(padTop, fontSize + 4); break
|
|
826
|
+
case 'bottom': padBottom = Math.max(padBottom, fontSize + 4); break
|
|
827
|
+
case 'left': padLeft = Math.max(padLeft, fontSize * 4 + 4); break
|
|
828
|
+
case 'right': padRight = Math.max(padRight, fontSize * 4 + 4); break
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const totalW = w + padLeft + padRight
|
|
833
|
+
const totalH = h + padTop + padBottom
|
|
834
|
+
const scale = 2
|
|
835
|
+
const canvas = document.createElement('canvas')
|
|
836
|
+
canvas.width = totalW * scale
|
|
837
|
+
canvas.height = totalH * scale
|
|
838
|
+
const ctx = canvas.getContext('2d')!
|
|
839
|
+
|
|
840
|
+
ctx.save()
|
|
841
|
+
ctx.scale(scale, scale)
|
|
842
|
+
ctx.translate(-left + padLeft, -top + padTop)
|
|
843
|
+
try {
|
|
844
|
+
// 화살표 렌더
|
|
845
|
+
this.component.render(ctx)
|
|
846
|
+
ctx.fillStyle = this.component.state.strokeStyle || '#666666'
|
|
847
|
+
ctx.fill()
|
|
848
|
+
// 텍스트 렌더
|
|
849
|
+
if ((this.component as any).hasTextProperty && (this.component as any).drawText) {
|
|
850
|
+
;(this.component as any).drawText(ctx)
|
|
851
|
+
}
|
|
852
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
853
|
+
ctx.restore()
|
|
854
|
+
|
|
855
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
856
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
857
|
+
|
|
858
|
+
const geo = new THREE.PlaneGeometry(totalW, totalH)
|
|
859
|
+
geo.rotateX(-Math.PI / 2)
|
|
860
|
+
|
|
861
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
862
|
+
map: this._texture,
|
|
863
|
+
alphaTest: 0.1,
|
|
864
|
+
side: THREE.DoubleSide,
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
868
|
+
// 비대칭 패딩 보정: 플레인 센터를 컴포넌트 bounds 센터에 맞춤
|
|
869
|
+
this._mesh.position.x = (padRight - padLeft) / 2
|
|
870
|
+
this._mesh.position.y = 1
|
|
871
|
+
this._mesh.position.z = (padBottom - padTop) / 2
|
|
872
|
+
this.object3d.add(this._mesh)
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
update() {
|
|
876
|
+
if (this._mesh) {
|
|
877
|
+
this.object3d.remove(this._mesh)
|
|
878
|
+
this._mesh.geometry.dispose()
|
|
879
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
880
|
+
this._texture?.dispose()
|
|
881
|
+
this._mesh = undefined
|
|
882
|
+
this._texture = undefined
|
|
883
|
+
}
|
|
884
|
+
this.build()
|
|
885
|
+
this.updateTransform()
|
|
886
|
+
this.updateHidden()
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
890
|
+
if ('width' in after || 'height' in after || 'orientation' in after ||
|
|
891
|
+
'direction' in after || 'strokeStyle' in after || 'INOUTTYPE' in after) {
|
|
892
|
+
this.update()
|
|
893
|
+
return
|
|
894
|
+
}
|
|
895
|
+
super.onchange(after, before)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
updateDimension() {}
|
|
899
|
+
updateAlpha() {}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
registerRealObjectFactory('PortFlow', (component: Component) => new PortFlowRealObject3D(component))
|
|
903
|
+
|
|
904
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
905
|
+
// Node — 2D render를 바닥 Plane 텍스처로 3D 표시
|
|
906
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
907
|
+
|
|
908
|
+
class NodeRealObject3D extends RealObjectGroup {
|
|
909
|
+
private _mesh?: THREE.Mesh
|
|
910
|
+
private _texture?: THREE.CanvasTexture
|
|
911
|
+
|
|
912
|
+
get effectiveDepth(): number { return 0 }
|
|
913
|
+
protected get syncZPosOffset(): number { return 0 }
|
|
914
|
+
|
|
915
|
+
build() {
|
|
916
|
+
super.build()
|
|
917
|
+
|
|
918
|
+
const { cx, cy } = this.component.state
|
|
919
|
+
const radius = 8
|
|
920
|
+
const size = radius * 2
|
|
921
|
+
const scale = 2
|
|
922
|
+
const pad = 2
|
|
923
|
+
|
|
924
|
+
const canvas = document.createElement('canvas')
|
|
925
|
+
canvas.width = (size + pad * 2) * scale
|
|
926
|
+
canvas.height = (size + pad * 2) * scale
|
|
927
|
+
const ctx = canvas.getContext('2d')!
|
|
928
|
+
|
|
929
|
+
ctx.save()
|
|
930
|
+
ctx.scale(scale, scale)
|
|
931
|
+
ctx.translate(-cx + radius + pad, -cy + radius + pad)
|
|
932
|
+
try {
|
|
933
|
+
this.component.render(ctx)
|
|
934
|
+
const { fillStyle = '#888888' } = this.component.state
|
|
935
|
+
ctx.fillStyle = typeof fillStyle === 'string' ? fillStyle : '#888888'
|
|
936
|
+
ctx.fill()
|
|
937
|
+
const { strokeStyle } = this.component.state
|
|
938
|
+
if (strokeStyle && typeof strokeStyle === 'string') {
|
|
939
|
+
ctx.strokeStyle = strokeStyle
|
|
940
|
+
ctx.lineWidth = 1
|
|
941
|
+
ctx.stroke()
|
|
942
|
+
}
|
|
943
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
944
|
+
ctx.restore()
|
|
945
|
+
|
|
946
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
947
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
948
|
+
|
|
949
|
+
const geo = new THREE.PlaneGeometry(size + pad * 2, size + pad * 2)
|
|
950
|
+
geo.rotateX(-Math.PI / 2)
|
|
951
|
+
|
|
952
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
953
|
+
map: this._texture,
|
|
954
|
+
alphaTest: 0.1,
|
|
955
|
+
side: THREE.DoubleSide,
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
959
|
+
this._mesh.position.y = 1
|
|
960
|
+
this.object3d.add(this._mesh)
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
update() {
|
|
964
|
+
if (this._mesh) {
|
|
965
|
+
this.object3d.remove(this._mesh)
|
|
966
|
+
this._mesh.geometry.dispose()
|
|
967
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
968
|
+
this._texture?.dispose()
|
|
969
|
+
this._mesh = undefined
|
|
970
|
+
this._texture = undefined
|
|
971
|
+
}
|
|
972
|
+
this.build()
|
|
973
|
+
this.updateTransform()
|
|
974
|
+
this.updateHidden()
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
978
|
+
if ('fillStyle' in after || 'strokeStyle' in after) {
|
|
979
|
+
this.update()
|
|
980
|
+
return
|
|
981
|
+
}
|
|
982
|
+
super.onchange(after, before)
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
updateDimension() {}
|
|
986
|
+
updateAlpha() {}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
registerRealObjectFactory('Node', (component: Component) => new NodeRealObject3D(component))
|
|
990
|
+
|
|
991
|
+
// Scanner — 이송 표면 높이의 기둥 형태
|
|
992
|
+
const SCANNER_CONFIG: MCS3DConfig = {
|
|
993
|
+
...DEFAULTS,
|
|
994
|
+
defaultDepth: TRANSPORT_SURFACE_HEIGHT * 0.2,
|
|
995
|
+
defaultZPosFromParent: true,
|
|
996
|
+
opacity: 1,
|
|
997
|
+
metalness: 0.3,
|
|
998
|
+
roughness: 0.7,
|
|
999
|
+
defaultColor: 0x888888,
|
|
1000
|
+
shape: 'box',
|
|
1001
|
+
}
|
|
1002
|
+
registerRealObjectFactory('Scanner', (component: Component) => new MCSRealObject3D(component, SCANNER_CONFIG))
|