@fmsim/machine 2.0.0-beta.6 → 2.0.0-beta.60
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 +49 -4
- 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 +8 -9
- package/dist/factories/agv-3d.js.map +1 -1
- package/dist/factories/conveyor-3d.js +166 -81
- package/dist/factories/conveyor-3d.js.map +1 -1
- package/dist/factories/crane-3d.js +5 -7
- package/dist/factories/crane-3d.js.map +1 -1
- package/dist/factories/crane-rail-3d.js +4 -6
- package/dist/factories/crane-rail-3d.js.map +1 -1
- package/dist/factories/machine-3d.js +363 -132
- package/dist/factories/machine-3d.js.map +1 -1
- package/dist/factories/oht-3d.js +20 -20
- package/dist/factories/oht-3d.js.map +1 -1
- package/dist/factories/shuttle-3d.js +2 -5
- 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 +48 -2
- 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 +9 -10
- package/src/factories/conveyor-3d.ts +168 -84
- package/src/factories/crane-3d.ts +6 -8
- package/src/factories/crane-rail-3d.ts +5 -7
- package/src/factories/machine-3d.ts +428 -144
- package/src/factories/oht-3d.ts +20 -20
- package/src/factories/shuttle-3d.ts +2 -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 * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
12
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
|
|
|
@@ -29,8 +30,8 @@ interface MCS3DConfig {
|
|
|
29
30
|
roughness: number
|
|
30
31
|
/** statusColor 미사용 시 기본 색상 */
|
|
31
32
|
defaultColor: number
|
|
32
|
-
/** 'box' | 'hexagon' */
|
|
33
|
-
shape: 'box' | 'hexagon'
|
|
33
|
+
/** 'box' | 'hexagon' | 'plane' | 'triangle' */
|
|
34
|
+
shape: 'box' | 'hexagon' | 'plane' | 'triangle'
|
|
34
35
|
/** mesh 생성 전 가시성 판정 — false 반환 시 mesh 미생성 */
|
|
35
36
|
isVisible?: (component: Component) => boolean
|
|
36
37
|
/** 부모 대비 크기 비율 (기본 1.0). < 1이면 부모 중심에 배치. */
|
|
@@ -41,6 +42,12 @@ interface MCS3DConfig {
|
|
|
41
42
|
matteTop?: boolean
|
|
42
43
|
/** 상판에 2D 렌더링 텍스처 적용 */
|
|
43
44
|
texturedTop?: boolean
|
|
45
|
+
/** 자체 발광 강도 (0~1). 조명 약한 면에서도 색상 유지 */
|
|
46
|
+
emissiveIntensity?: number
|
|
47
|
+
/** 바닥면 유지 (기본 false — 바닥면 제거) */
|
|
48
|
+
keepBottomFace?: boolean
|
|
49
|
+
/** syncZPosOffset 고정값 (설정 시 다른 로직 무시) */
|
|
50
|
+
zPosOverride?: number
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
/**
|
|
@@ -69,9 +76,14 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
69
76
|
this._config = config
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
// width, height 절대값 (음수 값은 양수로 취급)
|
|
79
|
+
// width, height 절대값 (음수 값은 양수로 취급).
|
|
80
|
+
// `resizable:false` 컴포넌트(예: Scanner)는 state.width/height 가 없고
|
|
81
|
+
// bounds 만 있을 수 있으므로 bounds 를 fallback 으로 사용.
|
|
73
82
|
protected get absSize(): { w: number; h: number } {
|
|
74
|
-
const
|
|
83
|
+
const state = (this.component.state || {}) as any
|
|
84
|
+
const bounds = (this.component.bounds || {}) as any
|
|
85
|
+
const width = state.width ?? bounds.width ?? 10
|
|
86
|
+
const height = state.height ?? bounds.height ?? 10
|
|
75
87
|
return { w: Math.abs(width), h: Math.abs(height) }
|
|
76
88
|
}
|
|
77
89
|
|
|
@@ -84,21 +96,35 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
protected get syncZPosOffset(): number {
|
|
99
|
+
// syncZPosOffset = placement + geometric (center origin = effectiveDepth/2)
|
|
100
|
+
// 중첩 보정(parent shift 상쇄)은 base RealObject.cz가 parent.geometricOffsetY를
|
|
101
|
+
// 빼는 방식으로 처리하므로 여기선 placement + 본인 geometric만 반환
|
|
102
|
+
const halfD = this.effectiveDepth / 2
|
|
103
|
+
// zPosOverride: 고정 높이 (OHTLine 등)
|
|
104
|
+
if (this._config.zPosOverride != null) {
|
|
105
|
+
return this._config.zPosOverride + halfD
|
|
106
|
+
}
|
|
87
107
|
// Port: 이송 표면 높이에 배치
|
|
88
108
|
if (this._config.defaultZPosFromParent) {
|
|
89
|
-
return TRANSPORT_SURFACE_HEIGHT
|
|
109
|
+
return TRANSPORT_SURFACE_HEIGHT + halfD
|
|
90
110
|
}
|
|
91
|
-
// Carrier:
|
|
111
|
+
// Carrier: 부모 유형에 따라 배치 위치 결정
|
|
92
112
|
if (this._config.sizeRatio && this._config.sizeRatio < 1) {
|
|
93
113
|
const parentType = this.component.parent?.state?.type
|
|
94
|
-
|
|
95
|
-
|
|
114
|
+
// OHT: 그리퍼 아래에 매달림 — placement = -effectiveDepth(carrier top이 OHT bottom)
|
|
115
|
+
if (parentType === 'OHT') {
|
|
116
|
+
return -this.effectiveDepth + halfD
|
|
117
|
+
}
|
|
118
|
+
if (parentType === 'Port' || parentType === 'Shelf' || parentType === 'Shuttle') {
|
|
119
|
+
return halfD
|
|
96
120
|
}
|
|
97
|
-
return TRANSPORT_SURFACE_HEIGHT
|
|
121
|
+
return TRANSPORT_SURFACE_HEIGHT + halfD
|
|
98
122
|
}
|
|
99
|
-
return
|
|
123
|
+
return halfD
|
|
100
124
|
}
|
|
101
125
|
|
|
126
|
+
// geometric 부분(center origin: depth/2)은 base 기본값 사용 — override 불필요
|
|
127
|
+
|
|
102
128
|
// 컴포넌트의 상태 색상(statusColor) 또는 fillStyle → Three.js 색상 변환
|
|
103
129
|
protected resolveColor(): number {
|
|
104
130
|
const comp = this.component as any
|
|
@@ -107,14 +133,14 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
107
133
|
// '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
|
|
108
134
|
try {
|
|
109
135
|
const sc = comp.statusColor
|
|
110
|
-
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
|
|
136
|
+
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()
|
|
111
137
|
} catch {
|
|
112
138
|
/* statusColor 미구현 시 무시 */
|
|
113
139
|
}
|
|
114
140
|
|
|
115
141
|
// 2) state.fillStyle fallback
|
|
116
142
|
const { fillStyle } = this.component.state
|
|
117
|
-
if (fillStyle && typeof fillStyle === 'string') {
|
|
143
|
+
if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
|
|
118
144
|
try {
|
|
119
145
|
return new THREE.Color(fillStyle).getHex()
|
|
120
146
|
} catch {
|
|
@@ -163,7 +189,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
163
189
|
|
|
164
190
|
private buildBoxGeometry(w: number, d: number, h: number): THREE.BufferGeometry {
|
|
165
191
|
const geo = new THREE.BoxGeometry(w, d, h)
|
|
166
|
-
return this.removeBottomFace(geo)
|
|
192
|
+
return this._config.keepBottomFace ? geo : this.removeBottomFace(geo)
|
|
167
193
|
}
|
|
168
194
|
|
|
169
195
|
private buildHexagonGeometry(w: number, d: number, h: number): THREE.BufferGeometry {
|
|
@@ -194,6 +220,43 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
194
220
|
return this.removeBottomFace(geo)
|
|
195
221
|
}
|
|
196
222
|
|
|
223
|
+
private buildPlaneGeometry(w: number, h: number): THREE.BufferGeometry {
|
|
224
|
+
const geo = new THREE.PlaneGeometry(w, h)
|
|
225
|
+
geo.rotateX(-Math.PI / 2)
|
|
226
|
+
return geo
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 삼각형 prism — 2D Triangle 의 꼭짓점(state.x1/y1, x2/y2, x3/y3) 을 그대로 사용.
|
|
231
|
+
* things-scene 의 TriangleExtrude 와 동일한 방식 — 각 Scanner 인스턴스의
|
|
232
|
+
* 실제 정점 배치(방향, 비대칭 등) 가 3D 에 반영된다.
|
|
233
|
+
*
|
|
234
|
+
* `geometry.center()` 로 로컬 원점을 bounding-box 중심으로 맞추므로
|
|
235
|
+
* 후속 placement 와 정합.
|
|
236
|
+
*/
|
|
237
|
+
private buildTriangleGeometry(d: number): THREE.BufferGeometry {
|
|
238
|
+
const state = (this.component.state || {}) as any
|
|
239
|
+
const { x1 = 0, y1 = 0, x2 = 0, y2 = 0, x3 = 0, y3 = 0 } = state
|
|
240
|
+
|
|
241
|
+
const shape = new THREE.Shape()
|
|
242
|
+
shape.moveTo(x1, y1)
|
|
243
|
+
shape.lineTo(x2, y2)
|
|
244
|
+
shape.lineTo(x3, y3)
|
|
245
|
+
shape.closePath()
|
|
246
|
+
|
|
247
|
+
const geo = new THREE.ExtrudeGeometry(shape, {
|
|
248
|
+
steps: 1,
|
|
249
|
+
depth: d,
|
|
250
|
+
bevelEnabled: false,
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// bounding box 중심으로 원점 정렬 후 Y-up 좌표계로 회전
|
|
254
|
+
geo.center()
|
|
255
|
+
geo.rotateX(Math.PI / 2)
|
|
256
|
+
|
|
257
|
+
return this.removeBottomFace(geo)
|
|
258
|
+
}
|
|
259
|
+
|
|
197
260
|
// ── Build ──
|
|
198
261
|
|
|
199
262
|
protected _ownMesh?: THREE.Mesh
|
|
@@ -217,19 +280,31 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
217
280
|
const { opacity, metalness, roughness, shape } = this._config
|
|
218
281
|
|
|
219
282
|
const geometry =
|
|
220
|
-
shape === '
|
|
221
|
-
? this.
|
|
222
|
-
:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
283
|
+
shape === 'plane'
|
|
284
|
+
? this.buildPlaneGeometry(w, h)
|
|
285
|
+
: shape === 'hexagon'
|
|
286
|
+
? this.buildHexagonGeometry(w, depth, h)
|
|
287
|
+
: shape === 'triangle'
|
|
288
|
+
? this.buildTriangleGeometry(depth)
|
|
289
|
+
: this.buildBoxGeometry(w, depth, h)
|
|
290
|
+
|
|
291
|
+
const isPlane = shape === 'plane'
|
|
292
|
+
const material = isPlane
|
|
293
|
+
? new THREE.MeshBasicMaterial({
|
|
294
|
+
color,
|
|
295
|
+
opacity,
|
|
296
|
+
transparent: opacity < 1,
|
|
297
|
+
side: THREE.DoubleSide,
|
|
298
|
+
})
|
|
299
|
+
: new THREE.MeshStandardMaterial({
|
|
300
|
+
color,
|
|
301
|
+
metalness,
|
|
302
|
+
roughness,
|
|
303
|
+
opacity,
|
|
304
|
+
transparent: opacity < 1,
|
|
305
|
+
side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide,
|
|
306
|
+
...(this._config.emissiveIntensity ? { emissive: color, emissiveIntensity: this._config.emissiveIntensity } : {}),
|
|
307
|
+
})
|
|
233
308
|
// z-fighting 방지: 바닥면(floor grid)보다 항상 앞에 렌더링
|
|
234
309
|
material.polygonOffset = true
|
|
235
310
|
material.polygonOffsetFactor = -1
|
|
@@ -254,11 +329,14 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
254
329
|
}
|
|
255
330
|
|
|
256
331
|
this._ownMesh = new THREE.Mesh(geometry, meshMaterial)
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
332
|
+
// ownGroup 기준(bottom-up): mesh 바닥을 y=0에 맞추고 ownGroup이 -d/2로 shift해
|
|
333
|
+
// center-origin object3d에 정렬됨.
|
|
334
|
+
// box/hexagon (center-local geometry): y=rawDepth/2 (바닥을 0에 맞춤)
|
|
335
|
+
// plane: y=1 (바닥 살짝 띄움)
|
|
336
|
+
this._ownMesh.position.y = isPlane ? 1 : rawDepth / 2
|
|
337
|
+
this._ownMesh.castShadow = !isPlane
|
|
260
338
|
this._ownMesh.receiveShadow = true
|
|
261
|
-
this.
|
|
339
|
+
this.components3D.add(this._ownMesh)
|
|
262
340
|
}
|
|
263
341
|
|
|
264
342
|
// ── Change handling ──
|
|
@@ -353,7 +431,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
353
431
|
update() {
|
|
354
432
|
// 자신의 mesh만 제거 (자식 컴포넌트 3D 오브젝트 보존)
|
|
355
433
|
if (this._ownMesh) {
|
|
356
|
-
this.
|
|
434
|
+
this.components3D.remove(this._ownMesh)
|
|
357
435
|
disposeObject3D(this._ownMesh)
|
|
358
436
|
this._ownMesh = undefined
|
|
359
437
|
}
|
|
@@ -368,9 +446,9 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
368
446
|
updateDimension() {}
|
|
369
447
|
|
|
370
448
|
updateAlpha() {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
const effective =
|
|
449
|
+
const state = this.component.state
|
|
450
|
+
// alpha가 명시적으로 설정되어 있으면 그 값을 사용, 없으면 config 기본값 적용
|
|
451
|
+
const effective = ('alpha' in state) ? (state.alpha as number) : this._config.opacity
|
|
374
452
|
|
|
375
453
|
this.object3d.traverse((o: any) => {
|
|
376
454
|
if (!o.material) return
|
|
@@ -414,18 +492,106 @@ register('Buffer', {
|
|
|
414
492
|
})
|
|
415
493
|
|
|
416
494
|
register('AGVLine', {
|
|
417
|
-
depthRatio: 0
|
|
495
|
+
depthRatio: 0,
|
|
496
|
+
defaultDepth: 0.1,
|
|
418
497
|
metalness: 0.6,
|
|
419
498
|
roughness: 0.3,
|
|
420
499
|
defaultColor: 0x888899,
|
|
421
500
|
})
|
|
422
501
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
502
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
503
|
+
// OHTLine — 천장 I-beam 레일
|
|
504
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
505
|
+
|
|
506
|
+
const OHT_LINE_RAIL_COLOR = 0xcccccc
|
|
507
|
+
const OHT_LINE_HEIGHT = 7 * TRANSPORT_SURFACE_HEIGHT // OHT 상단과 일치
|
|
508
|
+
|
|
509
|
+
class OHTLineRealObject3D extends RealObjectGroup {
|
|
510
|
+
private _mesh?: THREE.Mesh
|
|
511
|
+
|
|
512
|
+
get effectiveDepth(): number { return 0 }
|
|
513
|
+
protected get syncZPosOffset(): number { return OHT_LINE_HEIGHT }
|
|
514
|
+
|
|
515
|
+
build() {
|
|
516
|
+
super.build()
|
|
517
|
+
|
|
518
|
+
const { width = 10, height = 10 } = this.component.state
|
|
519
|
+
const w = Math.abs(width)
|
|
520
|
+
const h = Math.abs(height)
|
|
521
|
+
if (!w || !h) return
|
|
522
|
+
|
|
523
|
+
// I-beam 치수: 이송 방향 = 긴 변, 폭은 2D의 1/2
|
|
524
|
+
const isVertical = w < h
|
|
525
|
+
const shortSide = isVertical ? w : h // 2D 원본 짧은 쪽 폭
|
|
526
|
+
const beltW = shortSide * 0.25 // I-beam 폭: 원본의 1/4
|
|
527
|
+
const beamH = Math.max(beltW * 0.5, 3) // I-beam 전체 높이
|
|
528
|
+
const inset = shortSide * 0.5 // 양끝 여백: 원본 폭의 1/2
|
|
529
|
+
const railLen = (isVertical ? h : w) - inset * 2
|
|
530
|
+
const flangeH = beamH * 0.2 // 플랜지 두께
|
|
531
|
+
const flangeW = beltW * 0.8 // 플랜지 폭
|
|
532
|
+
const webH = beamH - flangeH * 2 // 웹 높이
|
|
533
|
+
const webW = Math.max(flangeW * 0.25, 1) // 웹 두께
|
|
534
|
+
|
|
535
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
536
|
+
color: OHT_LINE_RAIL_COLOR,
|
|
537
|
+
metalness: 0.5,
|
|
538
|
+
roughness: 0.4,
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
const geoms: THREE.BufferGeometry[] = []
|
|
542
|
+
|
|
543
|
+
// 상부 플랜지
|
|
544
|
+
const topFlange = isVertical
|
|
545
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
546
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
547
|
+
topFlange.translate(0, -flangeH / 2, 0)
|
|
548
|
+
geoms.push(topFlange)
|
|
549
|
+
|
|
550
|
+
// 웹 (수직판)
|
|
551
|
+
const web = isVertical
|
|
552
|
+
? new THREE.BoxGeometry(webW, webH, railLen)
|
|
553
|
+
: new THREE.BoxGeometry(railLen, webH, webW)
|
|
554
|
+
web.translate(0, -flangeH - webH / 2, 0)
|
|
555
|
+
geoms.push(web)
|
|
556
|
+
|
|
557
|
+
// 하부 플랜지
|
|
558
|
+
const botFlange = isVertical
|
|
559
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
560
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
561
|
+
botFlange.translate(0, -flangeH - webH - flangeH / 2, 0)
|
|
562
|
+
geoms.push(botFlange)
|
|
563
|
+
|
|
564
|
+
this._mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(geoms), mat)
|
|
565
|
+
this._mesh.castShadow = true
|
|
566
|
+
this.object3d.add(this._mesh)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
update() {
|
|
570
|
+
if (this._mesh) {
|
|
571
|
+
this.object3d.remove(this._mesh)
|
|
572
|
+
this._mesh.geometry.dispose()
|
|
573
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
574
|
+
this._mesh = undefined
|
|
575
|
+
}
|
|
576
|
+
this.build()
|
|
577
|
+
this.updateTransform()
|
|
578
|
+
this.updateHidden()
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
582
|
+
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
583
|
+
'fillStyle' in after || 'strokeStyle' in after) {
|
|
584
|
+
this.update()
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
super.onchange(after, before)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
updateDimension() {}
|
|
591
|
+
updateAlpha() {}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
registerRealObjectFactory('OHTLine', (component: Component) => new OHTLineRealObject3D(component))
|
|
429
595
|
|
|
430
596
|
// STOCKER: 벽면에 용량 게이지 바를 포함하는 커스텀 3D 오브젝트
|
|
431
597
|
const STOCKER_CONFIG: MCS3DConfig = {
|
|
@@ -523,8 +689,9 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
523
689
|
mat.polygonOffsetUnit = -2
|
|
524
690
|
|
|
525
691
|
this.gaugeMesh = new THREE.Mesh(geo, mat)
|
|
692
|
+
// ownGroup 기준(bottom-up): 볼륨 상단 = +depth. 그 위로 0.5 띄움.
|
|
526
693
|
this.gaugeMesh.position.set(0, depth + 0.5, 0)
|
|
527
|
-
this.
|
|
694
|
+
this.components3D.add(this.gaugeMesh)
|
|
528
695
|
}
|
|
529
696
|
|
|
530
697
|
private resolveGauge(data: any, mode: string) {
|
|
@@ -569,7 +736,7 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
569
736
|
const fullup = data.FULLUP === 'T'
|
|
570
737
|
const usage = Math.round((cur / max) * 100)
|
|
571
738
|
const legend = (this.component as any).root?.style?.LEGEND_CAPACITY || LEGEND_CAPACITY
|
|
572
|
-
const color = fullup ? 'red' : (
|
|
739
|
+
const color = fullup ? 'red' : (getValueOnRanges(usage, legend) || '#44aa44')
|
|
573
740
|
return {
|
|
574
741
|
segments: [{ ratio: usage / 100, color }],
|
|
575
742
|
borderColor: color,
|
|
@@ -579,6 +746,11 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
579
746
|
}
|
|
580
747
|
}
|
|
581
748
|
|
|
749
|
+
update() {
|
|
750
|
+
this.clearGauge()
|
|
751
|
+
super.update()
|
|
752
|
+
}
|
|
753
|
+
|
|
582
754
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
583
755
|
if ('data' in after) {
|
|
584
756
|
this.clearGauge()
|
|
@@ -594,7 +766,7 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
594
766
|
|
|
595
767
|
private clearGauge() {
|
|
596
768
|
if (this.gaugeMesh) {
|
|
597
|
-
this.
|
|
769
|
+
this.components3D.remove(this.gaugeMesh)
|
|
598
770
|
this.gaugeMesh.geometry?.dispose()
|
|
599
771
|
const mat = this.gaugeMesh.material as THREE.MeshBasicMaterial
|
|
600
772
|
mat?.map?.dispose()
|
|
@@ -610,16 +782,16 @@ registerRealObjectFactory('STOCKER', (component: Component) => new StockerRealOb
|
|
|
610
782
|
// Unit (이동체 · 홀더) — 낮은 depth, 불투명
|
|
611
783
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
612
784
|
|
|
613
|
-
// Port:
|
|
785
|
+
// Port: 바닥판 (PlaneGeometry)
|
|
614
786
|
const PORT_CONFIG: MCS3DConfig = {
|
|
615
787
|
...DEFAULTS,
|
|
616
|
-
depthRatio: 0.
|
|
788
|
+
depthRatio: 0.01,
|
|
617
789
|
defaultZPosFromParent: true,
|
|
618
|
-
opacity: 0.
|
|
619
|
-
metalness: 0.
|
|
620
|
-
roughness: 0.
|
|
790
|
+
opacity: 0.1,
|
|
791
|
+
metalness: 0.3,
|
|
792
|
+
roughness: 0.8,
|
|
621
793
|
defaultColor: 0xb0a090,
|
|
622
|
-
|
|
794
|
+
shape: 'plane',
|
|
623
795
|
}
|
|
624
796
|
registerRealObjectFactory('Port', (component: Component) => new MCSRealObject3D(component, PORT_CONFIG))
|
|
625
797
|
|
|
@@ -634,134 +806,246 @@ register('Shelf', {
|
|
|
634
806
|
const CARRIER_CONFIG: MCS3DConfig = {
|
|
635
807
|
...DEFAULTS,
|
|
636
808
|
depthRatio: 0.5,
|
|
637
|
-
metalness: 0
|
|
638
|
-
roughness:
|
|
639
|
-
defaultColor:
|
|
809
|
+
metalness: 0,
|
|
810
|
+
roughness: 1,
|
|
811
|
+
defaultColor: 0xf0f0f0,
|
|
640
812
|
isVisible: (c: Component) => !!(c.state as any).id,
|
|
641
813
|
sizeRatio: 0.8,
|
|
642
814
|
texturedTop: true,
|
|
643
815
|
}
|
|
644
816
|
|
|
645
817
|
class CarrierRealObject3D extends MCSRealObject3D {
|
|
646
|
-
private _animRaf: number | null = null
|
|
647
|
-
private _animStartTime = 0
|
|
648
|
-
|
|
649
818
|
constructor(component: Component) {
|
|
650
819
|
super(component, CARRIER_CONFIG)
|
|
651
820
|
}
|
|
821
|
+
}
|
|
652
822
|
|
|
653
|
-
|
|
654
|
-
super.build()
|
|
655
|
-
this.startCarrierAnimation()
|
|
656
|
-
}
|
|
823
|
+
registerRealObjectFactory('Carrier', (component: Component) => new CarrierRealObject3D(component))
|
|
657
824
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
*/
|
|
662
|
-
private startCarrierAnimation() {
|
|
663
|
-
this.stopCarrierAnimation()
|
|
825
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
826
|
+
// Gauge (용량 게이지) — 얇은 바
|
|
827
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
664
828
|
|
|
665
|
-
|
|
666
|
-
|
|
829
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
830
|
+
// Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
|
|
831
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
667
832
|
|
|
668
|
-
|
|
669
|
-
|
|
833
|
+
// Gauge 컴포넌트는 3D에서 표시하지 않음 (부모 STOCKER의 3D 모델에서 직접 게이지 렌더링)
|
|
834
|
+
// null 반환으로 3D 오브젝트 생성을 억제하고, default factory fallback도 방지
|
|
835
|
+
registerRealObjectFactory('StockerCapacityBar', () => undefined as any)
|
|
836
|
+
registerRealObjectFactory('MCSGaugeCapacityBar', () => undefined as any)
|
|
837
|
+
registerRealObjectFactory('ZoneCapacityBar', () => undefined as any)
|
|
670
838
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
839
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
840
|
+
// PortFlow — 2D render를 Plane 텍스처로 3D 표시
|
|
841
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
674
842
|
|
|
675
|
-
|
|
676
|
-
|
|
843
|
+
class PortFlowRealObject3D extends RealObjectGroup {
|
|
844
|
+
private _mesh?: THREE.Mesh
|
|
845
|
+
private _texture?: THREE.CanvasTexture
|
|
677
846
|
|
|
678
|
-
|
|
847
|
+
get effectiveDepth(): number { return 0 }
|
|
848
|
+
protected get syncZPosOffset(): number {
|
|
849
|
+
// 부모 Port/Conveyor의 zPos와 동일하게
|
|
850
|
+
const parent = (this.component as any).parentObject
|
|
851
|
+
if (parent?.state?.zPos != null) return parent.state.zPos
|
|
852
|
+
return TRANSPORT_SURFACE_HEIGHT
|
|
853
|
+
}
|
|
679
854
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const obj = this.object3d
|
|
855
|
+
build() {
|
|
856
|
+
super.build()
|
|
683
857
|
|
|
684
|
-
const
|
|
685
|
-
|
|
858
|
+
const { width = 10, height = 10 } = this.component.state
|
|
859
|
+
const w = Math.abs(width)
|
|
860
|
+
const h = Math.abs(height)
|
|
861
|
+
if (!w || !h) return
|
|
862
|
+
|
|
863
|
+
// 2D render를 캔버스에 캡처
|
|
864
|
+
// 화살표 날개 + 텍스트 영역이 bounds 밖으로 나가므로 전체 영역 계산
|
|
865
|
+
const { left, top } = this.component.bounds
|
|
866
|
+
const { fontSize = 24, labelPosition = 'top' } = this.component.state
|
|
867
|
+
const arrowPad = Math.min(w, h) * 0.4
|
|
868
|
+
|
|
869
|
+
// 텍스트 영역 확장
|
|
870
|
+
let padLeft = arrowPad, padRight = arrowPad, padTop = arrowPad, padBottom = arrowPad
|
|
871
|
+
if ((this.component as any).hasTextProperty) {
|
|
872
|
+
switch (labelPosition) {
|
|
873
|
+
case 'top': padTop = Math.max(padTop, fontSize + 4); break
|
|
874
|
+
case 'bottom': padBottom = Math.max(padBottom, fontSize + 4); break
|
|
875
|
+
case 'left': padLeft = Math.max(padLeft, fontSize * 4 + 4); break
|
|
876
|
+
case 'right': padRight = Math.max(padRight, fontSize * 4 + 4); break
|
|
877
|
+
}
|
|
878
|
+
}
|
|
686
879
|
|
|
687
|
-
|
|
688
|
-
|
|
880
|
+
const totalW = w + padLeft + padRight
|
|
881
|
+
const totalH = h + padTop + padBottom
|
|
882
|
+
const scale = 2
|
|
883
|
+
const canvas = document.createElement('canvas')
|
|
884
|
+
canvas.width = totalW * scale
|
|
885
|
+
canvas.height = totalH * scale
|
|
886
|
+
const ctx = canvas.getContext('2d')!
|
|
689
887
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
case 'vibration': {
|
|
702
|
-
const theta = config.animation.theta ?? 0.26
|
|
703
|
-
const current = Math.sin(progress * Math.PI * 2) * theta
|
|
704
|
-
obj.rotation.y = current
|
|
705
|
-
break
|
|
706
|
-
}
|
|
707
|
-
case 'fade': {
|
|
708
|
-
const alpha = 0.3 + 0.7 * (progress < 0.5 ? progress * 2 : (1 - progress) * 2)
|
|
709
|
-
obj.traverse((o: any) => {
|
|
710
|
-
if (o.material && !o.material.map) {
|
|
711
|
-
o.material.opacity = alpha
|
|
712
|
-
o.material.transparent = true
|
|
713
|
-
}
|
|
714
|
-
})
|
|
715
|
-
break
|
|
716
|
-
}
|
|
888
|
+
ctx.save()
|
|
889
|
+
ctx.scale(scale, scale)
|
|
890
|
+
ctx.translate(-left + padLeft, -top + padTop)
|
|
891
|
+
try {
|
|
892
|
+
// 화살표 렌더
|
|
893
|
+
this.component.render(ctx)
|
|
894
|
+
ctx.fillStyle = this.component.state.strokeStyle || '#666666'
|
|
895
|
+
ctx.fill()
|
|
896
|
+
// 텍스트 렌더
|
|
897
|
+
if ((this.component as any).hasTextProperty && (this.component as any).drawText) {
|
|
898
|
+
;(this.component as any).drawText(ctx)
|
|
717
899
|
}
|
|
900
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
901
|
+
ctx.restore()
|
|
718
902
|
|
|
719
|
-
|
|
720
|
-
|
|
903
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
904
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
721
905
|
|
|
722
|
-
|
|
723
|
-
|
|
906
|
+
const geo = new THREE.PlaneGeometry(totalW, totalH)
|
|
907
|
+
geo.rotateX(-Math.PI / 2)
|
|
724
908
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
}
|
|
730
|
-
}
|
|
909
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
910
|
+
map: this._texture,
|
|
911
|
+
alphaTest: 0.1,
|
|
912
|
+
side: THREE.DoubleSide,
|
|
913
|
+
})
|
|
731
914
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
this.object3d.scale.set(1, 1, 1)
|
|
739
|
-
this.object3d.rotation.y = 0
|
|
915
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
916
|
+
// 비대칭 패딩 보정: 플레인 센터를 컴포넌트 bounds 센터에 맞춤
|
|
917
|
+
this._mesh.position.x = (padRight - padLeft) / 2
|
|
918
|
+
this._mesh.position.y = 1
|
|
919
|
+
this._mesh.position.z = (padBottom - padTop) / 2
|
|
920
|
+
this.object3d.add(this._mesh)
|
|
740
921
|
}
|
|
741
922
|
|
|
742
923
|
update() {
|
|
743
|
-
this.
|
|
744
|
-
|
|
924
|
+
if (this._mesh) {
|
|
925
|
+
this.object3d.remove(this._mesh)
|
|
926
|
+
this._mesh.geometry.dispose()
|
|
927
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
928
|
+
this._texture?.dispose()
|
|
929
|
+
this._mesh = undefined
|
|
930
|
+
this._texture = undefined
|
|
931
|
+
}
|
|
932
|
+
this.build()
|
|
933
|
+
this.updateTransform()
|
|
934
|
+
this.updateHidden()
|
|
745
935
|
}
|
|
746
936
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
937
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
938
|
+
if ('width' in after || 'height' in after || 'orientation' in after ||
|
|
939
|
+
'direction' in after || 'strokeStyle' in after || 'INOUTTYPE' in after) {
|
|
940
|
+
this.update()
|
|
941
|
+
return
|
|
942
|
+
}
|
|
943
|
+
super.onchange(after, before)
|
|
750
944
|
}
|
|
945
|
+
|
|
946
|
+
updateDimension() {}
|
|
947
|
+
updateAlpha() {}
|
|
751
948
|
}
|
|
752
949
|
|
|
753
|
-
registerRealObjectFactory('
|
|
950
|
+
registerRealObjectFactory('PortFlow', (component: Component) => new PortFlowRealObject3D(component))
|
|
754
951
|
|
|
755
952
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
756
|
-
//
|
|
953
|
+
// Node — 2D render를 바닥 Plane 텍스처로 3D 표시
|
|
757
954
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
758
955
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
956
|
+
class NodeRealObject3D extends RealObjectGroup {
|
|
957
|
+
private _mesh?: THREE.Mesh
|
|
958
|
+
private _texture?: THREE.CanvasTexture
|
|
959
|
+
|
|
960
|
+
get effectiveDepth(): number { return 0 }
|
|
961
|
+
protected get syncZPosOffset(): number { return 0 }
|
|
962
|
+
|
|
963
|
+
build() {
|
|
964
|
+
super.build()
|
|
965
|
+
|
|
966
|
+
const { cx, cy } = this.component.state
|
|
967
|
+
const radius = 8
|
|
968
|
+
const size = radius * 2
|
|
969
|
+
const scale = 2
|
|
970
|
+
const pad = 2
|
|
971
|
+
|
|
972
|
+
const canvas = document.createElement('canvas')
|
|
973
|
+
canvas.width = (size + pad * 2) * scale
|
|
974
|
+
canvas.height = (size + pad * 2) * scale
|
|
975
|
+
const ctx = canvas.getContext('2d')!
|
|
976
|
+
|
|
977
|
+
ctx.save()
|
|
978
|
+
ctx.scale(scale, scale)
|
|
979
|
+
ctx.translate(-cx + radius + pad, -cy + radius + pad)
|
|
980
|
+
try {
|
|
981
|
+
this.component.render(ctx)
|
|
982
|
+
const { fillStyle = '#888888' } = this.component.state
|
|
983
|
+
ctx.fillStyle = typeof fillStyle === 'string' ? fillStyle : '#888888'
|
|
984
|
+
ctx.fill()
|
|
985
|
+
const { strokeStyle } = this.component.state
|
|
986
|
+
if (strokeStyle && typeof strokeStyle === 'string') {
|
|
987
|
+
ctx.strokeStyle = strokeStyle
|
|
988
|
+
ctx.lineWidth = 1
|
|
989
|
+
ctx.stroke()
|
|
990
|
+
}
|
|
991
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
992
|
+
ctx.restore()
|
|
993
|
+
|
|
994
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
995
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
996
|
+
|
|
997
|
+
const geo = new THREE.PlaneGeometry(size + pad * 2, size + pad * 2)
|
|
998
|
+
geo.rotateX(-Math.PI / 2)
|
|
999
|
+
|
|
1000
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
1001
|
+
map: this._texture,
|
|
1002
|
+
alphaTest: 0.1,
|
|
1003
|
+
side: THREE.DoubleSide,
|
|
1004
|
+
})
|
|
1005
|
+
|
|
1006
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
1007
|
+
this._mesh.position.y = 1
|
|
1008
|
+
this.object3d.add(this._mesh)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
update() {
|
|
1012
|
+
if (this._mesh) {
|
|
1013
|
+
this.object3d.remove(this._mesh)
|
|
1014
|
+
this._mesh.geometry.dispose()
|
|
1015
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
1016
|
+
this._texture?.dispose()
|
|
1017
|
+
this._mesh = undefined
|
|
1018
|
+
this._texture = undefined
|
|
1019
|
+
}
|
|
1020
|
+
this.build()
|
|
1021
|
+
this.updateTransform()
|
|
1022
|
+
this.updateHidden()
|
|
1023
|
+
}
|
|
762
1024
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1025
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
1026
|
+
if ('fillStyle' in after || 'strokeStyle' in after) {
|
|
1027
|
+
this.update()
|
|
1028
|
+
return
|
|
1029
|
+
}
|
|
1030
|
+
super.onchange(after, before)
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
updateDimension() {}
|
|
1034
|
+
updateAlpha() {}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
registerRealObjectFactory('Node', (component: Component) => new NodeRealObject3D(component))
|
|
1038
|
+
|
|
1039
|
+
// Scanner — 2D Triangle 실루엣과 정합되는 삼각형 prism.
|
|
1040
|
+
// 이송 표면 높이의 0.2배(=6) 얇은 기둥 — 원래 값 유지, shape 만 'box' → 'triangle'.
|
|
1041
|
+
const SCANNER_CONFIG: MCS3DConfig = {
|
|
1042
|
+
...DEFAULTS,
|
|
1043
|
+
defaultDepth: TRANSPORT_SURFACE_HEIGHT * 0.2,
|
|
1044
|
+
defaultZPosFromParent: true,
|
|
1045
|
+
opacity: 1,
|
|
1046
|
+
metalness: 0.3,
|
|
1047
|
+
roughness: 0.7,
|
|
1048
|
+
defaultColor: 0x888888,
|
|
1049
|
+
shape: 'triangle',
|
|
1050
|
+
}
|
|
1051
|
+
registerRealObjectFactory('Scanner', (component: Component) => new MCSRealObject3D(component, SCANNER_CONFIG))
|